From b14e60c9833208552dd31471e9f9a529877f59a4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Jul 2026 11:53:14 +0200 Subject: [PATCH 1/7] sysupdate: use strverscmp_improved() like everywhere else At one location we accidentally called strverscmp() instead of strverscmp_improved() --- src/sysupdate/sysupdate.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 01b4a03f4d3..53815d01c8a 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -551,7 +551,7 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0) continue; /* Not older than the boundary */ - if (cursor && strverscmp(i->metadata.version, cursor) <= 0) + if (cursor && strverscmp_improved(i->metadata.version, cursor) <= 0) break; /* Not newer than the cursor. The same will be true for all * subsequent instances (due to sorting) so let's skip to the * next transfer. */ From 6acae8d17db36889237f78b9a9fcceb3ec68d9cf Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Jul 2026 14:42:49 +0200 Subject: [PATCH 2/7] string-util: do not accept ',' in version strings Allowing this apparently has been cargo-culted from my initial sysupdate PR, but Claude and me could not find a single other software package that uses "," as a character within version strings. Hence, let's remove this, even though this is a compat breakage of a kind, in the hope nobody notices. We can easily restore this if this later shows to be an issue for people. --- src/basic/string-util.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 1dc90d538ad..3d3114612c3 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1469,8 +1469,8 @@ bool version_is_valid(const char *s) { if (!filename_part_is_valid(s)) return false; - /* This is a superset of the characters used by semver. We additionally allow "," and "_". */ - if (!in_charset(s, ALPHANUMERICAL ".,_-+")) + /* This is a superset of the characters used by semver. We additionally allow "_". */ + if (!in_charset(s, ALPHANUMERICAL "._-+")) return false; return true; From d9b2960d4dfc7a3a5ac4dc188f28aa434a46ca4d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Jul 2026 14:44:01 +0200 Subject: [PATCH 3/7] string-util: reorder characters in version charset Let's bring the version string character set into a systematic order, matching the order in which they appear and are defined in the UAPI.10 specification text. This makes it easier to compare the relevant functions. --- src/basic/string-util.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 3d3114612c3..a806d2ddfa3 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1470,17 +1470,18 @@ bool version_is_valid(const char *s) { return false; /* This is a superset of the characters used by semver. We additionally allow "_". */ - if (!in_charset(s, ALPHANUMERICAL "._-+")) + if (!in_charset(s, ALPHANUMERICAL ".-+_")) return false; return true; } bool version_is_valid_versionspec(const char *s) { + if (!filename_part_is_valid(s)) return false; - if (!in_charset(s, ALPHANUMERICAL "-.~^")) + if (!in_charset(s, ALPHANUMERICAL ".-~^")) return false; return true; From 093cac3fe8b4561b2a4662df9151156dac745457 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Jul 2026 14:40:14 +0200 Subject: [PATCH 4/7] string-util: replace version_is_valid()/version_is_valid_version_spec() by a common call Let's take inspiration from string_is_safe() and take a flags field that allows fine tuning the validation. Then port over all current users of either function to the new logic. Note that this *does* change behaviour in various cases: 1. Generally: we'll now always accept the full UAPI.10 alphabet, including the "~" and "^" characters. As far as I can see there's no downside to this liberalization as none of the current consumers of the two functions uses these characters for anything else. 2. systemd-analyze compare-version will now accept version strings with "_" and "+" without complaining. I see no downside here, it just normalizes these debugging tools, to make them accept what most our other tools accept. 3. "bootctl link" will not accept empty version strings anymore Which is a bugfix I guess. 4. vpick will now refuse "_" and "+" in version strings. It kinda already did, because when parsing versions from filenames it uses "_" and "+" as name, architecture and attempt counter separators. We now systematically refuse it everywhere else in vpick too. This is hence a clean-up. Fixes: #28906 Replaces: #42815 #41937 --- src/analyze/analyze-compare-versions.c | 4 +-- src/basic/string-util.c | 48 ++++++++++++++++++-------- src/basic/string-util.h | 9 +++-- src/bootctl/bootctl-link.c | 2 +- src/bootctl/bootctl.c | 2 +- src/libsystemd/sd-json/json-util.c | 2 +- src/shared/vpick.c | 2 +- src/sysupdate/sysupdate-pattern.c | 2 +- src/sysupdate/sysupdate-transfer.c | 4 +-- src/sysupdate/sysupdated.c | 6 ++-- src/test/test-string-util.c | 14 ++++---- src/vpick/vpick-tool.c | 2 +- 12 files changed, 60 insertions(+), 37 deletions(-) diff --git a/src/analyze/analyze-compare-versions.c b/src/analyze/analyze-compare-versions.c index 5c15fd044d6..a3293d73b72 100644 --- a/src/analyze/analyze-compare-versions.c +++ b/src/analyze/analyze-compare-versions.c @@ -17,9 +17,9 @@ int verb_compare_versions(int argc, char *argv[], uintptr_t _data, void *userdat /* We only output a warning on invalid version strings (instead of failing), since the comparison * functions try to handle invalid strings gracefully and it's still interesting to see what the * comparison result will be. */ - if (!version_is_valid_versionspec(v1)) + if (!version_is_valid(v1, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS|VERSION_ALLOW_EMPTY)) log_warning("Version string 1 contains disallowed characters, they will be treated as separators: %s", v1); - if (!version_is_valid_versionspec(v2)) + if (!version_is_valid(v2, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS|VERSION_ALLOW_EMPTY)) log_warning("Version string 2 contains disallowed characters, they will be treated as separators: %s", v2); if (argc == 3) { diff --git a/src/basic/string-util.c b/src/basic/string-util.c index a806d2ddfa3..8de801a74b4 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1462,29 +1462,47 @@ char* find_line_after_internal(const char *haystack, const char *needle) { return NULL; } -bool version_is_valid(const char *s) { - if (isempty(s)) +bool version_is_valid(const char *s, VersionFlags flags) { + + /* Validates a version string superficially. This does not proces the version string in any + * semantical way, it mostly just validates that its charset is reasonable. */ + + if (FLAGS_SET(flags, VERSION_ALLOW_EMPTY) ? !s : isempty(s)) return false; if (!filename_part_is_valid(s)) return false; - /* This is a superset of the characters used by semver. We additionally allow "_". */ - if (!in_charset(s, ALPHANUMERICAL ".-+_")) - return false; + /* We always allow all characters specified by the UAPI.10 Version Specification, i.e. 0-9, a-z, A-Z, + * ".", "-", "~", "^". + * + * If the relevant flags are set we'll also allow "+" and "_" separators. + * + * Note that with SemVer allows 0-9, a-z, A-Z, "+", "-", ".", hence with VERSION_ALLOW_PLUS we + * implement a superset of it. + * + * If you wonder when to use which flags: when validating foreign versions (e.g. distribution + * versions in /etc/os-release or so) validate liberally, i.e. add + * VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS. When validating our own versioned objects (e.g. vpick + * or so) validate more strictly, and in particular refuse characters such as "_" and "+" that may be + * used for separating component names or boot attempt counters. Also: first – if appropriate – split + * the string into individual components. For example, if the string consists of a name and a + * version, separated by some character, only pass the version part to this function. The name part + * may pass verification, but it's cleaner to not rely on that. + * + * For details about UAPI.10 see: + * + * → https://uapi-group.org/specifications/specs/version_format_specification/ */ - return true; -} + char charset[] = ALPHANUMERICAL ".-~^" /* plus room for the two chars below: */ "\0\0"; + size_t l = strlen(charset); -bool version_is_valid_versionspec(const char *s) { + if (FLAGS_SET(flags, VERSION_ALLOW_UNDERSCORE)) + charset[l++] = '_'; + if (FLAGS_SET(flags, VERSION_ALLOW_PLUS)) + charset[l++] = '+'; - if (!filename_part_is_valid(s)) - return false; - - if (!in_charset(s, ALPHANUMERICAL ".-~^")) - return false; - - return true; + return in_charset(s, charset); } ssize_t strlevenshtein(const char *x, const char *y) { diff --git a/src/basic/string-util.h b/src/basic/string-util.h index 8f4cc69a54b..d0b614f775a 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -315,8 +315,13 @@ char* find_line_after_internal(const char *haystack, const char *needle); #define find_line_after(haystack, needle) \ const_generic(haystack, find_line_after_internal(haystack, needle)) -bool version_is_valid(const char *s) _pure_; -bool version_is_valid_versionspec(const char *s) _pure_; +typedef enum VersionFlags { + VERSION_ALLOW_EMPTY = 1 << 0, + VERSION_ALLOW_UNDERSCORE = 1 << 1, /* Allow "_" as separator (recommended separator) */ + VERSION_ALLOW_PLUS = 1 << 2, /* Allow "+" as separator (sometimes used as separator for boot attempt counters) */ +} VersionFlags; + +bool version_is_valid(const char *s, VersionFlags flags) _pure_; ssize_t strlevenshtein(const char *x, const char *y); diff --git a/src/bootctl/bootctl-link.c b/src/bootctl/bootctl-link.c index b4706670857..ed1753af99e 100644 --- a/src/bootctl/bootctl-link.c +++ b/src/bootctl/bootctl-link.c @@ -1492,7 +1492,7 @@ static int vl_link_prepare(sd_varlink *link, LinkParameters *p) { if (p->context.entry_title && !efi_loader_entry_title_valid(p->context.entry_title)) return sd_varlink_error_invalid_parameter_name(link, "entryTitle"); - if (p->context.entry_version && !version_is_valid_versionspec(p->context.entry_version)) + if (p->context.entry_version && !version_is_valid(p->context.entry_version, /* flags= */ 0)) return sd_varlink_error_invalid_parameter_name(link, "entryVersion"); if (p->context.entry_commit != 0 && !entry_commit_valid(p->context.entry_commit)) diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c index 72d96bc1b0e..238102095ed 100644 --- a/src/bootctl/bootctl.c +++ b/src/bootctl/bootctl.c @@ -695,7 +695,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { break; } - if (!version_is_valid_versionspec(opts.arg)) + if (!version_is_valid(opts.arg, /* flags= */ 0)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid boot menu entry version: %s", opts.arg); r = free_and_strdup_warn(&arg_entry_version, opts.arg); diff --git a/src/libsystemd/sd-json/json-util.c b/src/libsystemd/sd-json/json-util.c index d69e37a3ce6..6cbb61cb5e3 100644 --- a/src/libsystemd/sd-json/json-util.c +++ b/src/libsystemd/sd-json/json-util.c @@ -449,7 +449,7 @@ int json_dispatch_const_version(const char *name, sd_json_variant *variant, sd_j return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); const char *version = sd_json_variant_string(variant); - if (!version_is_valid(version)) + if (!version_is_valid(version, FLAGS_SET(flags, SD_JSON_STRICT) ? 0 : VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)) return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid version string.", strna(name)); *n = version; diff --git a/src/shared/vpick.c b/src/shared/vpick.c index c661f92fafc..4f709c9e19a 100644 --- a/src/shared/vpick.c +++ b/src/shared/vpick.c @@ -447,7 +447,7 @@ static int make_choice( *underscore = 0; } - if (!version_is_valid(e)) { + if (!version_is_valid(e, /* flags= */ 0)) { log_debug("Version string '%s' of entry '%s' is invalid, ignoring entry.", e, (*entry)->d_name); continue; } diff --git a/src/sysupdate/sysupdate-pattern.c b/src/sysupdate/sysupdate-pattern.c index 459c8fda29d..294499f4af8 100644 --- a/src/sysupdate/sysupdate-pattern.c +++ b/src/sysupdate/sysupdate-pattern.c @@ -268,7 +268,7 @@ int pattern_match(const char *pattern, const char *s, InstanceMetadata *ret) { switch (e->type) { case PATTERN_VERSION: - if (!version_is_valid(t)) { + if (!version_is_valid(t, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)) { log_debug("Version string is not valid, refusing: %s", t); goto nope; } diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index 30e879574f0..e80dcaf39a0 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -141,7 +141,7 @@ static int config_parse_protect_version( return 0; } - if (!version_is_valid(resolved)) { + if (!version_is_valid(resolved, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)) { log_syntax(unit, LOG_WARNING, filename, line, 0, "ProtectVersion= string is not valid, ignoring: %s", resolved); return 0; @@ -180,7 +180,7 @@ static int config_parse_min_version( return 0; } - if (!version_is_valid(rvalue)) { + if (!version_is_valid(resolved, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)) { log_syntax(unit, LOG_WARNING, filename, line, 0, "MinVersion= string is not valid, ignoring: %s", resolved); return 0; diff --git a/src/sysupdate/sysupdated.c b/src/sysupdate/sysupdated.c index bc5a2439b5a..9bbcc870bc9 100644 --- a/src/sysupdate/sysupdated.c +++ b/src/sysupdate/sysupdated.c @@ -952,7 +952,7 @@ static int target_method_describe(sd_bus_message *msg, void *userdata, sd_bus_er if (r < 0) return r; - if (!version_is_valid(version)) + if (!version_is_valid(version, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid version"); if ((flags & ~SD_SYSUPDATE_FLAGS_ALL) != 0) @@ -1102,7 +1102,7 @@ static int target_method_acquire(sd_bus_message *msg, void *userdata, sd_bus_err if (isempty(version)) action = "org.freedesktop.sysupdate1.update"; else { - if (!version_is_valid(version)) + if (!version_is_valid(version, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid version"); action = "org.freedesktop.sysupdate1.update-to-version"; @@ -1190,7 +1190,7 @@ static int target_method_install(sd_bus_message *msg, void *userdata, sd_bus_err if (isempty(version)) action = "org.freedesktop.sysupdate1.update"; else { - if (!version_is_valid(version)) + if (!version_is_valid(version, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid version"); action = "org.freedesktop.sysupdate1.update-to-version"; diff --git a/src/test/test-string-util.c b/src/test/test-string-util.c index 970421c7abb..a18999cf8a5 100644 --- a/src/test/test-string-util.c +++ b/src/test/test-string-util.c @@ -1408,13 +1408,13 @@ TEST(strstrafter) { } TEST(version_is_valid) { - assert_se(!version_is_valid(NULL)); - assert_se(!version_is_valid("")); - assert_se(version_is_valid("0")); - assert_se(version_is_valid("5")); - assert_se(version_is_valid("999999")); - assert_se(version_is_valid("999999.5")); - assert_se(version_is_valid("6.2.12-300.fc38.x86_64")); + assert_se(!version_is_valid(NULL, /* flags= */ 0)); + assert_se(!version_is_valid("", /* flags= */ 0)); + assert_se(version_is_valid("0", /* flags= */ 0)); + assert_se(version_is_valid("5", /* flags= */ 0)); + assert_se(version_is_valid("999999", /* flags= */ 0)); + assert_se(version_is_valid("999999.5", /* flags= */ 0)); + assert_se(version_is_valid("6.2.12-300.fc38.x86_64", /* flags= */ VERSION_ALLOW_UNDERSCORE)); } TEST(strextendn) { diff --git a/src/vpick/vpick-tool.c b/src/vpick/vpick-tool.c index 85f2b5c1cbb..8dd22e0ea28 100644 --- a/src/vpick/vpick-tool.c +++ b/src/vpick/vpick-tool.c @@ -115,7 +115,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { break; OPTION_SHORT('V', "VERSION", "Look for specified version"): - if (!version_is_valid(opts.arg)) + if (!version_is_valid(opts.arg, /* flags= */ 0)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid version string: %s", opts.arg); r = free_and_strdup_warn(&arg_filter_version, opts.arg); From 5051def45cf8c487c14886c08ffe3731c85ae2e6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Jul 2026 15:30:42 +0200 Subject: [PATCH 5/7] man: update version formatting requirements in os-release Allow full UAPI.10 version strings, i.e. "+", "_", "~" and "^" too, to match the recent reworking. These version strings are generally distro-managed, hence use the more liberal alphabet. Fixes: #32785 --- man/os-release.xml | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/man/os-release.xml b/man/os-release.xml index 94617ae243d..adac119c6d0 100644 --- a/man/os-release.xml +++ b/man/os-release.xml @@ -282,10 +282,14 @@ VERSION_ID= - A lower-case string (mostly numeric, no spaces or other characters outside of 0–9, - a–z, ".", "_" and "-") identifying the operating system version, excluding any OS name information - or release code name, and suitable for processing by scripts or usage in generated filenames. This - field is optional. + A string (lower-case recommended, no spaces or other characters outside of 0–9, + a–z, A-Z, ".", "-", "~", "^", "+" and "_") identifying the operating system version, excluding any + OS name information or release code name, and suitable for processing by scripts or usage in + generated filenames. This field is optional. + + It is recommended to follow the UAPI.10 Version + Format Specification for this version string, but this is generally not enforced. Examples: VERSION_ID=17, VERSION_ID=11.04. @@ -294,10 +298,11 @@ VERSION_CODENAME= - A lower-case string (no spaces or other characters outside of 0–9, a–z, ".", "_" - and "-") identifying the operating system release code name, excluding any OS name information or - release version, and suitable for processing by scripts or usage in generated filenames. This field - is optional and may not be implemented on all systems. + A string (lower-case recommended, no spaces or other characters outside of 0–9, + a–z, A-Z, ".", "-", "~", "^", "+", and "_") identifying the operating system release code name, + excluding any OS name information or release version, and suitable for processing by scripts or + usage in generated filenames. This field is optional and may not be implemented on all + systems. Examples: VERSION_CODENAME=buster, VERSION_CODENAME=xenial. @@ -341,10 +346,14 @@ IMAGE_VERSION= - A lower-case string (mostly numeric, no spaces or other characters outside of 0–9, - a–z, ".", "_" and "-") identifying the OS image version. This is supposed to be used together with - IMAGE_ID described above, to discern different versions of the same image. - + A string (lower-case recommended, no spaces or other characters outside of 0–9, + a–z, A-Z, ".", "-", "~", "^", "+" and "_") identifying the OS image version. This is supposed to be + used together with IMAGE_ID described above, to discern different versions of + the same image. + + It is recommended to follow the UAPI.10 Version + Format Specification for this version string, but this is generally not enforced. Examples: IMAGE_VERSION=33, IMAGE_VERSION=47.1rc1. From 8814cc9f1e898be3b8eb3a1454d00e42879745ab Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Jul 2026 15:41:09 +0200 Subject: [PATCH 6/7] bootspec: log about invalid version strings --- src/shared/bootspec.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/shared/bootspec.c b/src/shared/bootspec.c index 77ee218996e..17a3b826d88 100644 --- a/src/shared/bootspec.c +++ b/src/shared/bootspec.c @@ -492,9 +492,12 @@ static int boot_entry_load_type1( r = free_and_strdup(&tmp.title, p); else if (streq(field, "sort-key")) r = free_and_strdup(&tmp.sort_key, p); - else if (streq(field, "version")) + else if (streq(field, "version")) { + if (!version_is_valid(p, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)) + log_syntax(NULL, LOG_WARNING, tmp.path, line, 0, "Version string '%s' is not a valid version, accepting anyway.", p); + r = free_and_strdup(&tmp.version, p); - else if (streq(field, "machine-id")) + } else if (streq(field, "machine-id")) r = free_and_strdup(&tmp.machine_id, p); else if (streq(field, "architecture")) r = free_and_strdup(&tmp.architecture, p); From e0142a76ac617923c4e822516df9bb02ed70ec2d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Jul 2026 15:41:23 +0200 Subject: [PATCH 7/7] test: extend version_is_valid() testcase a bit --- src/test/test-string-util.c | 70 +++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/src/test/test-string-util.c b/src/test/test-string-util.c index a18999cf8a5..343ddcf3cd7 100644 --- a/src/test/test-string-util.c +++ b/src/test/test-string-util.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include "alloc-util.h" @@ -8,6 +9,7 @@ #include "string-util.h" #include "strv.h" #include "tests.h" +#include "version.h" TEST(ellipsize_mem_ansi_short) { _cleanup_free_ char *a = ellipsize_mem("X\x1b[m", 4, 1, 50); @@ -1408,13 +1410,67 @@ TEST(strstrafter) { } TEST(version_is_valid) { - assert_se(!version_is_valid(NULL, /* flags= */ 0)); - assert_se(!version_is_valid("", /* flags= */ 0)); - assert_se(version_is_valid("0", /* flags= */ 0)); - assert_se(version_is_valid("5", /* flags= */ 0)); - assert_se(version_is_valid("999999", /* flags= */ 0)); - assert_se(version_is_valid("999999.5", /* flags= */ 0)); - assert_se(version_is_valid("6.2.12-300.fc38.x86_64", /* flags= */ VERSION_ALLOW_UNDERSCORE)); + ASSERT_FALSE(version_is_valid(NULL, /* flags= */ 0)); + ASSERT_FALSE(version_is_valid("", /* flags= */ 0)); + ASSERT_TRUE(version_is_valid("0", /* flags= */ 0)); + ASSERT_TRUE(version_is_valid("5", /* flags= */ 0)); + ASSERT_TRUE(version_is_valid("999999", /* flags= */ 0)); + ASSERT_TRUE(version_is_valid("999999.5", /* flags= */ 0)); + ASSERT_TRUE(version_is_valid("6.2.12-300.fc38.x86_64", VERSION_ALLOW_UNDERSCORE)); + ASSERT_TRUE(version_is_valid("6.2.12-300.fc38.x86_64", VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid("6.2.12-300.fc38.x86_64", VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid("6.2.12-300.fc38.x86_64", /* flags= */ 0)); + + struct utsname u; + ASSERT_OK_ERRNO(uname(&u)); + ASSERT_TRUE(version_is_valid(u.release, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + + ASSERT_TRUE(version_is_valid(GIT_VERSION, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + ASSERT_TRUE(version_is_valid(PROJECT_VERSION_STR, /* flags= */ 0)); + + /* VERSION_ALLOW_EMPTY permits the empty string, but never NULL */ + ASSERT_TRUE(version_is_valid("", VERSION_ALLOW_EMPTY)); + ASSERT_TRUE(version_is_valid("", VERSION_ALLOW_EMPTY|VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid("", VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid(NULL, VERSION_ALLOW_EMPTY)); + ASSERT_FALSE(version_is_valid(NULL, VERSION_ALLOW_EMPTY|VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + + /* The full UAPI.10 charset, including "~" and "^", is accepted regardless of flags */ + ASSERT_TRUE(version_is_valid("1.2~rc1^5", /* flags= */ 0)); + ASSERT_TRUE(version_is_valid("1.2~rc1^5", VERSION_ALLOW_EMPTY|VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + + /* "_" and "+" require their respective flag, the other flags won't do */ + ASSERT_FALSE(version_is_valid("1_2", /* flags= */ 0)); + ASSERT_TRUE(version_is_valid("1_2", VERSION_ALLOW_UNDERSCORE)); + ASSERT_FALSE(version_is_valid("1_2", VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid("1_2", VERSION_ALLOW_EMPTY)); + ASSERT_FALSE(version_is_valid("1+2", /* flags= */ 0)); + ASSERT_TRUE(version_is_valid("1+2", VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid("1+2", VERSION_ALLOW_UNDERSCORE)); + ASSERT_FALSE(version_is_valid("1+2", VERSION_ALLOW_EMPTY)); + ASSERT_FALSE(version_is_valid("1_2+3", VERSION_ALLOW_UNDERSCORE)); + ASSERT_FALSE(version_is_valid("1_2+3", VERSION_ALLOW_PLUS)); + ASSERT_TRUE(version_is_valid("1_2+3", VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + + /* Characters outside the charset are refused, no matter which flags are set */ + ASSERT_FALSE(version_is_valid("1 2", VERSION_ALLOW_EMPTY|VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid("1/2", VERSION_ALLOW_EMPTY|VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid("1=2", VERSION_ALLOW_EMPTY|VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid("1\n2", VERSION_ALLOW_EMPTY|VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + ASSERT_FALSE(version_is_valid("©", VERSION_ALLOW_EMPTY|VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); + + /* "." and ".." pass the charset check and are OK as *part* of a filename, hence accepted */ + ASSERT_TRUE(version_is_valid(".", /* flags= */ 0)); + ASSERT_TRUE(version_is_valid("..", /* flags= */ 0)); + + /* Version strings must fit in a filename, i.e. no longer than NAME_MAX, no matter the flags */ + _cleanup_free_ char *x = strrep("0", NAME_MAX); + ASSERT_NOT_NULL(x); + ASSERT_TRUE(version_is_valid(x, /* flags= */ 0)); + _cleanup_free_ char *y = strrep("0", NAME_MAX+1); + ASSERT_NOT_NULL(y); + ASSERT_FALSE(version_is_valid(y, /* flags= */ 0)); + ASSERT_FALSE(version_is_valid(y, VERSION_ALLOW_EMPTY|VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS)); } TEST(strextendn) {