Edit

kc3-lang/libxkbcommon/src/xkbcomp/rules.c

Branch :

  • Show log

    Commit

  • Author : Pierre Le Marre
    Date : 2025-07-01 18:37:22
    Hash : 84914512
    Message : chore: Rename indexes to indices Before this commit there was a mix between the two forms. While “indexes” is correct, “indices” is more usual and also the historical form used in this project.

  • src/xkbcomp/rules.c
  • /*
     * For HPND:
     * Copyright (c) 1996 by Silicon Graphics Computer Systems, Inc.
     *
     * For MIT:
     * Copyright © 2012 Ran Benita <ran234@gmail.com>
     *
     * SPDX-License-Identifier: HPND AND MIT
     */
    
    #include "config.h"
    
    #include <assert.h>
    #include <limits.h>
    #include <stdint.h>
    
    #include "xkbcommon/xkbcommon.h"
    #include "xkbcomp-priv.h"
    #include "context.h"
    #include "keymap.h"
    #include "messages-codes.h"
    #include "context.h"
    #include "rules.h"
    #include "rmlvo.h"
    #include "include.h"
    #include "scanner-utils.h"
    #include "darray.h"
    #include "utils.h"
    #include "utils-numbers.h"
    #include "utils-paths.h"
    
    #define MAX_INCLUDE_DEPTH 5
    
    /* Scanner / Lexer */
    
    /* Values returned with some tokens, like yylval. */
    union lvalue {
        struct sval string;
    };
    
    enum rules_token {
        TOK_END_OF_FILE = 0,
        TOK_END_OF_LINE,
        TOK_IDENTIFIER,
        TOK_GROUP_NAME,
        TOK_BANG,
        TOK_EQUALS,
        TOK_WILD_CARD_STAR,
        TOK_WILD_CARD_NONE,
        TOK_WILD_CARD_SOME,
        TOK_WILD_CARD_ANY,
        TOK_INCLUDE,
        TOK_ERROR
    };
    
    static inline bool
    is_ident(char ch)
    {
        return is_graph(ch) && ch != '\\';
    }
    
    static enum rules_token
    lex(struct scanner *s, union lvalue *val)
    {
    skip_more_whitespace_and_comments:
        /* Skip spaces. */
        while (scanner_chr(s, ' ') || scanner_chr(s, '\t') || scanner_chr(s, '\r'));
    
        /* Skip comments. */
        if (scanner_lit(s, "//")) {
            scanner_skip_to_eol(s);
        }
    
        /* New line. */
        if (scanner_eol(s)) {
            while (scanner_eol(s)) scanner_next(s);
            return TOK_END_OF_LINE;
        }
    
        /* Escaped line continuation. */
        if (scanner_chr(s, '\\')) {
            /* Optional \r. */
            scanner_chr(s, '\r');
            if (!scanner_eol(s)) {
                scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                            "illegal new line escape; must appear at end of line");
                return TOK_ERROR;
            }
            scanner_next(s);
            goto skip_more_whitespace_and_comments;
        }
    
        /* See if we're done. */
        if (scanner_eof(s)) return TOK_END_OF_FILE;
    
        /* New token. */
        s->token_pos = s->pos;
    
        /* Operators and punctuation. */
        if (scanner_chr(s, '!')) return TOK_BANG;
        if (scanner_chr(s, '=')) return TOK_EQUALS;
    
        /* Wild cards */
        if (scanner_chr(s, '*')) return TOK_WILD_CARD_STAR;
        if (scanner_lit(s, "<none>")) return TOK_WILD_CARD_NONE;
        if (scanner_lit(s, "<some>")) return TOK_WILD_CARD_SOME;
        if (scanner_lit(s, "<any>")) return TOK_WILD_CARD_ANY;
    
        /* Group name. */
        if (scanner_chr(s, '$')) {
            val->string.start = s->s + s->pos;
            val->string.len = 0;
            while (is_ident(scanner_peek(s))) {
                scanner_next(s);
                val->string.len++;
            }
            if (val->string.len == 0) {
                scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                            "unexpected character after \'$\'; expected name");
                return TOK_ERROR;
            }
            return TOK_GROUP_NAME;
        }
    
        /* Include statement. */
        if (scanner_lit(s, "include"))
            return TOK_INCLUDE;
    
        /* Identifier. */
        /* Ensure that we can parse KcCGST values with merge modes */
        assert(is_ident(MERGE_OVERRIDE_PREFIX));
        assert(is_ident(MERGE_AUGMENT_PREFIX));
        assert(is_ident(MERGE_REPLACE_PREFIX));
        if (is_ident(scanner_peek(s))) {
            val->string.start = s->s + s->pos;
            val->string.len = 0;
            while (is_ident(scanner_peek(s))) {
                scanner_next(s);
                val->string.len++;
            }
            return TOK_IDENTIFIER;
        }
    
        scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                    "unrecognized token");
        return TOK_ERROR;
    }
    
    /***====================================================================***/
    
    enum rules_mlvo {
        MLVO_MODEL,
        MLVO_LAYOUT,
        MLVO_VARIANT,
        MLVO_OPTION,
        _MLVO_NUM_ENTRIES
    };
    
    typedef uint8_t mlvo_index_t;
    typedef uint8_t mlvo_mask_t;
    
    static const struct sval rules_mlvo_svals[_MLVO_NUM_ENTRIES] = {
        [MLVO_MODEL] = SVAL_INIT("model"),
        [MLVO_LAYOUT] = SVAL_INIT("layout"),
        [MLVO_VARIANT] = SVAL_INIT("variant"),
        [MLVO_OPTION] = SVAL_INIT("option"),
    };
    
    enum rules_kccgst {
        KCCGST_KEYCODES,
        KCCGST_TYPES,
        KCCGST_COMPAT,
        KCCGST_SYMBOLS,
        KCCGST_GEOMETRY,
        _KCCGST_NUM_ENTRIES
    };
    
    typedef uint8_t kccgst_index_t;
    typedef uint8_t kccgst_mask_t;
    
    static const struct sval rules_kccgst_svals[_KCCGST_NUM_ENTRIES] = {
        [KCCGST_KEYCODES] = SVAL_INIT("keycodes"),
        [KCCGST_TYPES] = SVAL_INIT("types"),
        [KCCGST_COMPAT] = SVAL_INIT("compat"),
        [KCCGST_SYMBOLS] = SVAL_INIT("symbols"),
        [KCCGST_GEOMETRY] = SVAL_INIT("geometry"),
    };
    
    static_assert(XKB_MAX_GROUPS < (1u << 30),
                  "Layout index does not fix in matched_sval::matched_layouts");
    #define OPTIONS_MATCH_ALL_GROUPS XKB_MAX_GROUPS
    
    /* We use this to keep score whether an mlvo was matched or not; if not,
     * we warn the user that his preference was ignored. */
    struct matched_sval {
        struct sval sval;
        bool matched:1;
        /* Used for layout-specific options */
        xkb_layout_index_t layout:31;
    };
    typedef darray(struct matched_sval) darray_matched_sval;
    
    /*
     * A broken-down version of xkb_rule_names (without the rules,
     * obviously).
     */
    struct rule_names {
        struct matched_sval model;
        darray_matched_sval layouts;
        darray_matched_sval variants;
        darray_matched_sval options;
    };
    
    struct group {
        struct sval name;
        darray_sval elements;
    };
    
    struct mapping {
        enum rules_mlvo mlvo_at_pos[_MLVO_NUM_ENTRIES];
        mlvo_index_t num_mlvo;
        mlvo_mask_t defined_mlvo_mask;
        bool has_layout_idx_range;
        /* This member has 2 uses:
         * • Keep track of layout and variant indices while parsing MLVO headers.
         * • Store layout/variant range afterwards.
         * Thus provide 2 structs to reflect these semantics in the code. */
        union {
            struct { xkb_layout_index_t layout_idx, variant_idx; };
            struct { xkb_layout_index_t layout_idx_min, layout_idx_max; };
        };
        /* This member has 2 uses:
         * • Check if the mapping is active by interpreting the value as a boolean.
         * • Keep track of the remaining layout indices to match.
         * Thus provide 2 names to reflect these semantics in the code. */
        union {
            xkb_layout_mask_t active;
            xkb_layout_mask_t layouts_candidates_mask;
        };
        enum rules_kccgst kccgst_at_pos[_KCCGST_NUM_ENTRIES];
        kccgst_index_t num_kccgst;
        kccgst_mask_t defined_kccgst_mask;
    };
    
    enum mlvo_match_type {
        /** Match the given plain value */
        MLVO_MATCH_NORMAL = 0,
        /** Match depending on the value of `wildcard_match_type` */
        MLVO_MATCH_WILDCARD_LEGACY,
        /** Match empty value */
        MLVO_MATCH_WILDCARD_NONE,
        /** Match non-empty value */
        MLVO_MATCH_WILDCARD_SOME,
        /** Match any value, optionally empty */
        MLVO_MATCH_WILDCARD_ANY,
        /** Match any entry in a group */
        MLVO_MATCH_GROUP,
    };
    
    enum wildcard_match_type {
        /** ‘*’ matches only non-empty strings */
        WILDCARD_MATCH_NONEMPTY = 0,
        /** ‘*’ matches all strings */
        WILDCARD_MATCH_ALL,
    };
    
    struct rule {
        struct sval mlvo_value_at_pos[_MLVO_NUM_ENTRIES];
        enum mlvo_match_type match_type_at_pos[_MLVO_NUM_ENTRIES];
        mlvo_index_t num_mlvo_values;
        struct sval kccgst_value_at_pos[_KCCGST_NUM_ENTRIES];
        kccgst_index_t num_kccgst_values;
        bool skip;
    };
    
    #define SLICE_KCCGST_BIT_FIELD_SIZE 4
    static_assert(!((1u << (SLICE_KCCGST_BIT_FIELD_SIZE - 1)) &
                    (_KCCGST_NUM_ENTRIES - 1)),
                  "Cannot encode KcCGST enum safely (need space for the sign)");
    struct kccgst_buffer_slice {
        uint32_t length:(32 - SLICE_KCCGST_BIT_FIELD_SIZE);
        enum rules_kccgst kccgst:SLICE_KCCGST_BIT_FIELD_SIZE;
        xkb_layout_index_t layout;
    };
    
    /* Buffer for pending KcCGST values */
    struct kccgst_buffer {
        darray_char buffer;
        /* Slice corresponding to each value in the buffer */
        darray(struct kccgst_buffer_slice) slices;
    };
    
    /*
     * This is the main object used to match a given RMLVO against a rules
     * file and aggregate the results in a KcCGST. It goes through a simple
     * matching state machine, with tokens as transitions (see
     * matcher_match()).
     */
    struct matcher {
        struct xkb_context *ctx;
        /* Input.*/
        struct rule_names rmlvo;
        union lvalue val;
        darray(struct group) groups;
        /* Current mapping. */
        struct mapping mapping;
        /* Current rule. */
        struct rule rule;
        /*
         * Buffers for pending KcCGST values. Required in case of using layout
         * index ranges, to ensure that the values are merged in the expected order.
         * See the note: “Layout index ranges and merging KcCGST values”.
         */
        struct kccgst_buffer pending_kccgst;
        /* Output. */
        darray_char kccgst[_KCCGST_NUM_ENTRIES];
    };
    
    static struct sval
    strip_spaces(struct sval v)
    {
        while (v.len > 0 && is_space(v.start[0])) { v.len--; v.start++; }
        while (v.len > 0 && is_space(v.start[v.len - 1])) v.len--;
        return v;
    }
    
    static darray_matched_sval
    split_comma_separated_mlvo(struct xkb_context *ctx,
                               enum rules_mlvo mlvo, const char *s)
    {
        darray_matched_sval arr = darray_new();
    
        /*
         * Make sure the array returned by this function always includes at
         * least one value, e.g. "" -> { "" } and "," -> { "", "" }.
         */
    
        if (!s) {
            struct matched_sval val = { .sval = { NULL, 0 } };
            darray_append(arr, val);
            return arr;
        }
    
        while (true) {
            struct matched_sval val = {
                .sval = { s, 0 },
                .matched = false,
                /* NOTE: Cannot store XKB_LAYOUT_INVALID */
                .layout = OPTIONS_MATCH_ALL_GROUPS
            };
            while (*s != '\0' && *s != ',' && *s != OPTIONS_GROUP_SPECIFIER_PREFIX) {
                s++;
                val.sval.len++;
            }
            val.sval = strip_spaces(val.sval);
    
            if (*s == OPTIONS_GROUP_SPECIFIER_PREFIX) {
                /* Handle numeric layout index */
                s++;
                const char* const layout_start = s;
                xkb_layout_index_t layout = XKB_LAYOUT_INVALID;
    
                int count = parse_dec_to_uint32_t(s, SIZE_MAX, &layout);
                if (count > 0) {
                    /* Note: 1-indexed layout */
                    s += count;
                    if (layout == 0 || layout > XKB_MAX_GROUPS) {
                        log_err(ctx, XKB_ERROR_UNSUPPORTED_GROUP_INDEX,
                                "Invalid layout index %"PRIu32" "
                                "for the RMVLO component: \"%.*s\"\n", layout,
                                (unsigned int) val.sval.len, val.sval.start);
                    } else if (mlvo != MLVO_OPTION) {
                        log_warn(ctx, XKB_LOG_MESSAGE_NO_ID,
                                 "Layout index %"PRIu32" is not supported for "
                                 "the RMLVO component: \"%.*s\"\n", layout,
                                 (unsigned int) val.sval.len, val.sval.start);
                    } else {
                        val.layout = layout - 1;
                    }
                }
    
                /* Consume until reaching next item or end of string */
                const char* const layout_index_end = s;
                while (*s != '\0' && *s != ',') { s++; }
                if (count <= 0 || layout_index_end != s) {
                    log_err(ctx, XKB_ERROR_UNSUPPORTED_GROUP_INDEX,
                            "Invalid layout index \"%.*s\" for the RMLVO "
                            "component \"%.*s\"; discarding specifier.\n",
                            (unsigned int) (s - layout_start), layout_start,
                            (unsigned int) val.sval.len, val.sval.start);
                    val.layout = OPTIONS_MATCH_ALL_GROUPS;
                }
            }
    
            darray_append(arr, val);
    
            if (*s == '\0') break;
            if (*s == ',') s++;
        }
    
        return arr;
    }
    
    static struct matcher *
    matcher_new_from_rmlvo(const struct xkb_rmlvo_builder *rmlvo, const char **rules)
    {
        struct matcher *m = calloc(1, sizeof(*m));
        if (!m)
            return NULL;
    
        m->ctx = rmlvo->ctx;
    
        /* Sanitize */
        struct xkb_rule_names names = {
            .rules = rmlvo->rules,
            .model = rmlvo->model,
            /* Initial values only used to detect missing entries */
            .layout = (darray_empty(rmlvo->layouts)) ? NULL : "x",
            .variant = (darray_empty(rmlvo->layouts)) ? NULL : "x",
            .options = (darray_empty(rmlvo->options)) ? NULL : "x",
        };
        const enum RMLVO changed = xkb_context_sanitize_rule_names(rmlvo->ctx,
                                                                   &names);
    
        /* Initialize matcher with sanitized names, if relevant */
        if (changed & RMLVO_RULES) {
            *rules = names.rules;
        } else {
            *rules = rmlvo->rules;
        }
    
        if (changed & RMLVO_MODEL) {
            m->rmlvo.model.sval.start = names.model;
        } else {
            m->rmlvo.model.sval.start = rmlvo->model;
        }
        m->rmlvo.model.sval.len = strlen_safe(rmlvo->model);
        m->rmlvo.model.layout = OPTIONS_MATCH_ALL_GROUPS;
    
        assert((changed & RMLVO_LAYOUT) || !(changed & RMLVO_VARIANT));
        if (changed & RMLVO_LAYOUT) {
            /* Layout and variant are tied together */
            m->rmlvo.layouts = split_comma_separated_mlvo(rmlvo->ctx, MLVO_LAYOUT,
                                                          names.layout);
            m->rmlvo.variants = split_comma_separated_mlvo(rmlvo->ctx, MLVO_VARIANT,
                                                           names.variant);
            if (darray_size(m->rmlvo.layouts) > darray_size(m->rmlvo.variants)) {
                /* Do not warn if no variants was provided */
                if (!isempty(names.variant))
                    log_warn(m->ctx, XKB_LOG_MESSAGE_NO_ID,
                            "More layouts than variants: \"%s\" vs. \"%s\".\n",
                            names.layout ? names.layout : "(none)",
                            names.variant ? names.variant : "(none)");
                darray_resize0(m->rmlvo.variants, darray_size(m->rmlvo.layouts));
            } else if (darray_size(m->rmlvo.layouts) < darray_size(m->rmlvo.variants)) {
                log_err(m->ctx, XKB_LOG_MESSAGE_NO_ID,
                        "Less layouts than variants: \"%s\" vs. \"%s\".\n",
                        names.layout ? names.layout : "(none)",
                        names.variant ? names.variant : "(none)");
                darray_resize(m->rmlvo.variants, darray_size(m->rmlvo.layouts));
                darray_shrink(m->rmlvo.variants);
            }
        } else {
            struct xkb_rmlvo_builder_layout *layout;
            darray_foreach(layout, rmlvo->layouts) {
                struct matched_sval val = {
                    .sval = {
                        .start = layout->layout,
                        .len = strlen_safe(layout->layout)
                    },
                    .layout = OPTIONS_MATCH_ALL_GROUPS,
                    .matched = false
                };
                darray_append(m->rmlvo.layouts, val);
                val.sval.start = layout->variant;
                val.sval.len = strlen_safe(layout->variant);
                darray_append(m->rmlvo.variants, val);
            }
        }
    
        if (changed & RMLVO_OPTIONS) {
            m->rmlvo.options = split_comma_separated_mlvo(rmlvo->ctx, MLVO_OPTION,
                                                          names.options);
        } else {
            struct xkb_rmlvo_builder_option *option;
            darray_foreach(option, rmlvo->options) {
                struct matched_sval val = {
                    .sval = {
                        .start = option->option,
                        .len = strlen_safe(option->option),
                    },
                    .layout = (option->layout) == XKB_LAYOUT_INVALID
                        ? OPTIONS_MATCH_ALL_GROUPS
                        : option->layout,
                    .matched = false
                };
                darray_append(m->rmlvo.options, val);
            }
        }
    
        return m;
    }
    
    static struct matcher *
    matcher_new_from_names(struct xkb_context *ctx,
                const struct xkb_rule_names *rmlvo)
    {
        struct matcher *m = calloc(1, sizeof(*m));
        if (!m)
            return NULL;
    
        m->ctx = ctx;
        m->rmlvo.model.sval.start = rmlvo->model;
        m->rmlvo.model.sval.len = strlen_safe(rmlvo->model);
        m->rmlvo.model.layout = OPTIONS_MATCH_ALL_GROUPS;
        m->rmlvo.layouts = split_comma_separated_mlvo(ctx, MLVO_LAYOUT, rmlvo->layout);
        m->rmlvo.variants = split_comma_separated_mlvo(ctx, MLVO_VARIANT, rmlvo->variant);
        m->rmlvo.options = split_comma_separated_mlvo(ctx, MLVO_OPTION, rmlvo->options);
    
        if (darray_size(m->rmlvo.layouts) > darray_size(m->rmlvo.variants)) {
            /* Do not warn if no variants was provided */
            if (!isempty(rmlvo->variant))
                log_warn(ctx, XKB_LOG_MESSAGE_NO_ID,
                         "More layouts than variants: \"%s\" vs. \"%s\".\n",
                         rmlvo->layout ? rmlvo->layout : "(none)",
                         rmlvo->variant ? rmlvo->variant : "(none)");
            darray_resize0(m->rmlvo.variants, darray_size(m->rmlvo.layouts));
        } else if (darray_size(m->rmlvo.layouts) < darray_size(m->rmlvo.variants)) {
            log_err(ctx, XKB_LOG_MESSAGE_NO_ID,
                    "Less layouts than variants: \"%s\" vs. \"%s\".\n",
                    rmlvo->layout ? rmlvo->layout : "(none)",
                    rmlvo->variant ? rmlvo->variant : "(none)");
            darray_resize(m->rmlvo.variants, darray_size(m->rmlvo.layouts));
            darray_shrink(m->rmlvo.variants);
        }
    
        return m;
    }
    
    static void
    matcher_free(struct matcher *m)
    {
        if (!m)
            return;
        darray_free(m->rmlvo.layouts);
        darray_free(m->rmlvo.variants);
        darray_free(m->rmlvo.options);
        struct group *group;
        darray_foreach(group, m->groups)
            darray_free(group->elements);
        darray_free(m->pending_kccgst.buffer);
        darray_free(m->pending_kccgst.slices);
        for (kccgst_index_t i = 0; i < (kccgst_index_t) _KCCGST_NUM_ENTRIES; i++)
            darray_free(m->kccgst[i]);
        darray_free(m->groups);
        free(m);
    }
    
    static void
    matcher_group_start_new(struct matcher *m, struct sval name)
    {
        struct group group = { .name = name, .elements = darray_new() };
        darray_append(m->groups, group);
    }
    
    static void
    matcher_group_add_element(struct matcher *m, struct scanner *s,
                              struct sval element)
    {
        darray_append(darray_item(m->groups, darray_size(m->groups) - 1).elements,
                      element);
    }
    
    static bool
    read_rules_file(struct xkb_context *ctx,
                    struct matcher *matcher,
                    unsigned int include_depth,
                    FILE *file,
                    const char *path);
    
    static void
    matcher_include(struct matcher *m, struct scanner *parent_scanner,
                    unsigned int include_depth,
                    struct sval inc)
    {
        if (include_depth >= MAX_INCLUDE_DEPTH) {
            scanner_err(parent_scanner, XKB_LOG_MESSAGE_NO_ID,
                        "maximum include depth (%u) exceeded; "
                        "maybe there is an include loop?",
                        MAX_INCLUDE_DEPTH);
            return;
        }
    
        const char *stmt_file = inc.start;
        size_t stmt_file_len = inc.len;
    
        /* Process %-expansion, if any */
        char buf[PATH_MAX] = {0};
        const ssize_t expanded = expand_path(m->ctx, parent_scanner->file_name,
                                             stmt_file, stmt_file_len,
                                             FILE_TYPE_RULES,
                                             buf, sizeof(buf));
        if (expanded < 0) {
            /* Error */
            return;
        } else if (expanded > 0) {
            /* %-expanded */
            stmt_file = buf;
            stmt_file_len = (size_t) expanded;
            assert(stmt_file[stmt_file_len] == '\0');
        }
    
        /* Lookup the first candidate */
        FILE* file;
        unsigned int offset = 0;
        const bool absolute_path = is_absolute_path(stmt_file);
        if (absolute_path) {
            /* Absolute path: no need for lookup in XKB paths */
            if (!expanded) {
                /* No %expansion: ensure it’s NULL-terminated */
                if (stmt_file_len < sizeof(buf)) {
                    memcpy(buf, stmt_file, stmt_file_len);
                    buf[stmt_file_len] = '\0';
                    stmt_file = buf;
                } else {
                    log_err(m->ctx, XKB_ERROR_INVALID_PATH,
                            "Path is too long: %zu > %zu, got raw path: %.*s\n",
                            stmt_file_len, sizeof(buf),
                            (unsigned int) stmt_file_len, stmt_file);
                    return;
                }
            } else {
                /* %-expansion is always NULL-terminated */
                assert(stmt_file[stmt_file_len] == '\0');
            }
            file = fopen(stmt_file, "rb");
        } else {
            /* Relative path: lookup the first XKB path */
            if (unlikely(expanded)) {
                /*
                 * Relative path after expansion
                 *
                 * Unlikely to happen, because %-expansion is meant to use absolute
                 * paths. Considering that:
                 * - we do not resolve paths before expansion, leading to unexpected
                 *   result here;
                 * - we need the buffer afterwards, but it currently contains the
                 *   expanded path;
                 * - this is an edge case;
                 * we simply make the lookup fail.
                 */
                file = NULL;
            } else {
                file = FindFileInXkbPath(m->ctx, parent_scanner->file_name,
                                         stmt_file, stmt_file_len, FILE_TYPE_RULES,
                                         buf, sizeof(buf), &offset);
            }
        }
    
        while (file) {
            assert(strlen_safe(buf) < sizeof(buf));
            bool ret = read_rules_file(m->ctx, m, include_depth + 1, file, buf);
            fclose(file);
            if (ret)
                return;
            /* Failed to parse rules or get all the components */
            log_err(m->ctx, XKB_LOG_MESSAGE_NO_ID,
                    "No components returned from included XKB rules \"%s\"\n",
                    buf);
    
            if (absolute_path) {
                /* There is no point to search further if the path is absolute */
                break;
            }
    
            /* Try next XKB path */
            offset++;
            file = FindFileInXkbPath(m->ctx, parent_scanner->file_name,
                                     stmt_file, stmt_file_len, FILE_TYPE_RULES,
                                     buf, sizeof(buf), &offset);
        }
    
        log_err(m->ctx, XKB_LOG_MESSAGE_NO_ID,
                "Failed to open included XKB rules \"%.*s\"\n",
                (unsigned int) stmt_file_len, stmt_file);
    }
    
    static void
    matcher_mapping_start_new(struct matcher *m)
    {
        for (mlvo_index_t i = 0; i < (mlvo_index_t) _MLVO_NUM_ENTRIES; i++)
            m->mapping.mlvo_at_pos[i] = _MLVO_NUM_ENTRIES;
        for (kccgst_index_t i = 0; i < (kccgst_index_t) _KCCGST_NUM_ENTRIES; i++)
            m->mapping.kccgst_at_pos[i] = _KCCGST_NUM_ENTRIES;
        m->mapping.has_layout_idx_range = false;
        m->mapping.layout_idx = m->mapping.variant_idx = XKB_LAYOUT_INVALID;
        m->mapping.num_mlvo = m->mapping.num_kccgst = 0;
        m->mapping.defined_mlvo_mask = 0;
        m->mapping.defined_kccgst_mask = 0;
        m->mapping.active = true;
    }
    
    static int
    parse_layout_int_index(const char *s, size_t max_len, xkb_layout_index_t *out)
    {
        /* We expect a NULL-terminated string of at least length 3 */
        assert(max_len >= 3);
        uint32_t val = 0;
        const int count = parse_dec_to_uint32_t(&s[1], max_len - 2, &val);
        if (count <= 0 || s[1 + count] != ']' || val == 0 || val > XKB_MAX_GROUPS)
            return -1;
        /* To zero-based index. */
        *out = val - 1;
        return count + 2; /* == length "[index]" */
    }
    
    /* Parse Kccgst layout index:
     * "[%i]" or "[n]", where "n" is a decimal number */
    static int
    extract_layout_index(const char *s, size_t max_len, xkb_layout_index_t *out)
    {
        /* This function is pretty stupid, but works for now. */
        *out = XKB_LAYOUT_INVALID;
        if (max_len < 3 || s[0] != '[')
            return -1;
        if (max_len > 3 && s[1] == '%' && s[2] == 'i' && s[3] == ']') {
            /* Special index: %i */
            return 4; /* == length "[%i]" */
        }
        /* Numeric index */
        return parse_layout_int_index(s, max_len, out);
    }
    
    /* Special layout indices */
    enum layout_index_ranges {
        LAYOUT_INDEX_SINGLE = XKB_LAYOUT_INVALID - 4,
        LAYOUT_INDEX_FIRST  = XKB_LAYOUT_INVALID - 3,
        LAYOUT_INDEX_LATER  = XKB_LAYOUT_INVALID - 2,
        LAYOUT_INDEX_ANY    = XKB_LAYOUT_INVALID - 1
    };
    
    static_assert((xkb_layout_index_t) XKB_MAX_GROUPS <
                  (xkb_layout_index_t) LAYOUT_INDEX_SINGLE,
                  "Cannot define special indices");
    static_assert((xkb_layout_index_t) LAYOUT_INDEX_SINGLE <
                  (xkb_layout_index_t) LAYOUT_INDEX_FIRST &&
                  (xkb_layout_index_t) LAYOUT_INDEX_FIRST <
                  (xkb_layout_index_t) LAYOUT_INDEX_LATER &&
                  (xkb_layout_index_t) LAYOUT_INDEX_LATER <
                  (xkb_layout_index_t) LAYOUT_INDEX_ANY &&
                  (xkb_layout_index_t) LAYOUT_INDEX_ANY <
                  (xkb_layout_index_t) XKB_LAYOUT_INVALID,
                  "Special indices must respect certain order");
    
    /* Parse index of layout/variant in MLVO mapping */
    static int
    extract_mapping_layout_index(const char *s, size_t max_len,
                                 xkb_layout_index_t *out)
    {
        static const struct {
            const char* name;
            int length;
            enum layout_index_ranges range;
        } names[] = {
            { "single]", 7, LAYOUT_INDEX_SINGLE },
            { "first]" , 6, LAYOUT_INDEX_FIRST  },
            { "later]" , 6, LAYOUT_INDEX_LATER  },
            { "any]"   , 4, LAYOUT_INDEX_ANY    },
        };
    
        /* Check for minimal `[` + index + `]` */
        if (max_len < 3 || s[0] != '[') {
            *out = XKB_LAYOUT_INVALID;
            return -1;
        }
    
        /* Try named indices ranges */
        for (unsigned int k = 0; k < ARRAY_SIZE(names); k++) {
            if (strncmp(&s[1], names[k].name, names[k].length) == 0) {
                *out = (xkb_layout_index_t) names[k].range;
                return names[k].length + 1; /* == length "[index]" */
            }
        }
    
        /* Try numeric index */
        *out = XKB_LAYOUT_INVALID;
        return parse_layout_int_index(s, max_len, out);
    }
    
    static inline bool
    is_mlvo_mask_defined(struct matcher *m, enum rules_mlvo mlvo)
    {
        return m->mapping.defined_mlvo_mask & (1u << mlvo);
    }
    
    static void
    matcher_mapping_set_mlvo(struct matcher *m, struct scanner *s,
                             struct sval ident)
    {
        enum rules_mlvo mlvo;
        struct sval mlvo_sval;
    
        for (mlvo = 0; mlvo < _MLVO_NUM_ENTRIES; mlvo++) {
            mlvo_sval = rules_mlvo_svals[mlvo];
    
            if (svaleq_prefix(mlvo_sval, ident))
                break;
        }
    
        /* Not found. */
        if (mlvo >= _MLVO_NUM_ENTRIES) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid mapping: \"%.*s\" is not a valid value here; "
                        "ignoring rule set",
                        (unsigned int) ident.len, ident.start);
            m->mapping.active = false;
            return;
        }
    
        if (is_mlvo_mask_defined(m, mlvo)) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid mapping: \"%.*s\" appears twice on the same line; "
                        "ignoring rule set",
                        (unsigned int) mlvo_sval.len, mlvo_sval.start);
            m->mapping.active = false;
            return;
        }
    
        /* If there are leftovers still, it must be an index. */
        if (mlvo_sval.len < ident.len) {
            xkb_layout_index_t idx;
            int consumed = extract_mapping_layout_index(ident.start + mlvo_sval.len,
                                                        ident.len - mlvo_sval.len,
                                                        &idx);
            if ((int) (ident.len - mlvo_sval.len) != consumed) {
                scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                            "invalid mapping: \"%.*s\" may only be followed by a "
                            "valid group index; ignoring rule set",
                            (unsigned int) mlvo_sval.len, mlvo_sval.start);
                m->mapping.active = false;
                return;
            }
    
            if (mlvo == MLVO_LAYOUT) {
                m->mapping.layout_idx = idx;
            }
            else if (mlvo == MLVO_VARIANT) {
                m->mapping.variant_idx = idx;
            }
            else {
                scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                            "invalid mapping: \"%.*s\" cannot be followed by a group "
                            "index; ignoring rule set",
                            (unsigned int) mlvo_sval.len, mlvo_sval.start);
                m->mapping.active = false;
                return;
            }
        } else if (mlvo == MLVO_LAYOUT) {
            m->mapping.layout_idx = (xkb_layout_index_t) LAYOUT_INDEX_SINGLE;
        } else if (mlvo == MLVO_VARIANT) {
            m->mapping.variant_idx = (xkb_layout_index_t) LAYOUT_INDEX_SINGLE;
        }
    
        /* Check that if both layout and variant are defined, then they must have
         * the same index */
        if (((mlvo == MLVO_LAYOUT && is_mlvo_mask_defined(m, MLVO_VARIANT)) ||
             (mlvo == MLVO_VARIANT && is_mlvo_mask_defined(m, MLVO_LAYOUT))) &&
            m->mapping.layout_idx != m->mapping.variant_idx) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid mapping: \"layout\" index must be the same as the "
                        "\"variant\" index");
            m->mapping.active = false;
            return;
        }
    
        m->mapping.mlvo_at_pos[m->mapping.num_mlvo] = mlvo;
        m->mapping.defined_mlvo_mask |= (mlvo_mask_t) 1u << mlvo;
        m->mapping.num_mlvo++;
    }
    
    static void
    matcher_mapping_set_layout_bounds(struct matcher *m)
    {
        /* Handle case where one of the index is XKB_LAYOUT_INVALID */
        xkb_layout_index_t idx = MIN(m->mapping.layout_idx, m->mapping.variant_idx);
        switch (idx) {
            case XKB_LAYOUT_INVALID:
                /* No layout nor variant */
                assert(!is_mlvo_mask_defined(m, MLVO_LAYOUT) &&
                       !is_mlvo_mask_defined(m, MLVO_VARIANT));
                m->mapping.has_layout_idx_range = false;
                m->mapping.layout_idx_min = XKB_LAYOUT_INVALID;
                m->mapping.layout_idx_max = XKB_LAYOUT_INVALID;
                m->mapping.layouts_candidates_mask = 0x1;
                break;
            case LAYOUT_INDEX_LATER:
                m->mapping.has_layout_idx_range = true;
                m->mapping.layout_idx_min = 1;
                m->mapping.layout_idx_max = MIN(XKB_MAX_GROUPS,
                                                darray_size(m->rmlvo.layouts));
                m->mapping.layouts_candidates_mask = (xkb_layout_mask_t)
                    /* All but the first layout */
                    ((UINT64_C(1) << m->mapping.layout_idx_max) - UINT64_C(1)) &
                    ~UINT64_C(1);
                break;
            case LAYOUT_INDEX_ANY:
                m->mapping.has_layout_idx_range = true;
                m->mapping.layout_idx_min = 0;
                m->mapping.layout_idx_max = MIN(XKB_MAX_GROUPS,
                                                darray_size(m->rmlvo.layouts));
                m->mapping.layouts_candidates_mask = (xkb_layout_mask_t)
                    /* All layouts */
                    (UINT64_C(1) << m->mapping.layout_idx_max) - UINT64_C(1);
                break;
            case LAYOUT_INDEX_SINGLE:
            case LAYOUT_INDEX_FIRST:
                /* No index or first index */
                idx = 0;
                /* fallthrough */
            default:
                /* Mere layout index */
                m->mapping.has_layout_idx_range = false;
                m->mapping.layout_idx_min = idx;
                m->mapping.layout_idx_max = idx + 1;
                m->mapping.layouts_candidates_mask = UINT32_C(1) << idx;
        }
    }
    
    static void
    matcher_mapping_set_kccgst(struct matcher *m, struct scanner *s, struct sval ident)
    {
        enum rules_kccgst kccgst;
        struct sval kccgst_sval;
    
        for (kccgst = 0; kccgst < _KCCGST_NUM_ENTRIES; kccgst++) {
            kccgst_sval = rules_kccgst_svals[kccgst];
    
            if (svaleq(rules_kccgst_svals[kccgst], ident))
                break;
        }
    
        /* Not found. */
        if (kccgst >= _KCCGST_NUM_ENTRIES) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid mapping: \"%.*s\" is not a valid value here; "
                        "ignoring rule set",
                        (unsigned int) ident.len, ident.start);
            m->mapping.active = false;
            return;
        }
    
        if (m->mapping.defined_kccgst_mask & (1u << kccgst)) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid mapping: \"%.*s\" appears twice on the same line; "
                        "ignoring rule set",
                        (unsigned int) kccgst_sval.len, kccgst_sval.start);
            m->mapping.active = false;
            return;
        }
    
        m->mapping.kccgst_at_pos[m->mapping.num_kccgst] = kccgst;
        m->mapping.defined_kccgst_mask |= (kccgst_mask_t) 1u << kccgst;
        m->mapping.num_kccgst++;
    }
    
    static bool
    matcher_mapping_verify(struct matcher *m, struct scanner *s)
    {
        if (m->mapping.num_mlvo == 0) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid mapping: must have at least one value on the left "
                        "hand side; ignoring rule set");
            goto skip;
        }
    
        if (m->mapping.num_kccgst == 0) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid mapping: must have at least one value on the right "
                        "hand side; ignoring rule set");
            goto skip;
        }
    
        /*
         * This following is very stupid, but this is how it works.
         * See the "Notes" section in the overview above.
         */
    
        if (is_mlvo_mask_defined(m, MLVO_LAYOUT)) {
            assert(m->mapping.layout_idx != XKB_LAYOUT_INVALID);
            switch (m->mapping.layout_idx) {
                case LAYOUT_INDEX_SINGLE:
                    /* Layout rule without index matches when
                     * exactly 1 layout is specified */
                    if (darray_size(m->rmlvo.layouts) > 1)
                        goto skip;
                    break;
                case LAYOUT_INDEX_ANY:
                case LAYOUT_INDEX_LATER:
                case LAYOUT_INDEX_FIRST:
                    /* No restrictions */
                    break;
                default:
                    /* Layout rule with index matches when at least 2 layouts are
                     * specified. Index must be in valid range. */
                    if (darray_size(m->rmlvo.layouts) < 2 ||
                        m->mapping.layout_idx >= darray_size(m->rmlvo.layouts))
                        goto skip;
            }
        }
    
        if (is_mlvo_mask_defined(m, MLVO_VARIANT)) {
            assert(m->mapping.variant_idx != XKB_LAYOUT_INVALID);
            switch (m->mapping.variant_idx) {
                case LAYOUT_INDEX_SINGLE:
                    /* Variant rule without index matches
                     * when exactly 1 variant is specified */
                    if (darray_size(m->rmlvo.variants) > 1)
                        goto skip;
                    break;
                case LAYOUT_INDEX_ANY:
                case LAYOUT_INDEX_LATER:
                case LAYOUT_INDEX_FIRST:
                    /* No restriction */
                    break;
                default:
                    /* Variant rule with index matches when at least 2 variants are
                     * specified. Index must be in valid range. */
                    if (darray_size(m->rmlvo.variants) < 2 ||
                        m->mapping.variant_idx >= darray_size(m->rmlvo.variants))
                        goto skip;
            }
        }
    
        return true;
    
    skip:
        m->mapping.active = false;
        return false;
    }
    
    static void
    matcher_rule_start_new(struct matcher *m)
    {
        memset(&m->rule, 0, sizeof(m->rule));
        m->rule.skip = !m->mapping.active;
    }
    
    static void
    matcher_rule_set_mlvo_common(struct matcher *m, struct scanner *s,
                                 struct sval ident,
                                 enum mlvo_match_type match_type)
    {
        if (m->rule.num_mlvo_values >= m->mapping.num_mlvo) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid rule: has more values than the mapping line; "
                        "ignoring rule");
            m->rule.skip = true;
            return;
        }
        m->rule.match_type_at_pos[m->rule.num_mlvo_values] = match_type;
        m->rule.mlvo_value_at_pos[m->rule.num_mlvo_values] = ident;
        m->rule.num_mlvo_values++;
    }
    
    static void
    matcher_rule_set_mlvo_wildcard(struct matcher *m, struct scanner *s,
                                   enum mlvo_match_type match_type)
    {
        struct sval dummy = { NULL, 0 };
        matcher_rule_set_mlvo_common(m, s, dummy, match_type);
    }
    
    static void
    matcher_rule_set_mlvo_group(struct matcher *m, struct scanner *s,
                                struct sval ident)
    {
        matcher_rule_set_mlvo_common(m, s, ident, MLVO_MATCH_GROUP);
    }
    
    static void
    matcher_rule_set_mlvo(struct matcher *m, struct scanner *s,
                          struct sval ident)
    {
        matcher_rule_set_mlvo_common(m, s, ident, MLVO_MATCH_NORMAL);
    }
    
    static void
    matcher_rule_set_kccgst(struct matcher *m, struct scanner *s,
                            struct sval ident)
    {
        if (m->rule.num_kccgst_values >= m->mapping.num_kccgst) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid rule: has more values than the mapping line; "
                        "ignoring rule");
            m->rule.skip = true;
            return;
        }
        m->rule.kccgst_value_at_pos[m->rule.num_kccgst_values] = ident;
        m->rule.num_kccgst_values++;
    }
    
    static bool
    match_group(struct matcher *m, struct sval group_name, struct sval to)
    {
        struct group *group = NULL;
        struct sval *element;
        bool found = false;
    
        darray_foreach(group, m->groups) {
            if (svaleq(group->name, group_name)) {
                found = true;
                break;
            }
        }
    
        if (!found) {
            /*
             * rules/evdev intentionally uses some undeclared group names
             * in rules (e.g. commented group definitions which may be
             * uncommented if needed). So we continue silently.
             */
            return false;
        }
    
        darray_foreach(element, group->elements)
            if (svaleq(to, *element))
                return true;
    
        return false;
    }
    
    static bool
    match_value(struct matcher *m, struct sval val, struct sval to,
                enum mlvo_match_type match_type,
                enum wildcard_match_type wildcard_type)
    {
        switch (match_type) {
            case MLVO_MATCH_WILDCARD_LEGACY:
                /* Match empty values only if explicitly required */
                return wildcard_type == WILDCARD_MATCH_ALL || !!to.len;
            case MLVO_MATCH_WILDCARD_NONE:
                return !to.len;
            case MLVO_MATCH_WILDCARD_SOME:
                return !!to.len;
            case MLVO_MATCH_WILDCARD_ANY:
                /* Contrary to the legacy ‘*’, this wild card *always* matches */
                return true;
            case MLVO_MATCH_GROUP:
                return match_group(m, val, to);
            default:
                assert(match_type == MLVO_MATCH_NORMAL);
                return svaleq(val, to);
        }
    }
    
    static bool
    match_value_and_mark(struct matcher *m, struct sval val,
                         struct matched_sval *to, enum mlvo_match_type match_type,
                         enum wildcard_match_type wildcard_type)
    {
        bool matched = match_value(m, val, to->sval, match_type, wildcard_type);
        if (matched)
            to->matched = true;
        return matched;
    }
    
    /*
     * This function performs %-expansion on @value (see overview above),
     * and appends the result to @expanded.
     */
    static bool
    expand_rmlvo_in_kccgst_value(struct matcher *m, struct scanner *s,
                                 struct sval value, xkb_layout_index_t layout_idx,
                                 darray_char *expanded, size_t *i)
    {
        const char *str = value.start;
    
        /*
         * Some ugly hand-lexing here, but going through the scanner is more
         * trouble than it's worth, and the format is ugly on its own merit.
         */
        enum rules_mlvo mlv;
        xkb_layout_index_t idx;
        char pfx, sfx;
        struct matched_sval *expanded_value;
    
        /* %i not as layout/variant index "%l[%i]" but as qualifier ":%i" */
        if (str[*i] == 'i' &&
            (*i + 1 == value.len || is_merge_mode_prefix(str[*i + 1])))
        {
            if (layout_idx == XKB_LAYOUT_INVALID) {
                scanner_err(
                    s, XKB_ERROR_RULES_INVALID_LAYOUT_INDEX_PERCENT_EXPANSION,
                    "Invalid %%i in %%-expansion: there is no corresponding "
                    "layout nor variant in the MLVO fields of the rules header."
                );
                goto error;
            }
            (*i)++;
            char index_str[MAX_LAYOUT_INDEX_STR_LENGTH + 1];
            int count = snprintf(index_str, sizeof(index_str), "%"PRIu32,
                                 layout_idx + 1);
            darray_appends_nullterminate(*expanded, index_str, count);
            return true;
        }
    
        pfx = sfx = 0;
    
        /* Check for prefix. */
        if (str[*i] == '(' ||
            is_merge_mode_prefix(str[*i]) ||
            str[*i] == '_' || str[*i] == '-') {
            pfx = str[*i];
            if (str[*i] == '(') sfx = ')';
            if (++(*i) >= value.len) goto error;
        }
    
        /* Mandatory model/layout/variant specifier. */
        switch (str[(*i)++]) {
        case 'm': mlv = MLVO_MODEL; break;
        case 'l': mlv = MLVO_LAYOUT; break;
        case 'v': mlv = MLVO_VARIANT; break;
        default: goto error;
        }
    
        /* Check for index. */
        idx = XKB_LAYOUT_INVALID;
        bool expanded_index = false;
        if (*i < value.len && str[*i] == '[') {
            if (mlv != MLVO_LAYOUT && mlv != MLVO_VARIANT) {
                scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                            "invalid index in %%-expansion; "
                            "may only index layout or variant");
                goto error;
            }
    
            int consumed = extract_layout_index(str + (*i), value.len - (*i), &idx);
            if (consumed == -1) goto error;
            if (idx == XKB_LAYOUT_INVALID) {
                /* %i encountered */
                assert(layout_idx != XKB_LAYOUT_INVALID);
                idx = layout_idx;
                expanded_index = true;
            }
            *i += consumed;
        }
    
        /* Check for suffix, if there supposed to be one. */
        if (sfx != 0) {
            if (*i >= value.len) goto error;
            if (str[(*i)++] != sfx) goto error;
        }
    
        /* Get the expanded value. */
        expanded_value = NULL;
    
        if (mlv == MLVO_LAYOUT) {
            if (idx == XKB_LAYOUT_INVALID) {
                /* No index provided: match only if single layout */
                if (darray_size(m->rmlvo.layouts) == 1)
                    expanded_value = &darray_item(m->rmlvo.layouts, 0);
            /* Some index provided: expand only if it is %i or
             * if there are multiple layouts */
            } else if (idx < darray_size(m->rmlvo.layouts) &&
                       (expanded_index || darray_size(m->rmlvo.layouts) > 1)) {
                    expanded_value = &darray_item(m->rmlvo.layouts, idx);
            }
        }
        else if (mlv == MLVO_VARIANT) {
            if (idx == XKB_LAYOUT_INVALID) {
                /* No index provided: match only if single variant */
                if (darray_size(m->rmlvo.variants) == 1)
                    expanded_value = &darray_item(m->rmlvo.variants, 0);
            /* Some index provided: expand only if it is %i or
             * if there are multiple variants */
            } else if (idx < darray_size(m->rmlvo.variants) &&
                       (expanded_index || darray_size(m->rmlvo.variants) > 1)) {
                    expanded_value = &darray_item(m->rmlvo.variants, idx);
            }
        }
        else if (mlv == MLVO_MODEL) {
            expanded_value = &m->rmlvo.model;
        }
    
        /* If we didn't get one, skip silently. */
        if (!expanded_value || expanded_value->sval.len == 0) {
            return true;
        }
    
        if (pfx != 0)
            darray_appends_nullterminate(*expanded, &pfx, 1);
        darray_appends_nullterminate(*expanded,
                                     expanded_value->sval.start,
                                     (darray_size_t) expanded_value->sval.len);
        if (sfx != 0)
            darray_appends_nullterminate(*expanded, &sfx, 1);
        expanded_value->matched = true;
    
        return true;
    
    error:
        scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                    "invalid %%-expansion in value; not used");
        return false;
    }
    
    /*
     * This function performs :all replacement on @value (see overview above),
     * and appends the result to @expanded.
     */
    static void
    expand_qualifier_in_kccgst_value(
        struct matcher *m, struct scanner *s,
        struct sval value, darray_char *expanded,
        bool has_layout_idx_range, bool has_separator,
        darray_size_t prefix_idx, size_t *i)
    {
        const char *str = value.start;
    
        /* “all” followed by nothing or by a layout separator */
        if ((*i + 3 <= value.len || is_merge_mode_prefix(str[*i + 3])) &&
            str[*i] == 'a' && str[*i+1] == 'l' && str[*i+2] == 'l') {
            if (has_layout_idx_range)
                scanner_vrb(s, 2, XKB_LOG_MESSAGE_NO_ID,
                            "Using :all qualifier with indices range "
                            "is not recommended.");
            /* Add at least one layout */
            darray_appends_nullterminate(*expanded, "1", 1);
            /* Check for more layouts (slow path) */
            if (darray_size(m->rmlvo.layouts) > 1) {
                char layout_index[MAX_LAYOUT_INDEX_STR_LENGTH + 1];
                const darray_size_t prefix_length =
                    darray_size(*expanded) - prefix_idx - 1;
                for (xkb_layout_index_t l = 1;
                     l < MIN(XKB_MAX_GROUPS, darray_size(m->rmlvo.layouts));
                     l++)
                {
                    if (!has_separator)
                        darray_append(*expanded, MERGE_DEFAULT_PREFIX);
                    /* Append prefix */
                    darray_appends_nullterminate(*expanded,
                                                 &darray_item(*expanded, prefix_idx),
                                                 prefix_length);
                    /* Append index */
                    int count = snprintf(layout_index, sizeof(layout_index),
                                         "%"PRIu32, l + 1);
                    darray_appends_nullterminate(*expanded, layout_index, count);
                }
            }
            *i += 3;
        }
    }
    
    static inline void
    #ifdef _MSC_VER
    concat_kccgst(darray_char *into, darray_size_t size, _In_reads_(size) const char* from)
    #else
    concat_kccgst(darray_char *into, darray_size_t size, const char from[static size])
    #endif
    {
        /*
         * Appending  bar to  foo ->  foo (not an error if this happens)
         * Appending +bar to  foo ->  foo+bar
         * Appending  bar to +foo ->  bar+foo
         * Appending +bar to +foo -> +foo+bar
         */
        const bool from_plus = is_merge_mode_prefix(from[0]);
        if (from_plus || darray_empty(*into)) {
            darray_appends_nullterminate(*into, from, size);
        } else {
            const char ch =
                (char) (darray_empty(*into) ? '\0' : darray_item(*into, 0));
            const bool into_plus = is_merge_mode_prefix(ch);
            if (into_plus)
                darray_prepends_nullterminate(*into, from, size);
        }
    }
    
    /*
     * This function performs %-expansion and :all-expansion on @value
     * (see overview above), and appends the result to @to.
     */
    static bool
    append_expanded_kccgst_value(struct matcher *m, struct scanner *s,
                                 bool merge, darray_char *to, struct sval value,
                                 xkb_layout_index_t layout_idx)
    {
        const char *str = value.start;
        darray_char expanded = darray_new();
        darray_size_t last_item_idx = 0;
        bool has_separator = false;
    
        for (size_t i = 0; i < value.len; ) {
            /* Check if that's a start of an expansion or qualifier */
            switch (str[i]) {
                /* Qualifier */
                case ':':
                    darray_appends_nullterminate(expanded, &str[i++], 1);
                    expand_qualifier_in_kccgst_value(m, s, value, &expanded,
                                                     m->mapping.has_layout_idx_range,
                                                     has_separator,
                                                     last_item_idx, &i);
                    break;
                /* Expansion */
                case '%':
                    i++;
                    if (i >= value.len ||
                        !expand_rmlvo_in_kccgst_value(m, s, value, layout_idx,
                                                      &expanded, &i))
                            goto error;
                    break;
                /* New item */
                case MERGE_OVERRIDE_PREFIX:
                case MERGE_AUGMENT_PREFIX:
                case MERGE_REPLACE_PREFIX:
                    darray_appends_nullterminate(expanded, &str[i++], 1);
                    last_item_idx = darray_size(expanded) - 1;
                    has_separator = true;
                    break;
                /* Just a normal character. */
                default:
                    darray_appends_nullterminate(expanded, &str[i++], 1);
            }
        }
    
        /* See note: “Layout index ranges and merging KcCGST values” */
        if (merge) {
            if (!darray_empty(expanded))
                concat_kccgst(to, darray_size(expanded), darray_items(expanded));
        } else {
            darray_concat(*to, expanded);
        }
        darray_free(expanded);
        return true;
    
    error:
        darray_free(expanded);
        return false;
    }
    
    static bool
    matcher_append_pending_kccgst(struct matcher *m)
    {
        if (!m->mapping.has_layout_idx_range)
            return true;
        /*
         * Handle pending KcCGST values
         * See note: “Layout index ranges and merging KcCGST values”
         */
        for (kccgst_index_t i = 0; i < m->mapping.num_kccgst; i++) {
            const enum rules_kccgst kccgst = m->mapping.kccgst_at_pos[i];
            /* For each relevant layout, append the relevant KcCGST values to
             * the output. */
            for (xkb_layout_index_t layout = m->mapping.layout_idx_min;
                 layout < m->mapping.layout_idx_max;
                 layout++) {
                /* There may be multiple values to add if the rule set involved
                 * options. Process them sequentially. */
                register const struct kccgst_buffer* const buf = &m->pending_kccgst;
                size_t offset = 0;
                for (darray_size_t k = 0; k < darray_size(buf->slices); k++) {
                    register const struct kccgst_buffer_slice * const slice =
                        &darray_item(buf->slices, k);
                    if (slice->kccgst == kccgst && slice->layout == layout &&
                        slice->length)
                        concat_kccgst(&m->kccgst[kccgst], slice->length,
                                      darray_items(buf->buffer) + offset);
                    offset += slice->length;
                }
            }
        }
        /* Ensure we won’t come here before the next relevant rule set */
        m->mapping.has_layout_idx_range = false;
        return true;
    }
    
    static void
    matcher_rule_verify(struct matcher *m, struct scanner *s)
    {
        if (m->rule.num_mlvo_values != m->mapping.num_mlvo ||
            m->rule.num_kccgst_values != m->mapping.num_kccgst) {
            scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                        "invalid rule: must have same number of values "
                        "as mapping line; ignoring rule");
            m->rule.skip = true;
        }
    }
    
    static void
    matcher_rule_apply_if_matches(struct matcher *m, struct scanner *s)
    {
        /* Initial candidates (used if m->mapping.has_layout_idx_range == true) */
        xkb_layout_mask_t candidate_layouts = m->mapping.layouts_candidates_mask;
        xkb_layout_index_t idx;
        /* Loop over MLVO pattern components */
        for (mlvo_index_t i = 0; i < m->mapping.num_mlvo; i++) {
            enum rules_mlvo mlvo = m->mapping.mlvo_at_pos[i];
            struct sval value = m->rule.mlvo_value_at_pos[i];
            enum mlvo_match_type match_type = m->rule.match_type_at_pos[i];
            struct matched_sval *to;
            bool matched = false;
    
            /* NOTE: Wild card * matches empty values only for model and options, as
             * implemented in libxkbfile and xserver. The reason for such different
             * treatment is not documented. */
            if (mlvo == MLVO_MODEL) {
                to = &m->rmlvo.model;
                matched = match_value_and_mark(m, value, to, match_type,
                                               WILDCARD_MATCH_ALL);
            } else if (m->mapping.has_layout_idx_range) {
                /* Special index: loop over the index range */
                for (idx = m->mapping.layout_idx_min;
                     idx < m->mapping.layout_idx_max && candidate_layouts;
                     idx++)
                {
                    /* Process only if index not skipped */
                    const xkb_layout_mask_t mask = UINT32_C(1) << idx;
                    if (candidate_layouts & mask) {
                        switch (mlvo) {
                        case MLVO_LAYOUT:
                            to = &darray_item(m->rmlvo.layouts, idx);
                            if (match_value_and_mark(m, value, to, match_type,
                                                     WILDCARD_MATCH_NONEMPTY)) {
                                /* Mark matched, keep index */
                                matched = true;
                            } else {
                                /* Not matched, remove index */
                                candidate_layouts &= ~mask;
                            }
                            break;
                        case MLVO_VARIANT:
                            to = &darray_item(m->rmlvo.variants, idx);
                            if (match_value_and_mark(m, value, to, match_type,
                                                     WILDCARD_MATCH_NONEMPTY)) {
                                /* Mark matched, keep index */
                                matched = true;
                            } else {
                                /* Not matched, remove index */
                                candidate_layouts &= ~mask;
                            }
                            break;
                        default:
                            assert(mlvo == MLVO_OPTION);
                            bool found_option = false;
                            darray_foreach(to, m->rmlvo.options) {
                                /*
                                 * Skip if layout-specific option and the target
                                 * layout does not match.
                                 */
                                if (to->layout != OPTIONS_MATCH_ALL_GROUPS &&
                                    to->layout != idx)
                                    continue;
                                if (match_value_and_mark(m, value, to, match_type,
                                                         WILDCARD_MATCH_ALL)) {
                                    /* Mark matched, keep index */
                                    matched = true;
                                    found_option = true;
                                    break;
                                }
                            }
                            if (!found_option) {
                                /* Not matched, remove index */
                                candidate_layouts &= ~mask;
                            }
                        }
                    }
                }
            } else {
                /* Numeric index or no index */
                switch (mlvo) {
                case MLVO_LAYOUT:
                    to = &darray_item(m->rmlvo.layouts,
                                      m->mapping.layout_idx_min);
                    matched = match_value_and_mark(m, value, to, match_type,
                                                   WILDCARD_MATCH_NONEMPTY);
                    break;
                case MLVO_VARIANT:
                    to = &darray_item(m->rmlvo.variants,
                                      m->mapping.layout_idx_min);
                    matched = match_value_and_mark(m, value, to, match_type,
                                                   WILDCARD_MATCH_NONEMPTY);
                    break;
                default:
                    assert(mlvo == MLVO_OPTION);
                    darray_foreach(to, m->rmlvo.options) {
                        /*
                         * Skip if it is a layout-specific option and either:
                         * - the rule has no layout nor variant field
                         *   (layout_idx_min == XKB_LAYOUT_INVALID), or
                         * - the target layout index does not match.
                         */
                        if (to->layout != OPTIONS_MATCH_ALL_GROUPS &&
                            to->layout != m->mapping.layout_idx_min)
                            continue;
                        matched = match_value_and_mark(m, value, to, match_type,
                                                       WILDCARD_MATCH_ALL);
                        if (matched)
                            break;
                    }
                }
            }
    
            if (!matched)
                return;
        }
    
        if (m->mapping.has_layout_idx_range) {
            /* Special index: loop over the index range */
            for (idx = m->mapping.layout_idx_min;
                 idx < m->mapping.layout_idx_max;
                 idx++)
            {
                if (candidate_layouts & (UINT32_C(1) << idx)) {
                    for (kccgst_index_t i = 0; i < m->mapping.num_kccgst; i++) {
                        const enum rules_kccgst kccgst = m->mapping.kccgst_at_pos[i];
                        const struct sval value = m->rule.kccgst_value_at_pos[i];
                        /*
                         * [NOTE] Layout index ranges and merging KcCGST values
                         *
                         * Layout indices match following first the order of the
                         * rules in the file, then their natural order. So do not
                         * merge with the output for now but buffer the resulting
                         * KcCGST value and wait reaching the end of the rule set.
                         *
                         * Because the rule set may also involve options, it may
                         * match multiple times for the *same* layout index. So
                         * buffer the result of *each* match.
                         *
                         * When the end of the rule set is reached, merge buffered
                         * KcCGST sequentially, following first the layouts order,
                         * then the order of the rules in the file.
                         *
                         * Example:
                         *
                         * ! model = symbols
                         *   *     = pc
                         * ! layout[any] option = symbols
                         *   C           1      = +c1:%i
                         *   C           2      = +c2:%i
                         *   B           3      = skip
                         *   B           4      = +b:%i
                         *
                         * The result of {layout: "A,B,C", options: "4,3,2,1"} is:
                         * symbols = pc+b:2+c1:3+c2:3.
                         *
                         * - `skip` was dropped because it has no explicit merge
                         *   mode;
                         * - although every rule was matched in order, the resulting
                         *   order of the symbols follows the order of the layouts,
                         *   so `+b` appears before `+c1` and `+c2`.
                         * - the relative order of the options for layout C follows
                         *   the order within the rule set, not the order of RMLVO.
                         */
                        register struct kccgst_buffer * const buf =
                            &m->pending_kccgst;
                        const darray_size_t prev_buffer_length =
                            darray_size(buf->buffer);
                        append_expanded_kccgst_value(m, s, false, &buf->buffer,
                                                     value, idx);
                        const uint32_t length = (uint32_t) (darray_size(buf->buffer)
                                              - prev_buffer_length);
                        const struct kccgst_buffer_slice slice = {
                            .length = length,
                            .kccgst = kccgst,
                            .layout = idx
                        };
                        darray_append(buf->slices, slice);
                    }
                }
            }
        } else {
            /* Numeric index or no index */
            for (kccgst_index_t i = 0; i < m->mapping.num_kccgst; i++) {
                enum rules_kccgst kccgst = m->mapping.kccgst_at_pos[i];
                struct sval value = m->rule.kccgst_value_at_pos[i];
                append_expanded_kccgst_value(m, s, true, &m->kccgst[kccgst], value,
                                             m->mapping.layout_idx_min);
            }
        }
    
        /*
         * If a rule matches in a rule set, the rest of the set should be
         * skipped. However, rule sets matching against options may contain
         * several legitimate rules, so they are processed entirely.
         */
        if (!(is_mlvo_mask_defined(m, MLVO_OPTION))) {
            m->mapping.layouts_candidates_mask &= ~candidate_layouts;
        }
    }
    
    static enum rules_token
    gettok(struct matcher *m, struct scanner *s)
    {
        return lex(s, &m->val);
    }
    
    static bool
    matcher_match(struct matcher *m, struct scanner *s,
                  unsigned int include_depth,
                  const char *string, size_t len,
                  const char *file_name)
    {
        enum rules_token tok;
    
        if (!m)
            return false;
    
    initial:
        switch (tok = gettok(m, s)) {
        case TOK_BANG:
            goto bang;
        case TOK_END_OF_LINE:
            goto initial;
        case TOK_END_OF_FILE:
            goto finish;
        default:
            goto unexpected;
        }
    
    bang:
        switch (tok = gettok(m, s)) {
        case TOK_GROUP_NAME:
            matcher_group_start_new(m, m->val.string);
            goto group_name;
        case TOK_INCLUDE:
            goto include_statement;
        case TOK_IDENTIFIER:
            matcher_mapping_start_new(m);
            matcher_mapping_set_mlvo(m, s, m->val.string);
            goto mapping_mlvo;
        default:
            goto unexpected;
        }
    
    group_name:
        switch (tok = gettok(m, s)) {
        case TOK_EQUALS:
            goto group_element;
        default:
            goto unexpected;
        }
    
    group_element:
        switch (tok = gettok(m, s)) {
        case TOK_IDENTIFIER:
            matcher_group_add_element(m, s, m->val.string);
            goto group_element;
        case TOK_END_OF_LINE:
            goto initial;
        default:
            goto unexpected;
        }
    
    include_statement:
        switch (tok = gettok(m, s)) {
        case TOK_IDENTIFIER:
            matcher_include(m, s, include_depth, m->val.string);
            goto include_statement_end;
        default:
            goto unexpected;
        }
    
    include_statement_end:
        switch (tok = gettok(m, s)) {
        case TOK_END_OF_LINE:
            goto initial;
        default:
            goto unexpected;
        }
    
    mapping_mlvo:
        switch (tok = gettok(m, s)) {
        case TOK_IDENTIFIER:
            if (m->mapping.active)
                matcher_mapping_set_mlvo(m, s, m->val.string);
            goto mapping_mlvo;
        case TOK_EQUALS:
            goto mapping_kccgst;
        default:
            goto unexpected;
        }
    
    mapping_kccgst:
        switch (tok = gettok(m, s)) {
        case TOK_IDENTIFIER:
            if (m->mapping.active)
                matcher_mapping_set_kccgst(m, s, m->val.string);
            goto mapping_kccgst;
        case TOK_END_OF_LINE:
            if (m->mapping.active && matcher_mapping_verify(m, s)) {
                matcher_mapping_set_layout_bounds(m);
                if (m->mapping.has_layout_idx_range) {
                    /* Lazily reset buffers for layout index ranges.
                     * We’ll reuse the allocations. */
                    darray_size(m->pending_kccgst.buffer) = 0;
                    darray_size(m->pending_kccgst.slices) = 0;
                }
            }
            goto rule_mlvo_first;
        default:
            goto unexpected;
        }
    
    rule_mlvo_first:
        switch (tok = gettok(m, s)) {
        case TOK_BANG:
            matcher_append_pending_kccgst(m);
            goto bang;
        case TOK_END_OF_LINE:
            goto rule_mlvo_first;
        case TOK_END_OF_FILE:
            matcher_append_pending_kccgst(m);
            goto finish;
        default:
            matcher_rule_start_new(m);
            goto rule_mlvo_no_tok;
        }
    
    rule_mlvo:
        tok = gettok(m, s);
    rule_mlvo_no_tok:
        switch (tok) {
        case TOK_IDENTIFIER:
            if (!m->rule.skip) {
                if (m->val.string.len == 1 && m->val.string.start[0] == '+')
                    matcher_rule_set_mlvo_wildcard(m, s, MLVO_MATCH_WILDCARD_SOME);
                else
                    matcher_rule_set_mlvo(m, s, m->val.string);
            }
            goto rule_mlvo;
        case TOK_WILD_CARD_STAR:
            if (!m->rule.skip)
                matcher_rule_set_mlvo_wildcard(m, s, MLVO_MATCH_WILDCARD_LEGACY);
            goto rule_mlvo;
        case TOK_WILD_CARD_NONE:
            if (!m->rule.skip)
                matcher_rule_set_mlvo_wildcard(m, s, MLVO_MATCH_WILDCARD_NONE);
            goto rule_mlvo;
        case TOK_WILD_CARD_SOME:
            if (!m->rule.skip)
                matcher_rule_set_mlvo_wildcard(m, s, MLVO_MATCH_WILDCARD_SOME);
            goto rule_mlvo;
        case TOK_WILD_CARD_ANY:
            if (!m->rule.skip)
                matcher_rule_set_mlvo_wildcard(m, s, MLVO_MATCH_WILDCARD_ANY);
            goto rule_mlvo;
        case TOK_GROUP_NAME:
            if (!m->rule.skip)
                matcher_rule_set_mlvo_group(m, s, m->val.string);
            goto rule_mlvo;
        case TOK_EQUALS:
            goto rule_kccgst;
        default:
            goto unexpected;
        }
    
    rule_kccgst:
        switch (tok = gettok(m, s)) {
        case TOK_IDENTIFIER:
            if (!m->rule.skip)
                matcher_rule_set_kccgst(m, s, m->val.string);
            goto rule_kccgst;
        case TOK_END_OF_LINE:
            if (!m->rule.skip)
                matcher_rule_verify(m, s);
            if (!m->rule.skip)
                matcher_rule_apply_if_matches(m, s);
            goto rule_mlvo_first;
        default:
            goto unexpected;
        }
    
    unexpected:
        switch (tok) {
        case TOK_ERROR:
            goto error;
        default:
            goto state_error;
        }
    
    finish:
        return true;
    
    state_error:
        scanner_err(s, XKB_ERROR_INVALID_RULES_SYNTAX,
                    "unexpected token");
    error:
        return false;
    }
    
    static bool
    read_rules_file(struct xkb_context *ctx,
                    struct matcher *matcher,
                    unsigned int include_depth,
                    FILE *file,
                    const char *path)
    {
        bool ret;
        char *string;
        size_t size;
        struct scanner scanner;
    
        if (!map_file(file, &string, &size)) {
            log_err(ctx, XKB_LOG_MESSAGE_NO_ID,
                    "Couldn't read rules file \"%s\": %s\n",
                    path, strerror(errno));
            return false;
        }
    
        scanner_init(&scanner, matcher->ctx, string, size, path, NULL);
    
        /* Basic detection of wrong character encoding.
           The first character relevant to the grammar must be ASCII:
           whitespace, !, / (for comment) */
        if (!scanner_check_supported_char_encoding(&scanner)) {
            scanner_err(&scanner, XKB_ERROR_INVALID_FILE_ENCODING,
                        "This could be a file encoding issue. "
                        "Supported encodings must be backward compatible with ASCII.");
            scanner_err(&scanner, XKB_ERROR_INVALID_FILE_ENCODING,
                        "E.g. ISO/CEI 8859 and UTF-8 are supported "
                        "but UTF-16, UTF-32 and CP1026 are not.");
            unmap_file(string, size);
            return false;
        }
    
        ret = matcher_match(matcher, &scanner, include_depth, string, size, path);
        unmap_file(string, size);
        return ret;
    }
    
    static bool
    xkb_resolve_rules(struct xkb_context *ctx,
                      const char* rules, struct matcher *matcher,
                      struct xkb_component_names *out,
                      xkb_layout_index_t *explicit_layouts)
    {
        bool ret = false;
        FILE *file;
        struct matched_sval *mval;
        unsigned int offset = 0;
        char path[PATH_MAX];
    
        file = FindFileInXkbPath(ctx, "(unknown)",
                                 rules, strlen(rules), FILE_TYPE_RULES,
                                 path, sizeof(path), &offset);
        if (!file) {
            log_err(ctx, XKB_ERROR_CANNOT_RESOLVE_RMLVO,
                    "Cannot load XKB rules \"%s\"\n", rules);
            goto err_out;
        }
    
        ret = read_rules_file(ctx, matcher, 0, file, path);
        if (!ret ||
            darray_empty(matcher->kccgst[KCCGST_KEYCODES]) ||
            darray_empty(matcher->kccgst[KCCGST_TYPES]) ||
            darray_empty(matcher->kccgst[KCCGST_COMPAT]) ||
            /* darray_empty(matcher->kccgst[KCCGST_GEOMETRY]) || */
            darray_empty(matcher->kccgst[KCCGST_SYMBOLS])) {
            log_err(ctx, XKB_ERROR_CANNOT_RESOLVE_RMLVO,
                    "No components returned from XKB rules \"%s\"\n", path);
            ret = false;
            goto err_out;
        }
    
        darray_steal(matcher->kccgst[KCCGST_KEYCODES], &out->keycodes, NULL);
        darray_steal(matcher->kccgst[KCCGST_TYPES], &out->types, NULL);
        darray_steal(matcher->kccgst[KCCGST_COMPAT], &out->compatibility, NULL);
        darray_steal(matcher->kccgst[KCCGST_SYMBOLS], &out->symbols, NULL);
        darray_steal(matcher->kccgst[KCCGST_GEOMETRY], &out->geometry, NULL);
    
        mval = &matcher->rmlvo.model;
        if (!mval->matched && mval->sval.len > 0)
            log_err(matcher->ctx, XKB_ERROR_CANNOT_RESOLVE_RMLVO,
                    "Unrecognized RMLVO model \"%.*s\" was ignored\n",
                    (unsigned int) mval->sval.len, mval->sval.start);
        darray_foreach(mval, matcher->rmlvo.layouts)
            if (!mval->matched && mval->sval.len > 0)
                log_err(matcher->ctx, XKB_ERROR_CANNOT_RESOLVE_RMLVO,
                        "Unrecognized RMLVO layout \"%.*s\" was ignored\n",
                        (unsigned int) mval->sval.len, mval->sval.start);
        darray_foreach(mval, matcher->rmlvo.variants)
            if (!mval->matched && mval->sval.len > 0)
                log_err(matcher->ctx, XKB_ERROR_CANNOT_RESOLVE_RMLVO,
                        "Unrecognized RMLVO variant \"%.*s\" was ignored\n",
                        (unsigned int) mval->sval.len, mval->sval.start);
        darray_foreach(mval, matcher->rmlvo.options)
            if (!mval->matched && mval->sval.len > 0)
                log_err(matcher->ctx, XKB_ERROR_CANNOT_RESOLVE_RMLVO,
                        "Unrecognized RMLVO option \"%.*s\" was ignored\n",
                        (unsigned int) mval->sval.len, mval->sval.start);
    
        /* Set the number of explicit layouts */
        if (out->symbols != NULL && explicit_layouts != NULL) {
            *explicit_layouts = 1; /* at least one group */
            const char *symbols = out->symbols;
            /* Take the highest modifier */
            while ((symbols = strchr(symbols, ':')) != NULL && symbols[1] != '\0') {
                xkb_layout_index_t group = 0;
                const int count = parse_dec_to_uint32_t(++symbols, SIZE_MAX, &group);
                /* Update only when valid group index, but continue parsing
                 * even on invalid ones, as we do not handle them here. */
                if (count > 0 && (symbols[count] == '\0' ||
                    is_merge_mode_prefix(symbols[count])) &&
                    group > 0 && group <= XKB_MAX_GROUPS) {
                    *explicit_layouts = MAX(*explicit_layouts, group);
                    symbols += count;
                }
            }
        }
    
    err_out:
        if (file)
            fclose(file);
    
        return ret;
    }
    
    bool
    xkb_components_from_rmlvo_builder(const struct xkb_rmlvo_builder *rmlvo,
                                      struct xkb_component_names *out,
                                      xkb_layout_index_t *explicit_layouts)
    {
        const char *rules = rmlvo->rules;
        struct matcher *matcher = matcher_new_from_rmlvo(rmlvo, &rules);
        if (!matcher)
            return false;
    
        const bool ret = xkb_resolve_rules(rmlvo->ctx, rules, matcher,
                                           out, explicit_layouts);
    
        matcher_free(matcher);
        return ret;
    }
    
    
    bool
    xkb_components_from_rules_names(struct xkb_context *ctx,
                                    const struct xkb_rule_names *rmlvo,
                                    struct xkb_component_names *out,
                                    xkb_layout_index_t *explicit_layouts)
    {
        struct matcher *matcher = matcher_new_from_names(ctx, rmlvo);
        if (!matcher)
            return false;
    
        const bool ret = xkb_resolve_rules(ctx, rmlvo->rules, matcher,
                                           out, explicit_layouts);
    
        matcher_free(matcher);
        return ret;
    }