/* * 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 #include #include #include #include #include #include #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); ATTR_PRINTF(3, 4) static void rxkb_log(struct rxkb_context *ctx, enum rxkb_log_level level, const char *fmt, ...) { va_list args; if (ctx->log_level < level) return; va_start(args, fmt); ctx->log_fn(ctx, level, fmt, args); va_end(args); } /* * The format is not part of the argument list in order to avoid the * "ISO C99 requires rest arguments to be used" warning when only the * format is supplied without arguments. Not supplying it would still * result in an error, though. */ #define log_dbg(ctx, ...) \ rxkb_log((ctx), RXKB_LOG_LEVEL_DEBUG, __VA_ARGS__) #define log_info(ctx, ...) \ rxkb_log((ctx), RXKB_LOG_LEVEL_INFO, __VA_ARGS__) #define log_warn(ctx, ...) \ rxkb_log((ctx), RXKB_LOG_LEVEL_WARNING, __VA_ARGS__) #define log_err(ctx, ...) \ rxkb_log((ctx), RXKB_LOG_LEVEL_ERROR, __VA_ARGS__) #define log_wsgo(ctx, ...) \ rxkb_log((ctx), RXKB_LOG_LEVEL_CRITICAL, __VA_ARGS__) #define DECLARE_REF_UNREF_FOR_TYPE(type_) \ XKB_EXPORT struct type_ * type_##_ref(struct type_ *object) { \ rxkb_object_ref(&object->base); \ return object; \ } \ XKB_EXPORT struct type_ * type_##_unref(struct type_ *object) { \ if (!object) return NULL; \ return rxkb_object_unref(&object->base); \ } #define DECLARE_CREATE_FOR_TYPE(type_) \ static inline struct type_ * type_##_create(struct rxkb_object *parent) { \ struct type_ *t = calloc(1, sizeof *t); \ if (t) \ rxkb_object_init(&t->base, parent, (destroy_func_t)type_##_destroy); \ return t; \ } #define DECLARE_TYPED_GETTER_FOR_TYPE(type_, field_, rtype_) \ XKB_EXPORT rtype_ type_##_get_##field_(struct type_ *object) { \ return object->field_; \ } #define DECLARE_GETTER_FOR_TYPE(type_, field_) \ DECLARE_TYPED_GETTER_FOR_TYPE(type_, field_, const char*) #define DECLARE_FIRST_NEXT_FOR_TYPE(type_, parent_type_, parent_field_) \ XKB_EXPORT struct type_ * type_##_first(struct parent_type_ *parent) { \ struct type_ *o = NULL; \ if (!list_empty(&parent->parent_field_)) \ o = list_first_entry(&parent->parent_field_, o, base.link); \ return o; \ } \ XKB_EXPORT struct type_ * \ type_##_next(struct type_ *o) \ { \ struct parent_type_ *parent; \ struct type_ *next; \ parent = container_of(o->base.parent, struct parent_type_, base); \ next = list_first_entry(&o->base.link, o, base.link); \ if (list_is_last(&parent->parent_field_, &o->base.link)) \ return NULL; \ return next; \ } static void rxkb_object_init(struct rxkb_object *object, struct rxkb_object *parent, destroy_func_t destroy) { object->refcount = 1; object->destroy = destroy; object->parent = parent; list_init(&object->link); } static void rxkb_object_destroy(struct rxkb_object *object) { if (object->destroy) object->destroy(object); list_remove(&object->link); free(object); } static void * rxkb_object_ref(struct rxkb_object *object) { assert(object->refcount >= 1); ++object->refcount; return object; } static void * rxkb_object_unref(struct rxkb_object *object) { assert(object->refcount >= 1); if (--object->refcount == 0) rxkb_object_destroy(object); return NULL; } static void rxkb_iso639_code_destroy(struct rxkb_iso639_code *code) { free(code->code); } XKB_EXPORT struct rxkb_iso639_code * rxkb_layout_get_iso639_first(struct rxkb_layout *layout) { struct rxkb_iso639_code *code = NULL; if (!list_empty(&layout->iso639s)) code = list_first_entry(&layout->iso639s, code, base.link); return code; } XKB_EXPORT struct rxkb_iso639_code * rxkb_iso639_code_next(struct rxkb_iso639_code *code) { struct rxkb_iso639_code *next = NULL; struct rxkb_layout *layout; layout = container_of(code->base.parent, struct rxkb_layout, base); if (list_is_last(&layout->iso639s, &code->base.link)) return NULL; next = list_first_entry(&code->base.link, code, base.link); return next; } DECLARE_REF_UNREF_FOR_TYPE(rxkb_iso639_code); DECLARE_CREATE_FOR_TYPE(rxkb_iso639_code); DECLARE_GETTER_FOR_TYPE(rxkb_iso639_code, code); static void rxkb_iso3166_code_destroy(struct rxkb_iso3166_code *code) { free(code->code); } XKB_EXPORT struct rxkb_iso3166_code * rxkb_layout_get_iso3166_first(struct rxkb_layout *layout) { struct rxkb_iso3166_code *code = NULL; if (!list_empty(&layout->iso3166s)) code = list_first_entry(&layout->iso3166s, code, base.link); return code; } XKB_EXPORT struct rxkb_iso3166_code * rxkb_iso3166_code_next(struct rxkb_iso3166_code *code) { struct rxkb_iso3166_code *next = NULL; struct rxkb_layout *layout; layout = container_of(code->base.parent, struct rxkb_layout, base); if (list_is_last(&layout->iso3166s, &code->base.link)) return NULL; next = list_first_entry(&code->base.link, code, base.link); return next; } DECLARE_REF_UNREF_FOR_TYPE(rxkb_iso3166_code); DECLARE_CREATE_FOR_TYPE(rxkb_iso3166_code); DECLARE_GETTER_FOR_TYPE(rxkb_iso3166_code, code); static void rxkb_option_destroy(struct rxkb_option *o) { free(o->name); free(o->brief); free(o->description); } DECLARE_REF_UNREF_FOR_TYPE(rxkb_option); DECLARE_CREATE_FOR_TYPE(rxkb_option); DECLARE_GETTER_FOR_TYPE(rxkb_option, name); DECLARE_GETTER_FOR_TYPE(rxkb_option, brief); DECLARE_GETTER_FOR_TYPE(rxkb_option, description); DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_option, popularity, enum rxkb_popularity); DECLARE_FIRST_NEXT_FOR_TYPE(rxkb_option, rxkb_option_group, options); static void rxkb_layout_destroy(struct rxkb_layout *l) { struct rxkb_iso639_code *iso639, *tmp_639; struct rxkb_iso3166_code *iso3166, *tmp_3166; free(l->name); free(l->brief); free(l->description); free(l->variant); list_for_each_safe(iso639, tmp_639, &l->iso639s, base.link) { rxkb_iso639_code_unref(iso639); } list_for_each_safe(iso3166, tmp_3166, &l->iso3166s, base.link) { rxkb_iso3166_code_unref(iso3166); } } DECLARE_REF_UNREF_FOR_TYPE(rxkb_layout); DECLARE_CREATE_FOR_TYPE(rxkb_layout); DECLARE_GETTER_FOR_TYPE(rxkb_layout, name); DECLARE_GETTER_FOR_TYPE(rxkb_layout, brief); DECLARE_GETTER_FOR_TYPE(rxkb_layout, description); DECLARE_GETTER_FOR_TYPE(rxkb_layout, variant); DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_layout, popularity, enum rxkb_popularity); DECLARE_FIRST_NEXT_FOR_TYPE(rxkb_layout, rxkb_context, layouts); static void rxkb_model_destroy(struct rxkb_model *m) { free(m->name); free(m->vendor); free(m->description); } DECLARE_REF_UNREF_FOR_TYPE(rxkb_model); DECLARE_CREATE_FOR_TYPE(rxkb_model); DECLARE_GETTER_FOR_TYPE(rxkb_model, name); DECLARE_GETTER_FOR_TYPE(rxkb_model, vendor); DECLARE_GETTER_FOR_TYPE(rxkb_model, description); DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_model, popularity, enum rxkb_popularity); DECLARE_FIRST_NEXT_FOR_TYPE(rxkb_model, rxkb_context, models); static void rxkb_option_group_destroy(struct rxkb_option_group *og) { struct rxkb_option *o, *otmp; free(og->name); free(og->description); list_for_each_safe(o, otmp, &og->options, base.link) { rxkb_option_unref(o); } } XKB_EXPORT bool rxkb_option_group_allows_multiple(struct rxkb_option_group *g) { return g->allow_multiple; } DECLARE_REF_UNREF_FOR_TYPE(rxkb_option_group); DECLARE_CREATE_FOR_TYPE(rxkb_option_group); DECLARE_GETTER_FOR_TYPE(rxkb_option_group, name); DECLARE_GETTER_FOR_TYPE(rxkb_option_group, description); DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_option_group, popularity, enum rxkb_popularity); DECLARE_FIRST_NEXT_FOR_TYPE(rxkb_option_group, rxkb_context, option_groups); static void rxkb_context_destroy(struct rxkb_context *ctx) { struct rxkb_model *m, *mtmp; struct rxkb_layout *l, *ltmp; struct rxkb_option_group *og, *ogtmp; char **path; list_for_each_safe(m, mtmp, &ctx->models, base.link) rxkb_model_unref(m); assert(list_empty(&ctx->models)); list_for_each_safe(l, ltmp, &ctx->layouts, base.link) rxkb_layout_unref(l); assert(list_empty(&ctx->layouts)); list_for_each_safe(og, ogtmp, &ctx->option_groups, base.link) rxkb_option_group_unref(og); assert(list_empty(&ctx->option_groups)); darray_foreach(path, ctx->includes) free(*path); darray_free(ctx->includes); assert(darray_empty(ctx->includes)); } DECLARE_REF_UNREF_FOR_TYPE(rxkb_context); DECLARE_CREATE_FOR_TYPE(rxkb_context); DECLARE_TYPED_GETTER_FOR_TYPE(rxkb_context, log_level, enum rxkb_log_level); XKB_EXPORT void rxkb_context_set_log_level(struct rxkb_context *ctx, enum rxkb_log_level level) { ctx->log_level = level; } static const char * log_level_to_prefix(enum rxkb_log_level level) { switch (level) { case RXKB_LOG_LEVEL_DEBUG: return "xkbregistry: DEBUG: "; case RXKB_LOG_LEVEL_INFO: return "xkbregistry: INFO: "; case RXKB_LOG_LEVEL_WARNING: return "xkbregistry: WARNING: "; case RXKB_LOG_LEVEL_ERROR: return "xkbregistry: ERROR: "; case RXKB_LOG_LEVEL_CRITICAL: return "xkbregistry: CRITICAL: "; default: return NULL; } } ATTR_PRINTF(3, 0) static void default_log_fn(struct rxkb_context *ctx, enum rxkb_log_level level, const char *fmt, va_list args) { const char *prefix = log_level_to_prefix(level); if (prefix) fprintf(stderr, "%s", prefix); vfprintf(stderr, fmt, args); } static enum rxkb_log_level log_level(const char *level) { char *endptr; enum rxkb_log_level lvl; errno = 0; lvl = strtol(level, &endptr, 10); if (errno == 0 && (endptr[0] == '\0' || is_space(endptr[0]))) return lvl; if (istreq_prefix("crit", level)) return RXKB_LOG_LEVEL_CRITICAL; if (istreq_prefix("err", level)) return RXKB_LOG_LEVEL_ERROR; if (istreq_prefix("warn", level)) return RXKB_LOG_LEVEL_WARNING; if (istreq_prefix("info", level)) return RXKB_LOG_LEVEL_INFO; if (istreq_prefix("debug", level) || istreq_prefix("dbg", level)) return RXKB_LOG_LEVEL_DEBUG; return RXKB_LOG_LEVEL_ERROR; } XKB_EXPORT struct rxkb_context * rxkb_context_new(enum rxkb_context_flags flags) { struct rxkb_context *ctx = rxkb_context_create(NULL); const char *env; if (!ctx) return NULL; ctx->context_state = CONTEXT_NEW; ctx->load_extra_rules_files = flags & RXKB_CONTEXT_LOAD_EXOTIC_RULES; ctx->log_fn = default_log_fn; ctx->log_level = RXKB_LOG_LEVEL_ERROR; /* Environment overwrites defaults. */ env = secure_getenv("RXKB_LOG_LEVEL"); if (env) rxkb_context_set_log_level(ctx, log_level(env)); list_init(&ctx->models); list_init(&ctx->layouts); list_init(&ctx->option_groups); if (!(flags & RXKB_CONTEXT_NO_DEFAULT_INCLUDES) && !rxkb_context_include_path_append_default(ctx)) { rxkb_context_unref(ctx); return NULL; } return ctx; } XKB_EXPORT void rxkb_context_set_log_fn(struct rxkb_context *ctx, void (*log_fn)(struct rxkb_context *ctx, enum rxkb_log_level level, const char *fmt, va_list args)) { ctx->log_fn = (log_fn ? log_fn : default_log_fn); } XKB_EXPORT bool rxkb_context_include_path_append(struct rxkb_context *ctx, const char *path) { struct stat stat_buf; int err; char *tmp = NULL; char rules[PATH_MAX]; if (ctx->context_state != CONTEXT_NEW) { log_err(ctx, "include paths can only be appended to a new context\n"); return false; } tmp = strdup(path); if (!tmp) goto err; err = stat(path, &stat_buf); if (err != 0) goto err; if (!S_ISDIR(stat_buf.st_mode)) goto err; if (!check_eaccess(path, R_OK | X_OK)) goto err; /* Pre-filter for the 99.9% case - if we can't assemble the default ruleset * path, complain here instead of during parsing later. The niche cases * where this is the wrong behaviour aren't worth worrying about. */ if (!snprintf_safe(rules, sizeof(rules), "%s/rules/%s.xml", path, DEFAULT_XKB_RULES)) goto err; darray_append(ctx->includes, tmp); return true; err: free(tmp); return false; } XKB_EXPORT bool rxkb_context_include_path_append_default(struct rxkb_context *ctx) { const char *home, *xdg, *root, *extra; char *user_path; bool ret = false; if (ctx->context_state != CONTEXT_NEW) { log_err(ctx, "include paths can only be appended to a new context\n"); return false; } home = secure_getenv("HOME"); xdg = secure_getenv("XDG_CONFIG_HOME"); if (xdg != NULL) { user_path = asprintf_safe("%s/xkb", xdg); if (user_path) { ret |= rxkb_context_include_path_append(ctx, user_path); free(user_path); } } else if (home != NULL) { /* XDG_CONFIG_HOME fallback is $HOME/.config/ */ user_path = asprintf_safe("%s/.config/xkb", home); if (user_path) { ret |= rxkb_context_include_path_append(ctx, user_path); free(user_path); } } if (home != NULL) { user_path = asprintf_safe("%s/.xkb", home); if (user_path) { ret |= rxkb_context_include_path_append(ctx, user_path); free(user_path); } } extra = secure_getenv("XKB_CONFIG_EXTRA_PATH"); if (extra != NULL) ret |= rxkb_context_include_path_append(ctx, extra); else ret |= rxkb_context_include_path_append(ctx, DFLT_XKB_CONFIG_EXTRA_PATH); root = secure_getenv("XKB_CONFIG_ROOT"); if (root != NULL) ret |= rxkb_context_include_path_append(ctx, root); else ret |= rxkb_context_include_path_append(ctx, DFLT_XKB_CONFIG_ROOT); return ret; } XKB_EXPORT bool rxkb_context_parse_default_ruleset(struct rxkb_context *ctx) { return rxkb_context_parse(ctx, DEFAULT_XKB_RULES); } XKB_EXPORT bool rxkb_context_parse(struct rxkb_context *ctx, const char *ruleset) { char **path; bool success = false; if (ctx->context_state != CONTEXT_NEW) { log_err(ctx, "parse must only be called on a new context\n"); return false; } darray_foreach_reverse(path, ctx->includes) { char rules[PATH_MAX]; if (snprintf_safe(rules, sizeof(rules), "%s/rules/%s.xml", *path, ruleset)) { log_dbg(ctx, "Parsing %s\n", rules); if (parse(ctx, rules, RXKB_POPULARITY_STANDARD)) success = true; } if (ctx->load_extra_rules_files && snprintf_safe(rules, sizeof(rules), "%s/rules/%s.extras.xml", *path, ruleset)) { log_dbg(ctx, "Parsing %s\n", rules); if (parse(ctx, rules, RXKB_POPULARITY_EXOTIC)) success = true; } } ctx->context_state = success ? CONTEXT_PARSED : CONTEXT_FAILED; return success; } XKB_EXPORT void rxkb_context_set_user_data(struct rxkb_context *ctx, void *userdata) { ctx->userdata = userdata; } XKB_EXPORT void * rxkb_context_get_user_data(struct rxkb_context *ctx) { return ctx->userdata; } static inline bool is_node(xmlNode *node, const char *name) { return node->type == XML_ELEMENT_NODE && xmlStrEqual(node->name, (const xmlChar*)name); } /* return a copy of the text content from the first text node of this node */ static char * extract_text(xmlNode *node) { xmlNode *n; for (n = node->children; n; n = n->next) { if (n->type == XML_TEXT_NODE) return (char *)xmlStrdup(n->content); } return NULL; } static bool parse_config_item(struct rxkb_context *ctx, xmlNode *parent, char **name, char **description, char **brief, char **vendor) { xmlNode *node = NULL; xmlNode *ci = NULL; for (ci = parent->children; ci; ci = ci->next) { if (is_node(ci, "configItem")) { *name = NULL; *description = NULL; *brief = NULL; *vendor = NULL; for (node = ci->children; node; node = node->next) { if (is_node(node, "name")) *name = extract_text(node); else if (is_node(node, "description")) *description = extract_text(node); else if (is_node(node, "shortDescription")) *brief = extract_text(node); else if (is_node(node, "vendor")) *vendor = extract_text(node); /* Note: the DTD allows for vendor + brief but models only use * vendor and everything else only uses shortDescription */ } if (!*name || !strlen(*name)) { log_err(ctx, "xml:%d: missing required element 'name'\n", ci->line); return false; } return true; /* only one configItem allowed in the dtd */ } } return false; } static void parse_model(struct rxkb_context *ctx, xmlNode *model, enum rxkb_popularity popularity) { char *name, *description, *brief, *vendor; if (parse_config_item(ctx, model, &name, &description, &brief, &vendor)) { struct rxkb_model *m; list_for_each(m, &ctx->models, base.link) { if (streq(m->name, name)) { free(name); free(description); free(brief); free(vendor); return; } } /* new model */ m = rxkb_model_create(&ctx->base); m->name = name; m->description = description; m->vendor = vendor; m->popularity = popularity; list_append(&ctx->models, &m->base.link); } } static void parse_model_list(struct rxkb_context *ctx, xmlNode *model_list, enum rxkb_popularity popularity) { xmlNode *node = NULL; for (node = model_list->children; node; node = node->next) { if (is_node(node, "model")) parse_model(ctx, node, popularity); } } static void parse_language_list(xmlNode *language_list, struct rxkb_layout *layout) { xmlNode *node = NULL; struct rxkb_iso639_code *code; for (node = language_list->children; node; node = node->next) { if (is_node(node, "iso639Id")) { char *str = extract_text(node); struct rxkb_object *parent; parent = &layout->base; code = rxkb_iso639_code_create(parent); code->code = str; list_append(&layout->iso639s, &code->base.link); } } } static void parse_country_list(xmlNode *country_list, struct rxkb_layout *layout) { xmlNode *node = NULL; struct rxkb_iso3166_code *code; for (node = country_list->children; node; node = node->next) { if (is_node(node, "iso3166Id")) { char *str = extract_text(node); struct rxkb_object *parent; parent = &layout->base; code = rxkb_iso3166_code_create(parent); code->code = str; list_append(&layout->iso3166s, &code->base.link); } } } static void parse_variant(struct rxkb_context *ctx, struct rxkb_layout *l, xmlNode *variant, enum rxkb_popularity popularity) { xmlNode *ci; char *name, *description, *brief, *vendor; if (parse_config_item(ctx, variant, &name, &description, &brief, &vendor)) { struct rxkb_layout *v; bool exists = false; list_for_each(v, &ctx->layouts, base.link) { if (streq(v->name, name) && streq(v->name, l->name)) { exists = true; break; } } if (!exists) { v = rxkb_layout_create(&ctx->base); list_init(&v->iso639s); list_init(&v->iso3166s); v->name = strdup(l->name); v->variant = name; v->description = description; v->brief = brief; v->popularity = popularity; list_append(&ctx->layouts, &v->base.link); for (ci = variant->children; ci; ci = ci->next) { xmlNode *node; if (!is_node(ci, "configItem")) continue; for (node = ci->children; node; node = node->next) { if (is_node(node, "languageList")) parse_language_list(node, v); if (is_node(node, "countryList")) parse_country_list(node, v); } } } else { free(name); free(description); free(brief); free(vendor); } } } static void parse_variant_list(struct rxkb_context *ctx, struct rxkb_layout *l, xmlNode *variant_list, enum rxkb_popularity popularity) { xmlNode *node = NULL; for (node = variant_list->children; node; node = node->next) { if (is_node(node, "variant")) parse_variant(ctx, l, node, popularity); } } static void parse_layout(struct rxkb_context *ctx, xmlNode *layout, enum rxkb_popularity popularity) { char *name, *description, *brief, *vendor; struct rxkb_layout *l; xmlNode *node = NULL; bool exists = false; if (!parse_config_item(ctx, layout, &name, &description, &brief, &vendor)) return; list_for_each(l, &ctx->layouts, base.link) { if (streq(l->name, name) && l->variant == NULL) { exists = true; break; } } if (!exists) { l = rxkb_layout_create(&ctx->base); list_init(&l->iso639s); list_init(&l->iso3166s); l->name = name; l->variant = NULL; l->description = description; l->brief = brief; l->popularity = popularity; list_append(&ctx->layouts, &l->base.link); } else { free(name); free(description); free(brief); free(vendor); } for (node = layout->children; node; node = node->next) { if (is_node(node, "variantList")) { parse_variant_list(ctx, l, node, popularity); } if (!exists && is_node(node, "configItem")) { xmlNode *ll; for (ll = node->children; ll; ll = ll->next) { if (is_node(ll, "languageList")) parse_language_list(ll, l); if (is_node(ll, "countryList")) parse_country_list(ll, l); } } } } static void parse_layout_list(struct rxkb_context *ctx, xmlNode *layout_list, enum rxkb_popularity popularity) { xmlNode *node = NULL; for (node = layout_list->children; node; node = node->next) { if (is_node(node, "layout")) parse_layout(ctx, node, popularity); } } static void parse_option(struct rxkb_context *ctx, struct rxkb_option_group *group, xmlNode *option, enum rxkb_popularity popularity) { char *name, *description, *brief, *vendor; if (parse_config_item(ctx, option, &name, &description, &brief, &vendor)) { struct rxkb_option *o; list_for_each(o, &group->options, base.link) { if (streq(o->name, name)) { free(name); free(description); free(brief); free(vendor); return; } } o = rxkb_option_create(&group->base); o->name = name; o->description = description; o->popularity = popularity; list_append(&group->options, &o->base.link); } } static void parse_group(struct rxkb_context *ctx, xmlNode *group, enum rxkb_popularity popularity) { char *name, *description, *brief, *vendor; struct rxkb_option_group *g; xmlNode *node = NULL; xmlChar *multiple; bool exists = false; if (!parse_config_item(ctx, group, &name, &description, &brief, &vendor)) return; list_for_each(g, &ctx->option_groups, base.link) { if (streq(g->name, name)) { exists = true; break; } } if (!exists) { g = rxkb_option_group_create(&ctx->base); g->name = name; g->description = description; g->popularity = popularity; multiple = xmlGetProp(group, (const xmlChar*)"allowMultipleSelection"); if (multiple && xmlStrEqual(multiple, (const xmlChar*)"true")) g->allow_multiple = true; xmlFree(multiple); list_init(&g->options); list_append(&ctx->option_groups, &g->base.link); } else { free(name); free(description); free(brief); free(vendor); } for (node = group->children; node; node = node->next) { if (is_node(node, "option")) parse_option(ctx, g, node, popularity); } } static void parse_option_list(struct rxkb_context *ctx, xmlNode *option_list, enum rxkb_popularity popularity) { xmlNode *node = NULL; for (node = option_list->children; node; node = node->next) { if (is_node(node, "group")) parse_group(ctx, node, popularity); } } static void parse_rules_xml(struct rxkb_context *ctx, xmlNode *root, enum rxkb_popularity popularity) { xmlNode *node = NULL; for (node = root->children; node; node = node->next) { if (is_node(node, "modelList")) parse_model_list(ctx, node, popularity); else if (is_node(node, "layoutList")) parse_layout_list(ctx, node, popularity); else if (is_node(node, "optionList")) parse_option_list(ctx, node, popularity); } } static void ATTR_PRINTF(2, 0) xml_error_func(void *ctx, const char *msg, ...) { static char buf[PATH_MAX]; static int slen = 0; va_list args; int rc; /* libxml2 prints IO errors from bad includes paths by * calling the error function once per word. So we get to * re-assemble the message here and print it when we get * the line break. My enthusiasm about this is indescribable. */ va_start(args, msg); rc = vsnprintf(&buf[slen], sizeof(buf) - slen, msg, args); va_end(args); /* This shouldn't really happen */ if (rc < 0) { log_err(ctx, "+++ out of cheese error. redo from start +++\n"); slen = 0; memset(buf, 0, sizeof(buf)); return; } slen += rc; if (slen >= (int)sizeof(buf)) { /* truncated, let's flush this */ buf[sizeof(buf) - 1] = '\n'; slen = sizeof(buf); } /* We're assuming here that the last character is \n. */ if (buf[slen - 1] == '\n') { log_err(ctx, "%s", buf); memset(buf, 0, sizeof(buf)); slen = 0; } } static bool validate(struct rxkb_context *ctx, xmlDoc *doc) { bool success = false; xmlValidCtxt *dtdvalid = NULL; xmlDtd *dtd = NULL; xmlParserInputBufferPtr buf = NULL; /* This is a modified version of the xkeyboard-config xkb.dtd. That one * requires modelList, layoutList and optionList, we * allow for any of those to be missing. */ const char dtdstr[] = "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\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; }