summaryrefslogtreecommitdiffstats
path: root/src/import
diff options
context:
space:
mode:
Diffstat (limited to 'src/import')
-rw-r--r--src/import/curl-util.c416
-rw-r--r--src/import/curl-util.h39
-rw-r--r--src/import/export-raw.c332
-rw-r--r--src/import/export-raw.h18
-rw-r--r--src/import/export-tar.c331
-rw-r--r--src/import/export-tar.h18
-rw-r--r--src/import/export.c298
-rw-r--r--src/import/import-common.c259
-rw-r--r--src/import/import-common.h12
-rw-r--r--src/import/import-compress.c466
-rw-r--r--src/import/import-compress.h47
-rw-r--r--src/import/import-fs.c327
-rw-r--r--src/import/import-pubring.gpgbin0 -> 9551 bytes
-rw-r--r--src/import/import-raw.c438
-rw-r--r--src/import/import-raw.h18
-rw-r--r--src/import/import-tar.c369
-rw-r--r--src/import/import-tar.h18
-rw-r--r--src/import/import.c322
-rw-r--r--src/import/importd.c1266
-rw-r--r--src/import/meson.build79
-rw-r--r--src/import/org.freedesktop.import1.conf84
-rw-r--r--src/import/org.freedesktop.import1.policy51
-rw-r--r--src/import/org.freedesktop.import1.service14
-rw-r--r--src/import/pull-common.c530
-rw-r--r--src/import/pull-common.h18
-rw-r--r--src/import/pull-job.c636
-rw-r--r--src/import/pull-job.h93
-rw-r--r--src/import/pull-raw.c750
-rw-r--r--src/import/pull-raw.h18
-rw-r--r--src/import/pull-tar.c559
-rw-r--r--src/import/pull-tar.h18
-rw-r--r--src/import/pull.c333
-rw-r--r--src/import/qcow2-util.c334
-rw-r--r--src/import/qcow2-util.h5
-rw-r--r--src/import/test-qcow2.c36
35 files changed, 8552 insertions, 0 deletions
diff --git a/src/import/curl-util.c b/src/import/curl-util.c
new file mode 100644
index 0000000..7db03b2
--- /dev/null
+++ b/src/import/curl-util.c
@@ -0,0 +1,416 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "alloc-util.h"
+#include "build.h"
+#include "curl-util.h"
+#include "fd-util.h"
+#include "locale-util.h"
+#include "string-util.h"
+
+static void curl_glue_check_finished(CurlGlue *g) {
+ CURLMsg *msg;
+ int k = 0;
+
+ assert(g);
+
+ msg = curl_multi_info_read(g->curl, &k);
+ if (!msg)
+ return;
+
+ if (msg->msg != CURLMSG_DONE)
+ return;
+
+ if (g->on_finished)
+ g->on_finished(g, msg->easy_handle, msg->data.result);
+}
+
+static int curl_glue_on_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ CurlGlue *g = userdata;
+ int action, k = 0, translated_fd;
+
+ assert(s);
+ assert(g);
+
+ translated_fd = PTR_TO_FD(hashmap_get(g->translate_fds, FD_TO_PTR(fd)));
+
+ if (FLAGS_SET(revents, EPOLLIN | EPOLLOUT))
+ action = CURL_POLL_INOUT;
+ else if (revents & EPOLLIN)
+ action = CURL_POLL_IN;
+ else if (revents & EPOLLOUT)
+ action = CURL_POLL_OUT;
+ else
+ action = 0;
+
+ if (curl_multi_socket_action(g->curl, translated_fd, action, &k) != CURLM_OK)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Failed to propagate IO event.");
+
+ curl_glue_check_finished(g);
+ return 0;
+}
+
+static int curl_glue_socket_callback(CURLM *curl, curl_socket_t s, int action, void *userdata, void *socketp) {
+ sd_event_source *io;
+ CurlGlue *g = userdata;
+ uint32_t events = 0;
+ int r;
+
+ assert(curl);
+ assert(g);
+
+ io = hashmap_get(g->ios, FD_TO_PTR(s));
+
+ if (action == CURL_POLL_REMOVE) {
+ if (io) {
+ int fd;
+
+ fd = sd_event_source_get_io_fd(io);
+ assert(fd >= 0);
+
+ sd_event_source_set_enabled(io, SD_EVENT_OFF);
+ sd_event_source_unref(io);
+
+ hashmap_remove(g->ios, FD_TO_PTR(s));
+ hashmap_remove(g->translate_fds, FD_TO_PTR(fd));
+
+ safe_close(fd);
+ }
+
+ return 0;
+ }
+
+ r = hashmap_ensure_allocated(&g->ios, &trivial_hash_ops);
+ if (r < 0) {
+ log_oom();
+ return -1;
+ }
+
+ r = hashmap_ensure_allocated(&g->translate_fds, &trivial_hash_ops);
+ if (r < 0) {
+ log_oom();
+ return -1;
+ }
+
+ if (action == CURL_POLL_IN)
+ events = EPOLLIN;
+ else if (action == CURL_POLL_OUT)
+ events = EPOLLOUT;
+ else if (action == CURL_POLL_INOUT)
+ events = EPOLLIN|EPOLLOUT;
+
+ if (io) {
+ if (sd_event_source_set_io_events(io, events) < 0)
+ return -1;
+
+ if (sd_event_source_set_enabled(io, SD_EVENT_ON) < 0)
+ return -1;
+ } else {
+ _cleanup_close_ int fd = -1;
+
+ /* When curl needs to remove an fd from us it closes
+ * the fd first, and only then calls into us. This is
+ * nasty, since we cannot pass the fd on to epoll()
+ * anymore. Hence, duplicate the fds here, and keep a
+ * copy for epoll which we control after use. */
+
+ fd = fcntl(s, F_DUPFD_CLOEXEC, 3);
+ if (fd < 0)
+ return -1;
+
+ if (sd_event_add_io(g->event, &io, fd, events, curl_glue_on_io, g) < 0)
+ return -1;
+
+ (void) sd_event_source_set_description(io, "curl-io");
+
+ r = hashmap_put(g->ios, FD_TO_PTR(s), io);
+ if (r < 0) {
+ log_oom();
+ sd_event_source_unref(io);
+ return -1;
+ }
+
+ r = hashmap_put(g->translate_fds, FD_TO_PTR(fd), FD_TO_PTR(s));
+ if (r < 0) {
+ log_oom();
+ hashmap_remove(g->ios, FD_TO_PTR(s));
+ sd_event_source_unref(io);
+ return -1;
+ }
+
+ fd = -1;
+ }
+
+ return 0;
+}
+
+static int curl_glue_on_timer(sd_event_source *s, uint64_t usec, void *userdata) {
+ CurlGlue *g = userdata;
+ int k = 0;
+
+ assert(s);
+ assert(g);
+
+ if (curl_multi_socket_action(g->curl, CURL_SOCKET_TIMEOUT, 0, &k) != CURLM_OK)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Failed to propagate timeout.");
+
+ curl_glue_check_finished(g);
+ return 0;
+}
+
+static int curl_glue_timer_callback(CURLM *curl, long timeout_ms, void *userdata) {
+ CurlGlue *g = userdata;
+ usec_t usec;
+
+ assert(curl);
+ assert(g);
+
+ if (timeout_ms < 0) {
+ if (g->timer) {
+ if (sd_event_source_set_enabled(g->timer, SD_EVENT_OFF) < 0)
+ return -1;
+ }
+
+ return 0;
+ }
+
+ usec = now(clock_boottime_or_monotonic()) + (usec_t) timeout_ms * USEC_PER_MSEC + USEC_PER_MSEC - 1;
+
+ if (g->timer) {
+ if (sd_event_source_set_time(g->timer, usec) < 0)
+ return -1;
+
+ if (sd_event_source_set_enabled(g->timer, SD_EVENT_ONESHOT) < 0)
+ return -1;
+ } else {
+ if (sd_event_add_time(g->event, &g->timer, clock_boottime_or_monotonic(), usec, 0, curl_glue_on_timer, g) < 0)
+ return -1;
+
+ (void) sd_event_source_set_description(g->timer, "curl-timer");
+ }
+
+ return 0;
+}
+
+CurlGlue *curl_glue_unref(CurlGlue *g) {
+ sd_event_source *io;
+
+ if (!g)
+ return NULL;
+
+ if (g->curl)
+ curl_multi_cleanup(g->curl);
+
+ while ((io = hashmap_steal_first(g->ios))) {
+ int fd;
+
+ fd = sd_event_source_get_io_fd(io);
+ assert(fd >= 0);
+
+ hashmap_remove(g->translate_fds, FD_TO_PTR(fd));
+
+ safe_close(fd);
+ sd_event_source_unref(io);
+ }
+
+ hashmap_free(g->ios);
+
+ sd_event_source_unref(g->timer);
+ sd_event_unref(g->event);
+ return mfree(g);
+}
+
+int curl_glue_new(CurlGlue **glue, sd_event *event) {
+ _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL;
+ _cleanup_(curl_multi_cleanupp) CURL *c = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ int r;
+
+ if (event)
+ e = sd_event_ref(event);
+ else {
+ r = sd_event_default(&e);
+ if (r < 0)
+ return r;
+ }
+
+ c = curl_multi_init();
+ if (!c)
+ return -ENOMEM;
+
+ g = new(CurlGlue, 1);
+ if (!g)
+ return -ENOMEM;
+
+ *g = (CurlGlue) {
+ .event = TAKE_PTR(e),
+ .curl = TAKE_PTR(c),
+ };
+
+ if (curl_multi_setopt(g->curl, CURLMOPT_SOCKETDATA, g) != CURLM_OK)
+ return -EINVAL;
+
+ if (curl_multi_setopt(g->curl, CURLMOPT_SOCKETFUNCTION, curl_glue_socket_callback) != CURLM_OK)
+ return -EINVAL;
+
+ if (curl_multi_setopt(g->curl, CURLMOPT_TIMERDATA, g) != CURLM_OK)
+ return -EINVAL;
+
+ if (curl_multi_setopt(g->curl, CURLMOPT_TIMERFUNCTION, curl_glue_timer_callback) != CURLM_OK)
+ return -EINVAL;
+
+ *glue = TAKE_PTR(g);
+
+ return 0;
+}
+
+int curl_glue_make(CURL **ret, const char *url, void *userdata) {
+ _cleanup_(curl_easy_cleanupp) CURL *c = NULL;
+ const char *useragent;
+
+ assert(ret);
+ assert(url);
+
+ c = curl_easy_init();
+ if (!c)
+ return -ENOMEM;
+
+ /* curl_easy_setopt(c, CURLOPT_VERBOSE, 1L); */
+
+ if (curl_easy_setopt(c, CURLOPT_URL, url) != CURLE_OK)
+ return -EIO;
+
+ if (curl_easy_setopt(c, CURLOPT_PRIVATE, userdata) != CURLE_OK)
+ return -EIO;
+
+ useragent = strjoina(program_invocation_short_name, "/" GIT_VERSION);
+ if (curl_easy_setopt(c, CURLOPT_USERAGENT, useragent) != CURLE_OK)
+ return -EIO;
+
+ if (curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L) != CURLE_OK)
+ return -EIO;
+
+ *ret = TAKE_PTR(c);
+ return 0;
+}
+
+int curl_glue_add(CurlGlue *g, CURL *c) {
+ assert(g);
+ assert(c);
+
+ if (curl_multi_add_handle(g->curl, c) != CURLM_OK)
+ return -EIO;
+
+ return 0;
+}
+
+void curl_glue_remove_and_free(CurlGlue *g, CURL *c) {
+ assert(g);
+
+ if (!c)
+ return;
+
+ if (g->curl)
+ curl_multi_remove_handle(g->curl, c);
+
+ curl_easy_cleanup(c);
+}
+
+struct curl_slist *curl_slist_new(const char *first, ...) {
+ struct curl_slist *l;
+ va_list ap;
+
+ if (!first)
+ return NULL;
+
+ l = curl_slist_append(NULL, first);
+ if (!l)
+ return NULL;
+
+ va_start(ap, first);
+
+ for (;;) {
+ struct curl_slist *n;
+ const char *i;
+
+ i = va_arg(ap, const char*);
+ if (!i)
+ break;
+
+ n = curl_slist_append(l, i);
+ if (!n) {
+ va_end(ap);
+ curl_slist_free_all(l);
+ return NULL;
+ }
+
+ l = n;
+ }
+
+ va_end(ap);
+ return l;
+}
+
+int curl_header_strdup(const void *contents, size_t sz, const char *field, char **value) {
+ const char *p;
+ char *s;
+
+ p = memory_startswith_no_case(contents, sz, field);
+ if (!p)
+ return 0;
+
+ sz -= p - (const char*) contents;
+
+ if (memchr(p, 0, sz))
+ return 0;
+
+ /* Skip over preceeding whitespace */
+ while (sz > 0 && strchr(WHITESPACE, p[0])) {
+ p++;
+ sz--;
+ }
+
+ /* Truncate trailing whitespace */
+ while (sz > 0 && strchr(WHITESPACE, p[sz-1]))
+ sz--;
+
+ s = strndup(p, sz);
+ if (!s)
+ return -ENOMEM;
+
+ *value = s;
+ return 1;
+}
+
+int curl_parse_http_time(const char *t, usec_t *ret) {
+ _cleanup_(freelocalep) locale_t loc = (locale_t) 0;
+ const char *e;
+ struct tm tm;
+ time_t v;
+
+ assert(t);
+ assert(ret);
+
+ loc = newlocale(LC_TIME_MASK, "C", (locale_t) 0);
+ if (loc == (locale_t) 0)
+ return -errno;
+
+ /* RFC822 */
+ e = strptime_l(t, "%a, %d %b %Y %H:%M:%S %Z", &tm, loc);
+ if (!e || *e != 0)
+ /* RFC 850 */
+ e = strptime_l(t, "%A, %d-%b-%y %H:%M:%S %Z", &tm, loc);
+ if (!e || *e != 0)
+ /* ANSI C */
+ e = strptime_l(t, "%a %b %d %H:%M:%S %Y", &tm, loc);
+ if (!e || *e != 0)
+ return -EINVAL;
+
+ v = timegm(&tm);
+ if (v == (time_t) -1)
+ return -EINVAL;
+
+ *ret = (usec_t) v * USEC_PER_SEC;
+ return 0;
+}
diff --git a/src/import/curl-util.h b/src/import/curl-util.h
new file mode 100644
index 0000000..a03e844
--- /dev/null
+++ b/src/import/curl-util.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <curl/curl.h>
+#include <sys/types.h>
+
+#include "sd-event.h"
+
+#include "hashmap.h"
+
+typedef struct CurlGlue CurlGlue;
+
+struct CurlGlue {
+ sd_event *event;
+ CURLM *curl;
+ sd_event_source *timer;
+ Hashmap *ios;
+ Hashmap *translate_fds;
+
+ void (*on_finished)(CurlGlue *g, CURL *curl, CURLcode code);
+ void *userdata;
+};
+
+int curl_glue_new(CurlGlue **glue, sd_event *event);
+CurlGlue* curl_glue_unref(CurlGlue *glue);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(CurlGlue*, curl_glue_unref);
+
+int curl_glue_make(CURL **ret, const char *url, void *userdata);
+int curl_glue_add(CurlGlue *g, CURL *c);
+void curl_glue_remove_and_free(CurlGlue *g, CURL *c);
+
+struct curl_slist *curl_slist_new(const char *first, ...) _sentinel_;
+int curl_header_strdup(const void *contents, size_t sz, const char *field, char **value);
+int curl_parse_http_time(const char *t, usec_t *ret);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(CURL*, curl_easy_cleanup);
+DEFINE_TRIVIAL_CLEANUP_FUNC(CURL*, curl_multi_cleanup);
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct curl_slist*, curl_slist_free_all);
diff --git a/src/import/export-raw.c b/src/import/export-raw.c
new file mode 100644
index 0000000..6a02b47
--- /dev/null
+++ b/src/import/export-raw.c
@@ -0,0 +1,332 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <sys/sendfile.h>
+
+/* When we include libgen.h because we need dirname() we immediately
+ * undefine basename() since libgen.h defines it as a macro to the POSIX
+ * version which is really broken. We prefer GNU basename(). */
+#include <libgen.h>
+#undef basename
+
+#include "sd-daemon.h"
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "copy.h"
+#include "export-raw.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "import-common.h"
+#include "missing.h"
+#include "ratelimit.h"
+#include "stat-util.h"
+#include "string-util.h"
+#include "tmpfile-util.h"
+#include "util.h"
+
+#define COPY_BUFFER_SIZE (16*1024)
+
+struct RawExport {
+ sd_event *event;
+
+ RawExportFinished on_finished;
+ void *userdata;
+
+ char *path;
+
+ int input_fd;
+ int output_fd;
+
+ ImportCompress compress;
+
+ sd_event_source *output_event_source;
+
+ void *buffer;
+ size_t buffer_size;
+ size_t buffer_allocated;
+
+ uint64_t written_compressed;
+ uint64_t written_uncompressed;
+
+ unsigned last_percent;
+ RateLimit progress_rate_limit;
+
+ struct stat st;
+
+ bool eof;
+ bool tried_reflink;
+ bool tried_sendfile;
+};
+
+RawExport *raw_export_unref(RawExport *e) {
+ if (!e)
+ return NULL;
+
+ sd_event_source_unref(e->output_event_source);
+
+ import_compress_free(&e->compress);
+
+ sd_event_unref(e->event);
+
+ safe_close(e->input_fd);
+
+ free(e->buffer);
+ free(e->path);
+ return mfree(e);
+}
+
+int raw_export_new(
+ RawExport **ret,
+ sd_event *event,
+ RawExportFinished on_finished,
+ void *userdata) {
+
+ _cleanup_(raw_export_unrefp) RawExport *e = NULL;
+ int r;
+
+ assert(ret);
+
+ e = new(RawExport, 1);
+ if (!e)
+ return -ENOMEM;
+
+ *e = (RawExport) {
+ .output_fd = -1,
+ .input_fd = -1,
+ .on_finished = on_finished,
+ .userdata = userdata,
+ .last_percent = (unsigned) -1,
+ };
+
+ RATELIMIT_INIT(e->progress_rate_limit, 100 * USEC_PER_MSEC, 1);
+
+ if (event)
+ e->event = sd_event_ref(event);
+ else {
+ r = sd_event_default(&e->event);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(e);
+
+ return 0;
+}
+
+static void raw_export_report_progress(RawExport *e) {
+ unsigned percent;
+ assert(e);
+
+ if (e->written_uncompressed >= (uint64_t) e->st.st_size)
+ percent = 100;
+ else
+ percent = (unsigned) ((e->written_uncompressed * UINT64_C(100)) / (uint64_t) e->st.st_size);
+
+ if (percent == e->last_percent)
+ return;
+
+ if (!ratelimit_below(&e->progress_rate_limit))
+ return;
+
+ sd_notifyf(false, "X_IMPORT_PROGRESS=%u", percent);
+ log_info("Exported %u%%.", percent);
+
+ e->last_percent = percent;
+}
+
+static int raw_export_process(RawExport *e) {
+ ssize_t l;
+ int r;
+
+ assert(e);
+
+ if (!e->tried_reflink && e->compress.type == IMPORT_COMPRESS_UNCOMPRESSED) {
+
+ /* If we shall take an uncompressed snapshot we can
+ * reflink source to destination directly. Let's see
+ * if this works. */
+
+ r = btrfs_reflink(e->input_fd, e->output_fd);
+ if (r >= 0) {
+ r = 0;
+ goto finish;
+ }
+
+ e->tried_reflink = true;
+ }
+
+ if (!e->tried_sendfile && e->compress.type == IMPORT_COMPRESS_UNCOMPRESSED) {
+
+ l = sendfile(e->output_fd, e->input_fd, NULL, COPY_BUFFER_SIZE);
+ if (l < 0) {
+ if (errno == EAGAIN)
+ return 0;
+
+ e->tried_sendfile = true;
+ } else if (l == 0) {
+ r = 0;
+ goto finish;
+ } else {
+ e->written_uncompressed += l;
+ e->written_compressed += l;
+
+ raw_export_report_progress(e);
+
+ return 0;
+ }
+ }
+
+ while (e->buffer_size <= 0) {
+ uint8_t input[COPY_BUFFER_SIZE];
+
+ if (e->eof) {
+ r = 0;
+ goto finish;
+ }
+
+ l = read(e->input_fd, input, sizeof(input));
+ if (l < 0) {
+ r = log_error_errno(errno, "Failed to read raw file: %m");
+ goto finish;
+ }
+
+ if (l == 0) {
+ e->eof = true;
+ r = import_compress_finish(&e->compress, &e->buffer, &e->buffer_size, &e->buffer_allocated);
+ } else {
+ e->written_uncompressed += l;
+ r = import_compress(&e->compress, input, l, &e->buffer, &e->buffer_size, &e->buffer_allocated);
+ }
+ if (r < 0) {
+ r = log_error_errno(r, "Failed to encode: %m");
+ goto finish;
+ }
+ }
+
+ l = write(e->output_fd, e->buffer, e->buffer_size);
+ if (l < 0) {
+ if (errno == EAGAIN)
+ return 0;
+
+ r = log_error_errno(errno, "Failed to write output file: %m");
+ goto finish;
+ }
+
+ assert((size_t) l <= e->buffer_size);
+ memmove(e->buffer, (uint8_t*) e->buffer + l, e->buffer_size - l);
+ e->buffer_size -= l;
+ e->written_compressed += l;
+
+ raw_export_report_progress(e);
+
+ return 0;
+
+finish:
+ if (r >= 0) {
+ (void) copy_times(e->input_fd, e->output_fd);
+ (void) copy_xattr(e->input_fd, e->output_fd);
+ }
+
+ if (e->on_finished)
+ e->on_finished(e, r, e->userdata);
+ else
+ sd_event_exit(e->event, r);
+
+ return 0;
+}
+
+static int raw_export_on_output(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ RawExport *i = userdata;
+
+ return raw_export_process(i);
+}
+
+static int raw_export_on_defer(sd_event_source *s, void *userdata) {
+ RawExport *i = userdata;
+
+ return raw_export_process(i);
+}
+
+static int reflink_snapshot(int fd, const char *path) {
+ int new_fd, r;
+
+ new_fd = open_parent(path, O_TMPFILE|O_CLOEXEC|O_RDWR, 0600);
+ if (new_fd < 0) {
+ _cleanup_free_ char *t = NULL;
+
+ r = tempfn_random(path, NULL, &t);
+ if (r < 0)
+ return r;
+
+ new_fd = open(t, O_CLOEXEC|O_CREAT|O_NOCTTY|O_RDWR, 0600);
+ if (new_fd < 0)
+ return -errno;
+
+ (void) unlink(t);
+ }
+
+ r = btrfs_reflink(fd, new_fd);
+ if (r < 0) {
+ safe_close(new_fd);
+ return r;
+ }
+
+ return new_fd;
+}
+
+int raw_export_start(RawExport *e, const char *path, int fd, ImportCompressType compress) {
+ _cleanup_close_ int sfd = -1, tfd = -1;
+ int r;
+
+ assert(e);
+ assert(path);
+ assert(fd >= 0);
+ assert(compress < _IMPORT_COMPRESS_TYPE_MAX);
+ assert(compress != IMPORT_COMPRESS_UNKNOWN);
+
+ if (e->output_fd >= 0)
+ return -EBUSY;
+
+ r = fd_nonblock(fd, true);
+ if (r < 0)
+ return r;
+
+ r = free_and_strdup(&e->path, path);
+ if (r < 0)
+ return r;
+
+ sfd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (sfd < 0)
+ return -errno;
+
+ if (fstat(sfd, &e->st) < 0)
+ return -errno;
+ r = stat_verify_regular(&e->st);
+ if (r < 0)
+ return r;
+
+ /* Try to take a reflink snapshot of the file, if we can t make the export atomic */
+ tfd = reflink_snapshot(sfd, path);
+ if (tfd >= 0)
+ e->input_fd = TAKE_FD(tfd);
+ else
+ e->input_fd = TAKE_FD(sfd);
+
+ r = import_compress_init(&e->compress, compress);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_io(e->event, &e->output_event_source, fd, EPOLLOUT, raw_export_on_output, e);
+ if (r == -EPERM) {
+ r = sd_event_add_defer(e->event, &e->output_event_source, raw_export_on_defer, e);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_enabled(e->output_event_source, SD_EVENT_ON);
+ }
+ if (r < 0)
+ return r;
+
+ e->output_fd = fd;
+ return r;
+}
diff --git a/src/import/export-raw.h b/src/import/export-raw.h
new file mode 100644
index 0000000..196b8ef
--- /dev/null
+++ b/src/import/export-raw.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-event.h"
+
+#include "import-compress.h"
+#include "macro.h"
+
+typedef struct RawExport RawExport;
+
+typedef void (*RawExportFinished)(RawExport *export, int error, void *userdata);
+
+int raw_export_new(RawExport **export, sd_event *event, RawExportFinished on_finished, void *userdata);
+RawExport* raw_export_unref(RawExport *export);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(RawExport*, raw_export_unref);
+
+int raw_export_start(RawExport *export, const char *path, int fd, ImportCompressType compress);
diff --git a/src/import/export-tar.c b/src/import/export-tar.c
new file mode 100644
index 0000000..ed54676
--- /dev/null
+++ b/src/import/export-tar.c
@@ -0,0 +1,331 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "sd-daemon.h"
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "export-tar.h"
+#include "fd-util.h"
+#include "import-common.h"
+#include "process-util.h"
+#include "ratelimit.h"
+#include "string-util.h"
+#include "tmpfile-util.h"
+#include "util.h"
+
+#define COPY_BUFFER_SIZE (16*1024)
+
+struct TarExport {
+ sd_event *event;
+
+ TarExportFinished on_finished;
+ void *userdata;
+
+ char *path;
+ char *temp_path;
+
+ int output_fd;
+ int tar_fd;
+
+ ImportCompress compress;
+
+ sd_event_source *output_event_source;
+
+ void *buffer;
+ size_t buffer_size;
+ size_t buffer_allocated;
+
+ uint64_t written_compressed;
+ uint64_t written_uncompressed;
+
+ pid_t tar_pid;
+
+ struct stat st;
+ uint64_t quota_referenced;
+
+ unsigned last_percent;
+ RateLimit progress_rate_limit;
+
+ bool eof;
+ bool tried_splice;
+};
+
+TarExport *tar_export_unref(TarExport *e) {
+ if (!e)
+ return NULL;
+
+ sd_event_source_unref(e->output_event_source);
+
+ if (e->tar_pid > 1) {
+ (void) kill_and_sigcont(e->tar_pid, SIGKILL);
+ (void) wait_for_terminate(e->tar_pid, NULL);
+ }
+
+ if (e->temp_path) {
+ (void) btrfs_subvol_remove(e->temp_path, BTRFS_REMOVE_QUOTA);
+ free(e->temp_path);
+ }
+
+ import_compress_free(&e->compress);
+
+ sd_event_unref(e->event);
+
+ safe_close(e->tar_fd);
+
+ free(e->buffer);
+ free(e->path);
+ return mfree(e);
+}
+
+int tar_export_new(
+ TarExport **ret,
+ sd_event *event,
+ TarExportFinished on_finished,
+ void *userdata) {
+
+ _cleanup_(tar_export_unrefp) TarExport *e = NULL;
+ int r;
+
+ assert(ret);
+
+ e = new(TarExport, 1);
+ if (!e)
+ return -ENOMEM;
+
+ *e = (TarExport) {
+ .output_fd = -1,
+ .tar_fd = -1,
+ .on_finished = on_finished,
+ .userdata = userdata,
+ .quota_referenced = (uint64_t) -1,
+ .last_percent = (unsigned) -1,
+ };
+
+ RATELIMIT_INIT(e->progress_rate_limit, 100 * USEC_PER_MSEC, 1);
+
+ if (event)
+ e->event = sd_event_ref(event);
+ else {
+ r = sd_event_default(&e->event);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(e);
+
+ return 0;
+}
+
+static void tar_export_report_progress(TarExport *e) {
+ unsigned percent;
+ assert(e);
+
+ /* Do we have any quota info? If not, we don't know anything about the progress */
+ if (e->quota_referenced == (uint64_t) -1)
+ return;
+
+ if (e->written_uncompressed >= e->quota_referenced)
+ percent = 100;
+ else
+ percent = (unsigned) ((e->written_uncompressed * UINT64_C(100)) / e->quota_referenced);
+
+ if (percent == e->last_percent)
+ return;
+
+ if (!ratelimit_below(&e->progress_rate_limit))
+ return;
+
+ sd_notifyf(false, "X_IMPORT_PROGRESS=%u", percent);
+ log_info("Exported %u%%.", percent);
+
+ e->last_percent = percent;
+}
+
+static int tar_export_finish(TarExport *e) {
+ int r;
+
+ assert(e);
+ assert(e->tar_fd >= 0);
+
+ if (e->tar_pid > 0) {
+ r = wait_for_terminate_and_check("tar", e->tar_pid, WAIT_LOG);
+ e->tar_pid = 0;
+ if (r < 0)
+ return r;
+ if (r != EXIT_SUCCESS)
+ return -EPROTO;
+ }
+
+ e->tar_fd = safe_close(e->tar_fd);
+
+ return 0;
+}
+
+static int tar_export_process(TarExport *e) {
+ ssize_t l;
+ int r;
+
+ assert(e);
+
+ if (!e->tried_splice && e->compress.type == IMPORT_COMPRESS_UNCOMPRESSED) {
+
+ l = splice(e->tar_fd, NULL, e->output_fd, NULL, COPY_BUFFER_SIZE, 0);
+ if (l < 0) {
+ if (errno == EAGAIN)
+ return 0;
+
+ e->tried_splice = true;
+ } else if (l == 0) {
+ r = tar_export_finish(e);
+ goto finish;
+ } else {
+ e->written_uncompressed += l;
+ e->written_compressed += l;
+
+ tar_export_report_progress(e);
+
+ return 0;
+ }
+ }
+
+ while (e->buffer_size <= 0) {
+ uint8_t input[COPY_BUFFER_SIZE];
+
+ if (e->eof) {
+ r = tar_export_finish(e);
+ goto finish;
+ }
+
+ l = read(e->tar_fd, input, sizeof(input));
+ if (l < 0) {
+ r = log_error_errno(errno, "Failed to read tar file: %m");
+ goto finish;
+ }
+
+ if (l == 0) {
+ e->eof = true;
+ r = import_compress_finish(&e->compress, &e->buffer, &e->buffer_size, &e->buffer_allocated);
+ } else {
+ e->written_uncompressed += l;
+ r = import_compress(&e->compress, input, l, &e->buffer, &e->buffer_size, &e->buffer_allocated);
+ }
+ if (r < 0) {
+ r = log_error_errno(r, "Failed to encode: %m");
+ goto finish;
+ }
+ }
+
+ l = write(e->output_fd, e->buffer, e->buffer_size);
+ if (l < 0) {
+ if (errno == EAGAIN)
+ return 0;
+
+ r = log_error_errno(errno, "Failed to write output file: %m");
+ goto finish;
+ }
+
+ assert((size_t) l <= e->buffer_size);
+ memmove(e->buffer, (uint8_t*) e->buffer + l, e->buffer_size - l);
+ e->buffer_size -= l;
+ e->written_compressed += l;
+
+ tar_export_report_progress(e);
+
+ return 0;
+
+finish:
+ if (e->on_finished)
+ e->on_finished(e, r, e->userdata);
+ else
+ sd_event_exit(e->event, r);
+
+ return 0;
+}
+
+static int tar_export_on_output(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ TarExport *i = userdata;
+
+ return tar_export_process(i);
+}
+
+static int tar_export_on_defer(sd_event_source *s, void *userdata) {
+ TarExport *i = userdata;
+
+ return tar_export_process(i);
+}
+
+int tar_export_start(TarExport *e, const char *path, int fd, ImportCompressType compress) {
+ _cleanup_close_ int sfd = -1;
+ int r;
+
+ assert(e);
+ assert(path);
+ assert(fd >= 0);
+ assert(compress < _IMPORT_COMPRESS_TYPE_MAX);
+ assert(compress != IMPORT_COMPRESS_UNKNOWN);
+
+ if (e->output_fd >= 0)
+ return -EBUSY;
+
+ sfd = open(path, O_DIRECTORY|O_RDONLY|O_NOCTTY|O_CLOEXEC);
+ if (sfd < 0)
+ return -errno;
+
+ if (fstat(sfd, &e->st) < 0)
+ return -errno;
+
+ r = fd_nonblock(fd, true);
+ if (r < 0)
+ return r;
+
+ r = free_and_strdup(&e->path, path);
+ if (r < 0)
+ return r;
+
+ e->quota_referenced = (uint64_t) -1;
+
+ if (e->st.st_ino == 256) { /* might be a btrfs subvolume? */
+ BtrfsQuotaInfo q;
+
+ r = btrfs_subvol_get_subtree_quota_fd(sfd, 0, &q);
+ if (r >= 0)
+ e->quota_referenced = q.referenced;
+
+ e->temp_path = mfree(e->temp_path);
+
+ r = tempfn_random(path, NULL, &e->temp_path);
+ if (r < 0)
+ return r;
+
+ /* Let's try to make a snapshot, if we can, so that the export is atomic */
+ r = btrfs_subvol_snapshot_fd(sfd, e->temp_path, BTRFS_SNAPSHOT_READ_ONLY|BTRFS_SNAPSHOT_RECURSIVE);
+ if (r < 0) {
+ log_debug_errno(r, "Couldn't create snapshot %s of %s, not exporting atomically: %m", e->temp_path, path);
+ e->temp_path = mfree(e->temp_path);
+ }
+ }
+
+ r = import_compress_init(&e->compress, compress);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_io(e->event, &e->output_event_source, fd, EPOLLOUT, tar_export_on_output, e);
+ if (r == -EPERM) {
+ r = sd_event_add_defer(e->event, &e->output_event_source, tar_export_on_defer, e);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_enabled(e->output_event_source, SD_EVENT_ON);
+ }
+ if (r < 0)
+ return r;
+
+ e->tar_fd = import_fork_tar_c(e->temp_path ?: e->path, &e->tar_pid);
+ if (e->tar_fd < 0) {
+ e->output_event_source = sd_event_source_unref(e->output_event_source);
+ return e->tar_fd;
+ }
+
+ e->output_fd = fd;
+ return r;
+}
diff --git a/src/import/export-tar.h b/src/import/export-tar.h
new file mode 100644
index 0000000..6abb7d3
--- /dev/null
+++ b/src/import/export-tar.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-event.h"
+
+#include "import-compress.h"
+#include "macro.h"
+
+typedef struct TarExport TarExport;
+
+typedef void (*TarExportFinished)(TarExport *export, int error, void *userdata);
+
+int tar_export_new(TarExport **export, sd_event *event, TarExportFinished on_finished, void *userdata);
+TarExport* tar_export_unref(TarExport *export);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(TarExport*, tar_export_unref);
+
+int tar_export_start(TarExport *export, const char *path, int fd, ImportCompressType compress);
diff --git a/src/import/export.c b/src/import/export.c
new file mode 100644
index 0000000..4907106
--- /dev/null
+++ b/src/import/export.c
@@ -0,0 +1,298 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <getopt.h>
+
+#include "sd-event.h"
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "export-raw.h"
+#include "export-tar.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "hostname-util.h"
+#include "import-util.h"
+#include "machine-image.h"
+#include "main-func.h"
+#include "signal-util.h"
+#include "string-util.h"
+#include "verbs.h"
+
+static ImportCompressType arg_compress = IMPORT_COMPRESS_UNKNOWN;
+
+static void determine_compression_from_filename(const char *p) {
+
+ if (arg_compress != IMPORT_COMPRESS_UNKNOWN)
+ return;
+
+ if (!p) {
+ arg_compress = IMPORT_COMPRESS_UNCOMPRESSED;
+ return;
+ }
+
+ if (endswith(p, ".xz"))
+ arg_compress = IMPORT_COMPRESS_XZ;
+ else if (endswith(p, ".gz"))
+ arg_compress = IMPORT_COMPRESS_GZIP;
+ else if (endswith(p, ".bz2"))
+ arg_compress = IMPORT_COMPRESS_BZIP2;
+ else
+ arg_compress = IMPORT_COMPRESS_UNCOMPRESSED;
+}
+
+static int interrupt_signal_handler(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ log_notice("Transfer aborted.");
+ sd_event_exit(sd_event_source_get_event(s), EINTR);
+ return 0;
+}
+
+static void on_tar_finished(TarExport *export, int error, void *userdata) {
+ sd_event *event = userdata;
+ assert(export);
+
+ if (error == 0)
+ log_info("Operation completed successfully.");
+
+ sd_event_exit(event, abs(error));
+}
+
+static int export_tar(int argc, char *argv[], void *userdata) {
+ _cleanup_(tar_export_unrefp) TarExport *export = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_(image_unrefp) Image *image = NULL;
+ const char *path = NULL, *local = NULL;
+ _cleanup_close_ int open_fd = -1;
+ int r, fd;
+
+ if (machine_name_is_valid(argv[1])) {
+ r = image_find(IMAGE_MACHINE, argv[1], &image);
+ if (r == -ENOENT)
+ return log_error_errno(r, "Machine image %s not found.", argv[1]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to look for machine %s: %m", argv[1]);
+
+ local = image->path;
+ } else
+ local = argv[1];
+
+ if (argc >= 3)
+ path = argv[2];
+ if (isempty(path) || streq(path, "-"))
+ path = NULL;
+
+ determine_compression_from_filename(path);
+
+ if (path) {
+ open_fd = open(path, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC|O_NOCTTY, 0666);
+ if (open_fd < 0)
+ return log_error_errno(errno, "Failed to open tar image for export: %m");
+
+ fd = open_fd;
+
+ log_info("Exporting '%s', saving to '%s' with compression '%s'.", local, path, import_compress_type_to_string(arg_compress));
+ } else {
+ _cleanup_free_ char *pretty = NULL;
+
+ fd = STDOUT_FILENO;
+
+ (void) fd_get_path(fd, &pretty);
+ log_info("Exporting '%s', saving to '%s' with compression '%s'.", local, strna(pretty), import_compress_type_to_string(arg_compress));
+ }
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+ (void) sd_event_add_signal(event, NULL, SIGTERM, interrupt_signal_handler, NULL);
+ (void) sd_event_add_signal(event, NULL, SIGINT, interrupt_signal_handler, NULL);
+
+ r = tar_export_new(&export, event, on_tar_finished, event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate exporter: %m");
+
+ r = tar_export_start(export, local, fd, arg_compress);
+ if (r < 0)
+ return log_error_errno(r, "Failed to export image: %m");
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ log_info("Exiting.");
+ return -r;
+}
+
+static void on_raw_finished(RawExport *export, int error, void *userdata) {
+ sd_event *event = userdata;
+ assert(export);
+
+ if (error == 0)
+ log_info("Operation completed successfully.");
+
+ sd_event_exit(event, abs(error));
+}
+
+static int export_raw(int argc, char *argv[], void *userdata) {
+ _cleanup_(raw_export_unrefp) RawExport *export = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_(image_unrefp) Image *image = NULL;
+ const char *path = NULL, *local = NULL;
+ _cleanup_close_ int open_fd = -1;
+ int r, fd;
+
+ if (machine_name_is_valid(argv[1])) {
+ r = image_find(IMAGE_MACHINE, argv[1], &image);
+ if (r == -ENOENT)
+ return log_error_errno(r, "Machine image %s not found.", argv[1]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to look for machine %s: %m", argv[1]);
+
+ local = image->path;
+ } else
+ local = argv[1];
+
+ if (argc >= 3)
+ path = argv[2];
+ if (isempty(path) || streq(path, "-"))
+ path = NULL;
+
+ determine_compression_from_filename(path);
+
+ if (path) {
+ open_fd = open(path, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC|O_NOCTTY, 0666);
+ if (open_fd < 0)
+ return log_error_errno(errno, "Failed to open raw image for export: %m");
+
+ fd = open_fd;
+
+ log_info("Exporting '%s', saving to '%s' with compression '%s'.", local, path, import_compress_type_to_string(arg_compress));
+ } else {
+ _cleanup_free_ char *pretty = NULL;
+
+ fd = STDOUT_FILENO;
+
+ (void) fd_get_path(fd, &pretty);
+ log_info("Exporting '%s', saving to '%s' with compression '%s'.", local, strna(pretty), import_compress_type_to_string(arg_compress));
+ }
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+ (void) sd_event_add_signal(event, NULL, SIGTERM, interrupt_signal_handler, NULL);
+ (void) sd_event_add_signal(event, NULL, SIGINT, interrupt_signal_handler, NULL);
+
+ r = raw_export_new(&export, event, on_raw_finished, event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate exporter: %m");
+
+ r = raw_export_start(export, local, fd, arg_compress);
+ if (r < 0)
+ return log_error_errno(r, "Failed to export image: %m");
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ log_info("Exiting.");
+ return -r;
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+
+ printf("%s [OPTIONS...] {COMMAND} ...\n\n"
+ "Export container or virtual machine images.\n\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --format=FORMAT Select format\n\n"
+ "Commands:\n"
+ " tar NAME [FILE] Export a TAR image\n"
+ " raw NAME [FILE] Export a RAW image\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_FORMAT,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "format", required_argument, NULL, ARG_FORMAT },
+ {}
+ };
+
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ return help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_FORMAT:
+ if (streq(optarg, "uncompressed"))
+ arg_compress = IMPORT_COMPRESS_UNCOMPRESSED;
+ else if (streq(optarg, "xz"))
+ arg_compress = IMPORT_COMPRESS_XZ;
+ else if (streq(optarg, "gzip"))
+ arg_compress = IMPORT_COMPRESS_GZIP;
+ else if (streq(optarg, "bzip2"))
+ arg_compress = IMPORT_COMPRESS_BZIP2;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown format: %s", optarg);
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+
+ return 1;
+}
+
+static int export_main(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "tar", 2, 3, 0, export_tar },
+ { "raw", 2, 3, 0, export_raw },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ setlocale(LC_ALL, "");
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ (void) ignore_signals(SIGPIPE, -1);
+
+ return export_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/import/import-common.c b/src/import/import-common.c
new file mode 100644
index 0000000..89f0301
--- /dev/null
+++ b/src/import/import-common.c
@@ -0,0 +1,259 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <sched.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "capability-util.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "import-common.h"
+#include "os-util.h"
+#include "process-util.h"
+#include "signal-util.h"
+#include "tmpfile-util.h"
+#include "util.h"
+
+int import_make_read_only_fd(int fd) {
+ int r;
+
+ assert(fd >= 0);
+
+ /* First, let's make this a read-only subvolume if it refers
+ * to a subvolume */
+ r = btrfs_subvol_set_read_only_fd(fd, true);
+ if (IN_SET(r, -ENOTTY, -ENOTDIR, -EINVAL)) {
+ struct stat st;
+
+ /* This doesn't refer to a subvolume, or the file
+ * system isn't even btrfs. In that, case fall back to
+ * chmod()ing */
+
+ r = fstat(fd, &st);
+ if (r < 0)
+ return log_error_errno(errno, "Failed to stat temporary image: %m");
+
+ /* Drop "w" flag */
+ if (fchmod(fd, st.st_mode & 07555) < 0)
+ return log_error_errno(errno, "Failed to chmod() final image: %m");
+
+ return 0;
+
+ } else if (r < 0)
+ return log_error_errno(r, "Failed to make subvolume read-only: %m");
+
+ return 0;
+}
+
+int import_make_read_only(const char *path) {
+ _cleanup_close_ int fd = 1;
+
+ fd = open(path, O_RDONLY|O_NOCTTY|O_CLOEXEC);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open %s: %m", path);
+
+ return import_make_read_only_fd(fd);
+}
+
+int import_fork_tar_x(const char *path, pid_t *ret) {
+ _cleanup_close_pair_ int pipefd[2] = { -1, -1 };
+ pid_t pid;
+ int r;
+
+ assert(path);
+ assert(ret);
+
+ if (pipe2(pipefd, O_CLOEXEC) < 0)
+ return log_error_errno(errno, "Failed to create pipe for tar: %m");
+
+ r = safe_fork("(tar)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ uint64_t retain =
+ (1ULL << CAP_CHOWN) |
+ (1ULL << CAP_FOWNER) |
+ (1ULL << CAP_FSETID) |
+ (1ULL << CAP_MKNOD) |
+ (1ULL << CAP_SETFCAP) |
+ (1ULL << CAP_DAC_OVERRIDE);
+
+ /* Child */
+
+ pipefd[1] = safe_close(pipefd[1]);
+
+ r = rearrange_stdio(pipefd[0], -1, STDERR_FILENO);
+ if (r < 0) {
+ log_error_errno(r, "Failed to rearrange stdin/stdout: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ if (unshare(CLONE_NEWNET) < 0)
+ log_error_errno(errno, "Failed to lock tar into network namespace, ignoring: %m");
+
+ r = capability_bounding_set_drop(retain, true);
+ if (r < 0)
+ log_error_errno(r, "Failed to drop capabilities, ignoring: %m");
+
+ execlp("tar", "tar", "--numeric-owner", "-C", path, "-px", "--xattrs", "--xattrs-include=*", NULL);
+ log_error_errno(errno, "Failed to execute tar: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ *ret = pid;
+
+ return TAKE_FD(pipefd[1]);
+}
+
+int import_fork_tar_c(const char *path, pid_t *ret) {
+ _cleanup_close_pair_ int pipefd[2] = { -1, -1 };
+ pid_t pid;
+ int r;
+
+ assert(path);
+ assert(ret);
+
+ if (pipe2(pipefd, O_CLOEXEC) < 0)
+ return log_error_errno(errno, "Failed to create pipe for tar: %m");
+
+ r = safe_fork("(tar)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ uint64_t retain = (1ULL << CAP_DAC_OVERRIDE);
+
+ /* Child */
+
+ pipefd[0] = safe_close(pipefd[0]);
+
+ r = rearrange_stdio(-1, pipefd[1], STDERR_FILENO);
+ if (r < 0) {
+ log_error_errno(r, "Failed to rearrange stdin/stdout: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ if (unshare(CLONE_NEWNET) < 0)
+ log_error_errno(errno, "Failed to lock tar into network namespace, ignoring: %m");
+
+ r = capability_bounding_set_drop(retain, true);
+ if (r < 0)
+ log_error_errno(r, "Failed to drop capabilities, ignoring: %m");
+
+ execlp("tar", "tar", "-C", path, "-c", "--xattrs", "--xattrs-include=*", ".", NULL);
+ log_error_errno(errno, "Failed to execute tar: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ *ret = pid;
+
+ return TAKE_FD(pipefd[0]);
+}
+
+int import_mangle_os_tree(const char *path) {
+ _cleanup_closedir_ DIR *d = NULL, *cd = NULL;
+ _cleanup_free_ char *child = NULL, *t = NULL;
+ const char *joined;
+ struct dirent *de;
+ int r;
+
+ assert(path);
+
+ /* Some tarballs contain a single top-level directory that contains the actual OS directory tree. Try to
+ * recognize this, and move the tree one level up. */
+
+ r = path_is_os_tree(path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine whether '%s' is an OS tree: %m", path);
+ if (r > 0) {
+ log_debug("Directory tree '%s' is a valid OS tree.", path);
+ return 0;
+ }
+
+ log_debug("Directory tree '%s' is not recognizable as OS tree, checking whether to rearrange it.", path);
+
+ d = opendir(path);
+ if (!d)
+ return log_error_errno(r, "Failed to open directory '%s': %m", path);
+
+ errno = 0;
+ de = readdir_no_dot(d);
+ if (!de) {
+ if (errno != 0)
+ return log_error_errno(errno, "Failed to iterate through directory '%s': %m", path);
+
+ log_debug("Directory '%s' is empty, leaving it as it is.", path);
+ return 0;
+ }
+
+ child = strdup(de->d_name);
+ if (!child)
+ return log_oom();
+
+ errno = 0;
+ de = readdir_no_dot(d);
+ if (de) {
+ if (errno != 0)
+ return log_error_errno(errno, "Failed to iterate through directory '%s': %m", path);
+
+ log_debug("Directory '%s' does not look like a directory tree, and has multiple children, leaving as it is.", path);
+ return 0;
+ }
+
+ joined = strjoina(path, "/", child);
+ r = path_is_os_tree(joined);
+ if (r == -ENOTDIR) {
+ log_debug("Directory '%s' does not look like a directory tree, and contains a single regular file only, leaving as it is.", path);
+ return 0;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine whether '%s' is an OS tree: %m", joined);
+ if (r == 0) {
+ log_debug("Neither '%s' nor '%s' is a valid OS tree, leaving them as they are.", path, joined);
+ return 0;
+ }
+
+ /* Nice, we have checked now:
+ *
+ * 1. The top-level directory does not qualify as OS tree
+ * 1. The top-level directory only contains one item
+ * 2. That item is a directory
+ * 3. And that directory qualifies as OS tree
+ *
+ * Let's now rearrange things, moving everything in the inner directory one level up */
+
+ cd = xopendirat(dirfd(d), child, O_NOFOLLOW);
+ if (!cd)
+ return log_error_errno(errno, "Can't open directory '%s': %m", joined);
+
+ log_info("Rearranging '%s', moving OS tree one directory up.", joined);
+
+ /* Let's rename the child to an unguessable name so that we can be sure all files contained in it can be
+ * safely moved up and won't collide with the name. */
+ r = tempfn_random(child, NULL, &t);
+ if (r < 0)
+ return log_oom();
+ r = rename_noreplace(dirfd(d), child, dirfd(d), t);
+ if (r < 0)
+ return log_error_errno(r, "Unable to rename '%s' to '%s/%s': %m", joined, path, t);
+
+ FOREACH_DIRENT_ALL(de, cd, return log_error_errno(errno, "Failed to iterate through directory '%s': %m", joined)) {
+ if (dot_or_dot_dot(de->d_name))
+ continue;
+
+ r = rename_noreplace(dirfd(cd), de->d_name, dirfd(d), de->d_name);
+ if (r < 0)
+ return log_error_errno(r, "Unable to move '%s/%s/%s' to '%s/%s': %m", path, t, de->d_name, path, de->d_name);
+ }
+
+ if (unlinkat(dirfd(d), t, AT_REMOVEDIR) < 0)
+ return log_error_errno(errno, "Failed to remove temporary directory '%s/%s': %m", path, t);
+
+ log_info("Successfully rearranged OS tree.");
+
+ return 0;
+}
diff --git a/src/import/import-common.h b/src/import/import-common.h
new file mode 100644
index 0000000..94d224f
--- /dev/null
+++ b/src/import/import-common.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <sys/types.h>
+
+int import_make_read_only_fd(int fd);
+int import_make_read_only(const char *path);
+
+int import_fork_tar_c(const char *path, pid_t *ret);
+int import_fork_tar_x(const char *path, pid_t *ret);
+
+int import_mangle_os_tree(const char *path);
diff --git a/src/import/import-compress.c b/src/import/import-compress.c
new file mode 100644
index 0000000..3fbd067
--- /dev/null
+++ b/src/import/import-compress.c
@@ -0,0 +1,466 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "import-compress.h"
+#include "string-table.h"
+#include "util.h"
+
+void import_compress_free(ImportCompress *c) {
+ assert(c);
+
+ if (c->type == IMPORT_COMPRESS_XZ)
+ lzma_end(&c->xz);
+ else if (c->type == IMPORT_COMPRESS_GZIP) {
+ if (c->encoding)
+ deflateEnd(&c->gzip);
+ else
+ inflateEnd(&c->gzip);
+#if HAVE_BZIP2
+ } else if (c->type == IMPORT_COMPRESS_BZIP2) {
+ if (c->encoding)
+ BZ2_bzCompressEnd(&c->bzip2);
+ else
+ BZ2_bzDecompressEnd(&c->bzip2);
+#endif
+ }
+
+ c->type = IMPORT_COMPRESS_UNKNOWN;
+}
+
+int import_uncompress_detect(ImportCompress *c, const void *data, size_t size) {
+ static const uint8_t xz_signature[] = {
+ 0xfd, '7', 'z', 'X', 'Z', 0x00
+ };
+ static const uint8_t gzip_signature[] = {
+ 0x1f, 0x8b
+ };
+ static const uint8_t bzip2_signature[] = {
+ 'B', 'Z', 'h'
+ };
+
+ int r;
+
+ assert(c);
+
+ if (c->type != IMPORT_COMPRESS_UNKNOWN)
+ return 1;
+
+ if (size < MAX3(sizeof(xz_signature),
+ sizeof(gzip_signature),
+ sizeof(bzip2_signature)))
+ return 0;
+
+ assert(data);
+
+ if (memcmp(data, xz_signature, sizeof(xz_signature)) == 0) {
+ lzma_ret xzr;
+
+ xzr = lzma_stream_decoder(&c->xz, UINT64_MAX, LZMA_TELL_UNSUPPORTED_CHECK | LZMA_CONCATENATED);
+ if (xzr != LZMA_OK)
+ return -EIO;
+
+ c->type = IMPORT_COMPRESS_XZ;
+
+ } else if (memcmp(data, gzip_signature, sizeof(gzip_signature)) == 0) {
+ r = inflateInit2(&c->gzip, 15+16);
+ if (r != Z_OK)
+ return -EIO;
+
+ c->type = IMPORT_COMPRESS_GZIP;
+
+#if HAVE_BZIP2
+ } else if (memcmp(data, bzip2_signature, sizeof(bzip2_signature)) == 0) {
+ r = BZ2_bzDecompressInit(&c->bzip2, 0, 0);
+ if (r != BZ_OK)
+ return -EIO;
+
+ c->type = IMPORT_COMPRESS_BZIP2;
+#endif
+ } else
+ c->type = IMPORT_COMPRESS_UNCOMPRESSED;
+
+ c->encoding = false;
+
+ return 1;
+}
+
+int import_uncompress(ImportCompress *c, const void *data, size_t size, ImportCompressCallback callback, void *userdata) {
+ int r;
+
+ assert(c);
+ assert(callback);
+
+ r = import_uncompress_detect(c, data, size);
+ if (r <= 0)
+ return r;
+
+ if (c->encoding)
+ return -EINVAL;
+
+ if (size <= 0)
+ return 1;
+
+ assert(data);
+
+ switch (c->type) {
+
+ case IMPORT_COMPRESS_UNCOMPRESSED:
+ r = callback(data, size, userdata);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case IMPORT_COMPRESS_XZ:
+ c->xz.next_in = data;
+ c->xz.avail_in = size;
+
+ while (c->xz.avail_in > 0) {
+ uint8_t buffer[16 * 1024];
+ lzma_ret lzr;
+
+ c->xz.next_out = buffer;
+ c->xz.avail_out = sizeof(buffer);
+
+ lzr = lzma_code(&c->xz, LZMA_RUN);
+ if (!IN_SET(lzr, LZMA_OK, LZMA_STREAM_END))
+ return -EIO;
+
+ r = callback(buffer, sizeof(buffer) - c->xz.avail_out, userdata);
+ if (r < 0)
+ return r;
+ }
+
+ break;
+
+ case IMPORT_COMPRESS_GZIP:
+ c->gzip.next_in = (void*) data;
+ c->gzip.avail_in = size;
+
+ while (c->gzip.avail_in > 0) {
+ uint8_t buffer[16 * 1024];
+
+ c->gzip.next_out = buffer;
+ c->gzip.avail_out = sizeof(buffer);
+
+ r = inflate(&c->gzip, Z_NO_FLUSH);
+ if (!IN_SET(r, Z_OK, Z_STREAM_END))
+ return -EIO;
+
+ r = callback(buffer, sizeof(buffer) - c->gzip.avail_out, userdata);
+ if (r < 0)
+ return r;
+ }
+
+ break;
+
+#if HAVE_BZIP2
+ case IMPORT_COMPRESS_BZIP2:
+ c->bzip2.next_in = (void*) data;
+ c->bzip2.avail_in = size;
+
+ while (c->bzip2.avail_in > 0) {
+ uint8_t buffer[16 * 1024];
+
+ c->bzip2.next_out = (char*) buffer;
+ c->bzip2.avail_out = sizeof(buffer);
+
+ r = BZ2_bzDecompress(&c->bzip2);
+ if (!IN_SET(r, BZ_OK, BZ_STREAM_END))
+ return -EIO;
+
+ r = callback(buffer, sizeof(buffer) - c->bzip2.avail_out, userdata);
+ if (r < 0)
+ return r;
+ }
+
+ break;
+#endif
+
+ default:
+ assert_not_reached("Unknown compression");
+ }
+
+ return 1;
+}
+
+int import_compress_init(ImportCompress *c, ImportCompressType t) {
+ int r;
+
+ assert(c);
+
+ switch (t) {
+
+ case IMPORT_COMPRESS_XZ: {
+ lzma_ret xzr;
+
+ xzr = lzma_easy_encoder(&c->xz, LZMA_PRESET_DEFAULT, LZMA_CHECK_CRC64);
+ if (xzr != LZMA_OK)
+ return -EIO;
+
+ c->type = IMPORT_COMPRESS_XZ;
+ break;
+ }
+
+ case IMPORT_COMPRESS_GZIP:
+ r = deflateInit2(&c->gzip, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
+ if (r != Z_OK)
+ return -EIO;
+
+ c->type = IMPORT_COMPRESS_GZIP;
+ break;
+
+#if HAVE_BZIP2
+ case IMPORT_COMPRESS_BZIP2:
+ r = BZ2_bzCompressInit(&c->bzip2, 9, 0, 0);
+ if (r != BZ_OK)
+ return -EIO;
+
+ c->type = IMPORT_COMPRESS_BZIP2;
+ break;
+#endif
+
+ case IMPORT_COMPRESS_UNCOMPRESSED:
+ c->type = IMPORT_COMPRESS_UNCOMPRESSED;
+ break;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ c->encoding = true;
+ return 0;
+}
+
+static int enlarge_buffer(void **buffer, size_t *buffer_size, size_t *buffer_allocated) {
+ size_t l;
+ void *p;
+
+ if (*buffer_allocated > *buffer_size)
+ return 0;
+
+ l = MAX(16*1024U, (*buffer_size * 2));
+ p = realloc(*buffer, l);
+ if (!p)
+ return -ENOMEM;
+
+ *buffer = p;
+ *buffer_allocated = l;
+
+ return 1;
+}
+
+int import_compress(ImportCompress *c, const void *data, size_t size, void **buffer, size_t *buffer_size, size_t *buffer_allocated) {
+ int r;
+
+ assert(c);
+ assert(buffer);
+ assert(buffer_size);
+ assert(buffer_allocated);
+
+ if (!c->encoding)
+ return -EINVAL;
+
+ if (size <= 0)
+ return 0;
+
+ assert(data);
+
+ *buffer_size = 0;
+
+ switch (c->type) {
+
+ case IMPORT_COMPRESS_XZ:
+
+ c->xz.next_in = data;
+ c->xz.avail_in = size;
+
+ while (c->xz.avail_in > 0) {
+ lzma_ret lzr;
+
+ r = enlarge_buffer(buffer, buffer_size, buffer_allocated);
+ if (r < 0)
+ return r;
+
+ c->xz.next_out = (uint8_t*) *buffer + *buffer_size;
+ c->xz.avail_out = *buffer_allocated - *buffer_size;
+
+ lzr = lzma_code(&c->xz, LZMA_RUN);
+ if (lzr != LZMA_OK)
+ return -EIO;
+
+ *buffer_size += (*buffer_allocated - *buffer_size) - c->xz.avail_out;
+ }
+
+ break;
+
+ case IMPORT_COMPRESS_GZIP:
+
+ c->gzip.next_in = (void*) data;
+ c->gzip.avail_in = size;
+
+ while (c->gzip.avail_in > 0) {
+ r = enlarge_buffer(buffer, buffer_size, buffer_allocated);
+ if (r < 0)
+ return r;
+
+ c->gzip.next_out = (uint8_t*) *buffer + *buffer_size;
+ c->gzip.avail_out = *buffer_allocated - *buffer_size;
+
+ r = deflate(&c->gzip, Z_NO_FLUSH);
+ if (r != Z_OK)
+ return -EIO;
+
+ *buffer_size += (*buffer_allocated - *buffer_size) - c->gzip.avail_out;
+ }
+
+ break;
+
+#if HAVE_BZIP2
+ case IMPORT_COMPRESS_BZIP2:
+
+ c->bzip2.next_in = (void*) data;
+ c->bzip2.avail_in = size;
+
+ while (c->bzip2.avail_in > 0) {
+ r = enlarge_buffer(buffer, buffer_size, buffer_allocated);
+ if (r < 0)
+ return r;
+
+ c->bzip2.next_out = (void*) ((uint8_t*) *buffer + *buffer_size);
+ c->bzip2.avail_out = *buffer_allocated - *buffer_size;
+
+ r = BZ2_bzCompress(&c->bzip2, BZ_RUN);
+ if (r != BZ_RUN_OK)
+ return -EIO;
+
+ *buffer_size += (*buffer_allocated - *buffer_size) - c->bzip2.avail_out;
+ }
+
+ break;
+#endif
+
+ case IMPORT_COMPRESS_UNCOMPRESSED:
+
+ if (*buffer_allocated < size) {
+ void *p;
+
+ p = realloc(*buffer, size);
+ if (!p)
+ return -ENOMEM;
+
+ *buffer = p;
+ *buffer_allocated = size;
+ }
+
+ memcpy(*buffer, data, size);
+ *buffer_size = size;
+ break;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ return 0;
+}
+
+int import_compress_finish(ImportCompress *c, void **buffer, size_t *buffer_size, size_t *buffer_allocated) {
+ int r;
+
+ assert(c);
+ assert(buffer);
+ assert(buffer_size);
+ assert(buffer_allocated);
+
+ if (!c->encoding)
+ return -EINVAL;
+
+ *buffer_size = 0;
+
+ switch (c->type) {
+
+ case IMPORT_COMPRESS_XZ: {
+ lzma_ret lzr;
+
+ c->xz.avail_in = 0;
+
+ do {
+ r = enlarge_buffer(buffer, buffer_size, buffer_allocated);
+ if (r < 0)
+ return r;
+
+ c->xz.next_out = (uint8_t*) *buffer + *buffer_size;
+ c->xz.avail_out = *buffer_allocated - *buffer_size;
+
+ lzr = lzma_code(&c->xz, LZMA_FINISH);
+ if (!IN_SET(lzr, LZMA_OK, LZMA_STREAM_END))
+ return -EIO;
+
+ *buffer_size += (*buffer_allocated - *buffer_size) - c->xz.avail_out;
+ } while (lzr != LZMA_STREAM_END);
+
+ break;
+ }
+
+ case IMPORT_COMPRESS_GZIP:
+ c->gzip.avail_in = 0;
+
+ do {
+ r = enlarge_buffer(buffer, buffer_size, buffer_allocated);
+ if (r < 0)
+ return r;
+
+ c->gzip.next_out = (uint8_t*) *buffer + *buffer_size;
+ c->gzip.avail_out = *buffer_allocated - *buffer_size;
+
+ r = deflate(&c->gzip, Z_FINISH);
+ if (!IN_SET(r, Z_OK, Z_STREAM_END))
+ return -EIO;
+
+ *buffer_size += (*buffer_allocated - *buffer_size) - c->gzip.avail_out;
+ } while (r != Z_STREAM_END);
+
+ break;
+
+#if HAVE_BZIP2
+ case IMPORT_COMPRESS_BZIP2:
+ c->bzip2.avail_in = 0;
+
+ do {
+ r = enlarge_buffer(buffer, buffer_size, buffer_allocated);
+ if (r < 0)
+ return r;
+
+ c->bzip2.next_out = (void*) ((uint8_t*) *buffer + *buffer_size);
+ c->bzip2.avail_out = *buffer_allocated - *buffer_size;
+
+ r = BZ2_bzCompress(&c->bzip2, BZ_FINISH);
+ if (!IN_SET(r, BZ_FINISH_OK, BZ_STREAM_END))
+ return -EIO;
+
+ *buffer_size += (*buffer_allocated - *buffer_size) - c->bzip2.avail_out;
+ } while (r != BZ_STREAM_END);
+
+ break;
+#endif
+
+ case IMPORT_COMPRESS_UNCOMPRESSED:
+ break;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ return 0;
+}
+
+static const char* const import_compress_type_table[_IMPORT_COMPRESS_TYPE_MAX] = {
+ [IMPORT_COMPRESS_UNKNOWN] = "unknown",
+ [IMPORT_COMPRESS_UNCOMPRESSED] = "uncompressed",
+ [IMPORT_COMPRESS_XZ] = "xz",
+ [IMPORT_COMPRESS_GZIP] = "gzip",
+#if HAVE_BZIP2
+ [IMPORT_COMPRESS_BZIP2] = "bzip2",
+#endif
+};
+
+DEFINE_STRING_TABLE_LOOKUP(import_compress_type, ImportCompressType);
diff --git a/src/import/import-compress.h b/src/import/import-compress.h
new file mode 100644
index 0000000..859bd0e
--- /dev/null
+++ b/src/import/import-compress.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#if HAVE_BZIP2
+#include <bzlib.h>
+#endif
+#include <lzma.h>
+#include <sys/types.h>
+#include <zlib.h>
+
+#include "macro.h"
+
+typedef enum ImportCompressType {
+ IMPORT_COMPRESS_UNKNOWN,
+ IMPORT_COMPRESS_UNCOMPRESSED,
+ IMPORT_COMPRESS_XZ,
+ IMPORT_COMPRESS_GZIP,
+ IMPORT_COMPRESS_BZIP2,
+ _IMPORT_COMPRESS_TYPE_MAX,
+ _IMPORT_COMPRESS_TYPE_INVALID = -1,
+} ImportCompressType;
+
+typedef struct ImportCompress {
+ ImportCompressType type;
+ bool encoding;
+ union {
+ lzma_stream xz;
+ z_stream gzip;
+#if HAVE_BZIP2
+ bz_stream bzip2;
+#endif
+ };
+} ImportCompress;
+
+typedef int (*ImportCompressCallback)(const void *data, size_t size, void *userdata);
+
+void import_compress_free(ImportCompress *c);
+
+int import_uncompress_detect(ImportCompress *c, const void *data, size_t size);
+int import_uncompress(ImportCompress *c, const void *data, size_t size, ImportCompressCallback callback, void *userdata);
+
+int import_compress_init(ImportCompress *c, ImportCompressType t);
+int import_compress(ImportCompress *c, const void *data, size_t size, void **buffer, size_t *buffer_size, size_t *buffer_allocated);
+int import_compress_finish(ImportCompress *c, void **buffer, size_t *buffer_size, size_t *buffer_allocated);
+
+const char* import_compress_type_to_string(ImportCompressType t) _const_;
+ImportCompressType import_compress_type_from_string(const char *s) _pure_;
diff --git a/src/import/import-fs.c b/src/import/import-fs.c
new file mode 100644
index 0000000..35ba6ba
--- /dev/null
+++ b/src/import/import-fs.c
@@ -0,0 +1,327 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <getopt.h>
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "hostname-util.h"
+#include "import-common.h"
+#include "import-util.h"
+#include "machine-image.h"
+#include "mkdir.h"
+#include "parse-util.h"
+#include "ratelimit.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tmpfile-util.h"
+#include "verbs.h"
+
+static bool arg_force = false;
+static bool arg_read_only = false;
+static const char *arg_image_root = "/var/lib/machines";
+
+typedef struct ProgressInfo {
+ RateLimit limit;
+ char *path;
+ uint64_t size;
+ bool started;
+ bool logged_incomplete;
+} ProgressInfo;
+
+static volatile sig_atomic_t cancelled = false;
+
+static void sigterm_sigint(int sig) {
+ cancelled = true;
+}
+
+static void progress_info_free(ProgressInfo *p) {
+ free(p->path);
+}
+
+static void progress_show(ProgressInfo *p) {
+ assert(p);
+
+ /* Show progress only every now and then. */
+ if (!ratelimit_below(&p->limit))
+ return;
+
+ /* Suppress the first message, start with the second one */
+ if (!p->started) {
+ p->started = true;
+ return;
+ }
+
+ /* Mention the list is incomplete before showing first output. */
+ if (!p->logged_incomplete) {
+ log_notice("(Note, file list shown below is incomplete, and is intended as sporadic progress report only.)");
+ p->logged_incomplete = true;
+ }
+
+ if (p->size == 0)
+ log_info("Copying tree, currently at '%s'...", p->path);
+ else {
+ char buffer[FORMAT_BYTES_MAX];
+
+ log_info("Copying tree, currently at '%s' (@%s)...", p->path, format_bytes(buffer, sizeof(buffer), p->size));
+ }
+}
+
+static int progress_path(const char *path, const struct stat *st, void *userdata) {
+ ProgressInfo *p = userdata;
+ int r;
+
+ assert(p);
+
+ if (cancelled)
+ return -EOWNERDEAD;
+
+ r = free_and_strdup(&p->path, path);
+ if (r < 0)
+ return r;
+
+ p->size = 0;
+
+ progress_show(p);
+ return 0;
+}
+
+static int progress_bytes(uint64_t nbytes, void *userdata) {
+ ProgressInfo *p = userdata;
+
+ assert(p);
+ assert(p->size != UINT64_MAX);
+
+ if (cancelled)
+ return -EOWNERDEAD;
+
+ p->size += nbytes;
+
+ progress_show(p);
+ return 0;
+}
+
+static int import_fs(int argc, char *argv[], void *userdata) {
+ _cleanup_(rm_rf_subvolume_and_freep) char *temp_path = NULL;
+ _cleanup_(progress_info_free) ProgressInfo progress = {};
+ const char *path = NULL, *local = NULL, *final_path;
+ _cleanup_close_ int open_fd = -1;
+ struct sigaction old_sigint_sa, old_sigterm_sa;
+ static const struct sigaction sa = {
+ .sa_handler = sigterm_sigint,
+ .sa_flags = SA_RESTART,
+ };
+ int r, fd;
+
+ if (argc >= 2)
+ path = argv[1];
+ if (isempty(path) || streq(path, "-"))
+ path = NULL;
+
+ if (argc >= 3)
+ local = argv[2];
+ else if (path)
+ local = basename(path);
+ if (isempty(local) || streq(local, "-"))
+ local = NULL;
+
+ if (local) {
+ if (!machine_name_is_valid(local)) {
+ log_error("Local image name '%s' is not valid.", local);
+ return -EINVAL;
+ }
+
+ if (!arg_force) {
+ r = image_find(IMAGE_MACHINE, local, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
+ } else {
+ log_error("Image '%s' already exists.", local);
+ return -EEXIST;
+ }
+ }
+ } else
+ local = "imported";
+
+ if (path) {
+ open_fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+ if (open_fd < 0)
+ return log_error_errno(errno, "Failed to open directory to import: %m");
+
+ fd = open_fd;
+
+ log_info("Importing '%s', saving as '%s'.", path, local);
+ } else {
+ _cleanup_free_ char *pretty = NULL;
+
+ fd = STDIN_FILENO;
+
+ (void) fd_get_path(fd, &pretty);
+ log_info("Importing '%s', saving as '%s'.", strempty(pretty), local);
+ }
+
+ final_path = strjoina(arg_image_root, "/", local);
+
+ r = tempfn_random(final_path, NULL, &temp_path);
+ if (r < 0)
+ return log_oom();
+
+ (void) mkdir_parents_label(temp_path, 0700);
+
+ RATELIMIT_INIT(progress.limit, 200*USEC_PER_MSEC, 1);
+
+ /* Hook into SIGINT/SIGTERM, so that we can cancel things then */
+ assert(sigaction(SIGINT, &sa, &old_sigint_sa) >= 0);
+ assert(sigaction(SIGTERM, &sa, &old_sigterm_sa) >= 0);
+
+ r = btrfs_subvol_snapshot_fd_full(
+ fd,
+ temp_path,
+ BTRFS_SNAPSHOT_FALLBACK_COPY|BTRFS_SNAPSHOT_RECURSIVE|BTRFS_SNAPSHOT_FALLBACK_DIRECTORY|BTRFS_SNAPSHOT_QUOTA,
+ progress_path,
+ progress_bytes,
+ &progress);
+ if (r == -EOWNERDEAD) { /* SIGINT + SIGTERM cause this, see signal handler above */
+ log_error("Copy cancelled.");
+ goto finish;
+ }
+ if (r < 0) {
+ log_error_errno(r, "Failed to copy directory: %m");
+ goto finish;
+ }
+
+ r = import_mangle_os_tree(temp_path);
+ if (r < 0)
+ goto finish;
+
+ (void) import_assign_pool_quota_and_warn(temp_path);
+
+ if (arg_read_only) {
+ r = import_make_read_only(temp_path);
+ if (r < 0) {
+ log_error_errno(r, "Failed to make directory read-only: %m");
+ goto finish;
+ }
+ }
+
+ if (arg_force)
+ (void) rm_rf(final_path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+
+ r = rename_noreplace(AT_FDCWD, temp_path, AT_FDCWD, final_path);
+ if (r < 0) {
+ log_error_errno(r, "Failed to move image into place: %m");
+ goto finish;
+ }
+
+ temp_path = mfree(temp_path);
+
+ log_info("Exiting.");
+
+finish:
+ /* Put old signal handlers into place */
+ assert(sigaction(SIGINT, &old_sigint_sa, NULL) >= 0);
+ assert(sigaction(SIGTERM, &old_sigterm_sa, NULL) >= 0);
+
+ return 0;
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+
+ printf("%s [OPTIONS...] {COMMAND} ...\n\n"
+ "Import container images from a file system.\n\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --force Force creation of image\n"
+ " --image-root=PATH Image root directory\n"
+ " --read-only Create a read-only image\n\n"
+ "Commands:\n"
+ " run DIRECTORY [NAME] Import a directory\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_FORCE,
+ ARG_IMAGE_ROOT,
+ ARG_READ_ONLY,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "force", no_argument, NULL, ARG_FORCE },
+ { "image-root", required_argument, NULL, ARG_IMAGE_ROOT },
+ { "read-only", no_argument, NULL, ARG_READ_ONLY },
+ {}
+ };
+
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ return help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_FORCE:
+ arg_force = true;
+ break;
+
+ case ARG_IMAGE_ROOT:
+ arg_image_root = optarg;
+ break;
+
+ case ARG_READ_ONLY:
+ arg_read_only = true;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+
+ return 1;
+}
+
+static int import_fs_main(int argc, char *argv[]) {
+
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "run", 2, 3, 0, import_fs },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+int main(int argc, char *argv[]) {
+ int r;
+
+ setlocale(LC_ALL, "");
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ goto finish;
+
+ r = import_fs_main(argc, argv);
+
+finish:
+ return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/src/import/import-pubring.gpg b/src/import/import-pubring.gpg
new file mode 100644
index 0000000..be27776
--- /dev/null
+++ b/src/import/import-pubring.gpg
Binary files differ
diff --git a/src/import/import-raw.c b/src/import/import-raw.c
new file mode 100644
index 0000000..4b11615
--- /dev/null
+++ b/src/import/import-raw.c
@@ -0,0 +1,438 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <linux/fs.h>
+
+#include "sd-daemon.h"
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "chattr-util.h"
+#include "copy.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "hostname-util.h"
+#include "import-common.h"
+#include "import-compress.h"
+#include "import-raw.h"
+#include "io-util.h"
+#include "machine-pool.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "qcow2-util.h"
+#include "ratelimit.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tmpfile-util.h"
+#include "util.h"
+
+struct RawImport {
+ sd_event *event;
+
+ char *image_root;
+
+ RawImportFinished on_finished;
+ void *userdata;
+
+ char *local;
+ bool force_local;
+ bool read_only;
+
+ char *temp_path;
+ char *final_path;
+
+ int input_fd;
+ int output_fd;
+
+ ImportCompress compress;
+
+ sd_event_source *input_event_source;
+
+ uint8_t buffer[16*1024];
+ size_t buffer_size;
+
+ uint64_t written_compressed;
+ uint64_t written_uncompressed;
+
+ struct stat st;
+
+ unsigned last_percent;
+ RateLimit progress_rate_limit;
+};
+
+RawImport* raw_import_unref(RawImport *i) {
+ if (!i)
+ return NULL;
+
+ sd_event_unref(i->event);
+
+ if (i->temp_path) {
+ (void) unlink(i->temp_path);
+ free(i->temp_path);
+ }
+
+ import_compress_free(&i->compress);
+
+ sd_event_source_unref(i->input_event_source);
+
+ safe_close(i->output_fd);
+
+ free(i->final_path);
+ free(i->image_root);
+ free(i->local);
+ return mfree(i);
+}
+
+int raw_import_new(
+ RawImport **ret,
+ sd_event *event,
+ const char *image_root,
+ RawImportFinished on_finished,
+ void *userdata) {
+
+ _cleanup_(raw_import_unrefp) RawImport *i = NULL;
+ _cleanup_free_ char *root = NULL;
+ int r;
+
+ assert(ret);
+
+ root = strdup(image_root ?: "/var/lib/machines");
+ if (!root)
+ return -ENOMEM;
+
+ i = new(RawImport, 1);
+ if (!i)
+ return -ENOMEM;
+
+ *i = (RawImport) {
+ .input_fd = -1,
+ .output_fd = -1,
+ .on_finished = on_finished,
+ .userdata = userdata,
+ .last_percent = (unsigned) -1,
+ .image_root = TAKE_PTR(root),
+ };
+
+ RATELIMIT_INIT(i->progress_rate_limit, 100 * USEC_PER_MSEC, 1);
+
+ if (event)
+ i->event = sd_event_ref(event);
+ else {
+ r = sd_event_default(&i->event);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(i);
+
+ return 0;
+}
+
+static void raw_import_report_progress(RawImport *i) {
+ unsigned percent;
+ assert(i);
+
+ /* We have no size information, unless the source is a regular file */
+ if (!S_ISREG(i->st.st_mode))
+ return;
+
+ if (i->written_compressed >= (uint64_t) i->st.st_size)
+ percent = 100;
+ else
+ percent = (unsigned) ((i->written_compressed * UINT64_C(100)) / (uint64_t) i->st.st_size);
+
+ if (percent == i->last_percent)
+ return;
+
+ if (!ratelimit_below(&i->progress_rate_limit))
+ return;
+
+ sd_notifyf(false, "X_IMPORT_PROGRESS=%u", percent);
+ log_info("Imported %u%%.", percent);
+
+ i->last_percent = percent;
+}
+
+static int raw_import_maybe_convert_qcow2(RawImport *i) {
+ _cleanup_close_ int converted_fd = -1;
+ _cleanup_free_ char *t = NULL;
+ int r;
+
+ assert(i);
+
+ r = qcow2_detect(i->output_fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to detect whether this is a QCOW2 image: %m");
+ if (r == 0)
+ return 0;
+
+ /* This is a QCOW2 image, let's convert it */
+ r = tempfn_random(i->final_path, NULL, &t);
+ if (r < 0)
+ return log_oom();
+
+ converted_fd = open(t, O_RDWR|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0664);
+ if (converted_fd < 0)
+ return log_error_errno(errno, "Failed to create %s: %m", t);
+
+ r = chattr_fd(converted_fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set file attributes on %s: %m", t);
+
+ log_info("Unpacking QCOW2 file.");
+
+ r = qcow2_convert(i->output_fd, converted_fd);
+ if (r < 0) {
+ unlink(t);
+ return log_error_errno(r, "Failed to convert qcow2 image: %m");
+ }
+
+ (void) unlink(i->temp_path);
+ free_and_replace(i->temp_path, t);
+
+ safe_close(i->output_fd);
+ i->output_fd = TAKE_FD(converted_fd);
+
+ return 1;
+}
+
+static int raw_import_finish(RawImport *i) {
+ int r;
+
+ assert(i);
+ assert(i->output_fd >= 0);
+ assert(i->temp_path);
+ assert(i->final_path);
+
+ /* In case this was a sparse file, make sure the file system is right */
+ if (i->written_uncompressed > 0) {
+ if (ftruncate(i->output_fd, i->written_uncompressed) < 0)
+ return log_error_errno(errno, "Failed to truncate file: %m");
+ }
+
+ r = raw_import_maybe_convert_qcow2(i);
+ if (r < 0)
+ return r;
+
+ if (S_ISREG(i->st.st_mode)) {
+ (void) copy_times(i->input_fd, i->output_fd);
+ (void) copy_xattr(i->input_fd, i->output_fd);
+ }
+
+ if (i->read_only) {
+ r = import_make_read_only_fd(i->output_fd);
+ if (r < 0)
+ return r;
+ }
+
+ if (i->force_local)
+ (void) rm_rf(i->final_path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+
+ r = rename_noreplace(AT_FDCWD, i->temp_path, AT_FDCWD, i->final_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to move image into place: %m");
+
+ i->temp_path = mfree(i->temp_path);
+
+ return 0;
+}
+
+static int raw_import_open_disk(RawImport *i) {
+ int r;
+
+ assert(i);
+
+ assert(!i->final_path);
+ assert(!i->temp_path);
+ assert(i->output_fd < 0);
+
+ i->final_path = strjoin(i->image_root, "/", i->local, ".raw");
+ if (!i->final_path)
+ return log_oom();
+
+ r = tempfn_random(i->final_path, NULL, &i->temp_path);
+ if (r < 0)
+ return log_oom();
+
+ (void) mkdir_parents_label(i->temp_path, 0700);
+
+ i->output_fd = open(i->temp_path, O_RDWR|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0664);
+ if (i->output_fd < 0)
+ return log_error_errno(errno, "Failed to open destination %s: %m", i->temp_path);
+
+ r = chattr_fd(i->output_fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set file attributes on %s: %m", i->temp_path);
+
+ return 0;
+}
+
+static int raw_import_try_reflink(RawImport *i) {
+ off_t p;
+ int r;
+
+ assert(i);
+ assert(i->input_fd >= 0);
+ assert(i->output_fd >= 0);
+
+ if (i->compress.type != IMPORT_COMPRESS_UNCOMPRESSED)
+ return 0;
+
+ if (!S_ISREG(i->st.st_mode))
+ return 0;
+
+ p = lseek(i->input_fd, 0, SEEK_CUR);
+ if (p == (off_t) -1)
+ return log_error_errno(errno, "Failed to read file offset of input file: %m");
+
+ /* Let's only try a btrfs reflink, if we are reading from the beginning of the file */
+ if ((uint64_t) p != (uint64_t) i->buffer_size)
+ return 0;
+
+ r = btrfs_reflink(i->input_fd, i->output_fd);
+ if (r >= 0)
+ return 1;
+
+ return 0;
+}
+
+static int raw_import_write(const void *p, size_t sz, void *userdata) {
+ RawImport *i = userdata;
+ ssize_t n;
+
+ n = sparse_write(i->output_fd, p, sz, 64);
+ if (n < 0)
+ return (int) n;
+ if ((size_t) n < sz)
+ return -EIO;
+
+ i->written_uncompressed += sz;
+
+ return 0;
+}
+
+static int raw_import_process(RawImport *i) {
+ ssize_t l;
+ int r;
+
+ assert(i);
+ assert(i->buffer_size < sizeof(i->buffer));
+
+ l = read(i->input_fd, i->buffer + i->buffer_size, sizeof(i->buffer) - i->buffer_size);
+ if (l < 0) {
+ if (errno == EAGAIN)
+ return 0;
+
+ r = log_error_errno(errno, "Failed to read input file: %m");
+ goto finish;
+ }
+ if (l == 0) {
+ if (i->compress.type == IMPORT_COMPRESS_UNKNOWN) {
+ log_error("Premature end of file.");
+ r = -EIO;
+ goto finish;
+ }
+
+ r = raw_import_finish(i);
+ goto finish;
+ }
+
+ i->buffer_size += l;
+
+ if (i->compress.type == IMPORT_COMPRESS_UNKNOWN) {
+ r = import_uncompress_detect(&i->compress, i->buffer, i->buffer_size);
+ if (r < 0) {
+ log_error_errno(r, "Failed to detect file compression: %m");
+ goto finish;
+ }
+ if (r == 0) /* Need more data */
+ return 0;
+
+ r = raw_import_open_disk(i);
+ if (r < 0)
+ goto finish;
+
+ r = raw_import_try_reflink(i);
+ if (r < 0)
+ goto finish;
+ if (r > 0) {
+ r = raw_import_finish(i);
+ goto finish;
+ }
+ }
+
+ r = import_uncompress(&i->compress, i->buffer, i->buffer_size, raw_import_write, i);
+ if (r < 0) {
+ log_error_errno(r, "Failed to decode and write: %m");
+ goto finish;
+ }
+
+ i->written_compressed += i->buffer_size;
+ i->buffer_size = 0;
+
+ raw_import_report_progress(i);
+
+ return 0;
+
+finish:
+ if (i->on_finished)
+ i->on_finished(i, r, i->userdata);
+ else
+ sd_event_exit(i->event, r);
+
+ return 0;
+}
+
+static int raw_import_on_input(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ RawImport *i = userdata;
+
+ return raw_import_process(i);
+}
+
+static int raw_import_on_defer(sd_event_source *s, void *userdata) {
+ RawImport *i = userdata;
+
+ return raw_import_process(i);
+}
+
+int raw_import_start(RawImport *i, int fd, const char *local, bool force_local, bool read_only) {
+ int r;
+
+ assert(i);
+ assert(fd >= 0);
+ assert(local);
+
+ if (!machine_name_is_valid(local))
+ return -EINVAL;
+
+ if (i->input_fd >= 0)
+ return -EBUSY;
+
+ r = fd_nonblock(fd, true);
+ if (r < 0)
+ return r;
+
+ r = free_and_strdup(&i->local, local);
+ if (r < 0)
+ return r;
+ i->force_local = force_local;
+ i->read_only = read_only;
+
+ if (fstat(fd, &i->st) < 0)
+ return -errno;
+
+ r = sd_event_add_io(i->event, &i->input_event_source, fd, EPOLLIN, raw_import_on_input, i);
+ if (r == -EPERM) {
+ /* This fd does not support epoll, for example because it is a regular file. Busy read in that case */
+ r = sd_event_add_defer(i->event, &i->input_event_source, raw_import_on_defer, i);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_enabled(i->input_event_source, SD_EVENT_ON);
+ }
+ if (r < 0)
+ return r;
+
+ i->input_fd = fd;
+ return r;
+}
diff --git a/src/import/import-raw.h b/src/import/import-raw.h
new file mode 100644
index 0000000..de4c3ea
--- /dev/null
+++ b/src/import/import-raw.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-event.h"
+
+#include "import-util.h"
+#include "macro.h"
+
+typedef struct RawImport RawImport;
+
+typedef void (*RawImportFinished)(RawImport *import, int error, void *userdata);
+
+int raw_import_new(RawImport **import, sd_event *event, const char *image_root, RawImportFinished on_finished, void *userdata);
+RawImport* raw_import_unref(RawImport *import);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(RawImport*, raw_import_unref);
+
+int raw_import_start(RawImport *i, int fd, const char *local, bool force_local, bool read_only);
diff --git a/src/import/import-tar.c b/src/import/import-tar.c
new file mode 100644
index 0000000..4d0d925
--- /dev/null
+++ b/src/import/import-tar.c
@@ -0,0 +1,369 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <linux/fs.h>
+
+#include "sd-daemon.h"
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "copy.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "hostname-util.h"
+#include "import-common.h"
+#include "import-compress.h"
+#include "import-tar.h"
+#include "io-util.h"
+#include "machine-pool.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "qcow2-util.h"
+#include "ratelimit.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tmpfile-util.h"
+#include "util.h"
+
+struct TarImport {
+ sd_event *event;
+
+ char *image_root;
+
+ TarImportFinished on_finished;
+ void *userdata;
+
+ char *local;
+ bool force_local;
+ bool read_only;
+
+ char *temp_path;
+ char *final_path;
+
+ int input_fd;
+ int tar_fd;
+
+ ImportCompress compress;
+
+ sd_event_source *input_event_source;
+
+ uint8_t buffer[16*1024];
+ size_t buffer_size;
+
+ uint64_t written_compressed;
+ uint64_t written_uncompressed;
+
+ struct stat st;
+
+ pid_t tar_pid;
+
+ unsigned last_percent;
+ RateLimit progress_rate_limit;
+};
+
+TarImport* tar_import_unref(TarImport *i) {
+ if (!i)
+ return NULL;
+
+ sd_event_source_unref(i->input_event_source);
+
+ if (i->tar_pid > 1) {
+ (void) kill_and_sigcont(i->tar_pid, SIGKILL);
+ (void) wait_for_terminate(i->tar_pid, NULL);
+ }
+
+ if (i->temp_path) {
+ (void) rm_rf(i->temp_path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+ free(i->temp_path);
+ }
+
+ import_compress_free(&i->compress);
+
+ sd_event_unref(i->event);
+
+ safe_close(i->tar_fd);
+
+ free(i->final_path);
+ free(i->image_root);
+ free(i->local);
+ return mfree(i);
+}
+
+int tar_import_new(
+ TarImport **ret,
+ sd_event *event,
+ const char *image_root,
+ TarImportFinished on_finished,
+ void *userdata) {
+
+ _cleanup_(tar_import_unrefp) TarImport *i = NULL;
+ _cleanup_free_ char *root = NULL;
+ int r;
+
+ assert(ret);
+
+ root = strdup(image_root ?: "/var/lib/machines");
+ if (!root)
+ return -ENOMEM;
+
+ i = new(TarImport, 1);
+ if (!i)
+ return -ENOMEM;
+
+ *i = (TarImport) {
+ .input_fd = -1,
+ .tar_fd = -1,
+ .on_finished = on_finished,
+ .userdata = userdata,
+ .last_percent = (unsigned) -1,
+ .image_root = TAKE_PTR(root),
+ };
+
+ RATELIMIT_INIT(i->progress_rate_limit, 100 * USEC_PER_MSEC, 1);
+
+ if (event)
+ i->event = sd_event_ref(event);
+ else {
+ r = sd_event_default(&i->event);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(i);
+
+ return 0;
+}
+
+static void tar_import_report_progress(TarImport *i) {
+ unsigned percent;
+ assert(i);
+
+ /* We have no size information, unless the source is a regular file */
+ if (!S_ISREG(i->st.st_mode))
+ return;
+
+ if (i->written_compressed >= (uint64_t) i->st.st_size)
+ percent = 100;
+ else
+ percent = (unsigned) ((i->written_compressed * UINT64_C(100)) / (uint64_t) i->st.st_size);
+
+ if (percent == i->last_percent)
+ return;
+
+ if (!ratelimit_below(&i->progress_rate_limit))
+ return;
+
+ sd_notifyf(false, "X_IMPORT_PROGRESS=%u", percent);
+ log_info("Imported %u%%.", percent);
+
+ i->last_percent = percent;
+}
+
+static int tar_import_finish(TarImport *i) {
+ int r;
+
+ assert(i);
+ assert(i->tar_fd >= 0);
+ assert(i->temp_path);
+ assert(i->final_path);
+
+ i->tar_fd = safe_close(i->tar_fd);
+
+ if (i->tar_pid > 0) {
+ r = wait_for_terminate_and_check("tar", i->tar_pid, WAIT_LOG);
+ i->tar_pid = 0;
+ if (r < 0)
+ return r;
+ if (r != EXIT_SUCCESS)
+ return -EPROTO;
+ }
+
+ r = import_mangle_os_tree(i->temp_path);
+ if (r < 0)
+ return r;
+
+ if (i->read_only) {
+ r = import_make_read_only(i->temp_path);
+ if (r < 0)
+ return r;
+ }
+
+ if (i->force_local)
+ (void) rm_rf(i->final_path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+
+ r = rename_noreplace(AT_FDCWD, i->temp_path, AT_FDCWD, i->final_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to move image into place: %m");
+
+ i->temp_path = mfree(i->temp_path);
+
+ return 0;
+}
+
+static int tar_import_fork_tar(TarImport *i) {
+ int r;
+
+ assert(i);
+
+ assert(!i->final_path);
+ assert(!i->temp_path);
+ assert(i->tar_fd < 0);
+
+ i->final_path = strjoin(i->image_root, "/", i->local);
+ if (!i->final_path)
+ return log_oom();
+
+ r = tempfn_random(i->final_path, NULL, &i->temp_path);
+ if (r < 0)
+ return log_oom();
+
+ (void) mkdir_parents_label(i->temp_path, 0700);
+
+ r = btrfs_subvol_make(i->temp_path);
+ if (r == -ENOTTY) {
+ if (mkdir(i->temp_path, 0755) < 0)
+ return log_error_errno(errno, "Failed to create directory %s: %m", i->temp_path);
+ } else if (r < 0)
+ return log_error_errno(r, "Failed to create subvolume %s: %m", i->temp_path);
+ else
+ (void) import_assign_pool_quota_and_warn(i->temp_path);
+
+ i->tar_fd = import_fork_tar_x(i->temp_path, &i->tar_pid);
+ if (i->tar_fd < 0)
+ return i->tar_fd;
+
+ return 0;
+}
+
+static int tar_import_write(const void *p, size_t sz, void *userdata) {
+ TarImport *i = userdata;
+ int r;
+
+ r = loop_write(i->tar_fd, p, sz, false);
+ if (r < 0)
+ return r;
+
+ i->written_uncompressed += sz;
+
+ return 0;
+}
+
+static int tar_import_process(TarImport *i) {
+ ssize_t l;
+ int r;
+
+ assert(i);
+ assert(i->buffer_size < sizeof(i->buffer));
+
+ l = read(i->input_fd, i->buffer + i->buffer_size, sizeof(i->buffer) - i->buffer_size);
+ if (l < 0) {
+ if (errno == EAGAIN)
+ return 0;
+
+ r = log_error_errno(errno, "Failed to read input file: %m");
+ goto finish;
+ }
+ if (l == 0) {
+ if (i->compress.type == IMPORT_COMPRESS_UNKNOWN) {
+ log_error("Premature end of file.");
+ r = -EIO;
+ goto finish;
+ }
+
+ r = tar_import_finish(i);
+ goto finish;
+ }
+
+ i->buffer_size += l;
+
+ if (i->compress.type == IMPORT_COMPRESS_UNKNOWN) {
+ r = import_uncompress_detect(&i->compress, i->buffer, i->buffer_size);
+ if (r < 0) {
+ log_error_errno(r, "Failed to detect file compression: %m");
+ goto finish;
+ }
+ if (r == 0) /* Need more data */
+ return 0;
+
+ r = tar_import_fork_tar(i);
+ if (r < 0)
+ goto finish;
+ }
+
+ r = import_uncompress(&i->compress, i->buffer, i->buffer_size, tar_import_write, i);
+ if (r < 0) {
+ log_error_errno(r, "Failed to decode and write: %m");
+ goto finish;
+ }
+
+ i->written_compressed += i->buffer_size;
+ i->buffer_size = 0;
+
+ tar_import_report_progress(i);
+
+ return 0;
+
+finish:
+ if (i->on_finished)
+ i->on_finished(i, r, i->userdata);
+ else
+ sd_event_exit(i->event, r);
+
+ return 0;
+}
+
+static int tar_import_on_input(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ TarImport *i = userdata;
+
+ return tar_import_process(i);
+}
+
+static int tar_import_on_defer(sd_event_source *s, void *userdata) {
+ TarImport *i = userdata;
+
+ return tar_import_process(i);
+}
+
+int tar_import_start(TarImport *i, int fd, const char *local, bool force_local, bool read_only) {
+ int r;
+
+ assert(i);
+ assert(fd >= 0);
+ assert(local);
+
+ if (!machine_name_is_valid(local))
+ return -EINVAL;
+
+ if (i->input_fd >= 0)
+ return -EBUSY;
+
+ r = fd_nonblock(fd, true);
+ if (r < 0)
+ return r;
+
+ r = free_and_strdup(&i->local, local);
+ if (r < 0)
+ return r;
+ i->force_local = force_local;
+ i->read_only = read_only;
+
+ if (fstat(fd, &i->st) < 0)
+ return -errno;
+
+ r = sd_event_add_io(i->event, &i->input_event_source, fd, EPOLLIN, tar_import_on_input, i);
+ if (r == -EPERM) {
+ /* This fd does not support epoll, for example because it is a regular file. Busy read in that case */
+ r = sd_event_add_defer(i->event, &i->input_event_source, tar_import_on_defer, i);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_enabled(i->input_event_source, SD_EVENT_ON);
+ }
+ if (r < 0)
+ return r;
+
+ i->input_fd = fd;
+ return r;
+}
diff --git a/src/import/import-tar.h b/src/import/import-tar.h
new file mode 100644
index 0000000..347f522
--- /dev/null
+++ b/src/import/import-tar.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-event.h"
+
+#include "import-util.h"
+#include "macro.h"
+
+typedef struct TarImport TarImport;
+
+typedef void (*TarImportFinished)(TarImport *import, int error, void *userdata);
+
+int tar_import_new(TarImport **import, sd_event *event, const char *image_root, TarImportFinished on_finished, void *userdata);
+TarImport* tar_import_unref(TarImport *import);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(TarImport*, tar_import_unref);
+
+int tar_import_start(TarImport *import, int fd, const char *local, bool force_local, bool read_only);
diff --git a/src/import/import.c b/src/import/import.c
new file mode 100644
index 0000000..f34244a
--- /dev/null
+++ b/src/import/import.c
@@ -0,0 +1,322 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <getopt.h>
+
+#include "sd-event.h"
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "hostname-util.h"
+#include "import-raw.h"
+#include "import-tar.h"
+#include "import-util.h"
+#include "machine-image.h"
+#include "main-func.h"
+#include "signal-util.h"
+#include "string-util.h"
+#include "verbs.h"
+
+static bool arg_force = false;
+static bool arg_read_only = false;
+static const char *arg_image_root = "/var/lib/machines";
+
+static int interrupt_signal_handler(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ log_notice("Transfer aborted.");
+ sd_event_exit(sd_event_source_get_event(s), EINTR);
+ return 0;
+}
+
+static void on_tar_finished(TarImport *import, int error, void *userdata) {
+ sd_event *event = userdata;
+ assert(import);
+
+ if (error == 0)
+ log_info("Operation completed successfully.");
+
+ sd_event_exit(event, abs(error));
+}
+
+static int import_tar(int argc, char *argv[], void *userdata) {
+ _cleanup_(tar_import_unrefp) TarImport *import = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ const char *path = NULL, *local = NULL;
+ _cleanup_free_ char *ll = NULL;
+ _cleanup_close_ int open_fd = -1;
+ int r, fd;
+
+ if (argc >= 2)
+ path = argv[1];
+ if (isempty(path) || streq(path, "-"))
+ path = NULL;
+
+ if (argc >= 3)
+ local = argv[2];
+ else if (path)
+ local = basename(path);
+ if (isempty(local) || streq(local, "-"))
+ local = NULL;
+
+ if (local) {
+ r = tar_strip_suffixes(local, &ll);
+ if (r < 0)
+ return log_oom();
+
+ local = ll;
+
+ if (!machine_name_is_valid(local)) {
+ log_error("Local image name '%s' is not valid.", local);
+ return -EINVAL;
+ }
+
+ if (!arg_force) {
+ r = image_find(IMAGE_MACHINE, local, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
+ } else {
+ log_error("Image '%s' already exists.", local);
+ return -EEXIST;
+ }
+ }
+ } else
+ local = "imported";
+
+ if (path) {
+ open_fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (open_fd < 0)
+ return log_error_errno(errno, "Failed to open tar image to import: %m");
+
+ fd = open_fd;
+
+ log_info("Importing '%s', saving as '%s'.", path, local);
+ } else {
+ _cleanup_free_ char *pretty = NULL;
+
+ fd = STDIN_FILENO;
+
+ (void) fd_get_path(fd, &pretty);
+ log_info("Importing '%s', saving as '%s'.", strna(pretty), local);
+ }
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+ (void) sd_event_add_signal(event, NULL, SIGTERM, interrupt_signal_handler, NULL);
+ (void) sd_event_add_signal(event, NULL, SIGINT, interrupt_signal_handler, NULL);
+
+ r = tar_import_new(&import, event, arg_image_root, on_tar_finished, event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate importer: %m");
+
+ r = tar_import_start(import, fd, local, arg_force, arg_read_only);
+ if (r < 0)
+ return log_error_errno(r, "Failed to import image: %m");
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ log_info("Exiting.");
+ return -r;
+}
+
+static void on_raw_finished(RawImport *import, int error, void *userdata) {
+ sd_event *event = userdata;
+ assert(import);
+
+ if (error == 0)
+ log_info("Operation completed successfully.");
+
+ sd_event_exit(event, abs(error));
+}
+
+static int import_raw(int argc, char *argv[], void *userdata) {
+ _cleanup_(raw_import_unrefp) RawImport *import = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ const char *path = NULL, *local = NULL;
+ _cleanup_free_ char *ll = NULL;
+ _cleanup_close_ int open_fd = -1;
+ int r, fd;
+
+ if (argc >= 2)
+ path = argv[1];
+ if (isempty(path) || streq(path, "-"))
+ path = NULL;
+
+ if (argc >= 3)
+ local = argv[2];
+ else if (path)
+ local = basename(path);
+ if (isempty(local) || streq(local, "-"))
+ local = NULL;
+
+ if (local) {
+ r = raw_strip_suffixes(local, &ll);
+ if (r < 0)
+ return log_oom();
+
+ local = ll;
+
+ if (!machine_name_is_valid(local)) {
+ log_error("Local image name '%s' is not valid.", local);
+ return -EINVAL;
+ }
+
+ if (!arg_force) {
+ r = image_find(IMAGE_MACHINE, local, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
+ } else {
+ log_error("Image '%s' already exists.", local);
+ return -EEXIST;
+ }
+ }
+ } else
+ local = "imported";
+
+ if (path) {
+ open_fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (open_fd < 0)
+ return log_error_errno(errno, "Failed to open raw image to import: %m");
+
+ fd = open_fd;
+
+ log_info("Importing '%s', saving as '%s'.", path, local);
+ } else {
+ _cleanup_free_ char *pretty = NULL;
+
+ fd = STDIN_FILENO;
+
+ (void) fd_get_path(fd, &pretty);
+ log_info("Importing '%s', saving as '%s'.", strempty(pretty), local);
+ }
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+ (void) sd_event_add_signal(event, NULL, SIGTERM, interrupt_signal_handler, NULL);
+ (void) sd_event_add_signal(event, NULL, SIGINT, interrupt_signal_handler, NULL);
+
+ r = raw_import_new(&import, event, arg_image_root, on_raw_finished, event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate importer: %m");
+
+ r = raw_import_start(import, fd, local, arg_force, arg_read_only);
+ if (r < 0)
+ return log_error_errno(r, "Failed to import image: %m");
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ log_info("Exiting.");
+ return -r;
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+
+ printf("%s [OPTIONS...] {COMMAND} ...\n\n"
+ "Import container or virtual machine images.\n\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --force Force creation of image\n"
+ " --image-root=PATH Image root directory\n"
+ " --read-only Create a read-only image\n\n"
+ "Commands:\n"
+ " tar FILE [NAME] Import a TAR image\n"
+ " raw FILE [NAME] Import a RAW image\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_FORCE,
+ ARG_IMAGE_ROOT,
+ ARG_READ_ONLY,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "force", no_argument, NULL, ARG_FORCE },
+ { "image-root", required_argument, NULL, ARG_IMAGE_ROOT },
+ { "read-only", no_argument, NULL, ARG_READ_ONLY },
+ {}
+ };
+
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ return help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_FORCE:
+ arg_force = true;
+ break;
+
+ case ARG_IMAGE_ROOT:
+ arg_image_root = optarg;
+ break;
+
+ case ARG_READ_ONLY:
+ arg_read_only = true;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+
+ return 1;
+}
+
+static int import_main(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "tar", 2, 3, 0, import_tar },
+ { "raw", 2, 3, 0, import_raw },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ setlocale(LC_ALL, "");
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return 0;
+
+ (void) ignore_signals(SIGPIPE, -1);
+
+ return import_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/import/importd.c b/src/import/importd.c
new file mode 100644
index 0000000..2426933
--- /dev/null
+++ b/src/import/importd.c
@@ -0,0 +1,1266 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <sys/prctl.h>
+#include <sys/wait.h>
+
+#include "sd-bus.h"
+
+#include "alloc-util.h"
+#include "bus-common-errors.h"
+#include "bus-util.h"
+#include "def.h"
+#include "fd-util.h"
+#include "float.h"
+#include "hostname-util.h"
+#include "import-util.h"
+#include "machine-pool.h"
+#include "main-func.h"
+#include "missing.h"
+#include "mkdir.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "signal-util.h"
+#include "socket-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "syslog-util.h"
+#include "user-util.h"
+#include "util.h"
+#include "web-util.h"
+
+typedef struct Transfer Transfer;
+typedef struct Manager Manager;
+
+typedef enum TransferType {
+ TRANSFER_IMPORT_TAR,
+ TRANSFER_IMPORT_RAW,
+ TRANSFER_IMPORT_FS,
+ TRANSFER_EXPORT_TAR,
+ TRANSFER_EXPORT_RAW,
+ TRANSFER_PULL_TAR,
+ TRANSFER_PULL_RAW,
+ _TRANSFER_TYPE_MAX,
+ _TRANSFER_TYPE_INVALID = -1,
+} TransferType;
+
+struct Transfer {
+ Manager *manager;
+
+ uint32_t id;
+ char *object_path;
+
+ TransferType type;
+ ImportVerify verify;
+
+ char *remote;
+ char *local;
+ bool force_local;
+ bool read_only;
+
+ char *format;
+
+ pid_t pid;
+
+ int log_fd;
+
+ char log_message[LINE_MAX];
+ size_t log_message_size;
+
+ sd_event_source *pid_event_source;
+ sd_event_source *log_event_source;
+
+ unsigned n_canceled;
+ unsigned progress_percent;
+
+ int stdin_fd;
+ int stdout_fd;
+};
+
+struct Manager {
+ sd_event *event;
+ sd_bus *bus;
+
+ uint32_t current_transfer_id;
+ Hashmap *transfers;
+
+ Hashmap *polkit_registry;
+
+ int notify_fd;
+
+ sd_event_source *notify_event_source;
+};
+
+#define TRANSFERS_MAX 64
+
+static const char* const transfer_type_table[_TRANSFER_TYPE_MAX] = {
+ [TRANSFER_IMPORT_TAR] = "import-tar",
+ [TRANSFER_IMPORT_RAW] = "import-raw",
+ [TRANSFER_IMPORT_FS] = "import-fs",
+ [TRANSFER_EXPORT_TAR] = "export-tar",
+ [TRANSFER_EXPORT_RAW] = "export-raw",
+ [TRANSFER_PULL_TAR] = "pull-tar",
+ [TRANSFER_PULL_RAW] = "pull-raw",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(transfer_type, TransferType);
+
+static Transfer *transfer_unref(Transfer *t) {
+ if (!t)
+ return NULL;
+
+ if (t->manager)
+ hashmap_remove(t->manager->transfers, UINT32_TO_PTR(t->id));
+
+ sd_event_source_unref(t->pid_event_source);
+ sd_event_source_unref(t->log_event_source);
+
+ free(t->remote);
+ free(t->local);
+ free(t->format);
+ free(t->object_path);
+
+ if (t->pid > 0) {
+ (void) kill_and_sigcont(t->pid, SIGKILL);
+ (void) wait_for_terminate(t->pid, NULL);
+ }
+
+ safe_close(t->log_fd);
+ safe_close(t->stdin_fd);
+ safe_close(t->stdout_fd);
+
+ return mfree(t);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Transfer*, transfer_unref);
+
+static int transfer_new(Manager *m, Transfer **ret) {
+ _cleanup_(transfer_unrefp) Transfer *t = NULL;
+ uint32_t id;
+ int r;
+
+ assert(m);
+ assert(ret);
+
+ if (hashmap_size(m->transfers) >= TRANSFERS_MAX)
+ return -E2BIG;
+
+ r = hashmap_ensure_allocated(&m->transfers, &trivial_hash_ops);
+ if (r < 0)
+ return r;
+
+ t = new(Transfer, 1);
+ if (!t)
+ return -ENOMEM;
+
+ *t = (Transfer) {
+ .type = _TRANSFER_TYPE_INVALID,
+ .log_fd = -1,
+ .stdin_fd = -1,
+ .stdout_fd = -1,
+ .verify = _IMPORT_VERIFY_INVALID,
+ .progress_percent= (unsigned) -1,
+ };
+
+ id = m->current_transfer_id + 1;
+
+ if (asprintf(&t->object_path, "/org/freedesktop/import1/transfer/_%" PRIu32, id) < 0)
+ return -ENOMEM;
+
+ r = hashmap_put(m->transfers, UINT32_TO_PTR(id), t);
+ if (r < 0)
+ return r;
+
+ m->current_transfer_id = id;
+
+ t->manager = m;
+ t->id = id;
+
+ *ret = TAKE_PTR(t);
+
+ return 0;
+}
+
+static double transfer_percent_as_double(Transfer *t) {
+ assert(t);
+
+ if (t->progress_percent == (unsigned) -1)
+ return -DBL_MAX;
+
+ return (double) t->progress_percent / 100.0;
+}
+
+static void transfer_send_log_line(Transfer *t, const char *line) {
+ int r, priority = LOG_INFO;
+
+ assert(t);
+ assert(line);
+
+ syslog_parse_priority(&line, &priority, true);
+
+ log_full(priority, "(transfer%" PRIu32 ") %s", t->id, line);
+
+ r = sd_bus_emit_signal(
+ t->manager->bus,
+ t->object_path,
+ "org.freedesktop.import1.Transfer",
+ "LogMessage",
+ "us",
+ priority,
+ line);
+ if (r < 0)
+ log_warning_errno(r, "Cannot emit log message signal, ignoring: %m");
+ }
+
+static void transfer_send_logs(Transfer *t, bool flush) {
+ assert(t);
+
+ /* Try to send out all log messages, if we can. But if we
+ * can't we remove the messages from the buffer, but don't
+ * fail */
+
+ while (t->log_message_size > 0) {
+ _cleanup_free_ char *n = NULL;
+ char *e;
+
+ if (t->log_message_size >= sizeof(t->log_message))
+ e = t->log_message + sizeof(t->log_message);
+ else {
+ char *a, *b;
+
+ a = memchr(t->log_message, 0, t->log_message_size);
+ b = memchr(t->log_message, '\n', t->log_message_size);
+
+ if (a && b)
+ e = a < b ? a : b;
+ else if (a)
+ e = a;
+ else
+ e = b;
+ }
+
+ if (!e) {
+ if (!flush)
+ return;
+
+ e = t->log_message + t->log_message_size;
+ }
+
+ n = strndup(t->log_message, e - t->log_message);
+
+ /* Skip over NUL and newlines */
+ while (e < t->log_message + t->log_message_size && (*e == 0 || *e == '\n'))
+ e++;
+
+ memmove(t->log_message, e, t->log_message + sizeof(t->log_message) - e);
+ t->log_message_size -= e - t->log_message;
+
+ if (!n) {
+ log_oom();
+ continue;
+ }
+
+ if (isempty(n))
+ continue;
+
+ transfer_send_log_line(t, n);
+ }
+}
+
+static int transfer_finalize(Transfer *t, bool success) {
+ int r;
+
+ assert(t);
+
+ transfer_send_logs(t, true);
+
+ r = sd_bus_emit_signal(
+ t->manager->bus,
+ "/org/freedesktop/import1",
+ "org.freedesktop.import1.Manager",
+ "TransferRemoved",
+ "uos",
+ t->id,
+ t->object_path,
+ success ? "done" :
+ t->n_canceled > 0 ? "canceled" : "failed");
+
+ if (r < 0)
+ log_error_errno(r, "Cannot emit message: %m");
+
+ transfer_unref(t);
+ return 0;
+}
+
+static int transfer_cancel(Transfer *t) {
+ int r;
+
+ assert(t);
+
+ r = kill_and_sigcont(t->pid, t->n_canceled < 3 ? SIGTERM : SIGKILL);
+ if (r < 0)
+ return r;
+
+ t->n_canceled++;
+ return 0;
+}
+
+static int transfer_on_pid(sd_event_source *s, const siginfo_t *si, void *userdata) {
+ Transfer *t = userdata;
+ bool success = false;
+
+ assert(s);
+ assert(t);
+
+ if (si->si_code == CLD_EXITED) {
+ if (si->si_status != 0)
+ log_error("Transfer process failed with exit code %i.", si->si_status);
+ else {
+ log_debug("Transfer process succeeded.");
+ success = true;
+ }
+
+ } else if (IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED))
+ log_error("Transfer process terminated by signal %s.", signal_to_string(si->si_status));
+ else
+ log_error("Transfer process failed due to unknown reason.");
+
+ t->pid = 0;
+
+ return transfer_finalize(t, success);
+}
+
+static int transfer_on_log(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ Transfer *t = userdata;
+ ssize_t l;
+
+ assert(s);
+ assert(t);
+
+ l = read(fd, t->log_message + t->log_message_size, sizeof(t->log_message) - t->log_message_size);
+ if (l < 0)
+ log_error_errno(errno, "Failed to read log message: %m");
+ if (l <= 0) {
+ /* EOF/read error. We just close the pipe here, and
+ * close the watch, waiting for the SIGCHLD to arrive,
+ * before we do anything else. */
+ t->log_event_source = sd_event_source_unref(t->log_event_source);
+ return 0;
+ }
+
+ t->log_message_size += l;
+
+ transfer_send_logs(t, false);
+
+ return 0;
+}
+
+static int transfer_start(Transfer *t) {
+ _cleanup_close_pair_ int pipefd[2] = { -1, -1 };
+ int r;
+
+ assert(t);
+ assert(t->pid <= 0);
+
+ if (pipe2(pipefd, O_CLOEXEC) < 0)
+ return -errno;
+
+ r = safe_fork("(sd-transfer)", FORK_RESET_SIGNALS|FORK_DEATHSIG, &t->pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ const char *cmd[] = {
+ NULL, /* systemd-import, systemd-import-fs, systemd-export or systemd-pull */
+ NULL, /* tar, raw */
+ NULL, /* --verify= */
+ NULL, /* verify argument */
+ NULL, /* maybe --force */
+ NULL, /* maybe --read-only */
+ NULL, /* if so: the actual URL */
+ NULL, /* maybe --format= */
+ NULL, /* if so: the actual format */
+ NULL, /* remote */
+ NULL, /* local */
+ NULL
+ };
+ unsigned k = 0;
+
+ /* Child */
+
+ pipefd[0] = safe_close(pipefd[0]);
+
+ r = rearrange_stdio(t->stdin_fd,
+ t->stdout_fd < 0 ? pipefd[1] : t->stdout_fd,
+ pipefd[1]);
+ if (r < 0) {
+ log_error_errno(r, "Failed to set stdin/stdout/stderr: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ if (setenv("SYSTEMD_LOG_TARGET", "console-prefixed", 1) < 0 ||
+ setenv("NOTIFY_SOCKET", "/run/systemd/import/notify", 1) < 0) {
+ log_error_errno(errno, "setenv() failed: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ switch (t->type) {
+
+ case TRANSFER_IMPORT_TAR:
+ case TRANSFER_IMPORT_RAW:
+ cmd[k++] = SYSTEMD_IMPORT_PATH;
+ break;
+
+ case TRANSFER_IMPORT_FS:
+ cmd[k++] = SYSTEMD_IMPORT_FS_PATH;
+ break;
+
+ case TRANSFER_EXPORT_TAR:
+ case TRANSFER_EXPORT_RAW:
+ cmd[k++] = SYSTEMD_EXPORT_PATH;
+ break;
+
+ case TRANSFER_PULL_TAR:
+ case TRANSFER_PULL_RAW:
+ cmd[k++] = SYSTEMD_PULL_PATH;
+ break;
+
+ default:
+ assert_not_reached("Unexpected transfer type");
+ }
+
+ switch (t->type) {
+
+ case TRANSFER_IMPORT_TAR:
+ case TRANSFER_EXPORT_TAR:
+ case TRANSFER_PULL_TAR:
+ cmd[k++] = "tar";
+ break;
+
+ case TRANSFER_IMPORT_RAW:
+ case TRANSFER_EXPORT_RAW:
+ case TRANSFER_PULL_RAW:
+ cmd[k++] = "raw";
+ break;
+
+ case TRANSFER_IMPORT_FS:
+ cmd[k++] = "run";
+ break;
+
+ default:
+ break;
+ }
+
+ if (t->verify != _IMPORT_VERIFY_INVALID) {
+ cmd[k++] = "--verify";
+ cmd[k++] = import_verify_to_string(t->verify);
+ }
+
+ if (t->force_local)
+ cmd[k++] = "--force";
+ if (t->read_only)
+ cmd[k++] = "--read-only";
+
+ if (t->format) {
+ cmd[k++] = "--format";
+ cmd[k++] = t->format;
+ }
+
+ if (!IN_SET(t->type, TRANSFER_EXPORT_TAR, TRANSFER_EXPORT_RAW)) {
+ if (t->remote)
+ cmd[k++] = t->remote;
+ else
+ cmd[k++] = "-";
+ }
+
+ if (t->local)
+ cmd[k++] = t->local;
+ cmd[k] = NULL;
+
+ execv(cmd[0], (char * const *) cmd);
+ log_error_errno(errno, "Failed to execute %s tool: %m", cmd[0]);
+ _exit(EXIT_FAILURE);
+ }
+
+ pipefd[1] = safe_close(pipefd[1]);
+ t->log_fd = TAKE_FD(pipefd[0]);
+
+ t->stdin_fd = safe_close(t->stdin_fd);
+
+ r = sd_event_add_child(t->manager->event, &t->pid_event_source, t->pid, WEXITED, transfer_on_pid, t);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_io(t->manager->event, &t->log_event_source, t->log_fd, EPOLLIN, transfer_on_log, t);
+ if (r < 0)
+ return r;
+
+ /* Make sure always process logging before SIGCHLD */
+ r = sd_event_source_set_priority(t->log_event_source, SD_EVENT_PRIORITY_NORMAL -5);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_emit_signal(
+ t->manager->bus,
+ "/org/freedesktop/import1",
+ "org.freedesktop.import1.Manager",
+ "TransferNew",
+ "uo",
+ t->id,
+ t->object_path);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static Manager *manager_unref(Manager *m) {
+ Transfer *t;
+
+ if (!m)
+ return NULL;
+
+ sd_event_source_unref(m->notify_event_source);
+ safe_close(m->notify_fd);
+
+ while ((t = hashmap_first(m->transfers)))
+ transfer_unref(t);
+
+ hashmap_free(m->transfers);
+
+ bus_verify_polkit_async_registry_free(m->polkit_registry);
+
+ m->bus = sd_bus_flush_close_unref(m->bus);
+ sd_event_unref(m->event);
+
+ return mfree(m);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_unref);
+
+static int manager_on_notify(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+
+ char buf[NOTIFY_BUFFER_MAX+1];
+ struct iovec iovec = {
+ .iov_base = buf,
+ .iov_len = sizeof(buf)-1,
+ };
+ union {
+ struct cmsghdr cmsghdr;
+ uint8_t buf[CMSG_SPACE(sizeof(struct ucred)) +
+ CMSG_SPACE(sizeof(int) * NOTIFY_FD_MAX)];
+ } control = {};
+ struct msghdr msghdr = {
+ .msg_iov = &iovec,
+ .msg_iovlen = 1,
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ };
+ struct ucred *ucred = NULL;
+ Manager *m = userdata;
+ struct cmsghdr *cmsg;
+ char *p, *e;
+ Transfer *t;
+ Iterator i;
+ ssize_t n;
+ int r;
+
+ n = recvmsg(fd, &msghdr, MSG_DONTWAIT|MSG_CMSG_CLOEXEC);
+ if (n < 0) {
+ if (IN_SET(errno, EAGAIN, EINTR))
+ return 0;
+
+ return -errno;
+ }
+
+ cmsg_close_all(&msghdr);
+
+ CMSG_FOREACH(cmsg, &msghdr)
+ if (cmsg->cmsg_level == SOL_SOCKET &&
+ cmsg->cmsg_type == SCM_CREDENTIALS &&
+ cmsg->cmsg_len == CMSG_LEN(sizeof(struct ucred)))
+ ucred = (struct ucred*) CMSG_DATA(cmsg);
+
+ if (msghdr.msg_flags & MSG_TRUNC) {
+ log_warning("Got overly long notification datagram, ignoring.");
+ return 0;
+ }
+
+ if (!ucred || ucred->pid <= 0) {
+ log_warning("Got notification datagram lacking credential information, ignoring.");
+ return 0;
+ }
+
+ HASHMAP_FOREACH(t, m->transfers, i)
+ if (ucred->pid == t->pid)
+ break;
+
+ if (!t) {
+ log_warning("Got notification datagram from unexpected peer, ignoring.");
+ return 0;
+ }
+
+ buf[n] = 0;
+
+ p = startswith(buf, "X_IMPORT_PROGRESS=");
+ if (!p) {
+ p = strstr(buf, "\nX_IMPORT_PROGRESS=");
+ if (!p)
+ return 0;
+
+ p += 19;
+ }
+
+ e = strchrnul(p, '\n');
+ *e = 0;
+
+ r = parse_percent(p);
+ if (r < 0) {
+ log_warning("Got invalid percent value, ignoring.");
+ return 0;
+ }
+
+ t->progress_percent = (unsigned) r;
+
+ log_debug("Got percentage from client: %u%%", t->progress_percent);
+ return 0;
+}
+
+static int manager_new(Manager **ret) {
+ _cleanup_(manager_unrefp) Manager *m = NULL;
+ static const union sockaddr_union sa = {
+ .un.sun_family = AF_UNIX,
+ .un.sun_path = "/run/systemd/import/notify",
+ };
+ int r;
+
+ assert(ret);
+
+ m = new0(Manager, 1);
+ if (!m)
+ return -ENOMEM;
+
+ r = sd_event_default(&m->event);
+ if (r < 0)
+ return r;
+
+ sd_event_set_watchdog(m->event, true);
+
+ r = sd_bus_default_system(&m->bus);
+ if (r < 0)
+ return r;
+
+ m->notify_fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ if (m->notify_fd < 0)
+ return -errno;
+
+ (void) mkdir_parents_label(sa.un.sun_path, 0755);
+ (void) sockaddr_un_unlink(&sa.un);
+
+ if (bind(m->notify_fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0)
+ return -errno;
+
+ r = setsockopt_int(m->notify_fd, SOL_SOCKET, SO_PASSCRED, true);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_io(m->event, &m->notify_event_source, m->notify_fd, EPOLLIN, manager_on_notify, m);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(m);
+
+ return 0;
+}
+
+static Transfer *manager_find(Manager *m, TransferType type, const char *remote) {
+ Transfer *t;
+ Iterator i;
+
+ assert(m);
+ assert(type >= 0);
+ assert(type < _TRANSFER_TYPE_MAX);
+
+ HASHMAP_FOREACH(t, m->transfers, i)
+ if (t->type == type && streq_ptr(t->remote, remote))
+ return t;
+
+ return NULL;
+}
+
+static int method_import_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ _cleanup_(transfer_unrefp) Transfer *t = NULL;
+ int fd, force, read_only, r;
+ const char *local, *object;
+ Manager *m = userdata;
+ TransferType type;
+ uint32_t id;
+
+ assert(msg);
+ assert(m);
+
+ r = bus_verify_polkit_async(
+ msg,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.import1.import",
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = sd_bus_message_read(msg, "hsbb", &fd, &local, &force, &read_only);
+ if (r < 0)
+ return r;
+
+ r = fd_verify_regular(fd);
+ if (r < 0)
+ return r;
+
+ if (!machine_name_is_valid(local))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Local name %s is invalid", local);
+
+ r = setup_machine_directory(error);
+ if (r < 0)
+ return r;
+
+ type = streq_ptr(sd_bus_message_get_member(msg), "ImportTar") ? TRANSFER_IMPORT_TAR : TRANSFER_IMPORT_RAW;
+
+ r = transfer_new(m, &t);
+ if (r < 0)
+ return r;
+
+ t->type = type;
+ t->force_local = force;
+ t->read_only = read_only;
+
+ t->local = strdup(local);
+ if (!t->local)
+ return -ENOMEM;
+
+ t->stdin_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (t->stdin_fd < 0)
+ return -errno;
+
+ r = transfer_start(t);
+ if (r < 0)
+ return r;
+
+ object = t->object_path;
+ id = t->id;
+ t = NULL;
+
+ return sd_bus_reply_method_return(msg, "uo", id, object);
+}
+
+static int method_import_fs(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ _cleanup_(transfer_unrefp) Transfer *t = NULL;
+ int fd, force, read_only, r;
+ const char *local, *object;
+ Manager *m = userdata;
+ uint32_t id;
+
+ assert(msg);
+ assert(m);
+
+ r = bus_verify_polkit_async(
+ msg,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.import1.import",
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = sd_bus_message_read(msg, "hsbb", &fd, &local, &force, &read_only);
+ if (r < 0)
+ return r;
+
+ r = fd_verify_directory(fd);
+ if (r < 0)
+ return r;
+
+ if (!machine_name_is_valid(local))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Local name %s is invalid", local);
+
+ r = setup_machine_directory(error);
+ if (r < 0)
+ return r;
+
+ r = transfer_new(m, &t);
+ if (r < 0)
+ return r;
+
+ t->type = TRANSFER_IMPORT_FS;
+ t->force_local = force;
+ t->read_only = read_only;
+
+ t->local = strdup(local);
+ if (!t->local)
+ return -ENOMEM;
+
+ t->stdin_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (t->stdin_fd < 0)
+ return -errno;
+
+ r = transfer_start(t);
+ if (r < 0)
+ return r;
+
+ object = t->object_path;
+ id = t->id;
+ t = NULL;
+
+ return sd_bus_reply_method_return(msg, "uo", id, object);
+}
+
+static int method_export_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ _cleanup_(transfer_unrefp) Transfer *t = NULL;
+ int fd, r;
+ const char *local, *object, *format;
+ Manager *m = userdata;
+ TransferType type;
+ uint32_t id;
+
+ assert(msg);
+ assert(m);
+
+ r = bus_verify_polkit_async(
+ msg,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.import1.export",
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = sd_bus_message_read(msg, "shs", &local, &fd, &format);
+ if (r < 0)
+ return r;
+
+ if (!machine_name_is_valid(local))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Local name %s is invalid", local);
+
+ r = fd_verify_regular(fd);
+ if (r < 0)
+ return r;
+
+ type = streq_ptr(sd_bus_message_get_member(msg), "ExportTar") ? TRANSFER_EXPORT_TAR : TRANSFER_EXPORT_RAW;
+
+ r = transfer_new(m, &t);
+ if (r < 0)
+ return r;
+
+ t->type = type;
+
+ if (!isempty(format)) {
+ t->format = strdup(format);
+ if (!t->format)
+ return -ENOMEM;
+ }
+
+ t->local = strdup(local);
+ if (!t->local)
+ return -ENOMEM;
+
+ t->stdout_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (t->stdout_fd < 0)
+ return -errno;
+
+ r = transfer_start(t);
+ if (r < 0)
+ return r;
+
+ object = t->object_path;
+ id = t->id;
+ t = NULL;
+
+ return sd_bus_reply_method_return(msg, "uo", id, object);
+}
+
+static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ _cleanup_(transfer_unrefp) Transfer *t = NULL;
+ const char *remote, *local, *verify, *object;
+ Manager *m = userdata;
+ ImportVerify v;
+ TransferType type;
+ int force, r;
+ uint32_t id;
+
+ assert(msg);
+ assert(m);
+
+ r = bus_verify_polkit_async(
+ msg,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.import1.pull",
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = sd_bus_message_read(msg, "sssb", &remote, &local, &verify, &force);
+ if (r < 0)
+ return r;
+
+ if (!http_url_is_valid(remote))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "URL %s is invalid", remote);
+
+ if (isempty(local))
+ local = NULL;
+ else if (!machine_name_is_valid(local))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Local name %s is invalid", local);
+
+ if (isempty(verify))
+ v = IMPORT_VERIFY_SIGNATURE;
+ else
+ v = import_verify_from_string(verify);
+ if (v < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Unknown verification mode %s", verify);
+
+ r = setup_machine_directory(error);
+ if (r < 0)
+ return r;
+
+ type = streq_ptr(sd_bus_message_get_member(msg), "PullTar") ? TRANSFER_PULL_TAR : TRANSFER_PULL_RAW;
+
+ if (manager_find(m, type, remote))
+ return sd_bus_error_setf(error, BUS_ERROR_TRANSFER_IN_PROGRESS, "Transfer for %s already in progress.", remote);
+
+ r = transfer_new(m, &t);
+ if (r < 0)
+ return r;
+
+ t->type = type;
+ t->verify = v;
+ t->force_local = force;
+
+ t->remote = strdup(remote);
+ if (!t->remote)
+ return -ENOMEM;
+
+ if (local) {
+ t->local = strdup(local);
+ if (!t->local)
+ return -ENOMEM;
+ }
+
+ r = transfer_start(t);
+ if (r < 0)
+ return r;
+
+ object = t->object_path;
+ id = t->id;
+ t = NULL;
+
+ return sd_bus_reply_method_return(msg, "uo", id, object);
+}
+
+static int method_list_transfers(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ Manager *m = userdata;
+ Transfer *t;
+ Iterator i;
+ int r;
+
+ assert(msg);
+ assert(m);
+
+ r = sd_bus_message_new_method_return(msg, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "(usssdo)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(t, m->transfers, i) {
+
+ r = sd_bus_message_append(
+ reply,
+ "(usssdo)",
+ t->id,
+ transfer_type_to_string(t->type),
+ t->remote,
+ t->local,
+ transfer_percent_as_double(t),
+ t->object_path);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int method_cancel(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Transfer *t = userdata;
+ int r;
+
+ assert(msg);
+ assert(t);
+
+ r = bus_verify_polkit_async(
+ msg,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.import1.pull",
+ NULL,
+ false,
+ UID_INVALID,
+ &t->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = transfer_cancel(t);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(msg, NULL);
+}
+
+static int method_cancel_transfer(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Manager *m = userdata;
+ Transfer *t;
+ uint32_t id;
+ int r;
+
+ assert(msg);
+ assert(m);
+
+ r = bus_verify_polkit_async(
+ msg,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.import1.pull",
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = sd_bus_message_read(msg, "u", &id);
+ if (r < 0)
+ return r;
+ if (id <= 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid transfer id");
+
+ t = hashmap_get(m->transfers, UINT32_TO_PTR(id));
+ if (!t)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_TRANSFER, "No transfer by id %" PRIu32, id);
+
+ r = transfer_cancel(t);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(msg, NULL);
+}
+
+static int property_get_progress(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Transfer *t = userdata;
+
+ assert(bus);
+ assert(reply);
+ assert(t);
+
+ return sd_bus_message_append(reply, "d", transfer_percent_as_double(t));
+}
+
+static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_type, transfer_type, TransferType);
+static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_verify, import_verify, ImportVerify);
+
+static const sd_bus_vtable transfer_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_PROPERTY("Id", "u", NULL, offsetof(Transfer, id), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Local", "s", NULL, offsetof(Transfer, local), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Remote", "s", NULL, offsetof(Transfer, remote), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Type", "s", property_get_type, offsetof(Transfer, type), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Verify", "s", property_get_verify, offsetof(Transfer, verify), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Progress", "d", property_get_progress, 0, 0),
+ SD_BUS_METHOD("Cancel", NULL, NULL, method_cancel, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_SIGNAL("LogMessage", "us", 0),
+ SD_BUS_VTABLE_END,
+};
+
+static const sd_bus_vtable manager_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_METHOD("ImportTar", "hsbb", "uo", method_import_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("ImportRaw", "hsbb", "uo", method_import_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("ImportFileSystem", "hsbb", "uo", method_import_fs, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("ExportTar", "shs", "uo", method_export_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("ExportRaw", "shs", "uo", method_export_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("PullTar", "sssb", "uo", method_pull_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("PullRaw", "sssb", "uo", method_pull_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("ListTransfers", NULL, "a(usssdo)", method_list_transfers, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("CancelTransfer", "u", NULL, method_cancel_transfer, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_SIGNAL("TransferNew", "uo", 0),
+ SD_BUS_SIGNAL("TransferRemoved", "uos", 0),
+ SD_BUS_VTABLE_END,
+};
+
+static int transfer_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) {
+ Manager *m = userdata;
+ Transfer *t;
+ const char *p;
+ uint32_t id;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(interface);
+ assert(found);
+ assert(m);
+
+ p = startswith(path, "/org/freedesktop/import1/transfer/_");
+ if (!p)
+ return 0;
+
+ r = safe_atou32(p, &id);
+ if (r < 0 || id == 0)
+ return 0;
+
+ t = hashmap_get(m->transfers, UINT32_TO_PTR(id));
+ if (!t)
+ return 0;
+
+ *found = t;
+ return 1;
+}
+
+static int transfer_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error) {
+ _cleanup_strv_free_ char **l = NULL;
+ Manager *m = userdata;
+ Transfer *t;
+ unsigned k = 0;
+ Iterator i;
+
+ l = new0(char*, hashmap_size(m->transfers) + 1);
+ if (!l)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(t, m->transfers, i) {
+
+ l[k] = strdup(t->object_path);
+ if (!l[k])
+ return -ENOMEM;
+
+ k++;
+ }
+
+ *nodes = TAKE_PTR(l);
+
+ return 1;
+}
+
+static int manager_add_bus_objects(Manager *m) {
+ int r;
+
+ assert(m);
+
+ r = sd_bus_add_object_vtable(m->bus, NULL, "/org/freedesktop/import1", "org.freedesktop.import1.Manager", manager_vtable, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to register object: %m");
+
+ r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/import1/transfer", "org.freedesktop.import1.Transfer", transfer_vtable, transfer_object_find, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to register object: %m");
+
+ r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/import1/transfer", transfer_node_enumerator, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add transfer enumerator: %m");
+
+ r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.import1", 0, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request name: %m");
+
+ r = sd_bus_attach_event(m->bus, m->event, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+ return 0;
+}
+
+static bool manager_check_idle(void *userdata) {
+ Manager *m = userdata;
+
+ return hashmap_isempty(m->transfers);
+}
+
+static int manager_run(Manager *m) {
+ assert(m);
+
+ return bus_event_loop_with_idle(
+ m->event,
+ m->bus,
+ "org.freedesktop.import1",
+ DEFAULT_EXIT_USEC,
+ manager_check_idle,
+ m);
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(manager_unrefp) Manager *m = NULL;
+ int r;
+
+ log_setup_service();
+
+ umask(0022);
+
+ if (argc != 1) {
+ log_error("This program takes no arguments.");
+ return -EINVAL;
+ }
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, -1) >= 0);
+
+ r = manager_new(&m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate manager object: %m");
+
+ r = manager_add_bus_objects(m);
+ if (r < 0)
+ return r;
+
+ r = manager_run(m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/import/meson.build b/src/import/meson.build
new file mode 100644
index 0000000..1c15fd8
--- /dev/null
+++ b/src/import/meson.build
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+systemd_importd_sources = files('''
+ importd.c
+'''.split())
+
+systemd_pull_sources = files('''
+ pull.c
+ pull-raw.c
+ pull-raw.h
+ pull-tar.c
+ pull-tar.h
+ pull-job.c
+ pull-job.h
+ pull-common.c
+ pull-common.h
+ import-common.c
+ import-common.h
+ import-compress.c
+ import-compress.h
+ curl-util.c
+ curl-util.h
+ qcow2-util.c
+ qcow2-util.h
+'''.split())
+
+systemd_import_sources = files('''
+ import.c
+ import-raw.c
+ import-raw.h
+ import-tar.c
+ import-tar.h
+ import-common.c
+ import-common.h
+ import-compress.c
+ import-compress.h
+ qcow2-util.c
+ qcow2-util.h
+'''.split())
+
+systemd_import_fs_sources = files('''
+ import-fs.c
+ import-common.c
+ import-common.h
+'''.split())
+
+systemd_export_sources = files('''
+ export.c
+ export-tar.c
+ export-tar.h
+ export-raw.c
+ export-raw.h
+ import-common.c
+ import-common.h
+ import-compress.c
+ import-compress.h
+'''.split())
+
+if conf.get('ENABLE_IMPORTD') == 1
+ install_data('org.freedesktop.import1.conf',
+ install_dir : dbuspolicydir)
+ install_data('org.freedesktop.import1.service',
+ install_dir : dbussystemservicedir)
+ install_data('org.freedesktop.import1.policy',
+ install_dir : polkitpolicydir)
+
+ install_data('import-pubring.gpg',
+ install_dir : rootlibexecdir)
+ # TODO: shouldn't this be in pkgdatadir?
+endif
+
+tests += [
+ [['src/import/test-qcow2.c',
+ 'src/import/qcow2-util.c',
+ 'src/import/qcow2-util.h'],
+ [libshared],
+ [libz],
+ 'HAVE_ZLIB', 'manual'],
+]
diff --git a/src/import/org.freedesktop.import1.conf b/src/import/org.freedesktop.import1.conf
new file mode 100644
index 0000000..2fdb2ba
--- /dev/null
+++ b/src/import/org.freedesktop.import1.conf
@@ -0,0 +1,84 @@
+<?xml version="1.0"?> <!--*-nxml-*-->
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+
+<!--
+ SPDX-License-Identifier: LGPL-2.1+
+
+ This file is part of systemd.
+
+ systemd is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+-->
+
+<busconfig>
+
+ <policy user="root">
+ <allow own="org.freedesktop.import1"/>
+ <allow send_destination="org.freedesktop.import1"/>
+ <allow receive_sender="org.freedesktop.import1"/>
+ </policy>
+
+ <policy context="default">
+ <deny send_destination="org.freedesktop.import1"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.DBus.Introspectable"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.DBus.Peer"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.DBus.Properties"
+ send_member="Get"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.DBus.Properties"
+ send_member="GetAll"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Manager"
+ send_member="ListTransfers"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Manager"
+ send_member="CancelTransfer"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Manager"
+ send_member="ImportTar"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Manager"
+ send_member="ImportRaw"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Manager"
+ send_member="ImportFileSystem"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Manager"
+ send_member="ExportTar"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Manager"
+ send_member="ExportRaw"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Manager"
+ send_member="PullTar"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Manager"
+ send_member="PullRaw"/>
+
+ <allow send_destination="org.freedesktop.import1"
+ send_interface="org.freedesktop.import1.Transfer"
+ send_member="Cancel"/>
+
+ <allow receive_sender="org.freedesktop.import1"/>
+ </policy>
+
+</busconfig>
diff --git a/src/import/org.freedesktop.import1.policy b/src/import/org.freedesktop.import1.policy
new file mode 100644
index 0000000..beea5fe
--- /dev/null
+++ b/src/import/org.freedesktop.import1.policy
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
+<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+
+<!--
+ SPDX-License-Identifier: LGPL-2.1+
+
+ This file is part of systemd.
+
+ systemd is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+-->
+
+<policyconfig>
+
+ <vendor>The systemd Project</vendor>
+ <vendor_url>http://www.freedesktop.org/wiki/Software/systemd</vendor_url>
+
+ <action id="org.freedesktop.import1.import">
+ <description gettext-domain="systemd">Import a VM or container image</description>
+ <message gettext-domain="systemd">Authentication is required to import a VM or container image</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ </action>
+
+ <action id="org.freedesktop.import1.export">
+ <description gettext-domain="systemd">Export a VM or container image</description>
+ <message gettext-domain="systemd">Authentication is required to export a VM or container image</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ </action>
+
+ <action id="org.freedesktop.import1.pull">
+ <description gettext-domain="systemd">Download a VM or container image</description>
+ <message gettext-domain="systemd">Authentication is required to download a VM or container image</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ </action>
+
+</policyconfig>
diff --git a/src/import/org.freedesktop.import1.service b/src/import/org.freedesktop.import1.service
new file mode 100644
index 0000000..34d26d0
--- /dev/null
+++ b/src/import/org.freedesktop.import1.service
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[D-BUS Service]
+Name=org.freedesktop.import1
+Exec=/bin/false
+User=root
+SystemdService=dbus-org.freedesktop.import1.service
diff --git a/src/import/pull-common.c b/src/import/pull-common.c
new file mode 100644
index 0000000..acfe380
--- /dev/null
+++ b/src/import/pull-common.c
@@ -0,0 +1,530 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <sys/prctl.h>
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "capability-util.h"
+#include "copy.h"
+#include "dirent-util.h"
+#include "escape.h"
+#include "fd-util.h"
+#include "io-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "pull-common.h"
+#include "pull-job.h"
+#include "rlimit-util.h"
+#include "rm-rf.h"
+#include "signal-util.h"
+#include "siphash24.h"
+#include "string-util.h"
+#include "strv.h"
+#include "util.h"
+#include "web-util.h"
+
+#define FILENAME_ESCAPE "/.#\"\'"
+#define HASH_URL_THRESHOLD_LENGTH (_POSIX_PATH_MAX - 16)
+
+int pull_find_old_etags(
+ const char *url,
+ const char *image_root,
+ int dt,
+ const char *prefix,
+ const char *suffix,
+ char ***etags) {
+
+ _cleanup_free_ char *escaped_url = NULL;
+ _cleanup_closedir_ DIR *d = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+ struct dirent *de;
+ int r;
+
+ assert(url);
+ assert(etags);
+
+ if (!image_root)
+ image_root = "/var/lib/machines";
+
+ escaped_url = xescape(url, FILENAME_ESCAPE);
+ if (!escaped_url)
+ return -ENOMEM;
+
+ d = opendir(image_root);
+ if (!d) {
+ if (errno == ENOENT) {
+ *etags = NULL;
+ return 0;
+ }
+
+ return -errno;
+ }
+
+ FOREACH_DIRENT_ALL(de, d, return -errno) {
+ const char *a, *b;
+ char *u;
+
+ if (de->d_type != DT_UNKNOWN &&
+ de->d_type != dt)
+ continue;
+
+ if (prefix) {
+ a = startswith(de->d_name, prefix);
+ if (!a)
+ continue;
+ } else
+ a = de->d_name;
+
+ a = startswith(a, escaped_url);
+ if (!a)
+ continue;
+
+ a = startswith(a, ".");
+ if (!a)
+ continue;
+
+ if (suffix) {
+ b = endswith(de->d_name, suffix);
+ if (!b)
+ continue;
+ } else
+ b = strchr(de->d_name, 0);
+
+ if (a >= b)
+ continue;
+
+ r = cunescape_length(a, b - a, 0, &u);
+ if (r < 0)
+ return r;
+
+ if (!http_etag_is_valid(u)) {
+ free(u);
+ continue;
+ }
+
+ r = strv_consume(&l, u);
+ if (r < 0)
+ return r;
+ }
+
+ *etags = TAKE_PTR(l);
+
+ return 0;
+}
+
+int pull_make_local_copy(const char *final, const char *image_root, const char *local, bool force_local) {
+ const char *p;
+ int r;
+
+ assert(final);
+ assert(local);
+
+ if (!image_root)
+ image_root = "/var/lib/machines";
+
+ p = strjoina(image_root, "/", local);
+
+ if (force_local)
+ (void) rm_rf(p, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+
+ r = btrfs_subvol_snapshot(final, p,
+ BTRFS_SNAPSHOT_QUOTA|
+ BTRFS_SNAPSHOT_FALLBACK_COPY|
+ BTRFS_SNAPSHOT_FALLBACK_DIRECTORY|
+ BTRFS_SNAPSHOT_RECURSIVE);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create local image: %m");
+
+ log_info("Created new local image '%s'.", local);
+
+ return 0;
+}
+
+static int hash_url(const char *url, char **ret) {
+ uint64_t h;
+ static const sd_id128_t k = SD_ID128_ARRAY(df,89,16,87,01,cc,42,30,98,ab,4a,19,a6,a5,63,4f);
+
+ assert(url);
+
+ h = siphash24(url, strlen(url), k.bytes);
+ if (asprintf(ret, "%"PRIx64, h) < 0)
+ return -ENOMEM;
+
+ return 0;
+}
+
+int pull_make_path(const char *url, const char *etag, const char *image_root, const char *prefix, const char *suffix, char **ret) {
+ _cleanup_free_ char *escaped_url = NULL, *escaped_etag = NULL;
+ char *path;
+
+ assert(url);
+ assert(ret);
+
+ if (!image_root)
+ image_root = "/var/lib/machines";
+
+ escaped_url = xescape(url, FILENAME_ESCAPE);
+ if (!escaped_url)
+ return -ENOMEM;
+
+ if (etag) {
+ escaped_etag = xescape(etag, FILENAME_ESCAPE);
+ if (!escaped_etag)
+ return -ENOMEM;
+ }
+
+ path = strjoin(image_root, "/", strempty(prefix), escaped_url, escaped_etag ? "." : "",
+ strempty(escaped_etag), strempty(suffix));
+ if (!path)
+ return -ENOMEM;
+
+ /* URLs might make the path longer than the maximum allowed length for a file name.
+ * When that happens, a URL hash is used instead. Paths returned by this function
+ * can be later used with tempfn_random() which adds 16 bytes to the resulting name. */
+ if (strlen(path) >= HASH_URL_THRESHOLD_LENGTH) {
+ _cleanup_free_ char *hash = NULL;
+ int r;
+
+ free(path);
+
+ r = hash_url(url, &hash);
+ if (r < 0)
+ return r;
+
+ path = strjoin(image_root, "/", strempty(prefix), hash, escaped_etag ? "." : "",
+ strempty(escaped_etag), strempty(suffix));
+ if (!path)
+ return -ENOMEM;
+ }
+
+ *ret = path;
+ return 0;
+}
+
+int pull_make_auxiliary_job(
+ PullJob **ret,
+ const char *url,
+ int (*strip_suffixes)(const char *name, char **ret),
+ const char *suffix,
+ CurlGlue *glue,
+ PullJobFinished on_finished,
+ void *userdata) {
+
+ _cleanup_free_ char *last_component = NULL, *ll = NULL, *auxiliary_url = NULL;
+ _cleanup_(pull_job_unrefp) PullJob *job = NULL;
+ const char *q;
+ int r;
+
+ assert(ret);
+ assert(url);
+ assert(strip_suffixes);
+ assert(glue);
+
+ r = import_url_last_component(url, &last_component);
+ if (r < 0)
+ return r;
+
+ r = strip_suffixes(last_component, &ll);
+ if (r < 0)
+ return r;
+
+ q = strjoina(ll, suffix);
+
+ r = import_url_change_last_component(url, q, &auxiliary_url);
+ if (r < 0)
+ return r;
+
+ r = pull_job_new(&job, auxiliary_url, glue, userdata);
+ if (r < 0)
+ return r;
+
+ job->on_finished = on_finished;
+ job->compressed_max = job->uncompressed_max = 1ULL * 1024ULL * 1024ULL;
+
+ *ret = TAKE_PTR(job);
+
+ return 0;
+}
+
+int pull_make_verification_jobs(
+ PullJob **ret_checksum_job,
+ PullJob **ret_signature_job,
+ ImportVerify verify,
+ const char *url,
+ CurlGlue *glue,
+ PullJobFinished on_finished,
+ void *userdata) {
+
+ _cleanup_(pull_job_unrefp) PullJob *checksum_job = NULL, *signature_job = NULL;
+ int r;
+ const char *chksums = NULL;
+
+ assert(ret_checksum_job);
+ assert(ret_signature_job);
+ assert(verify >= 0);
+ assert(verify < _IMPORT_VERIFY_MAX);
+ assert(url);
+ assert(glue);
+
+ if (verify != IMPORT_VERIFY_NO) {
+ _cleanup_free_ char *checksum_url = NULL, *fn = NULL;
+
+ /* Queue jobs for the checksum file for the image. */
+ r = import_url_last_component(url, &fn);
+ if (r < 0)
+ return r;
+
+ chksums = strjoina(fn, ".sha256");
+
+ r = import_url_change_last_component(url, chksums, &checksum_url);
+ if (r < 0)
+ return r;
+
+ r = pull_job_new(&checksum_job, checksum_url, glue, userdata);
+ if (r < 0)
+ return r;
+
+ checksum_job->on_finished = on_finished;
+ checksum_job->uncompressed_max = checksum_job->compressed_max = 1ULL * 1024ULL * 1024ULL;
+ }
+
+ if (verify == IMPORT_VERIFY_SIGNATURE) {
+ _cleanup_free_ char *signature_url = NULL;
+
+ /* Queue job for the SHA256SUMS.gpg file for the image. */
+ r = import_url_change_last_component(url, "SHA256SUMS.gpg", &signature_url);
+ if (r < 0)
+ return r;
+
+ r = pull_job_new(&signature_job, signature_url, glue, userdata);
+ if (r < 0)
+ return r;
+
+ signature_job->on_finished = on_finished;
+ signature_job->uncompressed_max = signature_job->compressed_max = 1ULL * 1024ULL * 1024ULL;
+ }
+
+ *ret_checksum_job = checksum_job;
+ *ret_signature_job = signature_job;
+
+ checksum_job = signature_job = NULL;
+
+ return 0;
+}
+
+static int verify_one(PullJob *checksum_job, PullJob *job) {
+ _cleanup_free_ char *fn = NULL;
+ const char *line, *p;
+ int r;
+
+ assert(checksum_job);
+
+ if (!job)
+ return 0;
+
+ assert(IN_SET(job->state, PULL_JOB_DONE, PULL_JOB_FAILED));
+
+ /* Don't verify the checksum if we didn't actually successfully download something new */
+ if (job->state != PULL_JOB_DONE)
+ return 0;
+ if (job->error != 0)
+ return 0;
+ if (job->etag_exists)
+ return 0;
+
+ assert(job->calc_checksum);
+ assert(job->checksum);
+
+ r = import_url_last_component(job->url, &fn);
+ if (r < 0)
+ return log_oom();
+
+ if (!filename_is_valid(fn))
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG),
+ "Cannot verify checksum, could not determine server-side file name.");
+
+ line = strjoina(job->checksum, " *", fn, "\n");
+
+ p = memmem(checksum_job->payload,
+ checksum_job->payload_size,
+ line,
+ strlen(line));
+
+ if (!p) {
+ line = strjoina(job->checksum, " ", fn, "\n");
+
+ p = memmem(checksum_job->payload,
+ checksum_job->payload_size,
+ line,
+ strlen(line));
+ }
+
+ if (!p || (p != (char*) checksum_job->payload && p[-1] != '\n'))
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG),
+ "DOWNLOAD INVALID: Checksum of %s file did not checkout, file has been tampered with.", fn);
+
+ log_info("SHA256 checksum of %s is valid.", job->url);
+ return 1;
+}
+
+int pull_verify(PullJob *main_job,
+ PullJob *roothash_job,
+ PullJob *settings_job,
+ PullJob *checksum_job,
+ PullJob *signature_job) {
+
+ _cleanup_close_pair_ int gpg_pipe[2] = { -1, -1 };
+ _cleanup_close_ int sig_file = -1;
+ char sig_file_path[] = "/tmp/sigXXXXXX", gpg_home[] = "/tmp/gpghomeXXXXXX";
+ _cleanup_(sigkill_waitp) pid_t pid = 0;
+ bool gpg_home_created = false;
+ int r;
+
+ assert(main_job);
+ assert(main_job->state == PULL_JOB_DONE);
+
+ if (!checksum_job)
+ return 0;
+
+ assert(main_job->calc_checksum);
+ assert(main_job->checksum);
+
+ assert(checksum_job->state == PULL_JOB_DONE);
+
+ if (!checksum_job->payload || checksum_job->payload_size <= 0) {
+ log_error("Checksum is empty, cannot verify.");
+ return -EBADMSG;
+ }
+
+ r = verify_one(checksum_job, main_job);
+ if (r < 0)
+ return r;
+
+ r = verify_one(checksum_job, roothash_job);
+ if (r < 0)
+ return r;
+
+ r = verify_one(checksum_job, settings_job);
+ if (r < 0)
+ return r;
+
+ if (!signature_job)
+ return 0;
+
+ if (checksum_job->style == VERIFICATION_PER_FILE)
+ signature_job = checksum_job;
+
+ assert(signature_job->state == PULL_JOB_DONE);
+
+ if (!signature_job->payload || signature_job->payload_size <= 0) {
+ log_error("Signature is empty, cannot verify.");
+ return -EBADMSG;
+ }
+
+ r = pipe2(gpg_pipe, O_CLOEXEC);
+ if (r < 0)
+ return log_error_errno(errno, "Failed to create pipe for gpg: %m");
+
+ sig_file = mkostemp(sig_file_path, O_RDWR);
+ if (sig_file < 0)
+ return log_error_errno(errno, "Failed to create temporary file: %m");
+
+ r = loop_write(sig_file, signature_job->payload, signature_job->payload_size, false);
+ if (r < 0) {
+ log_error_errno(r, "Failed to write to temporary file: %m");
+ goto finish;
+ }
+
+ if (!mkdtemp(gpg_home)) {
+ r = log_error_errno(errno, "Failed to create tempory home for gpg: %m");
+ goto finish;
+ }
+
+ gpg_home_created = true;
+
+ r = safe_fork("(gpg)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ const char *cmd[] = {
+ "gpg",
+ "--no-options",
+ "--no-default-keyring",
+ "--no-auto-key-locate",
+ "--no-auto-check-trustdb",
+ "--batch",
+ "--trust-model=always",
+ NULL, /* --homedir= */
+ NULL, /* --keyring= */
+ NULL, /* --verify */
+ NULL, /* signature file */
+ NULL, /* dash */
+ NULL /* trailing NULL */
+ };
+ unsigned k = ELEMENTSOF(cmd) - 6;
+
+ /* Child */
+
+ gpg_pipe[1] = safe_close(gpg_pipe[1]);
+
+ r = rearrange_stdio(gpg_pipe[0], -1, STDERR_FILENO);
+ if (r < 0) {
+ log_error_errno(r, "Failed to rearrange stdin/stdout: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ (void) rlimit_nofile_safe();
+
+ cmd[k++] = strjoina("--homedir=", gpg_home);
+
+ /* We add the user keyring only to the command line
+ * arguments, if it's around since gpg fails
+ * otherwise. */
+ if (access(USER_KEYRING_PATH, F_OK) >= 0)
+ cmd[k++] = "--keyring=" USER_KEYRING_PATH;
+ else
+ cmd[k++] = "--keyring=" VENDOR_KEYRING_PATH;
+
+ cmd[k++] = "--verify";
+ if (checksum_job->style == VERIFICATION_PER_DIRECTORY) {
+ cmd[k++] = sig_file_path;
+ cmd[k++] = "-";
+ cmd[k++] = NULL;
+ }
+
+ execvp("gpg2", (char * const *) cmd);
+ execvp("gpg", (char * const *) cmd);
+ log_error_errno(errno, "Failed to execute gpg: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ gpg_pipe[0] = safe_close(gpg_pipe[0]);
+
+ r = loop_write(gpg_pipe[1], checksum_job->payload, checksum_job->payload_size, false);
+ if (r < 0) {
+ log_error_errno(r, "Failed to write to pipe: %m");
+ goto finish;
+ }
+
+ gpg_pipe[1] = safe_close(gpg_pipe[1]);
+
+ r = wait_for_terminate_and_check("gpg", pid, WAIT_LOG_ABNORMAL);
+ pid = 0;
+ if (r < 0)
+ goto finish;
+ if (r != EXIT_SUCCESS) {
+ log_error("DOWNLOAD INVALID: Signature verification failed.");
+ r = -EBADMSG;
+ } else {
+ log_info("Signature verification succeeded.");
+ r = 0;
+ }
+
+finish:
+ (void) unlink(sig_file_path);
+
+ if (gpg_home_created)
+ (void) rm_rf(gpg_home, REMOVE_ROOT|REMOVE_PHYSICAL);
+
+ return r;
+}
diff --git a/src/import/pull-common.h b/src/import/pull-common.h
new file mode 100644
index 0000000..65f239d
--- /dev/null
+++ b/src/import/pull-common.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <stdbool.h>
+
+#include "import-util.h"
+#include "pull-job.h"
+
+int pull_make_local_copy(const char *final, const char *root, const char *local, bool force_local);
+
+int pull_find_old_etags(const char *url, const char *root, int dt, const char *prefix, const char *suffix, char ***etags);
+
+int pull_make_path(const char *url, const char *etag, const char *image_root, const char *prefix, const char *suffix, char **ret);
+
+int pull_make_auxiliary_job(PullJob **ret, const char *url, int (*strip_suffixes)(const char *name, char **ret), const char *suffix, CurlGlue *glue, PullJobFinished on_finished, void *userdata);
+int pull_make_verification_jobs(PullJob **ret_checksum_job, PullJob **ret_signature_job, ImportVerify verify, const char *url, CurlGlue *glue, PullJobFinished on_finished, void *userdata);
+
+int pull_verify(PullJob *main_job, PullJob *roothash_job, PullJob *settings_job, PullJob *checksum_job, PullJob *signature_job);
diff --git a/src/import/pull-job.c b/src/import/pull-job.c
new file mode 100644
index 0000000..6881bd6
--- /dev/null
+++ b/src/import/pull-job.c
@@ -0,0 +1,636 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <sys/xattr.h>
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "gcrypt-util.h"
+#include "hexdecoct.h"
+#include "import-util.h"
+#include "io-util.h"
+#include "machine-pool.h"
+#include "parse-util.h"
+#include "pull-common.h"
+#include "pull-job.h"
+#include "string-util.h"
+#include "strv.h"
+#include "xattr-util.h"
+
+PullJob* pull_job_unref(PullJob *j) {
+ if (!j)
+ return NULL;
+
+ curl_glue_remove_and_free(j->glue, j->curl);
+ curl_slist_free_all(j->request_header);
+
+ safe_close(j->disk_fd);
+
+ import_compress_free(&j->compress);
+
+ if (j->checksum_context)
+ gcry_md_close(j->checksum_context);
+
+ free(j->url);
+ free(j->etag);
+ strv_free(j->old_etags);
+ free(j->payload);
+ free(j->checksum);
+
+ return mfree(j);
+}
+
+static void pull_job_finish(PullJob *j, int ret) {
+ assert(j);
+
+ if (IN_SET(j->state, PULL_JOB_DONE, PULL_JOB_FAILED))
+ return;
+
+ if (ret == 0) {
+ j->state = PULL_JOB_DONE;
+ j->progress_percent = 100;
+ log_info("Download of %s complete.", j->url);
+ } else {
+ j->state = PULL_JOB_FAILED;
+ j->error = ret;
+ }
+
+ if (j->on_finished)
+ j->on_finished(j);
+}
+
+static int pull_job_restart(PullJob *j) {
+ int r;
+ char *chksum_url = NULL;
+
+ r = import_url_change_last_component(j->url, "SHA256SUMS", &chksum_url);
+ if (r < 0)
+ return r;
+
+ free(j->url);
+ j->url = chksum_url;
+ j->state = PULL_JOB_INIT;
+ j->payload = mfree(j->payload);
+ j->payload_size = 0;
+ j->payload_allocated = 0;
+ j->written_compressed = 0;
+ j->written_uncompressed = 0;
+
+ r = pull_job_begin(j);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) {
+ PullJob *j = NULL;
+ CURLcode code;
+ long status;
+ int r;
+
+ if (curl_easy_getinfo(curl, CURLINFO_PRIVATE, (char **)&j) != CURLE_OK)
+ return;
+
+ if (!j || IN_SET(j->state, PULL_JOB_DONE, PULL_JOB_FAILED))
+ return;
+
+ if (result != CURLE_OK) {
+ log_error("Transfer failed: %s", curl_easy_strerror(result));
+ r = -EIO;
+ goto finish;
+ }
+
+ code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
+ if (code != CURLE_OK) {
+ log_error("Failed to retrieve response code: %s", curl_easy_strerror(code));
+ r = -EIO;
+ goto finish;
+ } else if (status == 304) {
+ log_info("Image already downloaded. Skipping download.");
+ j->etag_exists = true;
+ r = 0;
+ goto finish;
+ } else if (status >= 300) {
+ if (status == 404 && j->style == VERIFICATION_PER_FILE) {
+
+ /* retry pull job with SHA256SUMS file */
+ r = pull_job_restart(j);
+ if (r < 0)
+ goto finish;
+
+ code = curl_easy_getinfo(j->curl, CURLINFO_RESPONSE_CODE, &status);
+ if (code != CURLE_OK) {
+ log_error("Failed to retrieve response code: %s", curl_easy_strerror(code));
+ r = -EIO;
+ goto finish;
+ }
+
+ if (status == 0) {
+ j->style = VERIFICATION_PER_DIRECTORY;
+ return;
+ }
+ }
+
+ log_error("HTTP request to %s failed with code %li.", j->url, status);
+ r = -EIO;
+ goto finish;
+ } else if (status < 200) {
+ log_error("HTTP request to %s finished with unexpected code %li.", j->url, status);
+ r = -EIO;
+ goto finish;
+ }
+
+ if (j->state != PULL_JOB_RUNNING) {
+ log_error("Premature connection termination.");
+ r = -EIO;
+ goto finish;
+ }
+
+ if (j->content_length != (uint64_t) -1 &&
+ j->content_length != j->written_compressed) {
+ log_error("Download truncated.");
+ r = -EIO;
+ goto finish;
+ }
+
+ if (j->checksum_context) {
+ uint8_t *k;
+
+ k = gcry_md_read(j->checksum_context, GCRY_MD_SHA256);
+ if (!k) {
+ log_error("Failed to get checksum.");
+ r = -EIO;
+ goto finish;
+ }
+
+ j->checksum = hexmem(k, gcry_md_get_algo_dlen(GCRY_MD_SHA256));
+ if (!j->checksum) {
+ r = log_oom();
+ goto finish;
+ }
+
+ log_debug("SHA256 of %s is %s.", j->url, j->checksum);
+ }
+
+ if (j->disk_fd >= 0 && j->allow_sparse) {
+ /* Make sure the file size is right, in case the file was
+ * sparse and we just seeked for the last part */
+
+ if (ftruncate(j->disk_fd, j->written_uncompressed) < 0) {
+ r = log_error_errno(errno, "Failed to truncate file: %m");
+ goto finish;
+ }
+
+ if (j->etag)
+ (void) fsetxattr(j->disk_fd, "user.source_etag", j->etag, strlen(j->etag), 0);
+ if (j->url)
+ (void) fsetxattr(j->disk_fd, "user.source_url", j->url, strlen(j->url), 0);
+
+ if (j->mtime != 0) {
+ struct timespec ut[2];
+
+ timespec_store(&ut[0], j->mtime);
+ ut[1] = ut[0];
+ (void) futimens(j->disk_fd, ut);
+
+ (void) fd_setcrtime(j->disk_fd, j->mtime);
+ }
+ }
+
+ r = 0;
+
+finish:
+ pull_job_finish(j, r);
+}
+
+static int pull_job_write_uncompressed(const void *p, size_t sz, void *userdata) {
+ PullJob *j = userdata;
+ ssize_t n;
+
+ assert(j);
+ assert(p);
+
+ if (sz <= 0)
+ return 0;
+
+ if (j->written_uncompressed + sz < j->written_uncompressed)
+ return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW),
+ "File too large, overflow");
+
+ if (j->written_uncompressed + sz > j->uncompressed_max)
+ return log_error_errno(SYNTHETIC_ERRNO(EFBIG),
+ "File overly large, refusing");
+
+ if (j->disk_fd >= 0) {
+
+ if (j->allow_sparse)
+ n = sparse_write(j->disk_fd, p, sz, 64);
+ else {
+ n = write(j->disk_fd, p, sz);
+ if (n < 0)
+ n = -errno;
+ }
+ if (n < 0)
+ return log_error_errno((int) n, "Failed to write file: %m");
+ if ((size_t) n < sz)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write");
+ } else {
+
+ if (!GREEDY_REALLOC(j->payload, j->payload_allocated, j->payload_size + sz))
+ return log_oom();
+
+ memcpy(j->payload + j->payload_size, p, sz);
+ j->payload_size += sz;
+ }
+
+ j->written_uncompressed += sz;
+
+ return 0;
+}
+
+static int pull_job_write_compressed(PullJob *j, void *p, size_t sz) {
+ int r;
+
+ assert(j);
+ assert(p);
+
+ if (sz <= 0)
+ return 0;
+
+ if (j->written_compressed + sz < j->written_compressed)
+ return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "File too large, overflow");
+
+ if (j->written_compressed + sz > j->compressed_max)
+ return log_error_errno(SYNTHETIC_ERRNO(EFBIG), "File overly large, refusing.");
+
+ if (j->content_length != (uint64_t) -1 &&
+ j->written_compressed + sz > j->content_length)
+ return log_error_errno(SYNTHETIC_ERRNO(EFBIG),
+ "Content length incorrect.");
+
+ if (j->checksum_context)
+ gcry_md_write(j->checksum_context, p, sz);
+
+ r = import_uncompress(&j->compress, p, sz, pull_job_write_uncompressed, j);
+ if (r < 0)
+ return r;
+
+ j->written_compressed += sz;
+
+ return 0;
+}
+
+static int pull_job_open_disk(PullJob *j) {
+ int r;
+
+ assert(j);
+
+ if (j->on_open_disk) {
+ r = j->on_open_disk(j);
+ if (r < 0)
+ return r;
+ }
+
+ if (j->disk_fd >= 0) {
+ /* Check if we can do sparse files */
+
+ if (lseek(j->disk_fd, SEEK_SET, 0) == 0)
+ j->allow_sparse = true;
+ else {
+ if (errno != ESPIPE)
+ return log_error_errno(errno, "Failed to seek on file descriptor: %m");
+
+ j->allow_sparse = false;
+ }
+ }
+
+ if (j->calc_checksum) {
+ initialize_libgcrypt(false);
+
+ if (gcry_md_open(&j->checksum_context, GCRY_MD_SHA256, 0) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to initialize hash context.");
+ }
+
+ return 0;
+}
+
+static int pull_job_detect_compression(PullJob *j) {
+ _cleanup_free_ uint8_t *stub = NULL;
+ size_t stub_size;
+
+ int r;
+
+ assert(j);
+
+ r = import_uncompress_detect(&j->compress, j->payload, j->payload_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize compressor: %m");
+ if (r == 0)
+ return 0;
+
+ log_debug("Stream is compressed: %s", import_compress_type_to_string(j->compress.type));
+
+ r = pull_job_open_disk(j);
+ if (r < 0)
+ return r;
+
+ /* Now, take the payload we read so far, and decompress it */
+ stub = j->payload;
+ stub_size = j->payload_size;
+
+ j->payload = NULL;
+ j->payload_size = 0;
+ j->payload_allocated = 0;
+
+ j->state = PULL_JOB_RUNNING;
+
+ r = pull_job_write_compressed(j, stub, stub_size);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static size_t pull_job_write_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
+ PullJob *j = userdata;
+ size_t sz = size * nmemb;
+ int r;
+
+ assert(contents);
+ assert(j);
+
+ switch (j->state) {
+
+ case PULL_JOB_ANALYZING:
+ /* Let's first check what it actually is */
+
+ if (!GREEDY_REALLOC(j->payload, j->payload_allocated, j->payload_size + sz)) {
+ r = log_oom();
+ goto fail;
+ }
+
+ memcpy(j->payload + j->payload_size, contents, sz);
+ j->payload_size += sz;
+
+ r = pull_job_detect_compression(j);
+ if (r < 0)
+ goto fail;
+
+ break;
+
+ case PULL_JOB_RUNNING:
+
+ r = pull_job_write_compressed(j, contents, sz);
+ if (r < 0)
+ goto fail;
+
+ break;
+
+ case PULL_JOB_DONE:
+ case PULL_JOB_FAILED:
+ r = -ESTALE;
+ goto fail;
+
+ default:
+ assert_not_reached("Impossible state.");
+ }
+
+ return sz;
+
+fail:
+ pull_job_finish(j, r);
+ return 0;
+}
+
+static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
+ PullJob *j = userdata;
+ size_t sz = size * nmemb;
+ _cleanup_free_ char *length = NULL, *last_modified = NULL;
+ char *etag;
+ int r;
+
+ assert(contents);
+ assert(j);
+
+ if (IN_SET(j->state, PULL_JOB_DONE, PULL_JOB_FAILED)) {
+ r = -ESTALE;
+ goto fail;
+ }
+
+ assert(j->state == PULL_JOB_ANALYZING);
+
+ r = curl_header_strdup(contents, sz, "ETag:", &etag);
+ if (r < 0) {
+ log_oom();
+ goto fail;
+ }
+ if (r > 0) {
+ free(j->etag);
+ j->etag = etag;
+
+ if (strv_contains(j->old_etags, j->etag)) {
+ log_info("Image already downloaded. Skipping download.");
+ j->etag_exists = true;
+ pull_job_finish(j, 0);
+ return sz;
+ }
+
+ return sz;
+ }
+
+ r = curl_header_strdup(contents, sz, "Content-Length:", &length);
+ if (r < 0) {
+ log_oom();
+ goto fail;
+ }
+ if (r > 0) {
+ (void) safe_atou64(length, &j->content_length);
+
+ if (j->content_length != (uint64_t) -1) {
+ char bytes[FORMAT_BYTES_MAX];
+
+ if (j->content_length > j->compressed_max) {
+ log_error("Content too large.");
+ r = -EFBIG;
+ goto fail;
+ }
+
+ log_info("Downloading %s for %s.", format_bytes(bytes, sizeof(bytes), j->content_length), j->url);
+ }
+
+ return sz;
+ }
+
+ r = curl_header_strdup(contents, sz, "Last-Modified:", &last_modified);
+ if (r < 0) {
+ log_oom();
+ goto fail;
+ }
+ if (r > 0) {
+ (void) curl_parse_http_time(last_modified, &j->mtime);
+ return sz;
+ }
+
+ if (j->on_header) {
+ r = j->on_header(j, contents, sz);
+ if (r < 0)
+ goto fail;
+ }
+
+ return sz;
+
+fail:
+ pull_job_finish(j, r);
+ return 0;
+}
+
+static int pull_job_progress_callback(void *userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
+ PullJob *j = userdata;
+ unsigned percent;
+ usec_t n;
+
+ assert(j);
+
+ if (dltotal <= 0)
+ return 0;
+
+ percent = ((100 * dlnow) / dltotal);
+ n = now(CLOCK_MONOTONIC);
+
+ if (n > j->last_status_usec + USEC_PER_SEC &&
+ percent != j->progress_percent &&
+ dlnow < dltotal) {
+ char buf[FORMAT_TIMESPAN_MAX];
+
+ if (n - j->start_usec > USEC_PER_SEC && dlnow > 0) {
+ char y[FORMAT_BYTES_MAX];
+ usec_t left, done;
+
+ done = n - j->start_usec;
+ left = (usec_t) (((double) done * (double) dltotal) / dlnow) - done;
+
+ log_info("Got %u%% of %s. %s left at %s/s.",
+ percent,
+ j->url,
+ format_timespan(buf, sizeof(buf), left, USEC_PER_SEC),
+ format_bytes(y, sizeof(y), (uint64_t) ((double) dlnow / ((double) done / (double) USEC_PER_SEC))));
+ } else
+ log_info("Got %u%% of %s.", percent, j->url);
+
+ j->progress_percent = percent;
+ j->last_status_usec = n;
+
+ if (j->on_progress)
+ j->on_progress(j);
+ }
+
+ return 0;
+}
+
+int pull_job_new(PullJob **ret, const char *url, CurlGlue *glue, void *userdata) {
+ _cleanup_(pull_job_unrefp) PullJob *j = NULL;
+ _cleanup_free_ char *u = NULL;
+
+ assert(url);
+ assert(glue);
+ assert(ret);
+
+ u = strdup(url);
+ if (!u)
+ return -ENOMEM;
+
+ j = new(PullJob, 1);
+ if (!j)
+ return -ENOMEM;
+
+ *j = (PullJob) {
+ .state = PULL_JOB_INIT,
+ .disk_fd = -1,
+ .userdata = userdata,
+ .glue = glue,
+ .content_length = (uint64_t) -1,
+ .start_usec = now(CLOCK_MONOTONIC),
+ .compressed_max = 64LLU * 1024LLU * 1024LLU * 1024LLU, /* 64GB safety limit */
+ .uncompressed_max = 64LLU * 1024LLU * 1024LLU * 1024LLU, /* 64GB safety limit */
+ .style = VERIFICATION_STYLE_UNSET,
+ .url = TAKE_PTR(u),
+ };
+
+ *ret = TAKE_PTR(j);
+
+ return 0;
+}
+
+int pull_job_begin(PullJob *j) {
+ int r;
+
+ assert(j);
+
+ if (j->state != PULL_JOB_INIT)
+ return -EBUSY;
+
+ r = curl_glue_make(&j->curl, j->url, j);
+ if (r < 0)
+ return r;
+
+ if (!strv_isempty(j->old_etags)) {
+ _cleanup_free_ char *cc = NULL, *hdr = NULL;
+
+ cc = strv_join(j->old_etags, ", ");
+ if (!cc)
+ return -ENOMEM;
+
+ hdr = strappend("If-None-Match: ", cc);
+ if (!hdr)
+ return -ENOMEM;
+
+ if (!j->request_header) {
+ j->request_header = curl_slist_new(hdr, NULL);
+ if (!j->request_header)
+ return -ENOMEM;
+ } else {
+ struct curl_slist *l;
+
+ l = curl_slist_append(j->request_header, hdr);
+ if (!l)
+ return -ENOMEM;
+
+ j->request_header = l;
+ }
+ }
+
+ if (j->request_header) {
+ if (curl_easy_setopt(j->curl, CURLOPT_HTTPHEADER, j->request_header) != CURLE_OK)
+ return -EIO;
+ }
+
+ if (curl_easy_setopt(j->curl, CURLOPT_WRITEFUNCTION, pull_job_write_callback) != CURLE_OK)
+ return -EIO;
+
+ if (curl_easy_setopt(j->curl, CURLOPT_WRITEDATA, j) != CURLE_OK)
+ return -EIO;
+
+ if (curl_easy_setopt(j->curl, CURLOPT_HEADERFUNCTION, pull_job_header_callback) != CURLE_OK)
+ return -EIO;
+
+ if (curl_easy_setopt(j->curl, CURLOPT_HEADERDATA, j) != CURLE_OK)
+ return -EIO;
+
+ if (curl_easy_setopt(j->curl, CURLOPT_XFERINFOFUNCTION, pull_job_progress_callback) != CURLE_OK)
+ return -EIO;
+
+ if (curl_easy_setopt(j->curl, CURLOPT_XFERINFODATA, j) != CURLE_OK)
+ return -EIO;
+
+ if (curl_easy_setopt(j->curl, CURLOPT_NOPROGRESS, 0) != CURLE_OK)
+ return -EIO;
+
+ r = curl_glue_add(j->glue, j->curl);
+ if (r < 0)
+ return r;
+
+ j->state = PULL_JOB_ANALYZING;
+
+ return 0;
+}
diff --git a/src/import/pull-job.h b/src/import/pull-job.h
new file mode 100644
index 0000000..c907e74
--- /dev/null
+++ b/src/import/pull-job.h
@@ -0,0 +1,93 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <gcrypt.h>
+
+#include "curl-util.h"
+#include "import-compress.h"
+#include "macro.h"
+
+typedef struct PullJob PullJob;
+
+typedef void (*PullJobFinished)(PullJob *job);
+typedef int (*PullJobOpenDisk)(PullJob *job);
+typedef int (*PullJobHeader)(PullJob *job, const char *header, size_t sz);
+typedef void (*PullJobProgress)(PullJob *job);
+
+typedef enum PullJobState {
+ PULL_JOB_INIT,
+ PULL_JOB_ANALYZING, /* Still reading into ->payload, to figure out what we have */
+ PULL_JOB_RUNNING, /* Writing to destination */
+ PULL_JOB_DONE,
+ PULL_JOB_FAILED,
+ _PULL_JOB_STATE_MAX,
+ _PULL_JOB_STATE_INVALID = -1,
+} PullJobState;
+
+typedef enum VerificationStyle {
+ VERIFICATION_STYLE_UNSET,
+ VERIFICATION_PER_FILE, /* SuSE-style ".sha256" files with inline signature */
+ VERIFICATION_PER_DIRECTORY, /* Ubuntu-style SHA256SUM files with detach SHA256SUM.gpg signatures */
+} VerificationStyle;
+
+#define PULL_JOB_IS_COMPLETE(j) (IN_SET((j)->state, PULL_JOB_DONE, PULL_JOB_FAILED))
+
+struct PullJob {
+ PullJobState state;
+ int error;
+
+ char *url;
+
+ void *userdata;
+ PullJobFinished on_finished;
+ PullJobOpenDisk on_open_disk;
+ PullJobHeader on_header;
+ PullJobProgress on_progress;
+
+ CurlGlue *glue;
+ CURL *curl;
+ struct curl_slist *request_header;
+
+ char *etag;
+ char **old_etags;
+ bool etag_exists;
+
+ uint64_t content_length;
+ uint64_t written_compressed;
+ uint64_t written_uncompressed;
+
+ uint64_t uncompressed_max;
+ uint64_t compressed_max;
+
+ uint8_t *payload;
+ size_t payload_size;
+ size_t payload_allocated;
+
+ int disk_fd;
+
+ usec_t mtime;
+
+ ImportCompress compress;
+
+ unsigned progress_percent;
+ usec_t start_usec;
+ usec_t last_status_usec;
+
+ bool allow_sparse;
+
+ bool calc_checksum;
+ gcry_md_hd_t checksum_context;
+
+ char *checksum;
+
+ VerificationStyle style;
+};
+
+int pull_job_new(PullJob **job, const char *url, CurlGlue *glue, void *userdata);
+PullJob* pull_job_unref(PullJob *job);
+
+int pull_job_begin(PullJob *j);
+
+void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(PullJob*, pull_job_unref);
diff --git a/src/import/pull-raw.c b/src/import/pull-raw.c
new file mode 100644
index 0000000..3a3e015
--- /dev/null
+++ b/src/import/pull-raw.c
@@ -0,0 +1,750 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <curl/curl.h>
+#include <linux/fs.h>
+#include <sys/xattr.h>
+
+#include "sd-daemon.h"
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "chattr-util.h"
+#include "copy.h"
+#include "curl-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "hostname-util.h"
+#include "import-common.h"
+#include "import-util.h"
+#include "macro.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "pull-common.h"
+#include "pull-job.h"
+#include "pull-raw.h"
+#include "qcow2-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "utf8.h"
+#include "util.h"
+#include "web-util.h"
+
+typedef enum RawProgress {
+ RAW_DOWNLOADING,
+ RAW_VERIFYING,
+ RAW_UNPACKING,
+ RAW_FINALIZING,
+ RAW_COPYING,
+} RawProgress;
+
+struct RawPull {
+ sd_event *event;
+ CurlGlue *glue;
+
+ char *image_root;
+
+ PullJob *raw_job;
+ PullJob *roothash_job;
+ PullJob *settings_job;
+ PullJob *checksum_job;
+ PullJob *signature_job;
+
+ RawPullFinished on_finished;
+ void *userdata;
+
+ char *local;
+ bool force_local;
+ bool settings;
+ bool roothash;
+
+ char *final_path;
+ char *temp_path;
+
+ char *settings_path;
+ char *settings_temp_path;
+
+ char *roothash_path;
+ char *roothash_temp_path;
+
+ ImportVerify verify;
+};
+
+RawPull* raw_pull_unref(RawPull *i) {
+ if (!i)
+ return NULL;
+
+ pull_job_unref(i->raw_job);
+ pull_job_unref(i->settings_job);
+ pull_job_unref(i->roothash_job);
+ pull_job_unref(i->checksum_job);
+ pull_job_unref(i->signature_job);
+
+ curl_glue_unref(i->glue);
+ sd_event_unref(i->event);
+
+ if (i->temp_path) {
+ (void) unlink(i->temp_path);
+ free(i->temp_path);
+ }
+
+ if (i->roothash_temp_path) {
+ (void) unlink(i->roothash_temp_path);
+ free(i->roothash_temp_path);
+ }
+
+ if (i->settings_temp_path) {
+ (void) unlink(i->settings_temp_path);
+ free(i->settings_temp_path);
+ }
+
+ free(i->final_path);
+ free(i->roothash_path);
+ free(i->settings_path);
+ free(i->image_root);
+ free(i->local);
+ return mfree(i);
+}
+
+int raw_pull_new(
+ RawPull **ret,
+ sd_event *event,
+ const char *image_root,
+ RawPullFinished on_finished,
+ void *userdata) {
+
+ _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(raw_pull_unrefp) RawPull *i = NULL;
+ _cleanup_free_ char *root = NULL;
+ int r;
+
+ assert(ret);
+
+ root = strdup(image_root ?: "/var/lib/machines");
+ if (!root)
+ return -ENOMEM;
+
+ if (event)
+ e = sd_event_ref(event);
+ else {
+ r = sd_event_default(&e);
+ if (r < 0)
+ return r;
+ }
+
+ r = curl_glue_new(&g, e);
+ if (r < 0)
+ return r;
+
+ i = new(RawPull, 1);
+ if (!i)
+ return -ENOMEM;
+
+ *i = (RawPull) {
+ .on_finished = on_finished,
+ .userdata = userdata,
+ .image_root = TAKE_PTR(root),
+ .event = TAKE_PTR(e),
+ .glue = TAKE_PTR(g),
+ };
+
+ i->glue->on_finished = pull_job_curl_on_finished;
+ i->glue->userdata = i;
+
+ *ret = TAKE_PTR(i);
+
+ return 0;
+}
+
+static void raw_pull_report_progress(RawPull *i, RawProgress p) {
+ unsigned percent;
+
+ assert(i);
+
+ switch (p) {
+
+ case RAW_DOWNLOADING: {
+ unsigned remain = 80;
+
+ percent = 0;
+
+ if (i->settings_job) {
+ percent += i->settings_job->progress_percent * 5 / 100;
+ remain -= 5;
+ }
+
+ if (i->roothash_job) {
+ percent += i->roothash_job->progress_percent * 5 / 100;
+ remain -= 5;
+ }
+
+ if (i->checksum_job) {
+ percent += i->checksum_job->progress_percent * 5 / 100;
+ remain -= 5;
+ }
+
+ if (i->signature_job) {
+ percent += i->signature_job->progress_percent * 5 / 100;
+ remain -= 5;
+ }
+
+ if (i->raw_job)
+ percent += i->raw_job->progress_percent * remain / 100;
+ break;
+ }
+
+ case RAW_VERIFYING:
+ percent = 80;
+ break;
+
+ case RAW_UNPACKING:
+ percent = 85;
+ break;
+
+ case RAW_FINALIZING:
+ percent = 90;
+ break;
+
+ case RAW_COPYING:
+ percent = 95;
+ break;
+
+ default:
+ assert_not_reached("Unknown progress state");
+ }
+
+ sd_notifyf(false, "X_IMPORT_PROGRESS=%u", percent);
+ log_debug("Combined progress %u%%", percent);
+}
+
+static int raw_pull_maybe_convert_qcow2(RawPull *i) {
+ _cleanup_close_ int converted_fd = -1;
+ _cleanup_free_ char *t = NULL;
+ int r;
+
+ assert(i);
+ assert(i->raw_job);
+
+ r = qcow2_detect(i->raw_job->disk_fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to detect whether this is a QCOW2 image: %m");
+ if (r == 0)
+ return 0;
+
+ /* This is a QCOW2 image, let's convert it */
+ r = tempfn_random(i->final_path, NULL, &t);
+ if (r < 0)
+ return log_oom();
+
+ converted_fd = open(t, O_RDWR|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0664);
+ if (converted_fd < 0)
+ return log_error_errno(errno, "Failed to create %s: %m", t);
+
+ r = chattr_fd(converted_fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set file attributes on %s: %m", t);
+
+ log_info("Unpacking QCOW2 file.");
+
+ r = qcow2_convert(i->raw_job->disk_fd, converted_fd);
+ if (r < 0) {
+ unlink(t);
+ return log_error_errno(r, "Failed to convert qcow2 image: %m");
+ }
+
+ (void) unlink(i->temp_path);
+ free_and_replace(i->temp_path, t);
+
+ safe_close(i->raw_job->disk_fd);
+ i->raw_job->disk_fd = TAKE_FD(converted_fd);
+
+ return 1;
+}
+
+static int raw_pull_determine_path(RawPull *i, const char *suffix, char **field) {
+ int r;
+
+ assert(i);
+ assert(field);
+
+ if (*field)
+ return 0;
+
+ assert(i->raw_job);
+
+ r = pull_make_path(i->raw_job->url, i->raw_job->etag, i->image_root, ".raw-", suffix, field);
+ if (r < 0)
+ return log_oom();
+
+ return 1;
+}
+
+static int raw_pull_copy_auxiliary_file(
+ RawPull *i,
+ const char *suffix,
+ char **path) {
+
+ const char *local;
+ int r;
+
+ assert(i);
+ assert(suffix);
+ assert(path);
+
+ r = raw_pull_determine_path(i, suffix, path);
+ if (r < 0)
+ return r;
+
+ local = strjoina(i->image_root, "/", i->local, suffix);
+
+ r = copy_file_atomic(*path, local, 0644, 0, COPY_REFLINK | (i->force_local ? COPY_REPLACE : 0));
+ if (r == -EEXIST)
+ log_warning_errno(r, "File %s already exists, not replacing.", local);
+ else if (r == -ENOENT)
+ log_debug_errno(r, "Skipping creation of auxiliary file, since none was found.");
+ else if (r < 0)
+ log_warning_errno(r, "Failed to copy file %s, ignoring: %m", local);
+ else
+ log_info("Created new file %s.", local);
+
+ return 0;
+}
+
+static int raw_pull_make_local_copy(RawPull *i) {
+ _cleanup_free_ char *tp = NULL;
+ _cleanup_close_ int dfd = -1;
+ const char *p;
+ int r;
+
+ assert(i);
+ assert(i->raw_job);
+
+ if (!i->local)
+ return 0;
+
+ if (i->raw_job->etag_exists) {
+ /* We have downloaded this one previously, reopen it */
+
+ assert(i->raw_job->disk_fd < 0);
+
+ i->raw_job->disk_fd = open(i->final_path, O_RDONLY|O_NOCTTY|O_CLOEXEC);
+ if (i->raw_job->disk_fd < 0)
+ return log_error_errno(errno, "Failed to open vendor image: %m");
+ } else {
+ /* We freshly downloaded the image, use it */
+
+ assert(i->raw_job->disk_fd >= 0);
+
+ if (lseek(i->raw_job->disk_fd, SEEK_SET, 0) == (off_t) -1)
+ return log_error_errno(errno, "Failed to seek to beginning of vendor image: %m");
+ }
+
+ p = strjoina(i->image_root, "/", i->local, ".raw");
+
+ if (i->force_local)
+ (void) rm_rf(p, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+
+ r = tempfn_random(p, NULL, &tp);
+ if (r < 0)
+ return log_oom();
+
+ dfd = open(tp, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0664);
+ if (dfd < 0)
+ return log_error_errno(errno, "Failed to create writable copy of image: %m");
+
+ /* Turn off COW writing. This should greatly improve
+ * performance on COW file systems like btrfs, since it
+ * reduces fragmentation caused by not allowing in-place
+ * writes. */
+ r = chattr_fd(dfd, FS_NOCOW_FL, FS_NOCOW_FL, NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set file attributes on %s: %m", tp);
+
+ r = copy_bytes(i->raw_job->disk_fd, dfd, (uint64_t) -1, COPY_REFLINK);
+ if (r < 0) {
+ unlink(tp);
+ return log_error_errno(r, "Failed to make writable copy of image: %m");
+ }
+
+ (void) copy_times(i->raw_job->disk_fd, dfd);
+ (void) copy_xattr(i->raw_job->disk_fd, dfd);
+
+ dfd = safe_close(dfd);
+
+ r = rename(tp, p);
+ if (r < 0) {
+ r = log_error_errno(errno, "Failed to move writable image into place: %m");
+ unlink(tp);
+ return r;
+ }
+
+ log_info("Created new local image '%s'.", i->local);
+
+ if (i->roothash) {
+ r = raw_pull_copy_auxiliary_file(i, ".roothash", &i->roothash_path);
+ if (r < 0)
+ return r;
+ }
+
+ if (i->settings) {
+ r = raw_pull_copy_auxiliary_file(i, ".nspawn", &i->settings_path);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static bool raw_pull_is_done(RawPull *i) {
+ assert(i);
+ assert(i->raw_job);
+
+ if (!PULL_JOB_IS_COMPLETE(i->raw_job))
+ return false;
+ if (i->roothash_job && !PULL_JOB_IS_COMPLETE(i->roothash_job))
+ return false;
+ if (i->settings_job && !PULL_JOB_IS_COMPLETE(i->settings_job))
+ return false;
+ if (i->checksum_job && !PULL_JOB_IS_COMPLETE(i->checksum_job))
+ return false;
+ if (i->signature_job && !PULL_JOB_IS_COMPLETE(i->signature_job))
+ return false;
+
+ return true;
+}
+
+static int raw_pull_rename_auxiliary_file(
+ RawPull *i,
+ const char *suffix,
+ char **temp_path,
+ char **path) {
+
+ int r;
+
+ assert(i);
+ assert(temp_path);
+ assert(suffix);
+ assert(path);
+
+ /* Regenerate final name for this auxiliary file, we might know the etag of the file now, and we should
+ * incorporate it in the file name if we can */
+ *path = mfree(*path);
+ r = raw_pull_determine_path(i, suffix, path);
+ if (r < 0)
+ return r;
+
+ r = import_make_read_only(*temp_path);
+ if (r < 0)
+ return r;
+
+ r = rename_noreplace(AT_FDCWD, *temp_path, AT_FDCWD, *path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to rename file %s to %s: %m", *temp_path, *path);
+
+ *temp_path = mfree(*temp_path);
+
+ return 1;
+}
+
+static void raw_pull_job_on_finished(PullJob *j) {
+ RawPull *i;
+ int r;
+
+ assert(j);
+ assert(j->userdata);
+
+ i = j->userdata;
+ if (j == i->roothash_job) {
+ if (j->error != 0)
+ log_info_errno(j->error, "Root hash file could not be retrieved, proceeding without.");
+ } else if (j == i->settings_job) {
+ if (j->error != 0)
+ log_info_errno(j->error, "Settings file could not be retrieved, proceeding without.");
+ } else if (j->error != 0 && j != i->signature_job) {
+ if (j == i->checksum_job)
+ log_error_errno(j->error, "Failed to retrieve SHA256 checksum, cannot verify. (Try --verify=no?)");
+ else
+ log_error_errno(j->error, "Failed to retrieve image file. (Wrong URL?)");
+
+ r = j->error;
+ goto finish;
+ }
+
+ /* This is invoked if either the download completed
+ * successfully, or the download was skipped because we
+ * already have the etag. In this case ->etag_exists is
+ * true.
+ *
+ * We only do something when we got all three files */
+
+ if (!raw_pull_is_done(i))
+ return;
+
+ if (i->signature_job && i->checksum_job->style == VERIFICATION_PER_DIRECTORY && i->signature_job->error != 0) {
+ log_error_errno(j->error, "Failed to retrieve signature file, cannot verify. (Try --verify=no?)");
+
+ r = i->signature_job->error;
+ goto finish;
+ }
+
+ if (i->roothash_job)
+ i->roothash_job->disk_fd = safe_close(i->roothash_job->disk_fd);
+ if (i->settings_job)
+ i->settings_job->disk_fd = safe_close(i->settings_job->disk_fd);
+
+ r = raw_pull_determine_path(i, ".raw", &i->final_path);
+ if (r < 0)
+ goto finish;
+
+ if (!i->raw_job->etag_exists) {
+ /* This is a new download, verify it, and move it into place */
+ assert(i->raw_job->disk_fd >= 0);
+
+ raw_pull_report_progress(i, RAW_VERIFYING);
+
+ r = pull_verify(i->raw_job, i->roothash_job, i->settings_job, i->checksum_job, i->signature_job);
+ if (r < 0)
+ goto finish;
+
+ raw_pull_report_progress(i, RAW_UNPACKING);
+
+ r = raw_pull_maybe_convert_qcow2(i);
+ if (r < 0)
+ goto finish;
+
+ raw_pull_report_progress(i, RAW_FINALIZING);
+
+ r = import_make_read_only_fd(i->raw_job->disk_fd);
+ if (r < 0)
+ goto finish;
+
+ r = rename_noreplace(AT_FDCWD, i->temp_path, AT_FDCWD, i->final_path);
+ if (r < 0) {
+ log_error_errno(r, "Failed to rename raw file to %s: %m", i->final_path);
+ goto finish;
+ }
+
+ i->temp_path = mfree(i->temp_path);
+
+ if (i->roothash_job &&
+ i->roothash_job->error == 0) {
+ r = raw_pull_rename_auxiliary_file(i, ".roothash", &i->roothash_temp_path, &i->roothash_path);
+ if (r < 0)
+ goto finish;
+ }
+
+ if (i->settings_job &&
+ i->settings_job->error == 0) {
+ r = raw_pull_rename_auxiliary_file(i, ".nspawn", &i->settings_temp_path, &i->settings_path);
+ if (r < 0)
+ goto finish;
+ }
+ }
+
+ raw_pull_report_progress(i, RAW_COPYING);
+
+ r = raw_pull_make_local_copy(i);
+ if (r < 0)
+ goto finish;
+
+ r = 0;
+
+finish:
+ if (i->on_finished)
+ i->on_finished(i, r, i->userdata);
+ else
+ sd_event_exit(i->event, r);
+}
+
+static int raw_pull_job_on_open_disk_generic(
+ RawPull *i,
+ PullJob *j,
+ const char *extra,
+ char **temp_path) {
+
+ int r;
+
+ assert(i);
+ assert(j);
+ assert(extra);
+ assert(temp_path);
+
+ if (!*temp_path) {
+ r = tempfn_random_child(i->image_root, extra, temp_path);
+ if (r < 0)
+ return log_oom();
+ }
+
+ (void) mkdir_parents_label(*temp_path, 0700);
+
+ j->disk_fd = open(*temp_path, O_RDWR|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0664);
+ if (j->disk_fd < 0)
+ return log_error_errno(errno, "Failed to create %s: %m", *temp_path);
+
+ return 0;
+}
+
+static int raw_pull_job_on_open_disk_raw(PullJob *j) {
+ RawPull *i;
+ int r;
+
+ assert(j);
+ assert(j->userdata);
+
+ i = j->userdata;
+ assert(i->raw_job == j);
+
+ r = raw_pull_job_on_open_disk_generic(i, j, "raw", &i->temp_path);
+ if (r < 0)
+ return r;
+
+ r = chattr_fd(j->disk_fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set file attributes on %s, ignoring: %m", i->temp_path);
+
+ return 0;
+}
+
+static int raw_pull_job_on_open_disk_roothash(PullJob *j) {
+ RawPull *i;
+
+ assert(j);
+ assert(j->userdata);
+
+ i = j->userdata;
+ assert(i->roothash_job == j);
+
+ return raw_pull_job_on_open_disk_generic(i, j, "roothash", &i->roothash_temp_path);
+}
+
+static int raw_pull_job_on_open_disk_settings(PullJob *j) {
+ RawPull *i;
+
+ assert(j);
+ assert(j->userdata);
+
+ i = j->userdata;
+ assert(i->settings_job == j);
+
+ return raw_pull_job_on_open_disk_generic(i, j, "settings", &i->settings_temp_path);
+}
+
+static void raw_pull_job_on_progress(PullJob *j) {
+ RawPull *i;
+
+ assert(j);
+ assert(j->userdata);
+
+ i = j->userdata;
+
+ raw_pull_report_progress(i, RAW_DOWNLOADING);
+}
+
+int raw_pull_start(
+ RawPull *i,
+ const char *url,
+ const char *local,
+ bool force_local,
+ ImportVerify verify,
+ bool settings,
+ bool roothash) {
+
+ int r;
+
+ assert(i);
+ assert(verify < _IMPORT_VERIFY_MAX);
+ assert(verify >= 0);
+
+ if (!http_url_is_valid(url))
+ return -EINVAL;
+
+ if (local && !machine_name_is_valid(local))
+ return -EINVAL;
+
+ if (i->raw_job)
+ return -EBUSY;
+
+ r = free_and_strdup(&i->local, local);
+ if (r < 0)
+ return r;
+
+ i->force_local = force_local;
+ i->verify = verify;
+ i->settings = settings;
+ i->roothash = roothash;
+
+ /* Queue job for the image itself */
+ r = pull_job_new(&i->raw_job, url, i->glue, i);
+ if (r < 0)
+ return r;
+
+ i->raw_job->on_finished = raw_pull_job_on_finished;
+ i->raw_job->on_open_disk = raw_pull_job_on_open_disk_raw;
+ i->raw_job->on_progress = raw_pull_job_on_progress;
+ i->raw_job->calc_checksum = verify != IMPORT_VERIFY_NO;
+
+ r = pull_find_old_etags(url, i->image_root, DT_REG, ".raw-", ".raw", &i->raw_job->old_etags);
+ if (r < 0)
+ return r;
+
+ if (roothash) {
+ r = pull_make_auxiliary_job(&i->roothash_job, url, raw_strip_suffixes, ".roothash", i->glue, raw_pull_job_on_finished, i);
+ if (r < 0)
+ return r;
+
+ i->roothash_job->on_open_disk = raw_pull_job_on_open_disk_roothash;
+ i->roothash_job->on_progress = raw_pull_job_on_progress;
+ i->roothash_job->calc_checksum = verify != IMPORT_VERIFY_NO;
+ }
+
+ if (settings) {
+ r = pull_make_auxiliary_job(&i->settings_job, url, raw_strip_suffixes, ".nspawn", i->glue, raw_pull_job_on_finished, i);
+ if (r < 0)
+ return r;
+
+ i->settings_job->on_open_disk = raw_pull_job_on_open_disk_settings;
+ i->settings_job->on_progress = raw_pull_job_on_progress;
+ i->settings_job->calc_checksum = verify != IMPORT_VERIFY_NO;
+ }
+
+ r = pull_make_verification_jobs(&i->checksum_job, &i->signature_job, verify, url, i->glue, raw_pull_job_on_finished, i);
+ if (r < 0)
+ return r;
+
+ r = pull_job_begin(i->raw_job);
+ if (r < 0)
+ return r;
+
+ if (i->roothash_job) {
+ r = pull_job_begin(i->roothash_job);
+ if (r < 0)
+ return r;
+ }
+
+ if (i->settings_job) {
+ r = pull_job_begin(i->settings_job);
+ if (r < 0)
+ return r;
+ }
+
+ if (i->checksum_job) {
+ i->checksum_job->on_progress = raw_pull_job_on_progress;
+ i->checksum_job->style = VERIFICATION_PER_FILE;
+
+ r = pull_job_begin(i->checksum_job);
+ if (r < 0)
+ return r;
+ }
+
+ if (i->signature_job) {
+ i->signature_job->on_progress = raw_pull_job_on_progress;
+
+ r = pull_job_begin(i->signature_job);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
diff --git a/src/import/pull-raw.h b/src/import/pull-raw.h
new file mode 100644
index 0000000..4ccd65b
--- /dev/null
+++ b/src/import/pull-raw.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-event.h"
+
+#include "import-util.h"
+#include "macro.h"
+
+typedef struct RawPull RawPull;
+
+typedef void (*RawPullFinished)(RawPull *pull, int error, void *userdata);
+
+int raw_pull_new(RawPull **pull, sd_event *event, const char *image_root, RawPullFinished on_finished, void *userdata);
+RawPull* raw_pull_unref(RawPull *pull);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(RawPull*, raw_pull_unref);
+
+int raw_pull_start(RawPull *pull, const char *url, const char *local, bool force_local, ImportVerify verify, bool settings, bool roothash);
diff --git a/src/import/pull-tar.c b/src/import/pull-tar.c
new file mode 100644
index 0000000..e7a208e
--- /dev/null
+++ b/src/import/pull-tar.c
@@ -0,0 +1,559 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <curl/curl.h>
+#include <sys/prctl.h>
+
+#include "sd-daemon.h"
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "copy.h"
+#include "curl-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "hostname-util.h"
+#include "import-common.h"
+#include "import-util.h"
+#include "macro.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "pull-common.h"
+#include "pull-job.h"
+#include "pull-tar.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "utf8.h"
+#include "util.h"
+#include "web-util.h"
+
+typedef enum TarProgress {
+ TAR_DOWNLOADING,
+ TAR_VERIFYING,
+ TAR_FINALIZING,
+ TAR_COPYING,
+} TarProgress;
+
+struct TarPull {
+ sd_event *event;
+ CurlGlue *glue;
+
+ char *image_root;
+
+ PullJob *tar_job;
+ PullJob *settings_job;
+ PullJob *checksum_job;
+ PullJob *signature_job;
+
+ TarPullFinished on_finished;
+ void *userdata;
+
+ char *local;
+ bool force_local;
+ bool settings;
+
+ pid_t tar_pid;
+
+ char *final_path;
+ char *temp_path;
+
+ char *settings_path;
+ char *settings_temp_path;
+
+ ImportVerify verify;
+};
+
+TarPull* tar_pull_unref(TarPull *i) {
+ if (!i)
+ return NULL;
+
+ if (i->tar_pid > 1) {
+ (void) kill_and_sigcont(i->tar_pid, SIGKILL);
+ (void) wait_for_terminate(i->tar_pid, NULL);
+ }
+
+ pull_job_unref(i->tar_job);
+ pull_job_unref(i->settings_job);
+ pull_job_unref(i->checksum_job);
+ pull_job_unref(i->signature_job);
+
+ curl_glue_unref(i->glue);
+ sd_event_unref(i->event);
+
+ if (i->temp_path) {
+ (void) rm_rf(i->temp_path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+ free(i->temp_path);
+ }
+
+ if (i->settings_temp_path) {
+ (void) unlink(i->settings_temp_path);
+ free(i->settings_temp_path);
+ }
+
+ free(i->final_path);
+ free(i->settings_path);
+ free(i->image_root);
+ free(i->local);
+
+ return mfree(i);
+}
+
+int tar_pull_new(
+ TarPull **ret,
+ sd_event *event,
+ const char *image_root,
+ TarPullFinished on_finished,
+ void *userdata) {
+
+ _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(tar_pull_unrefp) TarPull *i = NULL;
+ _cleanup_free_ char *root = NULL;
+ int r;
+
+ assert(ret);
+
+ root = strdup(image_root ?: "/var/lib/machines");
+ if (!root)
+ return -ENOMEM;
+
+ if (event)
+ e = sd_event_ref(event);
+ else {
+ r = sd_event_default(&e);
+ if (r < 0)
+ return r;
+ }
+
+ r = curl_glue_new(&g, e);
+ if (r < 0)
+ return r;
+
+ i = new(TarPull, 1);
+ if (!i)
+ return -ENOMEM;
+
+ *i = (TarPull) {
+ .on_finished = on_finished,
+ .userdata = userdata,
+ .image_root = TAKE_PTR(root),
+ .event = TAKE_PTR(e),
+ .glue = TAKE_PTR(g),
+ };
+
+ i->glue->on_finished = pull_job_curl_on_finished;
+ i->glue->userdata = i;
+
+ *ret = TAKE_PTR(i);
+
+ return 0;
+}
+
+static void tar_pull_report_progress(TarPull *i, TarProgress p) {
+ unsigned percent;
+
+ assert(i);
+
+ switch (p) {
+
+ case TAR_DOWNLOADING: {
+ unsigned remain = 85;
+
+ percent = 0;
+
+ if (i->settings_job) {
+ percent += i->settings_job->progress_percent * 5 / 100;
+ remain -= 5;
+ }
+
+ if (i->checksum_job) {
+ percent += i->checksum_job->progress_percent * 5 / 100;
+ remain -= 5;
+ }
+
+ if (i->signature_job) {
+ percent += i->signature_job->progress_percent * 5 / 100;
+ remain -= 5;
+ }
+
+ if (i->tar_job)
+ percent += i->tar_job->progress_percent * remain / 100;
+ break;
+ }
+
+ case TAR_VERIFYING:
+ percent = 85;
+ break;
+
+ case TAR_FINALIZING:
+ percent = 90;
+ break;
+
+ case TAR_COPYING:
+ percent = 95;
+ break;
+
+ default:
+ assert_not_reached("Unknown progress state");
+ }
+
+ sd_notifyf(false, "X_IMPORT_PROGRESS=%u", percent);
+ log_debug("Combined progress %u%%", percent);
+}
+
+static int tar_pull_determine_path(TarPull *i, const char *suffix, char **field) {
+ int r;
+
+ assert(i);
+ assert(field);
+
+ if (*field)
+ return 0;
+
+ assert(i->tar_job);
+
+ r = pull_make_path(i->tar_job->url, i->tar_job->etag, i->image_root, ".tar-", suffix, field);
+ if (r < 0)
+ return log_oom();
+
+ return 1;
+}
+
+static int tar_pull_make_local_copy(TarPull *i) {
+ int r;
+
+ assert(i);
+ assert(i->tar_job);
+
+ if (!i->local)
+ return 0;
+
+ r = pull_make_local_copy(i->final_path, i->image_root, i->local, i->force_local);
+ if (r < 0)
+ return r;
+
+ if (i->settings) {
+ const char *local_settings;
+ assert(i->settings_job);
+
+ r = tar_pull_determine_path(i, ".nspawn", &i->settings_path);
+ if (r < 0)
+ return r;
+
+ local_settings = strjoina(i->image_root, "/", i->local, ".nspawn");
+
+ r = copy_file_atomic(i->settings_path, local_settings, 0664, 0, COPY_REFLINK | (i->force_local ? COPY_REPLACE : 0));
+ if (r == -EEXIST)
+ log_warning_errno(r, "Settings file %s already exists, not replacing.", local_settings);
+ else if (r == -ENOENT)
+ log_debug_errno(r, "Skipping creation of settings file, since none was found.");
+ else if (r < 0)
+ log_warning_errno(r, "Failed to copy settings files %s, ignoring: %m", local_settings);
+ else
+ log_info("Created new settings file %s.", local_settings);
+ }
+
+ return 0;
+}
+
+static bool tar_pull_is_done(TarPull *i) {
+ assert(i);
+ assert(i->tar_job);
+
+ if (!PULL_JOB_IS_COMPLETE(i->tar_job))
+ return false;
+ if (i->settings_job && !PULL_JOB_IS_COMPLETE(i->settings_job))
+ return false;
+ if (i->checksum_job && !PULL_JOB_IS_COMPLETE(i->checksum_job))
+ return false;
+ if (i->signature_job && !PULL_JOB_IS_COMPLETE(i->signature_job))
+ return false;
+
+ return true;
+}
+
+static void tar_pull_job_on_finished(PullJob *j) {
+ TarPull *i;
+ int r;
+
+ assert(j);
+ assert(j->userdata);
+
+ i = j->userdata;
+
+ if (j == i->settings_job) {
+ if (j->error != 0)
+ log_info_errno(j->error, "Settings file could not be retrieved, proceeding without.");
+ } else if (j->error != 0 && j != i->signature_job) {
+ if (j == i->checksum_job)
+ log_error_errno(j->error, "Failed to retrieve SHA256 checksum, cannot verify. (Try --verify=no?)");
+ else
+ log_error_errno(j->error, "Failed to retrieve image file. (Wrong URL?)");
+
+ r = j->error;
+ goto finish;
+ }
+
+ /* This is invoked if either the download completed
+ * successfully, or the download was skipped because we
+ * already have the etag. */
+
+ if (!tar_pull_is_done(i))
+ return;
+
+ if (i->signature_job && i->checksum_job->style == VERIFICATION_PER_DIRECTORY && i->signature_job->error != 0) {
+ log_error_errno(j->error, "Failed to retrieve signature file, cannot verify. (Try --verify=no?)");
+
+ r = i->signature_job->error;
+ goto finish;
+ }
+
+ i->tar_job->disk_fd = safe_close(i->tar_job->disk_fd);
+ if (i->settings_job)
+ i->settings_job->disk_fd = safe_close(i->settings_job->disk_fd);
+
+ r = tar_pull_determine_path(i, NULL, &i->final_path);
+ if (r < 0)
+ goto finish;
+
+ if (i->tar_pid > 0) {
+ r = wait_for_terminate_and_check("tar", i->tar_pid, WAIT_LOG);
+ i->tar_pid = 0;
+ if (r < 0)
+ goto finish;
+ if (r != EXIT_SUCCESS) {
+ r = -EIO;
+ goto finish;
+ }
+ }
+
+ if (!i->tar_job->etag_exists) {
+ /* This is a new download, verify it, and move it into place */
+
+ tar_pull_report_progress(i, TAR_VERIFYING);
+
+ r = pull_verify(i->tar_job, NULL, i->settings_job, i->checksum_job, i->signature_job);
+ if (r < 0)
+ goto finish;
+
+ tar_pull_report_progress(i, TAR_FINALIZING);
+
+ r = import_make_read_only(i->temp_path);
+ if (r < 0)
+ goto finish;
+
+ r = rename_noreplace(AT_FDCWD, i->temp_path, AT_FDCWD, i->final_path);
+ if (r < 0) {
+ log_error_errno(r, "Failed to rename to final image name to %s: %m", i->final_path);
+ goto finish;
+ }
+
+ i->temp_path = mfree(i->temp_path);
+
+ if (i->settings_job &&
+ i->settings_job->error == 0) {
+
+ /* Also move the settings file into place, if it exists. Note that we do so only if we also
+ * moved the tar file in place, to keep things strictly in sync. */
+ assert(i->settings_temp_path);
+
+ /* Regenerate final name for this auxiliary file, we might know the etag of the file now, and
+ * we should incorporate it in the file name if we can */
+ i->settings_path = mfree(i->settings_path);
+
+ r = tar_pull_determine_path(i, ".nspawn", &i->settings_path);
+ if (r < 0)
+ goto finish;
+
+ r = import_make_read_only(i->settings_temp_path);
+ if (r < 0)
+ goto finish;
+
+ r = rename_noreplace(AT_FDCWD, i->settings_temp_path, AT_FDCWD, i->settings_path);
+ if (r < 0) {
+ log_error_errno(r, "Failed to rename settings file to %s: %m", i->settings_path);
+ goto finish;
+ }
+
+ i->settings_temp_path = mfree(i->settings_temp_path);
+ }
+ }
+
+ tar_pull_report_progress(i, TAR_COPYING);
+
+ r = tar_pull_make_local_copy(i);
+ if (r < 0)
+ goto finish;
+
+ r = 0;
+
+finish:
+ if (i->on_finished)
+ i->on_finished(i, r, i->userdata);
+ else
+ sd_event_exit(i->event, r);
+}
+
+static int tar_pull_job_on_open_disk_tar(PullJob *j) {
+ TarPull *i;
+ int r;
+
+ assert(j);
+ assert(j->userdata);
+
+ i = j->userdata;
+ assert(i->tar_job == j);
+ assert(i->tar_pid <= 0);
+
+ if (!i->temp_path) {
+ r = tempfn_random_child(i->image_root, "tar", &i->temp_path);
+ if (r < 0)
+ return log_oom();
+ }
+
+ mkdir_parents_label(i->temp_path, 0700);
+
+ r = btrfs_subvol_make(i->temp_path);
+ if (r == -ENOTTY) {
+ if (mkdir(i->temp_path, 0755) < 0)
+ return log_error_errno(errno, "Failed to create directory %s: %m", i->temp_path);
+ } else if (r < 0)
+ return log_error_errno(r, "Failed to create subvolume %s: %m", i->temp_path);
+ else
+ (void) import_assign_pool_quota_and_warn(i->temp_path);
+
+ j->disk_fd = import_fork_tar_x(i->temp_path, &i->tar_pid);
+ if (j->disk_fd < 0)
+ return j->disk_fd;
+
+ return 0;
+}
+
+static int tar_pull_job_on_open_disk_settings(PullJob *j) {
+ TarPull *i;
+ int r;
+
+ assert(j);
+ assert(j->userdata);
+
+ i = j->userdata;
+ assert(i->settings_job == j);
+
+ if (!i->settings_temp_path) {
+ r = tempfn_random_child(i->image_root, "settings", &i->settings_temp_path);
+ if (r < 0)
+ return log_oom();
+ }
+
+ mkdir_parents_label(i->settings_temp_path, 0700);
+
+ j->disk_fd = open(i->settings_temp_path, O_RDWR|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0664);
+ if (j->disk_fd < 0)
+ return log_error_errno(errno, "Failed to create %s: %m", i->settings_temp_path);
+
+ return 0;
+}
+
+static void tar_pull_job_on_progress(PullJob *j) {
+ TarPull *i;
+
+ assert(j);
+ assert(j->userdata);
+
+ i = j->userdata;
+
+ tar_pull_report_progress(i, TAR_DOWNLOADING);
+}
+
+int tar_pull_start(
+ TarPull *i,
+ const char *url,
+ const char *local,
+ bool force_local,
+ ImportVerify verify,
+ bool settings) {
+
+ int r;
+
+ assert(i);
+ assert(verify < _IMPORT_VERIFY_MAX);
+ assert(verify >= 0);
+
+ if (!http_url_is_valid(url))
+ return -EINVAL;
+
+ if (local && !machine_name_is_valid(local))
+ return -EINVAL;
+
+ if (i->tar_job)
+ return -EBUSY;
+
+ r = free_and_strdup(&i->local, local);
+ if (r < 0)
+ return r;
+
+ i->force_local = force_local;
+ i->verify = verify;
+ i->settings = settings;
+
+ /* Set up download job for TAR file */
+ r = pull_job_new(&i->tar_job, url, i->glue, i);
+ if (r < 0)
+ return r;
+
+ i->tar_job->on_finished = tar_pull_job_on_finished;
+ i->tar_job->on_open_disk = tar_pull_job_on_open_disk_tar;
+ i->tar_job->on_progress = tar_pull_job_on_progress;
+ i->tar_job->calc_checksum = verify != IMPORT_VERIFY_NO;
+
+ r = pull_find_old_etags(url, i->image_root, DT_DIR, ".tar-", NULL, &i->tar_job->old_etags);
+ if (r < 0)
+ return r;
+
+ /* Set up download job for the settings file (.nspawn) */
+ if (settings) {
+ r = pull_make_auxiliary_job(&i->settings_job, url, tar_strip_suffixes, ".nspawn", i->glue, tar_pull_job_on_finished, i);
+ if (r < 0)
+ return r;
+
+ i->settings_job->on_open_disk = tar_pull_job_on_open_disk_settings;
+ i->settings_job->on_progress = tar_pull_job_on_progress;
+ i->settings_job->calc_checksum = verify != IMPORT_VERIFY_NO;
+ }
+
+ /* Set up download of checksum/signature files */
+ r = pull_make_verification_jobs(&i->checksum_job, &i->signature_job, verify, url, i->glue, tar_pull_job_on_finished, i);
+ if (r < 0)
+ return r;
+
+ r = pull_job_begin(i->tar_job);
+ if (r < 0)
+ return r;
+
+ if (i->settings_job) {
+ r = pull_job_begin(i->settings_job);
+ if (r < 0)
+ return r;
+ }
+
+ if (i->checksum_job) {
+ i->checksum_job->on_progress = tar_pull_job_on_progress;
+ i->checksum_job->style = VERIFICATION_PER_FILE;
+
+ r = pull_job_begin(i->checksum_job);
+ if (r < 0)
+ return r;
+ }
+
+ if (i->signature_job) {
+ i->signature_job->on_progress = tar_pull_job_on_progress;
+
+ r = pull_job_begin(i->signature_job);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
diff --git a/src/import/pull-tar.h b/src/import/pull-tar.h
new file mode 100644
index 0000000..76f920e
--- /dev/null
+++ b/src/import/pull-tar.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-event.h"
+
+#include "import-util.h"
+#include "macro.h"
+
+typedef struct TarPull TarPull;
+
+typedef void (*TarPullFinished)(TarPull *pull, int error, void *userdata);
+
+int tar_pull_new(TarPull **pull, sd_event *event, const char *image_root, TarPullFinished on_finished, void *userdata);
+TarPull* tar_pull_unref(TarPull *pull);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(TarPull*, tar_pull_unref);
+
+int tar_pull_start(TarPull *pull, const char *url, const char *local, bool force_local, ImportVerify verify, bool settings);
diff --git a/src/import/pull.c b/src/import/pull.c
new file mode 100644
index 0000000..3376992
--- /dev/null
+++ b/src/import/pull.c
@@ -0,0 +1,333 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <getopt.h>
+
+#include "sd-event.h"
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "hostname-util.h"
+#include "import-util.h"
+#include "machine-image.h"
+#include "main-func.h"
+#include "parse-util.h"
+#include "pull-raw.h"
+#include "pull-tar.h"
+#include "signal-util.h"
+#include "string-util.h"
+#include "verbs.h"
+#include "web-util.h"
+
+static bool arg_force = false;
+static const char *arg_image_root = "/var/lib/machines";
+static ImportVerify arg_verify = IMPORT_VERIFY_SIGNATURE;
+static bool arg_settings = true;
+static bool arg_roothash = true;
+
+static int interrupt_signal_handler(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ log_notice("Transfer aborted.");
+ sd_event_exit(sd_event_source_get_event(s), EINTR);
+ return 0;
+}
+
+static void on_tar_finished(TarPull *pull, int error, void *userdata) {
+ sd_event *event = userdata;
+ assert(pull);
+
+ if (error == 0)
+ log_info("Operation completed successfully.");
+
+ sd_event_exit(event, abs(error));
+}
+
+static int pull_tar(int argc, char *argv[], void *userdata) {
+ _cleanup_(tar_pull_unrefp) TarPull *pull = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ const char *url, *local;
+ _cleanup_free_ char *l = NULL, *ll = NULL;
+ int r;
+
+ url = argv[1];
+ if (!http_url_is_valid(url)) {
+ log_error("URL '%s' is not valid.", url);
+ return -EINVAL;
+ }
+
+ if (argc >= 3)
+ local = argv[2];
+ else {
+ r = import_url_last_component(url, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed get final component of URL: %m");
+
+ local = l;
+ }
+
+ if (isempty(local) || streq(local, "-"))
+ local = NULL;
+
+ if (local) {
+ r = tar_strip_suffixes(local, &ll);
+ if (r < 0)
+ return log_oom();
+
+ local = ll;
+
+ if (!machine_name_is_valid(local)) {
+ log_error("Local image name '%s' is not valid.", local);
+ return -EINVAL;
+ }
+
+ if (!arg_force) {
+ r = image_find(IMAGE_MACHINE, local, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
+ } else {
+ log_error("Image '%s' already exists.", local);
+ return -EEXIST;
+ }
+ }
+
+ log_info("Pulling '%s', saving as '%s'.", url, local);
+ } else
+ log_info("Pulling '%s'.", url);
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+ (void) sd_event_add_signal(event, NULL, SIGTERM, interrupt_signal_handler, NULL);
+ (void) sd_event_add_signal(event, NULL, SIGINT, interrupt_signal_handler, NULL);
+
+ r = tar_pull_new(&pull, event, arg_image_root, on_tar_finished, event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate puller: %m");
+
+ r = tar_pull_start(pull, url, local, arg_force, arg_verify, arg_settings);
+ if (r < 0)
+ return log_error_errno(r, "Failed to pull image: %m");
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ log_info("Exiting.");
+ return -r;
+}
+
+static void on_raw_finished(RawPull *pull, int error, void *userdata) {
+ sd_event *event = userdata;
+ assert(pull);
+
+ if (error == 0)
+ log_info("Operation completed successfully.");
+
+ sd_event_exit(event, abs(error));
+}
+
+static int pull_raw(int argc, char *argv[], void *userdata) {
+ _cleanup_(raw_pull_unrefp) RawPull *pull = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ const char *url, *local;
+ _cleanup_free_ char *l = NULL, *ll = NULL;
+ int r;
+
+ url = argv[1];
+ if (!http_url_is_valid(url)) {
+ log_error("URL '%s' is not valid.", url);
+ return -EINVAL;
+ }
+
+ if (argc >= 3)
+ local = argv[2];
+ else {
+ r = import_url_last_component(url, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed get final component of URL: %m");
+
+ local = l;
+ }
+
+ if (isempty(local) || streq(local, "-"))
+ local = NULL;
+
+ if (local) {
+ r = raw_strip_suffixes(local, &ll);
+ if (r < 0)
+ return log_oom();
+
+ local = ll;
+
+ if (!machine_name_is_valid(local)) {
+ log_error("Local image name '%s' is not valid.", local);
+ return -EINVAL;
+ }
+
+ if (!arg_force) {
+ r = image_find(IMAGE_MACHINE, local, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
+ } else {
+ log_error("Image '%s' already exists.", local);
+ return -EEXIST;
+ }
+ }
+
+ log_info("Pulling '%s', saving as '%s'.", url, local);
+ } else
+ log_info("Pulling '%s'.", url);
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+ (void) sd_event_add_signal(event, NULL, SIGTERM, interrupt_signal_handler, NULL);
+ (void) sd_event_add_signal(event, NULL, SIGINT, interrupt_signal_handler, NULL);
+
+ r = raw_pull_new(&pull, event, arg_image_root, on_raw_finished, event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate puller: %m");
+
+ r = raw_pull_start(pull, url, local, arg_force, arg_verify, arg_settings, arg_roothash);
+ if (r < 0)
+ return log_error_errno(r, "Failed to pull image: %m");
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ log_info("Exiting.");
+ return -r;
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+
+ printf("%s [OPTIONS...] {COMMAND} ...\n\n"
+ "Download container or virtual machine images.\n\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --force Force creation of image\n"
+ " --verify=MODE Verify downloaded image, one of: 'no',\n"
+ " 'checksum', 'signature'\n"
+ " --settings=BOOL Download settings file with image\n"
+ " --roothash=BOOL Download root hash file with image\n"
+ " --image-root=PATH Image root directory\n\n"
+ "Commands:\n"
+ " tar URL [NAME] Download a TAR image\n"
+ " raw URL [NAME] Download a RAW image\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_FORCE,
+ ARG_IMAGE_ROOT,
+ ARG_VERIFY,
+ ARG_SETTINGS,
+ ARG_ROOTHASH,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "force", no_argument, NULL, ARG_FORCE },
+ { "image-root", required_argument, NULL, ARG_IMAGE_ROOT },
+ { "verify", required_argument, NULL, ARG_VERIFY },
+ { "settings", required_argument, NULL, ARG_SETTINGS },
+ { "roothash", required_argument, NULL, ARG_ROOTHASH },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ return help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_FORCE:
+ arg_force = true;
+ break;
+
+ case ARG_IMAGE_ROOT:
+ arg_image_root = optarg;
+ break;
+
+ case ARG_VERIFY:
+ arg_verify = import_verify_from_string(optarg);
+ if (arg_verify < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid verification setting '%s'", optarg);
+
+ break;
+
+ case ARG_SETTINGS:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --settings= parameter '%s': %m", optarg);
+
+ arg_settings = r;
+ break;
+
+ case ARG_ROOTHASH:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --roothash= parameter '%s': %m", optarg);
+
+ arg_roothash = r;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+
+ return 1;
+}
+
+static int pull_main(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "tar", 2, 3, 0, pull_tar },
+ { "raw", 2, 3, 0, pull_raw },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ setlocale(LC_ALL, "");
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ (void) ignore_signals(SIGPIPE, -1);
+
+ return pull_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/import/qcow2-util.c b/src/import/qcow2-util.c
new file mode 100644
index 0000000..e927b60
--- /dev/null
+++ b/src/import/qcow2-util.c
@@ -0,0 +1,334 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <zlib.h>
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "qcow2-util.h"
+#include "sparse-endian.h"
+#include "util.h"
+
+#define QCOW2_MAGIC 0x514649fb
+
+#define QCOW2_COPIED (1ULL << 63)
+#define QCOW2_COMPRESSED (1ULL << 62)
+#define QCOW2_ZERO (1ULL << 0)
+
+typedef struct _packed_ Header {
+ be32_t magic;
+ be32_t version;
+
+ be64_t backing_file_offset;
+ be32_t backing_file_size;
+
+ be32_t cluster_bits;
+ be64_t size;
+ be32_t crypt_method;
+
+ be32_t l1_size;
+ be64_t l1_table_offset;
+
+ be64_t refcount_table_offset;
+ be32_t refcount_table_clusters;
+
+ be32_t nb_snapshots;
+ be64_t snapshots_offset;
+
+ /* The remainder is only present on QCOW3 */
+ be64_t incompatible_features;
+ be64_t compatible_features;
+ be64_t autoclear_features;
+
+ be32_t refcount_order;
+ be32_t header_length;
+} Header;
+
+#define HEADER_MAGIC(header) be32toh((header)->magic)
+#define HEADER_VERSION(header) be32toh((header)->version)
+#define HEADER_CLUSTER_BITS(header) be32toh((header)->cluster_bits)
+#define HEADER_CLUSTER_SIZE(header) (1ULL << HEADER_CLUSTER_BITS(header))
+#define HEADER_L2_BITS(header) (HEADER_CLUSTER_BITS(header) - 3)
+#define HEADER_SIZE(header) be64toh((header)->size)
+#define HEADER_CRYPT_METHOD(header) be32toh((header)->crypt_method)
+#define HEADER_L1_SIZE(header) be32toh((header)->l1_size)
+#define HEADER_L2_SIZE(header) (HEADER_CLUSTER_SIZE(header)/sizeof(uint64_t))
+#define HEADER_L1_TABLE_OFFSET(header) be64toh((header)->l1_table_offset)
+
+static uint32_t HEADER_HEADER_LENGTH(const Header *h) {
+ if (HEADER_VERSION(h) < 3)
+ return offsetof(Header, incompatible_features);
+
+ return be32toh(h->header_length);
+}
+
+static int copy_cluster(
+ int sfd, uint64_t soffset,
+ int dfd, uint64_t doffset,
+ uint64_t cluster_size,
+ void *buffer) {
+
+ ssize_t l;
+ int r;
+
+ r = btrfs_clone_range(sfd, soffset, dfd, doffset, cluster_size);
+ if (r >= 0)
+ return r;
+
+ l = pread(sfd, buffer, cluster_size, soffset);
+ if (l < 0)
+ return -errno;
+ if ((uint64_t) l != cluster_size)
+ return -EIO;
+
+ l = pwrite(dfd, buffer, cluster_size, doffset);
+ if (l < 0)
+ return -errno;
+ if ((uint64_t) l != cluster_size)
+ return -EIO;
+
+ return 0;
+}
+
+static int decompress_cluster(
+ int sfd, uint64_t soffset,
+ int dfd, uint64_t doffset,
+ uint64_t compressed_size,
+ uint64_t cluster_size,
+ void *buffer1,
+ void *buffer2) {
+
+ _cleanup_free_ void *large_buffer = NULL;
+ z_stream s = {};
+ uint64_t sz;
+ ssize_t l;
+ int r;
+
+ if (compressed_size > cluster_size) {
+ /* The usual cluster buffer doesn't suffice, let's
+ * allocate a larger one, temporarily */
+
+ large_buffer = malloc(compressed_size);
+ if (!large_buffer)
+ return -ENOMEM;
+
+ buffer1 = large_buffer;
+ }
+
+ l = pread(sfd, buffer1, compressed_size, soffset);
+ if (l < 0)
+ return -errno;
+ if ((uint64_t) l != compressed_size)
+ return -EIO;
+
+ s.next_in = buffer1;
+ s.avail_in = compressed_size;
+ s.next_out = buffer2;
+ s.avail_out = cluster_size;
+
+ r = inflateInit2(&s, -12);
+ if (r != Z_OK)
+ return -EIO;
+
+ r = inflate(&s, Z_FINISH);
+ sz = (uint8_t*) s.next_out - (uint8_t*) buffer2;
+ inflateEnd(&s);
+ if (r != Z_STREAM_END || sz != cluster_size)
+ return -EIO;
+
+ l = pwrite(dfd, buffer2, cluster_size, doffset);
+ if (l < 0)
+ return -errno;
+ if ((uint64_t) l != cluster_size)
+ return -EIO;
+
+ return 0;
+}
+
+static int normalize_offset(
+ const Header *header,
+ uint64_t p,
+ uint64_t *ret,
+ bool *compressed,
+ uint64_t *compressed_size) {
+
+ uint64_t q;
+
+ q = be64toh(p);
+
+ if (q & QCOW2_COMPRESSED) {
+ uint64_t sz, csize_shift, csize_mask;
+
+ if (!compressed)
+ return -EOPNOTSUPP;
+
+ csize_shift = 64 - 2 - (HEADER_CLUSTER_BITS(header) - 8);
+ csize_mask = (1ULL << (HEADER_CLUSTER_BITS(header) - 8)) - 1;
+ sz = (((q >> csize_shift) & csize_mask) + 1) * 512 - (q & 511);
+ q &= ((1ULL << csize_shift) - 1);
+
+ if (compressed_size)
+ *compressed_size = sz;
+
+ *compressed = true;
+
+ } else {
+ if (compressed) {
+ *compressed = false;
+ *compressed_size = 0;
+ }
+
+ if (q & QCOW2_ZERO) {
+ /* We make no distinction between zero blocks and holes */
+ *ret = 0;
+ return 0;
+ }
+
+ q &= ~QCOW2_COPIED;
+ }
+
+ *ret = q;
+ return q > 0; /* returns positive if not a hole */
+}
+
+static int verify_header(const Header *header) {
+ assert(header);
+
+ if (HEADER_MAGIC(header) != QCOW2_MAGIC)
+ return -EBADMSG;
+
+ if (!IN_SET(HEADER_VERSION(header), 2, 3))
+ return -EOPNOTSUPP;
+
+ if (HEADER_CRYPT_METHOD(header) != 0)
+ return -EOPNOTSUPP;
+
+ if (HEADER_CLUSTER_BITS(header) < 9) /* 512K */
+ return -EBADMSG;
+
+ if (HEADER_CLUSTER_BITS(header) > 21) /* 2MB */
+ return -EBADMSG;
+
+ if (HEADER_SIZE(header) % HEADER_CLUSTER_SIZE(header) != 0)
+ return -EBADMSG;
+
+ if (HEADER_L1_SIZE(header) > 32*1024*1024) /* 32MB */
+ return -EBADMSG;
+
+ if (HEADER_VERSION(header) == 3) {
+
+ if (header->incompatible_features != 0)
+ return -EOPNOTSUPP;
+
+ if (HEADER_HEADER_LENGTH(header) < sizeof(Header))
+ return -EBADMSG;
+ }
+
+ return 0;
+}
+
+int qcow2_convert(int qcow2_fd, int raw_fd) {
+ _cleanup_free_ void *buffer1 = NULL, *buffer2 = NULL;
+ _cleanup_free_ be64_t *l1_table = NULL, *l2_table = NULL;
+ uint64_t sz, i;
+ Header header;
+ ssize_t l;
+ int r;
+
+ l = pread(qcow2_fd, &header, sizeof(header), 0);
+ if (l < 0)
+ return -errno;
+ if (l != sizeof(header))
+ return -EIO;
+
+ r = verify_header(&header);
+ if (r < 0)
+ return r;
+
+ l1_table = new(be64_t, HEADER_L1_SIZE(&header));
+ if (!l1_table)
+ return -ENOMEM;
+
+ l2_table = malloc(HEADER_CLUSTER_SIZE(&header));
+ if (!l2_table)
+ return -ENOMEM;
+
+ buffer1 = malloc(HEADER_CLUSTER_SIZE(&header));
+ if (!buffer1)
+ return -ENOMEM;
+
+ buffer2 = malloc(HEADER_CLUSTER_SIZE(&header));
+ if (!buffer2)
+ return -ENOMEM;
+
+ /* Empty the file if it exists, we rely on zero bits */
+ if (ftruncate(raw_fd, 0) < 0)
+ return -errno;
+
+ if (ftruncate(raw_fd, HEADER_SIZE(&header)) < 0)
+ return -errno;
+
+ sz = sizeof(uint64_t) * HEADER_L1_SIZE(&header);
+ l = pread(qcow2_fd, l1_table, sz, HEADER_L1_TABLE_OFFSET(&header));
+ if (l < 0)
+ return -errno;
+ if ((uint64_t) l != sz)
+ return -EIO;
+
+ for (i = 0; i < HEADER_L1_SIZE(&header); i ++) {
+ uint64_t l2_begin, j;
+
+ r = normalize_offset(&header, l1_table[i], &l2_begin, NULL, NULL);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ l = pread(qcow2_fd, l2_table, HEADER_CLUSTER_SIZE(&header), l2_begin);
+ if (l < 0)
+ return -errno;
+ if ((uint64_t) l != HEADER_CLUSTER_SIZE(&header))
+ return -EIO;
+
+ for (j = 0; j < HEADER_L2_SIZE(&header); j++) {
+ uint64_t data_begin, p, compressed_size;
+ bool compressed;
+
+ p = ((i << HEADER_L2_BITS(&header)) + j) << HEADER_CLUSTER_BITS(&header);
+
+ r = normalize_offset(&header, l2_table[j], &data_begin, &compressed, &compressed_size);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ if (compressed)
+ r = decompress_cluster(
+ qcow2_fd, data_begin,
+ raw_fd, p,
+ compressed_size, HEADER_CLUSTER_SIZE(&header),
+ buffer1, buffer2);
+ else
+ r = copy_cluster(
+ qcow2_fd, data_begin,
+ raw_fd, p,
+ HEADER_CLUSTER_SIZE(&header), buffer1);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ return 0;
+}
+
+int qcow2_detect(int fd) {
+ be32_t id;
+ ssize_t l;
+
+ l = pread(fd, &id, sizeof(id), 0);
+ if (l < 0)
+ return -errno;
+ if (l != sizeof(id))
+ return -EIO;
+
+ return htobe32(QCOW2_MAGIC) == id;
+}
diff --git a/src/import/qcow2-util.h b/src/import/qcow2-util.h
new file mode 100644
index 0000000..7393d98
--- /dev/null
+++ b/src/import/qcow2-util.h
@@ -0,0 +1,5 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+int qcow2_detect(int fd);
+int qcow2_convert(int qcow2_fd, int raw_fd);
diff --git a/src/import/test-qcow2.c b/src/import/test-qcow2.c
new file mode 100644
index 0000000..bd2b458
--- /dev/null
+++ b/src/import/test-qcow2.c
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "fd-util.h"
+#include "log.h"
+#include "qcow2-util.h"
+#include "util.h"
+
+int main(int argc, char *argv[]) {
+ _cleanup_close_ int sfd = -1, dfd = -1;
+ int r;
+
+ if (argc != 3) {
+ log_error("Needs two arguments.");
+ return EXIT_FAILURE;
+ }
+
+ sfd = open(argv[1], O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (sfd < 0) {
+ log_error_errno(errno, "Can't open source file: %m");
+ return EXIT_FAILURE;
+ }
+
+ dfd = open(argv[2], O_WRONLY|O_CREAT|O_CLOEXEC|O_NOCTTY, 0666);
+ if (dfd < 0) {
+ log_error_errno(errno, "Can't open destination file: %m");
+ return EXIT_FAILURE;
+ }
+
+ r = qcow2_convert(sfd, dfd);
+ if (r < 0) {
+ log_error_errno(r, "Failed to unpack: %m");
+ return EXIT_FAILURE;
+ }
+
+ return EXIT_SUCCESS;
+}