]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysupdate: add new component "sysupdate"
authorLennart Poettering <lennart@poettering.net>
Mon, 28 Dec 2020 14:17:54 +0000 (15:17 +0100)
committerLennart Poettering <lennart@poettering.net>
Fri, 18 Mar 2022 23:13:55 +0000 (00:13 +0100)
21 files changed:
meson.build
meson_options.txt
src/sysupdate/meson.build [new file with mode: 0644]
src/sysupdate/sysupdate-cache.c [new file with mode: 0644]
src/sysupdate/sysupdate-cache.h [new file with mode: 0644]
src/sysupdate/sysupdate-instance.c [new file with mode: 0644]
src/sysupdate/sysupdate-instance.h [new file with mode: 0644]
src/sysupdate/sysupdate-partition.c [new file with mode: 0644]
src/sysupdate/sysupdate-partition.h [new file with mode: 0644]
src/sysupdate/sysupdate-pattern.c [new file with mode: 0644]
src/sysupdate/sysupdate-pattern.h [new file with mode: 0644]
src/sysupdate/sysupdate-resource.c [new file with mode: 0644]
src/sysupdate/sysupdate-resource.h [new file with mode: 0644]
src/sysupdate/sysupdate-transfer.c [new file with mode: 0644]
src/sysupdate/sysupdate-transfer.h [new file with mode: 0644]
src/sysupdate/sysupdate-update-set.c [new file with mode: 0644]
src/sysupdate/sysupdate-update-set.h [new file with mode: 0644]
src/sysupdate/sysupdate-util.c [new file with mode: 0644]
src/sysupdate/sysupdate-util.h [new file with mode: 0644]
src/sysupdate/sysupdate.c [new file with mode: 0644]
src/sysupdate/sysupdate.h [new file with mode: 0644]

index 107192b2112f938f7c0d1081213e77347e6a4580..05dcc79cfa815e6a00e43dfdf53ceada1538f06c 100644 (file)
@@ -1644,6 +1644,18 @@ conf.set('DEFAULT_DNSSEC_MODE',
          'DNSSEC_' + default_dnssec.underscorify().to_upper())
 conf.set_quoted('DEFAULT_DNSSEC_MODE_STR', default_dnssec)
 
+want_sysupdate = get_option('sysupdate')
+if want_sysupdate != 'false'
+        have = (conf.get('HAVE_OPENSSL') == 1 and
+                conf.get('HAVE_LIBFDISK') == 1)
+        if want_sysupdate == 'true' and not have
+                error('sysupdate support was requested, but dependencies are not available')
+        endif
+else
+        have = false
+endif
+conf.set10('ENABLE_SYSUPDATE', have)
+
 want_importd = get_option('importd')
 if want_importd != 'false'
         have = (conf.get('HAVE_LIBCURL') == 1 and
@@ -2006,6 +2018,7 @@ subdir('src/rpm')
 subdir('src/shutdown')
 subdir('src/sysext')
 subdir('src/systemctl')
+subdir('src/sysupdate')
 subdir('src/timedate')
 subdir('src/timesync')
 subdir('src/tmpfiles')
@@ -3074,6 +3087,22 @@ if conf.get('ENABLE_REPART') == 1
         endif
 endif
 
+if conf.get('ENABLE_SYSUPDATE') == 1
+        exe = executable(
+                'systemd-sysupdate',
+                systemd_sysupdate_sources,
+                include_directories : includes,
+                link_with : [libshared],
+                dependencies : [threads,
+                                libblkid,
+                                libfdisk,
+                                libopenssl],
+                install_rpath : rootlibexecdir,
+                install : true,
+                install_dir : rootlibexecdir)
+        public_programs += exe
+endif
+
 if conf.get('ENABLE_VCONSOLE') == 1
         executable(
                 'systemd-vconsole-setup',
@@ -4117,6 +4146,7 @@ foreach tuple : [
         ['rfkill'],
         ['sysext'],
         ['systemd-analyze',       conf.get('ENABLE_ANALYZE') == 1],
+        ['sysupdate'],
         ['sysusers'],
         ['timedated'],
         ['timesyncd'],
index 284109cadf4f3a11330c621d9811e770f1da6b43..27cfa9b697e0eec968f829162ed5169f5c2851c0 100644 (file)
@@ -100,6 +100,8 @@ option('binfmt', type : 'boolean',
        description : 'support for custom binary formats')
 option('repart', type : 'combo', choices : ['auto', 'true', 'false'],
        description : 'install the systemd-repart tool')
+option('sysupdate', type : 'combo', choices : ['auto', 'true', 'false'],
+       description : 'install the systemd-sysupdate tool')
 option('coredump', type : 'boolean',
        description : 'install the coredump handler')
 option('pstore', type : 'boolean',
diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build
new file mode 100644 (file)
index 0000000..2b1a256
--- /dev/null
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+systemd_sysupdate_sources = files('''
+        sysupdate-instance.c
+        sysupdate-instance.h
+        sysupdate-partition.c
+        sysupdate-partition.h
+        sysupdate-pattern.c
+        sysupdate-pattern.h
+        sysupdate-resource.c
+        sysupdate-resource.h
+        sysupdate-transfer.c
+        sysupdate-transfer.h
+        sysupdate-update-set.c
+        sysupdate-update-set.h
+        sysupdate-util.c
+        sysupdate-util.h
+        sysupdate-cache.c
+        sysupdate-cache.h
+        sysupdate.c
+        sysupdate.h
+'''.split())
diff --git a/src/sysupdate/sysupdate-cache.c b/src/sysupdate/sysupdate-cache.c
new file mode 100644 (file)
index 0000000..8dad3ee
--- /dev/null
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "memory-util.h"
+#include "sysupdate-cache.h"
+
+#define WEB_CACHE_ENTRIES_MAX 64U
+#define WEB_CACHE_ITEM_SIZE_MAX (64U*1024U*1024U)
+
+static WebCacheItem* web_cache_item_free(WebCacheItem *i) {
+        if (!i)
+                return NULL;
+
+        free(i->url);
+        return mfree(i);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(WebCacheItem*, web_cache_item_free);
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(web_cache_hash_ops, char, string_hash_func, string_compare_func, WebCacheItem, web_cache_item_free);
+
+int web_cache_add_item(
+                Hashmap **web_cache,
+                const char *url,
+                bool verified,
+                const void *data,
+                size_t size) {
+
+        _cleanup_(web_cache_item_freep) WebCacheItem *item = NULL;
+        _cleanup_free_ char *u = NULL;
+        int r;
+
+        assert(web_cache);
+        assert(url);
+        assert(data || size == 0);
+
+        if (size > WEB_CACHE_ITEM_SIZE_MAX)
+                return -E2BIG;
+
+        item = web_cache_get_item(*web_cache, url, verified);
+        if (item && memcmp_nn(item->data, item->size, data, size) == 0)
+                return 0;
+
+        if (hashmap_size(*web_cache) >= (size_t) (WEB_CACHE_ENTRIES_MAX + !!hashmap_get(*web_cache, url)))
+                return -ENOSPC;
+
+        r = hashmap_ensure_allocated(web_cache, &web_cache_hash_ops);
+        if (r < 0)
+                return r;
+
+        u = strdup(url);
+        if (!u)
+                return -ENOMEM;
+
+        item = malloc(offsetof(WebCacheItem, data) + size + 1);
+        if (!item)
+                return -ENOMEM;
+
+        *item = (WebCacheItem) {
+                .url = TAKE_PTR(u),
+                .size = size,
+                .verified = verified,
+        };
+
+        /* Just to be extra paranoid, let's NUL terminate the downloaded buffer */
+        *(uint8_t*) mempcpy(item->data, data, size) = 0;
+
+        web_cache_item_free(hashmap_remove(*web_cache, url));
+
+        r = hashmap_put(*web_cache, item->url, item);
+        if (r < 0)
+                return r;
+
+        TAKE_PTR(item);
+        return 1;
+}
+
+WebCacheItem* web_cache_get_item(Hashmap *web_cache, const char *url, bool verified) {
+        WebCacheItem *i;
+
+        i = hashmap_get(web_cache, url);
+        if (!i)
+                return NULL;
+
+        if (i->verified != verified)
+                return NULL;
+
+        return i;
+}
diff --git a/src/sysupdate/sysupdate-cache.h b/src/sysupdate/sysupdate-cache.h
new file mode 100644 (file)
index 0000000..d6a7897
--- /dev/null
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "hashmap.h"
+
+typedef struct WebCacheItem {
+        char *url;
+        bool verified;
+        size_t size;
+        uint8_t data[];
+} WebCacheItem;
+
+/* A simple in-memory cache for downloaded manifests. Very likely multiple transfers will use the same
+ * manifest URLs, hence let's make sure we only download them once within each sysupdate invocation. */
+
+int web_cache_add_item(Hashmap **cache, const char *url, bool verified, const void *data, size_t size);
+
+WebCacheItem* web_cache_get_item(Hashmap *cache, const char *url, bool verified);
diff --git a/src/sysupdate/sysupdate-instance.c b/src/sysupdate/sysupdate-instance.c
new file mode 100644 (file)
index 0000000..16bfab9
--- /dev/null
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#include "sysupdate-instance.h"
+
+void instance_metadata_destroy(InstanceMetadata *m) {
+        assert(m);
+        free(m->version);
+}
+
+int instance_new(
+                Resource *rr,
+                const char *path,
+                const InstanceMetadata *f,
+                Instance **ret) {
+
+        _cleanup_(instance_freep) Instance *i = NULL;
+        _cleanup_free_ char *p = NULL, *v = NULL;
+
+        assert(rr);
+        assert(path);
+        assert(f);
+        assert(f->version);
+        assert(ret);
+
+        p = strdup(path);
+        if (!p)
+                return log_oom();
+
+        v = strdup(f->version);
+        if (!v)
+                return log_oom();
+
+        i = new(Instance, 1);
+        if (!i)
+                return log_oom();
+
+        *i = (Instance) {
+                .resource = rr,
+                .metadata = *f,
+                .path = TAKE_PTR(p),
+                .partition_info = PARTITION_INFO_NULL,
+        };
+
+        i->metadata.version = TAKE_PTR(v);
+
+        *ret = TAKE_PTR(i);
+        return 0;
+}
+
+Instance *instance_free(Instance *i) {
+        if (!i)
+                return NULL;
+
+        instance_metadata_destroy(&i->metadata);
+
+        free(i->path);
+        partition_info_destroy(&i->partition_info);
+
+        return mfree(i);
+}
diff --git a/src/sysupdate/sysupdate-instance.h b/src/sysupdate/sysupdate-instance.h
new file mode 100644 (file)
index 0000000..2860d29
--- /dev/null
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+#include "sd-id128.h"
+
+#include "fs-util.h"
+#include "time-util.h"
+
+typedef struct InstanceMetadata InstanceMetadata;
+typedef struct Instance Instance;
+
+#include "sysupdate-resource.h"
+#include "sysupdate-partition.h"
+
+struct InstanceMetadata {
+        /* Various bits of metadata for each instance, that is either derived from the filename/GPT label or
+         * from metadata of the file/partition itself */
+        char *version;
+        sd_id128_t partition_uuid;
+        bool partition_uuid_set;
+        uint64_t partition_flags;          /* GPT partition flags */
+        bool partition_flags_set;
+        usec_t mtime;
+        mode_t mode;
+        uint64_t size;                     /* uncompressed size of the file */
+        uint64_t tries_done, tries_left;   /* for boot assessment counters */
+        int no_auto;
+        int read_only;
+        int growfs;
+        uint8_t sha256sum[32];             /* SHA256 sum of the download (i.e. compressed) file */
+        bool sha256sum_set;
+};
+
+#define INSTANCE_METADATA_NULL                  \
+        {                                       \
+                .mtime = USEC_INFINITY,         \
+                .mode = MODE_INVALID,           \
+                .size = UINT64_MAX,             \
+                .tries_done = UINT64_MAX,       \
+                .tries_left = UINT64_MAX,       \
+                .no_auto = -1,                  \
+                .read_only = -1,                \
+                .growfs = -1,                   \
+        }
+
+struct Instance {
+        /* A pointer back to the resource this belongs to */
+        Resource *resource;
+
+        /* Metadata of this version */
+        InstanceMetadata metadata;
+
+        /* Where we found the instance */
+        char *path;
+        PartitionInfo partition_info;
+};
+
+void instance_metadata_destroy(InstanceMetadata *m);
+
+int instance_new(Resource *rr, const char *path, const InstanceMetadata *f, Instance **ret);
+Instance *instance_free(Instance *i);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Instance*, instance_free);
diff --git a/src/sysupdate/sysupdate-partition.c b/src/sysupdate/sysupdate-partition.c
new file mode 100644 (file)
index 0000000..f3e2100
--- /dev/null
@@ -0,0 +1,379 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/file.h>
+
+#include "alloc-util.h"
+#include "extract-word.h"
+#include "gpt.h"
+#include "id128-util.h"
+#include "parse-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "sysupdate-partition.h"
+#include "util.h"
+
+void partition_info_destroy(PartitionInfo *p) {
+        assert(p);
+
+        p->label = mfree(p->label);
+        p->device = mfree(p->device);
+}
+
+static int fdisk_partition_get_attrs_as_uint64(
+                struct fdisk_partition *pa,
+                uint64_t *ret) {
+
+        uint64_t flags = 0;
+        const char *a;
+        int r;
+
+        assert(pa);
+        assert(ret);
+
+        /* Retrieve current flags as uint64_t mask */
+
+        a = fdisk_partition_get_attrs(pa);
+        if (!a) {
+                *ret = 0;
+                return 0;
+        }
+
+        for (;;) {
+                _cleanup_free_ char *word = NULL;
+
+                r = extract_first_word(&a, &word, ",", EXTRACT_DONT_COALESCE_SEPARATORS);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                if (streq(word, "RequiredPartition"))
+                        flags |= GPT_FLAG_REQUIRED_PARTITION;
+                else if (streq(word, "NoBlockIOProtocol"))
+                        flags |= GPT_FLAG_NO_BLOCK_IO_PROTOCOL;
+                else if (streq(word, "LegacyBIOSBootable"))
+                        flags |= GPT_FLAG_LEGACY_BIOS_BOOTABLE;
+                else {
+                        const char *e;
+                        unsigned u;
+
+                        /* Drop "GUID" prefix if specified */
+                        e = startswith(word, "GUID:") ?: word;
+
+                        if (safe_atou(e, &u) < 0) {
+                                log_debug("Unknown partition flag '%s', ignoring.", word);
+                                continue;
+                        }
+
+                        if (u >= sizeof(flags)*8) { /* partition flags on GPT are 64bit. Let's ignore any further
+                                                       bits should libfdisk report them */
+                                log_debug("Partition flag above bit 63 (%s), ignoring.", word);
+                                continue;
+                        }
+
+                        flags |= UINT64_C(1) << u;
+                }
+        }
+
+        *ret = flags;
+        return 0;
+}
+
+static int fdisk_partition_set_attrs_as_uint64(
+                struct fdisk_partition *pa,
+                uint64_t flags) {
+
+        _cleanup_free_ char *attrs = NULL;
+        int r;
+
+        assert(pa);
+
+        for (unsigned i = 0; i < sizeof(flags) * 8; i++) {
+                if (!FLAGS_SET(flags, UINT64_C(1) << i))
+                        continue;
+
+                r = strextendf_with_separator(&attrs, ",", "%u", i);
+                if (r < 0)
+                        return r;
+        }
+
+        return fdisk_partition_set_attrs(pa, strempty(attrs));
+}
+
+int read_partition_info(
+                struct fdisk_context *c,
+                struct fdisk_table *t,
+                size_t i,
+                PartitionInfo *ret) {
+
+        _cleanup_free_ char *label_copy = NULL, *device = NULL;
+        const char *pts, *ids, *label;
+        struct fdisk_partition *p;
+        struct fdisk_parttype *pt;
+        uint64_t start, size, flags;
+        sd_id128_t ptid, id;
+        size_t partno;
+        int r;
+
+        assert(c);
+        assert(t);
+        assert(ret);
+
+        p = fdisk_table_get_partition(t, i);
+        if (!p)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata: %m");
+
+        if (fdisk_partition_is_used(p) <= 0) {
+                *ret = (PartitionInfo) PARTITION_INFO_NULL;
+                return 0; /* not found! */
+        }
+
+        if (fdisk_partition_has_partno(p) <= 0 ||
+            fdisk_partition_has_start(p) <= 0 ||
+            fdisk_partition_has_size(p) <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a number, position or size.");
+
+        partno = fdisk_partition_get_partno(p);
+
+        start = fdisk_partition_get_start(p);
+        assert(start <= UINT64_MAX / 512U);
+        start *= 512U;
+
+        size = fdisk_partition_get_size(p);
+        assert(size <= UINT64_MAX / 512U);
+        size *= 512U;
+
+        label = fdisk_partition_get_name(p);
+        if (!label)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a label.");
+
+        pt = fdisk_partition_get_type(p);
+        if (!pt)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire type of partition: %m");
+
+        pts = fdisk_parttype_get_string(pt);
+        if (!pts)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire type of partition as string: %m");
+
+        r = sd_id128_from_string(pts, &ptid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse partition type UUID %s: %m", pts);
+
+        ids = fdisk_partition_get_uuid(p);
+        if (!ids)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a UUID.");
+
+        r = sd_id128_from_string(ids, &id);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse partition UUID %s: %m", ids);
+
+        r = fdisk_partition_get_attrs_as_uint64(p, &flags);
+        if (r < 0)
+                return log_error_errno(r, "Failed to get partition flags: %m");
+
+        r = fdisk_partition_to_string(p, c, FDISK_FIELD_DEVICE, &device);
+        if (r != 0)
+                return log_error_errno(r, "Failed to get partition device name: %m");
+
+        label_copy = strdup(label);
+        if (!label_copy)
+                return log_oom();
+
+        *ret = (PartitionInfo) {
+                .partno = partno,
+                .start = start,
+                .size = size,
+                .flags = flags,
+                .type = ptid,
+                .uuid = id,
+                .label = TAKE_PTR(label_copy),
+                .device = TAKE_PTR(device),
+                .no_auto = FLAGS_SET(flags, GPT_FLAG_NO_AUTO) && gpt_partition_type_knows_no_auto(ptid),
+                .read_only = FLAGS_SET(flags, GPT_FLAG_READ_ONLY) && gpt_partition_type_knows_read_only(ptid),
+                .growfs = FLAGS_SET(flags, GPT_FLAG_GROWFS) && gpt_partition_type_knows_growfs(ptid),
+        };
+
+        return 1; /* found! */
+}
+
+int find_suitable_partition(
+                const char *device,
+                uint64_t space,
+                sd_id128_t *partition_type,
+                PartitionInfo *ret) {
+
+        _cleanup_(partition_info_destroy) PartitionInfo smallest = PARTITION_INFO_NULL;
+        _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+        _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL;
+        size_t n_partitions;
+        int r;
+
+        assert(device);
+        assert(ret);
+
+        c = fdisk_new_context();
+        if (!c)
+                return log_oom();
+
+        r = fdisk_assign_device(c, device, /* readonly= */ true);
+        if (r < 0)
+                return log_error_errno(r, "Failed to open device '%s': %m", device);
+
+        if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT))
+                return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", device);
+
+        r = fdisk_get_partitions(c, &t);
+        if (r < 0)
+                return log_error_errno(r, "Failed to acquire partition table: %m");
+
+        n_partitions = fdisk_table_get_nents(t);
+        for (size_t i = 0; i < n_partitions; i++)  {
+                _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL;
+
+                r = read_partition_info(c, t, i, &pinfo);
+                if (r < 0)
+                        return r;
+                if (r == 0) /* not assigned */
+                        continue;
+
+                /* Filter out non-matching partition types */
+                if (partition_type && !sd_id128_equal(pinfo.type, *partition_type))
+                        continue;
+
+                if (!streq_ptr(pinfo.label, "_empty")) /* used */
+                        continue;
+
+                if (space != UINT64_MAX && pinfo.size < space) /* too small */
+                        continue;
+
+                if (smallest.partno != SIZE_MAX && smallest.size <= pinfo.size) /* already found smaller */
+                        continue;
+
+                smallest = pinfo;
+                pinfo = (PartitionInfo) PARTITION_INFO_NULL;
+        }
+
+        if (smallest.partno == SIZE_MAX)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), "No available partition of a suitable size found.");
+
+        *ret = smallest;
+        smallest = (PartitionInfo) PARTITION_INFO_NULL;
+
+        return 0;
+}
+
+int patch_partition(
+                const char *device,
+                const PartitionInfo *info,
+                PartitionChange change) {
+
+        _cleanup_(fdisk_unref_partitionp) struct fdisk_partition *pa = NULL;
+        _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+        bool tweak_no_auto, tweak_read_only, tweak_growfs;
+        int r, fd;
+
+        assert(device);
+        assert(info);
+        assert(change <= _PARTITION_CHANGE_MAX);
+
+        if (change == 0) /* Nothing to do */
+                return 0;
+
+        c = fdisk_new_context();
+        if (!c)
+                return log_oom();
+
+        r = fdisk_assign_device(c, device, /* readonly= */ false);
+        if (r < 0)
+                return log_error_errno(r, "Failed to open device '%s': %m", device);
+
+        assert_se((fd = fdisk_get_devfd(c)) >= 0);
+
+        /* Make sure udev doesn't read the device while we make changes (this lock is released automatically
+         * by the kernel when the fd is closed, i.e. when the fdisk context is freed, hence no explicit
+         * unlock by us here anywhere.) */
+        if (flock(fd, LOCK_EX) < 0)
+                return log_error_errno(errno, "Failed to lock block device '%s': %m", device);
+
+        if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT))
+                return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", device);
+
+        r = fdisk_get_partition(c, info->partno, &pa);
+        if (r < 0)
+                return log_error_errno(r, "Failed to read partition %zu of GPT label of '%s': %m", info->partno, device);
+
+        if (change & PARTITION_LABEL) {
+                r = fdisk_partition_set_name(pa, info->label);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to update partition label: %m");
+        }
+
+        if (change & PARTITION_UUID) {
+                r = fdisk_partition_set_uuid(pa, SD_ID128_TO_UUID_STRING(info->uuid));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to update partition UUID: %m");
+        }
+
+        /* Tweak the read-only flag, but only if supported by the partition type */
+        tweak_no_auto =
+                FLAGS_SET(change, PARTITION_NO_AUTO) &&
+                gpt_partition_type_knows_no_auto(info->type);
+        tweak_read_only =
+                FLAGS_SET(change, PARTITION_READ_ONLY) &&
+                gpt_partition_type_knows_read_only(info->type);
+        tweak_growfs =
+                FLAGS_SET(change, PARTITION_GROWFS) &&
+                gpt_partition_type_knows_growfs(info->type);
+
+        if (change & PARTITION_FLAGS) {
+                uint64_t flags;
+
+                /* Update the full flags parameter, and import the read-only flag into it */
+
+                flags = info->flags;
+                if (tweak_no_auto)
+                        SET_FLAG(flags, GPT_FLAG_NO_AUTO, info->no_auto);
+                if (tweak_read_only)
+                        SET_FLAG(flags, GPT_FLAG_READ_ONLY, info->read_only);
+                if (tweak_growfs)
+                        SET_FLAG(flags, GPT_FLAG_GROWFS, info->growfs);
+
+                r = fdisk_partition_set_attrs_as_uint64(pa, flags);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to update partition flags: %m");
+
+        } else if (tweak_no_auto || tweak_read_only || tweak_growfs) {
+                uint64_t old_flags, new_flags;
+
+                /* So we aren't supposed to update the full flags parameter, but we are supposed to update
+                 * the RO flag of it. */
+
+                r = fdisk_partition_get_attrs_as_uint64(pa, &old_flags);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to get old partition flags: %m");
+
+                new_flags = old_flags;
+                if (tweak_no_auto)
+                        SET_FLAG(new_flags, GPT_FLAG_NO_AUTO, info->no_auto);
+                if (tweak_read_only)
+                        SET_FLAG(new_flags, GPT_FLAG_READ_ONLY, info->read_only);
+                if (tweak_growfs)
+                        SET_FLAG(new_flags, GPT_FLAG_GROWFS, info->growfs);
+
+                if (new_flags != old_flags) {
+                        r = fdisk_partition_set_attrs_as_uint64(pa, new_flags);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to update partition flags: %m");
+                }
+        }
+
+        r = fdisk_set_partition(c, info->partno, pa);
+        if (r < 0)
+                return log_error_errno(r, "Failed to update partition: %m");
+
+        r = fdisk_write_disklabel(c);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write updated partition table: %m");
+
+        return 0;
+}
diff --git a/src/sysupdate/sysupdate-partition.h b/src/sysupdate/sysupdate-partition.h
new file mode 100644 (file)
index 0000000..672eb93
--- /dev/null
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <sys/types.h>
+
+#include "sd-id128.h"
+
+#include "fdisk-util.h"
+#include "macro.h"
+
+typedef struct PartitionInfo PartitionInfo;
+
+typedef enum PartitionChange {
+        PARTITION_FLAGS           = 1 << 0,
+        PARTITION_NO_AUTO         = 1 << 1,
+        PARTITION_READ_ONLY       = 1 << 2,
+        PARTITION_GROWFS          = 1 << 3,
+        PARTITION_UUID            = 1 << 4,
+        PARTITION_LABEL           = 1 << 5,
+        _PARTITION_CHANGE_MAX     = (1 << 6) - 1, /* all of the above */
+        _PARTITION_CHANGE_INVALID = -EINVAL,
+} PartitionChange;
+
+struct PartitionInfo {
+        size_t partno;
+        uint64_t start, size;
+        uint64_t flags;
+        sd_id128_t type, uuid;
+        char *label;
+        char *device; /* Note that this might point to some non-existing path in case we operate on a loopback file */
+        bool no_auto:1;
+        bool read_only:1;
+        bool growfs:1;
+};
+
+#define PARTITION_INFO_NULL                     \
+        {                                       \
+                .partno = SIZE_MAX,             \
+                .start = UINT64_MAX,            \
+                .size = UINT64_MAX,             \
+        }
+
+void partition_info_destroy(PartitionInfo *p);
+
+int read_partition_info(struct fdisk_context *c, struct fdisk_table *t, size_t i, PartitionInfo *ret);
+
+int find_suitable_partition(const char *device, uint64_t space, sd_id128_t *partition_type, PartitionInfo *ret);
+int patch_partition(const char *device, const PartitionInfo *info, PartitionChange change);
diff --git a/src/sysupdate/sysupdate-pattern.c b/src/sysupdate/sysupdate-pattern.c
new file mode 100644 (file)
index 0000000..4e0c417
--- /dev/null
@@ -0,0 +1,605 @@
+/* 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.", *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;
+        PatternElement *e;
+        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;
+        char **p;
+        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;
+        PatternElement *e;
+        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;
+}
diff --git a/src/sysupdate/sysupdate-pattern.h b/src/sysupdate/sysupdate-pattern.h
new file mode 100644 (file)
index 0000000..1c60fa0
--- /dev/null
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sysupdate-instance.h"
+#include "time-util.h"
+
+int pattern_match(const char *pattern, const char *s, InstanceMetadata *ret);
+int pattern_match_many(char **patterns, const char *s, InstanceMetadata *ret);
+int pattern_valid(const char *pattern);
+int pattern_format(const char *pattern, const InstanceMetadata *fields, char **ret);
diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c
new file mode 100644 (file)
index 0000000..97d8973
--- /dev/null
@@ -0,0 +1,633 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "blockdev-util.h"
+#include "chase-symlinks.h"
+#include "dirent-util.h"
+#include "env-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "glyph-util.h"
+#include "gpt.h"
+#include "hexdecoct.h"
+#include "import-util.h"
+#include "macro.h"
+#include "process-util.h"
+#include "sort-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "sysupdate-cache.h"
+#include "sysupdate-instance.h"
+#include "sysupdate-pattern.h"
+#include "sysupdate-resource.h"
+#include "sysupdate.h"
+#include "utf8.h"
+
+void resource_destroy(Resource *rr) {
+        assert(rr);
+
+        free(rr->path);
+        strv_free(rr->patterns);
+
+        for (size_t i = 0; i < rr->n_instances; i++)
+                instance_free(rr->instances[i]);
+        free(rr->instances);
+}
+
+static int resource_add_instance(
+                Resource *rr,
+                const char *path,
+                const InstanceMetadata *f,
+                Instance **ret) {
+
+        Instance *i;
+        int r;
+
+        assert(rr);
+        assert(path);
+        assert(f);
+        assert(f->version);
+
+        if (!GREEDY_REALLOC(rr->instances, rr->n_instances + 1))
+                return log_oom();
+
+        r = instance_new(rr, path, f, &i);
+        if (r < 0)
+                return r;
+
+        rr->instances[rr->n_instances++] = i;
+
+        if (ret)
+                *ret = i;
+
+        return 0;
+}
+
+static int resource_load_from_directory(
+                Resource *rr,
+                mode_t m) {
+
+        _cleanup_(closedirp) DIR *d = NULL;
+        int r;
+
+        assert(rr);
+        assert(IN_SET(rr->type, RESOURCE_TAR, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+        assert(IN_SET(m, S_IFREG, S_IFDIR));
+
+        d = opendir(rr->path);
+        if (!d) {
+                if (errno == ENOENT) {
+                        log_debug("Directory %s does not exist, not loading any resources.", rr->path);
+                        return 0;
+                }
+
+                return log_error_errno(errno, "Failed to open directory '%s': %m", rr->path);
+        }
+
+        for (;;) {
+                _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
+                _cleanup_free_ char *joined = NULL;
+                Instance *instance;
+                struct dirent *de;
+                struct stat st;
+
+                errno = 0;
+                de = readdir_no_dot(d);
+                if (!de) {
+                        if (errno != 0)
+                                return log_error_errno(errno, "Failed to read directory '%s': %m", rr->path);
+                        break;
+                }
+
+                switch (de->d_type) {
+
+                case DT_UNKNOWN:
+                        break;
+
+                case DT_DIR:
+                        if (m != S_IFDIR)
+                                continue;
+
+                        break;
+
+                case DT_REG:
+                        if (m != S_IFREG)
+                                continue;
+                        break;
+
+                default:
+                        continue;
+                }
+
+                if (fstatat(dirfd(d), de->d_name, &st, AT_NO_AUTOMOUNT) < 0) {
+                        if (errno == ENOENT) /* Gone by now? */
+                                continue;
+
+                        return log_error_errno(errno, "Failed to stat %s/%s: %m", rr->path, de->d_name);
+                }
+
+                if ((st.st_mode & S_IFMT) != m)
+                        continue;
+
+                r = pattern_match_many(rr->patterns, de->d_name, &extracted_fields);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to match pattern: %m");
+                if (r == 0)
+                        continue;
+
+                joined = path_join(rr->path, de->d_name);
+                if (!joined)
+                        return log_oom();
+
+                r = resource_add_instance(rr, joined, &extracted_fields, &instance);
+                if (r < 0)
+                        return r;
+
+                /* Inherit these from the source, if not explicitly overwritten */
+                if (instance->metadata.mtime == USEC_INFINITY)
+                        instance->metadata.mtime = timespec_load(&st.st_mtim) ?: USEC_INFINITY;
+
+                if (instance->metadata.mode == MODE_INVALID)
+                        instance->metadata.mode = st.st_mode & 0775; /* mask out world-writability and suid and stuff, for safety */
+        }
+
+        return 0;
+}
+
+static int resource_load_from_blockdev(Resource *rr) {
+        _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+        _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL;
+        size_t n_partitions;
+        int r;
+
+        assert(rr);
+
+        c = fdisk_new_context();
+        if (!c)
+                return log_oom();
+
+        r = fdisk_assign_device(c, rr->path, /* readonly= */ true);
+        if (r < 0)
+                return log_error_errno(r, "Failed to open device '%s': %m", rr->path);
+
+        if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT))
+                return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", rr->path);
+
+        r = fdisk_get_partitions(c, &t);
+        if (r < 0)
+                return log_error_errno(r, "Failed to acquire partition table: %m");
+
+        n_partitions = fdisk_table_get_nents(t);
+        for (size_t i = 0; i < n_partitions; i++)  {
+                _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
+                _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL;
+                Instance *instance;
+
+                r = read_partition_info(c, t, i, &pinfo);
+                if (r < 0)
+                        return r;
+                if (r == 0) /* not assigned */
+                        continue;
+
+                /* Check if partition type matches */
+                if (rr->partition_type_set && !sd_id128_equal(pinfo.type, rr->partition_type))
+                        continue;
+
+                /* A label of "_empty" means "not used so far" for us */
+                if (streq_ptr(pinfo.label, "_empty")) {
+                        rr->n_empty++;
+                        continue;
+                }
+
+                r = pattern_match_many(rr->patterns, pinfo.label, &extracted_fields);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to match pattern: %m");
+                if (r == 0)
+                        continue;
+
+                r = resource_add_instance(rr, pinfo.device, &extracted_fields, &instance);
+                if (r < 0)
+                        return r;
+
+                instance->partition_info = pinfo;
+                pinfo = (PartitionInfo) PARTITION_INFO_NULL;
+
+                /* Inherit data from source if not configured explicitly */
+                if (!instance->metadata.partition_uuid_set) {
+                        instance->metadata.partition_uuid = instance->partition_info.uuid;
+                        instance->metadata.partition_uuid_set = true;
+                }
+
+                if (!instance->metadata.partition_flags_set) {
+                        instance->metadata.partition_flags = instance->partition_info.flags;
+                        instance->metadata.partition_flags_set = true;
+                }
+
+                if (instance->metadata.read_only < 0)
+                        instance->metadata.read_only = instance->partition_info.read_only;
+        }
+
+        return 0;
+}
+
+static int download_manifest(
+                const char *url,
+                bool verify_signature,
+                char **ret_buffer,
+                size_t *ret_size) {
+
+        _cleanup_free_ char *buffer = NULL, *suffixed_url = NULL;
+        _cleanup_(close_pairp) int pfd[2] = { -1, -1 };
+        _cleanup_fclose_ FILE *manifest = NULL;
+        size_t size = 0;
+        pid_t pid;
+        int r;
+
+        assert(url);
+        assert(ret_buffer);
+        assert(ret_size);
+
+        /* Download a SHA256SUMS file as manifest */
+
+        r = import_url_append_component(url, "SHA256SUMS", &suffixed_url);
+        if (r < 0)
+                return log_error_errno(r, "Failed to append SHA256SUMS to URL: %m");
+
+        if (pipe2(pfd, O_CLOEXEC) < 0)
+                return log_error_errno(errno, "Failed to allocate pipe: %m");
+
+        log_info("%s Acquiring manifest file %s…", special_glyph(SPECIAL_GLYPH_DOWNLOAD), suffixed_url);
+
+        r = safe_fork("(sd-pull)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                /* Child */
+
+                const char *cmdline[] = {
+                        "systemd-pull",
+                        "raw",
+                        "--direct",                        /* just download the specified URL, don't download anything else */
+                        "--verify", verify_signature ? "signature" : "no", /* verify the manifest file */
+                        suffixed_url,
+                        "-",                               /* write to stdout */
+                        NULL
+                };
+
+                pfd[0] = safe_close(pfd[0]);
+
+                r = rearrange_stdio(-1, pfd[1], STDERR_FILENO);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to rearrange stdin/stdout: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                (void) unsetenv("NOTIFY_SOCKET");
+                execv(pull_binary_path(), (char *const*) cmdline);
+                log_error_errno(errno, "Failed to execute %s tool: %m", pull_binary_path());
+                _exit(EXIT_FAILURE);
+        };
+
+        pfd[1] = safe_close(pfd[1]);
+
+        /* We'll first load the entire manifest into memory before parsing it. That's because the
+         * systemd-pull tool can validate the download only after its completion, but still pass the data to
+         * us as it runs. We thus need to check the return value of the process *before* parsing, to be
+         * reasonably safe. */
+
+        manifest = fdopen(pfd[0], "r");
+        if (!manifest)
+                return log_error_errno(errno, "Failed allocate FILE object for manifest file: %m");
+
+        TAKE_FD(pfd[0]);
+
+        r = read_full_stream(manifest, &buffer, &size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to read manifest file from child: %m");
+
+        manifest = safe_fclose(manifest);
+
+        r = wait_for_terminate_and_check("(sd-pull)", pid, WAIT_LOG);
+        if (r < 0)
+                return r;
+        if (r != 0)
+                return -EPROTO;
+
+        *ret_buffer = TAKE_PTR(buffer);
+        *ret_size = size;
+
+        return 0;
+}
+
+static int resource_load_from_web(
+                Resource *rr,
+                bool verify,
+                Hashmap **web_cache) {
+
+        size_t manifest_size = 0, left = 0;
+        _cleanup_free_ char *buf = NULL;
+        const char *manifest, *p;
+        size_t line_nr = 1;
+        WebCacheItem *ci;
+        int r;
+
+        assert(rr);
+
+        ci = web_cache ? web_cache_get_item(*web_cache, rr->path, verify) : NULL;
+        if (ci) {
+                log_debug("Manifest web cache hit for %s.", rr->path);
+
+                manifest = (char*) ci->data;
+                manifest_size = ci->size;
+        } else {
+                log_debug("Manifest web cache miss for %s.", rr->path);
+
+                r = download_manifest(rr->path, verify, &buf, &manifest_size);
+                if (r < 0)
+                        return r;
+
+                manifest = buf;
+        }
+
+        if (memchr(manifest, 0, manifest_size))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file has embedded NUL byte, refusing.");
+        if (!utf8_is_valid_n(manifest, manifest_size))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file is not valid UTF-8, refusing.");
+
+        p = manifest;
+        left = manifest_size;
+
+        while (left > 0) {
+                _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
+                _cleanup_free_ char *fn = NULL;
+                _cleanup_free_ void *h = NULL;
+                Instance *instance;
+                const char *e;
+                size_t hlen;
+
+                /* 64 character hash + separator + filename + newline */
+                if (left < 67)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Corrupt manifest at line %zu, refusing.", line_nr);
+
+                if (p[0] == '\\')
+                        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "File names with escapes not supported in manifest at line %zu, refusing.", line_nr);
+
+                r = unhexmem(p, 64, &h, &hlen);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse digest at manifest line %zu, refusing.", line_nr);
+
+                p += 64, left -= 64;
+
+                if (*p != ' ')
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing space separator at manifest line %zu, refusing.", line_nr);
+                p++, left--;
+
+                if (!IN_SET(*p, '*', ' '))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing binary/text input marker at manifest line %zu, refusing.", line_nr);
+                p++, left--;
+
+                e = memchr(p, '\n', left);
+                if (!e)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Truncated manifest file at line %zu, refusing.", line_nr);
+                if (e == p)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty filename specified at manifest line %zu, refusing.", line_nr);
+
+                fn = strndup(p, e - p);
+                if (!fn)
+                        return log_oom();
+
+                if (!filename_is_valid(fn))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid filename specified at manifest line %zu, refusing.", line_nr);
+                if (string_has_cc(fn, NULL))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Filename contains control characters at manifest line %zu, refusing.", line_nr);
+
+                r = pattern_match_many(rr->patterns, fn, &extracted_fields);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to match pattern: %m");
+                if (r > 0) {
+                        _cleanup_free_ char *path = NULL;
+
+                        r = import_url_append_component(rr->path, fn, &path);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to build instance URL: %m");
+
+                        r = resource_add_instance(rr, path, &extracted_fields, &instance);
+                        if (r < 0)
+                                return r;
+
+                        assert(hlen == sizeof(instance->metadata.sha256sum));
+
+                        if (instance->metadata.sha256sum_set) {
+                                if (memcmp(instance->metadata.sha256sum, h, hlen) != 0)
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SHA256 sum parsed from filename and manifest don't match at line %zu, refusing.", line_nr);
+                        } else {
+                                memcpy(instance->metadata.sha256sum, h, hlen);
+                                instance->metadata.sha256sum_set = true;
+                        }
+                }
+
+                left -= (e - p) + 1;
+                p = e + 1;
+
+                line_nr++;
+        }
+
+        if (!ci && web_cache) {
+                r = web_cache_add_item(web_cache, rr->path, verify, manifest, manifest_size);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to add manifest '%s' to cache, ignoring: %m", rr->path);
+                else
+                        log_debug("Added manifest '%s' to cache.", rr->path);
+        }
+
+        return 0;
+}
+
+static int instance_cmp(Instance *const*a, Instance *const*b) {
+        int r;
+
+        assert(a);
+        assert(b);
+        assert(*a);
+        assert(*b);
+        assert((*a)->metadata.version);
+        assert((*b)->metadata.version);
+
+        /* Newest version at the beginning */
+        r = strverscmp_improved((*a)->metadata.version, (*b)->metadata.version);
+        if (r != 0)
+                return -r;
+
+        /* Instances don't have to be uniquely named (uniqueness on partition tables is not enforced at all,
+         * and since we allow multiple matching patterns not even in directories they are unique). Hence
+         * let's order by path as secondary ordering key. */
+        return path_compare((*a)->path, (*b)->path);
+}
+
+int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache) {
+        int r;
+
+        assert(rr);
+
+        switch (rr->type) {
+
+        case RESOURCE_TAR:
+        case RESOURCE_REGULAR_FILE:
+                r = resource_load_from_directory(rr, S_IFREG);
+                break;
+
+        case RESOURCE_DIRECTORY:
+        case RESOURCE_SUBVOLUME:
+                r = resource_load_from_directory(rr, S_IFDIR);
+                break;
+
+        case RESOURCE_PARTITION:
+                r = resource_load_from_blockdev(rr);
+                break;
+
+        case RESOURCE_URL_FILE:
+        case RESOURCE_URL_TAR:
+                r = resource_load_from_web(rr, verify, web_cache);
+                break;
+
+        default:
+                assert_not_reached();
+        }
+        if (r < 0)
+                return r;
+
+        typesafe_qsort(rr->instances, rr->n_instances, instance_cmp);
+        return 0;
+}
+
+Instance* resource_find_instance(Resource *rr, const char *version) {
+        Instance key = {
+                .metadata.version = (char*) version,
+        }, *k = &key;
+
+        return typesafe_bsearch(&k, rr->instances, rr->n_instances, instance_cmp);
+}
+
+int resource_resolve_path(
+                Resource *rr,
+                const char *root,
+                const char *node) {
+
+        _cleanup_free_ char *p = NULL;
+        dev_t d;
+        int r;
+
+        assert(rr);
+
+        if (rr->path_auto) {
+
+                /* NB: we don't actually check the backing device of the root fs "/", but of "/usr", in order
+                 * to support environments where the root fs is a tmpfs, and the OS itself placed exclusively
+                 * in /usr/. */
+
+                if (rr->type != RESOURCE_PARTITION)
+                        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+                                               "Automatic root path discovery only supported for partition resources.");
+
+                if (node) { /* If --image= is specified, directly use the loopback device */
+                        r = free_and_strdup_warn(&rr->path, node);
+                        if (r < 0)
+                                return r;
+
+                        return 0;
+                }
+
+                if (root)
+                        return log_error_errno(SYNTHETIC_ERRNO(EPERM),
+                                               "Block device is not allowed when using --root= mode.");
+
+                r = get_block_device_harder("/usr/", &d);
+
+        } else if (rr->type == RESOURCE_PARTITION) {
+                _cleanup_close_ int fd = -1, real_fd = -1;
+                _cleanup_free_ char *resolved = NULL;
+                struct stat st;
+
+                r = chase_symlinks(rr->path, root, CHASE_PREFIX_ROOT, &resolved, &fd);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to resolve '%s': %m", rr->path);
+
+                if (fstat(fd, &st) < 0)
+                        return log_error_errno(r, "Failed to stat '%s': %m", resolved);
+
+                if (S_ISBLK(st.st_mode) && root)
+                        return log_error_errno(SYNTHETIC_ERRNO(EPERM), "When using --root= or --image= access to device nodes is prohibited.");
+
+                if (S_ISREG(st.st_mode) || S_ISBLK(st.st_mode)) {
+                        /* Not a directory, hence no need to find backing block device for the path */
+                        free_and_replace(rr->path, resolved);
+                        return 0;
+                }
+
+                if (!S_ISDIR(st.st_mode))
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Target path '%s' does not refer to regular file, directory or block device, refusing.",  rr->path);
+
+                if (node) { /* If --image= is specified all file systems are backed by the same loopback device, hence shortcut things. */
+                        r = free_and_strdup_warn(&rr->path, node);
+                        if (r < 0)
+                                return r;
+
+                        return 0;
+                }
+
+                real_fd = fd_reopen(fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+                if (real_fd < 0)
+                        return log_error_errno(real_fd, "Failed to convert O_PATH file descriptor for %s to regular file descriptor: %m", rr->path);
+
+                r = get_block_device_harder_fd(fd, &d);
+
+        } else if (RESOURCE_IS_FILESYSTEM(rr->type) && root) {
+                _cleanup_free_ char *resolved = NULL;
+
+                r = chase_symlinks(rr->path, root, CHASE_PREFIX_ROOT, &resolved, NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to resolve '%s': %m", rr->path);
+
+                free_and_replace(rr->path, resolved);
+                return 0;
+        } else
+                return 0; /* Otherwise assume there's nothing to resolve */
+
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine block device of file system: %m");
+
+        r = block_get_whole_disk(d, &d);
+        if (r < 0)
+                return log_error_errno(r, "Failed to find whole disk device for partition backing file system: %m");
+        if (r == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "File system is not placed on a partition block device, cannot determine whole block device backing root file system.");
+
+        r = device_path_make_canonical(S_IFBLK, d, &p);
+        if (r < 0)
+                return r;
+
+        if (rr->path)
+                log_info("Automatically discovered block device '%s' from '%s'.", p, rr->path);
+        else
+                log_info("Automatically discovered root block device '%s'.", p);
+
+        free_and_replace(rr->path, p);
+        return 1;
+}
+
+static const char *resource_type_table[_RESOURCE_TYPE_MAX] = {
+        [RESOURCE_URL_FILE]     = "url-file",
+        [RESOURCE_URL_TAR]      = "url-tar",
+        [RESOURCE_TAR]          = "tar",
+        [RESOURCE_PARTITION]    = "partition",
+        [RESOURCE_REGULAR_FILE] = "regular-file",
+        [RESOURCE_DIRECTORY]    = "directory",
+        [RESOURCE_SUBVOLUME]    = "subvolume",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(resource_type, ResourceType);
diff --git a/src/sysupdate/sysupdate-resource.h b/src/sysupdate/sysupdate-resource.h
new file mode 100644 (file)
index 0000000..86be0d3
--- /dev/null
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+#include "sd-id128.h"
+
+#include "hashmap.h"
+#include "macro.h"
+
+/* Forward declare this type so that the headers below can use it */
+typedef struct Resource Resource;
+
+#include "sysupdate-instance.h"
+
+typedef enum ResourceType {
+        RESOURCE_URL_FILE,
+        RESOURCE_URL_TAR,
+        RESOURCE_TAR,
+        RESOURCE_PARTITION,
+        RESOURCE_REGULAR_FILE,
+        RESOURCE_DIRECTORY,
+        RESOURCE_SUBVOLUME,
+        _RESOURCE_TYPE_MAX,
+        _RESOURCE_TYPE_INVALID = -EINVAL,
+} ResourceType;
+
+static inline bool RESOURCE_IS_SOURCE(ResourceType t) {
+        return IN_SET(t,
+                      RESOURCE_URL_FILE,
+                      RESOURCE_URL_TAR,
+                      RESOURCE_TAR,
+                      RESOURCE_REGULAR_FILE,
+                      RESOURCE_DIRECTORY,
+                      RESOURCE_SUBVOLUME);
+}
+
+static inline bool RESOURCE_IS_TARGET(ResourceType t) {
+        return IN_SET(t,
+                      RESOURCE_PARTITION,
+                      RESOURCE_REGULAR_FILE,
+                      RESOURCE_DIRECTORY,
+                      RESOURCE_SUBVOLUME);
+}
+
+/* Returns true for all resources that deal with file system objects, i.e. where we operate on top of the
+ * file system layer, instead of below. */
+static inline bool RESOURCE_IS_FILESYSTEM(ResourceType t) {
+        return IN_SET(t,
+                      RESOURCE_TAR,
+                      RESOURCE_REGULAR_FILE,
+                      RESOURCE_DIRECTORY,
+                      RESOURCE_SUBVOLUME);
+}
+
+static inline bool RESOURCE_IS_TAR(ResourceType t) {
+        return IN_SET(t,
+                      RESOURCE_TAR,
+                      RESOURCE_URL_TAR);
+}
+
+static inline bool RESOURCE_IS_URL(ResourceType t) {
+        return IN_SET(t,
+                      RESOURCE_URL_TAR,
+                      RESOURCE_URL_FILE);
+}
+
+struct Resource {
+        ResourceType type;
+
+        /* Where to look for instances, and what to match precisely */
+        char *path;
+        bool path_auto; /* automatically find root path (only available if target resource, not source resource) */
+        char **patterns;
+        sd_id128_t partition_type;
+        bool partition_type_set;
+
+        /* All instances of this resource we found */
+        Instance **instances;
+        size_t n_instances;
+
+        /* If this is a partition resource (RESOURCE_PARTITION), then how many partition slots are currently unassigned, that we can use */
+        size_t n_empty;
+};
+
+void resource_destroy(Resource *rr);
+
+int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache);
+
+Instance* resource_find_instance(Resource *rr, const char *version);
+
+int resource_resolve_path(Resource *rr, const char *root, const char *node);
+
+ResourceType resource_type_from_string(const char *s) _pure_;
+const char *resource_type_to_string(ResourceType t) _const_;
diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c
new file mode 100644 (file)
index 0000000..a9fceed
--- /dev/null
@@ -0,0 +1,1247 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "blockdev-util.h"
+#include "chase-symlinks.h"
+#include "conf-parser.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "glyph-util.h"
+#include "gpt.h"
+#include "hexdecoct.h"
+#include "install-file.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "rm-rf.h"
+#include "specifier.h"
+#include "stat-util.h"
+#include "stdio-util.h"
+#include "strv.h"
+#include "sync-util.h"
+#include "sysupdate-pattern.h"
+#include "sysupdate-resource.h"
+#include "sysupdate-transfer.h"
+#include "sysupdate-util.h"
+#include "sysupdate.h"
+#include "tmpfile-util.h"
+#include "web-util.h"
+
+/* Default value for InstancesMax= for fs object targets */
+#define DEFAULT_FILE_INSTANCES_MAX 3
+
+Transfer *transfer_free(Transfer *t) {
+        if (!t)
+                return NULL;
+
+        t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path);
+
+        free(t->definition_path);
+        free(t->min_version);
+        strv_free(t->protected_versions);
+        free(t->current_symlink);
+        free(t->final_path);
+
+        partition_info_destroy(&t->partition_info);
+
+        resource_destroy(&t->source);
+        resource_destroy(&t->target);
+
+        return mfree(t);
+}
+
+Transfer *transfer_new(void) {
+        Transfer *t;
+
+        t = new(Transfer, 1);
+        if (!t)
+                return NULL;
+
+        *t = (Transfer) {
+                .source.type = _RESOURCE_TYPE_INVALID,
+                .target.type = _RESOURCE_TYPE_INVALID,
+                .remove_temporary = true,
+                .mode = MODE_INVALID,
+                .tries_left = UINT64_MAX,
+                .tries_done = UINT64_MAX,
+                .verify = true,
+
+                /* the three flags, as configured by the user */
+                .no_auto = -1,
+                .read_only = -1,
+                .growfs = -1,
+
+                /* the read only flag, as ultimately determined */
+                .install_read_only = -1,
+
+                .partition_info = PARTITION_INFO_NULL,
+        };
+
+        return t;
+}
+
+static const Specifier specifier_table[] = {
+        COMMON_SYSTEM_SPECIFIERS,
+        COMMON_TMP_SPECIFIERS,
+        {}
+};
+
+static int config_parse_protect_version(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        _cleanup_free_ char *resolved = NULL;
+        char ***protected_versions = data;
+        int r;
+
+        assert(rvalue);
+        assert(data);
+
+        r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Failed to expand specifiers in ProtectVersion=, ignoring: %s", rvalue);
+                return 0;
+        }
+
+        if (!version_is_valid(resolved))  {
+                log_syntax(unit, LOG_WARNING, filename, line, 0,
+                           "ProtectVersion= string is not valid, ignoring: %s", resolved);
+                return 0;
+        }
+
+        r = strv_extend(protected_versions, resolved);
+        if (r < 0)
+                return log_oom();
+
+        return 0;
+}
+
+static int config_parse_min_version(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        _cleanup_free_ char *resolved = NULL;
+        char **version = data;
+        int r;
+
+        assert(rvalue);
+        assert(data);
+
+        r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Failed to expand specifiers in MinVersion=, ignoring: %s", rvalue);
+                return 0;
+        }
+
+        if (!version_is_valid(rvalue)) {
+                log_syntax(unit, LOG_WARNING, filename, line, 0,
+                           "MinVersion= string is not valid, ignoring: %s", resolved);
+                return 0;
+        }
+
+        return free_and_replace(*version, resolved);
+}
+
+static int config_parse_current_symlink(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        _cleanup_free_ char *resolved = NULL;
+        char **current_symlink = data;
+        int r;
+
+        assert(rvalue);
+        assert(data);
+
+        r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Failed to expand specifiers in CurrentSymlink=, ignoring: %s", rvalue);
+                return 0;
+        }
+
+        r = path_simplify_and_warn(resolved, 0, unit, filename, line, lvalue);
+        if (r < 0)
+                return 0;
+
+        return free_and_replace(*current_symlink, resolved);
+}
+
+static int config_parse_instances_max(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        uint64_t *instances_max = data, i;
+        int r;
+
+        assert(rvalue);
+        assert(data);
+
+        if (isempty(rvalue)) {
+                *instances_max = 0; /* Revert to default logic, see transfer_read_definition() */
+                return 0;
+        }
+
+        r = safe_atou64(rvalue, &i);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Failed to parse InstancesMax= value, ignoring: %s", rvalue);
+                return 0;
+        }
+
+        if (i < 2) {
+                log_syntax(unit, LOG_WARNING, filename, line, 0,
+                           "InstancesMax= value must be at least 2, bumping: %s", rvalue);
+                *instances_max = 2;
+        } else
+                *instances_max = i;
+
+        return 0;
+}
+
+static int config_parse_resource_pattern(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        char ***patterns = data;
+        int r;
+
+        assert(rvalue);
+        assert(data);
+
+        if (isempty(rvalue)) {
+                *patterns = strv_free(*patterns);
+                return 0;
+        }
+
+        for (;;) {
+                _cleanup_free_ char *word = NULL, *resolved = NULL;
+
+                r = extract_first_word(&rvalue, &word, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX);
+                if (r < 0) {
+                        log_syntax(unit, LOG_WARNING, filename, line, r,
+                                   "Failed to extract first pattern from MatchPattern=, ignoring: %s", rvalue);
+                        return 0;
+                }
+                if (r == 0)
+                        break;
+
+                r = specifier_printf(word, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
+                if (r < 0) {
+                        log_syntax(unit, LOG_WARNING, filename, line, r,
+                                   "Failed to expand specifiers in MatchPattern=, ignoring: %s", rvalue);
+                        return 0;
+                }
+
+                if (!pattern_valid(resolved))
+                        return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
+                                          "MatchPattern= string is not valid, refusing: %s", resolved);
+
+                r = strv_consume(patterns, TAKE_PTR(resolved));
+                if (r < 0)
+                        return log_oom();
+        }
+
+        strv_uniq(*patterns);
+        return 0;
+}
+
+static int config_parse_resource_path(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        _cleanup_free_ char *resolved = NULL;
+        Resource *rr = data;
+        int r;
+
+        assert(rvalue);
+        assert(data);
+
+        if (streq(rvalue, "auto")) {
+                rr->path_auto = true;
+                rr->path = mfree(rr->path);
+                return 0;
+        }
+
+        r = specifier_printf(rvalue, PATH_MAX-1, specifier_table, arg_root, NULL, &resolved);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Failed to expand specifiers in Path=, ignoring: %s", rvalue);
+                return 0;
+        }
+
+        /* Note that we don't validate the path as being absolute or normalized. We'll do that in
+         * transfer_read_definition() as we might not know yet whether Path refers to an URL or a file system
+         * path. */
+
+        rr->path_auto = false;
+        return free_and_replace(rr->path, resolved);
+}
+
+static DEFINE_CONFIG_PARSE_ENUM(config_parse_resource_type, resource_type, ResourceType, "Invalid resource type");
+
+static int config_parse_resource_ptype(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        Resource *rr = data;
+        int r;
+
+        assert(rvalue);
+        assert(data);
+
+        r = gpt_partition_type_uuid_from_string(rvalue, &rr->partition_type);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Failed parse partition type, ignoring: %s", rvalue);
+                return 0;
+        }
+
+        rr->partition_type_set = true;
+        return 0;
+}
+
+static int config_parse_partition_uuid(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        Transfer *t = data;
+        int r;
+
+        assert(rvalue);
+        assert(data);
+
+        r = sd_id128_from_string(rvalue, &t->partition_uuid);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Failed parse partition UUID, ignoring: %s", rvalue);
+                return 0;
+        }
+
+        t->partition_uuid_set = true;
+        return 0;
+}
+
+static int config_parse_partition_flags(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        Transfer *t = data;
+        int r;
+
+        assert(rvalue);
+        assert(data);
+
+        r = safe_atou64(rvalue, &t->partition_flags);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Failed parse partition flags, ignoring: %s", rvalue);
+                return 0;
+        }
+
+        t->partition_flags_set = true;
+        return 0;
+}
+
+int transfer_read_definition(Transfer *t, const char *path) {
+        int r;
+
+        assert(t);
+        assert(path);
+
+        ConfigTableItem table[] = {
+                { "Transfer",    "MinVersion",              config_parse_min_version,          0, &t->min_version        },
+                { "Transfer",    "ProtectVersion",          config_parse_protect_version,      0, &t->protected_versions },
+                { "Transfer",    "Verify",                  config_parse_bool,                 0, &t->verify             },
+                { "Source",      "Type",                    config_parse_resource_type,        0, &t->source.type        },
+                { "Source",      "Path",                    config_parse_resource_path,        0, &t->source             },
+                { "Source",      "MatchPattern",            config_parse_resource_pattern,     0, &t->source.patterns    },
+                { "Target",      "Type",                    config_parse_resource_type,        0, &t->target.type        },
+                { "Target",      "Path",                    config_parse_resource_path,        0, &t->target             },
+                { "Target",      "MatchPattern",            config_parse_resource_pattern,     0, &t->target.patterns    },
+                { "Target",      "MatchPartitionType",      config_parse_resource_ptype,       0, &t->target             },
+                { "Target",      "PartitionUUID",           config_parse_partition_uuid,       0, t                      },
+                { "Target",      "PartitionFlags",          config_parse_partition_flags,      0, t                      },
+                { "Target",      "PartitionNoAuto",         config_parse_tristate,             0, &t->no_auto            },
+                { "Target",      "PartitionGrowFileSystem", config_parse_tristate,             0, &t->growfs             },
+                { "Target",      "ReadOnly",                config_parse_tristate,             0, &t->read_only          },
+                { "Target",      "Mode",                    config_parse_mode,                 0, &t->mode               },
+                { "Target",      "TriesLeft",               config_parse_uint64,               0, &t->tries_left         },
+                { "Target",      "TriesDone",               config_parse_uint64,               0, &t->tries_done         },
+                { "Target",      "InstancesMax",            config_parse_instances_max,        0, &t->instances_max      },
+                { "Target",      "RemoveTemporary",         config_parse_bool,                 0, &t->remove_temporary   },
+                { "Target",      "CurrentSymlink",          config_parse_current_symlink,      0, &t->current_symlink    },
+                {}
+        };
+
+        r = config_parse(NULL, path, NULL,
+                         "Transfer\0"
+                         "Source\0"
+                         "Target\0",
+                         config_item_table_lookup, table,
+                         CONFIG_PARSE_WARN,
+                         t,
+                         NULL);
+        if (r < 0)
+                return r;
+
+        if (!RESOURCE_IS_SOURCE(t->source.type))
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Source Type= must be one of url-file, url-tar, tar, regular-file, directory, subvolume.");
+
+        if (t->target.type < 0) {
+                switch (t->source.type) {
+
+                case RESOURCE_URL_FILE:
+                case RESOURCE_REGULAR_FILE:
+                        t->target.type =
+                                t->target.path && path_startswith(t->target.path, "/dev/") ?
+                                RESOURCE_PARTITION : RESOURCE_REGULAR_FILE;
+                        break;
+
+                case RESOURCE_URL_TAR:
+                case RESOURCE_TAR:
+                case RESOURCE_DIRECTORY:
+                        t->target.type = RESOURCE_DIRECTORY;
+                        break;
+
+                case RESOURCE_SUBVOLUME:
+                        t->target.type = RESOURCE_SUBVOLUME;
+                        break;
+
+                default:
+                        assert_not_reached();
+                }
+        }
+
+        if (!RESOURCE_IS_TARGET(t->target.type))
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Target Type= must be one of partition, regular-file, directory, subvolume.");
+
+        if ((IN_SET(t->source.type, RESOURCE_URL_FILE, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE) &&
+             !IN_SET(t->target.type, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE)) ||
+            (IN_SET(t->source.type, RESOURCE_URL_TAR, RESOURCE_TAR, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME) &&
+             !IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)))
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Target type '%s' is incompatible with source type '%s', refusing.",
+                                  resource_type_to_string(t->source.type), resource_type_to_string(t->target.type));
+
+        if (!t->source.path && !t->source.path_auto)
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Source specification lacks Path=.");
+
+        if (t->source.path) {
+                if (RESOURCE_IS_FILESYSTEM(t->source.type) || t->source.type == RESOURCE_PARTITION)
+                        if (!path_is_absolute(t->source.path) || !path_is_normalized(t->source.path))
+                                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                                  "Source path is not a normalized, absolute path: %s", t->source.path);
+
+                /* We unofficially support file:// in addition to http:// and https:// for url
+                 * sources. That's mostly for testing, since it relieves us from having to set up a HTTP
+                 * server, and CURL abstracts this away from us thankfully. */
+                if (RESOURCE_IS_URL(t->source.type))
+                        if (!http_url_is_valid(t->source.path) && !file_url_is_valid(t->source.path))
+                                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                                  "Source path is not a valid HTTP or HTTPS URL: %s", t->source.path);
+        }
+
+        if (strv_isempty(t->source.patterns))
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Source specification lacks MatchPattern=.");
+
+        if (!t->target.path && !t->target.path_auto)
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Target specification lacks Path= field.");
+
+        if (t->target.path &&
+            (!path_is_absolute(t->target.path) || !path_is_normalized(t->target.path)))
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Target path is not a normalized, absolute path: %s", t->target.path);
+
+        if (strv_isempty(t->target.patterns)) {
+                strv_free(t->target.patterns);
+                t->target.patterns = strv_copy(t->source.patterns);
+                if (!t->target.patterns)
+                        return log_oom();
+        }
+
+        if (t->current_symlink && !RESOURCE_IS_FILESYSTEM(t->target.type) && !path_is_absolute(t->current_symlink))
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Current symlink must be absolute path if target is partition: %s", t->current_symlink);
+
+        /* When no instance limit is set, use all available partition slots in case of partitions, or 3 in case of fs objects */
+        if (t->instances_max == 0)
+                t->instances_max = t->target.type == RESOURCE_PARTITION ? UINT64_MAX : DEFAULT_FILE_INSTANCES_MAX;
+
+        return 0;
+}
+
+int transfer_resolve_paths(
+                Transfer *t,
+                const char *root,
+                const char *node) {
+
+        int r;
+
+        /* If Path=auto is used in [Source] or [Target] sections, let's automatically detect the path of the
+         * block device to use. Moreover, if this path points to a directory but we need a block device,
+         * automatically determine the backing block device, so that users can reference block devices by
+         * mount point. */
+
+        assert(t);
+
+        r = resource_resolve_path(&t->source, root, node);
+        if (r < 0)
+                return r;
+
+        r = resource_resolve_path(&t->target, root, node);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static void transfer_remove_temporary(Transfer *t) {
+        _cleanup_(closedirp) DIR *d = NULL;
+        int r;
+
+        assert(t);
+
+        if (!t->remove_temporary)
+                return;
+
+        if (!IN_SET(t->target.type, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME))
+                return;
+
+        /* Removes all temporary files/dirs from previous runs in the target directory, i.e. all those starting with '.#' */
+
+        d = opendir(t->target.path);
+        if (!d) {
+                if (errno == ENOENT)
+                        return;
+
+                log_debug_errno(errno, "Failed to open target directory '%s', ignoring: %m", t->target.path);
+                return;
+        }
+
+        for (;;) {
+                struct dirent *de;
+
+                errno = 0;
+                de = readdir_no_dot(d);
+                if (!de) {
+                        if (errno != 0)
+                                log_debug_errno(errno, "Failed to read target directory '%s', ignoring: %m", t->target.path);
+                        break;
+                }
+
+                if (!startswith(de->d_name, ".#"))
+                        continue;
+
+                r = rm_rf_child(dirfd(d), de->d_name, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD);
+                if (r == -ENOENT)
+                        continue;
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to remove temporary resource instance '%s/%s', ignoring: %m", t->target.path, de->d_name);
+                        continue;
+                }
+
+                log_debug("Removed temporary resource instance '%s/%s'.", t->target.path, de->d_name);
+        }
+}
+
+int transfer_vacuum(
+                Transfer *t,
+                uint64_t space,
+                const char *extra_protected_version) {
+
+        uint64_t instances_max, limit;
+        int r, count = 0;
+
+        assert(t);
+
+        transfer_remove_temporary(t);
+
+        /* First, calculate how many instances to keep, based on the instance limit â€” but keep at least one */
+
+        instances_max = arg_instances_max != UINT64_MAX ? arg_instances_max : t->instances_max;
+        assert(instances_max >= 1);
+        if (instances_max == UINT64_MAX) /* Keep infinite instances? */
+                limit = UINT64_MAX;
+        else if (space > instances_max)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+                                       "Asked to delete more instances than total maximum allowed number of instances, refusing.");
+        else if (space == instances_max)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+                                       "Asked to delete all possible instances, can't allow that. One instance must always remain.");
+        else
+                limit = instances_max - space;
+
+        if (t->target.type == RESOURCE_PARTITION) {
+                uint64_t rm, remain;
+
+                /* If we are looking at a partition table, we also have to take into account how many
+                 * partition slots of the right type are available */
+
+                if (t->target.n_empty + t->target.n_instances < 2)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+                                               "Partition table has less than two partition slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), refusing.",
+                                               SD_ID128_FORMAT_VAL(t->target.partition_type),
+                                               gpt_partition_type_uuid_to_string(t->target.partition_type));
+                if (space > t->target.n_empty + t->target.n_instances)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+                                               "Partition table does not have enough partition slots of right type " SD_ID128_UUID_FORMAT_STR " (%s) for operation.",
+                                               SD_ID128_FORMAT_VAL(t->target.partition_type),
+                                               gpt_partition_type_uuid_to_string(t->target.partition_type));
+                if (space == t->target.n_empty + t->target.n_instances)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
+                                               "Asked to empty all partition table slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), can't allow that. One instance must always remain.",
+                                               SD_ID128_FORMAT_VAL(t->target.partition_type),
+                                               gpt_partition_type_uuid_to_string(t->target.partition_type));
+
+                rm = LESS_BY(space, t->target.n_empty);
+                remain = LESS_BY(t->target.n_instances, rm);
+                limit = MIN(limit, remain);
+        }
+
+        while (t->target.n_instances > limit) {
+                Instance *oldest;
+                size_t p = t->target.n_instances - 1;
+
+                for (;;) {
+                        oldest = t->target.instances[p];
+                        assert(oldest);
+
+                        /* If this is listed among the protected versions, then let's not remove it */
+                        if (!strv_contains(t->protected_versions, oldest->metadata.version) &&
+                            (!extra_protected_version || !streq(extra_protected_version, oldest->metadata.version)))
+                                break;
+
+                        log_debug("Version '%s' is protected, not removing.", oldest->metadata.version);
+                        if (p == 0) {
+                                oldest = NULL;
+                                break;
+                        }
+
+                        p--;
+                }
+
+                if (!oldest) /* Nothing more to remove */
+                        break;
+
+                assert(oldest->resource);
+
+                log_info("%s Removing old '%s' (%s).", special_glyph(SPECIAL_GLYPH_RECYCLING), oldest->path, resource_type_to_string(oldest->resource->type));
+
+                switch (t->target.type) {
+
+                case RESOURCE_REGULAR_FILE:
+                case RESOURCE_DIRECTORY:
+                case RESOURCE_SUBVOLUME:
+                        r = rm_rf(oldest->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD);
+                        if (r < 0 && r != -ENOENT)
+                                return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", oldest->path);
+
+                        break;
+
+                case RESOURCE_PARTITION: {
+                        PartitionInfo pinfo = oldest->partition_info;
+
+                        /* label "_empty" means "no contents" for our purposes */
+                        pinfo.label = (char*) "_empty";
+
+                        r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL);
+                        if (r < 0)
+                                return r;
+
+                        t->target.n_empty++;
+                        break;
+                }
+
+                default:
+                        assert_not_reached();
+                        break;
+                }
+
+                instance_free(oldest);
+                memmove(t->target.instances + p, t->target.instances + p + 1, (t->target.n_instances - p - 1) * sizeof(Instance*));
+                t->target.n_instances--;
+
+                count++;
+        }
+
+        return count;
+}
+
+static void compile_pattern_fields(
+                const Transfer *t,
+                const Instance *i,
+                InstanceMetadata *ret) {
+
+        assert(t);
+        assert(i);
+        assert(ret);
+
+        *ret = (InstanceMetadata) {
+                .version = i->metadata.version,
+
+                /* We generally prefer explicitly configured values for the transfer over those automatically
+                 * derived from the source instance. Also, if the source is a tar archive, then let's not
+                 * patch mtime/mode and use the one embedded in the tar file */
+                .partition_uuid = t->partition_uuid_set ? t->partition_uuid : i->metadata.partition_uuid,
+                .partition_uuid_set = t->partition_uuid_set || i->metadata.partition_uuid_set,
+                .partition_flags = t->partition_flags_set ? t->partition_flags : i->metadata.partition_flags,
+                .partition_flags_set = t->partition_flags_set || i->metadata.partition_flags_set,
+                .mtime = RESOURCE_IS_TAR(i->resource->type) ? USEC_INFINITY : i->metadata.mtime,
+                .mode = t->mode != MODE_INVALID ? t->mode : (RESOURCE_IS_TAR(i->resource->type) ? MODE_INVALID : i->metadata.mode),
+                .size = i->metadata.size,
+                .tries_done = t->tries_done != UINT64_MAX ? t->tries_done :
+                              i->metadata.tries_done != UINT64_MAX ? i->metadata.tries_done : 0,
+                .tries_left = t->tries_left != UINT64_MAX ? t->tries_left :
+                              i->metadata.tries_left != UINT64_MAX ? i->metadata.tries_left : 3,
+                .no_auto = t->no_auto >= 0 ? t->no_auto : i->metadata.no_auto,
+                .read_only = t->read_only >= 0 ? t->read_only : i->metadata.read_only,
+                .growfs = t->growfs >= 0 ? t->growfs : i->metadata.growfs,
+                .sha256sum_set = i->metadata.sha256sum_set,
+        };
+
+        memcpy(ret->sha256sum, i->metadata.sha256sum, sizeof(ret->sha256sum));
+}
+
+static int run_helper(
+                const char *name,
+                const char *path,
+                const char * const cmdline[]) {
+
+        int r;
+
+        assert(name);
+        assert(path);
+        assert(cmdline);
+
+        r = safe_fork(name, FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG|FORK_WAIT, NULL);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                /* Child */
+
+                (void) unsetenv("NOTIFY_SOCKET");
+                execv(path, (char *const*) cmdline);
+                log_error_errno(errno, "Failed to execute %s tool: %m", path);
+                _exit(EXIT_FAILURE);
+        }
+
+        return 0;
+}
+
+int transfer_acquire_instance(Transfer *t, Instance *i) {
+        _cleanup_free_ char *formatted_pattern = NULL, *digest = NULL;
+        char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1];
+        const char *where = NULL;
+        InstanceMetadata f;
+        Instance *existing;
+        int r;
+
+        assert(t);
+        assert(i);
+        assert(i->resource);
+        assert(t == container_of(i->resource, Transfer, source));
+
+        /* Does this instance already exist in the target? Then we don't need to acquire anything */
+        existing = resource_find_instance(&t->target, i->metadata.version);
+        if (existing) {
+                log_info("No need to acquire '%s', already installed.", i->path);
+                return 0;
+        }
+
+        assert(!t->final_path);
+        assert(!t->temporary_path);
+        assert(!strv_isempty(t->target.patterns));
+
+        /* Format the target name using the first pattern specified */
+        compile_pattern_fields(t, i, &f);
+        r = pattern_format(t->target.patterns[0], &f, &formatted_pattern);
+        if (r < 0)
+                return log_error_errno(r, "Failed to format target pattern: %m");
+
+        if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
+
+                if (!filename_is_valid(formatted_pattern))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern);
+
+                t->final_path = path_join(t->target.path, formatted_pattern);
+                if (!t->final_path)
+                        return log_oom();
+
+                r = tempfn_random(t->final_path, "sysupdate", &t->temporary_path);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to generate temporary target path: %m");
+
+                where = t->final_path;
+        }
+
+        if (t->target.type == RESOURCE_PARTITION) {
+                r = gpt_partition_label_valid(formatted_pattern);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pattern);
+                if (!r)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern);
+
+                r = find_suitable_partition(
+                                t->target.path,
+                                i->metadata.size,
+                                t->target.partition_type_set ? &t->target.partition_type : NULL,
+                                &t->partition_info);
+                if (r < 0)
+                        return r;
+
+                xsprintf(offset, "%" PRIu64, t->partition_info.start);
+                xsprintf(max_size, "%" PRIu64, t->partition_info.size);
+
+                where = t->partition_info.device;
+        }
+
+        assert(where);
+
+        log_info("%s Acquiring %s %s %s...", special_glyph(SPECIAL_GLYPH_DOWNLOAD), i->path, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), where);
+
+        if (RESOURCE_IS_URL(i->resource->type)) {
+                /* For URL sources we require the SHA256 sum to be known so that we can validate the
+                 * download. */
+
+                if (!i->metadata.sha256sum_set)
+                        return log_error_errno(r, "SHA256 checksum not known for download '%s', refusing.", i->path);
+
+                digest = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
+                if (!digest)
+                        return log_oom();
+        }
+
+        switch (i->resource->type) { /* Source */
+
+        case RESOURCE_REGULAR_FILE:
+
+                switch (t->target.type) { /* Target */
+
+                case RESOURCE_REGULAR_FILE:
+
+                        /* regular file â†’ regular file (why fork off systemd-import for such a simple file
+                         * copy case? implicit decompression mostly, and thus also sandboxing. Also, the
+                         * importer has some tricks up its sleeve, such as sparse file generation, which we
+                         * want to take benefit of, too.) */
+
+                        r = run_helper("(sd-import-raw)",
+                                       import_binary_path(),
+                                       (const char* const[]) {
+                                               "systemd-import",
+                                               "raw",
+                                               "--direct",          /* just copy/unpack the specified file, don't do anything else */
+                                               arg_sync ? "--sync=yes" : "--sync=no",
+                                               i->path,
+                                               t->temporary_path,
+                                               NULL
+                                       });
+                        break;
+
+                case RESOURCE_PARTITION:
+
+                        /* regular file â†’ partition */
+
+                        r = run_helper("(sd-import-raw)",
+                                       import_binary_path(),
+                                       (const char* const[]) {
+                                               "systemd-import",
+                                               "raw",
+                                               "--direct",          /* just copy/unpack the specified file, don't do anything else */
+                                               "--offset", offset,
+                                               "--size-max", max_size,
+                                               arg_sync ? "--sync=yes" : "--sync=no",
+                                               i->path,
+                                               t->target.path,
+                                               NULL
+                                       });
+                        break;
+
+                default:
+                        assert_not_reached();
+                }
+
+                break;
+
+        case RESOURCE_DIRECTORY:
+        case RESOURCE_SUBVOLUME:
+                assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+
+                /* directory/subvolume â†’ directory/subvolume */
+
+                r = run_helper("(sd-import-fs)",
+                               import_fs_binary_path(),
+                               (const char* const[]) {
+                                       "systemd-import-fs",
+                                       "run",
+                                       "--direct",          /* just untar the specified file, don't do anything else */
+                                       arg_sync ? "--sync=yes" : "--sync=no",
+                                       t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
+                                       i->path,
+                                       t->temporary_path,
+                                       NULL
+                               });
+                break;
+
+        case RESOURCE_TAR:
+                assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+
+                /* tar â†’ directory/subvolume */
+
+                r = run_helper("(sd-import-tar)",
+                               import_binary_path(),
+                               (const char* const[]) {
+                                       "systemd-import",
+                                       "tar",
+                                       "--direct",          /* just untar the specified file, don't do anything else */
+                                       arg_sync ? "--sync=yes" : "--sync=no",
+                                       t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
+                                       i->path,
+                                       t->temporary_path,
+                                       NULL
+                               });
+                break;
+
+        case RESOURCE_URL_FILE:
+
+                switch (t->target.type) {
+
+                case RESOURCE_REGULAR_FILE:
+
+                        /* url file â†’ regular file */
+
+                        r = run_helper("(sd-pull-raw)",
+                                       pull_binary_path(),
+                                       (const char* const[]) {
+                                               "systemd-pull",
+                                               "raw",
+                                               "--direct",          /* just download the specified URL, don't download anything else */
+                                               "--verify", digest,  /* validate by explicit SHA256 sum */
+                                               arg_sync ? "--sync=yes" : "--sync=no",
+                                               i->path,
+                                               t->temporary_path,
+                                               NULL
+                                       });
+                        break;
+
+                case RESOURCE_PARTITION:
+
+                        /* url file â†’ partition */
+
+                        r = run_helper("(sd-pull-raw)",
+                                       pull_binary_path(),
+                                       (const char* const[]) {
+                                               "systemd-pull",
+                                               "raw",
+                                               "--direct",              /* just download the specified URL, don't download anything else */
+                                               "--verify", digest,      /* validate by explicit SHA256 sum */
+                                               "--offset", offset,
+                                               "--size-max", max_size,
+                                               arg_sync ? "--sync=yes" : "--sync=no",
+                                               i->path,
+                                               t->target.path,
+                                               NULL
+                                       });
+                        break;
+
+                default:
+                        assert_not_reached();
+                }
+
+                break;
+
+        case RESOURCE_URL_TAR:
+                assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+
+                r = run_helper("(sd-pull-tar)",
+                               pull_binary_path(),
+                               (const char*const[]) {
+                                       "systemd-pull",
+                                       "tar",
+                                       "--direct",          /* just download the specified URL, don't download anything else */
+                                       "--verify", digest,  /* validate by explicit SHA256 sum */
+                                       t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
+                                       arg_sync ? "--sync=yes" : "--sync=no",
+                                       i->path,
+                                       t->temporary_path,
+                                       NULL
+                               });
+                break;
+
+        default:
+                assert_not_reached();
+        }
+        if (r < 0)
+                return r;
+
+        if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
+                bool need_sync = false;
+                assert(t->temporary_path);
+
+                /* Apply file attributes if set */
+                if (f.mtime != USEC_INFINITY) {
+                        struct timespec ts;
+
+                        timespec_store(&ts, f.mtime);
+
+                        if (utimensat(AT_FDCWD, t->temporary_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0)
+                                return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_path);
+
+                        need_sync = true;
+                }
+
+                if (f.mode != MODE_INVALID) {
+                        /* Try with AT_SYMLINK_NOFOLLOW first, because it's the safe thing to do. Older
+                         * kernels don't support that however, in that case we fall back to chmod(). Not as
+                         * safe, but shouldn't be a problem, given that we don't create symlinks here. */
+                        if (fchmodat(AT_FDCWD, t->temporary_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 &&
+                            (!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_path, f.mode) < 0))
+                                return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_path);
+
+                        need_sync = true;
+                }
+
+                /* Synchronize */
+                if (arg_sync && need_sync) {
+                        if (t->target.type == RESOURCE_REGULAR_FILE)
+                                r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_path);
+                        else {
+                                assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
+                                r = syncfs_path(AT_FDCWD, t->temporary_path);
+                        }
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_path);
+                }
+
+                t->install_read_only = f.read_only;
+        }
+
+        if (t->target.type == RESOURCE_PARTITION) {
+                free_and_replace(t->partition_info.label, formatted_pattern);
+                t->partition_change = PARTITION_LABEL;
+
+                if (f.partition_uuid_set) {
+                        t->partition_info.uuid = f.partition_uuid;
+                        t->partition_change |= PARTITION_UUID;
+                }
+
+                if (f.partition_flags_set) {
+                        t->partition_info.flags = f.partition_flags;
+                        t->partition_change |= PARTITION_FLAGS;
+                }
+
+                if (f.no_auto >= 0) {
+                        t->partition_info.no_auto = f.no_auto;
+                        t->partition_change |= PARTITION_NO_AUTO;
+                }
+
+                if (f.read_only >= 0) {
+                        t->partition_info.read_only = f.read_only;
+                        t->partition_change |= PARTITION_READ_ONLY;
+                }
+
+                if (f.growfs >= 0) {
+                        t->partition_info.growfs = f.growfs;
+                        t->partition_change |= PARTITION_GROWFS;
+                }
+        }
+
+        /* For regular file cases the only step left is to install the file in place, which install_file()
+         * will do via rename(). For partition cases the only step left is to update the partition table,
+         * which is done at the same place. */
+
+        log_info("Successfully acquired '%s'.", i->path);
+        return 0;
+}
+
+int transfer_install_instance(
+                Transfer *t,
+                Instance *i,
+                const char *root) {
+
+        int r;
+
+        assert(t);
+        assert(i);
+        assert(i->resource);
+        assert(t == container_of(i->resource, Transfer, source));
+
+        if (t->temporary_path) {
+                assert(RESOURCE_IS_FILESYSTEM(t->target.type));
+                assert(t->final_path);
+
+                r = install_file(AT_FDCWD, t->temporary_path,
+                                 AT_FDCWD, t->final_path,
+                                 INSTALL_REPLACE|
+                                 (t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)|
+                                 (t->target.type == RESOURCE_REGULAR_FILE ? INSTALL_FSYNC_FULL : INSTALL_SYNCFS));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to move '%s' into place: %m", t->final_path);
+
+                log_info("Successfully installed '%s' (%s) as '%s' (%s).",
+                         i->path,
+                         resource_type_to_string(i->resource->type),
+                         t->final_path,
+                         resource_type_to_string(t->target.type));
+
+                t->temporary_path = mfree(t->temporary_path);
+        }
+
+        if (t->partition_change != 0) {
+                assert(t->target.type == RESOURCE_PARTITION);
+
+                r = patch_partition(
+                                t->target.path,
+                                &t->partition_info,
+                                t->partition_change);
+                if (r < 0)
+                        return r;
+
+                log_info("Successfully installed '%s' (%s) as '%s' (%s).",
+                         i->path,
+                         resource_type_to_string(i->resource->type),
+                         t->partition_info.device,
+                         resource_type_to_string(t->target.type));
+        }
+
+        if (t->current_symlink) {
+                _cleanup_free_ char *buf = NULL, *parent = NULL, *relative = NULL, *resolved = NULL;
+                const char *link_path, *link_target;
+                bool resolve_link_path = false;
+
+                if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
+
+                        assert(t->target.path);
+
+                        if (path_is_absolute(t->current_symlink)) {
+                                link_path = t->current_symlink;
+                                resolve_link_path = true;
+                        } else {
+                                buf = path_make_absolute(t->current_symlink, t->target.path);
+                                if (!buf)
+                                        return log_oom();
+
+                                link_path = buf;
+                        }
+
+                        link_target = t->final_path;
+
+                } else if (t->target.type == RESOURCE_PARTITION) {
+
+                        assert(path_is_absolute(t->current_symlink));
+
+                        link_path = t->current_symlink;
+                        link_target = t->partition_info.device;
+
+                        resolve_link_path = true;
+                } else
+                        assert_not_reached();
+
+                if (resolve_link_path && root) {
+                        r = chase_symlinks(link_path, root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to resolve current symlink path '%s': %m", link_path);
+
+                        link_path = resolved;
+                }
+
+                if (link_target) {
+                        r = path_extract_directory(link_path, &parent);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to extract directory of target path '%s': %m", link_path);
+
+                        r = path_make_relative(parent, link_target, &relative);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to make symlink path '%s' relative to '%s': %m", link_target, parent);
+
+                        r = symlink_atomic(relative, link_path);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to update current symlink '%s' â†’ '%s': %m", link_path, relative);
+
+                        log_info("Updated symlink '%s' â†’ '%s'.", link_path, relative);
+                }
+        }
+
+        return 0;
+}
diff --git a/src/sysupdate/sysupdate-transfer.h b/src/sysupdate/sysupdate-transfer.h
new file mode 100644 (file)
index 0000000..b0c2a6e
--- /dev/null
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+#include "sd-id128.h"
+
+/* Forward declare this type so that the headers below can use it */
+typedef struct Transfer Transfer;
+
+#include "sysupdate-partition.h"
+#include "sysupdate-resource.h"
+
+struct Transfer {
+        char *definition_path;
+        char *min_version;
+        char **protected_versions;
+        char *current_symlink;
+        bool verify;
+
+        Resource source, target;
+
+        uint64_t instances_max;
+        bool remove_temporary;
+
+        /* When creating a new partition/file, optionally override these attributes explicitly */
+        sd_id128_t partition_uuid;
+        bool partition_uuid_set;
+        uint64_t partition_flags;
+        bool partition_flags_set;
+        mode_t mode;
+        uint64_t tries_left, tries_done;
+        int no_auto;
+        int read_only;
+        int growfs;
+
+        /* If we create a new file/dir/subvol in the fs, the temporary and final path we create it under, as well as the read-only flag for it */
+        char *temporary_path;
+        char *final_path;
+        int install_read_only;
+
+        /* If we write to a partition in a partition table, the metrics of it */
+        PartitionInfo partition_info;
+        PartitionChange partition_change;
+};
+
+Transfer *transfer_new(void);
+
+Transfer *transfer_free(Transfer *t);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Transfer*, transfer_free);
+
+int transfer_read_definition(Transfer *t, const char *path);
+
+int transfer_resolve_paths(Transfer *t, const char *root, const char *node);
+
+int transfer_vacuum(Transfer *t, uint64_t space, const char *extra_protected_version);
+
+int transfer_acquire_instance(Transfer *t, Instance *i);
+
+int transfer_install_instance(Transfer *t, Instance *i, const char *root);
diff --git a/src/sysupdate/sysupdate-update-set.c b/src/sysupdate/sysupdate-update-set.c
new file mode 100644 (file)
index 0000000..6d6051d
--- /dev/null
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "glyph-util.h"
+#include "string-util.h"
+#include "sysupdate-update-set.h"
+#include "terminal-util.h"
+
+UpdateSet *update_set_free(UpdateSet *us) {
+        if (!us)
+                return NULL;
+
+        free(us->version);
+        free(us->instances); /* The objects referenced by this array are freed via resource_free(), not us */
+
+        return mfree(us);
+}
+
+int update_set_cmp(UpdateSet *const*a, UpdateSet *const*b) {
+        assert(a);
+        assert(b);
+        assert(*a);
+        assert(*b);
+        assert((*a)->version);
+        assert((*b)->version);
+
+        /* Newest version at the beginning */
+        return -strverscmp_improved((*a)->version, (*b)->version);
+}
+
+const char *update_set_flags_to_color(UpdateSetFlags flags) {
+
+        if (flags == 0 || (flags & UPDATE_OBSOLETE))
+                return (flags & UPDATE_NEWEST) ? ansi_highlight_grey() : ansi_grey();
+
+        if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST))
+                return ansi_highlight();
+
+        if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_PROTECTED))
+                return ansi_highlight_magenta();
+
+        if ((flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_OBSOLETE)) == (UPDATE_AVAILABLE|UPDATE_NEWEST))
+                return ansi_highlight_green();
+
+        return NULL;
+}
+
+const char *update_set_flags_to_glyph(UpdateSetFlags flags) {
+
+        if (flags == 0 || (flags & UPDATE_OBSOLETE))
+                return special_glyph(SPECIAL_GLYPH_MULTIPLICATION_SIGN);
+
+        if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST))
+                return special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE);
+
+        if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_PROTECTED))
+                return special_glyph(SPECIAL_GLYPH_WHITE_CIRCLE);
+
+        if ((flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_OBSOLETE)) == (UPDATE_AVAILABLE|UPDATE_NEWEST))
+                return special_glyph(SPECIAL_GLYPH_CIRCLE_ARROW);
+
+        return " ";
+}
diff --git a/src/sysupdate/sysupdate-update-set.h b/src/sysupdate/sysupdate-update-set.h
new file mode 100644 (file)
index 0000000..5dd94bc
--- /dev/null
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+typedef struct UpdateSet UpdateSet;
+
+#include "sysupdate-instance.h"
+
+typedef enum UpdateSetFlags {
+        UPDATE_NEWEST    = 1 << 0,
+        UPDATE_AVAILABLE = 1 << 1,
+        UPDATE_INSTALLED = 1 << 2,
+        UPDATE_OBSOLETE  = 1 << 3,
+        UPDATE_PROTECTED = 1 << 4,
+} UpdateSetFlags;
+
+struct UpdateSet {
+        UpdateSetFlags flags;
+        char *version;
+        Instance **instances;
+        size_t n_instances;
+};
+
+UpdateSet *update_set_free(UpdateSet *us);
+
+int update_set_cmp(UpdateSet *const*a, UpdateSet *const*b);
+
+const char *update_set_flags_to_color(UpdateSetFlags flags);
+const char *update_set_flags_to_glyph(UpdateSetFlags flags);
diff --git a/src/sysupdate/sysupdate-util.c b/src/sysupdate/sysupdate-util.c
new file mode 100644 (file)
index 0000000..c7a2301
--- /dev/null
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "path-util.h"
+#include "sysupdate-util.h"
+
+bool version_is_valid(const char *s) {
+        if (isempty(s))
+                return false;
+
+        if (!filename_is_valid(s))
+                return false;
+
+        if (!in_charset(s, ALPHANUMERICAL ".,_-+"))
+                return false;
+
+        return true;
+}
diff --git a/src/sysupdate/sysupdate-util.h b/src/sysupdate/sysupdate-util.h
new file mode 100644 (file)
index 0000000..afa3a9d
--- /dev/null
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+bool version_is_valid(const char *s);
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
new file mode 100644 (file)
index 0000000..82787a7
--- /dev/null
@@ -0,0 +1,1412 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "chase-symlinks.h"
+#include "conf-files.h"
+#include "def.h"
+#include "dirent-util.h"
+#include "dissect-image.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "glyph-util.h"
+#include "hexdecoct.h"
+#include "login-util.h"
+#include "main-func.h"
+#include "mount-util.h"
+#include "os-util.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "set.h"
+#include "sort-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "sysupdate-transfer.h"
+#include "sysupdate-update-set.h"
+#include "sysupdate.h"
+#include "terminal-util.h"
+#include "utf8.h"
+#include "verbs.h"
+
+static char *arg_definitions = NULL;
+bool arg_sync = true;
+uint64_t arg_instances_max = UINT64_MAX;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+char *arg_root = NULL;
+static char *arg_image = NULL;
+static bool arg_reboot = false;
+static char *arg_component = NULL;
+static int arg_verify = -1;
+
+STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
+
+typedef struct Context {
+        Transfer **transfers;
+        size_t n_transfers;
+
+        UpdateSet **update_sets;
+        size_t n_update_sets;
+
+        UpdateSet *newest_installed, *candidate;
+
+        Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */
+} Context;
+
+static Context *context_free(Context *c) {
+        if (!c)
+                return NULL;
+
+        for (size_t i = 0; i < c->n_transfers; i++)
+                transfer_free(c->transfers[i]);
+        free(c->transfers);
+
+        for (size_t i = 0; i < c->n_update_sets; i++)
+                update_set_free(c->update_sets[i]);
+        free(c->update_sets);
+
+        hashmap_free(c->web_cache);
+
+        return mfree(c);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Context*, context_free);
+
+static Context *context_new(void) {
+        /* For now, no fields to initialize non-zero */
+        return new0(Context, 1);
+}
+
+static int context_read_definitions(
+                Context *c,
+                const char *directory,
+                const char *component,
+                const char *root,
+                const char *node) {
+
+        _cleanup_strv_free_ char **files = NULL;
+        char **f;
+        int r;
+
+        assert(c);
+
+        if (directory)
+                r = conf_files_list_strv(&files, ".conf", NULL, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) STRV_MAKE(directory));
+        else if (component) {
+                _cleanup_strv_free_ char **n = NULL;
+                char **l = CONF_PATHS_STRV(""), **i;
+                size_t k = 0;
+
+                n = new0(char*, strv_length(l) + 1);
+                if (!n)
+                        return log_oom();
+
+                STRV_FOREACH(i, l) {
+                        char *j;
+
+                        j = strjoin(*i, "sysupdate.", component, ".d");
+                        if (!j)
+                                return log_oom();
+
+                        n[k++] = j;
+                }
+
+                r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) n);
+        } else
+                r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) CONF_PATHS_STRV("sysupdate.d"));
+        if (r < 0)
+                return log_error_errno(r, "Failed to enumerate *.conf files: %m");
+
+        STRV_FOREACH(f, files) {
+                _cleanup_(transfer_freep) Transfer *t = NULL;
+
+                if (!GREEDY_REALLOC(c->transfers, c->n_transfers + 1))
+                        return log_oom();
+
+                t = transfer_new();
+                if (!t)
+                        return log_oom();
+
+                t->definition_path = strdup(*f);
+                if (!t->definition_path)
+                        return log_oom();
+
+                r = transfer_read_definition(t, *f);
+                if (r < 0)
+                        return r;
+
+                c->transfers[c->n_transfers++] = TAKE_PTR(t);
+        }
+
+        if (c->n_transfers == 0) {
+                if (arg_component)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+                                               "No transfer definitions for component '%s' found.", arg_component);
+
+                return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+                                       "No transfer definitions found.");
+        }
+
+        for (size_t i = 0; i < c->n_transfers; i++) {
+                r = transfer_resolve_paths(c->transfers[i], root, node);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int context_load_installed_instances(Context *c) {
+        int r;
+
+        assert(c);
+
+        log_info("Discovering installed instances…");
+
+        for (size_t i = 0; i < c->n_transfers; i++) {
+                r = resource_load_instances(
+                                &c->transfers[i]->target,
+                                arg_verify >= 0 ? arg_verify : c->transfers[i]->verify,
+                                &c->web_cache);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int context_load_available_instances(Context *c) {
+        int r;
+
+        assert(c);
+
+        log_info("Discovering available instances…");
+
+        for (size_t i = 0; i < c->n_transfers; i++) {
+                assert(c->transfers[i]);
+
+                r = resource_load_instances(
+                                &c->transfers[i]->source,
+                                arg_verify >= 0 ? arg_verify : c->transfers[i]->verify,
+                                &c->web_cache);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) {
+        _cleanup_free_ Instance **cursor_instances = NULL;
+        _cleanup_free_ char *boundary = NULL;
+        bool newest_found = false;
+        int r;
+
+        assert(c);
+        assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED));
+
+        for (;;) {
+                bool incomplete = false, exists = false;
+                UpdateSetFlags extra_flags = 0;
+                _cleanup_free_ char *cursor = NULL;
+                UpdateSet *us = NULL;
+
+                for (size_t k = 0; k < c->n_transfers; k++) {
+                        Transfer *t = c->transfers[k];
+                        bool cursor_found = false;
+                        Resource *rr;
+
+                        assert(t);
+
+                        if (flags == UPDATE_AVAILABLE)
+                                rr = &t->source;
+                        else {
+                                assert(flags == UPDATE_INSTALLED);
+                                rr = &t->target;
+                        }
+
+                        for (size_t j = 0; j < rr->n_instances; j++) {
+                                Instance *i = rr->instances[j];
+
+                                assert(i);
+
+                                /* Is the instance we are looking at equal or newer than the boundary? If so, we
+                                 * already checked this version, and it wasn't complete, let's ignore it. */
+                                if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0)
+                                        continue;
+
+                                if (cursor) {
+                                        if (strverscmp_improved(i->metadata.version, cursor) != 0)
+                                                continue;
+                                } else {
+                                        cursor = strdup(i->metadata.version);
+                                        if (!cursor)
+                                                return log_oom();
+                                }
+
+                                cursor_found = true;
+
+                                if (!cursor_instances) {
+                                        cursor_instances = new(Instance*, c->n_transfers);
+                                        if (!cursor_instances)
+                                                return -ENOMEM;
+                                }
+                                cursor_instances[k] = i;
+                                break;
+                        }
+
+                        if (!cursor) /* No suitable instance beyond the boundary found? Then we are done! */
+                                break;
+
+                        if (!cursor_found) {
+                                /* Hmm, we didn't find the version indicated by 'cursor' among the instances
+                                 * of this transfer, let's skip it. */
+                                incomplete = true;
+                                break;
+                        }
+
+                        if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0)
+                                extra_flags |= UPDATE_OBSOLETE;
+
+                        if (strv_contains(t->protected_versions, cursor))
+                                extra_flags |= UPDATE_PROTECTED;
+                }
+
+                if (!cursor) /* EOL */
+                        break;
+
+                r = free_and_strdup_warn(&boundary, cursor);
+                if (r < 0)
+                        return r;
+
+                if (incomplete) /* One transfer was missing this version, ignore the whole thing */
+                        continue;
+
+                /* See if we already have this update set in our table */
+                for (size_t i = 0; i < c->n_update_sets; i++) {
+                        if (strverscmp_improved(c->update_sets[i]->version, cursor) != 0)
+                                continue;
+
+                        /* We only store the instances we found first, but we remember we also found it again */
+                        c->update_sets[i]->flags |= flags | extra_flags;
+                        exists = true;
+                        newest_found = true;
+                        break;
+                }
+
+                if (exists)
+                        continue;
+
+                /* Doesn't exist yet, let's add it */
+                if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1))
+                        return log_oom();
+
+                us = new(UpdateSet, 1);
+                if (!us)
+                        return log_oom();
+
+                *us = (UpdateSet) {
+                        .flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags,
+                        .version = TAKE_PTR(cursor),
+                        .instances = TAKE_PTR(cursor_instances),
+                        .n_instances = c->n_transfers,
+                };
+
+                c->update_sets[c->n_update_sets++] = us;
+
+                newest_found = true;
+
+                /* Remember which one is the newest installed */
+                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED))
+                        c->newest_installed = us;
+
+                /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate" */
+                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
+                        c->candidate = us;
+        }
+
+        /* Newest installed is newer than or equal to candidate? Then suppress the candidate */
+        if (c->newest_installed && c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
+                c->candidate = NULL;
+
+        return 0;
+}
+
+static int context_discover_update_sets(Context *c) {
+        int r;
+
+        assert(c);
+
+        log_info("Determining installed update sets…");
+
+        r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED);
+        if (r < 0)
+                return r;
+
+        log_info("Determining available update sets…");
+
+        r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE);
+        if (r < 0)
+                return r;
+
+        typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp);
+        return 0;
+}
+
+static const char *update_set_flags_to_string(UpdateSetFlags flags) {
+
+        switch ((unsigned) flags) {
+
+        case 0:
+                return "n/a";
+
+        case UPDATE_INSTALLED|UPDATE_NEWEST:
+        case UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_PROTECTED:
+        case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST:
+        case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
+                return "current";
+
+        case UPDATE_AVAILABLE|UPDATE_NEWEST:
+        case UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
+                return "candidate";
+
+        case UPDATE_INSTALLED:
+        case UPDATE_INSTALLED|UPDATE_AVAILABLE:
+                return "installed";
+
+        case UPDATE_INSTALLED|UPDATE_PROTECTED:
+        case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PROTECTED:
+                return "protected";
+
+        case UPDATE_AVAILABLE:
+        case UPDATE_AVAILABLE|UPDATE_PROTECTED:
+                return "available";
+
+        case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST:
+        case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED:
+        case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST:
+        case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED:
+                return "current+obsolete";
+
+        case UPDATE_INSTALLED|UPDATE_OBSOLETE:
+        case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE:
+                return "installed+obsolete";
+
+        case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_PROTECTED:
+        case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED:
+                return "protected+obsolete";
+
+        case UPDATE_AVAILABLE|UPDATE_OBSOLETE:
+        case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED:
+        case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST:
+        case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED:
+                return "available+obsolete";
+
+        default:
+                assert_not_reached();
+        }
+}
+
+
+static int context_show_table(Context *c) {
+        _cleanup_(table_unrefp) Table *t = NULL;
+        int r;
+
+        assert(c);
+
+        t = table_new("", "version", "installed", "available", "assessment");
+        if (!t)
+                return log_oom();
+
+        (void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100);
+        (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50);
+        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50);
+
+        for (size_t i = 0; i < c->n_update_sets; i++) {
+                UpdateSet *us = c->update_sets[i];
+                const char *color;
+
+                color = update_set_flags_to_color(us->flags);
+
+                r = table_add_many(t,
+                                   TABLE_STRING,    update_set_flags_to_glyph(us->flags),
+                                   TABLE_SET_COLOR, color,
+                                   TABLE_STRING,    us->version,
+                                   TABLE_SET_COLOR, color,
+                                   TABLE_STRING,    special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)),
+                                   TABLE_SET_COLOR, color,
+                                   TABLE_STRING,    special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
+                                   TABLE_SET_COLOR, color,
+                                   TABLE_STRING,    update_set_flags_to_string(us->flags),
+                                   TABLE_SET_COLOR, color);
+                if (r < 0)
+                        return table_log_add_error(r);
+        }
+
+        return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+}
+
+static UpdateSet *context_update_set_by_version(Context *c, const char *version) {
+        assert(c);
+        assert(version);
+
+        for (size_t i = 0; i < c->n_update_sets; i++)
+                if (streq(c->update_sets[i]->version, version))
+                        return c->update_sets[i];
+
+        return NULL;
+}
+
+static int context_show_version(Context *c, const char *version) {
+        bool show_fs_columns = false, show_partition_columns = false,
+                have_fs_attributes = false, have_partition_attributes = false,
+                have_size = false, have_tries = false, have_no_auto = false,
+                have_read_only = false, have_growfs = false, have_sha256 = false;
+        _cleanup_(table_unrefp) Table *t = NULL;
+        UpdateSet *us;
+        int r;
+
+        assert(c);
+        assert(version);
+
+        us = context_update_set_by_version(c, version);
+        if (!us)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
+
+        if (arg_json_format_flags & (JSON_FORMAT_OFF|JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO))
+                (void) pager_open(arg_pager_flags);
+
+        if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF))
+                printf("%s%s%s Version: %s\n"
+                       "    State: %s%s%s\n"
+                       "Installed: %s%s\n"
+                       "Available: %s%s\n"
+                       "Protected: %s%s%s\n"
+                       " Obsolete: %s%s%s\n\n",
+                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
+                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
+                       yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
+                       yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
+                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
+                       us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
+
+
+        t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256");
+        if (!t)
+                return log_oom();
+
+        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100);
+        (void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100);
+        (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100);
+        (void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100);
+        (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100);
+        (void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100);
+        (void) table_set_empty_string(t, "-");
+
+        /* Determine if the target will make use of partition/fs attributes for any of the transfers */
+        for (size_t n = 0; n < c->n_transfers; n++) {
+                Transfer *tr = c->transfers[n];
+
+                if (tr->target.type == RESOURCE_PARTITION)
+                        show_partition_columns = true;
+                if (RESOURCE_IS_FILESYSTEM(tr->target.type))
+                        show_fs_columns = true;
+        }
+
+        for (size_t n = 0; n < us->n_instances; n++) {
+                Instance *i = us->instances[n];
+
+                r = table_add_many(t,
+                                   TABLE_STRING, resource_type_to_string(i->resource->type),
+                                   TABLE_PATH, i->path);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.partition_uuid_set) {
+                        have_partition_attributes = true;
+                        r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.partition_flags_set) {
+                        have_partition_attributes = true;
+                        r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.mtime != USEC_INFINITY) {
+                        have_fs_attributes = true;
+                        r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.mode != MODE_INVALID) {
+                        have_fs_attributes = true;
+                        r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.size != UINT64_MAX) {
+                        have_size = true;
+                        r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.tries_done != UINT64_MAX) {
+                        have_tries = true;
+                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.tries_left != UINT64_MAX) {
+                        have_tries = true;
+                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.no_auto >= 0) {
+                        bool b;
+
+                        have_no_auto = true;
+                        b = i->metadata.no_auto;
+                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+                if (i->metadata.read_only >= 0) {
+                        bool b;
+
+                        have_read_only = true;
+                        b = i->metadata.read_only;
+                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.growfs >= 0) {
+                        bool b;
+
+                        have_growfs = true;
+                        b = i->metadata.growfs;
+                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (i->metadata.sha256sum_set) {
+                        _cleanup_free_ char *formatted = NULL;
+
+                        have_sha256 = true;
+
+                        formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
+                        if (!formatted)
+                                return log_oom();
+
+                        r = table_add_cell(t, NULL, TABLE_STRING, formatted);
+                } else
+                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+        }
+
+        /* Hide the fs/partition columns if we don't have any data to show there */
+        if (!have_fs_attributes)
+                show_fs_columns = false;
+        if (!have_partition_attributes)
+                show_partition_columns = false;
+
+        if (!show_partition_columns)
+                (void) table_hide_column_from_display(t, 2, 3);
+        if (!show_fs_columns)
+                (void) table_hide_column_from_display(t, 4, 5);
+        if (!have_size)
+                (void) table_hide_column_from_display(t, 6);
+        if (!have_tries)
+                (void) table_hide_column_from_display(t, 7, 8);
+        if (!have_no_auto)
+                (void) table_hide_column_from_display(t, 9);
+        if (!have_read_only)
+                (void) table_hide_column_from_display(t, 10);
+        if (!have_growfs)
+                (void) table_hide_column_from_display(t, 11);
+        if (!have_sha256)
+                (void) table_hide_column_from_display(t, 12);
+
+        return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+}
+
+static int context_vacuum(
+                Context *c,
+                uint64_t space,
+                const char *extra_protected_version) {
+
+        int r, count = 0;
+
+        assert(c);
+
+        if (space == 0)
+                log_info("Making room…");
+        else
+                log_info("Making room for %" PRIu64 " updates…", space);
+
+        for (size_t i = 0; i < c->n_transfers; i++) {
+                r = transfer_vacuum(c->transfers[i], space, extra_protected_version);
+                if (r < 0)
+                        return r;
+
+                count = MAX(count, r);
+        }
+
+        if (count > 0)
+                log_info("Removed %i instances.", count);
+        else
+                log_info("Removed no instances.");
+
+        return 0;
+}
+
+static int context_make_offline(Context **ret, const char *node) {
+        _cleanup_(context_freep) Context* context = NULL;
+        int r;
+
+        assert(ret);
+
+        /* Allocates a context object and initializes everything we can initialize offline, i.e. without
+         * checking on the update source (i.e. the Internet) what versions are available */
+
+        context = context_new();
+        if (!context)
+                return log_oom();
+
+        r = context_read_definitions(context, arg_definitions, arg_component, arg_root, node);
+        if (r < 0)
+                return r;
+
+        r = context_load_installed_instances(context);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(context);
+        return 0;
+}
+
+static int context_make_online(Context **ret, const char *node) {
+        _cleanup_(context_freep) Context* context = NULL;
+        int r;
+
+        assert(ret);
+
+        /* Like context_make_offline(), but also communicates with the update source looking for new
+         * versions. */
+
+        r = context_make_offline(&context, node);
+        if (r < 0)
+                return r;
+
+        r = context_load_available_instances(context);
+        if (r < 0)
+                return r;
+
+        r = context_discover_update_sets(context);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(context);
+        return 0;
+}
+
+static int context_apply(
+                Context *c,
+                const char *version,
+                UpdateSet **ret_applied) {
+
+        UpdateSet *us = NULL;
+        int r;
+
+        assert(c);
+
+        if (version) {
+                us = context_update_set_by_version(c, version);
+                if (!us)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
+        } else {
+                if (!c->candidate) {
+                        log_info("No update needed.");
+
+                        if (ret_applied)
+                                *ret_applied = NULL;
+
+                        return 0;
+                }
+
+                us = c->candidate;
+        }
+
+        if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
+                log_info("Selected update '%s' is already installed. Skipping update.", us->version);
+
+                if (ret_applied)
+                        *ret_applied = NULL;
+
+                return 0;
+        }
+        if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version);
+        if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
+
+        assert((us->flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_OBSOLETE)) == UPDATE_AVAILABLE);
+
+        if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
+                log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
+        if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
+                log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
+
+        log_info("Selected update '%s' for install.", us->version);
+
+        (void) sd_notifyf(false,
+                          "STATUS=Making room for '%s'.", us->version);
+
+        /* Let's make some room. We make sure for each transfer we have one free space to fill. While
+         * removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier
+         * download succeeded already, in which case we shouldn't remove it just to acquire it again */
+        r = context_vacuum(
+                        c,
+                        /* space = */ 1,
+                        /* extra_protected_version = */ us->version);
+        if (r < 0)
+                return r;
+
+        if (arg_sync)
+                sync();
+
+        (void) sd_notifyf(false,
+                          "STATUS=Updating to '%s'.\n", us->version);
+
+        /* There should now be one instance picked for each transfer, and the order is the same */
+        assert(us->n_instances == c->n_transfers);
+
+        for (size_t i = 0; i < c->n_transfers; i++) {
+                r = transfer_acquire_instance(c->transfers[i], us->instances[i]);
+                if (r < 0)
+                        return r;
+        }
+
+        if (arg_sync)
+                sync();
+
+        for (size_t i = 0; i < c->n_transfers; i++) {
+                r = transfer_install_instance(c->transfers[i], us->instances[i], arg_root);
+                if (r < 0)
+                        return r;
+        }
+
+        log_info("%s Successfully installed update '%s'.", special_glyph(SPECIAL_GLYPH_SPARKLES), us->version);
+
+        if (ret_applied)
+                *ret_applied = us;
+
+        return 1;
+}
+
+static int reboot_now(void) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_(sd_bus_close_unrefp) sd_bus *bus = NULL;
+        int r;
+
+        r = sd_bus_open_system(&bus);
+        if (r < 0)
+                return log_error_errno(r, "Failed to open bus connection: %m");
+
+        r = bus_call_method(bus, bus_login_mgr, "RebootWithFlags", &error, NULL, "t",
+                            (uint64_t) SD_LOGIND_ROOT_CHECK_INHIBITORS);
+        if (r < 0)
+                return log_error_errno(r, "Failed to issue reboot request: %s", bus_error_message(&error, r));
+
+        return 0;
+}
+
+static int process_image(
+                bool ro,
+                char **ret_mounted_dir,
+                LoopDevice **ret_loop_device,
+                DecryptedImage **ret_decrypted_image) {
+
+        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+        _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+        int r;
+
+        assert(ret_mounted_dir);
+        assert(ret_loop_device);
+        assert(ret_decrypted_image);
+
+        if (!arg_image)
+                return 0;
+
+        assert(!arg_root);
+
+        r = mount_image_privately_interactively(
+                        arg_image,
+                        (ro ? DISSECT_IMAGE_READ_ONLY : 0) |
+                        DISSECT_IMAGE_FSCK |
+                        DISSECT_IMAGE_MKDIR |
+                        DISSECT_IMAGE_GROWFS |
+                        DISSECT_IMAGE_RELAX_VAR_CHECK |
+                        DISSECT_IMAGE_USR_NO_ROOT |
+                        DISSECT_IMAGE_GENERIC_ROOT |
+                        DISSECT_IMAGE_REQUIRE_ROOT,
+                        &mounted_dir,
+                        &loop_device,
+                        &decrypted_image);
+        if (r < 0)
+                return r;
+
+        arg_root = strdup(mounted_dir);
+        if (!arg_root)
+                return log_oom();
+
+        *ret_mounted_dir = TAKE_PTR(mounted_dir);
+        *ret_loop_device = TAKE_PTR(loop_device);
+        *ret_decrypted_image = TAKE_PTR(decrypted_image);
+
+        return 0;
+}
+
+static int verb_list(int argc, char **argv, void *userdata) {
+        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+        _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+        _cleanup_(context_freep) Context* context = NULL;
+        const char *version;
+        int r;
+
+        assert(argc <= 2);
+        version = argc >= 2 ? argv[1] : NULL;
+
+        r = process_image(/* ro= */ true, &mounted_dir, &loop_device, &decrypted_image);
+        if (r < 0)
+                return r;
+
+        r = context_make_online(&context, loop_device ? loop_device->node : NULL);
+        if (r < 0)
+                return r;
+
+        if (version)
+                return context_show_version(context, version);
+        else
+                return context_show_table(context);
+}
+
+static int verb_check_new(int argc, char **argv, void *userdata) {
+        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+        _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+        _cleanup_(context_freep) Context* context = NULL;
+        int r;
+
+        assert(argc <= 1);
+
+        r = process_image(/* ro= */ true, &mounted_dir, &loop_device, &decrypted_image);
+        if (r < 0)
+                return r;
+
+        r = context_make_online(&context, loop_device ? loop_device->node : NULL);
+        if (r < 0)
+                return r;
+
+        if (!context->candidate) {
+                log_debug("No candidate found.");
+                return EXIT_FAILURE;
+        }
+
+        puts(context->candidate->version);
+        return EXIT_SUCCESS;
+}
+
+static int verb_vacuum(int argc, char **argv, void *userdata) {
+        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+        _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+        _cleanup_(context_freep) Context* context = NULL;
+        int r;
+
+        assert(argc <= 1);
+
+        r = process_image(/* ro= */ false, &mounted_dir, &loop_device, &decrypted_image);
+        if (r < 0)
+                return r;
+
+        r = context_make_offline(&context, loop_device ? loop_device->node : NULL);
+        if (r < 0)
+                return r;
+
+        return context_vacuum(context, 0, NULL);
+}
+
+static int verb_update(int argc, char **argv, void *userdata) {
+        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+        _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+        _cleanup_(context_freep) Context* context = NULL;
+        _cleanup_free_ char *booted_version = NULL;
+        UpdateSet *applied = NULL;
+        const char *version;
+        int r;
+
+        assert(argc <= 2);
+        version = argc >= 2 ? argv[1] : NULL;
+
+        if (arg_reboot) {
+                /* If automatic reboot on completion is requested, let's first determine the currently booted image */
+
+                r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse /etc/os-release: %m");
+                if (!booted_version)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field.");
+        }
+
+        r = process_image(/* ro= */ false, &mounted_dir, &loop_device, &decrypted_image);
+        if (r < 0)
+                return r;
+
+        r = context_make_online(&context, loop_device ? loop_device->node : NULL);
+        if (r < 0)
+                return r;
+
+        r = context_apply(context, version, &applied);
+        if (r < 0)
+                return r;
+
+        if (r > 0 && arg_reboot) {
+                assert(applied);
+                assert(booted_version);
+
+                if (strverscmp_improved(applied->version, booted_version) > 0) {
+                        log_notice("Newly installed version is newer than booted version, rebooting.");
+                        return reboot_now();
+                }
+
+                log_info("Booted version is newer or identical to newly installed version, not rebooting.");
+        }
+
+        return 0;
+}
+
+static int verb_pending_or_reboot(int argc, char **argv, void *userdata) {
+        _cleanup_(context_freep) Context* context = NULL;
+        _cleanup_free_ char *booted_version = NULL;
+        int r;
+
+        assert(argc == 1);
+
+        if (arg_image || arg_root)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "The --root=/--image switches may not be combined with the '%s' operation.", argv[0]);
+
+        r = context_make_offline(&context, NULL);
+        if (r < 0)
+                return r;
+
+        log_info("Determining installed update sets…");
+
+        r = context_discover_update_sets_by_flag(context, UPDATE_INSTALLED);
+        if (r < 0)
+                return r;
+        if (!context->newest_installed)
+                return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions.");
+
+        r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
+        if (r < 0) /* yes, arg_root is NULL here, but we have to pass something, and it's a lot more readable
+                    * if we see what the first argument is about */
+                return log_error_errno(r, "Failed to parse /etc/os-release: %m");
+        if (!booted_version)
+                return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION= field.");
+
+        r = strverscmp_improved(context->newest_installed->version, booted_version);
+        if (r > 0) {
+                log_notice("Newest installed version '%s' is newer than booted version '%s'.%s",
+                           context->newest_installed->version, booted_version,
+                           streq(argv[0], "pending") ? " Reboot recommended." : "");
+
+                if (streq(argv[0], "reboot"))
+                        return reboot_now();
+
+                return EXIT_SUCCESS;
+        } else if (r == 0)
+                log_info("Newest installed version '%s' matches booted version '%s'.",
+                         context->newest_installed->version, booted_version);
+        else
+                log_warning("Newest installed version '%s' is older than booted version '%s'.",
+                            context->newest_installed->version, booted_version);
+
+        if (streq(argv[0], "pending")) /* When called as 'pending' tell the caller via failure exit code that there's nothing newer installed */
+                return EXIT_FAILURE;
+
+        return EXIT_SUCCESS;
+}
+
+static int component_name_valid(const char *c) {
+        _cleanup_free_ char *j = NULL;
+
+        /* See if the specified string enclosed in the directory prefix+suffix would be a valid file name */
+
+        if (isempty(c))
+                return false;
+
+        if (string_has_cc(c, NULL))
+                return false;
+
+        if (!utf8_is_valid(c))
+                return false;
+
+        j = strjoin("sysupdate.", c, ".d");
+        if (!j)
+                return -ENOMEM;
+
+        return filename_is_valid(j);
+}
+
+static int verb_components(int argc, char **argv, void *userdata) {
+        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+        _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
+        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+        _cleanup_(set_freep) Set *names = NULL;
+        _cleanup_free_ char **z = NULL; /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */
+        char **l = CONF_PATHS_STRV(""), **i;
+        bool has_default_component = false;
+        int r;
+
+        assert(argc <= 1);
+
+        r = process_image(/* ro= */ false, &mounted_dir, &loop_device, &decrypted_image);
+        if (r < 0)
+                return r;
+
+        STRV_FOREACH(i, l) {
+                _cleanup_closedir_ DIR *d = NULL;
+                _cleanup_free_ char *p = NULL;
+
+                r = chase_symlinks_and_opendir(*i, arg_root, CHASE_PREFIX_ROOT, &p, &d);
+                if (r == -ENOENT)
+                        continue;
+                if (r < 0)
+                        return log_error_errno(r, "Failed to open directory '%s': %m", *i);
+
+                for (;;) {
+                        _cleanup_free_ char *n = NULL;
+                        struct dirent *de;
+                        const char *e, *a;
+
+                        de = readdir_ensure_type(d);
+                        if (!de) {
+                                if (errno != 0)
+                                        return log_error_errno(errno, "Failed to enumerate directory '%s': %m", p);
+
+                                break;
+                        }
+
+                        if (de->d_type != DT_DIR)
+                                continue;
+
+                        if (dot_or_dot_dot(de->d_name))
+                                continue;
+
+                        if (streq(de->d_name, "sysupdate.d")) {
+                                has_default_component = true;
+                                continue;
+                        }
+
+                        e = startswith(de->d_name, "sysupdate.");
+                        if (!e)
+                                continue;
+
+                        a = endswith(e, ".d");
+                        if (!a)
+                                continue;
+
+                        n = strndup(e, a - e);
+                        if (!n)
+                                return log_oom();
+
+                        r = component_name_valid(n);
+                        if (r < 0)
+                                return log_error_errno(r, "Unable to validate component name: %m");
+                        if (r == 0)
+                                continue;
+
+                        r = set_ensure_consume(&names, &string_hash_ops_free, TAKE_PTR(n));
+                        if (r < 0 && r != -EEXIST)
+                                return log_error_errno(r, "Failed to add component to set: %m");
+                }
+        }
+
+        if (!has_default_component && set_isempty(names)) {
+                log_info("No components defined.");
+                return 0;
+        }
+
+        z = set_get_strv(names);
+        if (!z)
+                return log_oom();
+
+        strv_sort(z);
+
+        if (has_default_component)
+                printf("%s<default>%s\n",
+                       ansi_highlight(), ansi_normal());
+
+        STRV_FOREACH(i, z)
+                puts(*i);
+
+        return 0;
+}
+
+static int verb_help(int argc, char **argv, void *userdata) {
+        _cleanup_free_ char *link = NULL;
+        int r;
+
+        r = terminal_urlify_man("systemd-sysupdate", "1", &link);
+        if (r < 0)
+                return log_oom();
+
+        printf("%1$s [OPTIONS...] [VERSION]\n"
+               "\n%5$sUpdate OS images.%6$s\n"
+               "\n%3$sCommands:%4$s\n"
+               "  list [VERSION]          Show installed and available versions\n"
+               "  check-new               Check if there's a new version available\n"
+               "  update [VERSION]        Install new version now\n"
+               "  vacuum                  Make room, by deleting old versions\n"
+               "  pending                 Report whether a newer version is installed than\n"
+               "                          currently booted\n"
+               "  reboot                  Reboot if a newer version is installed than booted\n"
+               "  components              Show list of components\n"
+               "  -h --help               Show this help\n"
+               "     --version            Show package version\n"
+               "\n%3$sOptions:%4$s\n"
+               "  -C --component=NAME     Select component to update\n"
+               "     --definitions=DIR    Find transfer definitions in specified directory\n"
+               "     --root=PATH          Operate relative to root path\n"
+               "     --image=PATH         Operate relative to image file\n"
+               "  -m --instances-max=INT  How many instances to maintain\n"
+               "     --sync=BOOL          Controls whether to sync data to disk\n"
+               "     --verify=BOOL        Force signature verification on or off\n"
+               "     --reboot             Reboot after updating to newer version\n"
+               "     --no-pager           Do not pipe output into a pager\n"
+               "     --no-legend          Do not show the headers and footers\n"
+               "     --json=pretty|short|off\n"
+               "                          Generate JSON output\n"
+               "\nSee the %2$s for details.\n"
+               , program_invocation_short_name
+               , link
+               , ansi_underline(), ansi_normal()
+               , ansi_highlight(), ansi_normal()
+        );
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+        enum {
+                ARG_VERSION = 0x100,
+                ARG_NO_PAGER,
+                ARG_NO_LEGEND,
+                ARG_SYNC,
+                ARG_DEFINITIONS,
+                ARG_JSON,
+                ARG_ROOT,
+                ARG_IMAGE,
+                ARG_REBOOT,
+                ARG_VERIFY,
+        };
+
+        static const struct option options[] = {
+                { "help",              no_argument,       NULL, 'h'                   },
+                { "version",           no_argument,       NULL, ARG_VERSION           },
+                { "no-pager",          no_argument,       NULL, ARG_NO_PAGER          },
+                { "no-legend",         no_argument,       NULL, ARG_NO_LEGEND         },
+                { "definitions",       required_argument, NULL, ARG_DEFINITIONS       },
+                { "instances-max",     required_argument, NULL, 'm'                   },
+                { "sync",              required_argument, NULL, ARG_SYNC              },
+                { "json",              required_argument, NULL, ARG_JSON              },
+                { "root",              required_argument, NULL, ARG_ROOT              },
+                { "image",             required_argument, NULL, ARG_IMAGE             },
+                { "reboot",            no_argument,       NULL, ARG_REBOOT            },
+                { "component",         required_argument, NULL, 'C'                   },
+                { "verify",            required_argument, NULL, ARG_VERIFY            },
+                {}
+        };
+
+        int c, r;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "hm:C:", options, NULL)) >= 0) {
+
+                switch (c) {
+
+                case 'h':
+                        return verb_help(0, NULL, NULL);
+
+                case ARG_VERSION:
+                        return version();
+
+                case ARG_NO_PAGER:
+                        arg_pager_flags |= PAGER_DISABLE;
+                        break;
+
+                case ARG_NO_LEGEND:
+                        arg_legend = false;
+                        break;
+
+                case 'm':
+                        r = safe_atou64(optarg, &arg_instances_max);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", optarg);
+
+                        break;
+
+                case ARG_SYNC:
+                        r = parse_boolean_argument("--sync=", optarg, &arg_sync);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                case ARG_DEFINITIONS:
+                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_definitions);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                case ARG_JSON:
+                        r = parse_json_argument(optarg, &arg_json_format_flags);
+                        if (r <= 0)
+                                return r;
+
+                        break;
+
+                case ARG_ROOT:
+                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                case ARG_IMAGE:
+                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                case ARG_REBOOT:
+                        arg_reboot = true;
+                        break;
+
+                case 'C':
+                        if (isempty(optarg)) {
+                                arg_component = mfree(arg_component);
+                                break;
+                        }
+
+                        r = component_name_valid(optarg);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to determine if component name is valid: %m");
+                        if (r == 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", optarg);
+
+                        r = free_and_strdup_warn(&arg_component, optarg);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
+                case ARG_VERIFY: {
+                        bool b;
+
+                        r = parse_boolean_argument("--verify=", optarg, &b);
+                        if (r < 0)
+                                return r;
+
+                        arg_verify = b;
+                        break;
+                }
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached();
+                }
+        }
+
+        if (arg_image && arg_root)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
+
+        if ((arg_image || arg_root) && arg_reboot)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=.");
+
+        if (arg_definitions && arg_component)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined.");
+
+        return 1;
+}
+
+static int sysupdate_main(int argc, char *argv[]) {
+
+        static const Verb verbs[] = {
+                { "list",       VERB_ANY, 2, VERB_DEFAULT, verb_list              },
+                { "components", VERB_ANY, 1, 0,            verb_components        },
+                { "check-new",  VERB_ANY, 1, 0,            verb_check_new         },
+                { "update",     VERB_ANY, 2, 0,            verb_update               },
+                { "vacuum",     VERB_ANY, 1, 0,            verb_vacuum            },
+                { "reboot",     1,        1, 0,            verb_pending_or_reboot },
+                { "pending",    1,        1, 0,            verb_pending_or_reboot },
+                { "help",       VERB_ANY, 1, 0,            verb_help              },
+                {}
+        };
+
+        return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+        int r;
+
+        log_setup();
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                return r;
+
+        return sysupdate_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h
new file mode 100644 (file)
index 0000000..6d387b7
--- /dev/null
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+extern bool arg_sync;
+extern uint64_t arg_instances_max;
+extern char *arg_root;
+
+static inline const char* import_binary_path(void) {
+        return secure_getenv("SYSTEMD_IMPORT_PATH") ?: SYSTEMD_IMPORT_PATH;
+}
+
+static inline const char* import_fs_binary_path(void) {
+        return secure_getenv("SYSTEMD_IMPORT_FS_PATH") ?: SYSTEMD_IMPORT_FS_PATH;
+}
+
+static inline const char *pull_binary_path(void) {
+        return secure_getenv("SYSTEMD_PULL_PATH") ?: SYSTEMD_PULL_PATH;
+}