summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPeter Hutterer <peter.hutterer@who-t.net>2020-05-12 14:09:50 +1000
committerPeter Hutterer <peter.hutterer@who-t.net>2020-07-06 15:15:20 +1000
commitafb26e7df9090a0b765eb294b6efff448f763b6f (patch)
treeed8820db131fde2f5190333e0776aced657629d7 /src
parentfe8861338242762da6a3245a106042266280714c (diff)
downloadlibxkbcommon-afb26e7df9090a0b765eb294b6efff448f763b6f.tar.gz
libxkbcommon-afb26e7df9090a0b765eb294b6efff448f763b6f.tar.bz2
libxkbcommon-afb26e7df9090a0b765eb294b6efff448f763b6f.zip
Add libxkbregistry to query available RMLVO
This library is the replacement for clients parsing evdev.xml directly. Instead, they should use the API here so that in the future we may even be able to swap evdev.xml for a more suitable data format. The library parses through evdev.xml (using libxml2) and - if requested - through evdev.extras.xml as well. The merge approach is optimised for the default case where we have a system-installed rules XML and another file in $XDG_CONFIG_DIR that adds a few entries. We load the system file first, then append any custom ones to that. It's not possible to overwrite the MLVO list provided by the system files - if you want to do that, get the change upstream. XML validation is handled through the DTD itself which means we only need to check for a nonempty name, everything else the DTD validation should complain about. The logging system is effectively identical to xkbcommon. Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Diffstat (limited to 'src')
-rw-r--r--src/darray.h3
-rw-r--r--src/registry.c1193
-rw-r--r--src/util-list.c94
-rw-r--r--src/util-list.h71
4 files changed, 1361 insertions, 0 deletions
diff --git a/src/darray.h b/src/darray.h
index 8e87c94..de659cc 100644
--- a/src/darray.h
+++ b/src/darray.h
@@ -206,4 +206,7 @@ darray_next_alloc(unsigned alloc, unsigned need, unsigned itemSize)
(idx) < (arr).size; \
(idx)++, (val)++)
+#define darray_foreach_reverse(i, arr) \
+ for ((i) = &(arr).item[(arr).size - 1]; (arr).size > 0 && (i) >= &(arr).item[0]; (i)--)
+
#endif /* CCAN_DARRAY_H */
diff --git a/src/registry.c b/src/registry.c
new file mode 100644
index 0000000..19e004b
--- /dev/null
+++ b/src/registry.c
@@ -0,0 +1,1193 @@
+/*
+ * 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;
+ char *user_path;
+ int err;
+ 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) {
+ err = asprintf(&user_path, "%s/xkb", xdg);
+ if (err >= 0) {
+ ret |= rxkb_context_include_path_append(ctx, user_path);
+ free(user_path);
+ }
+ } else if (home != NULL) {
+ /* XDG_CONFIG_HOME fallback is $HOME/.config/ */
+ err = asprintf(&user_path, "%s/.config/xkb", home);
+ if (err >= 0) {
+ ret |= rxkb_context_include_path_append(ctx, user_path);
+ free(user_path);
+ }
+ }
+
+ if (home != NULL) {
+ err = asprintf(&user_path, "%s/.xkb", home);
+ if (err >= 0) {
+ ret |= rxkb_context_include_path_append(ctx, user_path);
+ free(user_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;
+}
diff --git a/src/util-list.c b/src/util-list.c
new file mode 100644
index 0000000..b6f5069
--- /dev/null
+++ b/src/util-list.c
@@ -0,0 +1,94 @@
+/*
+ * Copyright © 2008-2011 Kristian Høgsberg
+ * Copyright © 2011 Intel Corporation
+ * Copyright © 2013-2015 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 <assert.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#include "util-list.h"
+
+void
+list_init(struct list *list)
+{
+ list->prev = list;
+ list->next = list;
+}
+
+void
+list_insert(struct list *list, struct list *elm)
+{
+ assert((list->next != NULL && list->prev != NULL) ||
+ !"list->next|prev is NULL, possibly missing list_init()");
+ assert(((elm->next == NULL && elm->prev == NULL) || list_empty(elm)) ||
+ !"elm->next|prev is not NULL, list node used twice?");
+
+ elm->prev = list;
+ elm->next = list->next;
+ list->next = elm;
+ elm->next->prev = elm;
+}
+
+void
+list_append(struct list *list, struct list *elm)
+{
+ assert((list->next != NULL && list->prev != NULL) ||
+ !"list->next|prev is NULL, possibly missing list_init()");
+ assert(((elm->next == NULL && elm->prev == NULL) || list_empty(elm)) ||
+ !"elm->next|prev is not NULL, list node used twice?");
+
+ elm->next = list;
+ elm->prev = list->prev;
+ list->prev = elm;
+ elm->prev->next = elm;
+}
+
+void
+list_remove(struct list *elm)
+{
+ assert((elm->next != NULL && elm->prev != NULL) ||
+ !"list->next|prev is NULL, possibly missing list_init()");
+
+ elm->prev->next = elm->next;
+ elm->next->prev = elm->prev;
+ elm->next = NULL;
+ elm->prev = NULL;
+}
+
+bool
+list_empty(const struct list *list)
+{
+ assert((list->next != NULL && list->prev != NULL) ||
+ !"list->next|prev is NULL, possibly missing list_init()");
+
+ return list->next == list;
+}
+
+bool
+list_is_last(const struct list *list, const struct list *elm)
+{
+ return elm->next == list;
+}
diff --git a/src/util-list.h b/src/util-list.h
new file mode 100644
index 0000000..573dff7
--- /dev/null
+++ b/src/util-list.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2008-2011 Kristian Høgsberg
+ * Copyright © 2011 Intel Corporation
+ * Copyright © 2013-2015 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.
+ */
+
+#pragma once
+
+#include "config.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+
+/*
+ * This list data structure is a verbatim copy from wayland-util.h from the
+ * Wayland project; except that wl_ prefix has been removed.
+ */
+
+struct list {
+ struct list *prev;
+ struct list *next;
+};
+
+void list_init(struct list *list);
+void list_insert(struct list *list, struct list *elm);
+void list_append(struct list *list, struct list *elm);
+void list_remove(struct list *elm);
+bool list_empty(const struct list *list);
+bool list_is_last(const struct list *list, const struct list *elm);
+
+#define container_of(ptr, type, member) \
+ (__typeof__(type) *)((char *)(ptr) - \
+ offsetof(__typeof__(type), member))
+
+#define list_first_entry(head, pos, member) \
+ container_of((head)->next, __typeof__(*pos), member)
+
+#define list_last_entry(head, pos, member) \
+ container_of((head)->prev, __typeof__(*pos), member)
+
+#define list_for_each(pos, head, member) \
+ for (pos = 0, pos = list_first_entry(head, pos, member); \
+ &pos->member != (head); \
+ pos = list_first_entry(&pos->member, pos, member))
+
+#define list_for_each_safe(pos, tmp, head, member) \
+ for (pos = 0, tmp = 0, \
+ pos = list_first_entry(head, pos, member), \
+ tmp = list_first_entry(&pos->member, tmp, member); \
+ &pos->member != (head); \
+ pos = tmp, \
+ tmp = list_first_entry(&pos->member, tmp, member))