/* 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" #define DEPTH_MAX 64U enum { COLOR_SYMBOL_TYPE, /* interface, method, type, error */ COLOR_FIELD_TYPE, /* string, bool, … */ COLOR_IDENTIFIER, COLOR_MARKS, /* [], ->, ?, … */ COLOR_RESET, _COLOR_MAX, }; #define varlink_idl_log(error, format, ...) log_debug_errno(error, "Varlink-IDL: " format, ##__VA_ARGS__) #define varlink_idl_log_full(level, error, format, ...) log_full_errno(level, error, "Varlink-IDL: " format, ##__VA_ARGS__) 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 lifecycle 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 varlink_idl_log(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, unsigned depth); static int varlink_idl_subparse_field_type( const char **p, unsigned *line, unsigned *column, VarlinkField *field, unsigned depth) { 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, depth + 1); 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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *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, unsigned depth) { 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); if (depth > DEPTH_MAX) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Maximum nesting depth reached (%u).", *line, *column, DEPTH_MAX); 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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *line, *column); if (!streq(token, "(")) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s'.", *line, *column, token); state = STATE_NAME; allowed_delimiters = ")"; allowed_chars = VALID_CHARS_IDENTIFIER; break; case STATE_NAME: assert(!field_name); if (!token) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *line, *column); 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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Enum with struct fields, refusing.", *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, depth); 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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Struct with enum fields, refusing.", *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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s'.", *line, *column, token); break; case STATE_COMMA: assert(!field_name); if (!token) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s'.", *line, *column, token); break; default: assert_not_reached(); } } /* If we don't know the type of the symbol by now it was an empty () which doesn't allow us to * determine if we look at an enum or a struct */ if ((*symbol)->symbol_type < 0) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Ambiguous empty () enum/struct is not permitted.", *line, *column); 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 varlink_idl_log(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 varlink_idl_log(SYNTHETIC_ERRNO(ENETUNREACH), "Failed to find type '%s'.", field->named_type); if (!IN_SET(found->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_ENUM_TYPE)) return varlink_idl_log(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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s'.", *line, *column, token); break; case STATE_INTERFACE: assert(!interface); assert(n_symbols == 0); if (!token) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *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 varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s'.", *line, *column, token); break; case STATE_METHOD: assert(!symbol); n_fields = 0; if (!token) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *line, *column); 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, 0); if (r < 0) return r; state = STATE_METHOD_ARROW; allowed_chars = "->"; break; case STATE_METHOD_ARROW: assert(symbol); if (!token) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *line, *column); if (!streq(token, "->")) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s'.", *line, *column, token); r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_OUTPUT, 0); 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; if (!token) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *line, *column); 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 field 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, 0); 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; if (!token) return varlink_idl_log(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF.", *line, *column); 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, 0); 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, int level); static int varlink_idl_field_consistent( const VarlinkInterface *interface, const VarlinkSymbol *symbol, const VarlinkField *field, int level) { const char *symbol_name; int r; assert(interface); assert(symbol); assert(field); assert(field->name); symbol_name = symbol->name ?: ""; if (field->field_type <= 0 || field->field_type >= _VARLINK_FIELD_TYPE_MAX) return varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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, int 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 ?: ""; if (symbol->symbol_type < 0 || symbol->symbol_type >= _VARLINK_SYMBOL_TYPE_MAX) return varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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 varlink_idl_log_full(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, int level) { _cleanup_(set_freep) Set *name_set = NULL; int r; assert(interface); if (!varlink_idl_interface_name_is_valid(interface->name)) return varlink_idl_log_full(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 varlink_idl_log_full(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol name '%s' is not valid, refusing.", strempty((*symbol)->name)); if (set_contains(name_set, (*symbol)->name)) return varlink_idl_log_full(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 varlink_idl_log(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a bool, but it is not, refusing.", strna(field->name)); break; case VARLINK_INT: /* Allow strings here too, since integers with > 53 bits are often passed in as strings */ if (!json_variant_is_integer(v) && !json_variant_is_unsigned(v) && !json_variant_is_string(v)) return varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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 varlink_idl_log(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; }