| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| #include "alloc-util.h" |
| #include "hexdecoct.h" |
| #include "list.h" |
| #include "parse-util.h" |
| #include "path-util.h" |
| #include "stdio-util.h" |
| #include "string-util.h" |
| #include "sysupdate-pattern.h" |
| #include "sysupdate-util.h" |
| |
| typedef enum PatternElementType { |
| PATTERN_LITERAL, |
| PATTERN_VERSION, |
| PATTERN_PARTITION_UUID, |
| PATTERN_PARTITION_FLAGS, |
| PATTERN_MTIME, |
| PATTERN_MODE, |
| PATTERN_SIZE, |
| PATTERN_TRIES_DONE, |
| PATTERN_TRIES_LEFT, |
| PATTERN_NO_AUTO, |
| PATTERN_READ_ONLY, |
| PATTERN_GROWFS, |
| PATTERN_SHA256SUM, |
| _PATTERN_ELEMENT_TYPE_MAX, |
| _PATTERN_ELEMENT_TYPE_INVALID = -EINVAL, |
| } PatternElementType; |
| |
| typedef struct PatternElement PatternElement; |
| |
| struct PatternElement { |
| PatternElementType type; |
| LIST_FIELDS(PatternElement, elements); |
| char literal[]; |
| }; |
| |
| static PatternElement *pattern_element_free_all(PatternElement *e) { |
| PatternElement *p; |
| |
| while ((p = LIST_POP(elements, e))) |
| free(p); |
| |
| return NULL; |
| } |
| |
| DEFINE_TRIVIAL_CLEANUP_FUNC(PatternElement*, pattern_element_free_all); |
| |
| static PatternElementType pattern_element_type_from_char(char c) { |
| switch (c) { |
| case 'v': |
| return PATTERN_VERSION; |
| case 'u': |
| return PATTERN_PARTITION_UUID; |
| case 'f': |
| return PATTERN_PARTITION_FLAGS; |
| case 't': |
| return PATTERN_MTIME; |
| case 'm': |
| return PATTERN_MODE; |
| case 's': |
| return PATTERN_SIZE; |
| case 'd': |
| return PATTERN_TRIES_DONE; |
| case 'l': |
| return PATTERN_TRIES_LEFT; |
| case 'a': |
| return PATTERN_NO_AUTO; |
| case 'r': |
| return PATTERN_READ_ONLY; |
| case 'g': |
| return PATTERN_GROWFS; |
| case 'h': |
| return PATTERN_SHA256SUM; |
| default: |
| return _PATTERN_ELEMENT_TYPE_INVALID; |
| } |
| } |
| |
| static bool valid_char(char x) { |
| |
| /* Let's refuse control characters here, and let's reserve some characters typically used in pattern |
| * languages so that we can use them later, possibly. */ |
| |
| if ((unsigned) x < ' ' || x >= 127) |
| return false; |
| |
| return !IN_SET(x, '$', '*', '?', '[', ']', '!', '\\', '/', '|'); |
| } |
| |
| static int pattern_split( |
| const char *pattern, |
| PatternElement **ret) { |
| |
| _cleanup_(pattern_element_free_allp) PatternElement *first = NULL; |
| bool at = false, last_literal = true; |
| PatternElement *last = NULL; |
| uint64_t mask_found = 0; |
| size_t l, k = 0; |
| |
| assert(pattern); |
| |
| l = strlen(pattern); |
| |
| for (const char *e = pattern; *e != 0; e++) { |
| if (*e == '@') { |
| if (!at) { |
| at = true; |
| continue; |
| } |
| |
| /* Two at signs in a sequence, write out one */ |
| at = false; |
| |
| } else if (at) { |
| PatternElementType t; |
| uint64_t bit; |
| |
| t = pattern_element_type_from_char(*e); |
| if (t < 0) |
| return log_debug_errno(t, "Unknown pattern field marker '@%c'.", *e); |
| |
| bit = UINT64_C(1) << t; |
| if (mask_found & bit) |
| return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Pattern field marker '@%c' appears twice in pattern.", *e); |
| |
| /* We insist that two pattern field markers are separated by some literal string that |
| * we can use to separate the fields when parsing. */ |
| if (!last_literal) |
| return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Found two pattern field markers without separating literal."); |
| |
| if (ret) { |
| PatternElement *z; |
| |
| z = malloc(offsetof(PatternElement, literal)); |
| if (!z) |
| return -ENOMEM; |
| |
| z->type = t; |
| LIST_INSERT_AFTER(elements, first, last, z); |
| last = z; |
| } |
| |
| mask_found |= bit; |
| last_literal = at = false; |
| continue; |
| } |
| |
| if (!valid_char(*e)) |
| return log_debug_errno( |
| SYNTHETIC_ERRNO(EBADRQC), |
| "Invalid character 0x%0x in pattern, refusing.", |
| (unsigned) *e); |
| |
| last_literal = true; |
| |
| if (!ret) |
| continue; |
| |
| if (!last || last->type != PATTERN_LITERAL) { |
| PatternElement *z; |
| |
| z = malloc0(offsetof(PatternElement, literal) + l + 1); /* l is an upper bound to all literal elements */ |
| if (!z) |
| return -ENOMEM; |
| |
| z->type = PATTERN_LITERAL; |
| k = 0; |
| |
| LIST_INSERT_AFTER(elements, first, last, z); |
| last = z; |
| } |
| |
| assert(last); |
| assert(last->type == PATTERN_LITERAL); |
| |
| last->literal[k++] = *e; |
| } |
| |
| if (at) |
| return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Trailing @ character found, refusing."); |
| if (!(mask_found & (UINT64_C(1) << PATTERN_VERSION))) |
| return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Version field marker '@v' not specified in pattern, refusing."); |
| |
| if (ret) |
| *ret = TAKE_PTR(first); |
| |
| return 0; |
| } |
| |
| int pattern_match(const char *pattern, const char *s, InstanceMetadata *ret) { |
| _cleanup_(instance_metadata_destroy) InstanceMetadata found = INSTANCE_METADATA_NULL; |
| _cleanup_(pattern_element_free_allp) PatternElement *elements = NULL; |
| const char *p; |
| int r; |
| |
| assert(pattern); |
| assert(s); |
| |
| r = pattern_split(pattern, &elements); |
| if (r < 0) |
| return r; |
| |
| p = s; |
| LIST_FOREACH(elements, e, elements) { |
| _cleanup_free_ char *t = NULL; |
| const char *n; |
| |
| if (e->type == PATTERN_LITERAL) { |
| const char *k; |
| |
| /* Skip literal fields */ |
| k = startswith(p, e->literal); |
| if (!k) |
| goto nope; |
| |
| p = k; |
| continue; |
| } |
| |
| if (e->elements_next) { |
| /* The next element must be literal, as we use it to determine where to split */ |
| assert(e->elements_next->type == PATTERN_LITERAL); |
| |
| n = strstr(p, e->elements_next->literal); |
| if (!n) |
| goto nope; |
| |
| } else |
| /* End of the string */ |
| assert_se(n = strchr(p, 0)); |
| t = strndup(p, n - p); |
| if (!t) |
| return -ENOMEM; |
| |
| switch (e->type) { |
| |
| case PATTERN_VERSION: |
| if (!version_is_valid(t)) { |
| log_debug("Version string is not valid, refusing: %s", t); |
| goto nope; |
| } |
| |
| assert(!found.version); |
| found.version = TAKE_PTR(t); |
| break; |
| |
| case PATTERN_PARTITION_UUID: { |
| sd_id128_t id; |
| |
| if (sd_id128_from_string(t, &id) < 0) |
| goto nope; |
| |
| assert(!found.partition_uuid_set); |
| found.partition_uuid = id; |
| found.partition_uuid_set = true; |
| break; |
| } |
| |
| case PATTERN_PARTITION_FLAGS: { |
| uint64_t f; |
| |
| if (safe_atoux64(t, &f) < 0) |
| goto nope; |
| |
| if (found.partition_flags_set && found.partition_flags != f) |
| goto nope; |
| |
| assert(!found.partition_flags_set); |
| found.partition_flags = f; |
| found.partition_flags_set = true; |
| break; |
| } |
| |
| case PATTERN_MTIME: { |
| uint64_t v; |
| |
| if (safe_atou64(t, &v) < 0) |
| goto nope; |
| if (v == USEC_INFINITY) /* Don't permit our internal special infinity value */ |
| goto nope; |
| if (v / 1000000U > TIME_T_MAX) /* Make sure this fits in a timespec structure */ |
| goto nope; |
| |
| assert(found.mtime == USEC_INFINITY); |
| found.mtime = v; |
| break; |
| } |
| |
| case PATTERN_MODE: { |
| mode_t m; |
| |
| r = parse_mode(t, &m); |
| if (r < 0) |
| goto nope; |
| if (m & ~0775) /* Don't allow world-writable files or suid files to be generated this way */ |
| goto nope; |
| |
| assert(found.mode == MODE_INVALID); |
| found.mode = m; |
| break; |
| } |
| |
| case PATTERN_SIZE: { |
| uint64_t u; |
| |
| r = safe_atou64(t, &u); |
| if (r < 0) |
| goto nope; |
| if (u == UINT64_MAX) |
| goto nope; |
| |
| assert(found.size == UINT64_MAX); |
| found.size = u; |
| break; |
| } |
| |
| case PATTERN_TRIES_DONE: { |
| uint64_t u; |
| |
| r = safe_atou64(t, &u); |
| if (r < 0) |
| goto nope; |
| if (u == UINT64_MAX) |
| goto nope; |
| |
| assert(found.tries_done == UINT64_MAX); |
| found.tries_done = u; |
| break; |
| } |
| |
| case PATTERN_TRIES_LEFT: { |
| uint64_t u; |
| |
| r = safe_atou64(t, &u); |
| if (r < 0) |
| goto nope; |
| if (u == UINT64_MAX) |
| goto nope; |
| |
| assert(found.tries_left == UINT64_MAX); |
| found.tries_left = u; |
| break; |
| } |
| |
| case PATTERN_NO_AUTO: |
| r = parse_boolean(t); |
| if (r < 0) |
| goto nope; |
| |
| assert(found.no_auto < 0); |
| found.no_auto = r; |
| break; |
| |
| case PATTERN_READ_ONLY: |
| r = parse_boolean(t); |
| if (r < 0) |
| goto nope; |
| |
| assert(found.read_only < 0); |
| found.read_only = r; |
| break; |
| |
| case PATTERN_GROWFS: |
| r = parse_boolean(t); |
| if (r < 0) |
| goto nope; |
| |
| assert(found.growfs < 0); |
| found.growfs = r; |
| break; |
| |
| case PATTERN_SHA256SUM: { |
| _cleanup_free_ void *d = NULL; |
| size_t l; |
| |
| if (strlen(t) != sizeof(found.sha256sum) * 2) |
| goto nope; |
| |
| r = unhexmem(t, sizeof(found.sha256sum) * 2, &d, &l); |
| if (r == -ENOMEM) |
| return r; |
| if (r < 0) |
| goto nope; |
| |
| assert(!found.sha256sum_set); |
| assert(l == sizeof(found.sha256sum)); |
| memcpy(found.sha256sum, d, l); |
| found.sha256sum_set = true; |
| break; |
| } |
| |
| default: |
| assert_se("unexpected pattern element"); |
| } |
| |
| p = n; |
| } |
| |
| if (ret) { |
| *ret = found; |
| found = (InstanceMetadata) INSTANCE_METADATA_NULL; |
| } |
| |
| return true; |
| |
| nope: |
| if (ret) |
| *ret = (InstanceMetadata) INSTANCE_METADATA_NULL; |
| |
| return false; |
| } |
| |
| int pattern_match_many(char **patterns, const char *s, InstanceMetadata *ret) { |
| _cleanup_(instance_metadata_destroy) InstanceMetadata found = INSTANCE_METADATA_NULL; |
| int r; |
| |
| STRV_FOREACH(p, patterns) { |
| r = pattern_match(*p, s, &found); |
| if (r < 0) |
| return r; |
| if (r > 0) { |
| if (ret) { |
| *ret = found; |
| found = (InstanceMetadata) INSTANCE_METADATA_NULL; |
| } |
| |
| return true; |
| } |
| } |
| |
| if (ret) |
| *ret = (InstanceMetadata) INSTANCE_METADATA_NULL; |
| |
| return false; |
| } |
| |
| int pattern_valid(const char *pattern) { |
| int r; |
| |
| r = pattern_split(pattern, NULL); |
| if (r == -EINVAL) |
| return false; |
| if (r < 0) |
| return r; |
| |
| return true; |
| } |
| |
| int pattern_format( |
| const char *pattern, |
| const InstanceMetadata *fields, |
| char **ret) { |
| |
| _cleanup_(pattern_element_free_allp) PatternElement *elements = NULL; |
| _cleanup_free_ char *j = NULL; |
| int r; |
| |
| assert(pattern); |
| assert(fields); |
| assert(ret); |
| |
| r = pattern_split(pattern, &elements); |
| if (r < 0) |
| return r; |
| |
| LIST_FOREACH(elements, e, elements) { |
| |
| switch (e->type) { |
| |
| case PATTERN_LITERAL: |
| if (!strextend(&j, e->literal)) |
| return -ENOMEM; |
| |
| break; |
| |
| case PATTERN_VERSION: |
| if (!fields->version) |
| return -ENXIO; |
| |
| if (!strextend(&j, fields->version)) |
| return -ENOMEM; |
| break; |
| |
| case PATTERN_PARTITION_UUID: { |
| char formatted[SD_ID128_STRING_MAX]; |
| |
| if (!fields->partition_uuid_set) |
| return -ENXIO; |
| |
| if (!strextend(&j, sd_id128_to_string(fields->partition_uuid, formatted))) |
| return -ENOMEM; |
| |
| break; |
| } |
| |
| case PATTERN_PARTITION_FLAGS: |
| if (!fields->partition_flags_set) |
| return -ENXIO; |
| |
| r = strextendf(&j, "%" PRIx64, fields->partition_flags); |
| if (r < 0) |
| return r; |
| |
| break; |
| |
| case PATTERN_MTIME: |
| if (fields->mtime == USEC_INFINITY) |
| return -ENXIO; |
| |
| r = strextendf(&j, "%" PRIu64, fields->mtime); |
| if (r < 0) |
| return r; |
| |
| break; |
| |
| case PATTERN_MODE: |
| if (fields->mode == MODE_INVALID) |
| return -ENXIO; |
| |
| r = strextendf(&j, "%03o", fields->mode); |
| if (r < 0) |
| return r; |
| |
| break; |
| |
| case PATTERN_SIZE: |
| if (fields->size == UINT64_MAX) |
| return -ENXIO; |
| |
| r = strextendf(&j, "%" PRIu64, fields->size); |
| if (r < 0) |
| return r; |
| break; |
| |
| case PATTERN_TRIES_DONE: |
| if (fields->tries_done == UINT64_MAX) |
| return -ENXIO; |
| |
| r = strextendf(&j, "%" PRIu64, fields->tries_done); |
| if (r < 0) |
| return r; |
| break; |
| |
| case PATTERN_TRIES_LEFT: |
| if (fields->tries_left == UINT64_MAX) |
| return -ENXIO; |
| |
| r = strextendf(&j, "%" PRIu64, fields->tries_left); |
| if (r < 0) |
| return r; |
| break; |
| |
| case PATTERN_NO_AUTO: |
| if (fields->no_auto < 0) |
| return -ENXIO; |
| |
| if (!strextend(&j, one_zero(fields->no_auto))) |
| return -ENOMEM; |
| |
| break; |
| |
| case PATTERN_READ_ONLY: |
| if (fields->read_only < 0) |
| return -ENXIO; |
| |
| if (!strextend(&j, one_zero(fields->read_only))) |
| return -ENOMEM; |
| |
| break; |
| |
| case PATTERN_GROWFS: |
| if (fields->growfs < 0) |
| return -ENXIO; |
| |
| if (!strextend(&j, one_zero(fields->growfs))) |
| return -ENOMEM; |
| |
| break; |
| |
| case PATTERN_SHA256SUM: { |
| _cleanup_free_ char *h = NULL; |
| |
| if (!fields->sha256sum_set) |
| return -ENXIO; |
| |
| h = hexmem(fields->sha256sum, sizeof(fields->sha256sum)); |
| if (!h) |
| return -ENOMEM; |
| |
| if (!strextend(&j, h)) |
| return -ENOMEM; |
| |
| break; |
| } |
| |
| default: |
| assert_not_reached(); |
| } |
| } |
| |
| *ret = TAKE_PTR(j); |
| return 0; |
| } |