aboutsummaryrefslogtreecommitdiff
path: root/src/registry.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/registry.c')
-rw-r--r--src/registry.c1198
1 files changed, 1198 insertions, 0 deletions
diff --git a/src/registry.c b/src/registry.c
new file mode 100644
index 0000000..d3d95f5
--- /dev/null
+++ b/src/registry.c
@@ -0,0 +1,1198 @@
+/*
+ * Copyright © 2020 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <libxml/parser.h>
+
+#include "xkbcommon/xkbregistry.h"
+#include "utils.h"
+#include "util-list.h"
+
+struct rxkb_object;
+
+typedef void (*destroy_func_t)(struct rxkb_object *object);
+
+/**
+ * All our objects are refcounted and are linked to iterate through them.
+ * Abstract those bits away into a shared parent class so we can generate
+ * most of the functions through macros.
+ */
+struct rxkb_object {
+ struct rxkb_object *parent;
+ uint32_t refcount;
+ struct list link;
+ destroy_func_t destroy;
+};
+
+struct rxkb_iso639_code {
+ struct rxkb_object base;
+ char *code;
+};
+
+struct rxkb_iso3166_code {
+ struct rxkb_object base;
+ char *code;
+};
+
+enum context_state {
+ CONTEXT_NEW,
+ CONTEXT_PARSED,
+ CONTEXT_FAILED,
+};
+
+struct rxkb_context {
+ struct rxkb_object base;
+ enum context_state context_state;
+
+ bool load_extra_rules_files;
+
+ struct list models; /* list of struct rxkb_models */
+ struct list layouts; /* list of struct rxkb_layouts */
+ struct list option_groups; /* list of struct rxkb_option_group */
+
+ darray(char *) includes;
+
+
+ ATTR_PRINTF(3, 0) void (*log_fn)(struct rxkb_context *ctx,
+ enum rxkb_log_level level,
+ const char *fmt, va_list args);
+ enum rxkb_log_level log_level;
+
+ void *userdata;
+};
+
+struct rxkb_model {
+ struct rxkb_object base;
+
+ char *name;
+ char *vendor;
+ char *description;
+ enum rxkb_popularity popularity;
+};
+
+struct rxkb_layout {
+ struct rxkb_object base;
+
+ char *name;
+ char *brief;
+ char *description;
+ char *variant;
+ enum rxkb_popularity popularity;
+
+ struct list iso639s; /* list of struct rxkb_iso639_code */
+ struct list iso3166s; /* list of struct rxkb_iso3166_code */
+};
+
+struct rxkb_option_group {
+ struct rxkb_object base;
+
+ bool allow_multiple;
+ struct list options; /* list of struct rxkb_options */
+ char *name;
+ char *description;
+ enum rxkb_popularity popularity;
+};
+
+struct rxkb_option {
+ struct rxkb_object base;
+
+ char *name;
+ char *brief;
+ char *description;
+ enum rxkb_popularity popularity;
+};
+
+static bool
+parse(struct rxkb_context *ctx, const char *path,
+ enum rxkb_popularity popularity);
+
+static void
+rxkb_log(struct rxkb_context *ctx, enum rxkb_log_level level,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ if (ctx->log_level < level)
+ return;
+
+ va_start(args, fmt);
+ ctx->log_fn(ctx, level, fmt, args);
+ va_end(args);
+}
+
+/*
+ * The format is not part of the argument list in order to avoid the
+ * "ISO C99 requires rest arguments to be used" warning when only the
+ * format is supplied without arguments. Not supplying it would still
+ * result in an error, though.
+ */
+#define log_dbg(ctx, ...) \
+ rxkb_log((ctx), RXKB_LOG_LEVEL_DEBUG, __VA_ARGS__)
+#define log_info(ctx, ...) \
+ rxkb_log((ctx), RXKB_LOG_LEVEL_INFO, __VA_ARGS__)
+#define log_warn(ctx, ...) \
+ rxkb_log((ctx), RXKB_LOG_LEVEL_WARNING, __VA_ARGS__)
+#define log_err(ctx, ...) \
+ rxkb_log((ctx), RXKB_LOG_LEVEL_ERROR, __VA_ARGS__)
+#define log_wsgo(ctx, ...) \
+ rxkb_log((ctx), RXKB_LOG_LEVEL_CRITICAL, __VA_ARGS__)
+
+
+#define DECLARE_REF_UNREF_FOR_TYPE(type_) \
+XKB_EXPORT struct type_ * type_##_ref(struct type_ *object) { \
+ rxkb_object_ref(&object->base); \
+ return object; \
+} \
+XKB_EXPORT struct type_ * type_##_unref(struct type_ *object) { \
+ if (!object) return NULL; \
+ return rxkb_object_unref(&object->base); \
+}
+
+#define DECLARE_CREATE_FOR_TYPE(type_) \
+static inline struct type_ * type_##_create(struct rxkb_object *parent) { \
+ struct type_ *t = calloc(1, sizeof *t); \
+ if (t) \
+ rxkb_object_init(&t->base, parent, (destroy_func_t)type_##_destroy); \
+ return t; \
+}
+
+#define DECLARE_TYPED_GETTER_FOR_TYPE(type_, field_, rtype_) \
+XKB_EXPORT rtype_ type_##_get_##field_(struct type_ *object) { \
+ return object->field_; \
+}
+
+#define DECLARE_GETTER_FOR_TYPE(type_, field_) \
+ DECLARE_TYPED_GETTER_FOR_TYPE(type_, field_, const char*)
+
+#define DECLARE_FIRST_NEXT_FOR_TYPE(type_, parent_type_, parent_field_) \
+XKB_EXPORT struct type_ * type_##_first(struct parent_type_ *parent) { \
+ struct type_ *o = NULL; \
+ if (!list_empty(&parent->parent_field_)) \
+ o = list_first_entry(&parent->parent_field_, o, base.link); \
+ return o; \
+} \
+XKB_EXPORT struct type_ * \
+type_##_next(struct type_ *o) \
+{ \
+ struct parent_type_ *parent; \
+ struct type_ *next; \
+ parent = container_of(o->base.parent, struct parent_type_, base); \
+ next = list_first_entry(&o->base.link, o, base.link); \
+ if (list_is_last(&parent->parent_field_, &o->base.link)) \
+ return NULL; \
+ return next; \
+}
+
+static void
+rxkb_object_init(struct rxkb_object *object, struct rxkb_object *parent, destroy_func_t destroy)
+{
+ object->refcount = 1;
+ object->destroy = destroy;
+ object->parent = parent;
+ list_init(&object->link);
+}
+
+static void
+rxkb_object_destroy(struct rxkb_object *object)
+{
+ if (object->destroy)
+ object->destroy(object);
+ list_remove(&object->link);
+ free(object);
+}
+
+static void *
+rxkb_object_ref(struct rxkb_object *object)
+{
+ assert(object->refcount >= 1);
+ ++object->refcount;
+ return object;
+}
+
+static void *
+rxkb_object_unref(struct rxkb_object *object)
+{
+ assert(object->refcount >= 1);
+ if (--object->refcount == 0)
+ rxkb_object_destroy(object);
+ return NULL;
+}
+
+static void
+rxkb_iso639_code_destroy(struct rxkb_iso639_code *code)
+{
+ free(code->code);
+}
+
+XKB_EXPORT struct rxkb_iso639_code *
+rxkb_layout_get_iso639_first(struct rxkb_layout *layout)
+{
+ struct rxkb_iso639_code *code = NULL;
+
+ if (!list_empty(&layout->iso639s))
+ code = list_first_entry(&layout->iso639s, code, base.link);
+
+ return code;
+}
+
+XKB_EXPORT struct rxkb_iso639_code *
+rxkb_iso639_code_next(struct rxkb_iso639_code *code)
+{
+ struct rxkb_iso639_code *next = NULL;
+ struct rxkb_layout *layout;
+
+ layout = container_of(code->base.parent, struct rxkb_layout, base);
+
+ if (list_is_last(&layout->iso639s, &code->base.link))
+ return NULL;
+
+ next = list_first_entry(&code->base.link, code, base.link);
+
+ return next;
+}
+
+DECLARE_REF_UNREF_FOR_TYPE(rxkb_iso639_code);
+DECLARE_CREATE_FOR_TYPE(rxkb_iso639_code);
+DECLARE_GETTER_FOR_TYPE(rxkb_iso639_code, code);
+
+static void
+rxkb_iso3166_code_destroy(struct rxkb_iso3166_code *code)
+{
+ free(code->code);
+}
+
+XKB_EXPORT struct rxkb_iso3166_code *
+rxkb_layout_get_iso3166_first(struct rxkb_layout *layout)
+{
+ struct rxkb_iso3166_code *code = NULL;
+
+ if (!list_empty(&layout->iso3166s))
+ code = list_first_entry(&layout->iso3166s, code, base.link);
+
+ return code;
+}
+
+XKB_EXPORT struct rxkb_iso3166_code *
+rxkb_iso3166_code_next(struct rxkb_iso3166_code *code)
+{
+ struct rxkb_iso3166_code *next = NULL;
+ struct rxkb_layout *layout;
+
+ layout = container_of(code->base.parent, struct rxkb_layout, base);
+
+ if (list_is_last(&layout->iso3166s, &code->base.link))
+ return NULL;
+
+ next = list_first_entry(&code->base.link, code, base.link);
+
+ return next;
+}
+
+DECLARE_REF_UNREF_FOR_TYPE(rxkb_iso3166_code);
+DECLARE_CREATE_FOR_TYPE(rxkb_iso3166_code);
+DECLARE_GETTER_FOR_TYPE(rxkb_iso3166_code, code);
+
+static void
+rxkb_option_destroy(struct rxkb_option *o)
+{
+ free(o->name);
+ free(o->brief);
+ free(o->description);
+}
+
+DECLARE_REF_UNREF_FOR_TYPE(rxkb_option);
+DECLARE_CREATE_FOR_TYPE(rxkb_option);
+DECLARE_GETTER_FOR_TYPE(rxkb_option, name);
+DECLARE_GETTER_FOR_TYPE(rxkb_option, brief);
+DECLARE_GETTER_FOR_TYPE(rxkb_option, description);
+DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_option, popularity, enum rxkb_popularity);
+DECLARE_FIRST_NEXT_FOR_TYPE(rxkb_option, rxkb_option_group, options);
+
+static void
+rxkb_layout_destroy(struct rxkb_layout *l)
+{
+ struct rxkb_iso639_code *iso639, *tmp_639;
+ struct rxkb_iso3166_code *iso3166, *tmp_3166;
+
+ free(l->name);
+ free(l->brief);
+ free(l->description);
+ free(l->variant);
+
+ list_for_each_safe(iso639, tmp_639, &l->iso639s, base.link) {
+ rxkb_iso639_code_unref(iso639);
+ }
+ list_for_each_safe(iso3166, tmp_3166, &l->iso3166s, base.link) {
+ rxkb_iso3166_code_unref(iso3166);
+ }
+}
+
+DECLARE_REF_UNREF_FOR_TYPE(rxkb_layout);
+DECLARE_CREATE_FOR_TYPE(rxkb_layout);
+DECLARE_GETTER_FOR_TYPE(rxkb_layout, name);
+DECLARE_GETTER_FOR_TYPE(rxkb_layout, brief);
+DECLARE_GETTER_FOR_TYPE(rxkb_layout, description);
+DECLARE_GETTER_FOR_TYPE(rxkb_layout, variant);
+DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_layout, popularity, enum rxkb_popularity);
+DECLARE_FIRST_NEXT_FOR_TYPE(rxkb_layout, rxkb_context, layouts);
+
+static void
+rxkb_model_destroy(struct rxkb_model *m)
+{
+ free(m->name);
+ free(m->vendor);
+ free(m->description);
+}
+
+DECLARE_REF_UNREF_FOR_TYPE(rxkb_model);
+DECLARE_CREATE_FOR_TYPE(rxkb_model);
+DECLARE_GETTER_FOR_TYPE(rxkb_model, name);
+DECLARE_GETTER_FOR_TYPE(rxkb_model, vendor);
+DECLARE_GETTER_FOR_TYPE(rxkb_model, description);
+DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_model, popularity, enum rxkb_popularity);
+DECLARE_FIRST_NEXT_FOR_TYPE(rxkb_model, rxkb_context, models);
+
+static void
+rxkb_option_group_destroy(struct rxkb_option_group *og)
+{
+ struct rxkb_option *o, *otmp;
+
+ free(og->name);
+ free(og->description);
+
+ list_for_each_safe(o, otmp, &og->options, base.link) {
+ rxkb_option_unref(o);
+ }
+}
+
+XKB_EXPORT bool
+rxkb_option_group_allows_multiple(struct rxkb_option_group *g)
+{
+ return g->allow_multiple;
+}
+
+DECLARE_REF_UNREF_FOR_TYPE(rxkb_option_group);
+DECLARE_CREATE_FOR_TYPE(rxkb_option_group);
+DECLARE_GETTER_FOR_TYPE(rxkb_option_group, name);
+DECLARE_GETTER_FOR_TYPE(rxkb_option_group, description);
+DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_option_group, popularity, enum rxkb_popularity);
+DECLARE_FIRST_NEXT_FOR_TYPE(rxkb_option_group, rxkb_context, option_groups);
+
+static void
+rxkb_context_destroy(struct rxkb_context *ctx)
+{
+ struct rxkb_model *m, *mtmp;
+ struct rxkb_layout *l, *ltmp;
+ struct rxkb_option_group *og, *ogtmp;
+ char **path;
+
+ list_for_each_safe(m, mtmp, &ctx->models, base.link)
+ rxkb_model_unref(m);
+ assert(list_empty(&ctx->models));
+
+ list_for_each_safe(l, ltmp, &ctx->layouts, base.link)
+ rxkb_layout_unref(l);
+ assert(list_empty(&ctx->layouts));
+
+ list_for_each_safe(og, ogtmp, &ctx->option_groups, base.link)
+ rxkb_option_group_unref(og);
+ assert(list_empty(&ctx->option_groups));
+
+ darray_foreach(path, ctx->includes)
+ free(*path);
+ darray_free(ctx->includes);
+
+ assert(darray_empty(ctx->includes));
+}
+
+DECLARE_REF_UNREF_FOR_TYPE(rxkb_context);
+DECLARE_CREATE_FOR_TYPE(rxkb_context);
+DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_context, log_level, enum rxkb_log_level);
+
+XKB_EXPORT void
+rxkb_context_set_log_level(struct rxkb_context *ctx,
+ enum rxkb_log_level level)
+{
+ ctx->log_level = level;
+}
+
+static const char *
+log_level_to_prefix(enum rxkb_log_level level)
+{
+ switch (level) {
+ case RXKB_LOG_LEVEL_DEBUG:
+ return "xkbregistry: DEBUG: ";
+ case RXKB_LOG_LEVEL_INFO:
+ return "xkbregistry: INFO: ";
+ case RXKB_LOG_LEVEL_WARNING:
+ return "xkbregistry: WARNING: ";
+ case RXKB_LOG_LEVEL_ERROR:
+ return "xkbregistry: ERROR: ";
+ case RXKB_LOG_LEVEL_CRITICAL:
+ return "xkbregistry: CRITICAL: ";
+ default:
+ return NULL;
+ }
+}
+
+ATTR_PRINTF(3, 0) static void
+default_log_fn(struct rxkb_context *ctx, enum rxkb_log_level level,
+ const char *fmt, va_list args)
+{
+ const char *prefix = log_level_to_prefix(level);
+
+ if (prefix)
+ fprintf(stderr, "%s", prefix);
+ vfprintf(stderr, fmt, args);
+}
+
+static enum rxkb_log_level
+log_level(const char *level) {
+ char *endptr;
+ enum rxkb_log_level lvl;
+
+ errno = 0;
+ lvl = strtol(level, &endptr, 10);
+ if (errno == 0 && (endptr[0] == '\0' || is_space(endptr[0])))
+ return lvl;
+ if (istreq_prefix("crit", level))
+ return RXKB_LOG_LEVEL_CRITICAL;
+ if (istreq_prefix("err", level))
+ return RXKB_LOG_LEVEL_ERROR;
+ if (istreq_prefix("warn", level))
+ return RXKB_LOG_LEVEL_WARNING;
+ if (istreq_prefix("info", level))
+ return RXKB_LOG_LEVEL_INFO;
+ if (istreq_prefix("debug", level) || istreq_prefix("dbg", level))
+ return RXKB_LOG_LEVEL_DEBUG;
+
+ return RXKB_LOG_LEVEL_ERROR;
+}
+
+XKB_EXPORT struct rxkb_context *
+rxkb_context_new(enum rxkb_context_flags flags)
+{
+ struct rxkb_context *ctx = rxkb_context_create(NULL);
+ const char *env;
+
+ if (!ctx)
+ return NULL;
+
+ ctx->context_state = CONTEXT_NEW;
+ ctx->load_extra_rules_files = flags & RXKB_CONTEXT_LOAD_EXOTIC_RULES;
+ ctx->log_fn = default_log_fn;
+ ctx->log_level = RXKB_LOG_LEVEL_ERROR;
+
+ /* Environment overwrites defaults. */
+ env = secure_getenv("RXKB_LOG_LEVEL");
+ if (env)
+ rxkb_context_set_log_level(ctx, log_level(env));
+
+ list_init(&ctx->models);
+ list_init(&ctx->layouts);
+ list_init(&ctx->option_groups);
+
+ if (!(flags & RXKB_CONTEXT_NO_DEFAULT_INCLUDES) &&
+ !rxkb_context_include_path_append_default(ctx)) {
+ rxkb_context_unref(ctx);
+ return NULL;
+ }
+
+ return ctx;
+}
+
+XKB_EXPORT void
+rxkb_context_set_log_fn(struct rxkb_context *ctx,
+ void (*log_fn)(struct rxkb_context *ctx,
+ enum rxkb_log_level level,
+ const char *fmt, va_list args))
+{
+ ctx->log_fn = (log_fn ? log_fn : default_log_fn);
+}
+
+XKB_EXPORT bool
+rxkb_context_include_path_append(struct rxkb_context *ctx, const char *path)
+{
+ struct stat stat_buf;
+ int err;
+ char *tmp = NULL;
+ char rules[PATH_MAX];
+
+ if (ctx->context_state != CONTEXT_NEW) {
+ log_err(ctx, "include paths can only be appended to a new context\n");
+ return false;
+ }
+
+ tmp = strdup(path);
+ if (!tmp)
+ goto err;
+
+ err = stat(path, &stat_buf);
+ if (err != 0)
+ goto err;
+ if (!S_ISDIR(stat_buf.st_mode))
+ goto err;
+
+ if (!check_eaccess(path, R_OK | X_OK))
+ goto err;
+
+ /* Pre-filter for the 99.9% case - if we can't assemble the default ruleset
+ * path, complain here instead of during parsing later. The niche cases
+ * where this is the wrong behaviour aren't worth worrying about.
+ */
+ if (!snprintf_safe(rules, sizeof(rules), "%s/rules/%s.xml",
+ path, DEFAULT_XKB_RULES))
+ goto err;
+
+ darray_append(ctx->includes, tmp);
+
+ return true;
+
+err:
+ free(tmp);
+ return false;
+}
+
+XKB_EXPORT bool
+rxkb_context_include_path_append_default(struct rxkb_context *ctx)
+{
+ const char *home, *xdg, *root, *extra;
+ char *user_path;
+ bool ret = false;
+
+ if (ctx->context_state != CONTEXT_NEW) {
+ log_err(ctx, "include paths can only be appended to a new context\n");
+ return false;
+ }
+
+ home = secure_getenv("HOME");
+
+ xdg = secure_getenv("XDG_CONFIG_HOME");
+ if (xdg != NULL) {
+ user_path = asprintf_safe("%s/xkb", xdg);
+ if (user_path) {
+ ret |= rxkb_context_include_path_append(ctx, user_path);
+ free(user_path);
+ }
+ } else if (home != NULL) {
+ /* XDG_CONFIG_HOME fallback is $HOME/.config/ */
+ user_path = asprintf_safe("%s/.config/xkb", home);
+ if (user_path) {
+ ret |= rxkb_context_include_path_append(ctx, user_path);
+ free(user_path);
+ }
+ }
+
+ if (home != NULL) {
+ user_path = asprintf_safe("%s/.xkb", home);
+ if (user_path) {
+ ret |= rxkb_context_include_path_append(ctx, user_path);
+ free(user_path);
+ }
+ }
+
+ extra = secure_getenv("XKB_CONFIG_EXTRA_PATH");
+ if (extra != NULL)
+ ret |= rxkb_context_include_path_append(ctx, extra);
+ else
+ ret |= rxkb_context_include_path_append(ctx, DFLT_XKB_CONFIG_EXTRA_PATH);
+
+ root = secure_getenv("XKB_CONFIG_ROOT");
+ if (root != NULL)
+ ret |= rxkb_context_include_path_append(ctx, root);
+ else
+ ret |= rxkb_context_include_path_append(ctx, DFLT_XKB_CONFIG_ROOT);
+
+ return ret;
+}
+
+XKB_EXPORT bool
+rxkb_context_parse_default_ruleset(struct rxkb_context *ctx)
+{
+ return rxkb_context_parse(ctx, DEFAULT_XKB_RULES);
+}
+
+XKB_EXPORT bool
+rxkb_context_parse(struct rxkb_context *ctx, const char *ruleset)
+{
+ char **path;
+ bool success = false;
+
+ if (ctx->context_state != CONTEXT_NEW) {
+ log_err(ctx, "parse must only be called on a new context\n");
+ return false;
+ }
+
+ darray_foreach_reverse(path, ctx->includes) {
+ char rules[PATH_MAX];
+
+ if (snprintf_safe(rules, sizeof(rules), "%s/rules/%s.xml",
+ *path, ruleset)) {
+ log_dbg(ctx, "Parsing %s\n", rules);
+ if (parse(ctx, rules, RXKB_POPULARITY_STANDARD))
+ success = true;
+ }
+
+ if (ctx->load_extra_rules_files &&
+ snprintf_safe(rules, sizeof(rules), "%s/rules/%s.extras.xml",
+ *path, ruleset)) {
+ log_dbg(ctx, "Parsing %s\n", rules);
+ if (parse(ctx, rules, RXKB_POPULARITY_EXOTIC))
+ success = true;
+ }
+ }
+
+ ctx->context_state = success ? CONTEXT_PARSED : CONTEXT_FAILED;
+
+ return success;
+}
+
+
+XKB_EXPORT void
+rxkb_context_set_user_data(struct rxkb_context *ctx, void *userdata)
+{
+ ctx->userdata = userdata;
+}
+
+XKB_EXPORT void *
+rxkb_context_get_user_data(struct rxkb_context *ctx)
+{
+ return ctx->userdata;
+}
+
+static inline bool
+is_node(xmlNode *node, const char *name)
+{
+ return node->type == XML_ELEMENT_NODE &&
+ xmlStrEqual(node->name, (const xmlChar*)name);
+}
+
+/* return a copy of the text content from the first text node of this node */
+static char *
+extract_text(xmlNode *node)
+{
+ xmlNode *n;
+
+ for (n = node->children; n; n = n->next) {
+ if (n->type == XML_TEXT_NODE)
+ return (char *)xmlStrdup(n->content);
+ }
+ return NULL;
+}
+
+static bool
+parse_config_item(struct rxkb_context *ctx,
+ xmlNode *parent,
+ char **name,
+ char **description,
+ char **brief,
+ char **vendor)
+{
+ xmlNode *node = NULL;
+ xmlNode *ci = NULL;
+
+ for (ci = parent->children; ci; ci = ci->next) {
+ if (is_node(ci, "configItem")) {
+ *name = NULL;
+ *description = NULL;
+ *brief = NULL;
+ *vendor = NULL;
+
+ for (node = ci->children; node; node = node->next) {
+ if (is_node(node, "name"))
+ *name = extract_text(node);
+ else if (is_node(node, "description"))
+ *description = extract_text(node);
+ else if (is_node(node, "shortDescription"))
+ *brief = extract_text(node);
+ else if (is_node(node, "vendor"))
+ *vendor = extract_text(node);
+ /* Note: the DTD allows for vendor + brief but models only use
+ * vendor and everything else only uses shortDescription */
+ }
+
+ if (!*name || !strlen(*name)) {
+ log_err(ctx, "xml:%d: missing required element 'name'\n",
+ ci->line);
+ return false;
+ }
+
+ return true; /* only one configItem allowed in the dtd */
+ }
+ }
+
+ return false;
+}
+
+static void
+parse_model(struct rxkb_context *ctx, xmlNode *model,
+ enum rxkb_popularity popularity)
+{
+ char *name, *description, *brief, *vendor;
+
+ if (parse_config_item(ctx, model, &name, &description, &brief, &vendor)) {
+ struct rxkb_model *m;
+
+ list_for_each(m, &ctx->models, base.link) {
+ if (streq(m->name, name)) {
+ free(name);
+ free(description);
+ free(brief);
+ free(vendor);
+ return;
+ }
+ }
+
+ /* new model */
+ m = rxkb_model_create(&ctx->base);
+ m->name = name;
+ m->description = description;
+ m->vendor = vendor;
+ m->popularity = popularity;
+ list_append(&ctx->models, &m->base.link);
+ }
+}
+
+static void
+parse_model_list(struct rxkb_context *ctx, xmlNode *model_list,
+ enum rxkb_popularity popularity)
+{
+ xmlNode *node = NULL;
+
+ for (node = model_list->children; node; node = node->next) {
+ if (is_node(node, "model"))
+ parse_model(ctx, node, popularity);
+ }
+}
+
+static void
+parse_language_list(xmlNode *language_list, struct rxkb_layout *layout)
+{
+ xmlNode *node = NULL;
+ struct rxkb_iso639_code *code;
+
+ for (node = language_list->children; node; node = node->next) {
+ if (is_node(node, "iso639Id")) {
+ char *str = extract_text(node);
+ struct rxkb_object *parent;
+
+ parent = &layout->base;
+ code = rxkb_iso639_code_create(parent);
+ code->code = str;
+ list_append(&layout->iso639s, &code->base.link);
+ }
+ }
+}
+
+static void
+parse_country_list(xmlNode *country_list, struct rxkb_layout *layout)
+{
+ xmlNode *node = NULL;
+ struct rxkb_iso3166_code *code;
+
+ for (node = country_list->children; node; node = node->next) {
+ if (is_node(node, "iso3166Id")) {
+ char *str = extract_text(node);
+ struct rxkb_object *parent;
+
+ parent = &layout->base;
+ code = rxkb_iso3166_code_create(parent);
+ code->code = str;
+ list_append(&layout->iso3166s, &code->base.link);
+ }
+ }
+}
+
+static void
+parse_variant(struct rxkb_context *ctx, struct rxkb_layout *l,
+ xmlNode *variant, enum rxkb_popularity popularity)
+{
+ xmlNode *ci;
+ char *name, *description, *brief, *vendor;
+
+ if (parse_config_item(ctx, variant, &name, &description, &brief, &vendor)) {
+ struct rxkb_layout *v;
+ bool exists = false;
+
+ list_for_each(v, &ctx->layouts, base.link) {
+ if (streq(v->name, name) && streq(v->name, l->name)) {
+ exists = true;
+ break;
+ }
+ }
+
+ if (!exists) {
+ v = rxkb_layout_create(&ctx->base);
+ list_init(&v->iso639s);
+ list_init(&v->iso3166s);
+ v->name = strdup(l->name);
+ v->variant = name;
+ v->description = description;
+ v->brief = brief;
+ v->popularity = popularity;
+ list_append(&ctx->layouts, &v->base.link);
+
+ for (ci = variant->children; ci; ci = ci->next) {
+ xmlNode *node;
+
+ if (!is_node(ci, "configItem"))
+ continue;
+
+ for (node = ci->children; node; node = node->next) {
+ if (is_node(node, "languageList"))
+ parse_language_list(node, v);
+ if (is_node(node, "countryList"))
+ parse_country_list(node, v);
+ }
+ }
+ } else {
+ free(name);
+ free(description);
+ free(brief);
+ free(vendor);
+ }
+ }
+}
+
+static void
+parse_variant_list(struct rxkb_context *ctx, struct rxkb_layout *l,
+ xmlNode *variant_list, enum rxkb_popularity popularity)
+{
+ xmlNode *node = NULL;
+
+ for (node = variant_list->children; node; node = node->next) {
+ if (is_node(node, "variant"))
+ parse_variant(ctx, l, node, popularity);
+ }
+}
+
+static void
+parse_layout(struct rxkb_context *ctx, xmlNode *layout,
+ enum rxkb_popularity popularity)
+{
+ char *name, *description, *brief, *vendor;
+ struct rxkb_layout *l;
+ xmlNode *node = NULL;
+ bool exists = false;
+
+ if (!parse_config_item(ctx, layout, &name, &description, &brief, &vendor))
+ return;
+
+ list_for_each(l, &ctx->layouts, base.link) {
+ if (streq(l->name, name) && l->variant == NULL) {
+ exists = true;
+ break;
+ }
+ }
+
+ if (!exists) {
+ l = rxkb_layout_create(&ctx->base);
+ list_init(&l->iso639s);
+ list_init(&l->iso3166s);
+ l->name = name;
+ l->variant = NULL;
+ l->description = description;
+ l->brief = brief;
+ l->popularity = popularity;
+ list_append(&ctx->layouts, &l->base.link);
+ } else {
+ free(name);
+ free(description);
+ free(brief);
+ free(vendor);
+ }
+
+ for (node = layout->children; node; node = node->next) {
+ if (is_node(node, "variantList")) {
+ parse_variant_list(ctx, l, node, popularity);
+ }
+ if (!exists && is_node(node, "configItem")) {
+ xmlNode *ll;
+ for (ll = node->children; ll; ll = ll->next) {
+ if (is_node(ll, "languageList"))
+ parse_language_list(ll, l);
+ if (is_node(ll, "countryList"))
+ parse_country_list(ll, l);
+ }
+ }
+ }
+}
+
+static void
+parse_layout_list(struct rxkb_context *ctx, xmlNode *layout_list,
+ enum rxkb_popularity popularity)
+{
+ xmlNode *node = NULL;
+
+ for (node = layout_list->children; node; node = node->next) {
+ if (is_node(node, "layout"))
+ parse_layout(ctx, node, popularity);
+ }
+}
+
+static void
+parse_option(struct rxkb_context *ctx, struct rxkb_option_group *group,
+ xmlNode *option, enum rxkb_popularity popularity)
+{
+ char *name, *description, *brief, *vendor;
+
+ if (parse_config_item(ctx, option, &name, &description, &brief, &vendor)) {
+ struct rxkb_option *o;
+
+ list_for_each(o, &group->options, base.link) {
+ if (streq(o->name, name)) {
+ free(name);
+ free(description);
+ free(brief);
+ free(vendor);
+ return;
+ }
+ }
+
+ o = rxkb_option_create(&group->base);
+ o->name = name;
+ o->description = description;
+ o->popularity = popularity;
+ list_append(&group->options, &o->base.link);
+ }
+}
+
+static void
+parse_group(struct rxkb_context *ctx, xmlNode *group,
+ enum rxkb_popularity popularity)
+{
+ char *name, *description, *brief, *vendor;
+ struct rxkb_option_group *g;
+ xmlNode *node = NULL;
+ xmlChar *multiple;
+ bool exists = false;
+
+ if (!parse_config_item(ctx, group, &name, &description, &brief, &vendor))
+ return;
+
+ list_for_each(g, &ctx->option_groups, base.link) {
+ if (streq(g->name, name)) {
+ exists = true;
+ break;
+ }
+ }
+
+ if (!exists) {
+ g = rxkb_option_group_create(&ctx->base);
+ g->name = name;
+ g->description = description;
+ g->popularity = popularity;
+
+ multiple = xmlGetProp(group, (const xmlChar*)"allowMultipleSelection");
+ if (multiple && xmlStrEqual(multiple, (const xmlChar*)"true"))
+ g->allow_multiple = true;
+ xmlFree(multiple);
+
+ list_init(&g->options);
+ list_append(&ctx->option_groups, &g->base.link);
+ } else {
+ free(name);
+ free(description);
+ free(brief);
+ free(vendor);
+ }
+
+ for (node = group->children; node; node = node->next) {
+ if (is_node(node, "option"))
+ parse_option(ctx, g, node, popularity);
+ }
+}
+
+static void
+parse_option_list(struct rxkb_context *ctx, xmlNode *option_list,
+ enum rxkb_popularity popularity)
+{
+ xmlNode *node = NULL;
+
+ for (node = option_list->children; node; node = node->next) {
+ if (is_node(node, "group"))
+ parse_group(ctx, node, popularity);
+ }
+}
+
+static void
+parse_rules_xml(struct rxkb_context *ctx, xmlNode *root,
+ enum rxkb_popularity popularity)
+{
+ xmlNode *node = NULL;
+
+ for (node = root->children; node; node = node->next) {
+ if (is_node(node, "modelList"))
+ parse_model_list(ctx, node, popularity);
+ else if (is_node(node, "layoutList"))
+ parse_layout_list(ctx, node, popularity);
+ else if (is_node(node, "optionList"))
+ parse_option_list(ctx, node, popularity);
+ }
+}
+
+static void
+ATTR_PRINTF(2, 0)
+xml_error_func(void *ctx, const char *msg, ...)
+{
+ static char buf[PATH_MAX];
+ static int slen = 0;
+ va_list args;
+ int rc;
+
+ /* libxml2 prints IO errors from bad includes paths by
+ * calling the error function once per word. So we get to
+ * re-assemble the message here and print it when we get
+ * the line break. My enthusiasm about this is indescribable.
+ */
+ va_start(args, msg);
+ rc = vsnprintf(&buf[slen], sizeof(buf) - slen, msg, args);
+ va_end(args);
+
+ /* This shouldn't really happen */
+ if (rc < 0) {
+ log_err(ctx, "+++ out of cheese error. redo from start +++\n");
+ slen = 0;
+ memset(buf, 0, sizeof(buf));
+ return;
+ }
+
+ slen += rc;
+ if (slen >= (int)sizeof(buf)) {
+ /* truncated, let's flush this */
+ buf[sizeof(buf) - 1] = '\n';
+ slen = sizeof(buf);
+ }
+
+ /* We're assuming here that the last character is \n. */
+ if (buf[slen - 1] == '\n') {
+ log_err(ctx, "%s", buf);
+ memset(buf, 0, sizeof(buf));
+ slen = 0;
+ }
+}
+
+static bool
+validate(struct rxkb_context *ctx, xmlDoc *doc)
+{
+ bool success = false;
+ xmlValidCtxt *dtdvalid = NULL;
+ xmlDtd *dtd = NULL;
+ xmlParserInputBufferPtr buf = NULL;
+ /* This is a modified version of the xkeyboard-config xkb.dtd. That one
+ * requires modelList, layoutList and optionList, we
+ * allow for any of those to be missing.
+ */
+ const char dtdstr[] =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<!ELEMENT xkbConfigRegistry (modelList?, layoutList?, optionList?)>\n"
+ "<!ATTLIST xkbConfigRegistry version CDATA \"1.1\">\n"
+ "<!ELEMENT modelList (model*)>\n"
+ "<!ELEMENT model (configItem)>\n"
+ "<!ELEMENT layoutList (layout*)>\n"
+ "<!ELEMENT layout (configItem, variantList?)>\n"
+ "<!ELEMENT optionList (group*)>\n"
+ "<!ELEMENT variantList (variant*)>\n"
+ "<!ELEMENT variant (configItem)>\n"
+ "<!ELEMENT group (configItem, option*)>\n"
+ "<!ATTLIST group allowMultipleSelection (true|false) \"false\">\n"
+ "<!ELEMENT option (configItem)>\n"
+ "<!ELEMENT configItem (name, shortDescription?, description?, vendor?, countryList?, languageList?, hwList?)>\n"
+ "<!ATTLIST configItem popularity (standard|exotic) \"standard\">\n"
+ "<!ELEMENT name (#PCDATA)>\n"
+ "<!ELEMENT shortDescription (#PCDATA)>\n"
+ "<!ELEMENT description (#PCDATA)>\n"
+ "<!ELEMENT vendor (#PCDATA)>\n"
+ "<!ELEMENT countryList (iso3166Id+)>\n"
+ "<!ELEMENT iso3166Id (#PCDATA)>\n"
+ "<!ELEMENT languageList (iso639Id+)>\n"
+ "<!ELEMENT iso639Id (#PCDATA)>\n"
+ "<!ELEMENT hwList (hwId+)>\n"
+ "<!ELEMENT hwId (#PCDATA)>\n";
+
+ /* Note: do not use xmlParserInputBufferCreateStatic, it generates random
+ * DTD validity errors for unknown reasons */
+ buf = xmlParserInputBufferCreateMem(dtdstr, sizeof(dtdstr),
+ XML_CHAR_ENCODING_UTF8);
+ if (!buf)
+ return false;
+
+ dtd = xmlIOParseDTD(NULL, buf, XML_CHAR_ENCODING_UTF8);
+ if (!dtd) {
+ log_err(ctx, "Failed to load DTD\n");
+ return false;
+ }
+
+ dtdvalid = xmlNewValidCtxt();
+ if (xmlValidateDtd(dtdvalid, doc, dtd))
+ success = true;
+
+ if (dtd)
+ xmlFreeDtd(dtd);
+ if (dtdvalid)
+ xmlFreeValidCtxt(dtdvalid);
+
+ return success;
+}
+
+static bool
+parse(struct rxkb_context *ctx, const char *path,
+ enum rxkb_popularity popularity)
+{
+ bool success = false;
+ xmlDoc *doc = NULL;
+ xmlNode *root = NULL;
+
+ if (!check_eaccess(path, R_OK))
+ return false;
+
+ LIBXML_TEST_VERSION
+
+ xmlSetGenericErrorFunc(ctx, xml_error_func);
+
+ doc = xmlParseFile(path);
+ if (!doc)
+ return false;
+
+ if (!validate(ctx, doc)) {
+ log_err(ctx, "XML error: failed to validate document at %s\n", path);
+ goto error;
+ }
+
+ root = xmlDocGetRootElement(doc);
+ parse_rules_xml(ctx, root, popularity);
+
+ success = true;
+error:
+ xmlFreeDoc(doc);
+ xmlCleanupParser();
+
+ return success;
+}