libpurple/trie.c

Thu, 25 Aug 2022 23:25:12 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Thu, 25 Aug 2022 23:25:12 -0500
branch
gtk4
changeset 41598
2b34cd990c16
parent 41479
3d2e114380f6
permissions
-rw-r--r--

Replace the style-updated signal with GtkIconTheme:changed

Testing Done:
Ran and make sure the `GWarning` went away.

Reviewed at https://reviews.imfreedom.org/r/1653/

/*
 * Purple
 *
 * Purple is the legal property of its developers, whose names are too
 * numerous to list here. Please refer to the COPYRIGHT file distributed
 * with this source distribution
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
 */

#include "trie.h"

#include <string.h>

#include "debug.h"
#include "memorypool.h"

/* A single internal (that don't have any children) consists
 * of 256 + 4 pointers. That's 1040 bytes on 32-bit machine or 2080 bytes
 * on 64-bit.
 *
 * Thus, in 10500-byte pool block we can hold about 5-10 internal states.
 * Threshold of 100 states means, we'd need 10-20 "small" blocks before
 * switching to ~1-2 large blocks.
 */
#define PURPLE_TRIE_LARGE_THRESHOLD 100
#define PURPLE_TRIE_STATES_SMALL_POOL_BLOCK_SIZE 10880
#define PURPLE_TRIE_STATES_LARGE_POOL_BLOCK_SIZE 102400

typedef struct _PurpleTrieRecord PurpleTrieRecord;
typedef struct _PurpleTrieState PurpleTrieState;
typedef struct _PurpleTrieRecordList PurpleTrieRecordList;

struct _PurpleTrie
{
	GObject parent;
};

typedef struct
{
	gboolean reset_on_match;

	PurpleMemoryPool *records_str_mempool;
	PurpleMemoryPool *records_obj_mempool;
	PurpleTrieRecordList *records;
	GHashTable *records_map;
	gsize records_total_size;

	PurpleMemoryPool *states_mempool;
	PurpleTrieState *root_state;
} PurpleTriePrivate;

struct _PurpleTrieRecord
{
	gchar *word;
	guint word_len;
	gpointer data;
};

struct _PurpleTrieRecordList
{
	PurpleTrieRecord *rec;
	PurpleTrieRecordList *next;
	PurpleTrieRecordList *prev;

	gpointer extra_data;
};

struct _PurpleTrieState
{
	PurpleTrieState *parent;
	PurpleTrieState **children;

	PurpleTrieState *longest_suffix;

	PurpleTrieRecord *found_word;
};

typedef struct
{
	PurpleTrieState *state;

	PurpleTrieState *root_state;
	gboolean reset_on_match;

	PurpleTrieReplaceCb replace_cb;
	PurpleTrieFindCb find_cb;
	gpointer user_data;
} PurpleTrieMachine;

/* TODO: an option to make it eager or lazy (now, it's eager) */
enum
{
	PROP_ZERO,
	PROP_RESET_ON_MATCH,
	PROP_LAST
};

static GParamSpec *properties[PROP_LAST];

G_DEFINE_TYPE_WITH_PRIVATE(PurpleTrie, purple_trie, G_TYPE_OBJECT);

/*******************************************************************************
 * Records list
 ******************************************************************************/

static PurpleTrieRecordList *
purple_record_list_new(PurpleMemoryPool *mpool, PurpleTrieRecord *rec)
{
	PurpleTrieRecordList *node;

	node = purple_memory_pool_alloc0(mpool,
		sizeof(PurpleTrieRecordList), sizeof(gpointer));
	g_return_val_if_fail(node != NULL, NULL);

	node->rec = rec;

	return node;
}

static PurpleTrieRecordList *
purple_record_list_prepend(PurpleMemoryPool *mpool,
	PurpleTrieRecordList *old_head, PurpleTrieRecord *rec)
{
	PurpleTrieRecordList *new_head;

	new_head = purple_record_list_new(mpool, rec);
	g_return_val_if_fail(new_head != NULL, NULL);

	new_head->next = old_head;
	if (old_head)
		old_head->prev = new_head;

	return new_head;
}

static PurpleTrieRecordList *
purple_record_list_copy(PurpleMemoryPool *mpool,
	const PurpleTrieRecordList *head)
{
	PurpleTrieRecordList *new_head = NULL, *new_tail = NULL;

	while (head) {
		PurpleTrieRecordList *node;

		node = purple_record_list_new(mpool, head->rec);
		g_return_val_if_fail(node != NULL, NULL); /* there is no leak */

		node->prev = new_tail;
		if (new_tail)
			new_tail->next = node;
		new_tail = node;
		if (!new_head)
			new_head = node;

		head = head->next;
	}

	return new_head;
}

static PurpleTrieRecordList *
purple_record_list_remove(PurpleTrieRecordList *head,
	PurpleTrieRecordList *node)
{
	g_return_val_if_fail(head != NULL, NULL);
	g_return_val_if_fail(node != NULL, head);
	g_return_val_if_fail(head->prev == NULL, NULL);

	if (head == node) {
		if (head->next != NULL)
			head->next->prev = NULL;
		return head->next;
	} else {
		g_return_val_if_fail(node->prev != NULL, NULL);
		node->prev->next = node->next;
		if (node->next != NULL)
			node->next->prev = node->prev;
		return head;
	}
}


/*******************************************************************************
 * States management
 ******************************************************************************/

static void
purple_trie_states_cleanup(PurpleTriePrivate *priv)
{
	if (priv->root_state != NULL) {
		purple_memory_pool_cleanup(priv->states_mempool);
		priv->root_state = NULL;
	}
}

/* Allocates a state and binds it to the parent. */
static PurpleTrieState *
purple_trie_state_new(PurpleTriePrivate *priv, PurpleTrieState *parent,
                      guchar character)
{
	PurpleTrieState *state;

	state = purple_memory_pool_alloc0(priv->states_mempool,
		sizeof(PurpleTrieState), sizeof(gpointer));
	g_return_val_if_fail(state != NULL, NULL);

	if (parent == NULL)
		return state;

	state->parent = parent;
	if (parent->children == NULL) {
		parent->children = purple_memory_pool_alloc0(
			priv->states_mempool,
			/* PurpleTrieState *children[G_MAXUCHAR + 1] */
			256 * sizeof(gpointer),
			sizeof(gpointer));
	}

	if (parent->children == NULL) {
		purple_memory_pool_free(priv->states_mempool, state);
		g_warn_if_reached();
		return NULL;
	}

	parent->children[character] = state;

	return state;
}

static gboolean
purple_trie_states_build(PurpleTriePrivate *priv)
{
	PurpleTrieState *root;
	PurpleMemoryPool *reclist_mpool;
	PurpleTrieRecordList *reclist, *it;
	gulong cur_len;

	if (priv->root_state != NULL)
		return TRUE;

	if (priv->records_total_size < PURPLE_TRIE_LARGE_THRESHOLD) {
		purple_memory_pool_set_block_size(priv->states_mempool,
			PURPLE_TRIE_STATES_SMALL_POOL_BLOCK_SIZE);
	} else {
		purple_memory_pool_set_block_size(priv->states_mempool,
			PURPLE_TRIE_STATES_LARGE_POOL_BLOCK_SIZE);
	}

	priv->root_state = root = purple_trie_state_new(priv, NULL, '\0');
	g_return_val_if_fail(root != NULL, FALSE);
	g_assert(root->longest_suffix == NULL);

	/* reclist is a list of words not yet added to the trie. Shorter words
	 * are removed from the list, when they are fully added to the trie. */
	reclist_mpool = purple_memory_pool_new();
	reclist = purple_record_list_copy(reclist_mpool, priv->records);

	/* extra_data on every element of reclist will be a pointer to a trie
	 * node -- the prefix of the word with len of cur_len */
	for (it = reclist; it != NULL; it = it->next) {
		it->extra_data = root;
	}

	/* Iterate over indexes of words -- every loop iteration checks certain
	 * index of all remaining words. Loop finishes when there are no words
	 * longer than cur_len. */
	for (cur_len = 0; reclist != NULL; cur_len++) {
		for (it = reclist; it; it = it->next) {
			PurpleTrieRecord *rec = it->rec;
			guchar character = rec->word[cur_len];
			PurpleTrieState *prefix = it->extra_data;
			PurpleTrieState *lon_suf_parent;

			g_assert(character != '\0');

			if (prefix->children && prefix->children[character]) {
				/* Word's prefix is already in the trie, added
				 * by the other word. */
				prefix = prefix->children[character];
			} else {
				/* We need to create a new branch of trie. */
				prefix = purple_trie_state_new(priv, prefix,
				                               character);
				if (!prefix) {
					g_warn_if_reached();
					g_object_unref(reclist_mpool);
					return FALSE;
				}
			}
			it->extra_data = prefix;
			/* prefix is now of length increased by one character. */

			/* The whole word is now added to the trie. */
			if (rec->word[cur_len + 1] == '\0') {
				if (prefix->found_word == NULL)
					prefix->found_word = rec;
				else {
					purple_debug_warning("trie", "found "
						"a collision of \"%s\" words",
						rec->word);
				}

				/* "it" is not modified here, so it->next is
				 * still valid */
				reclist = purple_record_list_remove(reclist, it);
			}

			/* We need to fill the longest_suffix field -- a longest
			 * complete suffix of the prefix we created. We look for
			 * that suffix in any path starting in root and ending
			 * in the (cur_len - 1) level of trie. */
			if (prefix->longest_suffix != NULL)
				continue;
			lon_suf_parent = prefix->parent->longest_suffix;
			while (lon_suf_parent) {
				if (lon_suf_parent->children &&
					lon_suf_parent->children[character])
				{
					prefix->longest_suffix = lon_suf_parent->
						children[character];
					break;
				}
				lon_suf_parent = lon_suf_parent->longest_suffix;
			}
			if (prefix->longest_suffix == NULL)
				prefix->longest_suffix = root;
			if (prefix->found_word == NULL) {
				prefix->found_word =
					prefix->longest_suffix->found_word;
			}
		}
	}

	g_object_unref(reclist_mpool);

	return TRUE;
}

/*******************************************************************************
 * Searching
 ******************************************************************************/

static void
purple_trie_advance(PurpleTrieMachine *m, const guchar character)
{
	/* change state after processing a character */
	while (TRUE) {
		/* Perfect fit - next character is the same, as the child of the
		 * prefix we reached so far. */
		if (m->state->children && m->state->children[character]) {
			m->state = m->state->children[character];
			break;
		}

		/* We reached root, that's a pity. */
		if (m->state == m->root_state)
			break;

		/* Let's try a bit shorter suffix. */
		m->state = m->state->longest_suffix;
	}
}

static gboolean
purple_trie_replace_do_replacement(PurpleTrieMachine *m, GString *out)
{
	gboolean was_replaced = FALSE;
	gsize str_old_len;

	/* if we reached a "found" state, let's process it */
	if (!m->state->found_word)
		return FALSE;

	/* let's get back to the beginning of the word */
	g_assert(out->len >= m->state->found_word->word_len - 1);
	str_old_len = out->len;
	out->len -= m->state->found_word->word_len - 1;

	was_replaced = m->replace_cb(out, m->state->found_word->word,
		m->state->found_word->data, m->user_data);

	/* output was untouched, revert to the previous position */
	if (!was_replaced)
		out->len = str_old_len;

	/* XXX */
	if (was_replaced || m->reset_on_match)
		m->state = m->root_state;

	return was_replaced;
}

static gboolean
purple_trie_find_do_discovery(PurpleTrieMachine *m)
{
	gboolean was_accepted;

	/* if we reached a "found" state, let's process it */
	if (!m->state->found_word)
		return FALSE;

	if (m->find_cb) {
		was_accepted = m->find_cb(m->state->found_word->word,
			m->state->found_word->data, m->user_data);
	} else {
		was_accepted = TRUE;
	}

	if (was_accepted && m->reset_on_match)
		m->state = m->root_state;

	return was_accepted;
}

gchar *
purple_trie_replace(PurpleTrie *trie, const gchar *src,
	PurpleTrieReplaceCb replace_cb, gpointer user_data)
{
	PurpleTriePrivate *priv = NULL;
	PurpleTrieMachine machine;
	GString *out;
	gsize i;

	if (src == NULL)
		return NULL;

	g_return_val_if_fail(replace_cb != NULL, g_strdup(src));
	g_return_val_if_fail(PURPLE_IS_TRIE(trie), NULL);

	priv = purple_trie_get_instance_private(trie);

	purple_trie_states_build(priv);

	machine.state = priv->root_state;
	machine.root_state = priv->root_state;
	machine.reset_on_match = priv->reset_on_match;
	machine.replace_cb = replace_cb;
	machine.user_data = user_data;

	out = g_string_new(NULL);
	i = 0;
	while (src[i] != '\0') {
		guchar character = src[i++];
		gboolean was_replaced;

		purple_trie_advance(&machine, character);
		was_replaced = purple_trie_replace_do_replacement(&machine, out);

		/* We skipped a character without finding any records,
		 * let's just copy it to the output. */
		if (!was_replaced)
			g_string_append_c(out, character);
	}

	return g_string_free(out, FALSE);
}

gchar *
purple_trie_multi_replace(const GSList *tries, const gchar *src,
	PurpleTrieReplaceCb replace_cb, gpointer user_data)
{
	guint tries_count, m_idx;
	PurpleTrieMachine *machines;
	GString *out;
	gsize i;

	if (src == NULL)
		return NULL;

	g_return_val_if_fail(replace_cb != NULL, g_strdup(src));

	tries_count = g_slist_length((GSList*)tries);
	if (tries_count == 0)
		return g_strdup(src);

	/* Initialize all machines. */
	machines = g_new(PurpleTrieMachine, tries_count);
	for (i = 0; i < tries_count; i++, tries = tries->next) {
		PurpleTrie *trie = tries->data;
		PurpleTriePrivate *priv = NULL;

		if (!PURPLE_TRIE(trie)) {
			g_warn_if_reached();
			g_free(machines);
			return NULL;
		}

		priv = purple_trie_get_instance_private(trie);

		purple_trie_states_build(priv);

		machines[i].state = priv->root_state;
		machines[i].root_state = priv->root_state;
		machines[i].reset_on_match = priv->reset_on_match;
		machines[i].replace_cb = replace_cb;
		machines[i].user_data = user_data;
	}

	out = g_string_new(NULL);
	i = 0;
	while (src[i] != '\0') {
		guchar character = src[i++];
		gboolean was_replaced = FALSE;

		/* Advance every machine and possibly perform a replacement. */
		for (m_idx = 0; m_idx < tries_count; m_idx++) {
			purple_trie_advance(&machines[m_idx], character);
			if (was_replaced)
				continue;
			was_replaced = purple_trie_replace_do_replacement(
				&machines[m_idx], out);
		}

		/* We skipped a character without finding any records,
		 * let's just copy it to the output. */
		if (!was_replaced)
			g_string_append_c(out, character);

		/* If we replaced a word, reset _all_ machines */
		if (was_replaced) {
			for (m_idx = 0; m_idx < tries_count; m_idx++) {
				machines[m_idx].state =
					machines[m_idx].root_state;
			}
		}
	}

	g_free(machines);
	return g_string_free(out, FALSE);
}

gulong
purple_trie_find(PurpleTrie *trie, const gchar *src,
	PurpleTrieFindCb find_cb, gpointer user_data)
{
	PurpleTriePrivate *priv = NULL;
	PurpleTrieMachine machine;
	gulong found_count = 0;
	gsize i;

	if (src == NULL)
		return 0;

	g_return_val_if_fail(PURPLE_IS_TRIE(trie), 0);

	priv = purple_trie_get_instance_private(trie);

	purple_trie_states_build(priv);

	machine.state = priv->root_state;
	machine.root_state = priv->root_state;
	machine.reset_on_match = priv->reset_on_match;
	machine.find_cb = find_cb;
	machine.user_data = user_data;

	i = 0;
	while (src[i] != '\0') {
		guchar character = src[i++];
		gboolean was_found;

		purple_trie_advance(&machine, character);

		was_found = purple_trie_find_do_discovery(&machine);

		if (was_found)
			found_count++;
	}

	return found_count;
}

gulong
purple_trie_multi_find(const GSList *tries, const gchar *src,
	PurpleTrieFindCb find_cb, gpointer user_data)
{
	guint tries_count, m_idx;
	PurpleTrieMachine *machines;
	gulong found_count = 0;
	gsize i;

	if (src == NULL)
		return 0;

	tries_count = g_slist_length((GSList*)tries);
	if (tries_count == 0)
		return 0;

	/* Initialize all machines. */
	machines = g_new(PurpleTrieMachine, tries_count);
	for (i = 0; i < tries_count; i++, tries = tries->next) {
		PurpleTrie *trie = tries->data;
		PurpleTriePrivate *priv = NULL;

		if (!PURPLE_IS_TRIE(trie)) {
			g_warn_if_reached();
			g_free(machines);
			return 0;
		}

		priv = purple_trie_get_instance_private(trie);

		purple_trie_states_build(priv);

		machines[i].state = priv->root_state;
		machines[i].root_state = priv->root_state;
		machines[i].reset_on_match = priv->reset_on_match;
		machines[i].find_cb = find_cb;
		machines[i].user_data = user_data;
	}

	i = 0;
	while (src[i] != '\0') {
		guchar character = src[i++];
		gboolean was_found = FALSE;

		/* Advance every machine and possibly perform a replacement. */
		for (m_idx = 0; m_idx < tries_count; m_idx++) {
			purple_trie_advance(&machines[m_idx], character);
			if (was_found)
				continue;
			was_found =
				purple_trie_find_do_discovery(&machines[m_idx]);
			if (was_found)
				found_count++;
		}

		/* If we replaced a word, reset _all_ machines */
		if (was_found) {
			for (m_idx = 0; m_idx < tries_count; m_idx++) {
				if (!machines[m_idx].reset_on_match)
					continue;
				machines[m_idx].state =
					machines[m_idx].root_state;
			}
		}
	}

	g_free(machines);
	return found_count;
}


/*******************************************************************************
 * Records
 ******************************************************************************/

gboolean
purple_trie_add(PurpleTrie *trie, const gchar *word, gpointer data)
{
	PurpleTriePrivate *priv = NULL;
	PurpleTrieRecord *rec;

	g_return_val_if_fail(PURPLE_IS_TRIE(trie), FALSE);
	g_return_val_if_fail(word != NULL, FALSE);
	g_return_val_if_fail(word[0] != '\0', FALSE);

	priv = purple_trie_get_instance_private(trie);

	if (g_hash_table_lookup(priv->records_map, word) != NULL) {
		purple_debug_warning("trie", "record exists: %s", word);
		return FALSE;
	}

	/* Every change in a trie invalidates longest_suffix map.
	 * These prefixes could be updated instead of cleaning the whole graph.
	 */
	purple_trie_states_cleanup(priv);

	rec = purple_memory_pool_alloc(priv->records_obj_mempool,
		sizeof(PurpleTrieRecord), sizeof(gpointer));
	rec->word = purple_memory_pool_strdup(priv->records_str_mempool, word);
	rec->word_len = strlen(word);
	g_assert(rec->word_len > 0);
	rec->data = data;

	priv->records_total_size += rec->word_len;
	priv->records = purple_record_list_prepend(priv->records_obj_mempool,
		priv->records, rec);
	g_hash_table_insert(priv->records_map, rec->word, priv->records);

	return TRUE;
}

void
purple_trie_remove(PurpleTrie *trie, const gchar *word)
{
	PurpleTriePrivate *priv = NULL;
	PurpleTrieRecordList *it;

	g_return_if_fail(PURPLE_IS_TRIE(trie));
	g_return_if_fail(word != NULL);
	g_return_if_fail(word[0] != '\0');

	priv = purple_trie_get_instance_private(trie);

	it = g_hash_table_lookup(priv->records_map, word);
	if (it == NULL)
		return;

	/* see purple_trie_add */
	purple_trie_states_cleanup(priv);

	priv->records_total_size -= it->rec->word_len;
	priv->records = purple_record_list_remove(priv->records, it);
	g_hash_table_remove(priv->records_map, it->rec->word);

	purple_memory_pool_free(priv->records_str_mempool, it->rec->word);
	purple_memory_pool_free(priv->records_obj_mempool, it->rec);
	purple_memory_pool_free(priv->records_obj_mempool, it);
}

guint
purple_trie_get_size(PurpleTrie *trie)
{
	PurpleTriePrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_TRIE(trie), 0);

	priv = purple_trie_get_instance_private(trie);
	return g_hash_table_size(priv->records_map);
}


/*******************************************************************************
 * API implementation
 ******************************************************************************/

gboolean
purple_trie_get_reset_on_match(PurpleTrie *trie)
{
	PurpleTriePrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_TRIE(trie), FALSE);

	priv = purple_trie_get_instance_private(trie);
	return priv->reset_on_match;
}

void
purple_trie_set_reset_on_match(PurpleTrie *trie, gboolean reset)
{
	PurpleTriePrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_TRIE(trie));

	priv = purple_trie_get_instance_private(trie);
	priv->reset_on_match = reset;
	g_object_notify_by_pspec(G_OBJECT(trie), properties[PROP_RESET_ON_MATCH]);
}

/*******************************************************************************
 * Object stuff
 ******************************************************************************/

PurpleTrie *
purple_trie_new(void)
{
	return g_object_new(PURPLE_TYPE_TRIE, NULL);
}

static void
purple_trie_init(PurpleTrie *trie)
{
	PurpleTriePrivate *priv = purple_trie_get_instance_private(trie);

	priv->records_obj_mempool = purple_memory_pool_new();
	priv->records_str_mempool = purple_memory_pool_new();
	priv->states_mempool = purple_memory_pool_new();
	purple_memory_pool_set_block_size(priv->states_mempool,
		PURPLE_TRIE_STATES_SMALL_POOL_BLOCK_SIZE);

	priv->records_map = g_hash_table_new(g_str_hash, g_str_equal);
}

static void
purple_trie_finalize(GObject *obj)
{
	PurpleTriePrivate *priv =
			purple_trie_get_instance_private(PURPLE_TRIE(obj));

	g_hash_table_destroy(priv->records_map);
	g_object_unref(priv->records_obj_mempool);
	g_object_unref(priv->records_str_mempool);
	g_object_unref(priv->states_mempool);

	G_OBJECT_CLASS(purple_trie_parent_class)->finalize(obj);
}

static void
purple_trie_get_property(GObject *obj, guint param_id, GValue *value,
	GParamSpec *pspec)
{
	PurpleTrie *trie = PURPLE_TRIE(obj);
	PurpleTriePrivate *priv = purple_trie_get_instance_private(trie);

	switch (param_id) {
		case PROP_RESET_ON_MATCH:
			g_value_set_boolean(value, priv->reset_on_match);
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
	}
}

static void
purple_trie_set_property(GObject *obj, guint param_id,
	const GValue *value, GParamSpec *pspec)
{
	PurpleTrie *trie = PURPLE_TRIE(obj);
	PurpleTriePrivate *priv = purple_trie_get_instance_private(trie);

	switch (param_id) {
		case PROP_RESET_ON_MATCH:
			priv->reset_on_match = g_value_get_boolean(value);
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
	}
}

static void
purple_trie_class_init(PurpleTrieClass *klass)
{
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = purple_trie_finalize;
	obj_class->get_property = purple_trie_get_property;
	obj_class->set_property = purple_trie_set_property;

	properties[PROP_RESET_ON_MATCH] = g_param_spec_boolean("reset-on-match",
		"Reset on match", "Determines, if the search state machine "
		"should be reset to the initial state on every match. This "
		"ensures, that every match is distinct from each other. "
		"Please note, that it's not well-defined for a replace "
		"operation, so it's better to leave this value default, unless "
		"you perform only find operations.", TRUE,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, PROP_LAST, properties);
}

mercurial