From afb26e7df9090a0b765eb294b6efff448f763b6f Mon Sep 17 00:00:00 2001
From: Peter Hutterer <peter.hutterer@who-t.net>
Date: Tue, 12 May 2020 14:09:50 +1000
Subject: [PATCH] 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>
---
 meson.build                |   58 +-
 meson_options.txt          |    6 +
 src/darray.h               |    3 +
 src/registry.c             | 1193 ++++++++++++++++++++++++++++++++++++
 src/util-list.c            |   94 +++
 src/util-list.h            |   71 +++
 test/registry.c            |  843 +++++++++++++++++++++++++
 tools/registry-list.c      |  223 +++++++
 xkbcommon/xkbregistry.h    |  782 +++++++++++++++++++++++
 xkbregistry.map            |   61 ++
 15 files changed, 3435 insertions(+), 7 deletions(-)
 create mode 100644 src/registry.c
 create mode 100644 src/util-list.c
 create mode 100644 src/util-list.h
 create mode 100644 test/registry.c
 create mode 100644 tools/registry-list.c
 create mode 100644 xkbcommon/xkbregistry.h
 create mode 100644 xkbregistry.map

 diff --git a/meson.build b/meson.build
index 7041d35f8..52aeafd07 100644
--- a/meson.build
+++ b/meson.build
@@ -296,6 +296,53 @@ You can disable X11 support with -Denable-x11=false.''')
     )
 endif
 
+# libxkbregistry
+if get_option('enable-xkbregistry')
+    dep_libxml = dependency('libxml-2.0')
+    deps_libxkbregistry = [dep_libxml]
+    libxkbregistry_sources = [
+        'src/registry.c',
+        'src/utils.h',
+        'src/utils.c',
+        'src/util-list.h',
+        'src/util-list.c',
+    ]
+    libxkbregistry_link_args = []
+    if have_version_script
+        libxkbregistry_link_args += '-Wl,--version-script=' + join_paths(meson.source_root(), 'xkbregistry.map')
+    endif
+    libxkbregistry = library(
+        'xkbregistry',
+        'xkbcommon/xkbregistry.h',
+        libxkbregistry_sources,
+        link_args: libxkbregistry_link_args,
+        link_depends: 'xkbregistry.map',
+        dependencies: deps_libxkbregistry,
+        version: '0.0.0',
+        install: true,
+        include_directories: include_directories('src'),
+    )
+    install_headers(
+        'xkbcommon/xkbregistry.h',
+        subdir: 'xkbcommon',
+    )
+    pkgconfig.generate(
+        name: 'xkbregistry',
+        filebase: 'xkbregistry',
+        libraries: libxkbregistry,
+        version: meson.project_version(),
+        description: 'XKB API to query available rules, models, layouts, variants and options',
+    )
+
+    dep_libxkbregistry = declare_dependency(
+                                include_directories: include_directories('xkbcommon'),
+                                link_with: libxkbregistry
+                                )
+    executable('xkbcommon-registry-list',
+               'tools/registry-list.c',
+               dependencies: dep_libxkbregistry,
+               install: false)
+endif
 
 # Tests
 test_env = environment()
@@ -440,7 +487,15 @@ if get_option('enable-x11')
     # See: https://github.com/xkbcommon/libxkbcommon/issues/30
     executable('test-x11comp', 'test/x11comp.c', dependencies: x11_test_dep)
 endif
-
+if get_option('enable-xkbregistry')
+    test(
+        'registry',
+        executable('test-registry', 'test/registry.c',
+                   include_directories: include_directories('src'),
+                   dependencies: dep_libxkbregistry),
+        env: test_env,
+    )
+endif
 
 # Fuzzing target programs.
 executable('fuzz-keymap', 'fuzz/keymap/target.c', dependencies: test_dep)
@@ -577,6 +632,7 @@ You can disable the documentation with -Denable-docs=false.''')
         'xkbcommon/xkbcommon-names.h',
         'xkbcommon/xkbcommon-x11.h',
         'xkbcommon/xkbcommon-compose.h',
+        'xkbcommon/xkbregistry.h',
     ]
     doxygen_data = configuration_data()
     doxygen_data.set('PACKAGE_NAME', meson.project_name())
diff --git a/meson_options.txt b/meson_options.txt
index 0e166b134..5eaa081db 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -56,3 +56,9 @@ option(
     value: true,
     description: 'Enable support for Wayland utility programs',
 )
+option(
+    'enable-xkbregistry',
+    type: 'boolean',
+    value: true,
+    description: 'Enable building libxkbregistry',
+)
diff --git a/src/darray.h b/src/darray.h
index 8e87c942e..de659ccad 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 000000000..19e004ba5
--- /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 000000000..b6f506930
--- /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 000000000..573dff79d
--- /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))
diff --git a/test/registry.c b/test/registry.c
new file mode 100644
index 000000000..68e74d024
--- /dev/null
+++ b/test/registry.c
@@ -0,0 +1,843 @@
+/*
+ * 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 <assert.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "xkbcommon/xkbregistry.h"
+
+#include "utils.h"
+
+#define NO_VARIANT NULL
+
+enum {
+    MODEL = 78,
+    LAYOUT,
+    VARIANT,
+    OPTION,
+};
+
+struct test_model {
+    const char *name; /* required */
+    const char *vendor;
+    const char *description;
+};
+
+struct test_layout {
+    const char *name; /* required */
+    const char *variant;
+    const char *brief;
+    const char *description;
+};
+
+struct test_option {
+    const char *name;
+    const char *description;
+};
+
+struct test_option_group {
+    const char *name;
+    const char *description;
+    bool allow_multiple_selection;
+
+    struct test_option options[10];
+};
+
+static void
+fprint_config_item(FILE *fp,
+                   const char *name,
+                   const char *vendor,
+                   const char *brief,
+                   const char *description)
+{
+    fprintf(fp, "  <configItem>\n"
+                "    <name>%s</name>\n", name);
+    if (brief)
+        fprintf(fp, "    <shortDescription>%s</shortDescription>\n", brief);
+    if (description)
+        fprintf(fp, "    <description>%s</description>\n", description);
+    if (vendor)
+        fprintf(fp, "    <vendor>%s</vendor>\n", vendor);
+    fprintf(fp, "  </configItem>\n");
+}
+
+/**
+ * Create a directory populated with a rules/<ruleset>.xml that contains the
+ * given items.
+ *
+ * @return the XKB base directory
+ */
+static char *
+test_create_rules(const char *ruleset,
+                  const struct test_model *test_models,
+                  const struct test_layout *test_layouts,
+                  const struct test_option_group *test_groups)
+{
+    static int iteration;
+    char *tmpdir;
+    char buf[PATH_MAX];
+    int rc;
+    FILE *fp;
+
+    rc = asprintf(&tmpdir, "/tmp/%s.%d.XXXXXX", ruleset, iteration++);
+    assert(rc > 0);
+    assert(mkdtemp(tmpdir) == tmpdir);
+
+    rc = snprintf_safe(buf, sizeof(buf), "%s/rules", tmpdir);
+    assert(rc);
+    rc = mkdir(buf, 0777);
+    assert(rc == 0);
+    rc = snprintf_safe(buf, sizeof(buf), "%s/rules/%s.xml", tmpdir, ruleset);
+    assert(rc);
+
+    fp = fopen(buf, "w");
+    assert(fp);
+
+    fprintf(fp,
+            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            "<!DOCTYPE xkbConfigRegistry SYSTEM \"xkb.dtd\">\n"
+            "<xkbConfigRegistry version=\"1.1\">\n");
+
+    if (test_models) {
+        fprintf(fp, "<modelList>\n");
+
+        for (const struct test_model *m = test_models; m->name; m++) {
+            fprintf(fp, "<model>\n");
+            fprint_config_item(fp, m->name, m->vendor, NULL, m->description);
+            fprintf(fp, "</model>\n");
+        }
+        fprintf(fp, "</modelList>\n");
+    }
+
+    if (test_layouts) {
+        const struct test_layout *l, *next;
+
+        fprintf(fp, "<layoutList>\n");
+
+        l = test_layouts;
+        next = l + 1;
+
+        assert(l->variant == NULL);
+
+        while (l->name) {
+            fprintf(fp, "<layout>\n");
+            fprint_config_item(fp, l->name, NULL, l->brief, l->description);
+
+            if (next->name && streq(next->name, l->name)) {
+                fprintf(fp, "<variantList>\n");
+                do {
+                    fprintf(fp, "<variant>\n");
+                    fprint_config_item(fp, next->variant, NULL, next->brief,
+                                       next->description);
+                    fprintf(fp, "</variant>\n");
+                    l = next;
+                    next++;
+                } while (next->name && streq(next->name, l->name));
+                fprintf(fp, "</variantList>\n");
+            }
+            fprintf(fp, "</layout>\n");
+            l++;
+        }
+        fprintf(fp, "</layoutList>\n");
+    }
+
+    if (test_groups) {
+        fprintf(fp, "<optionList>\n");
+
+        for (const struct test_option_group *g = test_groups; g->name; g++) {
+            fprintf(fp, "<group allowMultipleSelection=\"%s\">\n",
+                    g->allow_multiple_selection ? "true" : "false");
+            fprint_config_item(fp, g->name, NULL, NULL, g->description);
+            for (const struct test_option *o = g->options; o->name; o++) {
+                fprintf(fp, "  <option>\n");
+                fprint_config_item(fp, o->name, NULL, NULL, o->description);
+                fprintf(fp, "</option>\n");
+            }
+            fprintf(fp, "</group>\n");
+        }
+        fprintf(fp, "</optionList>\n");
+    }
+
+    fprintf(fp, "</xkbConfigRegistry>\n");
+    fclose(fp);
+
+    return tmpdir;
+}
+
+static void
+test_remove_rules(char *basedir, const char *ruleset)
+{
+    char path[PATH_MAX];
+    int rc;
+
+    rc = snprintf_safe(path, sizeof(path), "%s/rules/%s.xml", basedir,
+                       ruleset);
+    assert(rc);
+    unlink(path);
+    rc = snprintf_safe(path, sizeof(path), "%s/xkb/rules", basedir);
+    assert(rc);
+    rmdir(path);
+    rmdir(basedir);
+    free(basedir);
+}
+
+static struct rxkb_context *
+test_setup_context_for(const char *ruleset,
+                       struct test_model *system_models,
+                       struct test_model *user_models,
+                       struct test_layout *system_layouts,
+                       struct test_layout *user_layouts,
+                       struct test_option_group *system_groups,
+                       struct test_option_group *user_groups)
+{
+    char *sysdir = NULL, *userdir = NULL;
+    struct rxkb_context *ctx;
+
+    sysdir = test_create_rules(ruleset, system_models, system_layouts,
+                               system_groups);
+    if (user_models || user_layouts || user_groups)
+        userdir = test_create_rules(ruleset, user_models, user_layouts,
+                                    user_groups);
+
+    ctx = rxkb_context_new(RXKB_CONTEXT_NO_DEFAULT_INCLUDES);
+    assert(ctx);
+    if (userdir)
+        assert(rxkb_context_include_path_append(ctx, userdir));
+    assert(rxkb_context_include_path_append(ctx, sysdir));
+    assert(rxkb_context_parse(ctx, ruleset));
+
+    test_remove_rules(sysdir, ruleset);
+    if (userdir)
+        test_remove_rules(userdir, ruleset);
+
+    return ctx;
+}
+
+static struct rxkb_context *
+test_setup_context(struct test_model *system_models,
+                   struct test_model *user_models,
+                   struct test_layout *system_layouts,
+                   struct test_layout *user_layouts,
+                   struct test_option_group *system_groups,
+                   struct test_option_group *user_groups)
+{
+    const char *ruleset = "xkbtests";
+    return test_setup_context_for(ruleset, system_models,
+                                  user_models, system_layouts,
+                                  user_layouts, system_groups,
+                                  user_groups);
+}
+
+static struct rxkb_model *
+fetch_model(struct rxkb_context *ctx, const char *model)
+{
+    struct rxkb_model *m = rxkb_model_first(ctx);
+    while (m) {
+        if (streq(rxkb_model_get_name(m), model))
+            return rxkb_model_ref(m);
+        m = rxkb_model_next(m);
+    }
+    return NULL;
+}
+
+static bool
+find_model(struct rxkb_context *ctx, const char *model)
+{
+    struct rxkb_model *m = fetch_model(ctx, model);
+    rxkb_model_unref(m);
+    return m != NULL;
+}
+
+static bool
+find_models(struct rxkb_context *ctx, ...)
+{
+    va_list args;
+    const char *name;
+    int idx = 0;
+
+    va_start(args, ctx);
+    name = va_arg(args, const char *);
+    while(name) {
+        assert(++idx < 20); /* safety guard */
+        if (!find_model(ctx, name))
+            return false;
+        name = va_arg(args, const char *);
+    };
+
+    va_end(args);
+    return true;
+}
+
+static struct rxkb_layout *
+fetch_layout(struct rxkb_context *ctx, const char *layout, const char *variant)
+{
+    struct rxkb_layout *l = rxkb_layout_first(ctx);
+    while (l) {
+        const char *v = rxkb_layout_get_variant(l);
+
+        if (streq(rxkb_layout_get_name(l), layout) &&
+            ((v == NULL && variant == NULL) ||
+             (v != NULL && variant != NULL && streq(v, variant))))
+            return rxkb_layout_ref(l);
+        l = rxkb_layout_next(l);
+    }
+    return NULL;
+}
+
+static bool
+find_layout(struct rxkb_context *ctx, const char *layout, const char *variant)
+{
+    struct rxkb_layout *l = fetch_layout(ctx, layout, variant);
+    rxkb_layout_unref(l);
+    return l != NULL;
+}
+
+static bool
+find_layouts(struct rxkb_context *ctx, ...)
+{
+    va_list args;
+    const char *name, *variant;
+    int idx = 0;
+
+    va_start(args, ctx);
+    name = va_arg(args, const char *);
+    variant = va_arg(args, const char *);
+    while(name) {
+        assert(++idx < 20); /* safety guard */
+        if (!find_layout(ctx, name, variant))
+            return false;
+        name = va_arg(args, const char *);
+        if (name)
+            variant = va_arg(args, const char *);
+    };
+
+    va_end(args);
+    return true;
+}
+
+static struct rxkb_option_group *
+fetch_option_group(struct rxkb_context *ctx, const char *grp)
+{
+    struct rxkb_option_group *g = rxkb_option_group_first(ctx);
+    while (g) {
+        if (streq(grp, rxkb_option_group_get_name(g)))
+            return rxkb_option_group_ref(g);
+        g = rxkb_option_group_next(g);
+    }
+    return NULL;
+}
+
+static inline bool
+find_option_group(struct rxkb_context *ctx, const char *grp)
+{
+    struct rxkb_option_group *g = fetch_option_group(ctx, grp);
+    rxkb_option_group_unref(g);
+    return g != NULL;
+}
+
+static struct rxkb_option *
+fetch_option(struct rxkb_context *ctx, const char *grp, const char *opt)
+{
+    struct rxkb_option_group *g = rxkb_option_group_first(ctx);
+    while (g) {
+        if (streq(grp, rxkb_option_group_get_name(g))) {
+            struct rxkb_option *o = rxkb_option_first(g);
+
+            while (o) {
+                if (streq(opt, rxkb_option_get_name(o)))
+                    return rxkb_option_ref(o);
+                o = rxkb_option_next(o);
+            }
+        }
+        g = rxkb_option_group_next(g);
+    }
+    return NULL;
+}
+
+static bool
+find_option(struct rxkb_context *ctx, const char *grp, const char *opt)
+{
+    struct rxkb_option *o = fetch_option(ctx, grp, opt);
+    rxkb_option_unref(o);
+    return o != NULL;
+}
+
+static bool
+find_options(struct rxkb_context *ctx, ...)
+{
+    va_list args;
+    const char *grp, *opt;
+    int idx = 0;
+
+    va_start(args, ctx);
+    grp = va_arg(args, const char *);
+    opt = va_arg(args, const char *);
+    while(grp) {
+        assert(++idx < 20); /* safety guard */
+        if (!find_option(ctx, grp, opt))
+            return false;
+        grp = va_arg(args, const char *);
+        if (grp)
+            opt = va_arg(args, const char *);
+    };
+
+    va_end(args);
+    return true;
+}
+
+static bool
+cmp_models(struct test_model *tm, struct rxkb_model *m)
+{
+    if (!tm || !m)
+        return false;
+
+    if (!streq(tm->name, rxkb_model_get_name(m)))
+        return false;
+
+    if (!streq_null(tm->vendor, rxkb_model_get_vendor(m)))
+        return false;
+
+    if (!streq_null(tm->description, rxkb_model_get_description(m)))
+        return false;
+
+    return true;
+}
+
+static bool
+cmp_layouts(struct test_layout *tl, struct rxkb_layout *l)
+{
+    if (!tl || !l)
+        return false;
+
+    if (!streq(tl->name, rxkb_layout_get_name(l)))
+        return false;
+
+    if (!streq_null(tl->variant, rxkb_layout_get_variant(l)))
+        return false;
+
+    if (!streq_null(tl->brief, rxkb_layout_get_brief(l)))
+        return false;
+
+    if (!streq_null(tl->description, rxkb_layout_get_description(l)))
+        return false;
+
+    return true;
+}
+
+static bool
+cmp_options(struct test_option *to, struct rxkb_option *o)
+{
+    if (!to || !o)
+        return false;
+
+    if (!streq(to->name, rxkb_option_get_name(o)))
+        return false;
+
+    if (!streq_null(to->description, rxkb_option_get_description(o)))
+        return false;
+
+    return true;
+}
+
+enum cmp_type {
+    CMP_EXACT,
+    CMP_MATCHING_ONLY,
+};
+
+static bool
+cmp_option_groups(struct test_option_group *tg, struct rxkb_option_group *g,
+                  enum cmp_type cmp)
+{
+    struct rxkb_option *o;
+    struct test_option *to;
+
+    if (!tg || !g)
+        return false;
+
+    if (!streq(tg->name, rxkb_option_group_get_name(g)))
+        return false;
+
+    if (!streq_null(tg->description, rxkb_option_group_get_description(g)))
+        return false;
+
+    if (tg->allow_multiple_selection != rxkb_option_group_allows_multiple(g))
+        return false;
+
+    to = tg->options;
+    o = rxkb_option_first(g);
+
+    while (o && to->name) {
+        if (!cmp_options(to, o))
+            return false;
+        to++;
+        o = rxkb_option_next(o);
+    }
+
+    if (cmp == CMP_EXACT && (o || to->name))
+        return false;
+
+    return true;
+}
+
+static void
+test_load_basic(void)
+{
+    struct test_model system_models[] =  {
+        {"m1"},
+        {"m2"},
+        {NULL},
+    };
+    struct test_layout system_layouts[] =  {
+        {"l1"},
+        {"l1", "v1"},
+        {NULL},
+    };
+    struct test_option_group system_groups[] = {
+        {"grp1", NULL, true,
+          { {"grp1:1"}, {"grp1:2"} } },
+        {"grp2", NULL, false,
+          { {"grp2:1"}, {"grp2:2"} } },
+        { NULL },
+    };
+    struct rxkb_context *ctx;
+
+    ctx = test_setup_context(system_models, NULL,
+                             system_layouts, NULL,
+                             system_groups, NULL);
+
+    assert(find_models(ctx, "m1", "m2", NULL));
+    assert(find_layouts(ctx, "l1", NO_VARIANT,
+                             "l1", "v1", NULL));
+    assert(find_options(ctx, "grp1", "grp1:1",
+                             "grp1", "grp1:2",
+                             "grp2", "grp2:1",
+                             "grp2", "grp2:2", NULL));
+    rxkb_context_unref(ctx);
+}
+
+static void
+test_load_full(void)
+{
+    struct test_model system_models[] =  {
+        {"m1", "vendor1", "desc1"},
+        {"m2", "vendor2", "desc2"},
+        {NULL},
+    };
+    struct test_layout system_layouts[] =  {
+        {"l1", NO_VARIANT, "lbrief1", "ldesc1"},
+        {"l1", "v1", "vbrief1", "vdesc1"},
+        {NULL},
+    };
+    struct test_option_group system_groups[] = {
+        {"grp1", "gdesc1", true,
+          { {"grp1:1", "odesc11"}, {"grp1:2", "odesc12"} } },
+        {"grp2", "gdesc2", false,
+          { {"grp2:1", "odesc21"}, {"grp2:2", "odesc22"} } },
+        { NULL },
+    };
+    struct rxkb_context *ctx;
+    struct rxkb_model *m;
+    struct rxkb_layout *l;
+    struct rxkb_option_group *g;
+
+    ctx = test_setup_context(system_models, NULL,
+                             system_layouts, NULL,
+                             system_groups, NULL);
+
+    m = fetch_model(ctx, "m1");
+    assert(cmp_models(&system_models[0], m));
+    rxkb_model_unref(m);
+
+    m = fetch_model(ctx, "m2");
+    assert(cmp_models(&system_models[1], m));
+    rxkb_model_unref(m);
+
+    l = fetch_layout(ctx, "l1", NO_VARIANT);
+    assert(cmp_layouts(&system_layouts[0], l));
+    rxkb_layout_unref(l);
+
+    l = fetch_layout(ctx, "l1", "v1");
+    assert(cmp_layouts(&system_layouts[1], l));
+    rxkb_layout_unref(l);
+
+    g = fetch_option_group(ctx, "grp1");
+    assert(cmp_option_groups(&system_groups[0], g, CMP_EXACT));
+    rxkb_option_group_unref(g);
+
+    g = fetch_option_group(ctx, "grp2");
+    assert(cmp_option_groups(&system_groups[1], g, CMP_EXACT));
+    rxkb_option_group_unref(g);
+
+    rxkb_context_unref(ctx);
+}
+
+static void
+test_popularity(void)
+{
+    struct test_layout system_layouts[] =  {
+        {"l1", NO_VARIANT },
+        {"l1", "v1" },
+        {NULL},
+    };
+    struct rxkb_context *ctx;
+    struct rxkb_layout *l;
+    const char *ruleset = "xkbtests.extras";
+    char *dir = NULL;
+
+    dir = test_create_rules(ruleset, NULL, system_layouts, NULL);
+    ctx = rxkb_context_new(RXKB_CONTEXT_NO_DEFAULT_INCLUDES |
+                           RXKB_CONTEXT_LOAD_EXOTIC_RULES);
+    assert(ctx);
+    assert(rxkb_context_include_path_append(ctx, dir));
+    /* Hack: rulest above generates xkbtests.extras.xml, loading "xkbtests"
+     * means the extras file counts as exotic */
+    assert(rxkb_context_parse(ctx, "xkbtests"));
+
+    l = fetch_layout(ctx, "l1", NO_VARIANT);
+    assert(rxkb_layout_get_popularity(l) == RXKB_POPULARITY_EXOTIC);
+    rxkb_layout_unref(l);
+
+    l = fetch_layout(ctx, "l1", "v1");
+    assert(rxkb_layout_get_popularity(l) == RXKB_POPULARITY_EXOTIC);
+    rxkb_layout_unref(l);
+
+    test_remove_rules(dir, ruleset);
+    rxkb_context_unref(ctx);
+}
+
+
+static void
+test_load_merge(void)
+{
+    struct test_model system_models[] =  {
+        {"m1", "vendor1", "desc1"},
+        {"m2", "vendor2", "desc2"},
+        {NULL},
+    };
+    struct test_model user_models[] =  {
+        {"m3", "vendor3", "desc3"},
+        {"m4", "vendor4", "desc4"},
+        {NULL},
+    };
+    struct test_layout system_layouts[] =  {
+        {"l1", NO_VARIANT, "lbrief1", "ldesc1"},
+        {"l1", "v1", "vbrief1", "vdesc1"},
+        {NULL},
+    };
+    struct test_layout user_layouts[] =  {
+        {"l2", NO_VARIANT, "lbrief2", "ldesc2"},
+        {"l2", "v2", "vbrief2", "vdesc2"},
+        {NULL},
+    };
+    struct test_option_group system_groups[] = {
+        {"grp1", NULL, true,
+          { {"grp1:1"}, {"grp1:2"} } },
+        {"grp2", NULL, false,
+          { {"grp2:1"}, {"grp2:2"} } },
+        { NULL },
+    };
+    struct test_option_group user_groups[] = {
+        {"grp3", NULL, true,
+          { {"grp3:1"}, {"grp3:2"} } },
+        {"grp4", NULL, false,
+          { {"grp4:1"}, {"grp4:2"} } },
+        { NULL },
+    };
+    struct rxkb_context *ctx;
+    struct rxkb_model *m;
+    struct rxkb_layout *l;
+    struct rxkb_option_group *g;
+
+    ctx = test_setup_context(system_models, user_models,
+                             system_layouts, user_layouts,
+                             system_groups, user_groups);
+
+    assert(find_models(ctx, "m1", "m2", "m3", "m4", NULL));
+    assert(find_layouts(ctx, "l1", NO_VARIANT,
+                             "l1", "v1",
+                             "l2", NO_VARIANT,
+                             "l2", "v2", NULL));
+
+    m = fetch_model(ctx, "m1");
+    assert(cmp_models(&system_models[0], m));
+    rxkb_model_unref(m);
+
+    m = fetch_model(ctx, "m2");
+    assert(cmp_models(&system_models[1], m));
+    rxkb_model_unref(m);
+
+    m = fetch_model(ctx, "m3");
+    assert(cmp_models(&user_models[0], m));
+    rxkb_model_unref(m);
+
+    m = fetch_model(ctx, "m4");
+    assert(cmp_models(&user_models[1], m));
+    rxkb_model_unref(m);
+
+    l = fetch_layout(ctx, "l1", NO_VARIANT);
+    assert(cmp_layouts(&system_layouts[0], l));
+    rxkb_layout_unref(l);
+
+    l = fetch_layout(ctx, "l1", "v1");
+    assert(cmp_layouts(&system_layouts[1], l));
+    rxkb_layout_unref(l);
+
+    l = fetch_layout(ctx, "l2", NO_VARIANT);
+    assert(cmp_layouts(&user_layouts[0], l));
+    rxkb_layout_unref(l);
+
+    l = fetch_layout(ctx, "l2", "v2");
+    assert(cmp_layouts(&user_layouts[1], l));
+    rxkb_layout_unref(l);
+
+    g = fetch_option_group(ctx, "grp1");
+    assert(cmp_option_groups(&system_groups[0], g, CMP_EXACT));
+    rxkb_option_group_unref(g);
+
+    g = fetch_option_group(ctx, "grp2");
+    assert(cmp_option_groups(&system_groups[1], g, CMP_EXACT));
+    rxkb_option_group_unref(g);
+
+    g = fetch_option_group(ctx, "grp3");
+    assert(cmp_option_groups(&user_groups[0], g, CMP_EXACT));
+    rxkb_option_group_unref(g);
+
+    g = fetch_option_group(ctx, "grp4");
+    assert(cmp_option_groups(&user_groups[1], g, CMP_EXACT));
+    rxkb_option_group_unref(g);
+
+    rxkb_context_unref(ctx);
+}
+
+static void
+test_load_merge_no_overwrite(void)
+{
+    struct test_model system_models[] =  {
+        {"m1", "vendor1", "desc1"},
+        {"m2", "vendor2", "desc2"},
+        {NULL},
+    };
+    struct test_model user_models[] =  {
+        {"m1", "vendor3", "desc3"}, /* must not overwrite */
+        {"m4", "vendor4", "desc4"},
+        {NULL},
+    };
+    struct test_layout system_layouts[] =  {
+        {"l1", NO_VARIANT, "lbrief1", "ldesc1"},
+        {"l1", "v1", "vbrief1", "vdesc1"},
+        {NULL},
+    };
+    struct test_layout user_layouts[] =  {
+        {"l2", NO_VARIANT, "lbrief2", "ldesc2"},
+        {"l2", "v2", "vbrief2", "vdesc2"},
+        {"l1", NO_VARIANT, "lbrief3", "ldesc3"}, /* must not overwrite */
+        {"l1", "v2", "vbrief3", "vdesc3"}, /* must not overwrite */
+        {NULL},
+    };
+    struct test_option_group system_groups[] = {
+        {"grp1", "gdesc1", true,
+          { {"grp1:1", "odesc11"}, {"grp1:2", "odesc12"} } },
+        {"grp2", "gdesc2", false,
+          { {"grp2:1", "odesc21"}, {"grp2:2", "odesc22"} } },
+        { NULL },
+    };
+    struct test_option_group user_groups[] = {
+        {"grp1", "XXXXX", false, /* must not overwrite */
+          { {"grp1:1", "YYYYYYY"}, /* must not overwrite */
+            {"grp1:3", "ZZZZZZ"} } }, /* append */
+        {"grp4", "gdesc4", false,
+          { {"grp4:1", "odesc41"}, {"grp4:2", "odesc42"} } },
+        { NULL },
+    };
+    struct rxkb_context *ctx;
+    struct rxkb_model *m;
+    struct rxkb_layout *l;
+    struct rxkb_option_group *g;
+
+    ctx = test_setup_context(system_models, user_models,
+                             system_layouts, user_layouts,
+                             system_groups, user_groups);
+
+    m = fetch_model(ctx, "m1");
+    assert(cmp_models(&system_models[0], m));
+    rxkb_model_unref(m);
+
+    l = fetch_layout(ctx, "l1", NO_VARIANT);
+    assert(cmp_layouts(&system_layouts[0], l));
+    rxkb_layout_unref(l);
+
+    l = fetch_layout(ctx, "l1", "v1");
+    assert(cmp_layouts(&system_layouts[1], l));
+    rxkb_layout_unref(l);
+
+    assert(find_option(ctx, "grp1", "grp1:3"));
+    g = fetch_option_group(ctx, "grp1");
+    assert(cmp_option_groups(&system_groups[0], g, CMP_MATCHING_ONLY));
+    rxkb_option_group_unref(g);
+
+    rxkb_context_unref(ctx);
+}
+
+static void
+test_no_include_paths(void)
+{
+    struct rxkb_context *ctx;
+
+    ctx = rxkb_context_new(RXKB_CONTEXT_NO_DEFAULT_INCLUDES);
+    assert(ctx);
+    assert(!rxkb_context_parse_default_ruleset(ctx));
+
+    rxkb_context_unref(ctx);
+}
+
+static void
+test_invalid_include(void)
+{
+    struct rxkb_context *ctx;
+
+    ctx = rxkb_context_new(RXKB_CONTEXT_NO_DEFAULT_INCLUDES);
+    assert(ctx);
+    assert(!rxkb_context_include_path_append(ctx, "/foo/bar/baz/bat"));
+    assert(!rxkb_context_parse_default_ruleset(ctx));
+
+    rxkb_context_unref(ctx);
+}
+
+int
+main(void)
+{
+    test_no_include_paths();
+    test_invalid_include();
+    test_load_basic();
+    test_load_full();
+    test_load_merge();
+    test_load_merge_no_overwrite();
+    test_popularity();
+
+    return 0;
+}
diff --git a/tools/registry-list.c b/tools/registry-list.c
new file mode 100644
index 000000000..e51c41fb7
--- /dev/null
+++ b/tools/registry-list.c
@@ -0,0 +1,223 @@
+/*
+ * 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 <assert.h>
+#include <stdio.h>
+#include <getopt.h>
+
+#include "xkbcommon/xkbregistry.h"
+
+static void
+usage(const char *progname)
+{
+    fprintf(stderr,
+            "Usage: %s [OPTIONS] [/path/to/xkb_base_directory [/path2]...]\n"
+            "\n"
+            "Options:\n"
+            "  --verbose, -v .......... Increase verbosity, use multiple times for debugging output\n"
+            "  --ruleset=foo .......... Load the 'foo' ruleset\n"
+            "  --skip-default-paths ... Do not load the default XKB paths\n"
+            "  --load-exotic .......... Load the exotic (extra) rulesets\n"
+            "\n"
+            "Trailing arguments are treated as XKB base directory installations.\n",
+            progname);
+}
+
+int
+main(int argc, char **argv)
+{
+    int rc = 1;
+    struct rxkb_context *ctx = NULL;
+    struct rxkb_model *m;
+    struct rxkb_layout *l;
+    struct rxkb_option_group *g;
+    enum rxkb_context_flags flags = RXKB_CONTEXT_NO_FLAGS;
+    bool load_defaults = true;
+    int verbosity = 0;
+    const char *ruleset = DEFAULT_XKB_RULES;
+
+    static const struct option opts[] = {
+        {"help",                no_argument,        0, 'h'},
+        {"verbose",             no_argument,        0, 'v'},
+        {"load-exotic",         no_argument,        0, 'e'},
+        {"skip-default-paths",  no_argument,        0, 'd'},
+        {"ruleset",             required_argument,  0, 'r'},
+        {0, 0, 0, 0},
+    };
+
+    while (1) {
+        int c;
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "hev", opts, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+            case 'h':
+            case '?':
+                usage(argv[0]);
+                return 1;
+            case 'd':
+                load_defaults = false;
+                break;
+            case 'e':
+                flags |= RXKB_CONTEXT_LOAD_EXOTIC_RULES;
+                break;
+            case 'r':
+                ruleset = optarg;
+                break;
+            case 'v':
+                verbosity++;
+                break;
+        }
+    }
+
+    if (optind < argc)
+        flags |= RXKB_CONTEXT_NO_DEFAULT_INCLUDES;
+
+    ctx = rxkb_context_new(flags);
+    assert(ctx);
+
+    switch (verbosity) {
+        case 0:
+            rxkb_context_set_log_level(ctx, RXKB_LOG_LEVEL_ERROR);
+            break;
+        case 1:
+            rxkb_context_set_log_level(ctx, RXKB_LOG_LEVEL_INFO);
+            break;
+        default:
+            rxkb_context_set_log_level(ctx, RXKB_LOG_LEVEL_DEBUG);
+            break;
+    }
+
+    if (optind < argc) {
+        for (int i = optind; i < argc; i++) {
+            if (!rxkb_context_include_path_append(ctx, argv[i])) {
+                fprintf(stderr, "Failed to append include path '%s'\n",
+                        argv[i]);
+                goto err;
+            }
+        }
+
+        if (load_defaults) {
+            if (!rxkb_context_include_path_append_default(ctx)) {
+                fprintf(stderr, "Failed to include default paths.\n");
+                goto err;
+            }
+        }
+    }
+    if (!rxkb_context_parse(ctx, ruleset)) {
+        fprintf(stderr, "Failed to parse XKB descriptions.\n");
+        goto err;
+    }
+
+    printf("Models:\n");
+    m = rxkb_model_first(ctx);
+    assert(m); /* Empty model list is usually a bug or a bad xml file */
+    while (m) {
+        printf("- %s:%s:%s\n",
+               rxkb_model_get_name(m),
+               rxkb_model_get_vendor(m),
+               rxkb_model_get_description(m));
+        m = rxkb_model_next(m);
+    }
+
+    printf("\n");
+    printf("Layouts:\n");
+    l = rxkb_layout_first(ctx);
+    assert(l); /* Empty layout list is usually a bug or a bad xml file */
+    while (l) {
+        struct rxkb_iso639_code *iso639;
+        struct rxkb_iso3166_code *iso3166;
+        const char *variant = rxkb_layout_get_variant(l);
+        const char *brief = rxkb_layout_get_brief(l);
+        bool first;
+
+        printf("- %s%s%s%s:%s:%s",
+               rxkb_layout_get_name(l),
+               variant ? "(" : "",
+               variant ? variant : "",
+               variant ? ")" : "",
+               brief ? brief : "",
+               rxkb_layout_get_description(l));
+
+        iso639 = rxkb_layout_get_iso639_first(l);
+        if (iso639)
+            printf(":iso639-");
+        first = true;
+        while (iso639) {
+            printf("%s%s", first ? "" : ",", rxkb_iso639_code_get_code(iso639));
+            iso639 = rxkb_iso639_code_next(iso639);
+            first = false;
+        }
+        iso3166 = rxkb_layout_get_iso3166_first(l);
+        if (iso3166)
+            printf(":iso3166-");
+        first = true;
+        while (iso3166) {
+            printf("%s%s", first ? "" : ",", rxkb_iso3166_code_get_code(iso3166));
+            iso3166 = rxkb_iso3166_code_next(iso3166);
+            first = false;
+        }
+
+        printf("\n");
+        l = rxkb_layout_next(l);
+    }
+    printf("\n");
+    printf("Options:\n");
+    g = rxkb_option_group_first(ctx);
+    assert(g); /* Empty option goups list is usually a bug or a bad xml file */
+    while (g) {
+        struct rxkb_option *o;
+
+        printf("- %s:%s (%s)\n",
+               rxkb_option_group_get_name(g),
+               rxkb_option_group_get_description(g),
+               rxkb_option_group_allows_multiple(g) ? "multiple" : "single");
+
+        o = rxkb_option_first(g);
+        assert(o); /* Empty option list is usually a bug or a bad xml file */
+        while (o) {
+            const char *brief = rxkb_option_get_brief(o);
+
+            printf("  - %s:%s:%s\n",
+                   rxkb_option_get_name(o),
+                   brief ? brief : "",
+                   rxkb_option_get_description(o));
+            o = rxkb_option_next(o);
+        }
+
+        g = rxkb_option_group_next(g);
+    }
+
+    rc = 0;
+
+err:
+    if (ctx)
+        rxkb_context_unref(ctx);
+
+    return rc;
+}
diff --git a/xkbcommon/xkbregistry.h b/xkbcommon/xkbregistry.h
new file mode 100644
index 000000000..4e7e926c4
--- /dev/null
+++ b/xkbcommon/xkbregistry.h
@@ -0,0 +1,782 @@
+/*
+ * 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.
+ */
+
+
+#ifndef _XKBREGISTRY_H_
+#define _XKBREGISTRY_H_
+
+#include <stdarg.h>
+#include <stdbool.h>
+
+/**
+ * @file
+ * @brief Query for available RMLVO
+ *
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @defgroup registry Query for available RMLVO
+ *
+ * The libxkbregistry API to query for available rules, models, layouts,
+ * variants and options (RMLVO). libxkbregistry is a separate library to
+ * libxkbcommon.
+ *
+ * This library is the replacement for clients currently parsing evdev.xml
+ * directly. The library is intended to provide easy access to the set of
+ * **possible** MLVO configurations for a given ruleset. It is not a library to
+ * apply these configurations, merely to enumerate them. The intended users of
+ * this library are the configuration UIs that allow a user to select their
+ * keyboard layout of choice.
+ *
+ * @{
+ */
+
+/**
+ * @struct rxkb_context
+ *
+ * Opaque top level library context object.
+ *
+ * The context contains general library state, like include paths and parsed
+ * data. Objects are created in a specific context, and multiple contexts
+ * may coexist simultaneously. Objects from different contexts are
+ * completely separated and do not share any memory or state.
+ */
+struct rxkb_context;
+
+/**
+ * @struct rxkb_model
+ *
+ * Opaque struct representing an XKB model.
+ */
+struct rxkb_model;
+
+/**
+ * @struct rxkb_layout
+ *
+ * Opaque struct representing an XKB layout, including an optional variant.
+ * Where the variant is NULL, the layout is the base layout.
+ *
+ * For example, "us" is the base layout, "us(intl)" is the "intl" variant of the
+ * layout "us".
+ */
+struct rxkb_layout;
+
+/**
+ * @struct rxkb_option_group
+ *
+ * Opaque struct representing an option group. Option groups divide the
+ * individual options into logical groups. Their main purpose is to indicate
+ * whether some options are mutually exclusive or not.
+ */
+struct rxkb_option_group;
+
+/**
+ * @struct rxkb_option
+ *
+ * Opaque struct representing an XKB option. Options are grouped inside an @ref
+ * rxkb_option_group.
+ */
+struct rxkb_option;
+
+/**
+ *
+ * @struct rxkb_iso639_code
+ *
+ * Opaque struct representing an ISO 639-3 code (e.g. "eng", "fra"). There
+ * is no guarantee that two identical ISO codes share the same struct. You
+ * must not rely on the pointer value of this struct.
+ *
+ * See https://iso639-3.sil.org/code_tables/639/data for a list of codes.
+ */
+struct rxkb_iso639_code;
+
+/**
+ *
+ * @struct rxkb_iso3166_code
+ *
+ * Opaque struct representing an ISO 3166 Alpha 2 code (e.g. "US", "FR").
+ * There is no guarantee that two identical ISO codes share the same struct.
+ * You must not rely on the pointer value of this struct.
+ *
+ * See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes for a list
+ * of codes.
+ */
+struct rxkb_iso3166_code;
+
+/**
+ * Describes the popularity of an item. Historically, some highly specialized or
+ * experimental definitions are excluded from the default list and shipped in
+ * separate files. If these extra definitions are loaded (see @ref
+ * RXKB_CONTEXT_LOAD_EXOTIC_RULES), the popularity of the item is set
+ * accordingly.
+ *
+ * If the exotic items are not loaded, all items will have the standard
+ * popularity.
+ */
+enum rxkb_popularity {
+    RXKB_POPULARITY_STANDARD = 1,
+    RXKB_POPULARITY_EXOTIC,
+};
+
+/**
+ * Flags for context creation.
+ */
+enum rxkb_context_flags {
+    RXKB_CONTEXT_NO_FLAGS = 0,
+    /**
+     * Skip the default include paths. This requires the caller to call
+     * rxkb_context_include_path_append() or
+     * rxkb_context_include_path_append_default().
+     */
+    RXKB_CONTEXT_NO_DEFAULT_INCLUDES = (1 << 0),
+    /**
+     * Load the extra items that are considered too exotic for the default list.
+     *
+     * For historical reasons, xkeyboard-config ships those exotic rules in a
+     * separate file (e.g. `evdev.extras.xml`). Where the exotic rules are
+     * requested, libxkbregistry will look for and load `$ruleset.extras.xml`
+     * in the include paths, see rxkb_context_include_path_append() for details
+     * on the lookup behavior.
+     */
+    RXKB_CONTEXT_LOAD_EXOTIC_RULES = (1 << 1),
+};
+
+/**
+ * Create a new xkb registry context.
+ *
+ * The context has an initial refcount of 1. Use rxkb_context_unref() to release
+ * memory associated with this context.
+ *
+ * Creating a context does not parse the files yet, use
+ * rxkb_context_parse().
+ *
+ * @param flags Flags affecting context behavior
+ * @return A new xkb registry context or NULL on failure
+ */
+struct rxkb_context *
+rxkb_context_new(enum rxkb_context_flags flags);
+
+/** Specifies a logging level. */
+enum rxkb_log_level {
+    RXKB_LOG_LEVEL_CRITICAL = 10, /**< Log critical internal errors only. */
+    RXKB_LOG_LEVEL_ERROR = 20,    /**< Log all errors. */
+    RXKB_LOG_LEVEL_WARNING = 30,  /**< Log warnings and errors. */
+    RXKB_LOG_LEVEL_INFO = 40,     /**< Log information, warnings, and errors. */
+    RXKB_LOG_LEVEL_DEBUG = 50     /**< Log everything. */
+};
+
+/**
+ * Set the current logging level.
+ *
+ * @param ctx     The context in which to set the logging level.
+ * @param level   The logging level to use.  Only messages from this level
+ * and below will be logged.
+ *
+ * The default level is RXKB_LOG_LEVEL_ERROR.  The environment variable
+ * RXKB_LOG_LEVEL, if set at the time the context was created, overrides the
+ * default value.  It may be specified as a level number or name.
+ */
+void
+rxkb_context_set_log_level(struct rxkb_context *ctx,
+                           enum rxkb_log_level level);
+
+/**
+ * Get the current logging level.
+ */
+enum rxkb_log_level
+rxkb_context_get_log_level(struct rxkb_context *ctx);
+
+/**
+ * Set a custom function to handle logging messages.
+ *
+ * @param ctx     The context in which to use the set logging function.
+ * @param log_fn  The function that will be called for logging messages.
+ * Passing NULL restores the default function, which logs to stderr.
+ *
+ * By default, log messages from this library are printed to stderr.  This
+ * function allows you to replace the default behavior with a custom
+ * handler.  The handler is only called with messages which match the
+ * current logging level and verbosity settings for the context.
+ * level is the logging level of the message.  @a format and @a args are
+ * the same as in the vprintf(3) function.
+ *
+ * You may use rxkb_context_set_user_data() on the context, and then call
+ * rxkb_context_get_user_data() from within the logging function to provide
+ * it with additional private context.
+ */
+void
+rxkb_context_set_log_fn(struct rxkb_context *ctx,
+                        void (*log_fn)(struct rxkb_context *ctx,
+                                       enum rxkb_log_level level,
+                                       const char *format, va_list args));
+
+
+/**
+ * Parse the given ruleset. This can only be called once per context and once
+ * parsed the data in the context is considered constant and will never
+ * change.
+ *
+ * This function parses all files with the given ruleset name. See
+ * rxkb_context_include_path_append() for details.
+ *
+ * If this function returns false, libxkbregistry failed to parse the xml files.
+ * This is usually caused by invalid files on the host and should be debugged by
+ * the host's administrator using external tools. Callers should reduce the
+ * include paths to known good paths and/or fall back to a default RMLVO set.
+ *
+ * If this function returns false, the context should be be considered dead and
+ * must be released with rxkb_context_unref().
+ *
+ * @param ctx The xkb registry context
+ * @param ruleset The ruleset to parse, e.g. "evdev"
+ * @return true on success or false on failure
+ */
+bool
+rxkb_context_parse(struct rxkb_context *ctx, const char *ruleset);
+
+/**
+ * Parse the default ruleset as configured at build time. See
+ * rxkb_context_parse() for details.
+ */
+bool
+rxkb_context_parse_default_ruleset(struct rxkb_context *ctx);
+
+/**
+ * Increases the refcount of this object by one and returns the object.
+ *
+ * @param ctx The xkb registry context
+ * @return The passed in object
+ */
+struct rxkb_context*
+rxkb_context_ref(struct rxkb_context *ctx);
+
+/**
+ * Decreases the refcount of this object by one. Where the refcount of an
+ * object hits zero, associated resources will be freed.
+ *
+ * @param ctx The xkb registry context
+ * @return always NULL
+ */
+struct rxkb_context*
+rxkb_context_unref(struct rxkb_context *ctx);
+
+/**
+ * Assign user-specific data. libxkbregistry will not look at or modify the
+ * data, it will merely return the same pointer in
+ * rxkb_context_get_user_data().
+ *
+ * @param ctx The xkb registry context
+ * @param user_data User-specific data pointer
+ */
+void
+rxkb_context_set_user_data(struct rxkb_context *ctx, void *user_data);
+
+/**
+ * Return the pointer passed into rxkb_context_get_user_data().
+ *
+ * @param ctx The xkb registry context
+ * @return User-specific data pointer
+ */
+void *
+rxkb_context_get_user_data(struct rxkb_context *ctx);
+
+/**
+ * Append a new entry to the context's include path.
+ *
+ * The include path handling is optimized for the most common use-case: a set of
+ * system files that provide a complete set of MLVO and some
+ * custom MLVO provided by a user **in addition** to the system set.
+ *
+ * The include paths should be given so that the least complete path is
+ * specified first and the most complete path is appended last. For example:
+ *
+ * @code
+ *    ctx = rxkb_context_new(RXKB_CONTEXT_NO_DEFAULT_INCLUDES);
+ *    rxkb_context_include_path_append(ctx, "/home/user/.config/xkb");
+ *    rxkb_context_include_path_append(ctx, "/usr/share/X11/xkb");
+ *    rxkb_context_parse(ctx, "evdev");
+ * @endcode
+ *
+ * The above example reflects the default behavior unless @ref
+ * RXKB_CONTEXT_NO_DEFAULT_INCLUDES is provided.
+ *
+ * Loading of the files is in **reverse order**, i.e. the last path appended is
+ * loaded first - in this case the ``/usr/share/X11/xkb`` path.
+ * Any models, layouts, variants and options defined in the "evdev" ruleset
+ * are loaded into the context. Then, any RMLVO found in the "evdev" ruleset of
+ * the user's path (``/home/user/.config/xkb`` in this example) are **appended**
+ * to the existing set.
+ *
+ * Note that data from previously loaded include paths is never overwritten,
+ * only appended to. It is not not possible to change the system-provided data,
+ * only to append new models, layouts, variants and options to it.
+ *
+ * In other words, to define a new variant of the "us" layout called "banana",
+ * the following XML is sufficient.
+ *
+ * @verbatim
+ * <xkbConfigRegistry version="1.1">
+ * <layoutList>
+ *   <layout>
+ *     <configItem>
+ *       <name>us</name>
+ *     </configItem>
+ *     <variantList>
+ *       <variant>
+ *         <configItem>
+ *          <name>banana</name>
+ *          <description>English (Banana)</description>
+ *        </configItem>
+ *      </variant>
+ *    </layout>
+ * </layoutList>
+ * </xkbConfigRegistry>
+ * @endverbatim
+ *
+ * The list of models, options and all other layouts (including "us" and its
+ * variants) is taken from the system files. The resulting list of layouts will
+ * thus have a "us" keyboard layout with the variant "banana" and all other
+ * system-provided variants (dvorak, colemak, intl, etc.)
+ *
+ * This function must be called before rxkb_context_parse() or
+ * rxkb_context_parse_default_ruleset().
+ *
+ * @returns true on success, or false if the include path could not be added
+ * or is inaccessible.
+ */
+bool
+rxkb_context_include_path_append(struct rxkb_context *ctx, const char *path);
+
+/**
+ * Append the default include paths to the context's include path.
+ * See rxkb_context_include_path_append() for details about the merge order.
+ *
+ * This function must be called before rxkb_context_parse() or
+ * rxkb_context_parse_default_ruleset().
+ *
+ * @returns true on success, or false if the include path could not be added
+ * or is inaccessible.
+ */
+bool
+rxkb_context_include_path_append_default(struct rxkb_context *ctx);
+
+/**
+ * Return the first model for this context. Use this to start iterating over
+ * the models, followed by calls to rxkb_model_next(). Models are not sorted.
+ *
+ * The refcount of the returned model is not increased. Use rxkb_model_ref() if
+ * you need to keep this struct outside the immediate scope.
+ *
+ * @return The first model in the model list.
+ */
+struct rxkb_model *
+rxkb_model_first(struct rxkb_context *ctx);
+
+/**
+ * Return the next model for this context. Returns NULL when no more models
+ * are available.
+ *
+ * The refcount of the returned model is not increased. Use rxkb_model_ref() if
+ * you need to keep this struct outside the immediate scope.
+ *
+ * @return the next model or NULL at the end of the list
+ */
+struct rxkb_model *
+rxkb_model_next(struct rxkb_model *m);
+
+/**
+ * Increase the refcount of the argument by one.
+ *
+ * @returns The argument passed in to this function.
+ */
+struct rxkb_model *
+rxkb_model_ref(struct rxkb_model *m);
+
+/**
+ * Decrease the refcount of the argument by one. When the refcount hits zero,
+ * all memory associated with this struct is freed.
+ *
+ * @returns always NULL
+ */
+struct rxkb_model *
+rxkb_model_unref(struct rxkb_model *m);
+
+/**
+ * Return the name of this model. This is the value for M in RMLVO, to be used
+ * with libxkbcommon.
+ */
+const char *
+rxkb_model_get_name(struct rxkb_model *m);
+
+/**
+ * Return a human-readable description of this model. This function may return
+ * NULL.
+ */
+const char *
+rxkb_model_get_description(struct rxkb_model *m);
+
+/**
+ * Return the vendor name for this model. This function may return NULL.
+ */
+const char *
+rxkb_model_get_vendor(struct rxkb_model *m);
+
+/**
+ * Return the popularity for this model.
+ */
+enum rxkb_popularity
+rxkb_model_get_popularity(struct rxkb_model *m);
+
+/**
+ * Return the first layout for this context. Use this to start iterating over
+ * the layouts, followed by calls to rxkb_layout_next(). Layouts are not sorted.
+ *
+ * The refcount of the returned layout is not increased. Use rxkb_layout_ref() if
+ * you need to keep this struct outside the immediate scope.
+ *
+ * @return The first layout in the layout list.
+ */
+struct rxkb_layout *
+rxkb_layout_first(struct rxkb_context *ctx);
+
+/**
+ * Return the next layout for this context. Returns NULL when no more layouts
+ * are available.
+ *
+ * The refcount of the returned layout is not increased. Use rxkb_layout_ref()
+ * if you need to keep this struct outside the immediate scope.
+ *
+ * @return the next layout or NULL at the end of the list
+ */
+struct rxkb_layout *
+rxkb_layout_next(struct rxkb_layout *l);
+
+/**
+ * Increase the refcount of the argument by one.
+ *
+ * @returns The argument passed in to this function.
+ */
+struct rxkb_layout *
+rxkb_layout_ref(struct rxkb_layout *l);
+
+/**
+ * Decrease the refcount of the argument by one. When the refcount hits zero,
+ * all memory associated with this struct is freed.
+ *
+ * @returns always NULL
+ */
+struct rxkb_layout *
+rxkb_layout_unref(struct rxkb_layout *l);
+
+/**
+ * Return the name of this layout. This is the value for L in RMLVO, to be used
+ * with libxkbcommon.
+ */
+const char *
+rxkb_layout_get_name(struct rxkb_layout *l);
+
+/**
+ * Return the variant of this layout. This is the value for V in RMLVO, to be
+ * used with libxkbcommon.
+ *
+ * A variant does not stand on its own, it always depends on the base layout.
+ * e.g. there may be multiple variants called "intl" but there is only one
+ * "us(intl)".
+ *
+ * Where the variant is NULL, the layout is the base layout (e.g. "us").
+ */
+const char *
+rxkb_layout_get_variant(struct rxkb_layout *l);
+
+/**
+ * Return a short (one-word) description of this layout. This function may
+ * return NULL.
+ */
+const char *
+rxkb_layout_get_brief(struct rxkb_layout *l);
+
+/**
+ * Return a human-readable description of this layout. This function may return
+ * NULL.
+ */
+const char *
+rxkb_layout_get_description(struct rxkb_layout *l);
+
+/**
+ * Return the popularity for this layout.
+ */
+enum rxkb_popularity
+rxkb_layout_get_popularity(struct rxkb_layout *l);
+
+/**
+ * Return the first option group for this context. Use this to start iterating
+ * over the option groups, followed by calls to rxkb_option_group_next().
+ * Option groups are not sorted.
+ *
+ * The refcount of the returned option group is not increased. Use
+ * rxkb_option_group_ref() if you need to keep this struct outside the immediate
+ * scope.
+ *
+ * @return The first option group in the option group list.
+ */
+struct rxkb_option_group *
+rxkb_option_group_first(struct rxkb_context *ctx);
+
+/**
+ * Return the next option group for this context. Returns NULL when no more
+ * option groups are available.
+ *
+ * The refcount of the returned option group is not increased. Use
+ * rxkb_option_group_ref() if you need to keep this struct outside the immediate
+ * scope.
+ *
+ * @return the next option group or NULL at the end of the list
+ */
+struct rxkb_option_group *
+rxkb_option_group_next(struct rxkb_option_group *g);
+
+/**
+ * Increase the refcount of the argument by one.
+ *
+ * @returns The argument passed in to this function.
+ */
+struct rxkb_option_group *
+rxkb_option_group_ref(struct rxkb_option_group *g);
+
+/**
+ * Decrease the refcount of the argument by one. When the refcount hits zero,
+ * all memory associated with this struct is freed.
+ *
+ * @returns always NULL
+ */
+struct rxkb_option_group *
+rxkb_option_group_unref(struct rxkb_option_group *g);
+
+/**
+ * Return the name of this option group. This is **not** the value for O in
+ * RMLVO, the name can be used for internal sorting in the caller. This function
+ * may return NULL.
+ */
+const char *
+rxkb_option_group_get_name(struct rxkb_option_group *m);
+
+/**
+ * Return a human-readable description of this option group. This function may
+ * return NULL.
+ */
+const char *
+rxkb_option_group_get_description(struct rxkb_option_group *m);
+
+/**
+ * @return true if multiple options within this option group can be selected
+ *              simultaneously, false if all options within this option group
+ *              are mutually exclusive.
+ */
+bool
+rxkb_option_group_allows_multiple(struct rxkb_option_group *g);
+
+/**
+ * Return the popularity for this option group.
+ */
+enum rxkb_popularity
+rxkb_option_group_get_popularity(struct rxkb_option_group *g);
+
+/**
+ * Return the first option for this option group. Use this to start iterating
+ * over the options, followed by calls to rxkb_option_next(). Options are not
+ * sorted.
+ *
+ * The refcount of the returned option is not increased. Use rxkb_option_ref()
+ * if you need to keep this struct outside the immediate scope.
+ *
+ * @return The first option in the option list.
+ */
+struct rxkb_option *
+rxkb_option_first(struct rxkb_option_group *group);
+
+/**
+ * Return the next option for this option group. Returns NULL when no more
+ * options are available.
+ *
+ * The refcount of the returned options is not increased. Use rxkb_option_ref()
+ * if you need to keep this struct outside the immediate scope.
+ *
+ * @returns The next option or NULL at the end of the list
+ */
+struct rxkb_option *
+rxkb_option_next(struct rxkb_option *o);
+
+/**
+ * Increase the refcount of the argument by one.
+ *
+ * @returns The argument passed in to this function.
+ */
+struct rxkb_option *
+rxkb_option_ref(struct rxkb_option *o);
+
+/**
+ * Decrease the refcount of the argument by one. When the refcount hits zero,
+ * all memory associated with this struct is freed.
+ *
+ * @returns always NULL
+ */
+struct rxkb_option *
+rxkb_option_unref(struct rxkb_option *o);
+
+/**
+ * Return the name of this option. This is the value for O in RMLVO, to be used
+ * with libxkbcommon.
+ */
+const char *
+rxkb_option_get_name(struct rxkb_option *o);
+
+/**
+ * Return a short (one-word) description of this option. This function may
+ * return NULL.
+ */
+const char *
+rxkb_option_get_brief(struct rxkb_option *o);
+
+/**
+ * Return a human-readable description of this option. This function may return
+ * NULL.
+ */
+const char *
+rxkb_option_get_description(struct rxkb_option *o);
+
+/**
+ * Return the popularity for this option.
+ */
+enum rxkb_popularity
+rxkb_option_get_popularity(struct rxkb_option *o);
+
+/**
+ * Increase the refcount of the argument by one.
+ *
+ * @returns The argument passed in to this function.
+ */
+struct rxkb_iso639_code *
+rxkb_iso639_code_ref(struct rxkb_iso639_code *iso639);
+
+/**
+ * Decrease the refcount of the argument by one. When the refcount hits zero,
+ * all memory associated with this struct is freed.
+ *
+ * @returns always NULL
+ */
+struct rxkb_iso639_code *
+rxkb_iso639_code_unref(struct rxkb_iso639_code *iso639);
+
+/**
+ * Return the ISO 639-3 code for this code (e.g. "eng", "fra").
+ */
+const char *
+rxkb_iso639_code_get_code(struct rxkb_iso639_code *iso639);
+
+/**
+ * Return the first ISO 639 for this layout. Use this to start iterating over
+ * the codes, followed by calls to rxkb_iso639_code_next(). Codes are not
+ * sorted.
+ *
+ * The refcount of the returned code is not increased. Use rxkb_iso639_code_ref()
+ * if you need to keep this struct outside the immediate scope.
+ *
+ * @return The first code in the code list.
+ */
+struct rxkb_iso639_code *
+rxkb_layout_get_iso639_first(struct rxkb_layout *layout);
+
+/**
+ * Return the next code in the list. Returns NULL when no more codes
+ * are available.
+ *
+ * The refcount of the returned codes is not increased. Use
+ * rxkb_iso639_code_ref() if you need to keep this struct outside the immediate
+ * scope.
+ *
+ * @returns The next code or NULL at the end of the list
+ */
+struct rxkb_iso639_code *
+rxkb_iso639_code_next(struct rxkb_iso639_code *iso639);
+
+/**
+ * Increase the refcount of the argument by one.
+ *
+ * @returns The argument passed in to this function.
+ */
+struct rxkb_iso3166_code *
+rxkb_iso3166_code_ref(struct rxkb_iso3166_code *iso3166);
+
+/**
+ * Decrease the refcount of the argument by one. When the refcount hits zero,
+ * all memory associated with this struct is freed.
+ *
+ * @returns always NULL
+ */
+struct rxkb_iso3166_code *
+rxkb_iso3166_code_unref(struct rxkb_iso3166_code *iso3166);
+
+/**
+ * Return the ISO 3166 Alpha 2 code for this code (e.g. "US", "FR").
+ */
+const char *
+rxkb_iso3166_code_get_code(struct rxkb_iso3166_code *iso3166);
+
+/**
+ * Return the first ISO 3166 for this layout. Use this to start iterating over
+ * the codes, followed by calls to rxkb_iso3166_code_next(). Codes are not
+ * sorted.
+ *
+ * The refcount of the returned code is not increased. Use
+ * rxkb_iso3166_code_ref() if you need to keep this struct outside the immediate
+ * scope.
+ *
+ * @return The first code in the code list.
+ */
+struct rxkb_iso3166_code *
+rxkb_layout_get_iso3166_first(struct rxkb_layout *layout);
+
+/**
+ * Return the next code in the list. Returns NULL when no more codes
+ * are available.
+ *
+ * The refcount of the returned codes is not increased. Use
+ * rxkb_iso3166_code_ref() if you need to keep this struct outside the immediate
+ * scope.
+ *
+ * @returns The next code or NULL at the end of the list
+ */
+struct rxkb_iso3166_code *
+rxkb_iso3166_code_next(struct rxkb_iso3166_code *iso3166);
+
+/** @} */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* _XKBREGISTRY_H_ */
diff --git a/xkbregistry.map b/xkbregistry.map
new file mode 100644
index 000000000..6e18b9c35
--- /dev/null
+++ b/xkbregistry.map
@@ -0,0 +1,61 @@
+/* versions are kept in sync with libxkbcommon.so */
+V_0.11.0 {
+global:
+        rxkb_context_new;
+        rxkb_context_parse;
+        rxkb_context_parse_default_ruleset;
+        rxkb_context_ref;
+        rxkb_context_unref;
+        rxkb_context_set_user_data;
+        rxkb_context_get_user_data;
+        rxkb_context_set_log_level;
+        rxkb_context_get_log_level;
+        rxkb_context_set_log_fn;
+        rxkb_context_include_path_append;
+        rxkb_context_include_path_append_default;
+        rxkb_model_first;
+        rxkb_model_next;
+        rxkb_model_ref;
+        rxkb_model_unref;
+        rxkb_model_get_name;
+        rxkb_model_get_description;
+        rxkb_model_get_vendor;
+        rxkb_model_get_popularity;
+        rxkb_layout_first;
+        rxkb_layout_next;
+        rxkb_layout_ref;
+        rxkb_layout_unref;
+        rxkb_layout_get_name;
+        rxkb_layout_get_brief;
+        rxkb_layout_get_description;
+        rxkb_layout_get_variant;
+        rxkb_layout_get_popularity;
+        rxkb_option_group_first;
+        rxkb_option_group_next;
+        rxkb_option_group_ref;
+        rxkb_option_group_unref;
+        rxkb_option_group_get_name;
+        rxkb_option_group_get_description;
+        rxkb_option_group_allows_multiple;
+        rxkb_option_group_get_popularity;
+        rxkb_option_first;
+        rxkb_option_next;
+        rxkb_option_ref;
+        rxkb_option_unref;
+        rxkb_option_get_name;
+        rxkb_option_get_brief;
+        rxkb_option_get_description;
+        rxkb_option_get_popularity;
+        rxkb_layout_get_iso639_first;
+        rxkb_iso639_code_next;
+        rxkb_iso639_code_ref;
+        rxkb_iso639_code_unref;
+        rxkb_iso639_code_get_code;
+        rxkb_layout_get_iso3166_first;
+        rxkb_iso3166_code_next;
+        rxkb_iso3166_code_ref;
+        rxkb_iso3166_code_unref;
+        rxkb_iso3166_code_get_code;
+local:
+	*;
+};
