/* * 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, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * * Author: Ryan Lortie */ #include "gvdb-reader.h" #include "gvdb-format.h" #include 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) { const struct gvdb_header *header; 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 (file->size < sizeof (struct gvdb_header)) goto invalid; 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 goto invalid; gvdb_table_setup_root (file, &header->root); return file; invalid: if (filename) g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "%s: invalid header", filename); else g_set_error_literal (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_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 * @returns: a new #GvdbTable * * 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. **/ 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 * @returns: a new #GvdbTable * * 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. **/ 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 * @returns: a %NULL-terminated string array * * 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. **/ 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 * @returns: %TRUE if @key is in the table * * Checks for a value named @key in @file. * * Note: this function does not consider non-value nodes (other hash * tables, for example). **/ gboolean gvdb_table_has_value (GvdbTable *file, const gchar *key) { static const struct gvdb_hash_item *item; gsize size; item = gvdb_table_lookup (file, key, 'v'); if (item == NULL) return FALSE; return gvdb_table_dereference (file, &item->value.pointer, 8, &size) != 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 * @returns: a #GVariant, or %NULL * * 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. **/ 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 * @returns: a #GVariant, or %NULL * * Looks up a value named @key in @file. * * This call is equivalent to gvdb_table_get_value() except that it * never byteswaps the value. **/ 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 * @returns: a new #GvdbTable, or %NULL * * 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. **/ 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 * @returns: a new reference on @file * * Increases the reference count 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 * @returns: %TRUE if @table is still valid * * 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. **/ 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') { const guint32_le *dir; guint length; if (gvdb_table_list_from_item (table, item, &dir, &length)) { gint i; /* In order to avoid files with recursive contents * we impose the rule that a directory's data must * follow the data of any directory pointing to * it. * * If we discover that our newly-discovered * directory follows the one we're traversing now * then bail out. */ if (dir <= pointers[index]) continue; if (open_func (name, name_len, user_data)) { index++; g_assert (index < 64); name_lengths[index] = name_len; pointers[index] = dir; enders[index] = pointers[index] + length; } } } 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); } } } } } }