| /* |
| * Copyright © 2010 Codethink Limited |
| * Copyright © 2013 Canonical 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 <desrt@desrt.ca> |
| */ |
| |
| #define _(x)(x) |
| |
| #include <gio/gio.h> |
| #include "gvdb/gvdb-builder.h" |
| #include "strinfo.c" |
| |
| /* Forward declarations {{{1 */ |
| typedef struct _FileRef FileRef; |
| typedef struct _Enum Enum; |
| typedef struct _Schema Schema; |
| typedef struct _Override Override; |
| typedef struct _Key Key; |
| typedef struct _Dir Dir; |
| |
| static Schema * dir_resolve_schema (Dir *dir, |
| const gchar *id, |
| const gchar *detail, |
| const gchar *purpose, |
| const gchar *caller, |
| GError **error); |
| static Enum * dir_resolve_enum (Dir *dir, |
| const gchar *id, |
| gboolean is_flags, |
| const gchar *for_key, |
| const gchar *of_schema, |
| GError **error); |
| static Enum * schema_resolve_enum (Schema *schema, |
| const gchar *id, |
| gboolean is_flags, |
| const gchar *for_key, |
| GError **error); |
| |
| static gboolean dir_add_enum (Dir *dir, |
| GMarkupReader *reader, |
| const gchar *id, |
| Enum *enum_, |
| GError **error); |
| |
| static gboolean dir_add_schema (Dir *dir, |
| GMarkupReader *reader, |
| const gchar *id, |
| Schema *schema, |
| GError **error); |
| |
| static gboolean schema_add_key (Schema *schema, |
| GMarkupReader *reader, |
| const gchar *name, |
| Key *key, |
| GError **error); |
| |
| /* <enum> and <flags> {{{1 */ |
| |
| struct _Enum |
| { |
| Dir *dir; |
| gchar *id; |
| gboolean is_flags; |
| GString *strinfo; |
| }; |
| |
| static void |
| enum_free (gpointer data) |
| { |
| Enum *enum_ = data; |
| |
| g_string_free (enum_->strinfo, TRUE); |
| g_free (enum_->id); |
| |
| g_slice_free (Enum, enum_); |
| } |
| |
| static gboolean |
| enum_parse_value (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Enum *enum_, |
| GError **error) |
| { |
| const gchar *nick, *valuestr; |
| gint64 value; |
| gchar *end; |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRING, "nick", &nick, |
| G_MARKUP_COLLECT_STRING, "value", &valuestr, |
| G_MARKUP_COLLECT_INVALID)) |
| return FALSE; |
| |
| if (nick[0] == '\0' || nick[1] == '\0') |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "nick must be a minimum of 2 characters"); |
| return FALSE; |
| } |
| |
| value = g_ascii_strtoll (valuestr, &end, 0); |
| if (*end || enum_->is_flags ? |
| (value > G_MAXUINT32 || value < 0) : |
| (value > G_MAXINT32 || value < G_MININT32)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "invalid numeric value"); |
| return FALSE; |
| } |
| |
| if (strinfo_builder_contains (enum_->strinfo, nick)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<value nick='%s'/> already specified", nick); |
| return FALSE; |
| } |
| |
| if (strinfo_builder_contains_value (enum_->strinfo, value)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "value='%s' already specified", valuestr); |
| return FALSE; |
| } |
| |
| /* Silently drop the null case if it is mentioned. |
| * It is properly denoted with an empty array. |
| */ |
| if (enum_->is_flags && value == 0) |
| return TRUE; |
| |
| if (enum_->is_flags && (value & (value - 1))) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "flags values must have at most 1 bit set"); |
| return FALSE; |
| } |
| |
| /* Since we reject exact duplicates of value='' and we only allow one |
| * bit to be set, it's not possible to have overlaps. |
| * |
| * If we loosen the one-bit-set restriction we need an overlap check. |
| */ |
| |
| strinfo_builder_append_item (enum_->strinfo, nick, value); |
| |
| return g_markup_reader_expect_end (reader, NULL, error); |
| } |
| |
| static gboolean |
| enum_parse (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Dir *dir, |
| GError **error) |
| { |
| Enum *enum_; |
| |
| enum_ = g_slice_new0 (Enum); |
| enum_->is_flags = g_markup_reader_is_start_element (reader, "flags"); |
| enum_->strinfo = g_string_new (NULL); |
| |
| g_assert (enum_->is_flags || g_markup_reader_is_start_element (reader, "enum")); |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRDUP, "id", &enum_->id, |
| G_MARKUP_COLLECT_INVALID)) |
| goto error; |
| |
| if (!g_markup_reader_collect_elements (reader, cancellable, enum_, error, "value", enum_parse_value, NULL)) |
| goto error; |
| |
| if (!dir_add_enum (dir, reader, enum_->id, enum_, error)) |
| goto error; |
| |
| return TRUE; |
| |
| error: |
| enum_free (enum_); |
| |
| return FALSE; |
| } |
| |
| /* <key> {{{1 */ |
| |
| struct _Key |
| { |
| Schema *schema; |
| const gchar *name; |
| |
| gchar *type_string; |
| GVariantType *type; |
| gchar *enum_name; |
| Enum *enum_; |
| gchar *flags_name; |
| Enum *flags; |
| |
| gchar l10n; |
| gchar *l10n_context; |
| gchar *default_text; |
| GVariant *default_value; |
| |
| GString *strinfo; |
| gboolean is_enum; |
| gboolean is_flags; |
| |
| GVariant *minimum; |
| GVariant *maximum; |
| |
| gboolean has_choices; |
| gboolean has_aliases; |
| gboolean is_override; |
| |
| gboolean checked; |
| GVariant *serialised; |
| }; |
| |
| static void |
| key_free (gpointer data) |
| { |
| Key *key = data; |
| |
| g_slice_free (Key, key); |
| } |
| |
| static gboolean |
| key_resolve (Key *key, |
| GError **error) |
| { |
| if (key->enum_name) |
| { |
| key->enum_ = schema_resolve_enum (key->schema, key->enum_name, FALSE, key->name, error); |
| if (key->enum_ == NULL) |
| return FALSE; |
| } |
| |
| if (key->flags_name) |
| { |
| key->flags = schema_resolve_enum (key->schema, key->flags_name, TRUE, key->name, error); |
| if (key->flags == NULL) |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| key_parse_default (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Key *key, |
| GError **error) |
| { |
| if (key->default_value) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "<default/> must be specified exactly once"); |
| return FALSE; |
| } |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "l10n", &key->l10n, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "context", &key->l10n_context, |
| G_MARKUP_COLLECT_INVALID)) |
| return FALSE; |
| |
| key->default_text = g_markup_reader_collect_text (reader, cancellable, error); |
| |
| return key->default_text != NULL; |
| } |
| |
| static gboolean |
| key_parse_range (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Key *key, |
| GError **error) |
| { |
| const gchar *min_str, *max_str; |
| const struct { |
| const gchar type; |
| const gchar *min; |
| const gchar *max; |
| } table[] = { |
| { 'y', "0", "255" }, |
| { 'n', "-32768", "32767" }, |
| { 'q', "0", "65535" }, |
| { 'i', "-2147483648", "2147483647" }, |
| { 'u', "0", "4294967295" }, |
| { 'x', "-9223372036854775808", "9223372036854775807" }, |
| { 't', "0", "18446744073709551615" }, |
| { 'd', "-inf", "inf" }, |
| }; |
| gboolean type_ok = FALSE; |
| gint i; |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "min", &min_str, |
| G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "max", &max_str, |
| G_MARKUP_COLLECT_INVALID)) |
| return FALSE; |
| |
| if (key->minimum) |
| { |
| g_set_error_literal (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<range/> already specified for this key"); |
| return FALSE; |
| } |
| |
| for (i = 0; i < G_N_ELEMENTS (table); i++) |
| if (*(char *) key->type == table[i].type) |
| { |
| min_str = min_str ? min_str : table[i].min; |
| max_str = max_str ? max_str : table[i].max; |
| type_ok = TRUE; |
| break; |
| } |
| |
| if (!type_ok) |
| { |
| g_set_error (error, G_MARKUP_ERROR, |
| G_MARKUP_ERROR_INVALID_CONTENT, |
| "<range> not allowed for keys of type '%s'", key->type_string); |
| return FALSE; |
| } |
| |
| key->minimum = g_variant_parse (key->type, min_str, NULL, NULL, error); |
| if (key->minimum == NULL) |
| return FALSE; |
| |
| key->maximum = g_variant_parse (key->type, max_str, NULL, NULL, error); |
| if (key->maximum == NULL) |
| return FALSE; |
| |
| if (g_variant_compare (key->minimum, key->maximum) > 0) |
| { |
| g_set_error (error, G_MARKUP_ERROR, |
| G_MARKUP_ERROR_INVALID_CONTENT, |
| "<range> specified minimum is greater than maxmimum"); |
| return FALSE; |
| } |
| |
| return g_markup_reader_expect_end (reader, cancellable, error); |
| } |
| |
| static gboolean |
| key_parse_choice (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Key *key, |
| GError **error) |
| { |
| const gchar *value; |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRING, "value", &value, |
| G_MARKUP_COLLECT_INVALID)) |
| return FALSE; |
| |
| if (strinfo_builder_contains (key->strinfo, value)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<choice value='%s'/> already given", value); |
| return FALSE; |
| } |
| |
| strinfo_builder_append_item (key->strinfo, value, 0); |
| key->has_choices = TRUE; |
| |
| return g_markup_reader_expect_end (reader, cancellable, error); |
| } |
| |
| static gboolean |
| key_parse_choices (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Key *key, |
| GError **error) |
| { |
| if (!g_markup_reader_collect_attributes (reader, error, G_MARKUP_COLLECT_INVALID, NULL)) |
| return FALSE; |
| |
| if (!key->type_string || (!g_str_equal (key->type_string, "s") && |
| !g_str_equal (key->type_string, "as") && |
| !g_str_equal (key->type_string, "ms"))) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<choices> only allowed for keys with type 's', 'as' or 'ms'"); |
| return FALSE; |
| } |
| |
| key->strinfo = g_string_new (NULL); |
| |
| if (!g_markup_reader_collect_elements (reader, cancellable, key, error, "choice", key_parse_choice, NULL)) |
| return FALSE; |
| |
| if (!key->has_choices) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<choices> must contain at least one <choice>"); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| key_parse_aliases (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Key *key, |
| GError **error) |
| { |
| return TRUE; |
| } |
| |
| static gboolean |
| ignore_text (GMarkupReader *reader, |
| GCancellable *cancellable, |
| gpointer user_data, |
| GError **error) |
| { |
| while (g_markup_reader_advance (reader, cancellable, error) && g_markup_reader_is_text (reader)) |
| ; |
| |
| if (g_markup_reader_is_end_element (reader)) |
| return TRUE; |
| |
| g_markup_reader_unexpected (reader, error); |
| |
| return FALSE; |
| } |
| |
| static gboolean |
| key_parse (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Schema *schema, |
| GError **error) |
| { |
| Key *key; |
| |
| g_assert (g_markup_reader_is_start_element (reader, "key")); |
| |
| key = g_slice_new0 (Key); |
| key->schema = schema; |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRDUP, |
| "name", &key->name, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "type", &key->type_string, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "enum", &key->enum_name, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "flags", &key->flags_name, |
| G_MARKUP_COLLECT_INVALID)) |
| goto error; |
| |
| if ((key->type_string != NULL) + (key->enum_name != NULL) + (key->flags_name != NULL) != 1) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_MISSING_ATTRIBUTE, |
| _("exactly one of 'type', 'enum' or 'flags' must be specified as an attribute to <key>")); |
| return FALSE; |
| } |
| |
| if (key->type_string) |
| { |
| if (!g_variant_type_string_is_valid (key->type_string)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| _("invalid GVariant type string '%s'"), key->type_string); |
| return FALSE; |
| } |
| |
| key->type = g_variant_type_new (key->type_string); |
| } |
| else if (key->enum_name) |
| key->type = g_variant_type_copy (G_VARIANT_TYPE_STRING); |
| else /* flags */ |
| key->type = g_variant_type_copy (G_VARIANT_TYPE_STRING_ARRAY); |
| |
| if (!g_markup_reader_collect_elements (reader, cancellable, key, error, |
| "summary", ignore_text, |
| "description", ignore_text, |
| "default", key_parse_default, |
| "range", key_parse_range, |
| "choices", key_parse_choices, |
| "aliases", key_parse_aliases, |
| NULL)) |
| goto error; |
| |
| if (!schema_add_key (schema, reader, key->name, key, error)) |
| goto error; |
| |
| return TRUE; |
| |
| error: |
| return FALSE; |
| } |
| |
| /* <schema> {{{1 */ |
| |
| struct _Override |
| { |
| Schema *schema; |
| gchar *name; |
| |
| gchar *text; |
| gchar *context; |
| gchar *l10n; |
| }; |
| |
| static void |
| override_free (gpointer data) |
| { |
| Override *override = data; |
| |
| g_free (override->name); |
| g_free (override->text); |
| g_free (override->context); |
| g_free (override->l10n); |
| |
| g_slice_free (Override, override); |
| } |
| |
| struct _Schema |
| { |
| Dir *dir; |
| gchar *id; |
| |
| gboolean has_translated; |
| gboolean resolved; |
| |
| gchar *gettext_domain; |
| gchar *path; |
| |
| const gchar *extends_name; |
| Schema *extends; |
| const gchar *list_of_name; |
| Schema *list_of; |
| |
| GHashTable *children_names; |
| GHashTable *children; |
| GHashTable *keys; |
| GHashTable *overrides; |
| }; |
| |
| static void |
| schema_free (gpointer data) |
| { |
| Schema *schema = data; |
| |
| g_slice_free (Schema, schema); |
| } |
| |
| static gboolean |
| schema_resolve (Schema *schema, |
| GError **error) |
| { |
| static GSList *now_resolving; /* we don't unwind this properly in case of error */ |
| GSList me = { schema }; |
| |
| if (schema->resolved) |
| return TRUE; |
| |
| if (g_slist_find (now_resolving, schema)) |
| { |
| GSList *node; |
| GString *str; |
| |
| /* We have a reference cycle. */ |
| str = g_string_new ("Reference cycle detected: '%s'"); |
| for (node = now_resolving; node; node = node->next) |
| { |
| Schema *s = node->data; |
| |
| g_string_append_printf (str, "<- '%s'", s->id); |
| |
| /* Stop once we get back to ourselves. */ |
| if (s == schema) |
| break; |
| } |
| |
| g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, str->str); |
| g_string_free (str, TRUE); |
| return FALSE; |
| } |
| |
| me.next = now_resolving; |
| now_resolving = &me; |
| |
| if (schema->extends_name) |
| { |
| schema->extends = dir_resolve_schema (schema->dir, schema->extends_name, |
| "extends", "reference", schema->id, |
| error); |
| if (!schema->extends) |
| return FALSE; |
| } |
| |
| if (schema->list_of_name) |
| { |
| schema->list_of = dir_resolve_schema (schema->dir, schema->list_of_name, |
| "list-of", "reference", schema->id, |
| error); |
| if (!schema->list_of) |
| return FALSE; |
| } |
| |
| if (schema->children_names) |
| { |
| GHashTableIter iter; |
| gpointer name, id; |
| |
| schema->children = g_hash_table_new (g_str_hash, g_str_equal); |
| |
| g_hash_table_iter_init (&iter, schema->children_names); |
| while (g_hash_table_iter_next (&iter, &name, &id)) |
| { |
| Schema *child; |
| |
| child = dir_resolve_schema (schema->dir, id, |
| name, "child", schema->id, |
| error); |
| if (!child) |
| return FALSE; |
| |
| g_hash_table_insert (schema->children, name, child); |
| } |
| } |
| |
| if (schema->keys) |
| { |
| GHashTableIter iter; |
| gpointer key; |
| |
| g_hash_table_iter_init (&iter, schema->keys); |
| while (g_hash_table_iter_next (&iter, NULL, &key)) |
| if (!key_resolve (key, error)) |
| return FALSE; |
| } |
| |
| now_resolving = me.next; |
| |
| return TRUE; |
| } |
| |
| static Enum * |
| schema_resolve_enum (Schema *schema, |
| const gchar *id, |
| gboolean is_flags, |
| const gchar *for_key, |
| GError **error) |
| { |
| return dir_resolve_enum (schema->dir, id, is_flags, for_key, schema->id, error); |
| } |
| |
| static gboolean |
| schema_add_key (Schema *schema, |
| GMarkupReader *reader, |
| const gchar *name, |
| Key *key, |
| GError **error) |
| { |
| if (g_hash_table_contains (schema->keys, name)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<key name='%s'/> already defined in <schema id='%s'/>", name, schema->id); |
| return FALSE; |
| } |
| |
| g_hash_table_insert (schema->keys, g_strdup (name), key); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| schema_parse_child (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Schema *schema, |
| GError **error) |
| { |
| const gchar *name, *schema_id; |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRING, "name", &name, |
| G_MARKUP_COLLECT_STRING, "schema", &schema_id, |
| G_MARKUP_COLLECT_INVALID)) |
| return FALSE; |
| |
| if (g_hash_table_contains (schema->children_names, name)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<child name='%s'/> appears twice in <schema id='%s'/>", |
| name, schema->id); |
| return FALSE; |
| } |
| |
| g_hash_table_insert (schema->children_names, g_strdup (name), g_strdup (schema_id)); |
| |
| if (!g_markup_reader_expect_end (reader, cancellable, error)) |
| return FALSE; |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| schema_parse_override (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Schema *schema, |
| GError **error) |
| { |
| Override *override; |
| |
| override = g_slice_new0 (Override); |
| override->schema = schema; |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRING, "name", &override->name, |
| G_MARKUP_COLLECT_STRING, "l10n", &override->l10n, |
| G_MARKUP_COLLECT_STRING, "context", &override->context, |
| G_MARKUP_COLLECT_INVALID)) |
| goto error; |
| |
| if (schema->overrides == NULL) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<override name='%s'/> appears within <schema id='%s'/> that is not extending another", |
| override->name, schema->id); |
| goto error; |
| } |
| |
| if (g_hash_table_contains (schema->overrides, override->name)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<override name='%s'/> appears twice in <schema id='%s'/>", override->name, schema->id); |
| return FALSE; |
| } |
| |
| override->text = g_markup_reader_collect_text (reader, cancellable, error); |
| if (!override->text) |
| goto error; |
| |
| g_hash_table_insert (schema->overrides, g_strdup (override->name), override); |
| |
| return TRUE; |
| |
| error: |
| override_free (override); |
| return FALSE; |
| } |
| |
| static gboolean |
| schema_parse (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Dir *dir, |
| GError **error) |
| { |
| Schema *schema; |
| |
| g_assert (g_markup_reader_is_start_element (reader, "schema")); |
| |
| schema = g_slice_new0 (Schema); |
| schema->dir = dir; |
| schema->keys = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, key_free); |
| schema->children_names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRDUP, |
| "id", &schema->id, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "path", &schema->path, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "gettext-domain", &schema->gettext_domain, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "extends", &schema->extends_name, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "list-of", &schema->list_of_name, |
| G_MARKUP_COLLECT_INVALID)) |
| goto error; |
| |
| if (schema->extends_name) |
| schema->overrides = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, override_free); |
| |
| if (!g_markup_reader_collect_elements (reader, cancellable, schema, error, |
| "key", key_parse, |
| "child", schema_parse_child, |
| "override", schema_parse_override, |
| NULL)) |
| goto error; |
| |
| if (!dir_add_schema (dir, reader, schema->id, schema, error)) |
| goto error; |
| |
| return TRUE; |
| |
| error: |
| return FALSE; |
| } |
| |
| /* Directory handling {{{1 */ |
| |
| struct _Dir |
| { |
| Dir *parent_dir; |
| const gchar *path; |
| GHashTable *excludes; |
| gboolean parsed; |
| |
| /* temporarily set while parsing <schemalist> */ |
| gchar *gettext_domain; |
| |
| GHashTable *schemas; |
| GHashTable *enums; |
| }; |
| |
| static gboolean |
| dir_add_enum (Dir *dir, |
| GMarkupReader *reader, |
| const gchar *id, |
| Enum *enum_, |
| GError **error) |
| { |
| if (g_hash_table_contains (dir->enums, id)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<enum id='%s'/> or <flags id='%s'/> already defined in directory %s", |
| id, id, dir->path); |
| return FALSE; |
| } |
| |
| g_hash_table_insert (dir->enums, g_strdup (id), enum_); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| dir_add_schema (Dir *dir, |
| GMarkupReader *reader, |
| const gchar *id, |
| Schema *schema, |
| GError **error) |
| { |
| if (g_hash_table_contains (dir->schemas, id)) |
| { |
| g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, |
| "<schema id='%s'/> already defined in directory %s", id, dir->path); |
| return FALSE; |
| } |
| |
| g_hash_table_insert (dir->schemas, g_strdup (id), schema); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| dir_parse_schemalist (GMarkupReader *reader, |
| GCancellable *cancellable, |
| Dir *dir, |
| GError **error) |
| { |
| gboolean ok; |
| |
| if (!g_markup_reader_collect_attributes (reader, error, |
| G_MARKUP_COLLECT_STRDUP | G_MARKUP_COLLECT_OPTIONAL, |
| "gettext-domain", &dir->gettext_domain, |
| G_MARKUP_COLLECT_INVALID)) |
| return FALSE; |
| |
| ok = g_markup_reader_collect_elements (reader, cancellable, dir, error, |
| "schema", schema_parse, "enum", enum_parse, "flags", enum_parse, NULL); |
| |
| g_free (dir->gettext_domain); |
| dir->gettext_domain = NULL; |
| |
| return ok; |
| } |
| |
| static gboolean |
| dir_parse_one_file (Dir *dir, |
| const gchar *filename, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| GFileInputStream *stream; |
| GMarkupReader *reader; |
| GFile *file; |
| gboolean ok; |
| |
| file = g_file_new_for_path (filename); |
| stream = g_file_read (file, cancellable, error); |
| g_object_unref (file); |
| |
| if (!stream) |
| return FALSE; |
| |
| reader = g_markup_reader_new (G_INPUT_STREAM (stream), |
| G_MARKUP_PREFIX_ERROR_POSITION | |
| G_MARKUP_TREAT_CDATA_AS_TEXT | |
| G_MARKUP_IGNORE_QUALIFIED | |
| G_MARKUP_IGNORE_PASSTHROUGH); |
| g_object_unref (stream); |
| |
| ok = g_markup_reader_collect_elements (reader, cancellable, dir, error, "schemalist", dir_parse_schemalist, NULL); |
| |
| g_object_unref (reader); |
| |
| if (!ok) |
| g_prefix_error (error, "%s: ", filename); |
| |
| return ok; |
| } |
| |
| static Dir *compile_dir; |
| |
| static gboolean |
| dir_parse (Dir *dir, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| GError *local_error = NULL; |
| gboolean ok = TRUE; |
| const gchar *name; |
| GDir *dirp; |
| |
| g_assert (!dir->parsed); |
| |
| dirp = g_dir_open (dir->path, 0, &local_error); |
| |
| dir->schemas = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, schema_free); |
| dir->enums = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, enum_free); |
| |
| if (!dirp) |
| { |
| /* If we get a G_FILE_ERROR_NOENT then ignore it, unless it was |
| * for the toplevel directory (ie: the one we were asked to |
| * compile). |
| */ |
| if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT) && dir != compile_dir) |
| { |
| g_error_free (local_error); |
| return TRUE; |
| } |
| |
| g_propagate_error (error, local_error); |
| return FALSE; |
| } |
| |
| while (ok && (name = g_dir_read_name (dirp))) |
| { |
| gchar *fullname; |
| |
| if (!g_str_has_suffix (name, ".xml")) |
| continue; |
| |
| if (dir->excludes && g_hash_table_contains (dir->excludes, name)) |
| continue; |
| |
| fullname = g_build_filename (dir->path, name, NULL); |
| ok = dir_parse_one_file (dir, fullname, cancellable, error); |
| g_free (fullname); |
| } |
| |
| g_dir_close (dirp); |
| |
| dir->parsed = ok; |
| |
| return ok; |
| } |
| |
| static Dir * |
| dir_new (const gchar *path, |
| Dir *parent_dir) |
| { |
| Dir *dir; |
| |
| dir = g_slice_new0 (Dir); |
| dir->path = path; |
| dir->parent_dir = parent_dir; |
| |
| return dir; |
| } |
| |
| static void |
| setup_compile_dir (const gchar *directory, |
| const gchar * const *excluded) |
| { |
| const gchar * const * system_dirs; |
| gint i, n; |
| |
| /* If we are compiling a system directory we want to include all of |
| * the directories that come before it. |
| * |
| * If we are compiling a non-system directory then we want to include |
| * all of the system directories before it. |
| * |
| * For example, of XDG_DATA_DIRS=/a:/b:/c then: |
| * |
| * - for building /a, we want a path of /a, /b, /c |
| * |
| * - for building /b, we want a path of /b, /c |
| * |
| * - for building /c, we want a path of /c |
| * |
| * - for building /x we want a path of /x, /a, /b, /c |
| */ |
| system_dirs = g_get_system_data_dirs (); |
| n = g_strv_length ((gchar **) system_dirs); |
| |
| /* We're building a linked list -- start at the end */ |
| for (i = n - 1; i >= 0; i--) |
| { |
| /* If we see our own directory then stop -- we don't want any more |
| * system dirs. We will deal with our directory below. |
| */ |
| if (g_str_equal (system_dirs[i], directory)) |
| break; |
| |
| compile_dir = dir_new (system_dirs[i], compile_dir); |
| } |
| |
| compile_dir = dir_new (directory, compile_dir); |
| } |
| |
| Schema * |
| dir_resolve_schema (Dir *dir, |
| const gchar *id, |
| const gchar *detail, |
| const gchar *purpose, |
| const gchar *caller, |
| GError **error) |
| { |
| Schema *schema = NULL; |
| Dir *d; |
| |
| g_assert (dir); |
| |
| for (d = dir; d; d = d->parent_dir) |
| { |
| if (!dir->parsed) |
| if (!dir_parse (dir, NULL, error)) |
| return FALSE; |
| |
| schema = g_hash_table_lookup (dir->schemas, id); |
| |
| if (schema) |
| break; |
| } |
| |
| if (!schema) |
| { |
| g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, |
| "Unable to locate schema '%s' needed for '%s' %s of %s", |
| id, detail, purpose, caller); |
| return NULL; |
| } |
| |
| if (!schema_resolve (schema, error)) |
| return NULL; |
| |
| return schema; |
| } |
| |
| Enum * |
| dir_resolve_enum (Dir *dir, |
| const gchar *id, |
| gboolean is_flags, |
| const gchar *for_key, |
| const gchar *of_schema, |
| GError **error) |
| { |
| Enum *enum_ = NULL; |
| Dir *d; |
| |
| g_assert (dir); |
| |
| for (d = dir; d; d = d->parent_dir) |
| { |
| if (!dir->parsed) |
| if (!dir_parse (dir, NULL, error)) |
| return FALSE; |
| |
| enum_ = g_hash_table_lookup (dir->enums, id); |
| |
| if (enum_) |
| break; |
| } |
| |
| if (!enum_ || enum_->is_flags != is_flags) |
| { |
| g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, |
| "Unable to locate <%s id='%s'/> needed for key '%s' of schema '%s'", |
| is_flags ? "flags" : "enum", id, for_key, of_schema); |
| return NULL; |
| } |
| |
| return enum_; |
| } |
| |
| static gboolean |
| dir_resolve (Dir *dir, |
| GError **error) |
| { |
| GHashTableIter iter; |
| gpointer schema; |
| |
| if (!dir_parse (dir, NULL, error)) |
| return FALSE; |
| |
| g_hash_table_iter_init (&iter, dir->schemas); |
| while (g_hash_table_iter_next (&iter, NULL, &schema)) |
| if (!schema_resolve (schema, error)) |
| return FALSE; |
| |
| return TRUE; |
| } |
| |
| static GVariant * |
| key_compile (Key *key, |
| GError **error) |
| { |
| GVariantBuilder builder; |
| |
| g_variant_builder_init (&builder, G_VARIANT_TYPE_TUPLE); |
| |
| return g_variant_builder_end (&builder); |
| } |
| |
| static GHashTable * |
| schema_compile (Schema *schema, |
| GError **error) |
| { |
| GHashTable *compiled_schema; |
| GvdbItem *root_item; |
| GHashTableIter iter; |
| gpointer key, name; |
| gpointer id; |
| |
| compiled_schema = gvdb_hash_table_new (NULL, NULL); |
| root_item = gvdb_hash_table_insert (compiled_schema, ""); |
| |
| if (schema->path) |
| gvdb_hash_table_insert_string (compiled_schema, ".path", schema->path); |
| |
| if (schema->list_of) |
| gvdb_hash_table_insert_string (compiled_schema, ".list-of", schema->list_of_name); |
| |
| if (schema->extends) |
| gvdb_hash_table_insert_string (compiled_schema, ".extends", schema->extends_name); |
| |
| /* Only store the gettext domain if a key was actually translated */ |
| if (schema->has_translated) |
| gvdb_hash_table_insert_string (compiled_schema, ".gettext-domain", schema->gettext_domain); |
| |
| g_hash_table_iter_init (&iter, schema->keys); |
| while (g_hash_table_iter_next (&iter, &name, &key)) |
| { |
| GVariant *compiled_key; |
| GvdbItem *key_item; |
| |
| compiled_key = key_compile (key, error); |
| if (!compiled_key) |
| goto error; |
| |
| key_item = gvdb_hash_table_insert (compiled_schema, name); |
| gvdb_item_set_parent (key_item, root_item); |
| gvdb_item_set_value (key_item, compiled_key); |
| } |
| |
| g_hash_table_iter_init (&iter, schema->children); |
| while (g_hash_table_iter_next (&iter, &name, &id)) |
| { |
| GvdbItem *child_item; |
| |
| child_item = gvdb_hash_table_insert (compiled_schema, name); |
| gvdb_item_set_parent (child_item, root_item); |
| gvdb_item_set_value (child_item, g_variant_new_string (id)); |
| } |
| |
| return compiled_schema; |
| error: |
| g_assert_not_reached (); |
| } |
| |
| static GHashTable * |
| dir_compile (Dir *dir, |
| GError **error) |
| { |
| GHashTable *compiled_dir; |
| GvdbItem *root_item; |
| GHashTableIter iter; |
| gpointer schema, id; |
| |
| compiled_dir = gvdb_hash_table_new (NULL, NULL); |
| root_item = gvdb_hash_table_insert (compiled_dir, ""); |
| |
| g_hash_table_iter_init (&iter, dir->schemas); |
| while (g_hash_table_iter_next (&iter, &id, &schema)) |
| { |
| GHashTable *compiled_schema; |
| GvdbItem *schema_item; |
| |
| compiled_schema = schema_compile (schema, error); |
| if (!compiled_schema) |
| goto error; |
| |
| schema_item = gvdb_hash_table_insert (compiled_dir, id); |
| gvdb_item_set_parent (schema_item, root_item); |
| gvdb_item_set_hash_table (schema_item, compiled_schema); |
| } |
| |
| return compiled_dir; |
| error: |
| g_assert_not_reached (); |
| } |
| |
| int |
| main (void) |
| { |
| GError *error = NULL; |
| setup_compile_dir ("/home/desrt/.cache/jhbuild/install/share/glib-2.0/schemas", NULL); |
| if (!dir_resolve (compile_dir, &error)) |
| g_printerr ("%s\n", error->message); |
| return error ? 1 : 0; |
| } |
| |
| /* Epilogue {{{1 */ |
| |
| /* vim:set foldmethod=marker: */ |
| |