/* 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) {
return NULL;
}
-bool version_is_valid(const char *s) {
- if (isempty(s))
- return false;
+bool version_is_valid(const char *s, VersionFlags flags) {
- if (!filename_part_is_valid(s))
- return false;
+ /* 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. */
- /* This is a superset of the characters used by semver. We additionally allow "_". */
- if (!in_charset(s, ALPHANUMERICAL ".-+_"))
+ if (FLAGS_SET(flags, VERSION_ALLOW_EMPTY) ? !s : isempty(s))
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 ".-~^"))
- 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);
+
+ if (FLAGS_SET(flags, VERSION_ALLOW_UNDERSCORE))
+ charset[l++] = '_';
+ if (FLAGS_SET(flags, VERSION_ALLOW_PLUS))
+ charset[l++] = '+';
+
+ return in_charset(s, charset);
}
ssize_t strlevenshtein(const char *x, const char *y) {
#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);
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))
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);
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;
*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;
}
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;
}
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;
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;
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)
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";
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";
}
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) {
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);