]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
varlink: add IDL/introspection logic
authorLennart Poettering <lennart@poettering.net>
Fri, 22 Sep 2023 20:29:41 +0000 (22:29 +0200)
committerLennart Poettering <lennart@poettering.net>
Fri, 6 Oct 2023 09:49:38 +0000 (11:49 +0200)
src/shared/meson.build
src/shared/varlink-idl.c [new file with mode: 0644]
src/shared/varlink-idl.h [new file with mode: 0644]

index 3df178bfa1f5f88a3f8c516d2c6c9713278677ac..da32e1435d6c8edf6fd92fc41f986d2b88fe39ff 100644 (file)
@@ -166,6 +166,7 @@ shared_sources = files(
         'userdb-dropin.c',
         'userdb.c',
         'varlink.c',
+        'varlink-idl.c',
         'verb-log-control.c',
         'verbs.c',
         'vlan-util.c',
diff --git a/src/shared/varlink-idl.c b/src/shared/varlink-idl.c
new file mode 100644 (file)
index 0000000..157474f
--- /dev/null
@@ -0,0 +1,1582 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "memstream-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "varlink-idl.h"
+#include "set.h"
+
+enum {
+        COLOR_SYMBOL_TYPE,   /* interface, method, type, error */
+        COLOR_FIELD_TYPE,    /* string, bool, … */
+        COLOR_IDENTIFIER,
+        COLOR_MARKS,         /* [], ->, ?, … */
+        COLOR_RESET,
+        _COLOR_MAX,
+};
+
+static int varlink_idl_format_all_fields(FILE *f, const VarlinkSymbol *symbol, VarlinkFieldDirection direction, const char *indent, const char *const colors[static _COLOR_MAX]);
+
+static int varlink_idl_format_enum_values(
+                FILE *f,
+                const VarlinkSymbol *symbol,
+                const char *indent,
+                const char *const colors[static _COLOR_MAX]) {
+
+        bool first = true;
+
+        assert(f);
+        assert(symbol);
+        assert(symbol->symbol_type == VARLINK_ENUM_TYPE);
+
+        for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) {
+
+                if (first) {
+                        first = false;
+                        fputs("(\n", f);
+                } else
+                        fputs(",\n", f);
+
+                fputs(strempty(indent), f);
+                fputs("\t", f);
+                fputs(colors[COLOR_IDENTIFIER], f);
+                fputs(field->name, f);
+                fputs(colors[COLOR_RESET], f);
+        }
+
+        if (first)
+                fputs("()", f);
+        else {
+                fputs("\n", f);
+                fputs(strempty(indent), f);
+                fputs(")", f);
+        }
+
+        return 0;
+}
+
+static int varlink_idl_format_field(
+                FILE *f,
+                const VarlinkField *field,
+                const char *indent,
+                const char *const colors[static _COLOR_MAX]) {
+
+        assert(f);
+        assert(field);
+
+        fputs(strempty(indent), f);
+        fputs(colors[COLOR_IDENTIFIER], f);
+        fputs(field->name, f);
+        fputs(colors[COLOR_RESET], f);
+        fputs(": ", f);
+
+        if (FLAGS_SET(field->field_flags, VARLINK_NULLABLE)) {
+                fputs(colors[COLOR_MARKS], f);
+                fputs("?", f);
+                fputs(colors[COLOR_RESET], f);
+        }
+
+        switch (field->field_flags & (VARLINK_MAP|VARLINK_ARRAY)) {
+
+        case VARLINK_MAP:
+                fputs(colors[COLOR_MARKS], f);
+                fputs("[", f);
+                fputs(colors[COLOR_FIELD_TYPE], f);
+                fputs("string", f);
+                fputs(colors[COLOR_MARKS], f);
+                fputs("]", f);
+                fputs(colors[COLOR_RESET], f);
+                break;
+
+        case VARLINK_ARRAY:
+                fputs(colors[COLOR_MARKS], f);
+                fputs("[]", f);
+                fputs(colors[COLOR_RESET], f);
+                break;
+
+        case 0:
+                break;
+
+        default:
+                assert_not_reached();
+        }
+
+        switch (field->field_type) {
+
+        case VARLINK_BOOL:
+                fputs(colors[COLOR_FIELD_TYPE], f);
+                fputs("bool", f);
+                fputs(colors[COLOR_RESET], f);
+                break;
+
+        case VARLINK_INT:
+                fputs(colors[COLOR_FIELD_TYPE], f);
+                fputs("int", f);
+                fputs(colors[COLOR_RESET], f);
+                break;
+
+        case VARLINK_FLOAT:
+                fputs(colors[COLOR_FIELD_TYPE], f);
+                fputs("float", f);
+                fputs(colors[COLOR_RESET], f);
+                break;
+
+        case VARLINK_STRING:
+                fputs(colors[COLOR_FIELD_TYPE], f);
+                fputs("string", f);
+                fputs(colors[COLOR_RESET], f);
+                break;
+
+        case VARLINK_OBJECT:
+                fputs(colors[COLOR_FIELD_TYPE], f);
+                fputs("object", f);
+                fputs(colors[COLOR_RESET], f);
+                break;
+
+        case VARLINK_NAMED_TYPE:
+                fputs(colors[COLOR_IDENTIFIER], f);
+                fputs(ASSERT_PTR(field->named_type), f);
+                fputs(colors[COLOR_RESET], f);
+                break;
+
+        case VARLINK_STRUCT:
+                return varlink_idl_format_all_fields(f, ASSERT_PTR(field->symbol), VARLINK_REGULAR, indent, colors);
+
+        case VARLINK_ENUM:
+                return varlink_idl_format_enum_values(f, ASSERT_PTR(field->symbol), indent, colors);
+
+        default:
+                assert_not_reached();
+        }
+
+        return 0;
+}
+
+static int varlink_idl_format_all_fields(
+                FILE *f,
+                const VarlinkSymbol *symbol,
+                VarlinkFieldDirection filter_direction,
+                const char *indent,
+                const char *const colors[static _COLOR_MAX]) {
+
+        _cleanup_free_ char *indent2 = NULL;
+        bool first = true;
+        int r;
+
+        assert(f);
+        assert(symbol);
+        assert(IN_SET(symbol->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_METHOD, VARLINK_ERROR));
+
+        indent2 = strjoin(strempty(indent), "\t");
+        if (!indent2)
+                return -ENOMEM;
+
+        for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) {
+
+                if (field->field_direction != filter_direction)
+                        continue;
+
+                if (first) {
+                        first = false;
+                        fputs("(\n", f);
+                } else
+                        fputs(",\n", f);
+
+                r = varlink_idl_format_field(f, field, indent2, colors);
+                if (r < 0)
+                        return r;
+        }
+
+        if (first)
+                fputs("()", f);
+        else {
+                fputs("\n", f);
+                fputs(strempty(indent), f);
+                fputs(")", f);
+        }
+
+        return 0;
+}
+
+static int varlink_idl_format_symbol(
+                FILE *f,
+                const VarlinkSymbol *symbol,
+                const char *const colors[static _COLOR_MAX]) {
+        int r;
+
+        assert(f);
+        assert(symbol);
+
+        switch (symbol->symbol_type) {
+
+        case VARLINK_ENUM_TYPE:
+                fputs(colors[COLOR_SYMBOL_TYPE], f);
+                fputs("type ", f);
+                fputs(colors[COLOR_IDENTIFIER], f);
+                fputs(symbol->name, f);
+                fputs(colors[COLOR_RESET], f);
+
+                r = varlink_idl_format_enum_values(f, symbol, /* indent= */ NULL, colors);
+                break;
+
+        case VARLINK_STRUCT_TYPE:
+                fputs(colors[COLOR_SYMBOL_TYPE], f);
+                fputs("type ", f);
+                fputs(colors[COLOR_IDENTIFIER], f);
+                fputs(symbol->name, f);
+                fputs(colors[COLOR_RESET], f);
+
+                r = varlink_idl_format_all_fields(f, symbol, VARLINK_REGULAR, /* indent= */ NULL, colors);
+                break;
+
+        case VARLINK_METHOD:
+                fputs(colors[COLOR_SYMBOL_TYPE], f);
+                fputs("method ", f);
+                fputs(colors[COLOR_IDENTIFIER], f);
+                fputs(symbol->name, f);
+                fputs(colors[COLOR_RESET], f);
+
+                r = varlink_idl_format_all_fields(f, symbol, VARLINK_INPUT, /* indent= */ NULL, colors);
+                if (r < 0)
+                        return r;
+
+                fputs(colors[COLOR_MARKS], f);
+                fputs(" -> ", f);
+                fputs(colors[COLOR_RESET], f);
+
+                r = varlink_idl_format_all_fields(f, symbol, VARLINK_OUTPUT, /* indent= */ NULL, colors);
+                break;
+
+        case VARLINK_ERROR:
+                fputs(colors[COLOR_SYMBOL_TYPE], f);
+                fputs("error ", f);
+                fputs(colors[COLOR_IDENTIFIER], f);
+                fputs(symbol->name, f);
+                fputs(colors[COLOR_RESET], f);
+
+                r = varlink_idl_format_all_fields(f, symbol, VARLINK_REGULAR, /* indent= */ NULL, colors);
+                break;
+
+        default:
+                assert_not_reached();
+        }
+        if (r < 0)
+                return r;
+
+        fputs("\n", f);
+        return 0;
+}
+
+static int varlink_idl_format_all_symbols(
+                FILE *f,
+                const VarlinkInterface *interface,
+                VarlinkSymbolType filter_type,
+                const char *const colors[static _COLOR_MAX]) {
+
+        int r;
+
+        assert(f);
+        assert(interface);
+
+        for (const VarlinkSymbol *const*symbol = interface->symbols; *symbol; symbol++) {
+
+                if ((*symbol)->symbol_type != filter_type)
+                        continue;
+
+                fputs("\n", f);
+
+                r = varlink_idl_format_symbol(f, *symbol, colors);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int varlink_idl_dump(FILE *f, int use_colors, const VarlinkInterface *interface) {
+        static const char* const color_table[_COLOR_MAX] = {
+                [COLOR_SYMBOL_TYPE] = ANSI_HIGHLIGHT_GREEN,
+                [COLOR_FIELD_TYPE]  = ANSI_HIGHLIGHT_BLUE,
+                [COLOR_IDENTIFIER]  = ANSI_NORMAL,
+                [COLOR_MARKS]       = ANSI_HIGHLIGHT_MAGENTA,
+                [COLOR_RESET]       = ANSI_NORMAL,
+        };
+
+        static const char* const color_off[_COLOR_MAX] = {
+                "", "", "", "", "",
+        };
+
+        int r;
+
+        assert(interface);
+
+        if (!f)
+                f = stdout;
+
+        if (use_colors < 0)
+                use_colors = colors_enabled();
+
+        const char *const *colors = use_colors ? color_table : color_off;
+
+        fputs(colors[COLOR_SYMBOL_TYPE], f);
+        fputs("interface ", f);
+        fputs(colors[COLOR_IDENTIFIER], f);
+        fputs(ASSERT_PTR(interface->name), f);
+        fputs(colors[COLOR_RESET], f);
+        fputs("\n", f);
+
+        for (VarlinkSymbolType t = 0; t < _VARLINK_SYMBOL_TYPE_MAX; t++) {
+                r = varlink_idl_format_all_symbols(f, interface, t, colors);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int varlink_idl_format(const VarlinkInterface *interface, char **ret) {
+        _cleanup_(memstream_done) MemStream memstream = {};
+        int r;
+
+        if (!memstream_init(&memstream))
+                return -errno;
+
+        r = varlink_idl_dump(memstream.f, /* use_colors= */ false, interface);
+        if (r < 0)
+                return r;
+
+        return memstream_finalize(&memstream, ret, NULL);
+}
+
+static VarlinkSymbol *varlink_symbol_free(VarlinkSymbol *symbol) {
+        if (!symbol)
+                return NULL;
+
+        /* See comment in varlink_interface_free() regarding the casting away of `const` */
+
+        free((char*) symbol->name);
+
+        for (size_t i = 0; symbol->fields[i].field_type != _VARLINK_FIELD_TYPE_END_MARKER; i++) {
+                VarlinkField *field = symbol->fields + i;
+
+                free((void*) field->name);
+                free((void*) field->named_type);
+
+                /* The symbol pointer might either point to a named symbol, in which case that symbol is
+                 * owned by the interface, or by an anomyous symbol, in which case it is owned by us, and we
+                 * need to free it */
+                if (field->symbol && field->field_type != VARLINK_NAMED_TYPE)
+                        varlink_symbol_free((VarlinkSymbol*) field->symbol);
+        }
+
+        return mfree(symbol);
+}
+
+VarlinkInterface* varlink_interface_free(VarlinkInterface *interface) {
+        if (!interface)
+                return NULL;
+
+        /* So here's the thing: in most cases we want that users of this define their interface descriptions
+         * in C code, and hence the definitions are constant and immutable during the life-cycle of the
+         * system. Because of that we define all structs with const* pointers. It makes it very nice and
+         * straight-forward to populate these structs with literal C strings. However, in some not so common
+         * cases we also want to allocate these structures dynamically on the heap, when parsing interface
+         * descriptions. But given this should be the exceptional and not the common case, we decided to
+         * simple cast away the 'const' where needed, even if it is ugly. */
+
+        free((char*) interface->name);
+
+        for (size_t i = 0; interface->symbols[i]; i++)
+                varlink_symbol_free((VarlinkSymbol*) interface->symbols[i]);
+
+        return mfree(interface);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkSymbol*, varlink_symbol_free);
+
+static int varlink_interface_realloc(VarlinkInterface **interface, size_t n_symbols) {
+        VarlinkInterface *n;
+
+        assert(interface);
+
+        n_symbols ++; /* Space for trailing NULL end marker symbol */
+
+        /* Overflow check */
+        if (n_symbols > (SIZE_MAX - offsetof(VarlinkInterface, symbols)) / sizeof(VarlinkSymbol*))
+                return -ENOMEM;
+
+        n = realloc0(*interface, offsetof(VarlinkInterface, symbols) + sizeof(VarlinkSymbol*) * n_symbols);
+        if (!n)
+                return -ENOMEM;
+
+        *interface = n;
+        return 0;
+}
+
+static int varlink_symbol_realloc(VarlinkSymbol **symbol, size_t n_fields) {
+        VarlinkSymbol *n;
+
+        assert(symbol);
+
+        n_fields ++; /* Space for trailing end marker field */
+
+        /* Overflow check */
+        if (n_fields > (SIZE_MAX - offsetof(VarlinkSymbol, fields)) / sizeof(VarlinkField))
+                return -ENOMEM;
+
+        n = realloc0(*symbol, offsetof(VarlinkSymbol, fields) + sizeof(VarlinkField) * n_fields);
+        if (!n)
+                return -ENOMEM;
+
+        *symbol = n;
+        return 0;
+}
+
+#define VALID_CHARS_IDENTIFIER ALPHANUMERICAL "_"
+#define VALID_CHARS_RESERVED LOWERCASE_LETTERS
+#define VALID_CHARS_INTERFACE_NAME ALPHANUMERICAL ".-"
+
+static void advance_line_column(const char *p, size_t n, unsigned *line, unsigned *column) {
+
+        assert(p);
+        assert(line);
+        assert(column);
+
+        for (; n > 0; p++, n--) {
+
+                if (*p == '\n') {
+                        (*line)++;
+                        *column = 1;
+                } else
+                        (*column)++;
+        }
+}
+
+static size_t token_match(
+                const char *p,
+                const char *allowed_delimiters,
+                const char *allowed_chars) {
+
+        /* Checks if the string p begins either with one of the token characters in allowed_delimiters or
+         * with a string consisting of allowed_chars. */
+
+        assert(p);
+
+        if (allowed_delimiters && strchr(allowed_delimiters, *p))
+                return 1;
+
+        if (!allowed_chars)
+                return 0;
+
+        return strspn(p, allowed_chars);
+}
+
+static int varlink_idl_subparse_token(
+                const char **p,
+                unsigned *line,
+                unsigned *column,
+                const char *allowed_delimiters,
+                const char *allowed_chars,
+                char **ret_token) {
+
+        _cleanup_free_ char *t = NULL;
+        size_t l;
+
+        assert(p);
+        assert(*p);
+        assert(line);
+        assert(column);
+        assert(ret_token);
+
+        if (**p == '\0') { /* eof */
+                *ret_token = NULL;
+                return 0;
+        }
+
+        l = token_match(*p, allowed_delimiters, allowed_chars);
+
+        /* No token of the permitted character set found? Then let's try to skip over whitespace and try again */
+        if (l == 0) {
+                size_t ll;
+
+                ll = strspn(*p, WHITESPACE);
+                advance_line_column(*p, ll, line, column);
+                *p += ll;
+
+                if (**p == '\0') { /* eof */
+                        *ret_token = NULL;
+                        return 0;
+                }
+
+                l = token_match(*p, allowed_delimiters, allowed_chars);
+                if (l == 0)
+                        return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Couldn't find token of allowed chars '%s' or allowed delimiters '%s'.", strempty(allowed_chars), strempty(allowed_delimiters));
+        }
+
+        t = strndup(*p, l);
+        if (!t)
+                return -ENOMEM;
+
+        advance_line_column(*p, l, line, column);
+        *p += l;
+
+        *ret_token = TAKE_PTR(t);
+        return 1;
+}
+
+static int varlink_idl_subparse_comment(
+                const char **p,
+                unsigned *line,
+                unsigned *column) {
+
+        size_t l;
+
+        assert(p);
+        assert(*p);
+        assert(line);
+        assert(column);
+
+        l = strcspn(*p, NEWLINE);
+        advance_line_column(*p, l + 1, line, column);
+        *p += l;
+
+        return 1;
+}
+
+static int varlink_idl_subparse_whitespace(
+                const char **p,
+                unsigned *line,
+                unsigned *column) {
+
+        size_t l;
+
+        assert(p);
+        assert(*p);
+        assert(line);
+        assert(column);
+
+        l = strspn(*p, WHITESPACE);
+        advance_line_column(*p, l, line, column);
+        *p += l;
+
+        return 1;
+}
+
+static int varlink_idl_subparse_struct_or_enum(const char **p, unsigned *line, unsigned *column, VarlinkSymbol **symbol, size_t *n_fields, VarlinkFieldDirection direction);
+
+static int varlink_idl_subparse_field_type(
+                const char **p,
+                unsigned *line,
+                unsigned *column,
+                VarlinkField *field) {
+
+        size_t l;
+        int r;
+
+        assert(p);
+        assert(*p);
+        assert(line);
+        assert(field);
+
+        r = varlink_idl_subparse_whitespace(p, line, column);
+        if (r < 0)
+                return r;
+
+        if (startswith(*p, "?")) {
+                field->field_flags |= VARLINK_NULLABLE;
+                l = 1;
+        } else {
+                field->field_flags &= ~VARLINK_NULLABLE;
+                l = 0;
+        }
+
+        advance_line_column(*p, l, line, column);
+        *p += l;
+
+        if (startswith(*p, "[]")) {
+                l = 2;
+                field->field_flags = (field->field_flags & ~VARLINK_MAP) | VARLINK_ARRAY;
+        } else if (startswith(*p, "[string]")) {
+                l = 8;
+                field->field_flags = (field->field_flags & ~VARLINK_ARRAY) | VARLINK_MAP;
+        } else {
+                l = 0;
+                field->field_flags = field->field_flags & ~(VARLINK_MAP | VARLINK_ARRAY);
+        }
+
+        advance_line_column(*p, l, line, column);
+        *p += l;
+
+        if (startswith(*p, "bool")) {
+                l = 4;
+                field->field_type = VARLINK_BOOL;
+        } else if (startswith(*p, "int")) {
+                l = 3;
+                field->field_type = VARLINK_INT;
+        } else if (startswith(*p, "float")) {
+                l = 5;
+                field->field_type = VARLINK_FLOAT;
+        } else if (startswith(*p, "string")) {
+                l = 6;
+                field->field_type = VARLINK_STRING;
+        } else if (startswith(*p, "object")) {
+                l = 6;
+                field->field_type = VARLINK_OBJECT;
+        } else if (**p == '(') {
+                _cleanup_(varlink_symbol_freep) VarlinkSymbol *symbol = NULL;
+                size_t n_fields = 0;
+
+                r = varlink_symbol_realloc(&symbol, n_fields);
+                if (r < 0)
+                        return r;
+
+                symbol->symbol_type = _VARLINK_SYMBOL_TYPE_INVALID;
+
+                r = varlink_idl_subparse_struct_or_enum(
+                                p,
+                                line,
+                                column,
+                                &symbol,
+                                &n_fields,
+                                VARLINK_REGULAR);
+                if (r < 0)
+                        return r;
+
+                if (symbol->symbol_type == VARLINK_STRUCT_TYPE)
+                        field->field_type = VARLINK_STRUCT;
+                else {
+                        assert(symbol->symbol_type == VARLINK_ENUM_TYPE);
+                        field->field_type = VARLINK_ENUM;
+                }
+
+                field->symbol = TAKE_PTR(symbol);
+                l = 0;
+        } else {
+                _cleanup_free_ char *token = NULL;
+
+                r = varlink_idl_subparse_token(p, line, column, /* valid_tokens= */ NULL, VALID_CHARS_IDENTIFIER, &token);
+                if (r < 0)
+                        return r;
+                if (!token)
+                        return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column);
+
+                field->named_type = TAKE_PTR(token);
+                field->field_type = VARLINK_NAMED_TYPE;
+                l = 0;
+        }
+
+        advance_line_column(*p, l, line, column);
+        *p += l;
+
+        return 0;
+}
+
+static int varlink_idl_subparse_struct_or_enum(
+                const char **p,
+                unsigned *line,
+                unsigned *column,
+                VarlinkSymbol **symbol,
+                size_t *n_fields,
+                VarlinkFieldDirection direction) {
+
+        enum {
+                STATE_OPEN,
+                STATE_NAME,
+                STATE_COLON,
+                STATE_COMMA,
+                STATE_DONE,
+        } state = STATE_OPEN;
+        _cleanup_free_ char *field_name = NULL;
+        const char *allowed_delimiters = "(", *allowed_chars = NULL;
+        int r;
+
+        assert(p);
+        assert(*p);
+        assert(line);
+        assert(column);
+        assert(symbol);
+        assert(*symbol);
+        assert(n_fields);
+
+        while (state != STATE_DONE) {
+                _cleanup_free_ char *token = NULL;
+
+                r = varlink_idl_subparse_token(
+                                p,
+                                line,
+                                column,
+                                allowed_delimiters,
+                                allowed_chars,
+                                &token);
+                if (r < 0)
+                        return r;
+
+                switch (state) {
+
+                case STATE_OPEN:
+                        if (!token)
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column);
+                        if (!streq(token, "("))
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token);
+
+                        state = STATE_NAME;
+                        allowed_delimiters = ")";
+                        allowed_chars = VALID_CHARS_IDENTIFIER;
+                        break;
+
+                case STATE_NAME:
+                        assert(!field_name);
+
+                        if (!token)
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column);
+                        if (streq(token, "#")) {
+                                r = varlink_idl_subparse_comment(p, line, column);
+                                if (r < 0)
+                                        return r;
+                        } else if (streq(token, ")"))
+                                state = STATE_DONE;
+                        else {
+                                field_name = TAKE_PTR(token);
+                                state = STATE_COLON;
+                                allowed_delimiters = ":,)";
+                                allowed_chars = NULL;
+                        }
+
+                        break;
+
+                case STATE_COLON:
+                        assert(field_name);
+
+                        if (!token)
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column);
+
+                        if (streq(token, ":")) {
+                                VarlinkField *field;
+
+                                if ((*symbol)->symbol_type < 0)
+                                        (*symbol)->symbol_type = VARLINK_STRUCT_TYPE;
+                                if ((*symbol)->symbol_type == VARLINK_ENUM_TYPE)
+                                        return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Enum with struct fields, refusing: %m", *line, *column);
+
+                                r = varlink_symbol_realloc(symbol, *n_fields + 1);
+                                if (r < 0)
+                                        return r;
+
+                                field = (*symbol)->fields + (*n_fields)++;
+                                *field = (VarlinkField) {
+                                        .name = TAKE_PTR(field_name),
+                                        .field_type = _VARLINK_FIELD_TYPE_INVALID,
+                                        .field_direction = direction,
+                                };
+
+                                r = varlink_idl_subparse_field_type(p, line, column, field);
+                                if (r < 0)
+                                        return r;
+
+                                state = STATE_COMMA;
+                                allowed_delimiters = ",)";
+                                allowed_chars = NULL;
+
+                        } else if (STR_IN_SET(token, ",", ")")) {
+                                VarlinkField *field;
+
+                                if ((*symbol)->symbol_type < 0)
+                                        (*symbol)->symbol_type = VARLINK_ENUM_TYPE;
+                                if ((*symbol)->symbol_type != VARLINK_ENUM_TYPE)
+                                        return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Struct with enum fields, refusing: %m", *line, *column);
+
+                                r = varlink_symbol_realloc(symbol, *n_fields + 1);
+                                if (r < 0)
+                                        return r;
+
+                                field = (*symbol)->fields + (*n_fields)++;
+                                *field = (VarlinkField) {
+                                        .name = TAKE_PTR(field_name),
+                                        .field_type = VARLINK_ENUM_VALUE,
+                                };
+
+                                if (streq(token, ",")) {
+                                        state = STATE_NAME;
+                                        allowed_delimiters = NULL;
+                                        allowed_chars = VALID_CHARS_IDENTIFIER;
+                                } else {
+                                        assert(streq(token, ")"));
+                                        state = STATE_DONE;
+                                }
+                        } else
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token);
+
+                        break;
+
+                case STATE_COMMA:
+                        assert(!field_name);
+
+                        if (!token)
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column);
+                        if (streq(token, ",")) {
+                                state = STATE_NAME;
+                                allowed_delimiters = NULL;
+                                allowed_chars = VALID_CHARS_IDENTIFIER;
+                        } else if (streq(token, ")"))
+                                state = STATE_DONE;
+                        else
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token);
+                        break;
+
+                default:
+                        assert_not_reached();
+                }
+        }
+
+        return 0;
+}
+
+static int varlink_idl_resolve_symbol_types(VarlinkInterface *interface, VarlinkSymbol *symbol) {
+        assert(interface);
+        assert(symbol);
+
+        for (VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) {
+                const VarlinkSymbol *found;
+
+                if (field->field_type != VARLINK_NAMED_TYPE)
+                        continue;
+
+                if (field->symbol) /* Already resolved */
+                        continue;
+
+                if (!field->named_type)
+                        return log_debug_errno(SYNTHETIC_ERRNO(ENETUNREACH), "Named type field lacking a type name.");
+
+                found = varlink_idl_find_symbol(interface, _VARLINK_SYMBOL_TYPE_INVALID, field->named_type);
+                if (!found)
+                        return log_debug_errno(SYNTHETIC_ERRNO(ENETUNREACH), "Failed to find type '%s'.", field->named_type);
+
+                if (!IN_SET(found->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_ENUM_TYPE))
+                        return log_debug_errno(SYNTHETIC_ERRNO(ENETUNREACH), "Symbol '%s' is referenced as type but is not a type.", field->named_type);
+
+                field->symbol = found;
+        }
+
+        return 0;
+}
+
+static int varlink_idl_resolve_types(VarlinkInterface *interface) {
+        int r;
+
+        assert(interface);
+
+        for (VarlinkSymbol **symbol = (VarlinkSymbol**) interface->symbols; *symbol; symbol++) {
+                r = varlink_idl_resolve_symbol_types(interface, *symbol);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int varlink_idl_parse(
+                const char *text,
+                unsigned *line,
+                unsigned *column,
+                VarlinkInterface **ret) {
+
+        _cleanup_(varlink_interface_freep) VarlinkInterface *interface = NULL;
+        _cleanup_(varlink_symbol_freep) VarlinkSymbol *symbol = NULL;
+        enum {
+                STATE_PRE_INTERFACE,
+                STATE_INTERFACE,
+                STATE_PRE_SYMBOL,
+                STATE_METHOD,
+                STATE_METHOD_NAME,
+                STATE_METHOD_ARROW,
+                STATE_TYPE,
+                STATE_TYPE_NAME,
+                STATE_ERROR,
+                STATE_ERROR_NAME,
+                STATE_DONE,
+        } state = STATE_PRE_INTERFACE;
+        const char *allowed_delimiters = "#", *allowed_chars = VALID_CHARS_RESERVED;
+        size_t n_symbols = 0, n_fields = 1;
+        unsigned _line = 0, _column = 1;
+        const char **p = &text;
+        int r;
+
+        if (!line)
+                line = &_line;
+        if (!column)
+                column = &_column;
+
+        while (state != STATE_DONE) {
+                _cleanup_free_ char *token = NULL;
+
+                r = varlink_idl_subparse_token(
+                                p,
+                                line,
+                                column,
+                                allowed_delimiters,
+                                allowed_chars,
+                                &token);
+                if (r < 0)
+                        return r;
+
+                switch (state) {
+
+                case STATE_PRE_INTERFACE:
+                        if (!token)
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column);
+                        if (streq(token, "#")) {
+                                r = varlink_idl_subparse_comment(&text, line, column);
+                                if (r < 0)
+                                        return r;
+                        } else if (streq(token, "interface")) {
+                                state = STATE_INTERFACE;
+                                allowed_delimiters = NULL;
+                                allowed_chars = VALID_CHARS_INTERFACE_NAME;
+                        } else
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token);
+                        break;
+
+                case STATE_INTERFACE:
+                        assert(!interface);
+                        assert(n_symbols == 0);
+
+                        if (!token)
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column);
+
+                        r = varlink_interface_realloc(&interface, n_symbols);
+                        if (r < 0)
+                                return r;
+
+                        interface->name = TAKE_PTR(token);
+                        state = STATE_PRE_SYMBOL;
+                        allowed_delimiters = "#";
+                        allowed_chars = VALID_CHARS_RESERVED;
+                        break;
+
+                case STATE_PRE_SYMBOL:
+                        if (!token) {
+                                state = STATE_DONE;
+                                break;
+                        }
+
+                        if (streq(token, "#")) {
+                                r = varlink_idl_subparse_comment(&text, line, column);
+                                if (r < 0)
+                                        return r;
+                        } else if (streq(token, "method")) {
+                                state = STATE_METHOD;
+                                allowed_chars = VALID_CHARS_IDENTIFIER;
+                        } else if (streq(token, "type")) {
+                                state = STATE_TYPE;
+                                allowed_chars = VALID_CHARS_IDENTIFIER;
+                        } else if (streq(token, "error")) {
+                                state = STATE_ERROR;
+                                allowed_chars = VALID_CHARS_IDENTIFIER;
+                        } else
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token);
+
+                        break;
+
+                case STATE_METHOD:
+                        assert(!symbol);
+                        n_fields = 0;
+
+                        r = varlink_symbol_realloc(&symbol, n_fields);
+                        if (r < 0)
+                                return r;
+
+                        symbol->symbol_type = VARLINK_METHOD;
+                        symbol->name = TAKE_PTR(token);
+
+                        r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_INPUT);
+                        if (r < 0)
+                                return r;
+
+                        state = STATE_METHOD_ARROW;
+                        allowed_chars = "->";
+                        break;
+
+                case STATE_METHOD_ARROW:
+                        assert(symbol);
+
+                        if (!streq(token, "->"))
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token);
+
+                        r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_OUTPUT);
+                        if (r < 0)
+                                return r;
+
+                        r = varlink_interface_realloc(&interface, n_symbols + 1);
+                        if (r < 0)
+                                return r;
+
+                        interface->symbols[n_symbols++] = TAKE_PTR(symbol);
+
+                        state = STATE_PRE_SYMBOL;
+                        allowed_chars = VALID_CHARS_RESERVED "#";
+                        break;
+
+                case STATE_TYPE:
+                        assert(!symbol);
+                        n_fields = 0;
+
+                        r = varlink_symbol_realloc(&symbol, n_fields);
+                        if (r < 0)
+                                return r;
+
+                        symbol->symbol_type = _VARLINK_SYMBOL_TYPE_INVALID; /* don't know yet if enum or struct, will be fileld in by varlink_idl_subparse_struct_or_enum() */
+                        symbol->name = TAKE_PTR(token);
+
+                        r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_REGULAR);
+                        if (r < 0)
+                                return r;
+
+                        r = varlink_interface_realloc(&interface, n_symbols + 1);
+                        if (r < 0)
+                                return r;
+
+                        interface->symbols[n_symbols++] = TAKE_PTR(symbol);
+
+                        state = STATE_PRE_SYMBOL;
+                        allowed_chars = VALID_CHARS_RESERVED "#";
+                        break;
+
+                case STATE_ERROR:
+                        assert(!symbol);
+                        n_fields = 0;
+
+                        r = varlink_symbol_realloc(&symbol, n_fields);
+                        if (r < 0)
+                                return r;
+
+                        symbol->symbol_type = VARLINK_ERROR;
+                        symbol->name = TAKE_PTR(token);
+
+                        r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_REGULAR);
+                        if (r < 0)
+                                return r;
+
+                        r = varlink_interface_realloc(&interface, n_symbols + 1);
+                        if (r < 0)
+                                return r;
+
+                        interface->symbols[n_symbols++] = TAKE_PTR(symbol);
+
+                        state = STATE_PRE_SYMBOL;
+                        allowed_chars = VALID_CHARS_RESERVED "#";
+                        break;
+
+                default:
+                        assert_not_reached();
+                }
+        }
+
+        r = varlink_idl_resolve_types(interface);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(interface);
+        return 0;
+}
+
+bool varlink_idl_field_name_is_valid(const char *name) {
+        if (isempty(name))
+                return false;
+
+        /* Field names may start with lower or uppercase char, but no numerals or underscore */
+        if (!strchr(LETTERS, name[0]))
+                return false;
+
+        /* Otherwise fields may be alphanumerical or underscore, but no two underscore may immediately follow
+         * each other or be trailing */
+        bool underscore = false;
+        for (const char *c = name + 1; *c; c++) {
+                if (*c == '_') {
+                        if (underscore)
+                                return false;
+
+                        underscore = true;
+                        continue;
+                }
+
+                if (!strchr(ALPHANUMERICAL, *c))
+                        return false;
+
+                underscore = false;
+        }
+
+        if (underscore)
+                return false;
+
+        return true;
+}
+
+bool varlink_idl_symbol_name_is_valid(const char *name) {
+        if (isempty(name))
+                return false;
+
+        /* We might want to reference VARLINK_STRUCT_TYPE and VARLINK_ENUM_TYPE symbols where we also
+         * reference native types, hence make sure the native type names are refused as symbol names. */
+        if (STR_IN_SET(name, "bool", "int", "float", "string", "object"))
+                return false;
+
+        /* Symbols must be named with an uppercase letter as first character */
+        if (!strchr(UPPERCASE_LETTERS, name[0]))
+                return false;
+
+        for (const char *c = name + 1; *c; c++)
+                if (!strchr(ALPHANUMERICAL, *c))
+                        return false;
+
+        return true;
+}
+
+bool varlink_idl_interface_name_is_valid(const char *name) {
+        if (isempty(name))
+                return false;
+
+        /* Interface names must start with a letter, uppercase or lower case, but nothing else */
+        if (!strchr(LETTERS, name[0]))
+                return false;
+
+        /* Otherwise it may be a series of non-empty dot separated labels, which are alphanumerical and may
+         * contain single dashes in the middle */
+        bool dot = false, dash = false;
+        for (const char *c = name + 1; *c; c++) {
+                switch (*c) {
+
+                case '.':
+                        if (dot || dash)
+                                return false;
+
+                        dot = true;
+                        break;
+
+                case '-':
+                        if (dot || dash)
+                                return false;
+
+                        dash = true;
+                        break;
+
+                default:
+                        if (!strchr(ALPHANUMERICAL, *c))
+                                return false;
+
+                        dot = dash = false;
+                }
+        }
+
+        if (dot || dash)
+                return false;
+
+        return true;
+}
+
+static int varlink_idl_symbol_consistent(const VarlinkInterface *interface, const VarlinkSymbol *symbol, bool level);
+
+static int varlink_idl_field_consistent(
+                const VarlinkInterface *interface,
+                const VarlinkSymbol *symbol,
+                const VarlinkField *field,
+                bool level) {
+
+        const char *symbol_name;
+        int r;
+
+        assert(interface);
+        assert(symbol);
+        assert(field);
+        assert(field->name);
+
+        symbol_name = symbol->name ?: "<anonymous>";
+
+        if (field->field_type <= 0 || field->field_type >= _VARLINK_FIELD_TYPE_MAX)
+                return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Field type for '%s' in symbol '%s' is not valid, refusing.", field->name, symbol_name);
+
+        if (field->field_type == VARLINK_ENUM_VALUE) {
+
+                if (symbol->symbol_type != VARLINK_ENUM_TYPE)
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Enum field type for '%s' in non-enum symbol '%s', refusing.", field->name, symbol_name);
+
+                if (field->field_flags != 0)
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Enum field '%s' in symbol '%s' has non-zero flags set, refusing.", field->name, symbol_name);
+        } else {
+                if (symbol->symbol_type == VARLINK_ENUM_TYPE)
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Non-enum field type for '%s' in enum symbol '%s', refusing.", field->name, symbol_name);
+
+                if (!IN_SET(field->field_flags & ~VARLINK_NULLABLE, 0, VARLINK_ARRAY, VARLINK_MAP))
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Flags of field '%s' in symbol '%s' is invalid, refusing.", field->name, symbol_name);
+        }
+
+        if (symbol->symbol_type != VARLINK_METHOD) {
+                if (field->field_direction != VARLINK_REGULAR)
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Direction of '%s' in non-method symbol '%s' not regular, refusing.", field->name, symbol_name);
+        } else {
+                if (!IN_SET(field->field_direction, VARLINK_INPUT, VARLINK_OUTPUT))
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Direction of '%s' in method symbol '%s' is not input or output, refusing.", field->name, symbol_name);
+        }
+
+        if (field->symbol) {
+                if (!IN_SET(field->field_type, VARLINK_STRUCT, VARLINK_ENUM, VARLINK_NAMED_TYPE))
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Target symbol for field '%s' in symbol '%s' defined for elemental field, refusing.", field->name, symbol_name);
+
+                if (field->field_type == VARLINK_NAMED_TYPE) {
+                        const VarlinkSymbol *found;
+
+                        if (!field->symbol->name || !field->named_type || !streq(field->symbol->name, field->named_type))
+                                return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Resolved symbol name and named type of field '%s' in symbol '%s' do do not match, refusing.", field->name, symbol_name);
+
+                        /* If this is a named type, then check if it's properly part of the interface */
+                        found = varlink_idl_find_symbol(interface, _VARLINK_SYMBOL_TYPE_INVALID, field->symbol->name);
+                        if (!found)
+                                return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Resolved symbol of named type of field '%s' in symbol '%s' is not part of the interface, refusing.", field->name, symbol_name);
+
+                        if (!IN_SET(found->symbol_type, VARLINK_ENUM_TYPE, VARLINK_STRUCT_TYPE))
+                                return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Resolved symbol of named type of field '%s' in symbol '%s' is not a type, refusing.", field->name, symbol_name);
+                } else {
+                        /* If this is an anonymous type, then we recursively check if it's consistent, since
+                         * it's not part of the interface, and hence we won't validate it from there. */
+
+                        r = varlink_idl_symbol_consistent(interface, field->symbol, level);
+                        if (r < 0)
+                                return r;
+                }
+
+        } else {
+                if (IN_SET(field->field_type, VARLINK_STRUCT, VARLINK_ENUM, VARLINK_NAMED_TYPE))
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "No target symbol for field '%s' in symbol '%s' defined for elemental field, refusing.", field->name, symbol_name);
+
+                if (field->named_type)
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Unresolved symbol in field '%s' in symbol '%s', refusing.", field->name, symbol_name);
+        }
+
+        if (field->named_type) {
+                if (field->field_type != VARLINK_NAMED_TYPE)
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Named type set for field '%s' in symbol '%s' but not a named type field, refusing.", field->name, symbol_name);
+        } else {
+                if (field->field_type == VARLINK_NAMED_TYPE)
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "No named type set for field '%s' in symbol '%s' but field is a named type field, refusing.", field->name, symbol_name);
+        }
+
+        return 0;
+}
+
+static bool varlink_symbol_is_empty(const VarlinkSymbol *symbol) {
+        assert(symbol);
+
+        return symbol->fields[0].field_type == _VARLINK_FIELD_TYPE_END_MARKER;
+}
+
+static int varlink_idl_symbol_consistent(
+                const VarlinkInterface *interface,
+                const VarlinkSymbol *symbol,
+                bool level) {
+
+        _cleanup_(set_freep) Set *input_set = NULL, *output_set = NULL;
+        const char *symbol_name;
+        int r;
+
+        assert(interface);
+        assert(symbol);
+
+        symbol_name = symbol->name ?: "<anonymous>";
+
+        if (symbol->symbol_type < 0 || symbol->symbol_type >= _VARLINK_SYMBOL_TYPE_MAX)
+                return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol type for '%s' is not valid, refusing.", symbol_name);
+
+        if (IN_SET(symbol->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_ENUM_TYPE) && varlink_symbol_is_empty(symbol))
+                return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol '%s' is empty, refusing.", symbol_name);
+
+        for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) {
+                Set **name_set = field->field_direction == VARLINK_OUTPUT ? &output_set : &input_set; /* for the method case we need two separate sets, otherwise we use the same */
+
+                if (!varlink_idl_field_name_is_valid(field->name))
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Field name '%s' in symbol '%s' not valid, refusing.", field->name, symbol_name);
+
+                if (set_contains(*name_set, field->name))
+                        return log_full_errno(level, SYNTHETIC_ERRNO(ENOTUNIQ), "Field '%s' defined twice in symbol '%s', refusing.", field->name, symbol_name);
+
+                if (set_ensure_put(name_set, &string_hash_ops, field->name) < 0)
+                        return log_oom();
+
+                r = varlink_idl_field_consistent(interface, symbol, field, level);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int varlink_idl_consistent(const VarlinkInterface *interface, bool level) {
+        _cleanup_(set_freep) Set *name_set = NULL;
+        int r;
+
+        assert(interface);
+
+        if (!varlink_idl_interface_name_is_valid(interface->name))
+                return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Interface name '%s' is not valid, refusing.", interface->name);
+
+        for (const VarlinkSymbol *const *symbol = interface->symbols; *symbol; symbol++) {
+
+                if (!varlink_idl_symbol_name_is_valid((*symbol)->name))
+                        return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol name '%s' is not valid, refusing.", strempty((*symbol)->name));
+
+                if (set_contains(name_set, (*symbol)->name))
+                        return log_full_errno(level, SYNTHETIC_ERRNO(ENOTUNIQ), "Symbol '%s' defined twice in interface, refusing.", (*symbol)->name);
+
+                if (set_ensure_put(&name_set, &string_hash_ops, (*symbol)->name) < 0)
+                        return log_oom();
+
+                r = varlink_idl_symbol_consistent(interface, *symbol, level);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int varlink_idl_validate_symbol(const VarlinkSymbol *symbol, JsonVariant *v, VarlinkFieldDirection direction, const char **bad_field);
+
+static int varlink_idl_validate_field_element_type(const VarlinkField *field, JsonVariant *v) {
+        assert(field);
+
+        switch (field->field_type) {
+
+        case VARLINK_STRUCT:
+        case VARLINK_ENUM:
+        case VARLINK_NAMED_TYPE:
+                return varlink_idl_validate_symbol(field->symbol, v, VARLINK_REGULAR, NULL);
+
+        case VARLINK_BOOL:
+                if (!json_variant_is_boolean(v))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a bool, but it is not, refusing.", strna(field->name));
+
+                break;
+
+        case VARLINK_INT:
+                if (!json_variant_is_integer(v) && !json_variant_is_unsigned(v))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an int, but it is not, refusing.", strna(field->name));
+
+                break;
+
+        case VARLINK_FLOAT:
+                if (!json_variant_is_number(v))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a float, but it is not, refusing.", strna(field->name));
+
+                break;
+
+        case VARLINK_STRING:
+                if (!json_variant_is_string(v))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a string, but it is not, refusing.", strna(field->name));
+
+                break;
+
+        case VARLINK_OBJECT:
+                if (!json_variant_is_object(v))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an object, but it is not, refusing.", strna(field->name));
+
+                break;
+
+        default:
+                assert_not_reached();
+        }
+
+        return 0;
+}
+
+static int varlink_idl_validate_field(const VarlinkField *field, JsonVariant *v) {
+        int r;
+
+        assert(field);
+
+        if (!v || json_variant_is_null(v)) {
+
+                if (!FLAGS_SET(field->field_flags, VARLINK_NULLABLE))
+                        return log_debug_errno(SYNTHETIC_ERRNO(ENOANO), "Mandatory field '%s' is null or missing on object, refusing.", strna(field->name));
+
+        } else if (FLAGS_SET(field->field_flags, VARLINK_ARRAY)) {
+                JsonVariant *i;
+
+                if (!json_variant_is_array(v))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an array, but it is not, refusing.", strna(field->name));
+
+                JSON_VARIANT_ARRAY_FOREACH(i, v) {
+                        r = varlink_idl_validate_field_element_type(field, i);
+                        if (r < 0)
+                                return r;
+                }
+
+        } else if (FLAGS_SET(field->field_flags, VARLINK_MAP)) {
+                _unused_ const char *k;
+                JsonVariant *e;
+
+                if (!json_variant_is_object(v))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an object, but it is not, refusing.", strna(field->name));
+
+                JSON_VARIANT_OBJECT_FOREACH(k, e, v) {
+                        r = varlink_idl_validate_field_element_type(field, e);
+                        if (r < 0)
+                                return r;
+                }
+        } else {
+
+                r = varlink_idl_validate_field_element_type(field, v);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int varlink_idl_validate_symbol(const VarlinkSymbol *symbol, JsonVariant *v, VarlinkFieldDirection direction, const char **bad_field) {
+        int r;
+
+        assert(symbol);
+
+        if (!v) {
+                if (bad_field)
+                        *bad_field = NULL;
+                return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Null object passed, refusing.");
+        }
+
+        switch (symbol->symbol_type) {
+
+        case VARLINK_ENUM_TYPE: {
+                bool found = false;
+                const char *s;
+
+                if (!json_variant_is_string(v)) {
+                        if (bad_field)
+                                *bad_field = symbol->name;
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Passed non-string to enum field '%s', refusing.", strna(symbol->name));
+                }
+
+                assert_se(s = json_variant_string(v));
+
+                for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) {
+
+                        assert(field->field_type == VARLINK_ENUM_VALUE);
+
+                        if (streq_ptr(field->name, s)) {
+                                found = true;
+                                break;
+                        }
+                }
+
+                if (!found) {
+                        if (bad_field)
+                                *bad_field = s;
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Passed unrecognized string '%s' to enum field '%s', refusing.", s, strna(symbol->name));
+                }
+
+                break;
+        }
+
+        case VARLINK_STRUCT_TYPE:
+        case VARLINK_METHOD:
+        case VARLINK_ERROR: {
+                if (!json_variant_is_object(v)) {
+                        if (bad_field)
+                                *bad_field = symbol->name;
+                        return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Passed non-object to field '%s', refusing.", strna(symbol->name));
+                }
+
+                for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) {
+
+                        if (field->field_direction != direction)
+                                continue;
+
+                        r = varlink_idl_validate_field(field, json_variant_by_key(v, field->name));
+                        if (r < 0) {
+                                if (bad_field)
+                                        *bad_field = field->name;
+                                return r;
+                        }
+                }
+
+                _unused_ JsonVariant *e;
+                const char *name;
+                JSON_VARIANT_OBJECT_FOREACH(name, e, v) {
+                        if (!varlink_idl_find_field(symbol, name)) {
+                                if (bad_field)
+                                        *bad_field = name;
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Field '%s' not defined for object, refusing.", name);
+                        }
+                }
+
+                break;
+        }
+
+        default:
+                assert_not_reached();
+        }
+
+        return 1; /* validated */
+}
+
+static int varlink_idl_validate_method(const VarlinkSymbol *method, JsonVariant *v, VarlinkFieldDirection direction, const char **bad_field) {
+        assert(IN_SET(direction, VARLINK_INPUT, VARLINK_OUTPUT));
+
+        if (!method)
+                return 0; /* Can't validate */
+        if (method->symbol_type != VARLINK_METHOD)
+                return -EBADMSG;
+
+        return varlink_idl_validate_symbol(method, v, direction, bad_field);
+}
+
+int varlink_idl_validate_method_call(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field) {
+        return varlink_idl_validate_method(method, v, VARLINK_INPUT, bad_field);
+}
+
+int varlink_idl_validate_method_reply(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field) {
+        return varlink_idl_validate_method(method, v, VARLINK_OUTPUT, bad_field);
+}
+
+int varlink_idl_validate_error(const VarlinkSymbol *error, JsonVariant *v, const char **bad_field) {
+        if (!error)
+                return 0; /* Can't validate */
+        if (error->symbol_type != VARLINK_ERROR)
+                return -EBADMSG;
+
+        return varlink_idl_validate_symbol(error, v, VARLINK_REGULAR, bad_field);
+}
+
+const VarlinkSymbol* varlink_idl_find_symbol(
+                const VarlinkInterface *interface,
+                VarlinkSymbolType type,
+                const char *name) {
+
+        assert(interface);
+        assert(type < _VARLINK_SYMBOL_TYPE_MAX);
+
+        if (isempty(name))
+                return NULL;
+
+        for (const VarlinkSymbol *const*symbol = interface->symbols; *symbol; symbol++) {
+                if (type >= 0 && (*symbol)->symbol_type != type)
+                        continue;
+
+                if (streq_ptr((*symbol)->name, name))
+                        return *symbol;
+        }
+
+        return NULL;
+}
+
+const VarlinkField* varlink_idl_find_field(
+                const VarlinkSymbol *symbol,
+                const char *name) {
+
+        assert(symbol);
+
+        if (isempty(name))
+                return NULL;
+
+        for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++)
+                if (streq_ptr(field->name, name))
+                        return field;
+
+        return NULL;
+}
diff --git a/src/shared/varlink-idl.h b/src/shared/varlink-idl.h
new file mode 100644 (file)
index 0000000..3a7075e
--- /dev/null
@@ -0,0 +1,158 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "json.h"
+#include "macro.h"
+
+/* This implements the Varlink Interface Definition Language ("Varlink IDL"),
+ * i.e. https://varlink.org/Interface-Definition
+ *
+ * Primarily allows encoding static interface definitions in C code, that can be converted to the textual IDL
+ * format on-the-fly. Can also parse the textual format back to C structures. Validates the interface
+ * definitions for internal consistency and validates JSON objects against the interface definitions. */
+
+typedef enum VarlinkSymbolType {
+        VARLINK_ENUM_TYPE,
+        VARLINK_STRUCT_TYPE,
+        VARLINK_METHOD,
+        VARLINK_ERROR,
+        _VARLINK_SYMBOL_TYPE_MAX,
+        _VARLINK_SYMBOL_TYPE_INVALID = -EINVAL,
+} VarlinkSymbolType;
+
+typedef enum VarlinkFieldType {
+        _VARLINK_FIELD_TYPE_END_MARKER = 0, /* zero type means: this is the last entry in the fields[] array of VarlinkSymbol */
+        VARLINK_STRUCT,
+        VARLINK_ENUM,
+        VARLINK_NAMED_TYPE,
+        VARLINK_BOOL,
+        VARLINK_INT,
+        VARLINK_FLOAT,
+        VARLINK_STRING,
+        VARLINK_OBJECT,
+        VARLINK_ENUM_VALUE,
+        _VARLINK_FIELD_TYPE_MAX,
+        _VARLINK_FIELD_TYPE_INVALID = -EINVAL,
+} VarlinkFieldType;
+
+typedef enum VarlinkFieldDirection {
+        VARLINK_REGULAR,
+        VARLINK_INPUT,
+        VARLINK_OUTPUT,
+        _VARLINK_FIELD_DIRECTION_MAX,
+        _VARLINK_FIELD_DIRECTION_INVALID = -EINVAL,
+} VarlinkFieldDirection;
+
+typedef enum VarlinkFieldFlags {
+        VARLINK_ARRAY                = 1 << 0,
+        VARLINK_MAP                  = 1 << 1,
+        VARLINK_NULLABLE             = 1 << 2,
+        _VARLINK_FIELD_FLAGS_MAX     = (1 << 3) - 1,
+        _VARLINK_FIELD_FLAGS_INVALID = -EINVAL,
+} VarlinkFieldFlags;
+
+typedef struct VarlinkField VarlinkField;
+typedef struct VarlinkSymbol VarlinkSymbol;
+typedef struct VarlinkInterface VarlinkInterface;
+
+/* Fields are the components making up symbols */
+struct VarlinkField {
+        const char *name;
+        VarlinkFieldType field_type;
+        VarlinkFieldFlags field_flags;
+        VarlinkFieldDirection field_direction; /* in case of method call fields: whether input or output argument */
+        const VarlinkSymbol *symbol;           /* VARLINK_STRUCT, VARLINK_ENUM: anonymous symbol that carries the definitions, VARLINK_NAMED_TYPE: resolved symbol */
+        const char *named_type;                /* VARLINK_NAMED_TYPE */
+};
+
+/* Symbols are primary named concepts in an interface, and are methods, errors or named types (either enum or struct). */
+struct VarlinkSymbol {
+        const char *name; /* most symbols have a name, but sometimes they are created on-the-fly for fields, in which case they are anonymous */
+        VarlinkSymbolType symbol_type;
+        VarlinkField fields[];
+};
+
+/* An interface definition has a name and consist of symbols */
+struct VarlinkInterface {
+        const char *name;
+        const VarlinkSymbol *symbols[];
+};
+
+#define VARLINK_DEFINE_FIELD(_name, _field_type, _field_flags)        \
+        { .name = #_name, .field_type = (_field_type), .field_flags = (_field_flags) }
+
+#define VARLINK_DEFINE_FIELD_BY_TYPE(_name, _named_type, _field_flags) \
+        { .name = #_name, .field_type = VARLINK_NAMED_TYPE, .named_type = #_named_type, .symbol = &vl_type_ ## _named_type, .field_flags = (_field_flags) }
+
+#define VARLINK_DEFINE_INPUT(_name, _field_type, _field_flags)        \
+        { .name = #_name, .field_type = (_field_type), .field_flags = (_field_flags), .field_direction = VARLINK_INPUT }
+
+#define VARLINK_DEFINE_INPUT_BY_TYPE(_name, _named_type, _field_flags) \
+        { .name = #_name, .field_type = VARLINK_NAMED_TYPE, .named_type = #_named_type, .symbol = &vl_type_ ## _named_type, .field_flags = (_field_flags), .field_direction = VARLINK_INPUT }
+
+#define VARLINK_DEFINE_OUTPUT(_name, _field_type, _field_flags)        \
+        { .name = #_name, .field_type = (_field_type), .field_flags = (_field_flags), .field_direction = VARLINK_OUTPUT }
+
+#define VARLINK_DEFINE_OUTPUT_BY_TYPE(_name, _named_type, _field_flags) \
+        { .name = #_name, .field_type = VARLINK_NAMED_TYPE, .named_type = #_named_type, .symbol = &vl_type_ ## _named_type, .field_flags = (_field_flags), .field_direction = VARLINK_OUTPUT }
+
+#define VARLINK_DEFINE_ENUM_VALUE(_name) \
+        { .name = #_name, .field_type = VARLINK_ENUM_VALUE }
+
+#define VARLINK_DEFINE_METHOD(_name, ...)                               \
+        const VarlinkSymbol vl_method_ ## _name = {                     \
+                .name = #_name,                                         \
+                .symbol_type = VARLINK_METHOD,                          \
+                .fields = { __VA_ARGS__ __VA_OPT__(,) {}},              \
+        }
+
+#define VARLINK_DEFINE_ERROR(_name, ...)                                \
+        const VarlinkSymbol vl_error_ ## _name = {                      \
+                .name = #_name,                                         \
+                .symbol_type = VARLINK_ERROR,                           \
+                .fields = { __VA_ARGS__ __VA_OPT__(,) {}},              \
+        }
+
+#define VARLINK_DEFINE_STRUCT_TYPE(_name, ...)                          \
+        const VarlinkSymbol vl_type_ ## _name = {                       \
+                .name = #_name,                                         \
+                .symbol_type = VARLINK_STRUCT_TYPE,                     \
+                .fields = { __VA_ARGS__ __VA_OPT__(,) {}},              \
+        }
+
+#define VARLINK_DEFINE_ENUM_TYPE(_name, ...)                            \
+        const VarlinkSymbol vl_type_ ## _name = {                       \
+                .name = #_name,                                         \
+                .symbol_type = VARLINK_ENUM_TYPE,                       \
+                .fields = { __VA_ARGS__ __VA_OPT__(,) {}},              \
+        }
+
+#define VARLINK_DEFINE_INTERFACE(_name, _full_name, ...)                \
+        const VarlinkInterface vl_interface_ ## _name = {               \
+                .name = (_full_name),                                   \
+                .symbols = { __VA_ARGS__ __VA_OPT__(,) NULL},           \
+        }
+
+int varlink_idl_dump(FILE *f, int use_colors, const VarlinkInterface *interface);
+int varlink_idl_format(const VarlinkInterface *interface, char **ret);
+
+int varlink_idl_parse(const char *text, unsigned *ret_line, unsigned *ret_column, VarlinkInterface **ret);
+VarlinkInterface* varlink_interface_free(VarlinkInterface *interface);
+DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkInterface*, varlink_interface_free);
+
+bool varlink_idl_field_name_is_valid(const char *name);
+bool varlink_idl_symbol_name_is_valid(const char *name);
+bool varlink_idl_interface_name_is_valid(const char *name);
+
+int varlink_idl_consistent(const VarlinkInterface *interface, bool level);
+
+const VarlinkSymbol* varlink_idl_find_symbol(const VarlinkInterface *interface, VarlinkSymbolType type, const char *name);
+const VarlinkField* varlink_idl_find_field(const VarlinkSymbol *symbol, const char *name);
+
+int varlink_idl_validate_method_call(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field);
+int varlink_idl_validate_method_reply(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field);
+int varlink_idl_validate_error(const VarlinkSymbol *error, JsonVariant *v, const char **bad_field);