From 16e20ba0b241860abe8800072703a5b8f6c4c999 Mon Sep 17 00:00:00 2001 From: Simon McVittie <smcv@collabora.com> Date: Tue, 1 Jun 2021 12:55:52 +0100 Subject: [PATCH] mtree: Parse the ignore, nochange and optional flags The nochange flag is fully implemented here: it suppresses changing the modification time or permissions. The optional flag is mostly implemented here: it suppresses the error that would ordinarily occur if a file with content does not already exist. The ignore flag is not yet implemented, only parsed, because it only really applies when verifying whether a filesystem tree matches a specification. Signed-off-by: Simon McVittie <smcv@collabora.com> --- pressure-vessel/mtree.c | 264 ++++++++++++++++++--------- pressure-vessel/mtree.h | 18 ++ tests/pressure-vessel/mtree-apply.py | 10 +- tests/pressure-vessel/utils.c | 25 ++- 4 files changed, 223 insertions(+), 94 deletions(-) diff --git a/pressure-vessel/mtree.c b/pressure-vessel/mtree.c index 9302492ea..dad24b02d 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,111 +139,134 @@ 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=") - || g_str_has_prefix (tokens[i], "content=")) + 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")) { 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, - "%s:%u: Invalid time %s", - filename, line_number, equals + 1); + 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 @@ -226,18 +283,17 @@ pv_mtree_entry_parse (const char *line, ns = g_ascii_strtoull (dot + 1, &endptr, 10); if (dot[1] == '\0' || *endptr != '\0' || ns > 999999999) - return glnx_throw (error, - "%s:%u: Invalid nanoseconds count %s", - filename, line_number, dot + 1); + return glnx_throw (error, "Invalid nanoseconds count %s", + dot + 1); /* 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, - "%s:%u: Ambiguous nanoseconds count %s, " + "Ambiguous nanoseconds count %s, " "should have exactly 9 digits", - filename, line_number, dot + 1); + dot + 1); } /* We store it as a GTimeSpan which is "only" microsecond @@ -246,10 +302,12 @@ pv_mtree_entry_parse (const char *line, 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; @@ -259,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; } @@ -500,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\"", @@ -554,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\": ", @@ -562,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 19e9a8e44..f0206cf4f 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 42fe885b5..ec4d81dfb 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 540f0261b..6d1c9981c 100644 --- a/tests/pressure-vessel/utils.c +++ b/tests/pressure-vessel/utils.c @@ -330,10 +330,11 @@ 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.500000000", "./foo", @@ -343,10 +344,11 @@ test_mtree_entry_parse (Fixture *f, .kind = PV_MTREE_ENTRY_KIND_FILE }, .sha256 = "ffff", }, - { "./foo type=file sha256digest=ffff mode=4755 time=1234567890", + { "./foo type=file sha256digest=ffff mode=4755 time=1234567890 optional", "./foo", { .size = -1, .mtime_usec = (1234567890 * G_TIME_SPAN_SECOND), - .mode = 04755, .kind = PV_MTREE_ENTRY_KIND_FILE }, + .mode = 04755, .kind = PV_MTREE_ENTRY_KIND_FILE, + .entry_flags = PV_MTREE_ENTRY_FLAGS_OPTIONAL }, .sha256 = "ffff", }, { "./foo type=file sha256=ffff sha256digest=ffff time=1234567890.0", @@ -355,10 +357,11 @@ test_mtree_entry_parse (Fixture *f, .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 }, @@ -424,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); } } } -- GitLab