/*
* Copyright © 2010 Codethink Limited
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the licence, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see .
*
* Author: Ryan Lortie
*/
#include "config.h"
#include "dconf-changeset.h"
#include "dconf-paths.h"
#include
#include
/**
* SECTION:changeset
* @title: DConfChangeset
* @Short_description: A set of changes to a dconf database
*
* #DConfChangeset represents a set of changes that can be made to a
* dconf database. Currently supported operations are writing new
* values to keys and resetting keys and dirs.
*
* Create the changeset with dconf_changeset_new() and populate it with
* dconf_changeset_set(). Submit it to dconf with
* dconf_client_change_fast() or dconf_client_change_sync().
* dconf_changeset_new_write() is a convenience constructor for the
* common case of writing or resetting a single value.
**/
/**
* DConfChangeset:
*
* This is a reference counted opaque structure type. It is not a
* #GObject.
*
* Use dconf_changeset_ref() and dconf_changeset_unref() to manipulate
* references.
**/
struct _DConfChangeset
{
GHashTable *table;
GHashTable *dir_resets;
guint is_database : 1;
guint is_sealed : 1;
gint ref_count;
gchar *prefix;
const gchar **paths;
GVariant **values;
};
static void
unref_gvariant0 (gpointer data)
{
if (data)
g_variant_unref (data);
}
/**
* dconf_changeset_new:
*
* Creates a new, empty, #DConfChangeset.
*
* Returns: (transfer full): the new #DConfChangeset.
**/
DConfChangeset *
dconf_changeset_new (void)
{
DConfChangeset *changeset;
changeset = g_slice_new0 (DConfChangeset);
changeset->table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, unref_gvariant0);
changeset->ref_count = 1;
return changeset;
}
/**
* dconf_changeset_new_database:
* @copy_of: (nullable): a #DConfChangeset to copy
*
* Creates a new #DConfChangeset in "database" mode, possibly
* initialising it with the values of another changeset.
*
* In a certain sense it's possible to imagine that a #DConfChangeset
* could express the contents of an entire dconf database -- the
* contents are the database are what you would have if you applied the
* changeset to an empty database. One thing that fails to map in this
* analogy are reset operations -- if we start with an empty database
* then reset operations are meaningless.
*
* A "database" mode changeset is therefore a changeset which is
* incapable of containing reset operations.
*
* It is not permitted to use a database-mode changeset for most
* operations (such as the @change argument to dconf_changeset_change()
* or the @changeset argument to #DConfClient APIs).
*
* If @copy_of is non-%NULL then its contents will be copied into the
* created changeset. @copy_of must be a database-mode changeset.
*
* Returns: (transfer full): a new #DConfChangeset in "database" mode
*
* Since: 0.16
*/
DConfChangeset *
dconf_changeset_new_database (DConfChangeset *copy_of)
{
DConfChangeset *changeset;
g_return_val_if_fail (copy_of == NULL || copy_of->is_database, NULL);
changeset = dconf_changeset_new ();
changeset->is_database = TRUE;
if (copy_of)
{
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init (&iter, copy_of->table);
while (g_hash_table_iter_next (&iter, &key, &value))
g_hash_table_insert (changeset->table, g_strdup (key), g_variant_ref (value));
}
return changeset;
}
/**
* dconf_changeset_unref:
* @changeset: a #DConfChangeset
*
* Releases a #DConfChangeset reference.
**/
void
dconf_changeset_unref (DConfChangeset *changeset)
{
if (g_atomic_int_dec_and_test (&changeset->ref_count))
{
g_free (changeset->prefix);
g_free (changeset->paths);
g_free (changeset->values);
g_hash_table_unref (changeset->table);
if (changeset->dir_resets)
g_hash_table_unref (changeset->dir_resets);
g_slice_free (DConfChangeset, changeset);
}
}
/**
* dconf_changeset_ref:
* @changeset: a #DConfChangeset
*
* Increases the reference count on @changeset
*
* Returns: (transfer full): @changeset
**/
DConfChangeset *
dconf_changeset_ref (DConfChangeset *changeset)
{
g_atomic_int_inc (&changeset->ref_count);
return changeset;
}
static void
dconf_changeset_record_dir_reset (DConfChangeset *changeset,
const gchar *dir)
{
g_return_if_fail (dconf_is_dir (dir, NULL));
g_return_if_fail (!changeset->is_database);
g_return_if_fail (!changeset->is_sealed);
if (!changeset->dir_resets)
changeset->dir_resets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
g_hash_table_insert (changeset->table, g_strdup (dir), NULL);
g_hash_table_add (changeset->dir_resets, g_strdup (dir));
}
/**
* dconf_changeset_set:
* @changeset: a #DConfChangeset
* @path: a path to modify
* @value: (nullable): the value for the key, or %NULL to reset. If it has a
* floating reference it's consumed.
*
* Adds an operation to modify @path to a #DConfChangeset.
*
* @path may either be a key or a dir. If it is a key then @value may
* be a #GVariant, or %NULL (to set or reset the key).
*
* If @path is a dir then this must be a reset operation: @value must be
* %NULL. It is not permitted to assign a #GVariant value to a dir.
**/
void
dconf_changeset_set (DConfChangeset *changeset,
const gchar *path,
GVariant *value)
{
g_return_if_fail (!changeset->is_sealed);
g_return_if_fail (dconf_is_path (path, NULL));
/* Check if we are performing a path reset */
if (g_str_has_suffix (path, "/"))
{
GHashTableIter iter;
gpointer key;
g_return_if_fail (value == NULL);
/* When we reset a path we must also reset all keys within that
* path.
*/
g_hash_table_iter_init (&iter, changeset->table);
while (g_hash_table_iter_next (&iter, &key, NULL))
if (g_str_has_prefix (key, path))
g_hash_table_iter_remove (&iter);
/* If this is a non-database then record the reset itself. */
if (!changeset->is_database)
dconf_changeset_record_dir_reset (changeset, path);
}
/* ...or a value reset */
else if (value == NULL)
{
/* If we're a non-database, record the reset explicitly.
* Otherwise, just reset whatever may be there already.
*/
if (!changeset->is_database)
g_hash_table_insert (changeset->table, g_strdup (path), NULL);
else
g_hash_table_remove (changeset->table, path);
}
/* ...or a normal write. */
else
g_hash_table_insert (changeset->table, g_strdup (path), g_variant_ref_sink (value));
}
/**
* dconf_changeset_get:
* @changeset: a #DConfChangeset
* @key: the key to check
* @value: (transfer full) (optional) (nullable): a return location for the value, or %NULL
*
* Checks if a #DConfChangeset has an outstanding request to change
* the value of the given @key.
*
* If the change doesn't involve @key then %FALSE is returned and the
* @value is unmodified.
*
* If the change modifies @key then @value is set either to the value
* for that key, or %NULL in the case that the key is being reset by the
* request.
*
* Returns: %TRUE if the key is being modified by the change
*/
gboolean
dconf_changeset_get (DConfChangeset *changeset,
const gchar *key,
GVariant **value)
{
gpointer tmp;
if (!g_hash_table_lookup_extended (changeset->table, key, NULL, &tmp))
{
/* Did not find an exact match, so check for dir resets */
if (changeset->dir_resets)
{
GHashTableIter iter;
gpointer dir;
g_hash_table_iter_init (&iter, changeset->dir_resets);
while (g_hash_table_iter_next (&iter, &dir, NULL))
if (g_str_has_prefix (key, dir))
{
if (value)
*value = NULL;
return TRUE;
}
}
return FALSE;
}
if (value)
*value = tmp ? g_variant_ref (tmp) : NULL;
return TRUE;
}
/**
* dconf_changeset_is_similar_to:
* @changeset: a #DConfChangeset
* @other: another #DConfChangeset
*
* Checks if @changeset is similar to @other.
*
* Two changes are considered similar if they write to the exact same
* set of keys. The values written are not considered.
*
* This check is used to prevent building up a queue of repeated writes
* of the same keys. This is often seen when an application writes to a
* key on every move of a slider or an application window.
*
* Strictly speaking, a write resettings all of "/a/" after a write
* containing "/a/b" could cause the later to be removed from the queue,
* but this situation is difficult to detect and is expected to be
* extremely rare.
*
* Returns: %TRUE if the changes are similar
**/
gboolean
dconf_changeset_is_similar_to (DConfChangeset *changeset,
DConfChangeset *other)
{
GHashTableIter iter;
gpointer key;
if (g_hash_table_size (changeset->table) != g_hash_table_size (other->table))
return FALSE;
g_hash_table_iter_init (&iter, changeset->table);
while (g_hash_table_iter_next (&iter, &key, NULL))
if (!g_hash_table_contains (other->table, key))
return FALSE;
return TRUE;
}
/**
* DConfChangesetPredicate:
* @path: a path, as per dconf_is_path()
* @value: (nullable): a #GVariant, or %NULL
* @user_data: user data pointer
*
* Callback function type for predicates over items in a
* #DConfChangeset.
*
* Use with dconf_changeset_all().
*
* Returns: %TRUE if the predicate is met for the given @path and @value
**/
/**
* dconf_changeset_all:
* @changeset: a #DConfChangeset
* @predicate: a #DConfChangesetPredicate
* @user_data: user data to pass to @predicate
*
* Checks if all changes in the changeset satisfy @predicate.
*
* @predicate is called on each item in the changeset, in turn, until it
* returns %FALSE.
*
* If @predicate returns %FALSE for any item, this function returns
* %FALSE. If not (including the case of no items) then this function
* returns %TRUE.
*
* Returns: %TRUE if all items in @changeset satisfy @predicate
*/
gboolean
dconf_changeset_all (DConfChangeset *changeset,
DConfChangesetPredicate predicate,
gpointer user_data)
{
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init (&iter, changeset->table);
while (g_hash_table_iter_next (&iter, &key, &value))
if (!(* predicate) (key, value, user_data))
return FALSE;
return TRUE;
}
static gint
dconf_changeset_string_ptr_compare (gconstpointer a_p,
gconstpointer b_p)
{
const gchar * const *a = a_p;
const gchar * const *b = b_p;
return strcmp (*a, *b);
}
/**
* dconf_changeset_seal:
* @changeset: a #DConfChangeset
*
* Seals @changeset.
*
* When a #DConfChangeset is first created, it is mutable and
* non-threadsafe. Once the changeset is populated with the required
* changes, it can be shared between multiple threads, but only by
* making it immutable by "sealing" it.
*
* After the changeset is sealed, you cannot call dconf_changeset_set()
* or any other functions that would modify it. It is safe, however, to
* share it between multiple threads.
*
* All changesets are unsealed on creation, including those that are
* made by copying changesets that are sealed.
* dconf_changeset_describe() will implicitly seal a changeset.
*
* This function is idempotent.
*
* Since: 0.18
**/
void
dconf_changeset_seal (DConfChangeset *changeset)
{
gsize prefix_length;
gint n_items;
if (changeset->is_sealed)
return;
changeset->is_sealed = TRUE;
/* This function used to be called dconf_changeset_build_description()
* because that's basically what sealing is...
*/
n_items = g_hash_table_size (changeset->table);
/* If there are no items then what is there to describe? */
if (n_items == 0)
return;
/* We do three separate passes. This might take a bit longer than
* doing it all at once but it keeps the complexity down.
*
* First, we iterate the table in order to determine the common
* prefix.
*
* Next, we iterate the table again to pull the strings out excluding
* the leading prefix.
*
* We sort the list of paths at this point because the writer
* requires a sorted list in order to ensure that dir resets come
* before writes to keys in that dir.
*
* Finally, we iterate over the sorted list and use the normal
* hashtable lookup in order to populate the values array in the same
* order.
*
* Doing it this way avoids the complication of trying to sort two
* arrays (keys and values) at the same time.
*/
/* Pass 1: determine the common prefix. */
{
GHashTableIter iter;
const gchar *first;
gboolean have_one;
gpointer key;
g_hash_table_iter_init (&iter, changeset->table);
/* We checked above that we have at least one item. */
have_one = g_hash_table_iter_next (&iter, &key, NULL);
g_assert (have_one);
prefix_length = strlen (key);
first = key;
/* Consider the remaining items to find the common prefix */
while (g_hash_table_iter_next (&iter, &key, NULL))
{
const gchar *this = key;
gint i;
for (i = 0; i < prefix_length; i++)
if (first[i] != this[i])
{
prefix_length = i;
break;
}
}
/* We must surely always have a common prefix of '/' */
g_assert (prefix_length > 0);
g_assert (first[0] == '/');
/* We may find that "/a/ab" and "/a/ac" have a common prefix of
* "/a/a" but really we want to trim that back to "/a/".
*
* If there is only one item, leave it alone.
*/
if (n_items > 1)
{
while (first[prefix_length - 1] != '/')
prefix_length--;
}
changeset->prefix = g_strndup (first, prefix_length);
}
/* Pass 2: collect the list of keys, dropping the prefix */
{
GHashTableIter iter;
gpointer key;
gint i = 0;
changeset->paths = g_new (const gchar *, n_items + 1);
g_hash_table_iter_init (&iter, changeset->table);
while (g_hash_table_iter_next (&iter, &key, NULL))
{
const gchar *path = key;
changeset->paths[i++] = path + prefix_length;
}
changeset->paths[i] = NULL;
g_assert (i == n_items);
/* Sort the list of keys */
qsort (changeset->paths, n_items, sizeof (const gchar *), dconf_changeset_string_ptr_compare);
}
/* Pass 3: collect the list of values */
{
gint i;
changeset->values = g_new (GVariant *, n_items);
for (i = 0; i < n_items; i++)
/* We dropped the prefix when collecting the array.
* Bring it back temporarily, for the lookup.
*/
changeset->values[i] = g_hash_table_lookup (changeset->table, changeset->paths[i] - prefix_length);
}
}
/**
* dconf_changeset_describe:
* @changeset: a #DConfChangeset
* @prefix: (transfer none) (optional) (out): the prefix under which changes have been requested
* @paths: (transfer none) (optional) (out): the list of paths changed, relative to @prefix
* @values: (transfer none) (optional) (out): the list of values changed
*
* Describes @changeset.
*
* @prefix and @paths are presented in the same way as they are for the
* DConfClient::changed signal. @values is an array of the same length
* as @paths. For each key described by an element in @paths, @values
* will contain either a #GVariant (the requested new value of that key)
* or %NULL (to reset a reset).
*
* The @paths array is returned in an order such that dir will always
* come before keys contained within those dirs.
*
* If @changeset is not already sealed then this call will implicitly
* seal it. See dconf_changeset_seal().
*
* Returns: the number of changes (the length of @changes and @values).
**/
guint
dconf_changeset_describe (DConfChangeset *changeset,
const gchar **prefix,
const gchar * const **paths,
GVariant * const **values)
{
gint n_items;
n_items = g_hash_table_size (changeset->table);
dconf_changeset_seal (changeset);
if (prefix)
*prefix = changeset->prefix;
if (paths)
*paths = changeset->paths;
if (values)
*values = changeset->values;
return n_items;
}
/**
* dconf_changeset_serialise:
* @changeset: a #DConfChangeset
*
* Serialises a #DConfChangeset.
*
* The returned value has no particular format and should only be passed
* to dconf_changeset_deserialise().
*
* Returns: (transfer full): a floating #GVariant
**/
GVariant *
dconf_changeset_serialise (DConfChangeset *changeset)
{
GVariantBuilder builder;
GHashTableIter iter;
gpointer key, value;
g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{smv}"));
g_hash_table_iter_init (&iter, changeset->table);
while (g_hash_table_iter_next (&iter, &key, &value))
g_variant_builder_add (&builder, "{smv}", key, value);
return g_variant_builder_end (&builder);
}
/**
* dconf_changeset_deserialise:
* @serialised: (transfer none): a #GVariant from dconf_changeset_serialise()
*
* Creates a #DConfChangeset according to a serialised description
* returned from an earlier call to dconf_changeset_serialise().
*
* @serialised has no particular format -- you should only pass a value
* that resulted from an earlier serialise operation.
*
* This call never fails, even if @serialised is not in the correct
* format. Improperly-formatted parts are simply ignored.
*
* Returns: (transfer full): a new #DConfChangeset
**/
DConfChangeset *
dconf_changeset_deserialise (GVariant *serialised)
{
DConfChangeset *changeset;
GVariantIter iter;
const gchar *key;
GVariant *value;
changeset = dconf_changeset_new ();
g_variant_iter_init (&iter, serialised);
while (g_variant_iter_loop (&iter, "{&smv}", &key, &value))
{
/* If value is NULL: we may be resetting a key or a dir (a path).
* If value is non-NULL: we must be setting a key.
*
* ie: it is not possible to set a value to a directory.
*
* If we get an invalid case, just fall through and ignore it.
*/
if (dconf_is_key (key, NULL))
g_hash_table_insert (changeset->table, g_strdup (key), value ? g_variant_ref (value) : NULL);
else if (dconf_is_dir (key, NULL) && value == NULL)
dconf_changeset_record_dir_reset (changeset, key);
}
return changeset;
}
/**
* dconf_changeset_new_write:
* @path: a dconf path
* @value: (nullable): a #GVariant, or %NULL. If it has a floating reference it's
* consumed.
*
* Creates a new #DConfChangeset with one change. This is equivalent to
* calling dconf_changeset_new() and then dconf_changeset_set() with
* @path and @value.
*
* Returns: a new #DConfChangeset
**/
DConfChangeset *
dconf_changeset_new_write (const gchar *path,
GVariant *value)
{
DConfChangeset *changeset;
changeset = dconf_changeset_new ();
dconf_changeset_set (changeset, path, value);
return changeset;
}
/**
* dconf_changeset_is_empty:
* @changeset: a #DConfChangeset
*
* Checks if @changeset is empty (ie: contains no changes).
*
* Returns: %TRUE if @changeset is empty
**/
gboolean
dconf_changeset_is_empty (DConfChangeset *changeset)
{
return !g_hash_table_size (changeset->table);
}
/**
* dconf_changeset_change:
* @changeset: a #DConfChangeset (to be changed)
* @changes: the changes to make to @changeset
*
* Applies @changes to @changeset.
*
* If @changeset is a normal changeset then reset requests in @changes
* will be allied to @changeset and then copied down into it. In this
* case the two changesets are effectively being merged.
*
* If @changeset is in database mode then the reset operations in
* @changes will simply be applied to @changeset.
*
* Since: 0.16
**/
void
dconf_changeset_change (DConfChangeset *changeset,
DConfChangeset *changes)
{
gsize prefix_len;
gint i;
g_return_if_fail (!changeset->is_sealed);
/* Handling resets is a little bit tricky...
*
* Consider the case that we have @changeset containing a key /a/b and
* @changes containing a reset request for /a/ and a set request for
* /a/c.
*
* It's clear that at the end of this all, we should have only /a/c
* but in order for that to be the case, we need to make sure that we
* process the reset of /a/ before we process the set of /a/c.
*
* The easiest way to do this is to visit the strings in sorted order.
* That removes the possibility of iterating over the hash table, but
* dconf_changeset_build_description() makes the list in the order we
* need so just call it and then iterate over the result.
*/
if (!dconf_changeset_describe (changes, NULL, NULL, NULL))
return;
prefix_len = strlen (changes->prefix);
for (i = 0; changes->paths[i]; i++)
{
const gchar *path;
GVariant *value;
/* The changes->paths are just pointers into the keys of the
* hashtable, fast-forwarded past the prefix. Rewind a bit.
*/
path = changes->paths[i] - prefix_len;
value = changes->values[i];
dconf_changeset_set (changeset, path, value);
}
}
/**
* dconf_changeset_filter_changes:
* @base: a database mode changeset
* @changes: a changeset
*
* Produces a changeset that contains all the changes in @changes that
* are not already present in @base
*
* If there are no such changes, %NULL is returned
*
* Applying the result to @base will yield the same result as applying
* @changes to @base
*
* Returns: (transfer full) (nullable): the minimal changes, or %NULL
*
* Since: 0.35.1
*/
DConfChangeset *
dconf_changeset_filter_changes (DConfChangeset *base,
DConfChangeset *changes)
{
DConfChangeset *result = NULL;
GHashTableIter iter_changes;
gpointer key, val;
g_return_val_if_fail (base->is_database, NULL);
/* We create the list of changes by iterating the 'changes' changeset
* and noting any keys that are not in the 'base' changeset or do not
* have the same value in the 'base' changeset
*
* Note: because 'base' is a database changeset we don't have to
* worry about it containing NULL values (dir resets).
*/
g_hash_table_iter_init (&iter_changes, changes->table);
while (g_hash_table_iter_next (&iter_changes, &key, &val))
{
GVariant *base_val = g_hash_table_lookup (base->table, key);
if (g_str_has_suffix (key, "/"))
{
// Path reset
gboolean reset_is_effective = FALSE;
GHashTableIter iter_base;
gpointer base_key = NULL;
g_return_val_if_fail (val == NULL, NULL);
// First we check whether there are any keys in base that would be reset
g_hash_table_iter_init (&iter_base, base->table);
while (g_hash_table_iter_next (&iter_base, &base_key, NULL))
if (g_str_has_prefix (base_key, key) && !g_str_equal (base_key, key))
{
reset_is_effective = TRUE;
break;
}
if (reset_is_effective)
{
if (!result)
result = dconf_changeset_new ();
dconf_changeset_set (result, key, val);
}
}
else if (base_val == NULL && val == NULL)
continue; // Resetting a key that wasn't set
else if (val == NULL || base_val == NULL || !g_variant_equal (val, base_val))
{
// Resetting an existing key, inserting a value under a key that was not
// set, or replacing an existing value with a different one.
if (!result)
result = dconf_changeset_new ();
dconf_changeset_set (result, key, val);
}
}
return result;
}
/**
* dconf_changeset_diff:
* @from: a database mode changeset
* @to: a database mode changeset
*
* Compares to database-mode changesets and produces a changeset that
* describes their differences.
*
* If there is no difference, %NULL is returned.
*
* Applying the returned changeset to @from using
* dconf_changeset_change() will result in the two changesets being
* equal.
*
* Returns: (transfer full) (nullable): the changes, or %NULL
*
* Since: 0.16
*/
DConfChangeset *
dconf_changeset_diff (DConfChangeset *from,
DConfChangeset *to)
{
DConfChangeset *changeset;
GHashTableIter iter;
gpointer key, val;
g_return_val_if_fail (from->is_database, NULL);
g_return_val_if_fail (to->is_database, NULL);
/* We make no attempt to do dir resets, but we could...
*
* For now, we just reset each key individually.
*
* We create our list of changes in two steps:
*
* - call dconf_changeset_filter_changes to find values from 'to'
* which are not present in 'from' or hold different values to 'to'
*
* - iterate the 'from' changeset and note any keys not present in
* the 'to' changeset, recording resets for them
*
* This will cover all changes.
*
* Note: because 'from' and 'to' are database changesets we don't have
* to worry about seeing NULL values or dirs.
*/
changeset = dconf_changeset_filter_changes (from, to);
g_hash_table_iter_init (&iter, from->table);
while (g_hash_table_iter_next (&iter, &key, &val))
if (!g_hash_table_lookup (to->table, key))
{
if (!changeset)
changeset = dconf_changeset_new ();
dconf_changeset_set (changeset, key, NULL);
}
return changeset;
}