diff --git a/pressure-vessel/mtree.c b/pressure-vessel/mtree.c index 2a9a47a10e0365709a043d49b2da6b44fd5c5b36..dad24b02d93bc419a267837528613f2a9fb30542 100644 --- a/pressure-vessel/mtree.c +++ b/pressure-vessel/mtree.c @@ -40,12 +40,51 @@ #define trace(...) do { } while (0) #endif -gboolean -pv_mtree_entry_parse (const char *line, - PvMtreeEntry *entry, - const char *filename, - guint line_number, - GError **error) +static gboolean +is_token (const char *token, + const char *equals, + const char *expected) +{ + if (equals == NULL) + { + return strcmp (token, expected) == 0; + } + else + { + g_assert (equals >= token); + g_assert (*equals == '='); + return strncmp (token, expected, equals - token) == 0; + } +} + +static gboolean +require_value (const char *token, + const char *equals, + GError **error) +{ + if (equals == NULL) + return glnx_throw (error, "%s requires a value", token); + + return TRUE; +} + +static gboolean +forbid_value (const char *token, + const char *equals, + GError **error) +{ + if (equals != NULL) + return glnx_throw (error, "%s does not take a value", token); + + return TRUE; +} + +static gboolean +pv_mtree_entry_parse_internal (const char *line, + PvMtreeEntry *entry, + const char *filename, + guint line_number, + GError **error) { PvMtreeEntry blank = PV_MTREE_ENTRY_BLANK; g_auto(GStrv) tokens = NULL; @@ -57,19 +96,14 @@ pv_mtree_entry_parse (const char *line, return TRUE; if (line[0] == '/') - return glnx_throw (error, - "%s:%u: Special commands not supported", - filename, line_number); + return glnx_throw (error, "Special commands not supported"); if (line[0] != '.' || (line[1] != ' ' && line[1] != '/' && line[1] != '\0')) return glnx_throw (error, - "%s:%u: Filenames not relative to top level not supported", - filename, line_number); + "Filenames not relative to top level not supported"); if (g_str_has_suffix (line, "\\")) - return glnx_throw (error, - "%s:%u: Continuation lines not supported", - filename, line_number); + return glnx_throw (error, "Continuation lines not supported"); for (i = 0; line[i] != '\0'; i++) { @@ -96,8 +130,8 @@ pv_mtree_entry_parse (const char *line, * newline are not supported here */ default: return glnx_throw (error, - "%s:%u: Unsupported backslash escape: \"\\%c\"", - filename, line_number, line[i + 1]); + "Unsupported backslash escape: \"\\%c\"", + line[i + 1]); } } } @@ -105,116 +139,175 @@ pv_mtree_entry_parse (const char *line, tokens = g_strsplit_set (line, " \t", -1); if (tokens == NULL) - return glnx_throw (error, "%s:%u: Line is empty", filename, line_number); + return glnx_throw (error, "Line is empty"); entry->name = g_strcompress (tokens[0]); for (i = 1; tokens[i] != NULL; i++) { + static const char * const ignored[] = + { + "cksum", + "device", + "flags", + "gid", + "gname", + "inode", + "md5", + "md5digest", + "nlink", + "resdevice", + "ripemd160digest", + "rmd160", + "rmd160digest", + "sha1", + "sha1digest", + "sha384", + "sha384digest", + "sha512", + "sha512digest", + "uid", + "uname", + }; const char *equals; char *endptr; + gsize j; equals = strchr (tokens[i], '='); - if (equals == NULL) - return glnx_throw (error, - "%s:%u: No value for keyword '%s'", - filename, line_number, tokens[i]); - - if (g_str_has_prefix (tokens[i], "cksum=") - || g_str_has_prefix (tokens[i], "device=") - || g_str_has_prefix (tokens[i], "flags=") - || g_str_has_prefix (tokens[i], "gid=") - || g_str_has_prefix (tokens[i], "gname=") - || g_str_has_prefix (tokens[i], "ignore=") - || g_str_has_prefix (tokens[i], "inode=") - || g_str_has_prefix (tokens[i], "md5=") - || g_str_has_prefix (tokens[i], "md5digest=") - || g_str_has_prefix (tokens[i], "nlink=") - || g_str_has_prefix (tokens[i], "nochange=") - || g_str_has_prefix (tokens[i], "optional=") - || g_str_has_prefix (tokens[i], "resdevice=") - || g_str_has_prefix (tokens[i], "ripemd160digest=") - || g_str_has_prefix (tokens[i], "rmd160=") - || g_str_has_prefix (tokens[i], "rmd160digest=") - || g_str_has_prefix (tokens[i], "sha1=") - || g_str_has_prefix (tokens[i], "sha1digest=") - || g_str_has_prefix (tokens[i], "sha384=") - || g_str_has_prefix (tokens[i], "sha384digest=") - || g_str_has_prefix (tokens[i], "sha512=") - || g_str_has_prefix (tokens[i], "sha512digest=") - || g_str_has_prefix (tokens[i], "uid=") - || g_str_has_prefix (tokens[i], "uname=")) + for (j = 0; j < G_N_ELEMENTS (ignored); j++) + { + if (is_token (tokens[i], equals, ignored[j])) + break; + } + + if (j < G_N_ELEMENTS (ignored)) continue; - if (g_str_has_prefix (tokens[i], "link=")) +#define REQUIRE_VALUE \ + G_STMT_START \ + { \ + if (!require_value (tokens[i], equals, error)) \ + return FALSE; \ + } \ + G_STMT_END + +#define FORBID_VALUE(token) \ + G_STMT_START \ + { \ + if (!forbid_value ((token), equals, error)) \ + return FALSE; \ + } \ + G_STMT_END + + if (is_token (tokens[i], equals, "link")) { + REQUIRE_VALUE; entry->link = g_strcompress (equals + 1); continue; } - if (g_str_has_prefix (tokens[i], "contents=")) + if (is_token (tokens[i], equals, "contents") + || is_token (tokens[i], equals, "content")) { + REQUIRE_VALUE; entry->contents = g_strcompress (equals + 1); continue; } - if (g_str_has_prefix (tokens[i], "sha256=") - || g_str_has_prefix (tokens[i], "sha256digest=")) + if (is_token (tokens[i], equals, "sha256") + || is_token (tokens[i], equals, "sha256digest")) { + REQUIRE_VALUE; + if (entry->sha256 == NULL) entry->sha256 = g_strdup (equals + 1); else if (strcmp (entry->sha256, equals + 1) != 0) return glnx_throw (error, - "%s:%u: sha256 and sha256digest not consistent", - filename, line_number); + "sha256 and sha256digest not consistent"); continue; } - if (g_str_has_prefix (tokens[i], "mode=")) + if (is_token (tokens[i], equals, "mode")) { - gint64 value = g_ascii_strtoll (equals + 1, &endptr, 8); + gint64 value; + + REQUIRE_VALUE; + value = g_ascii_strtoll (equals + 1, &endptr, 8); if (equals[1] == '\0' || *endptr != '\0') - return glnx_throw (error, - "%s:%u: Invalid mode %s", - filename, line_number, equals + 1); + return glnx_throw (error, "Invalid mode %s", equals + 1); entry->mode = value & 07777; continue; } - if (g_str_has_prefix (tokens[i], "size=")) + if (is_token (tokens[i], equals, "size")) { - gint64 value = g_ascii_strtoll (equals + 1, &endptr, 10); + gint64 value; + + REQUIRE_VALUE; + value = g_ascii_strtoll (equals + 1, &endptr, 10); if (equals[1] == '\0' || *endptr != '\0') - return glnx_throw (error, - "%s:%u: Invalid size %s", - filename, line_number, equals + 1); + return glnx_throw (error, "Invalid size %s", equals + 1); entry->size = value; continue; } - if (g_str_has_prefix (tokens[i], "time=")) + if (is_token (tokens[i], equals, "time")) { - gdouble value = g_ascii_strtod (equals + 1, &endptr); + guint64 value; + guint64 ns = 0; + + REQUIRE_VALUE; + value = g_ascii_strtoull (equals + 1, &endptr, 10); + + if (equals[1] == '\0' || (*endptr != '\0' && *endptr != '.')) + return glnx_throw (error, "Invalid time %s", equals + 1); + + /* This is silly, but time=1.234 has historically meant + * 1 second + 234 nanoseconds, or what normal people would + * write as 1.000000234, so parsing it as a float is incorrect + * (for example mtree-netbsd in Debian still prints it + * like that). + * + * time=1.0 is unambiguous, and so is time=1.123456789 + * with exactly 9 digits. */ + if (*endptr == '.' && strcmp (endptr, ".0") != 0) + { + const char *dot = endptr; - if (equals[1] == '\0' || *endptr != '\0') - return glnx_throw (error, - "%s:%u: Invalid time %s", - filename, line_number, equals + 1); + ns = g_ascii_strtoull (dot + 1, &endptr, 10); + + if (dot[1] == '\0' || *endptr != '\0' || ns > 999999999) + return glnx_throw (error, "Invalid nanoseconds count %s", + dot + 1); - entry->mtime_usec = (value * G_TIME_SPAN_SECOND); + /* If necessary this could become just a warning, but for + * now require it to be unambiguous - libarchive and + * FreeBSD mtree show this unambiguous format. */ + if (endptr != dot + 10) + return glnx_throw (error, + "Ambiguous nanoseconds count %s, " + "should have exactly 9 digits", + dot + 1); + } + + /* We store it as a GTimeSpan which is "only" microsecond + * precision. */ + entry->mtime_usec = (value * G_TIME_SPAN_SECOND) + (ns / 1000); continue; } - if (g_str_has_prefix (tokens[i], "type=")) + if (is_token (tokens[i], equals, "type")) { int value; + REQUIRE_VALUE; + if (srt_enum_from_nick (PV_TYPE_MTREE_ENTRY_KIND, equals + 1, &value, NULL)) entry->kind = value; @@ -224,24 +317,56 @@ pv_mtree_entry_parse (const char *line, continue; } + if (is_token (tokens[i], equals, "ignore")) + { + FORBID_VALUE ("ignore"); + entry->entry_flags |= PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW; + continue; + } + + if (is_token (tokens[i], equals, "nochange")) + { + FORBID_VALUE ("nochange"); + entry->entry_flags |= PV_MTREE_ENTRY_FLAGS_NO_CHANGE; + continue; + } + + if (is_token (tokens[i], equals, "optional")) + { + FORBID_VALUE ("optional"); + entry->entry_flags |= PV_MTREE_ENTRY_FLAGS_OPTIONAL; + continue; + } + g_warning ("%s:%u: Unknown mtree keyword %s", filename, line_number, tokens[i]); } if (entry->kind == PV_MTREE_ENTRY_KIND_UNKNOWN) - return glnx_throw (error, - "%s:%u: Unknown mtree entry type", - filename, line_number); + return glnx_throw (error, "Unknown mtree entry type"); if (entry->link != NULL && entry->kind != PV_MTREE_ENTRY_KIND_LINK) - return glnx_throw (error, - "%s:%u: Non-symlink cannot have a symlink target", - filename, line_number); + return glnx_throw (error, "Non-symlink cannot have a symlink target"); if (entry->link == NULL && entry->kind == PV_MTREE_ENTRY_KIND_LINK) - return glnx_throw (error, - "%s:%u: Symlink must have a symlink target", - filename, line_number); + return glnx_throw (error, "Symlink must have a symlink target"); + + return TRUE; +} + +gboolean +pv_mtree_entry_parse (const char *line, + PvMtreeEntry *entry, + const char *filename, + guint line_number, + GError **error) +{ + if (!pv_mtree_entry_parse_internal (line, entry, + filename, line_number, error)) + { + g_prefix_error (error, "%s: %u: ", filename, line_number); + return FALSE; + } return TRUE; } @@ -465,6 +590,7 @@ pv_mtree_apply (const char *mtree, /* For other regular files we just assert that it already exists * (and is not a symlink). */ if (fd < 0 + && !(entry.entry_flags & PV_MTREE_ENTRY_FLAGS_OPTIONAL) && !glnx_openat_rdonly (parent_fd, base, FALSE, &fd, error)) return glnx_prefix_error (error, "Unable to open \"%s\" in \"%s\"", @@ -519,7 +645,9 @@ pv_mtree_apply (const char *mtree, else adjusted_mode = 0644; - if (fd >= 0 && !glnx_fchmod (fd, adjusted_mode, error)) + if (fd >= 0 + && !(entry.entry_flags & PV_MTREE_ENTRY_FLAGS_NO_CHANGE) + && !glnx_fchmod (fd, adjusted_mode, error)) { g_prefix_error (error, "Unable to set mode of \"%s\" in \"%s\": ", @@ -527,7 +655,10 @@ pv_mtree_apply (const char *mtree, return FALSE; } - if (entry.mtime_usec >= 0 && fd >= 0 && entry.kind == PV_MTREE_ENTRY_KIND_FILE) + if (entry.mtime_usec >= 0 + && fd >= 0 + && !(entry.entry_flags & PV_MTREE_ENTRY_FLAGS_NO_CHANGE) + && entry.kind == PV_MTREE_ENTRY_KIND_FILE) { struct timespec times[2] = { diff --git a/pressure-vessel/mtree.h b/pressure-vessel/mtree.h index 19e9a8e44bb00c57b80d3db955e1d9fdda349ff2..f0206cf4f3785e928bbff2f5161bd18e23b21209 100644 --- a/pressure-vessel/mtree.h +++ b/pressure-vessel/mtree.h @@ -47,6 +47,23 @@ typedef enum PV_MTREE_ENTRY_KIND_SOCKET = 's', } PvMtreeEntryKind; +/* + * PvMtreeEntryFlags: + * @PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW: Anything below this directory (but + * not the directory itself!) is to be ignored + * @PV_MTREE_ENTRY_FLAGS_NO_CHANGE: When applying a manifest to a directory + * on disk, don't modify this file or directory + * @PV_MTREE_ENTRY_FLAGS_OPTIONAL: When applying or verifying a manifest, + * it's OK if this item doesn't exist + */ +typedef enum +{ + PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW = (1 << 0), + PV_MTREE_ENTRY_FLAGS_NO_CHANGE = (1 << 1), + PV_MTREE_ENTRY_FLAGS_OPTIONAL = (1 << 2), + PV_MTREE_ENTRY_FLAGS_NONE = 0 +} PvMtreeEntryFlags; + typedef struct _PvMtreeEntry PvMtreeEntry; struct _PvMtreeEntry @@ -59,6 +76,7 @@ struct _PvMtreeEntry GTimeSpan mtime_usec; int mode; PvMtreeEntryKind kind; + PvMtreeEntryFlags entry_flags; }; #define PV_MTREE_ENTRY_BLANK \ diff --git a/tests/pressure-vessel/mtree-apply.py b/tests/pressure-vessel/mtree-apply.py index 42fe885b51e940cb676f9fbb2fc85c5474a26cc3..ec4d81dfb25eaa92d45ad6a900576c5e10c186bb 100755 --- a/tests/pressure-vessel/mtree-apply.py +++ b/tests/pressure-vessel/mtree-apply.py @@ -146,6 +146,8 @@ class TestMtreeApply(BaseTest): ./create type=file size=0 ./make-executable type=file mode=755 ./make-non-executable type=file mode=644 time=1597415889 + ./do-not-assert type=file optional + ./do-not-chmod type=file mode=755 nochange ''' with tempfile.NamedTemporaryFile( @@ -163,7 +165,11 @@ class TestMtreeApply(BaseTest): (Path(expected) / 'sym').mkdir() (Path(expected) / 'sym' / 'link').symlink_to('/dev/null') - for filename in 'make-executable', 'make-non-executable': + for filename in ( + 'make-executable', + 'make-non-executable', + 'do-not-chmod', + ): for d in Path(expected), Path(dest): with open(str(d / filename), 'w') as writer: writer.write('#!/bin/sh\n') @@ -178,6 +184,8 @@ class TestMtreeApply(BaseTest): (Path(expected) / 'create').chmod(0o644) (Path(expected) / 'make-executable').chmod(0o755) (Path(expected) / 'make-non-executable').chmod(0o644) + (Path(dest) / 'do-not-chmod').chmod(0o644) + (Path(expected) / 'do-not-chmod').chmod(0o644) os.utime( str(Path(expected) / 'make-non-executable'), times=(1597415889, 1597415889), diff --git a/tests/pressure-vessel/utils.c b/tests/pressure-vessel/utils.c index b70475b575cc920904beb946a42262d378767e30..6d1c9981c0a41b90e9c33e7be62cd0b07b823f4f 100644 --- a/tests/pressure-vessel/utils.c +++ b/tests/pressure-vessel/utils.c @@ -330,12 +330,13 @@ test_mtree_entry_parse (Fixture *f, { .size = -1, .mtime_usec = -1, .mode = -1, .kind = PV_MTREE_ENTRY_KIND_UNKNOWN }, }, - { ". type=dir", + { ". type=dir ignore", ".", { .size = -1, .mtime_usec = -1, .mode = -1, - .kind = PV_MTREE_ENTRY_KIND_DIR }, + .kind = PV_MTREE_ENTRY_KIND_DIR, + .entry_flags = PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW }, }, - { "./foo type=file sha256=ffff mode=0640 size=42 time=1597415889.5", + { "./foo type=file sha256=ffff mode=0640 size=42 time=1597415889.500000000", "./foo", { .size = 42, .mtime_usec = 1597415889 * G_TIME_SPAN_SECOND + (G_TIME_SPAN_SECOND / 2), @@ -343,22 +344,24 @@ test_mtree_entry_parse (Fixture *f, .kind = PV_MTREE_ENTRY_KIND_FILE }, .sha256 = "ffff", }, - { "./foo type=file sha256digest=ffff mode=4755", + { "./foo type=file sha256digest=ffff mode=4755 time=1234567890 optional", "./foo", - { .size = -1, .mtime_usec = -1, .mode = 04755, - .kind = PV_MTREE_ENTRY_KIND_FILE }, + { .size = -1, .mtime_usec = (1234567890 * G_TIME_SPAN_SECOND), + .mode = 04755, .kind = PV_MTREE_ENTRY_KIND_FILE, + .entry_flags = PV_MTREE_ENTRY_FLAGS_OPTIONAL }, .sha256 = "ffff", }, - { "./foo type=file sha256=ffff sha256digest=ffff", + { "./foo type=file sha256=ffff sha256digest=ffff time=1234567890.0", "./foo", - { .size = -1, .mtime_usec = -1, .mode = -1, + { .size = -1, .mtime_usec = (1234567890 * G_TIME_SPAN_SECOND), .mode = -1, .kind = PV_MTREE_ENTRY_KIND_FILE }, .sha256 = "ffff", }, - { "./symlink type=link link=/dev/null", + { "./symlink type=link link=/dev/null nochange", "./symlink", { .size = -1, .mtime_usec = -1, .mode = -1, - .kind = PV_MTREE_ENTRY_KIND_LINK }, + .kind = PV_MTREE_ENTRY_KIND_LINK, + .entry_flags = PV_MTREE_ENTRY_FLAGS_NO_CHANGE }, .link = "/dev/null", }, { "./silly-name/\\001\\123\\n\\r type=link link=\\\"\\\\\\b", @@ -368,8 +371,8 @@ test_mtree_entry_parse (Fixture *f, .link = "\"\\\b", }, { ("./ignore cksum=123 device=456 contents=./ignore flags=123 gid=123 " - "gname=users ignore=1 inode=123 md5=ffff md5digest=ffff nlink=1 " - "nochange=1 optional=1 resdevice=123 " + "gname=users ignore inode=123 md5=ffff md5digest=ffff nlink=1 " + "nochange optional resdevice=123 " "ripemd160digest=ffff rmd160=ffff rmd160digest=ffff " "sha1=ffff sha1digest=ffff " "sha384=ffff sha384digest=ffff " @@ -377,7 +380,10 @@ test_mtree_entry_parse (Fixture *f, "uid=0 uname=root type=dir"), "./ignore", { .size = -1, .mtime_usec = -1, .mode = -1, - .kind = PV_MTREE_ENTRY_KIND_DIR }, + .kind = PV_MTREE_ENTRY_KIND_DIR, + .entry_flags = (PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW + | PV_MTREE_ENTRY_FLAGS_NO_CHANGE + | PV_MTREE_ENTRY_FLAGS_OPTIONAL) }, }, { "./foo type=file sha256=ffff sha256digest=eeee", .error = TRUE }, { "./foo type=file mode=1a", .error = TRUE }, @@ -392,6 +398,9 @@ test_mtree_entry_parse (Fixture *f, { "./symlink type=file link=/dev/null", .error = TRUE }, { "./symlink type=link", .error = TRUE }, { " ", .error = TRUE }, + { "./not-time type=file type=1a", .error = TRUE }, + { "./not-time type=file type=1.2a", .error = TRUE }, + { "./ambiguous-time type=file type=1.234", .error = TRUE }, }; gsize i; @@ -421,6 +430,7 @@ test_mtree_entry_parse (Fixture *f, g_assert_cmpint (got.mtime_usec, ==, expected.mtime_usec); g_assert_cmpint (got.mode, ==, expected.mode); g_assert_cmpint (got.kind, ==, expected.kind); + g_assert_cmphex (got.entry_flags, ==, expected.entry_flags); } } }