mirror of
https://gitlab.gnome.org/GNOME/glib.git
synced 2025-09-06 16:08:43 +02:00
1238 lines
36 KiB
C
1238 lines
36 KiB
C
/*
|
|
* 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: */
|
|
|