Removed build dependency on xproto.
[platform/upstream/libxkbcommon.git] / src / xkbcomp / rules.c
index 107f602..f5d9c49 100644 (file)
 /************************************************************
- Copyright (c) 1996 by Silicon Graphics Computer Systems, Inc.
-
- Permission to use, copy, modify, and distribute this
- software and its documentation for any purpose and without
- fee is hereby granted, provided that the above copyright
- notice appear in all copies and that both that copyright
- notice and this permission notice appear in supporting
- documentation, and that the name of Silicon Graphics not be
- used in advertising or publicity pertaining to distribution
- of the software without specific prior written permission.
- Silicon Graphics makes no representation about the suitability
- of this software for any purpose. It is provided "as is"
- without any express or implied warranty.
-
- SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
- SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON
- GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
- DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
- DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION  WITH
- THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
Copyright (c) 1996 by Silicon Graphics Computer Systems, Inc.
+ *
Permission to use, copy, modify, and distribute this
software and its documentation for any purpose and without
fee is hereby granted, provided that the above copyright
notice appear in all copies and that both that copyright
notice and this permission notice appear in supporting
documentation, and that the name of Silicon Graphics not be
used in advertising or publicity pertaining to distribution
of the software without specific prior written permission.
Silicon Graphics makes no representation about the suitability
of this software for any purpose. It is provided "as is"
without any express or implied warranty.
+ *
SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON
GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION  WITH
THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
  ********************************************************/
 
-#include <stdio.h>
-#include <ctype.h>
+/*
+ * Copyright © 2012 Ran Benita <ran234@gmail.com>
+ *
+ * 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 "xkbcomp-priv.h"
 #include "rules.h"
-#include "path.h"
+#include "include.h"
+#include "scanner-utils.h"
 
-#define DFLT_LINE_SIZE 128
+#define MAX_INCLUDE_DEPTH 5
 
-struct input_line {
-    size_t size;
-    size_t offset;
-    char buf[DFLT_LINE_SIZE];
-    char *line;
+/* Scanner / Lexer */
+
+/* Values returned with some tokens, like yylval. */
+union lvalue {
+    struct sval string;
 };
 
-static void
-input_line_init(struct input_line *line)
-{
-    line->size = DFLT_LINE_SIZE;
-    line->offset = 0;
-    line->line = line->buf;
-}
+enum rules_token {
+    TOK_END_OF_FILE = 0,
+    TOK_END_OF_LINE,
+    TOK_IDENTIFIER,
+    TOK_GROUP_NAME,
+    TOK_BANG,
+    TOK_EQUALS,
+    TOK_STAR,
+    TOK_INCLUDE,
+    TOK_ERROR
+};
 
-static void
-input_line_deinit(struct input_line *line)
+static inline bool
+is_ident(char ch)
 {
-    if (line->line != line->buf)
-        free(line->line);
-    line->offset = 0;
-    line->size = DFLT_LINE_SIZE;
-    line->line = line->buf;
+    return is_graph(ch) && ch != '\\';
 }
 
-static int
-input_line_add_char(struct input_line *line, int ch)
+static enum rules_token
+lex(struct scanner *s, union lvalue *val)
 {
-    if (line->offset >= line->size) {
-        if (line->line == line->buf) {
-            line->line = malloc(line->size * 2);
-            memcpy(line->line, line->buf, line->size);
-        }
-        else {
-            line->line = realloc(line->line, line->size * 2);
-        }
+skip_more_whitespace_and_comments:
+    /* Skip spaces. */
+    while (scanner_chr(s, ' ') || scanner_chr(s, '\t') || scanner_chr(s, '\r'));
 
-        line->size *= 2;
+    /* Skip comments. */
+    if (scanner_lit(s, "//")) {
+        scanner_skip_to_eol(s);
     }
 
-    line->line[line->offset++] = ch;
-    return ch;
-}
-
-static bool
-input_line_get(FILE *file, struct input_line *line)
-{
-    int ch;
-    bool end_of_file = false;
-    bool space_pending;
-    bool slash_pending;
-    bool in_comment;
-
-    while (!end_of_file && line->offset == 0) {
-        space_pending = slash_pending = in_comment = false;
-
-        while ((ch = getc(file)) != '\n' && ch != EOF) {
-            if (ch == '\\') {
-                ch = getc(file);
-
-                if (ch == EOF)
-                    break;
-
-                if (ch == '\n') {
-                    in_comment = false;
-                    ch = ' ';
-                }
-            }
-
-            if (in_comment)
-                continue;
-
-            if (ch == '/') {
-                if (slash_pending) {
-                    in_comment = true;
-                    slash_pending = false;
-                }
-                else {
-                    slash_pending = true;
-                }
-
-                continue;
-            }
-
-            if (slash_pending) {
-                if (space_pending) {
-                    input_line_add_char(line, ' ');
-                    space_pending = false;
-                }
-
-                input_line_add_char(line, '/');
-                slash_pending = false;
-            }
-
-            if (isspace(ch)) {
-                while (isspace(ch) && ch != '\n' && ch != EOF)
-                    ch = getc(file);
-
-                if (ch == EOF)
-                    break;
-
-                if (ch != '\n' && line->offset > 0)
-                    space_pending = true;
-
-                ungetc(ch, file);
-            }
-            else {
-                if (space_pending) {
-                    input_line_add_char(line, ' ');
-                    space_pending = false;
-                }
-
-                if (ch == '!') {
-                    if (line->offset != 0) {
-                        WARN("The '!' is legal only at start of line\n");
-                        ACTION("Line containing '!' ignored\n");
-                        line->offset = 0;
-                        break;
-                    }
-                }
+    /* New line. */
+    if (scanner_eol(s)) {
+        while (scanner_eol(s)) scanner_next(s);
+        return TOK_END_OF_LINE;
+    }
 
-                input_line_add_char(line, ch);
-            }
+    /* Escaped line continuation. */
+    if (scanner_chr(s, '\\')) {
+        /* Optional \r. */
+        scanner_chr(s, '\r');
+        if (!scanner_eol(s)) {
+            scanner_err(s, "illegal new line escape; must appear at end of line");
+            return TOK_ERROR;
         }
+        scanner_next(s);
+        goto skip_more_whitespace_and_comments;
+    }
 
-        if (ch == EOF)
-            end_of_file = true;
+    /* See if we're done. */
+    if (scanner_eof(s)) return TOK_END_OF_FILE;
+
+    /* New token. */
+    s->token_line = s->line;
+    s->token_column = s->column;
+
+    /* Operators and punctuation. */
+    if (scanner_chr(s, '!')) return TOK_BANG;
+    if (scanner_chr(s, '=')) return TOK_EQUALS;
+    if (scanner_chr(s, '*')) return TOK_STAR;
+
+    /* 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, "unexpected character after \'$\'; expected name");
+            return TOK_ERROR;
+        }
+        return TOK_GROUP_NAME;
     }
 
-    if (line->offset == 0 && end_of_file)
-        return false;
+    /* Include statement. */
+    if (scanner_lit(s, "include"))
+        return TOK_INCLUDE;
+
+    /* Identifier. */
+    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;
+    }
 
-    input_line_add_char(line, '\0');
-    return true;
+    scanner_err(s, "unrecognized token");
+    return TOK_ERROR;
 }
 
 /***====================================================================***/
 
-enum {
-    /* "Parts" - the MLVO which rules file maps to components. */
-    MODEL = 0,
-    LAYOUT,
-    VARIANT,
-    OPTION,
-
-#define PART_MASK \
-    ((1 << MODEL) | (1 << LAYOUT) | (1 << VARIANT) | (1 << OPTION))
-
-    /* Components */
-    KEYCODES,
-    SYMBOLS,
-    TYPES,
-    COMPAT,
-    GEOMETRY,
-    KEYMAP,
-
-#define COMPONENT_MASK \
-    ((1 << KEYCODES) | (1 << SYMBOLS) | (1 << TYPES) | (1 << COMPAT) | \
-     (1 << GEOMETRY) | (1 << KEYMAP))
-
-    MAX_WORDS
+enum rules_mlvo {
+    MLVO_MODEL,
+    MLVO_LAYOUT,
+    MLVO_VARIANT,
+    MLVO_OPTION,
+    _MLVO_NUM_ENTRIES
 };
 
-static const char *cname[] = {
-    [MODEL] = "model",
-    [LAYOUT] = "layout",
-    [VARIANT] = "variant",
-    [OPTION] = "option",
-
-    [KEYCODES] = "keycodes",
-    [SYMBOLS] = "symbols",
-    [TYPES] = "types",
-    [COMPAT] = "compat",
-    [GEOMETRY] = "geometry",
-    [KEYMAP] = "keymap",
-};
+#define SVAL_LIT(literal) { literal, sizeof(literal) - 1 }
 
-struct var_defs {
-    const char *model;
-    const char *layout;
-    const char *variant;
-    const char *options;
+static const struct sval rules_mlvo_svals[_MLVO_NUM_ENTRIES] = {
+    [MLVO_MODEL] = SVAL_LIT("model"),
+    [MLVO_LAYOUT] = SVAL_LIT("layout"),
+    [MLVO_VARIANT] = SVAL_LIT("variant"),
+    [MLVO_OPTION] = SVAL_LIT("option"),
 };
 
-struct mapping {
-    /* Sequential id for the mappings. */
-    int number;
-    size_t num_maps;
-
-    struct {
-        int word;
-        int index;
-    } map[MAX_WORDS];
+enum rules_kccgst {
+    KCCGST_KEYCODES,
+    KCCGST_TYPES,
+    KCCGST_COMPAT,
+    KCCGST_SYMBOLS,
+    KCCGST_GEOMETRY,
+    _KCCGST_NUM_ENTRIES
 };
 
-struct multi_defs {
-    const char *model;
-    const char *layout[XkbNumKbdGroups + 1];
-    const char *variant[XkbNumKbdGroups + 1];
-    char *options;
+static const struct sval rules_kccgst_svals[_KCCGST_NUM_ENTRIES] = {
+    [KCCGST_KEYCODES] = SVAL_LIT("keycodes"),
+    [KCCGST_TYPES] = SVAL_LIT("types"),
+    [KCCGST_COMPAT] = SVAL_LIT("compat"),
+    [KCCGST_SYMBOLS] = SVAL_LIT("symbols"),
+    [KCCGST_GEOMETRY] = SVAL_LIT("geometry"),
 };
 
-struct var_desc {
-    char *name;
-    char *desc;
+/* 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;
 };
+typedef darray(struct matched_sval) darray_matched_sval;
 
-struct describe_vars {
-    size_t sz_desc;
-    size_t num_desc;
-    struct var_desc *desc;
+/*
+ * 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 {
-    int number;
-    char *name;
-    char *words;
+    struct sval name;
+    darray_sval elements;
 };
 
-enum rule_flag {
-    RULE_FLAG_PENDING_MATCH = (1L << 1),
-    RULE_FLAG_OPTION        = (1L << 2),
-    RULE_FLAG_APPEND        = (1L << 3),
-    RULE_FLAG_NORMAL        = (1L << 4),
+struct mapping {
+    int mlvo_at_pos[_MLVO_NUM_ENTRIES];
+    unsigned int num_mlvo;
+    unsigned int defined_mlvo_mask;
+    xkb_layout_index_t layout_idx, variant_idx;
+    int kccgst_at_pos[_KCCGST_NUM_ENTRIES];
+    unsigned int num_kccgst;
+    unsigned int defined_kccgst_mask;
+    bool skip;
 };
 
-struct rule {
-    int number;
-
-    char *model;
-    char *layout;
-    int layout_num;
-    char *variant;
-    int variant_num;
-    char *option;
-
-    /* yields */
-
-    char *keycodes;
-    char *symbols;
-    char *types;
-    char *compat;
-    char *keymap;
-    unsigned flags;
+enum mlvo_match_type {
+    MLVO_MATCH_NORMAL = 0,
+    MLVO_MATCH_WILDCARD,
+    MLVO_MATCH_GROUP,
 };
 
-struct rules {
-    struct describe_vars models;
-    struct describe_vars layouts;
-    struct describe_vars variants;
-    struct describe_vars options;
-
-    size_t sz_rules;
-    size_t num_rules;
-    struct rule *rules;
-
-    size_t sz_groups;
-    size_t num_groups;
-    struct group *groups;
+struct rule {
+    struct sval mlvo_value_at_pos[_MLVO_NUM_ENTRIES];
+    enum mlvo_match_type match_type_at_pos[_MLVO_NUM_ENTRIES];
+    unsigned int num_mlvo_values;
+    struct sval kccgst_value_at_pos[_KCCGST_NUM_ENTRIES];
+    unsigned int num_kccgst_values;
+    bool skip;
 };
 
-/***====================================================================***/
-
 /*
- * Resolve numeric index, such as "[4]" in layout[4]. Missing index
- * means zero.
+ * This is the main object used to match a given RMLVO against a rules
+ * file and aggragate the results in a KcCGST. It goes through a simple
+ * matching state machine, with tokens as transitions (see
+ * matcher_match()).
  */
-static char *
-get_index(char *str, int *ndx)
-{
-    int empty = 0, consumed = 0, num;
-
-    sscanf(str, "[%n%d]%n", &empty, &num, &consumed);
-    if (consumed > 0) {
-        *ndx = num;
-        str += consumed;
-    } else if (empty > 0) {
-        *ndx = -1;
-    } else {
-        *ndx = 0;
-    }
+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;
+    /* Output. */
+    darray_char kccgst[_KCCGST_NUM_ENTRIES];
+};
 
-    return str;
+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;
 }
 
-/*
- * Match a mapping line which opens a rule, e.g:
- * ! model      layout[4]       variant[4]      =       symbols       geometry
- * Which will be followed by lines such as:
- *   *          ben             basic           =       +in(ben):4    nec(pc98)
- * So if the MLVO matches the LHS of some line, we'll get the components
- * on the RHS.
- * In this example, we will get for the second and fourth columns:
- * mapping->map[1] = {.word = LAYOUT, .index = 4}
- * mapping->map[3] = {.word = SYMBOLS, .index = 0}
- */
-static void
-match_mapping_line(struct input_line *line, struct mapping *mapping)
+static darray_matched_sval
+split_comma_separated_mlvo(const char *s)
 {
-    char *tok;
-    char *str = &line->line[1];
-    unsigned present = 0, layout_ndx_present = 0, variant_ndx_present = 0;
-    int i, tmp;
-    size_t len;
-    int ndx;
-    char *strtok_buf;
-    bool found;
+    darray_matched_sval arr = darray_new();
 
     /*
-     * Remember the last sequential mapping id (incremented if the match
-     * is successful).
+     * Make sure the array returned by this function always includes at
+     * least one value, e.g. "" -> { "" } and "," -> { "", "" }.
      */
-    tmp = mapping->number;
-    memset(mapping, 0, sizeof(*mapping));
-    mapping->number = tmp;
 
-    while ((tok = strtok_r(str, " ", &strtok_buf)) != NULL) {
-        found = false;
-        str = NULL;
+    if (!s) {
+        struct matched_sval val = { .sval = { NULL, 0 } };
+        darray_append(arr, val);
+        return arr;
+    }
 
-        if (strcmp(tok, "=") == 0)
-            continue;
+    while (true) {
+        struct matched_sval val = { .sval = { s, 0 } };
+        while (*s != '\0' && *s != ',') { s++; val.sval.len++; }
+        val.sval = strip_spaces(val.sval);
+        darray_append(arr, val);
+        if (*s == '\0') break;
+        if (*s == ',') s++;
+    }
 
-        for (i = 0; i < MAX_WORDS; i++) {
-            len = strlen(cname[i]);
-
-            if (strncmp(cname[i], tok, len) == 0) {
-                if (strlen(tok) > len) {
-                    char *end = get_index(tok + len, &ndx);
-
-                    if ((i != LAYOUT && i != VARIANT) ||
-                        *end != '\0' || ndx == -1) {
-                        WARN("Illegal %s index: %d\n", cname[i], ndx);
-                        WARN("Can only index layout and variant\n");
-                        break;
-                    }
-
-                    if (ndx < 1 || ndx > XkbNumKbdGroups) {
-                        WARN("Illegal %s index: %d\n", cname[i], ndx);
-                        WARN("Index must be in range 1..%d\n", XkbNumKbdGroups);
-                        break;
-                    }
-                } else {
-                    ndx = 0;
-                }
+    return arr;
+}
 
-                found = true;
+static struct matcher *
+matcher_new(struct xkb_context *ctx,
+            const struct xkb_rule_names *rmlvo)
+{
+    struct matcher *m = calloc(1, sizeof(*m));
+    if (!m)
+        return NULL;
 
-                if (present & (1 << i)) {
-                    if ((i == LAYOUT && layout_ndx_present & (1 << ndx)) ||
-                        (i == VARIANT && variant_ndx_present & (1 << ndx))) {
-                        WARN("Component \"%s\" listed twice\n", tok);
-                        ACTION("Second definition ignored\n");
-                        break;
-                    }
-                }
+    m->ctx = ctx;
+    m->rmlvo.model.sval.start = rmlvo->model;
+    m->rmlvo.model.sval.len = strlen_safe(rmlvo->model);
+    m->rmlvo.layouts = split_comma_separated_mlvo(rmlvo->layout);
+    m->rmlvo.variants = split_comma_separated_mlvo(rmlvo->variant);
+    m->rmlvo.options = split_comma_separated_mlvo(rmlvo->options);
 
-                present |= (1 << i);
-                if (i == LAYOUT)
-                    layout_ndx_present |= 1 << ndx;
-                if (i == VARIANT)
-                    variant_ndx_present |= 1 << ndx;
+    return m;
+}
 
-                mapping->map[mapping->num_maps].word = i;
-                mapping->map[mapping->num_maps].index = ndx;
-                mapping->num_maps++;
-                break;
-            }
-        }
+static void
+matcher_free(struct matcher *m)
+{
+    struct group *group;
+    if (!m)
+        return;
+    darray_free(m->rmlvo.layouts);
+    darray_free(m->rmlvo.variants);
+    darray_free(m->rmlvo.options);
+    darray_foreach(group, m->groups)
+        darray_free(group->elements);
+    for (int i = 0; i < _KCCGST_NUM_ENTRIES; i++)
+        darray_free(m->kccgst[i]);
+    darray_free(m->groups);
+    free(m);
+}
 
-        if (!found) {
-            WARN("Unknown component \"%s\"\n", tok);
-            ACTION("ignored\n");
-        }
-    }
+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);
+}
 
-    if ((present & PART_MASK) == 0) {
-        WARN("Mapping needs at least one MLVO part\n");
-        ACTION("Illegal mapping ignored\n");
-        mapping->num_maps = 0;
+static bool
+read_rules_file(struct xkb_context *ctx,
+                struct matcher *matcher,
+                unsigned include_depth,
+                FILE *file,
+                const char *path);
+
+static void
+matcher_include(struct matcher *m, struct scanner *parent_scanner,
+                unsigned include_depth,
+                struct sval inc)
+{
+    struct scanner s; /* parses the !include value */
+    FILE *file;
+
+    scanner_init(&s, m->ctx, inc.start, inc.len,
+                 parent_scanner->file_name, NULL);
+    s.token_line = parent_scanner->token_line;
+    s.token_column = parent_scanner->token_column;
+    s.buf_pos = 0;
+
+    if (include_depth >= MAX_INCLUDE_DEPTH) {
+        scanner_err(&s, "maximum include depth (%d) exceeded; maybe there is an include loop?",
+                    MAX_INCLUDE_DEPTH);
         return;
     }
 
-    if ((present & COMPONENT_MASK) == 0) {
-        WARN("Mapping needs at least one component\n");
-        ACTION("Illegal mapping ignored\n");
-        mapping->num_maps = 0;
+    while (!scanner_eof(&s) && !scanner_eol(&s)) {
+        if (scanner_chr(&s, '%')) {
+            if (scanner_chr(&s, '%')) {
+                scanner_buf_append(&s, '%');
+            }
+            else if (scanner_chr(&s, 'H')) {
+                const char *home = xkb_context_getenv(m->ctx, "HOME");
+                if (!home) {
+                    scanner_err(&s, "%%H was used in an include statement, but the HOME environment variable is not set");
+                    return;
+                }
+                if (!scanner_buf_appends(&s, home)) {
+                    scanner_err(&s, "include path after expanding %%H is too long");
+                    return;
+                }
+            }
+            else if (scanner_chr(&s, 'S')) {
+                const char *default_root = xkb_context_include_path_get_system_path(m->ctx);
+                if (!scanner_buf_appends(&s, default_root) || !scanner_buf_appends(&s, "/rules")) {
+                    scanner_err(&s, "include path after expanding %%S is too long");
+                    return;
+                }
+            }
+            else if (scanner_chr(&s, 'E')) {
+                const char *default_root = xkb_context_include_path_get_extra_path(m->ctx);
+                if (!scanner_buf_appends(&s, default_root) || !scanner_buf_appends(&s, "/rules")) {
+                    scanner_err(&s, "include path after expanding %%E is too long");
+                    return;
+                }
+            }
+            else {
+                scanner_err(&s, "unknown %% format (%c) in include statement", scanner_peek(&s));
+                return;
+            }
+        }
+        else {
+            scanner_buf_append(&s, scanner_next(&s));
+        }
+    }
+    if (!scanner_buf_append(&s, '\0')) {
+        scanner_err(&s, "include path is too long");
         return;
     }
 
-    if (((present & COMPONENT_MASK) & (1 << KEYMAP)) &&
-        ((present & COMPONENT_MASK) != (1 << KEYMAP))) {
-        WARN("Keymap cannot appear with other components\n");
-        ACTION("Illegal mapping ignored\n");
-        mapping->num_maps = 0;
-        return;
+    file = fopen(s.buf, "rb");
+    if (file) {
+        bool ret = read_rules_file(m->ctx, m, include_depth + 1, file, s.buf);
+        if (!ret)
+            log_err(m->ctx, XKB_LOG_MESSAGE_NO_ID,
+                    "No components returned from included XKB rules \"%s\"\n",
+                    s.buf);
+        fclose(file);
+    } else {
+        log_err(m->ctx, XKB_LOG_MESSAGE_NO_ID,
+                "Failed to open included XKB rules \"%s\"\n",
+                s.buf);
     }
+}
 
-    mapping->number++;
+static void
+matcher_mapping_start_new(struct matcher *m)
+{
+    for (unsigned i = 0; i < _MLVO_NUM_ENTRIES; i++)
+        m->mapping.mlvo_at_pos[i] = -1;
+    for (unsigned i = 0; i < _KCCGST_NUM_ENTRIES; i++)
+        m->mapping.kccgst_at_pos[i] = -1;
+    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.skip = false;
 }
 
-/*
- * Match a line such as:
- * ! $pcmodels = pc101 pc102 pc104 pc105
- */
-static bool
-match_group_line(struct input_line *line, struct group *group)
+static int
+extract_layout_index(const char *s, size_t max_len, xkb_layout_index_t *out)
 {
-    int i;
-    char *name = strchr(line->line, '$');
-    char *words = strchr(name, ' ');
+    /* This function is pretty stupid, but works for now. */
+    *out = XKB_LAYOUT_INVALID;
+    if (max_len < 3)
+        return -1;
+    if (s[0] != '[' || !is_digit(s[1]) || s[2] != ']')
+        return -1;
+    if (s[1] - '0' < 1 || s[1] - '0' > XKB_MAX_GROUPS)
+        return -1;
+    /* To zero-based index. */
+    *out = s[1] - '0' - 1;
+    return 3;
+}
 
-    if (!words)
-        return false;
+static void
+matcher_mapping_set_mlvo(struct matcher *m, struct scanner *s,
+                         struct sval ident)
+{
+    enum rules_mlvo mlvo;
+    struct sval mlvo_sval;
 
-    *words++ = '\0';
+    for (mlvo = 0; mlvo < _MLVO_NUM_ENTRIES; mlvo++) {
+        mlvo_sval = rules_mlvo_svals[mlvo];
 
-    for (; *words; words++) {
-        if (*words != '=' && *words != ' ')
+        if (svaleq_prefix(mlvo_sval, ident))
             break;
     }
 
-    if (*words == '\0')
-        return false;
+    /* Not found. */
+    if (mlvo >= _MLVO_NUM_ENTRIES) {
+        scanner_err(s, "invalid mapping: %.*s is not a valid value here; ignoring rule set",
+                    ident.len, ident.start);
+        m->mapping.skip = true;
+        return;
+    }
 
-    group->name = strdup(name);
-    group->words = strdup(words);
+    if (m->mapping.defined_mlvo_mask & (1u << mlvo)) {
+        scanner_err(s, "invalid mapping: %.*s appears twice on the same line; ignoring rule set",
+                    mlvo_sval.len, mlvo_sval.start);
+        m->mapping.skip = true;
+        return;
+    }
 
-    words = group->words;
-    for (i = 1; *words; words++) {
-        if (*words == ' ') {
-            *words++ = '\0';
-            i++;
+    /* If there are leftovers still, it must be an index. */
+    if (mlvo_sval.len < ident.len) {
+        xkb_layout_index_t idx;
+        int consumed = extract_layout_index(ident.start + mlvo_sval.len,
+                                            ident.len - mlvo_sval.len, &idx);
+        if ((int) (ident.len - mlvo_sval.len) != consumed) {
+            scanner_err(s, "invalid mapping: \"%.*s\" may only be followed by a valid group index; ignoring rule set",
+                        mlvo_sval.len, mlvo_sval.start);
+            m->mapping.skip = true;
+            return;
         }
-    }
-    group->number = i;
 
-    return true;
+        if (mlvo == MLVO_LAYOUT) {
+            m->mapping.layout_idx = idx;
+        }
+        else if (mlvo == MLVO_VARIANT) {
+            m->mapping.variant_idx = idx;
+        }
+        else {
+            scanner_err(s, "invalid mapping: \"%.*s\" cannot be followed by a group index; ignoring rule set",
+                        mlvo_sval.len, mlvo_sval.start);
+            m->mapping.skip = true;
+            return;
+        }
+    }
 
+    m->mapping.mlvo_at_pos[m->mapping.num_mlvo] = mlvo;
+    m->mapping.defined_mlvo_mask |= 1u << mlvo;
+    m->mapping.num_mlvo++;
 }
 
-/* Match lines following a mapping (see match_mapping_line comment). */
-static bool
-match_rule_line(struct input_line *line, struct mapping *mapping,
-                struct rule *rule)
+static void
+matcher_mapping_set_kccgst(struct matcher *m, struct scanner *s, struct sval ident)
 {
-    char *str, *tok;
-    int nread, i;
-    char *strtok_buf;
-    bool append = false;
-    const char *names[MAX_WORDS] = { NULL };
-
-    if (mapping->num_maps == 0) {
-        WARN("Must have a mapping before first line of data\n");
-        ACTION("Illegal line of data ignored\n");
-        return false;
+    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;
     }
 
-    str = line->line;
+    /* Not found. */
+    if (kccgst >= _KCCGST_NUM_ENTRIES) {
+        scanner_err(s, "invalid mapping: %.*s is not a valid value here; ignoring rule set",
+                    ident.len, ident.start);
+        m->mapping.skip = true;
+        return;
+    }
 
-    for (nread = 0; (tok = strtok_r(str, " ", &strtok_buf)) != NULL; nread++) {
-        str = NULL;
+    if (m->mapping.defined_kccgst_mask & (1u << kccgst)) {
+        scanner_err(s, "invalid mapping: %.*s appears twice on the same line; ignoring rule set",
+                    kccgst_sval.len, kccgst_sval.start);
+        m->mapping.skip = true;
+        return;
+    }
 
-        if (strcmp(tok, "=") == 0) {
-            nread--;
-            continue;
-        }
+    m->mapping.kccgst_at_pos[m->mapping.num_kccgst] = kccgst;
+    m->mapping.defined_kccgst_mask |= 1u << kccgst;
+    m->mapping.num_kccgst++;
+}
 
-        if (nread > mapping->num_maps) {
-            WARN("Too many words on a line\n");
-            ACTION("Extra word \"%s\" ignored\n", tok);
-            continue;
-        }
+static void
+matcher_mapping_verify(struct matcher *m, struct scanner *s)
+{
+    if (m->mapping.num_mlvo == 0) {
+        scanner_err(s, "invalid mapping: must have at least one value on the left hand side; ignoring rule set");
+        goto skip;
+    }
 
-        names[mapping->map[nread].word] = tok;
-        if (*tok == '+' || *tok == '|')
-            append = true;
+    if (m->mapping.num_kccgst == 0) {
+        scanner_err(s, "invalid mapping: must have at least one value on the right hand side; ignoring rule set");
+        goto skip;
     }
 
-    if (nread < mapping->num_maps) {
-        WARN("Too few words on a line: %s\n", line->line);
-        ACTION("line ignored\n");
-        return false;
+    /*
+     * This following is very stupid, but this is how it works.
+     * See the "Notes" section in the overview above.
+     */
+
+    if (m->mapping.defined_mlvo_mask & (1u << MLVO_LAYOUT)) {
+        if (m->mapping.layout_idx == XKB_LAYOUT_INVALID) {
+            if (darray_size(m->rmlvo.layouts) > 1)
+                goto skip;
+        }
+        else {
+            if (darray_size(m->rmlvo.layouts) == 1 ||
+                m->mapping.layout_idx >= darray_size(m->rmlvo.layouts))
+                goto skip;
+        }
     }
 
-    rule->flags = 0;
-    rule->number = mapping->number;
-
-    if (names[OPTION])
-        rule->flags |= RULE_FLAG_OPTION;
-    else if (append)
-        rule->flags |= RULE_FLAG_APPEND;
-    else
-        rule->flags |= RULE_FLAG_NORMAL;
-
-    rule->model = uDupString(names[MODEL]);
-    rule->layout = uDupString(names[LAYOUT]);
-    rule->variant = uDupString(names[VARIANT]);
-    rule->option = uDupString(names[OPTION]);
-
-    rule->keycodes = uDupString(names[KEYCODES]);
-    rule->symbols = uDupString(names[SYMBOLS]);
-    rule->types = uDupString(names[TYPES]);
-    rule->compat = uDupString(names[COMPAT]);
-    rule->keymap = uDupString(names[KEYMAP]);
-
-    rule->layout_num = rule->variant_num = 0;
-    for (i = 0; i < nread; i++) {
-        if (mapping->map[i].index) {
-            if (mapping->map[i].word == LAYOUT)
-                rule->layout_num = mapping->map[i].index;
-            if (mapping->map[i].word == VARIANT)
-                rule->variant_num = mapping->map[i].index;
+    if (m->mapping.defined_mlvo_mask & (1u << MLVO_VARIANT)) {
+        if (m->mapping.variant_idx == XKB_LAYOUT_INVALID) {
+            if (darray_size(m->rmlvo.variants) > 1)
+                goto skip;
+        }
+        else {
+            if (darray_size(m->rmlvo.variants) == 1 ||
+                m->mapping.variant_idx >= darray_size(m->rmlvo.variants))
+                goto skip;
         }
     }
 
-    return true;
+    return;
+
+skip:
+    m->mapping.skip = true;
 }
 
-static bool
-match_line(struct input_line *line, struct mapping *mapping,
-           struct rule *rule, struct group *group)
+static void
+matcher_rule_start_new(struct matcher *m)
 {
-    if (line->line[0] != '!')
-        return match_rule_line(line, mapping, rule);
-
-    if (line->line[1] == '$' || (line->line[1] == ' ' && line->line[2] == '$'))
-        return match_group_line(line, group);
-
-    match_mapping_line(line, mapping);
-    return false;
+    memset(&m->rule, 0, sizeof(m->rule));
+    m->rule.skip = m->mapping.skip;
 }
 
 static void
-squeeze_spaces(char *p1)
+matcher_rule_set_mlvo_common(struct matcher *m, struct scanner *s,
+                             struct sval ident,
+                             enum mlvo_match_type match_type)
 {
-   char *p2;
-   for (p2 = p1; *p2; p2++) {
-       *p1 = *p2;
-       if (*p1 != ' ') p1++;
-   }
-   *p1 = '\0';
+    if (m->rule.num_mlvo_values + 1 > m->mapping.num_mlvo) {
+        scanner_err(s, "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 bool
-MakeMultiDefs(struct multi_defs *mdefs, const struct var_defs *defs)
+static void
+matcher_rule_set_mlvo_wildcard(struct matcher *m, struct scanner *s)
 {
-   memset(mdefs, 0, sizeof(*mdefs));
-   mdefs->model = defs->model;
-   mdefs->options = uDupString(defs->options);
-   if (mdefs->options) squeeze_spaces(mdefs->options);
-
-   if (defs->layout) {
-       if (!strchr(defs->layout, ',')) {
-           mdefs->layout[0] = defs->layout;
-       } else {
-           char *p;
-           int i;
-           p = uDupString(defs->layout);
-           if (p == NULL)
-              return false;
-           squeeze_spaces(p);
-           mdefs->layout[1] = p;
-           for (i = 2; i <= XkbNumKbdGroups; i++) {
-              if ((p = strchr(p, ','))) {
-                 *p++ = '\0';
-                 mdefs->layout[i] = p;
-              } else {
-                 break;
-              }
-           }
-           if (p && (p = strchr(p, ',')))
-              *p = '\0';
-       }
-   }
-
-   if (defs->variant) {
-       if (!strchr(defs->variant, ',')) {
-           mdefs->variant[0] = defs->variant;
-       } else {
-           char *p;
-           int i;
-           p = uDupString(defs->variant);
-           if (p == NULL)
-              return false;
-           squeeze_spaces(p);
-           mdefs->variant[1] = p;
-           for (i = 2; i <= XkbNumKbdGroups; i++) {
-              if ((p = strchr(p, ','))) {
-                 *p++ = '\0';
-                 mdefs->variant[i] = p;
-              } else {
-                 break;
-              }
-           }
-           if (p && (p = strchr(p, ',')))
-              *p = '\0';
-       }
-   }
-   return true;
+    struct sval dummy = { NULL, 0 };
+    matcher_rule_set_mlvo_common(m, s, dummy, MLVO_MATCH_WILDCARD);
 }
 
 static void
-FreeMultiDefs(struct multi_defs *defs)
+matcher_rule_set_mlvo_group(struct matcher *m, struct scanner *s,
+                            struct sval ident)
 {
-    free(defs->options);
-    free(UNCONSTIFY(defs->layout[1]));
-    free(UNCONSTIFY(defs->variant[1]));
+    matcher_rule_set_mlvo_common(m, s, ident, MLVO_MATCH_GROUP);
 }
 
 static void
-Apply(char *src, char **dst)
+matcher_rule_set_mlvo(struct matcher *m, struct scanner *s,
+                      struct sval ident)
 {
-    int ret;
-    char *tmp;
+    matcher_rule_set_mlvo_common(m, s, ident, MLVO_MATCH_NORMAL);
+}
 
-    if (!src)
+static void
+matcher_rule_set_kccgst(struct matcher *m, struct scanner *s,
+                        struct sval ident)
+{
+    if (m->rule.num_kccgst_values + 1 > m->mapping.num_kccgst) {
+        scanner_err(s, "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++;
+}
 
-    if (*src == '+' || *src == '!') {
-        tmp = *dst;
-        ret = asprintf(dst, "%s%s", *dst, src);
-        if (ret < 0)
-            *dst = NULL;
-        free(tmp);
+static bool
+match_group(struct matcher *m, struct sval group_name, struct sval to)
+{
+    struct group *group;
+    struct sval *element;
+    bool found = false;
+
+    darray_foreach(group, m->groups) {
+        if (svaleq(group->name, group_name)) {
+            found = true;
+            break;
+        }
     }
-    else if (*dst == NULL) {
-        *dst = uDupString(src);
+
+    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 void
-XkbRF_ApplyRule(struct rule *rule, struct xkb_component_names *names)
+static bool
+match_value(struct matcher *m, struct sval val, struct sval to,
+            enum mlvo_match_type match_type)
 {
-    /* clear the flag because it's applied */
-    rule->flags &= ~RULE_FLAG_PENDING_MATCH;
-
-    Apply(rule->keycodes, &names->keycodes);
-    Apply(rule->symbols,  &names->symbols);
-    Apply(rule->types,    &names->types);
-    Apply(rule->compat,   &names->compat);
-    Apply(rule->keymap,   &names->keymap);
+    if (match_type == MLVO_MATCH_WILDCARD)
+        return true;
+    if (match_type == MLVO_MATCH_GROUP)
+        return match_group(m, val, to);
+    return svaleq(val, to);
 }
 
 static bool
-CheckGroup(struct rules *rules, const char *group_name, const char *name)
+match_value_and_mark(struct matcher *m, struct sval val,
+                     struct matched_sval *to, enum mlvo_match_type match_type)
 {
-   int i;
-   const char *p;
-   struct group *group;
-
-   for (i = 0, group = rules->groups; i < rules->num_groups; i++, group++) {
-       if (! strcmp(group->name, group_name)) {
-           break;
-       }
-   }
-   if (i == rules->num_groups)
-       return false;
-   for (i = 0, p = group->words; i < group->number; i++, p += strlen(p)+1) {
-       if (! strcmp(p, name)) {
-           return true;
-       }
-   }
-   return false;
+    bool matched = match_value(m, val, to->sval, match_type);
+    if (matched)
+        to->matched = true;
+    return matched;
 }
 
+/*
+ * This function performs %-expansion on @value (see overview above),
+ * and appends the result to @to.
+ */
 static bool
-MatchOneOf(char *wanted, char *vals_defined)
+append_expanded_kccgst_value(struct matcher *m, struct scanner *s,
+                             darray_char *to, struct sval value)
 {
-    char *str, *next;
-    int want_len = strlen(wanted);
-
-    for (str = vals_defined, next = NULL; str != NULL; str = next) {
-        int len;
-        next = strchr(str, ',');
-        if (next) {
-            len = next-str;
-            next++;
+    const char *str = value.start;
+    darray_char expanded = darray_new();
+    char ch;
+    bool expanded_plus, to_plus;
+
+    /*
+     * 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.
+     */
+    for (unsigned i = 0; i < value.len; ) {
+        enum rules_mlvo mlv;
+        xkb_layout_index_t idx;
+        char pfx, sfx;
+        struct matched_sval *expanded_value;
+
+        /* Check if that's a start of an expansion. */
+        if (str[i] != '%') {
+            /* Just a normal character. */
+            darray_appends_nullterminate(expanded, &str[i++], 1);
+            continue;
         }
-        else {
-            len = strlen(str);
+        if (++i >= value.len) goto error;
+
+        pfx = sfx = 0;
+
+        /* Check for prefix. */
+        if (str[i] == '(' || str[i] == '+' || str[i] == '|' ||
+            str[i] == '_' || str[i] == '-') {
+            pfx = str[i];
+            if (str[i] == '(') sfx = ')';
+            if (++i >= value.len) goto error;
         }
 
-        if (len == want_len && strncmp(wanted, str, len) == 0)
-            return true;
-    }
+        /* 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;
+        }
 
-    return false;
-}
+        /* Check for index. */
+        idx = XKB_LAYOUT_INVALID;
+        if (i < value.len && str[i] == '[') {
+            int consumed;
 
-static int
-XkbRF_CheckApplyRule(struct rule *rule, struct multi_defs *mdefs,
-                     struct xkb_component_names *names, struct rules *rules)
-{
-    bool pending = false;
-
-    if (rule->model != NULL) {
-        if(mdefs->model == NULL)
-            return 0;
-        if (strcmp(rule->model, "*") == 0) {
-            pending = true;
-        } else {
-            if (rule->model[0] == '$') {
-               if (!CheckGroup(rules, rule->model, mdefs->model))
-                  return 0;
-            } else {
-              if (strcmp(rule->model, mdefs->model) != 0)
-                 return 0;
-           }
-       }
-    }
-    if (rule->option != NULL) {
-       if (mdefs->options == NULL)
-           return 0;
-       if ((!MatchOneOf(rule->option,mdefs->options)))
-           return 0;
-    }
+            if (mlv != MLVO_LAYOUT && mlv != MLVO_VARIANT) {
+                scanner_err(s, "invalid index in %%-expansion; may only index layout or variant");
+                goto error;
+            }
 
-    if (rule->layout != NULL) {
-       if(mdefs->layout[rule->layout_num] == NULL ||
-          *mdefs->layout[rule->layout_num] == '\0')
-           return 0;
-        if (strcmp(rule->layout, "*") == 0) {
-            pending = true;
-        } else {
-            if (rule->layout[0] == '$') {
-               if (!CheckGroup(rules, rule->layout,
-                               mdefs->layout[rule->layout_num]))
-                  return 0;
-           } else {
-              if (strcmp(rule->layout, mdefs->layout[rule->layout_num]) != 0)
-                  return 0;
-           }
-       }
-    }
-    if (rule->variant != NULL) {
-       if (mdefs->variant[rule->variant_num] == NULL ||
-           *mdefs->variant[rule->variant_num] == '\0')
-           return 0;
-        if (strcmp(rule->variant, "*") == 0) {
-            pending = true;
-        } else {
-            if (rule->variant[0] == '$') {
-               if (!CheckGroup(rules, rule->variant,
-                               mdefs->variant[rule->variant_num]))
-                  return 0;
-            } else {
-              if (strcmp(rule->variant,
-                          mdefs->variant[rule->variant_num]) != 0)
-                  return 0;
-           }
-       }
-    }
-    if (pending) {
-        rule->flags |= RULE_FLAG_PENDING_MATCH;
-       return rule->number;
-    }
-    /* exact match, apply it now */
-    XkbRF_ApplyRule(rule,names);
-    return rule->number;
-}
+            consumed = extract_layout_index(str + i, value.len - i, &idx);
+            if (consumed == -1) goto error;
+            i += consumed;
+        }
 
-static void
-XkbRF_ClearPartialMatches(struct rules *rules)
-{
-    int i;
-    struct rule *rule;
+        /* Check for suffix, if there supposed to be one. */
+        if (sfx != 0) {
+            if (i >= value.len) goto error;
+            if (str[i++] != sfx) goto error;
+        }
 
-    for (i=0,rule=rules->rules;i<rules->num_rules;i++,rule++) {
-       rule->flags &= ~RULE_FLAG_PENDING_MATCH;
-    }
-}
+        /* Get the expanded value. */
+        expanded_value = NULL;
+
+        if (mlv == MLVO_LAYOUT) {
+            if (idx != XKB_LAYOUT_INVALID &&
+                idx < darray_size(m->rmlvo.layouts) &&
+                darray_size(m->rmlvo.layouts) > 1)
+                expanded_value = &darray_item(m->rmlvo.layouts, idx);
+            else if (idx == XKB_LAYOUT_INVALID &&
+                     darray_size(m->rmlvo.layouts) == 1)
+                expanded_value = &darray_item(m->rmlvo.layouts, 0);
+        }
+        else if (mlv == MLVO_VARIANT) {
+            if (idx != XKB_LAYOUT_INVALID &&
+                idx < darray_size(m->rmlvo.variants) &&
+                darray_size(m->rmlvo.variants) > 1)
+                expanded_value = &darray_item(m->rmlvo.variants, idx);
+            else if (idx == XKB_LAYOUT_INVALID &&
+                     darray_size(m->rmlvo.variants) == 1)
+                expanded_value = &darray_item(m->rmlvo.variants, 0);
+        }
+        else if (mlv == MLVO_MODEL) {
+            expanded_value = &m->rmlvo.model;
+        }
 
-static void
-XkbRF_ApplyPartialMatches(struct rules *rules,
-                          struct xkb_component_names *names)
-{
-    int i;
-    struct rule *rule;
+        /* If we didn't get one, skip silently. */
+        if (!expanded_value || expanded_value->sval.len == 0)
+            continue;
 
-    for (rule = rules->rules, i = 0; i < rules->num_rules; i++, rule++) {
-       if ((rule->flags & RULE_FLAG_PENDING_MATCH)==0)
-           continue;
-       XkbRF_ApplyRule(rule,names);
+        if (pfx != 0)
+            darray_appends_nullterminate(expanded, &pfx, 1);
+        darray_appends_nullterminate(expanded,
+                                     expanded_value->sval.start,
+                                     expanded_value->sval.len);
+        if (sfx != 0)
+            darray_appends_nullterminate(expanded, &sfx, 1);
+        expanded_value->matched = true;
     }
+
+    /*
+     * 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
+     */
+
+    ch = (darray_empty(expanded) ? '\0' : darray_item(expanded, 0));
+    expanded_plus = (ch == '+' || ch == '|');
+    ch = (darray_empty(*to) ? '\0' : darray_item(*to, 0));
+    to_plus = (ch == '+' || ch == '|');
+
+    if (expanded_plus || darray_empty(*to))
+        darray_appends_nullterminate(*to, expanded.item, expanded.size);
+    else if (to_plus)
+        darray_prepends_nullterminate(*to, expanded.item, expanded.size);
+
+    darray_free(expanded);
+    return true;
+
+error:
+    darray_free(expanded);
+    scanner_err(s, "invalid %%-expansion in value; not used");
+    return false;
 }
 
 static void
-XkbRF_CheckApplyRules(struct rules *rules, struct multi_defs *mdefs,
-                      struct xkb_component_names *names, unsigned int flags)
+matcher_rule_verify(struct matcher *m, struct scanner *s)
 {
-    int i;
-    struct rule *rule;
-    int skip;
-
-    for (rule = rules->rules, i=0; i < rules->num_rules; rule++, i++) {
-       if ((rule->flags & flags) != flags)
-           continue;
-       skip = XkbRF_CheckApplyRule(rule, mdefs, names, rules);
-       if (skip && !(flags & RULE_FLAG_OPTION)) {
-           for ( ;(i < rules->num_rules) && (rule->number == skip);
-                 rule++, i++);
-           rule--; i--;
-       }
+    if (m->rule.num_mlvo_values != m->mapping.num_mlvo ||
+        m->rule.num_kccgst_values != m->mapping.num_kccgst) {
+        scanner_err(s, "invalid rule: must have same number of values as mapping line; ignoring rule");
+        m->rule.skip = true;
     }
 }
 
-/***====================================================================***/
-
-static char *
-XkbRF_SubstituteVars(char *name, struct multi_defs *mdefs)
+static void
+matcher_rule_apply_if_matches(struct matcher *m, struct scanner *s)
 {
-    char *str, *outstr, *orig, *var;
-    size_t len;
-    int ndx;
-
-    if (!name)
-        return NULL;
-
-    orig= name;
-    str= strchr(name,'%');
-    if (str==NULL)
-       return name;
-    len= strlen(name);
-    while (str!=NULL) {
-       char pfx= str[1];
-       int   extra_len= 0;
-       if ((pfx=='+')||(pfx=='|')||(pfx=='_')||(pfx=='-')) {
-           extra_len= 1;
-           str++;
-       }
-       else if (pfx=='(') {
-           extra_len= 2;
-           str++;
-       }
-       var = str + 1;
-       str = get_index(var + 1, &ndx);
-       if (ndx == -1) {
-           str = strchr(str,'%');
-           continue;
+    for (unsigned 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;
+
+        if (mlvo == MLVO_MODEL) {
+            to = &m->rmlvo.model;
+            matched = match_value_and_mark(m, value, to, match_type);
         }
-       if ((*var=='l') && mdefs->layout[ndx] && *mdefs->layout[ndx])
-           len+= strlen(mdefs->layout[ndx])+extra_len;
-       else if ((*var=='m')&&mdefs->model)
-           len+= strlen(mdefs->model)+extra_len;
-       else if ((*var=='v') && mdefs->variant[ndx] && *mdefs->variant[ndx])
-           len+= strlen(mdefs->variant[ndx])+extra_len;
-       if ((pfx=='(')&&(*str==')')) {
-           str++;
-       }
-       str= strchr(&str[0],'%');
-    }
-    name = malloc(len + 1);
-    str= orig;
-    outstr= name;
-    while (*str!='\0') {
-       if (str[0]=='%') {
-           char pfx,sfx;
-           str++;
-           pfx= str[0];
-           sfx= '\0';
-           if ((pfx=='+')||(pfx=='|')||(pfx=='_')||(pfx=='-')) {
-               str++;
-           }
-           else if (pfx=='(') {
-               sfx= ')';
-               str++;
-           }
-           else pfx= '\0';
-
-           var = str;
-           str = get_index(var + 1, &ndx);
-           if (ndx == -1) {
-               continue;
+        else if (mlvo == MLVO_LAYOUT) {
+            xkb_layout_index_t idx = m->mapping.layout_idx;
+            idx = (idx == XKB_LAYOUT_INVALID ? 0 : idx);
+            to = &darray_item(m->rmlvo.layouts, idx);
+            matched = match_value_and_mark(m, value, to, match_type);
+        }
+        else if (mlvo == MLVO_VARIANT) {
+            xkb_layout_index_t idx = m->mapping.layout_idx;
+            idx = (idx == XKB_LAYOUT_INVALID ? 0 : idx);
+            to = &darray_item(m->rmlvo.variants, idx);
+            matched = match_value_and_mark(m, value, to, match_type);
+        }
+        else if (mlvo == MLVO_OPTION) {
+            darray_foreach(to, m->rmlvo.options) {
+                matched = match_value_and_mark(m, value, to, match_type);
+                if (matched)
+                    break;
             }
-           if ((*var=='l') && mdefs->layout[ndx] && *mdefs->layout[ndx]) {
-               if (pfx) *outstr++= pfx;
-               strcpy(outstr,mdefs->layout[ndx]);
-               outstr+= strlen(mdefs->layout[ndx]);
-               if (sfx) *outstr++= sfx;
-           }
-           else if ((*var=='m')&&(mdefs->model)) {
-               if (pfx) *outstr++= pfx;
-               strcpy(outstr,mdefs->model);
-               outstr+= strlen(mdefs->model);
-               if (sfx) *outstr++= sfx;
-           }
-           else if ((*var=='v') && mdefs->variant[ndx] && *mdefs->variant[ndx]) {
-               if (pfx) *outstr++= pfx;
-               strcpy(outstr,mdefs->variant[ndx]);
-               outstr+= strlen(mdefs->variant[ndx]);
-               if (sfx) *outstr++= sfx;
-           }
-           if ((pfx=='(')&&(*str==')'))
-               str++;
-       }
-       else {
-           *outstr++= *str++;
-       }
+        }
+
+        if (!matched)
+            return;
     }
-    *outstr++= '\0';
-    if (orig!=name)
-       free(orig);
-    return name;
-}
 
-/***====================================================================***/
+    for (unsigned 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, &m->kccgst[kccgst], value);
+    }
 
-static bool
-XkbcRF_GetComponents(struct rules *rules, const struct var_defs *defs,
-                     struct xkb_component_names *names)
-{
-    struct multi_defs mdefs;
-
-    MakeMultiDefs(&mdefs, defs);
-
-    memset(names, 0, sizeof(struct xkb_component_names));
-    XkbRF_ClearPartialMatches(rules);
-    XkbRF_CheckApplyRules(rules, &mdefs, names, RULE_FLAG_NORMAL);
-    XkbRF_ApplyPartialMatches(rules, names);
-    XkbRF_CheckApplyRules(rules, &mdefs, names, RULE_FLAG_APPEND);
-    XkbRF_ApplyPartialMatches(rules, names);
-    XkbRF_CheckApplyRules(rules, &mdefs, names, RULE_FLAG_OPTION);
-    XkbRF_ApplyPartialMatches(rules, names);
-
-    names->keycodes = XkbRF_SubstituteVars(names->keycodes, &mdefs);
-    names->symbols = XkbRF_SubstituteVars(names->symbols, &mdefs);
-    names->types = XkbRF_SubstituteVars(names->types, &mdefs);
-    names->compat = XkbRF_SubstituteVars(names->compat, &mdefs);
-    names->keymap = XkbRF_SubstituteVars(names->keymap, &mdefs);
-
-    FreeMultiDefs(&mdefs);
-    return (names->keycodes && names->symbols && names->types &&
-               names->compat) || names->keymap;
+    /*
+     * 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 (!(m->mapping.defined_mlvo_mask & (1 << MLVO_OPTION)))
+        m->mapping.skip = true;
 }
 
-static struct rule *
-XkbcRF_AddRule(struct rules *rules)
+static enum rules_token
+gettok(struct matcher *m, struct scanner *s)
 {
-    if (rules->sz_rules<1) {
-       rules->sz_rules= 16;
-       rules->num_rules= 0;
-       rules->rules = calloc(rules->sz_rules, sizeof(*rules->rules));
-    }
-    else if (rules->num_rules>=rules->sz_rules) {
-       rules->sz_rules*= 2;
-        rules->rules = realloc(rules->rules,
-                               rules->sz_rules * sizeof(*rules->rules));
-    }
-    if (!rules->rules) {
-       rules->sz_rules= rules->num_rules= 0;
-       return NULL;
-    }
-    memset(&rules->rules[rules->num_rules], 0, sizeof(*rules->rules));
-    return &rules->rules[rules->num_rules++];
+    return lex(s, &m->val);
 }
 
-static struct group *
-XkbcRF_AddGroup(struct rules *rules)
+static bool
+matcher_match(struct matcher *m, struct scanner *s,
+              unsigned include_depth,
+              const char *string, size_t len,
+              const char *file_name)
 {
-    if (rules->sz_groups<1) {
-       rules->sz_groups= 16;
-       rules->num_groups= 0;
-        rules->groups = calloc(rules->sz_groups, sizeof(*rules->groups));
-    }
-    else if (rules->num_groups >= rules->sz_groups) {
-       rules->sz_groups *= 2;
-        rules->groups = realloc(rules->groups,
-                                rules->sz_groups * sizeof(*rules->groups));
+    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;
     }
-    if (!rules->groups) {
-       rules->sz_groups= rules->num_groups= 0;
-       return NULL;
+
+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;
     }
 
-    memset(&rules->groups[rules->num_groups], 0, sizeof(*rules->groups));
-    return &rules->groups[rules->num_groups++];
-}
+group_name:
+    switch (tok = gettok(m, s)) {
+    case TOK_EQUALS:
+        goto group_element;
+    default:
+        goto unexpected;
+    }
 
-static struct rules *
-XkbcRF_LoadRules(FILE *file)
-{
-    struct input_line line;
-    struct mapping mapping;
-    struct rule trule, *rule;
-    struct group tgroup, *group;
-    struct rules *rules;
+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;
+    }
 
-    rules = calloc(1, sizeof(*rules));
-    if (!rules)
-        return NULL;
+include_statement:
+    switch (tok = gettok(m, s)) {
+    case TOK_IDENTIFIER:
+        matcher_include(m, s, include_depth, m->val.string);
+        goto initial;
+    default:
+        goto unexpected;
+    }
 
-    memset(&mapping, 0, sizeof(mapping));
-    memset(&tgroup, 0, sizeof(tgroup));
-    input_line_init(&line);
-
-    while (input_line_get(file, &line)) {
-        if (match_line(&line, &mapping, &trule, &tgroup)) {
-            if (tgroup.number) {
-               if ((group= XkbcRF_AddGroup(rules))!=NULL) {
-                   *group= tgroup;
-                   memset(&tgroup, 0, sizeof(tgroup));
-               }
-           } else {
-               if ((rule= XkbcRF_AddRule(rules))!=NULL) {
-                   *rule= trule;
-                   memset(&trule, 0, sizeof(trule));
-               }
-           }
-       }
-       line.offset = 0;
+mapping_mlvo:
+    switch (tok = gettok(m, s)) {
+    case TOK_IDENTIFIER:
+        if (!m->mapping.skip)
+            matcher_mapping_set_mlvo(m, s, m->val.string);
+        goto mapping_mlvo;
+    case TOK_EQUALS:
+        goto mapping_kccgst;
+    default:
+        goto unexpected;
     }
-    input_line_deinit(&line);
-    return rules;
-}
 
-static void
-XkbRF_ClearVarDescriptions(struct describe_vars *var)
-{
-    int i;
+mapping_kccgst:
+    switch (tok = gettok(m, s)) {
+    case TOK_IDENTIFIER:
+        if (!m->mapping.skip)
+            matcher_mapping_set_kccgst(m, s, m->val.string);
+        goto mapping_kccgst;
+    case TOK_END_OF_LINE:
+        if (!m->mapping.skip)
+            matcher_mapping_verify(m, s);
+        goto rule_mlvo_first;
+    default:
+        goto unexpected;
+    }
 
-    for (i=0;i<var->num_desc;i++) {
-       free(var->desc[i].name);
-       free(var->desc[i].desc);
-       var->desc[i].name= var->desc[i].desc= NULL;
+rule_mlvo_first:
+    switch (tok = gettok(m, s)) {
+    case TOK_BANG:
+        goto bang;
+    case TOK_END_OF_LINE:
+        goto rule_mlvo_first;
+    case TOK_END_OF_FILE:
+        goto finish;
+    default:
+        matcher_rule_start_new(m);
+        goto rule_mlvo_no_tok;
     }
-    free(var->desc);
-    var->desc= NULL;
-}
 
-static void
-XkbcRF_Free(struct rules *rules)
-{
-    int i;
-    struct rule *rule;
-    struct group *group;
+rule_mlvo:
+    tok = gettok(m, s);
+rule_mlvo_no_tok:
+    switch (tok) {
+    case TOK_IDENTIFIER:
+        if (!m->rule.skip)
+            matcher_rule_set_mlvo(m, s, m->val.string);
+        goto rule_mlvo;
+    case TOK_STAR:
+        if (!m->rule.skip)
+            matcher_rule_set_mlvo_wildcard(m, s);
+        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;
+    }
 
-    if (!rules)
-       return;
-    XkbRF_ClearVarDescriptions(&rules->models);
-    XkbRF_ClearVarDescriptions(&rules->layouts);
-    XkbRF_ClearVarDescriptions(&rules->variants);
-    XkbRF_ClearVarDescriptions(&rules->options);
-
-    for (i=0, rule = rules->rules; i < rules->num_rules && rules; i++, rule++) {
-        free(rule->model);
-        free(rule->layout);
-        free(rule->variant);
-        free(rule->option);
-        free(rule->keycodes);
-        free(rule->symbols);
-        free(rule->types);
-        free(rule->compat);
-        free(rule->keymap);
+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;
     }
-    free(rules->rules);
 
-    for (i=0, group = rules->groups; i < rules->num_groups && group; i++, group++) {
-        free(group->name);
-        free(group->words);
+unexpected:
+    switch (tok) {
+    case TOK_ERROR:
+        goto error;
+    default:
+        goto state_error;
     }
-    free(rules->groups);
 
-    free(rules);
+finish:
+    return true;
+
+state_error:
+    scanner_err(s, "unexpected token");
+error:
+    return false;
 }
 
-struct xkb_component_names *
-xkb_components_from_rules(struct xkb_context *ctx,
-                          const struct xkb_rule_names *rmlvo)
+static bool
+read_rules_file(struct xkb_context *ctx,
+                struct matcher *matcher,
+                unsigned include_depth,
+                FILE *file,
+                const char *path)
 {
-    int i;
-    FILE *rulesFile;
-    char *rulesPath;
-    struct rules *loaded;
-    struct xkb_component_names *names = NULL;
-    struct var_defs defs = {
-        .model = rmlvo->model,
-        .layout = rmlvo->layout,
-        .variant = rmlvo->variant,
-        .options = rmlvo->options,
-    };
-
-    rulesFile = XkbFindFileInPath(ctx, rmlvo->rules, XkmRulesFile,
-                                  &rulesPath);
-    if (!rulesFile) {
-        ERROR("could not find \"%s\" rules in XKB path\n", rmlvo->rules);
-        ERROR("%d include paths searched:\n",
-              xkb_context_num_include_paths(ctx));
-        for (i = 0; i < xkb_context_num_include_paths(ctx); i++)
-            ERROR("\t%s\n", xkb_context_include_path_get(ctx, i));
-        return NULL;
+    bool ret = false;
+    char *string;
+    size_t size;
+    struct scanner scanner;
+
+    ret = map_file(file, &string, &size);
+    if (!ret) {
+        log_err(ctx, XKB_LOG_MESSAGE_NO_ID,
+                "Couldn't read rules file \"%s\": %s\n",
+                path, strerror(errno));
+        goto out;
     }
 
-    loaded = XkbcRF_LoadRules(rulesFile);
-    if (!loaded) {
-        ERROR("failed to load XKB rules \"%s\"\n", rulesPath);
-        goto unwind_file;
-    }
+    scanner_init(&scanner, matcher->ctx, string, size, path, NULL);
 
-    names = calloc(1, sizeof(*names));
-    if (!names) {
-        ERROR("failed to allocate XKB components\n");
-        goto unwind_file;
-    }
+    ret = matcher_match(matcher, &scanner, include_depth, string, size, path);
 
-    if (!XkbcRF_GetComponents(loaded, &defs, names)) {
-        free(names->keymap);
-        free(names->keycodes);
-        free(names->types);
-        free(names->compat);
-        free(names->symbols);
-        free(names);
-        names = NULL;
-        ERROR("no components returned from XKB rules \"%s\"\n", rulesPath);
+    unmap_file(string, size);
+out:
+    return ret;
+}
+
+bool
+xkb_components_from_rules(struct xkb_context *ctx,
+                          const struct xkb_rule_names *rmlvo,
+                          struct xkb_component_names *out)
+{
+    bool ret = false;
+    FILE *file;
+    char *path = NULL;
+    struct matcher *matcher = NULL;
+    struct matched_sval *mval;
+    unsigned int offset = 0;
+
+    file = FindFileInXkbPath(ctx, rmlvo->rules, FILE_TYPE_RULES, &path, &offset);
+    if (!file)
+        goto err_out;
+
+    matcher = matcher_new(ctx, rmlvo);
+
+    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_LOG_MESSAGE_NO_ID,
+                "No components returned from XKB rules \"%s\"\n", path);
+        ret = false;
+        goto err_out;
     }
 
-unwind_file:
-    XkbcRF_Free(loaded);
-    if (rulesFile)
-        fclose(rulesFile);
-    free(rulesPath);
-    return names;
+    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->compat, NULL);
+    darray_steal(matcher->kccgst[KCCGST_SYMBOLS], &out->symbols, NULL);
+    darray_free(matcher->kccgst[KCCGST_GEOMETRY]);
+
+    mval = &matcher->rmlvo.model;
+    if (!mval->matched && mval->sval.len > 0)
+        log_err(matcher->ctx, XKB_LOG_MESSAGE_NO_ID,
+                "Unrecognized RMLVO model \"%.*s\" was ignored\n",
+                mval->sval.len, mval->sval.start);
+    darray_foreach(mval, matcher->rmlvo.layouts)
+        if (!mval->matched && mval->sval.len > 0)
+            log_err(matcher->ctx, XKB_LOG_MESSAGE_NO_ID,
+                    "Unrecognized RMLVO layout \"%.*s\" was ignored\n",
+                    mval->sval.len, mval->sval.start);
+    darray_foreach(mval, matcher->rmlvo.variants)
+        if (!mval->matched && mval->sval.len > 0)
+            log_err(matcher->ctx, XKB_LOG_MESSAGE_NO_ID,
+                    "Unrecognized RMLVO variant \"%.*s\" was ignored\n",
+                    mval->sval.len, mval->sval.start);
+    darray_foreach(mval, matcher->rmlvo.options)
+        if (!mval->matched && mval->sval.len > 0)
+            log_err(matcher->ctx, XKB_LOG_MESSAGE_NO_ID,
+                    "Unrecognized RMLVO option \"%.*s\" was ignored\n",
+                    mval->sval.len, mval->sval.start);
+
+err_out:
+    if (file)
+        fclose(file);
+    matcher_free(matcher);
+    free(path);
+    return ret;
 }