| /* |
| * 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 <http://www.gnu.org/licenses/>. |
| * |
| * Author: Ryan Lortie <desrt@desrt.ca> |
| */ |
| |
| #include "gvdb-reader.h" |
| #include "gvdb-format.h" |
| |
| #include <string.h> |
| |
| struct _GvdbTable { |
| gint ref_count; |
| |
| const gchar *data; |
| gsize size; |
| |
| gpointer user_data; |
| GvdbRefFunc ref_user_data; |
| GDestroyNotify unref_user_data; |
| |
| gboolean byteswapped; |
| gboolean trusted; |
| |
| const guint32_le *bloom_words; |
| guint32 n_bloom_words; |
| guint bloom_shift; |
| |
| const guint32_le *hash_buckets; |
| guint32 n_buckets; |
| |
| struct gvdb_hash_item *hash_items; |
| guint32 n_hash_items; |
| }; |
| |
| static const gchar * |
| gvdb_table_item_get_key (GvdbTable *file, |
| const struct gvdb_hash_item *item, |
| gsize *size) |
| { |
| guint32 start, end; |
| |
| start = guint32_from_le (item->key_start); |
| *size = guint16_from_le (item->key_size); |
| end = start + *size; |
| |
| if G_UNLIKELY (start > end || end > file->size) |
| return NULL; |
| |
| return file->data + start; |
| } |
| |
| static gconstpointer |
| gvdb_table_dereference (GvdbTable *file, |
| const struct gvdb_pointer *pointer, |
| gint alignment, |
| gsize *size) |
| { |
| guint32 start, end; |
| |
| start = guint32_from_le (pointer->start); |
| end = guint32_from_le (pointer->end); |
| |
| if G_UNLIKELY (start > end || end > file->size || start & (alignment - 1)) |
| return NULL; |
| |
| *size = end - start; |
| |
| return file->data + start; |
| } |
| |
| static void |
| gvdb_table_setup_root (GvdbTable *file, |
| const struct gvdb_pointer *pointer) |
| { |
| const struct gvdb_hash_header *header; |
| guint32 n_bloom_words; |
| guint32 n_buckets; |
| gsize size; |
| |
| header = gvdb_table_dereference (file, pointer, 4, &size); |
| |
| if G_UNLIKELY (header == NULL || size < sizeof *header) |
| return; |
| |
| size -= sizeof *header; |
| |
| n_bloom_words = guint32_from_le (header->n_bloom_words); |
| n_buckets = guint32_from_le (header->n_buckets); |
| n_bloom_words &= (1u << 27) - 1; |
| |
| if G_UNLIKELY (n_bloom_words * sizeof (guint32_le) > size) |
| return; |
| |
| file->bloom_words = (gpointer) (header + 1); |
| size -= n_bloom_words * sizeof (guint32_le); |
| file->n_bloom_words = n_bloom_words; |
| |
| if G_UNLIKELY (n_buckets > G_MAXUINT / sizeof (guint32_le) || |
| n_buckets * sizeof (guint32_le) > size) |
| return; |
| |
| file->hash_buckets = file->bloom_words + file->n_bloom_words; |
| size -= n_buckets * sizeof (guint32_le); |
| file->n_buckets = n_buckets; |
| |
| if G_UNLIKELY (size % sizeof (struct gvdb_hash_item)) |
| return; |
| |
| file->hash_items = (gpointer) (file->hash_buckets + n_buckets); |
| file->n_hash_items = size / sizeof (struct gvdb_hash_item); |
| } |
| |
| static GvdbTable * |
| new_from_data (const void *data, |
| gsize data_len, |
| gboolean trusted, |
| gpointer user_data, |
| GvdbRefFunc ref, |
| GDestroyNotify unref, |
| const char *filename, |
| GError **error) |
| { |
| GvdbTable *file; |
| |
| file = g_slice_new0 (GvdbTable); |
| file->data = data; |
| file->size = data_len; |
| file->trusted = trusted; |
| file->ref_count = 1; |
| file->ref_user_data = ref; |
| file->unref_user_data = unref; |
| file->user_data = user_data; |
| |
| if (sizeof (struct gvdb_header) <= file->size) |
| { |
| const struct gvdb_header *header = (gpointer) file->data; |
| |
| if (header->signature[0] == GVDB_SIGNATURE0 && |
| header->signature[1] == GVDB_SIGNATURE1 && |
| guint32_from_le (header->version) == 0) |
| file->byteswapped = FALSE; |
| |
| else if (header->signature[0] == GVDB_SWAPPED_SIGNATURE0 && |
| header->signature[1] == GVDB_SWAPPED_SIGNATURE1 && |
| guint32_from_le (header->version) == 0) |
| file->byteswapped = TRUE; |
| |
| else |
| { |
| if (filename) |
| g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, |
| "%s: invalid header", filename); |
| else |
| g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, |
| "invalid gvdb header"); |
| g_slice_free (GvdbTable, file); |
| if (unref) |
| unref (user_data); |
| |
| return NULL; |
| } |
| |
| gvdb_table_setup_root (file, &header->root); |
| } |
| |
| return file; |
| } |
| |
| /** |
| * gvdb_table_new: |
| * @filename: the path to the hash file |
| * @trusted: if the contents of @filename are trusted |
| * @error: %NULL, or a pointer to a %NULL #GError |
| * |
| * Creates a new #GvdbTable from the contents of the file found at |
| * @filename. |
| * |
| * The only time this function fails is if the file cannot be opened. |
| * In that case, the #GError that is returned will be an error from |
| * g_mapped_file_new(). |
| * |
| * An empty or otherwise corrupted file is considered to be a valid |
| * #GvdbTable with no entries. |
| * |
| * You should call gvdb_table_unref() on the return result when you no |
| * longer require it. |
| * |
| * Returns: a new #GvdbTable |
| **/ |
| GvdbTable * |
| gvdb_table_new (const gchar *filename, |
| gboolean trusted, |
| GError **error) |
| { |
| GMappedFile *mapped; |
| |
| if ((mapped = g_mapped_file_new (filename, FALSE, error)) == NULL) |
| return NULL; |
| |
| return new_from_data (g_mapped_file_get_contents (mapped), |
| g_mapped_file_get_length (mapped), |
| trusted, |
| mapped, |
| (GvdbRefFunc)g_mapped_file_ref, |
| (GDestroyNotify)g_mapped_file_unref, |
| filename, |
| error); |
| } |
| |
| /** |
| * gvdb_table_new_from_data: |
| * @data: the data |
| * @data_len: the length of @data in bytes |
| * @trusted: if the contents of @data are trusted |
| * @user_data: User supplied data that owns @data |
| * @ref: Ref function for @user_data |
| * @unref: Unref function for @user_data |
| * |
| * Creates a new #GvdbTable from the data in @data. |
| * |
| * An empty or otherwise corrupted data is considered to be a valid |
| * #GvdbTable with no entries. |
| * |
| * You should call gvdb_table_unref() on the return result when you no |
| * longer require it. |
| * |
| * Returns: a new #GvdbTable |
| **/ |
| GvdbTable * |
| gvdb_table_new_from_data (const void *data, |
| gsize data_len, |
| gboolean trusted, |
| gpointer user_data, |
| GvdbRefFunc ref, |
| GDestroyNotify unref, |
| GError **error) |
| { |
| return new_from_data (data, data_len, |
| trusted, |
| user_data, ref, unref, |
| NULL, |
| error); |
| } |
| |
| static gboolean |
| gvdb_table_bloom_filter (GvdbTable *file, |
| guint32 hash_value) |
| { |
| guint32 word, mask; |
| |
| if (file->n_bloom_words == 0) |
| return TRUE; |
| |
| word = (hash_value / 32) % file->n_bloom_words; |
| mask = 1 << (hash_value & 31); |
| mask |= 1 << ((hash_value >> file->bloom_shift) & 31); |
| |
| return (guint32_from_le (file->bloom_words[word]) & mask) == mask; |
| } |
| |
| static gboolean |
| gvdb_table_check_name (GvdbTable *file, |
| struct gvdb_hash_item *item, |
| const gchar *key, |
| guint key_length) |
| { |
| const gchar *this_key; |
| gsize this_size; |
| guint32 parent; |
| |
| this_key = gvdb_table_item_get_key (file, item, &this_size); |
| |
| if G_UNLIKELY (this_key == NULL || this_size > key_length) |
| return FALSE; |
| |
| key_length -= this_size; |
| |
| if G_UNLIKELY (memcmp (this_key, key + key_length, this_size) != 0) |
| return FALSE; |
| |
| parent = guint32_from_le (item->parent); |
| if (key_length == 0 && parent == 0xffffffffu) |
| return TRUE; |
| |
| if G_LIKELY (parent < file->n_hash_items && this_size > 0) |
| return gvdb_table_check_name (file, |
| &file->hash_items[parent], |
| key, key_length); |
| |
| return FALSE; |
| } |
| |
| static const struct gvdb_hash_item * |
| gvdb_table_lookup (GvdbTable *file, |
| const gchar *key, |
| gchar type) |
| { |
| guint32 hash_value = 5381; |
| guint key_length; |
| guint32 bucket; |
| guint32 lastno; |
| guint32 itemno; |
| |
| if G_UNLIKELY (file->n_buckets == 0 || file->n_hash_items == 0) |
| return NULL; |
| |
| for (key_length = 0; key[key_length]; key_length++) |
| hash_value = (hash_value * 33) + ((signed char *) key)[key_length]; |
| |
| if (!gvdb_table_bloom_filter (file, hash_value)) |
| return NULL; |
| |
| bucket = hash_value % file->n_buckets; |
| itemno = guint32_from_le (file->hash_buckets[bucket]); |
| |
| if (bucket == file->n_buckets - 1 || |
| (lastno = guint32_from_le(file->hash_buckets[bucket + 1])) > file->n_hash_items) |
| lastno = file->n_hash_items; |
| |
| while G_LIKELY (itemno < lastno) |
| { |
| struct gvdb_hash_item *item = &file->hash_items[itemno]; |
| |
| if (hash_value == guint32_from_le (item->hash_value)) |
| if G_LIKELY (gvdb_table_check_name (file, item, key, key_length)) |
| if G_LIKELY (item->type == type) |
| return item; |
| |
| itemno++; |
| } |
| |
| return NULL; |
| } |
| |
| static const struct gvdb_hash_item * |
| gvdb_table_get_item (GvdbTable *table, |
| guint32_le item_no) |
| { |
| guint32 item_no_native = guint32_from_le (item_no); |
| |
| if G_LIKELY (item_no_native < table->n_hash_items) |
| return table->hash_items + item_no_native; |
| |
| return NULL; |
| } |
| |
| static gboolean |
| gvdb_table_list_from_item (GvdbTable *table, |
| const struct gvdb_hash_item *item, |
| const guint32_le **list, |
| guint *length) |
| { |
| gsize size; |
| |
| *list = gvdb_table_dereference (table, &item->value.pointer, 4, &size); |
| |
| if G_LIKELY (*list == NULL || size % 4) |
| return FALSE; |
| |
| *length = size / 4; |
| |
| return TRUE; |
| } |
| |
| |
| /** |
| * gvdb_table_list: |
| * @file: a #GvdbTable |
| * @key: a string |
| * |
| * List all of the keys that appear below @key. The nesting of keys |
| * within the hash file is defined by the program that created the hash |
| * file. One thing is constant: each item in the returned array can be |
| * concatenated to @key to obtain the full name of that key. |
| * |
| * It is not possible to tell from this function if a given key is |
| * itself a path, a value, or another hash table; you are expected to |
| * know this for yourself. |
| * |
| * You should call g_strfreev() on the return result when you no longer |
| * require it. |
| * |
| * Returns: a %NULL-terminated string array |
| **/ |
| gchar ** |
| gvdb_table_list (GvdbTable *file, |
| const gchar *key) |
| { |
| const struct gvdb_hash_item *item; |
| const guint32_le *list; |
| gchar **strv; |
| guint length; |
| guint i; |
| |
| if ((item = gvdb_table_lookup (file, key, 'L')) == NULL) |
| return NULL; |
| |
| if (!gvdb_table_list_from_item (file, item, &list, &length)) |
| return NULL; |
| |
| strv = g_new (gchar *, length + 1); |
| for (i = 0; i < length; i++) |
| { |
| guint32 itemno = guint32_from_le (list[i]); |
| |
| if (itemno < file->n_hash_items) |
| { |
| const struct gvdb_hash_item *item; |
| const gchar *string; |
| gsize strsize; |
| |
| item = file->hash_items + itemno; |
| |
| string = gvdb_table_item_get_key (file, item, &strsize); |
| |
| if (string != NULL) |
| strv[i] = g_strndup (string, strsize); |
| else |
| strv[i] = g_malloc0 (1); |
| } |
| else |
| strv[i] = g_malloc0 (1); |
| } |
| |
| strv[i] = NULL; |
| |
| return strv; |
| } |
| |
| /** |
| * gvdb_table_has_value: |
| * @file: a #GvdbTable |
| * @key: a string |
| * |
| * Checks for a value named @key in @file. |
| * |
| * Note: this function does not consider non-value nodes (other hash |
| * tables, for example). |
| * |
| * Returns: %TRUE if @key is in the table |
| **/ |
| gboolean |
| gvdb_table_has_value (GvdbTable *file, |
| const gchar *key) |
| { |
| return gvdb_table_lookup (file, key, 'v') != NULL; |
| } |
| |
| static GVariant * |
| gvdb_table_value_from_item (GvdbTable *table, |
| const struct gvdb_hash_item *item) |
| { |
| GVariant *variant, *value; |
| gconstpointer data; |
| gsize size; |
| |
| data = gvdb_table_dereference (table, &item->value.pointer, 8, &size); |
| |
| if G_UNLIKELY (data == NULL) |
| return NULL; |
| |
| variant = g_variant_new_from_data (G_VARIANT_TYPE_VARIANT, |
| data, size, table->trusted, |
| table->unref_user_data, |
| table->ref_user_data ? table->ref_user_data (table->user_data) : table->user_data); |
| value = g_variant_get_variant (variant); |
| g_variant_unref (variant); |
| |
| return value; |
| } |
| |
| /** |
| * gvdb_table_get_value: |
| * @file: a #GvdbTable |
| * @key: a string |
| * |
| * Looks up a value named @key in @file. |
| * |
| * If the value is not found then %NULL is returned. Otherwise, a new |
| * #GVariant instance is returned. The #GVariant does not depend on the |
| * continued existence of @file. |
| * |
| * You should call g_variant_unref() on the return result when you no |
| * longer require it. |
| * |
| * Returns: a #GVariant, or %NULL |
| **/ |
| GVariant * |
| gvdb_table_get_value (GvdbTable *file, |
| const gchar *key) |
| { |
| const struct gvdb_hash_item *item; |
| GVariant *value; |
| |
| if ((item = gvdb_table_lookup (file, key, 'v')) == NULL) |
| return NULL; |
| |
| value = gvdb_table_value_from_item (file, item); |
| |
| if (value && file->byteswapped) |
| { |
| GVariant *tmp; |
| |
| tmp = g_variant_byteswap (value); |
| g_variant_unref (value); |
| value = tmp; |
| } |
| |
| return value; |
| } |
| |
| /** |
| * gvdb_table_get_raw_value: |
| * @table: a #GvdbTable |
| * @key: a string |
| * |
| * Looks up a value named @key in @file. |
| * |
| * This call is equivalent to gvdb_table_get_value() except that it |
| * never byteswaps the value. |
| * |
| * Returns: a #GVariant, or %NULL |
| **/ |
| GVariant * |
| gvdb_table_get_raw_value (GvdbTable *table, |
| const gchar *key) |
| { |
| const struct gvdb_hash_item *item; |
| |
| if ((item = gvdb_table_lookup (table, key, 'v')) == NULL) |
| return NULL; |
| |
| return gvdb_table_value_from_item (table, item); |
| } |
| |
| /** |
| * gvdb_table_get_table: |
| * @file: a #GvdbTable |
| * @key: a string |
| * |
| * Looks up the hash table named @key in @file. |
| * |
| * The toplevel hash table in a #GvdbTable can contain reference to |
| * child hash tables (and those can contain further references...). |
| * |
| * If @key is not found in @file then %NULL is returned. Otherwise, a |
| * new #GvdbTable is returned, referring to the child hashtable as |
| * contained in the file. This newly-created #GvdbTable does not depend |
| * on the continued existence of @file. |
| * |
| * You should call gvdb_table_unref() on the return result when you no |
| * longer require it. |
| * |
| * Returns: a new #GvdbTable, or %NULL |
| **/ |
| GvdbTable * |
| gvdb_table_get_table (GvdbTable *file, |
| const gchar *key) |
| { |
| const struct gvdb_hash_item *item; |
| GvdbTable *new; |
| |
| item = gvdb_table_lookup (file, key, 'H'); |
| |
| if (item == NULL) |
| return NULL; |
| |
| new = g_slice_new0 (GvdbTable); |
| new->user_data = file->ref_user_data ? file->ref_user_data (file->user_data) : file->user_data; |
| new->ref_user_data = file->ref_user_data; |
| new->unref_user_data = file->unref_user_data; |
| new->byteswapped = file->byteswapped; |
| new->trusted = file->trusted; |
| new->data = file->data; |
| new->size = file->size; |
| new->ref_count = 1; |
| |
| gvdb_table_setup_root (new, &item->value.pointer); |
| |
| return new; |
| } |
| |
| /** |
| * gvdb_table_ref: |
| * @file: a #GvdbTable |
| * |
| * Increases the reference count on @file. |
| * |
| * Returns: a new reference on @file |
| **/ |
| GvdbTable * |
| gvdb_table_ref (GvdbTable *file) |
| { |
| g_atomic_int_inc (&file->ref_count); |
| |
| return file; |
| } |
| |
| /** |
| * gvdb_table_unref: |
| * @file: a #GvdbTable |
| * |
| * Decreases the reference count on @file, possibly freeing it. |
| * |
| * Since: 2.26 |
| **/ |
| void |
| gvdb_table_unref (GvdbTable *file) |
| { |
| if (g_atomic_int_dec_and_test (&file->ref_count)) |
| { |
| if (file->unref_user_data) |
| file->unref_user_data (file->user_data); |
| g_slice_free (GvdbTable, file); |
| } |
| } |
| |
| /** |
| * gvdb_table_is_valid: |
| * @table: a #GvdbTable |
| * |
| * Checks if the table is still valid. |
| * |
| * An on-disk GVDB can be marked as invalid. This happens when the file |
| * has been replaced. The appropriate action is typically to reopen the |
| * file. |
| * |
| * Returns: %TRUE if @table is still valid |
| **/ |
| gboolean |
| gvdb_table_is_valid (GvdbTable *table) |
| { |
| return !!*table->data; |
| } |
| |
| /** |
| * gvdb_table_walk: |
| * @table: a #GvdbTable |
| * @key: a key corresponding to a list |
| * @open_func: the #GvdbWalkOpenFunc |
| * @value_func: the #GvdbWalkValueFunc |
| * @close_func: the #GvdbWalkCloseFunc |
| * @user_data: data to pass to the callbacks |
| * |
| * Looks up the list at @key and iterate over the items in it. |
| * |
| * First, @open_func is called to signal that we are starting to iterate over |
| * the list. Then the list is iterated. When all items in the list have been |
| * iterated over, the @close_func is called. |
| * |
| * When iterating, if a given item in the list is a value then @value_func is |
| * called. |
| * |
| * If a given item in the list is itself a list then @open_func is called. If |
| * that function returns %TRUE then the walk begins iterating the items in the |
| * sublist, until there are no more items, at which point a matching |
| * @close_func call is made. If @open_func returns %FALSE then no iteration of |
| * the sublist occurs and no corresponding @close_func call is made. |
| **/ |
| void |
| gvdb_table_walk (GvdbTable *table, |
| const gchar *key, |
| GvdbWalkOpenFunc open_func, |
| GvdbWalkValueFunc value_func, |
| GvdbWalkCloseFunc close_func, |
| gpointer user_data) |
| { |
| const struct gvdb_hash_item *item; |
| const guint32_le *pointers[64]; |
| const guint32_le *enders[64]; |
| gsize name_lengths[64]; |
| gint index = 0; |
| |
| item = gvdb_table_lookup (table, key, 'L'); |
| name_lengths[0] = 0; |
| pointers[0] = NULL; |
| enders[0] = NULL; |
| goto start_here; |
| |
| while (index) |
| { |
| close_func (name_lengths[index], user_data); |
| index--; |
| |
| while (pointers[index] < enders[index]) |
| { |
| const gchar *name; |
| gsize name_len; |
| |
| item = gvdb_table_get_item (table, *pointers[index]++); |
| start_here: |
| |
| if (item != NULL && |
| (name = gvdb_table_item_get_key (table, item, &name_len))) |
| { |
| if (item->type == 'L') |
| { |
| if (open_func (name, name_len, user_data)) |
| { |
| guint length = 0; |
| |
| index++; |
| g_assert (index < 64); |
| |
| gvdb_table_list_from_item (table, item, |
| &pointers[index], |
| &length); |
| enders[index] = pointers[index] + length; |
| name_lengths[index] = name_len; |
| } |
| } |
| else if (item->type == 'v') |
| { |
| GVariant *value; |
| |
| value = gvdb_table_value_from_item (table, item); |
| |
| if (value != NULL) |
| { |
| if (table->byteswapped) |
| { |
| GVariant *tmp; |
| |
| tmp = g_variant_byteswap (value); |
| g_variant_unref (value); |
| value = tmp; |
| } |
| |
| value_func (name, name_len, value, user_data); |
| g_variant_unref (value); |
| } |
| } |
| } |
| } |
| } |
| } |