diff --git a/README.md b/README.md deleted file mode 100644 index 7342728d557c602f51c6d278bba9f3dd9faaf356..0000000000000000000000000000000000000000 --- a/README.md +++ /dev/null @@ -1,11 +0,0 @@ -Anolis OS -======================================= -# 代码仓库说明 -## 分支说明 ->进行代码开发工作时,请注意选择当前版本对应的分支 -* aX分支为对应大版本的主分支,如a8分支对应当前最新版本 -* aX.Y分支为对应小版本的维护分支,如a8.2分支对应8.2版本 -## 开发流程 -1. 首先fork目标分支到自己的namespace -2. 在自己的fork分支上做出修改 -3. 向对应的仓库中提交merge request,源分支为fork分支 diff --git a/flatpak-1.15.8.tar.xz b/flatpak-1.15.8.tar.xz deleted file mode 100644 index cf0c399d20a483004b057f2b495c0318e6a77a2e..0000000000000000000000000000000000000000 Binary files a/flatpak-1.15.8.tar.xz and /dev/null differ diff --git a/flatpak-1.16.0.tar.xz b/flatpak-1.16.0.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..e2caabe28f3208869f19a29269d3bb4556b951d9 Binary files /dev/null and b/flatpak-1.16.0.tar.xz differ diff --git a/flatpak-add-support-for-preinstalling-flatpaks.patch b/flatpak-add-support-for-preinstalling-flatpaks.patch new file mode 100644 index 0000000000000000000000000000000000000000..e1e1314e2ef0068c43e1d251017031425fbc059b --- /dev/null +++ b/flatpak-add-support-for-preinstalling-flatpaks.patch @@ -0,0 +1,1815 @@ +From 4d171a522dfef4039adea792f8ff856036f14686 Mon Sep 17 00:00:00 2001 +From: Kalev Lember +Date: Tue, 16 Apr 2024 12:18:59 +0200 +Subject: Add initial support for preinstalling flatpaks + +This adds new FlatpakTransaction API, and a new top level CLI command to +preinstall flatpaks, that is to install flatpaks that are considered +part of the operating system. + +A new drop-in directory /etc/flatpak/preinstall.d/ allows configuring +what apps should be preinstalled, and a new flatpak preinstall command +installs and removes apps based on the current configuration. + +A drop-in loupe.preinstall file can look something like this: + +[Flatpak Preinstall org.gnome.Loupe] +Branch=stable +IsRuntime=false + +The corresponding API is flatpak_transaction_add_sync_preinstalled() +which can be implemented by GUI clients to drive the actual installs +on system startup. + +Resolves: https://github.com/flatpak/flatpak/issues/5579 +Co-authored-by: Sebastian Wick +--- + app/flatpak-builtins-preinstall.c | 156 ++++++++++ + app/flatpak-builtins.h | 1 + + app/flatpak-main.c | 1 + + app/meson.build | 1 + + common/flatpak-dir-private.h | 23 +- + common/flatpak-dir.c | 406 +++++++++++++++++++++++++- + common/flatpak-installation.c | 2 +- + common/flatpak-transaction.c | 187 ++++++++++-- + common/flatpak-transaction.h | 3 + + doc/flatpak-docs.xml.in | 1 + + doc/flatpak-preinstall.xml | 276 +++++++++++++++++ + doc/meson.build | 1 + + po/POTFILES.in | 1 + + system-helper/flatpak-system-helper.c | 5 +- + tests/libtest.sh | 1 + + tests/test-basic.sh | 2 +- + tests/test-matrix/meson.build | 1 + + tests/test-preinstall.sh | 226 ++++++++++++++ + 18 files changed, 1264 insertions(+), 30 deletions(-) + create mode 100644 app/flatpak-builtins-preinstall.c + create mode 100644 doc/flatpak-preinstall.xml + create mode 100755 tests/test-preinstall.sh + +diff --git a/app/flatpak-builtins-preinstall.c b/app/flatpak-builtins-preinstall.c +new file mode 100644 +index 00000000..3d52cb8b +--- /dev/null ++++ b/app/flatpak-builtins-preinstall.c +@@ -0,0 +1,156 @@ ++/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s: ++ * Copyright © 2024 Red Hat, Inc ++ * ++ * This program 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. ++ * ++ * This library is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library. If not, see . ++ * ++ * Authors: ++ * Kalev Lember ++ */ ++ ++#include "config.h" ++ ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include ++ ++#include "libglnx.h" ++ ++#include "flatpak-builtins.h" ++#include "flatpak-builtins-utils.h" ++#include "flatpak-transaction-private.h" ++#include "flatpak-cli-transaction.h" ++#include "flatpak-quiet-transaction.h" ++#include "flatpak-utils-http-private.h" ++#include "flatpak-utils-private.h" ++#include "flatpak-error.h" ++#include "flatpak-chain-input-stream-private.h" ++ ++static char **opt_sideload_repos; ++static gboolean opt_no_pull; ++static gboolean opt_no_deploy; ++static gboolean opt_no_related; ++static gboolean opt_no_deps; ++static gboolean opt_no_static_deltas; ++static gboolean opt_include_sdk; ++static gboolean opt_include_debug; ++static gboolean opt_yes; ++static gboolean opt_reinstall; ++static gboolean opt_noninteractive; ++ ++static GOptionEntry options[] = { ++ { "no-pull", 0, 0, G_OPTION_ARG_NONE, &opt_no_pull, N_("Don't pull, only install from local cache"), NULL }, ++ { "no-deploy", 0, 0, G_OPTION_ARG_NONE, &opt_no_deploy, N_("Don't deploy, only download to local cache"), NULL }, ++ { "no-related", 0, 0, G_OPTION_ARG_NONE, &opt_no_related, N_("Don't install related refs"), NULL }, ++ { "no-deps", 0, 0, G_OPTION_ARG_NONE, &opt_no_deps, N_("Don't verify/install runtime dependencies"), NULL }, ++ { "no-static-deltas", 0, 0, G_OPTION_ARG_NONE, &opt_no_static_deltas, N_("Don't use static deltas"), NULL }, ++ { "include-sdk", 0, 0, G_OPTION_ARG_NONE, &opt_include_sdk, N_("Additionally install the SDK used to build the given refs") }, ++ { "include-debug", 0, 0, G_OPTION_ARG_NONE, &opt_include_debug, N_("Additionally install the debug info for the given refs and their dependencies") }, ++ { "assumeyes", 'y', 0, G_OPTION_ARG_NONE, &opt_yes, N_("Automatically answer yes for all questions"), NULL }, ++ { "reinstall", 0, 0, G_OPTION_ARG_NONE, &opt_reinstall, N_("Uninstall first if already installed"), NULL }, ++ { "noninteractive", 0, 0, G_OPTION_ARG_NONE, &opt_noninteractive, N_("Produce minimal output and don't ask questions"), NULL }, ++ /* Translators: A sideload is when you install from a local USB drive rather than the Internet. */ ++ { "sideload-repo", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_sideload_repos, N_("Use this local repo for sideloads"), N_("PATH") }, ++ { NULL } ++}; ++ ++gboolean ++flatpak_builtin_preinstall (int argc, char **argv, GCancellable *cancellable, GError **error) ++{ ++ g_autoptr(GOptionContext) context = NULL; ++ g_autoptr(GPtrArray) dirs = NULL; ++ g_autoptr(FlatpakDir) dir = NULL; ++ g_autoptr(FlatpakTransaction) transaction = NULL; ++ ++ context = g_option_context_new (_("- Install flatpaks that are part of the operating system")); ++ g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); ++ ++ if (!flatpak_option_context_parse (context, options, &argc, &argv, ++ FLATPAK_BUILTIN_FLAG_ALL_DIRS | FLATPAK_BUILTIN_FLAG_OPTIONAL_REPO, ++ &dirs, cancellable, error)) ++ return FALSE; ++ ++ /* Use the default dir */ ++ dir = g_object_ref (g_ptr_array_index (dirs, 0)); ++ ++ if (opt_noninteractive) ++ opt_yes = TRUE; /* Implied */ ++ ++ if (opt_noninteractive) ++ transaction = flatpak_quiet_transaction_new (dir, error); ++ else ++ transaction = flatpak_cli_transaction_new (dir, opt_yes, TRUE, FALSE, error); ++ if (transaction == NULL) ++ return FALSE; ++ ++ flatpak_transaction_set_no_pull (transaction, opt_no_pull); ++ flatpak_transaction_set_no_deploy (transaction, opt_no_deploy); ++ flatpak_transaction_set_disable_static_deltas (transaction, opt_no_static_deltas); ++ flatpak_transaction_set_disable_dependencies (transaction, opt_no_deps); ++ flatpak_transaction_set_disable_related (transaction, opt_no_related); ++ flatpak_transaction_set_reinstall (transaction, opt_reinstall); ++ flatpak_transaction_set_auto_install_sdk (transaction, opt_include_sdk); ++ flatpak_transaction_set_auto_install_debug (transaction, opt_include_debug); ++ ++ for (int i = 0; opt_sideload_repos != NULL && opt_sideload_repos[i] != NULL; i++) ++ flatpak_transaction_add_sideload_repo (transaction, opt_sideload_repos[i]); ++ ++ if (!flatpak_transaction_add_sync_preinstalled (transaction, error)) ++ return FALSE; ++ ++ if (flatpak_transaction_is_empty (transaction)) ++ { ++ g_print (_("Nothing to do.\n")); ++ ++ return TRUE; ++ } ++ ++ if (!flatpak_transaction_run (transaction, cancellable, error)) ++ { ++ if (g_error_matches (*error, FLATPAK_ERROR, FLATPAK_ERROR_ABORTED)) ++ g_clear_error (error); /* Don't report on stderr */ ++ ++ return FALSE; ++ } ++ ++ return TRUE; ++} ++ ++gboolean ++flatpak_complete_preinstall (FlatpakCompletion *completion) ++{ ++ g_autoptr(GOptionContext) context = NULL; ++ g_autoptr(GPtrArray) dirs = NULL; ++ ++ context = g_option_context_new (""); ++ if (!flatpak_option_context_parse (context, options, &completion->argc, &completion->argv, ++ FLATPAK_BUILTIN_FLAG_ONE_DIR | FLATPAK_BUILTIN_FLAG_OPTIONAL_REPO, ++ &dirs, NULL, NULL)) ++ return FALSE; ++ ++ switch (completion->argc) ++ { ++ default: /* REF */ ++ flatpak_complete_options (completion, global_entries); ++ flatpak_complete_options (completion, options); ++ flatpak_complete_options (completion, user_entries); ++ break; ++ } ++ ++ return TRUE; ++} +diff --git a/app/flatpak-builtins.h b/app/flatpak-builtins.h +index 32a9c952..20eb2272 100644 +--- a/app/flatpak-builtins.h ++++ b/app/flatpak-builtins.h +@@ -127,6 +127,7 @@ BUILTINPROTO (repair) + BUILTINPROTO (create_usb) + BUILTINPROTO (kill) + BUILTINPROTO (history) ++BUILTINPROTO (preinstall) + + #undef BUILTINPROTO + +diff --git a/app/flatpak-main.c b/app/flatpak-main.c +index 45498895..d5794d99 100644 +--- a/app/flatpak-main.c ++++ b/app/flatpak-main.c +@@ -89,6 +89,7 @@ static FlatpakCommand commands[] = { + { "config", N_("Configure flatpak"), flatpak_builtin_config, flatpak_complete_config }, + { "repair", N_("Repair flatpak installation"), flatpak_builtin_repair, flatpak_complete_repair }, + { "create-usb", N_("Put applications or runtimes onto removable media"), flatpak_builtin_create_usb, flatpak_complete_create_usb }, ++ { "preinstall", N_("Install flatpaks that are part of the operating system"), flatpak_builtin_preinstall, flatpak_complete_preinstall }, + + /* translators: please keep the leading newline and space */ + { N_("\n Find applications and runtimes") }, +diff --git a/app/meson.build b/app/meson.build +index 258d582a..8e6ef1dd 100644 +--- a/app/meson.build ++++ b/app/meson.build +@@ -100,6 +100,7 @@ sources = [ + 'flatpak-builtins-permission-set.c', + 'flatpak-builtins-permission-show.c', + 'flatpak-builtins-pin.c', ++ 'flatpak-builtins-preinstall.c', + 'flatpak-builtins-ps.c', + 'flatpak-builtins-remote-add.c', + 'flatpak-builtins-remote-delete.c', +diff --git a/common/flatpak-dir-private.h b/common/flatpak-dir-private.h +index ffcff5ff..2c75890c 100644 +--- a/common/flatpak-dir-private.h ++++ b/common/flatpak-dir-private.h +@@ -187,6 +187,7 @@ typedef enum { + FLATPAK_HELPER_DEPLOY_FLAGS_APP_HINT = 1 << 5, + FLATPAK_HELPER_DEPLOY_FLAGS_INSTALL_HINT = 1 << 6, + FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PINNED = 1 << 7, ++ FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PREINSTALLED = 1 << 8, + } FlatpakHelperDeployFlags; + + #define FLATPAK_HELPER_DEPLOY_FLAGS_ALL (FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE | \ +@@ -196,18 +197,21 @@ typedef enum { + FLATPAK_HELPER_DEPLOY_FLAGS_NO_INTERACTION | \ + FLATPAK_HELPER_DEPLOY_FLAGS_APP_HINT | \ + FLATPAK_HELPER_DEPLOY_FLAGS_INSTALL_HINT | \ +- FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PINNED) ++ FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PINNED | \ ++ FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PREINSTALLED) + + typedef enum { + FLATPAK_HELPER_UNINSTALL_FLAGS_NONE = 0, + FLATPAK_HELPER_UNINSTALL_FLAGS_KEEP_REF = 1 << 0, + FLATPAK_HELPER_UNINSTALL_FLAGS_FORCE_REMOVE = 1 << 1, + FLATPAK_HELPER_UNINSTALL_FLAGS_NO_INTERACTION = 1 << 2, ++ FLATPAK_HELPER_UNINSTALL_FLAGS_UPDATE_PREINSTALLED = 1 << 3, + } FlatpakHelperUninstallFlags; + + #define FLATPAK_HELPER_UNINSTALL_FLAGS_ALL (FLATPAK_HELPER_UNINSTALL_FLAGS_KEEP_REF | \ + FLATPAK_HELPER_UNINSTALL_FLAGS_FORCE_REMOVE | \ +- FLATPAK_HELPER_UNINSTALL_FLAGS_NO_INTERACTION) ++ FLATPAK_HELPER_UNINSTALL_FLAGS_NO_INTERACTION | \ ++ FLATPAK_HELPER_UNINSTALL_FLAGS_UPDATE_PREINSTALLED) + + typedef enum { + FLATPAK_HELPER_CONFIGURE_REMOTE_FLAGS_NONE = 0, +@@ -371,6 +375,19 @@ gboolean flatpak_remove_override_keyfile (const char *app_id, + gboolean user, + GError **error); + ++typedef struct ++{ ++ FlatpakDecomposed *ref; ++ char *collection_id; ++} FlatpakPreinstallConfig; ++ ++GPtrArray * flatpak_get_preinstall_config (const char *default_arch, ++ GCancellable *cancellable, ++ GError **error); ++gboolean flatpak_dir_uninitialized_mark_preinstalled (FlatpakDir *self, ++ const GPtrArray *preinstall_config, ++ GError **error); ++ + int flatpak_deploy_data_get_version (GBytes *deploy_data); + const char * flatpak_deploy_data_get_origin (GBytes *deploy_data); + const char * flatpak_deploy_data_get_commit (GBytes *deploy_data); +@@ -687,6 +704,7 @@ gboolean flatpak_dir_deploy_install (Fla + const char **previous_ids, + gboolean reinstall, + gboolean pin_on_deploy, ++ gboolean update_preinstalled_on_deploy, + GCancellable *cancellable, + GError **error); + gboolean flatpak_dir_install (FlatpakDir *self, +@@ -696,6 +714,7 @@ gboolean flatpak_dir_install (Fla + gboolean reinstall, + gboolean app_hint, + gboolean pin_on_deploy, ++ gboolean update_preinstalled_on_deploy, + FlatpakRemoteState *state, + FlatpakDecomposed *ref, + const char *opt_commit, +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index 1c304525..6300f3cc 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -86,6 +86,16 @@ + #define FLATPAK_REMOTES_DIR "remotes.d" + #define FLATPAK_REMOTES_FILE_EXT ".flatpakrepo" + ++#define FLATPAK_PREINSTALL_DIR "preinstall.d" ++#define FLATPAK_PREINSTALL_FILE_EXT ".preinstall" ++ ++#define FLATPAK_PREINSTALL_GROUP_PREFIX "Flatpak Preinstall " ++#define FLATPAK_PREINSTALL_IS_RUNTIME_KEY "IsRuntime" ++#define FLATPAK_PREINSTALL_NAME_KEY "Name" ++#define FLATPAK_PREINSTALL_BRANCH_KEY "Branch" ++#define FLATPAK_PREINSTALL_COLLECTION_ID_KEY "CollectionID" ++#define FLATPAK_PREINSTALL_INSTALL_KEY "Install" ++ + #define SIDELOAD_REPOS_DIR_NAME "sideload-repos" + + #define FLATPAK_TRIGGERS_DIR "triggers" +@@ -2043,6 +2053,381 @@ get_system_locations (GCancellable *cancellable, + return g_steal_pointer (&locations); + } + ++typedef struct ++{ ++ char *name; ++ char *branch; ++ gboolean is_runtime; ++ char *collection_id; ++ gboolean install; ++} PreinstallConfig; ++ ++static PreinstallConfig * ++preinstall_config_new (const char *name) ++{ ++ PreinstallConfig *config = g_new0 (PreinstallConfig, 1); ++ ++ config->name = g_strdup (name); ++ config->branch = g_strdup ("master"); ++ config->install = TRUE; ++ ++ return config; ++} ++ ++static void ++preinstall_config_free (PreinstallConfig *config) ++{ ++ g_clear_pointer (&config->name, g_free); ++ g_clear_pointer (&config->branch, g_free); ++ g_clear_pointer (&config->collection_id, g_free); ++ g_free (config); ++} ++ ++G_DEFINE_AUTOPTR_CLEANUP_FUNC (PreinstallConfig, preinstall_config_free) ++ ++static void ++flatpak_preinstall_config_free (FlatpakPreinstallConfig *preinstall) ++{ ++ g_clear_pointer (&preinstall->ref, flatpak_decomposed_unref); ++ g_clear_pointer (&preinstall->collection_id, g_free); ++ g_free (preinstall); ++} ++ ++G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakPreinstallConfig, flatpak_preinstall_config_free) ++ ++static void ++flatpak_parse_preinstall_config_file (GKeyFile *keyfile, ++ GHashTable *configs) ++{ ++ g_auto(GStrv) groups = NULL; ++ ++ groups = g_key_file_get_groups (keyfile, NULL); ++ ++ for (int i = 0; groups[i] != NULL; i++) ++ { ++ const char *group_name = groups[i]; ++ const char *name; ++ g_autoptr(PreinstallConfig) config = NULL; ++ g_autoptr(GError) local_error = NULL; ++ g_autofree char *owned_name = NULL; ++ g_autofree char *branch = NULL; ++ gboolean is_runtime = FALSE; ++ g_autofree char *collection_id = NULL; ++ gboolean install = TRUE; ++ ++ if (!g_str_has_prefix (group_name, FLATPAK_PREINSTALL_GROUP_PREFIX) || ++ *(group_name + strlen (FLATPAK_PREINSTALL_GROUP_PREFIX)) == '\0') ++ { ++ g_info ("Skipping unknown group %s", group_name); ++ continue; ++ } ++ ++ name = group_name + strlen (FLATPAK_PREINSTALL_GROUP_PREFIX); ++ ++ if (!g_hash_table_steal_extended (configs, name, ++ (gpointer *)&owned_name, ++ (gpointer *)&config)) ++ { ++ config = preinstall_config_new (name); ++ owned_name = g_strdup (name); ++ } ++ ++ branch = g_key_file_get_string (keyfile, ++ group_name, ++ FLATPAK_PREINSTALL_BRANCH_KEY, ++ NULL); ++ if (branch) ++ { ++ g_clear_pointer (&config->branch, g_free); ++ if (*branch != '\0') ++ config->branch = g_steal_pointer (&branch); ++ } ++ ++ is_runtime = g_key_file_get_boolean (keyfile, ++ group_name, ++ FLATPAK_PREINSTALL_IS_RUNTIME_KEY, ++ &local_error); ++ if (!local_error) ++ { ++ config->is_runtime = is_runtime; ++ } ++ else if (!g_error_matches (local_error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) ++ { ++ g_info ("Invalid file format, %s is not a boolean", ++ FLATPAK_PREINSTALL_IS_RUNTIME_KEY); ++ continue; ++ } ++ g_clear_error (&local_error); ++ ++ collection_id = g_key_file_get_string (keyfile, ++ group_name, ++ FLATPAK_PREINSTALL_COLLECTION_ID_KEY, ++ NULL); ++ if (collection_id) ++ { ++ g_clear_pointer (&config->collection_id, g_free); ++ if (*collection_id != '\0') ++ config->collection_id = g_steal_pointer (&collection_id); ++ } ++ ++ install = g_key_file_get_boolean (keyfile, ++ group_name, ++ FLATPAK_PREINSTALL_INSTALL_KEY, ++ &local_error); ++ if (!local_error) ++ { ++ config->install = install; ++ } ++ else if (!g_error_matches (local_error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) ++ { ++ g_info ("Invalid file format, %s is not a boolean", ++ FLATPAK_PREINSTALL_INSTALL_KEY); ++ continue; ++ } ++ g_clear_error (&local_error); ++ ++ g_hash_table_insert (configs, ++ g_steal_pointer (&owned_name), ++ g_steal_pointer (&config)); ++ } ++} ++ ++typedef struct ++{ ++ char *name; ++ GFile *file; ++} PreinstallConfigFile; ++ ++static gint ++preinstall_config_file_sort (gconstpointer a, ++ gconstpointer b) ++{ ++ const PreinstallConfigFile *ca = a; ++ const PreinstallConfigFile *cb = b; ++ ++ return g_strcmp0 (ca->name, cb->name); ++} ++ ++static void ++preinstall_config_file_free (PreinstallConfigFile *preinstall_config_file) ++{ ++ g_clear_pointer (&preinstall_config_file->name, g_free); ++ g_clear_object (&preinstall_config_file->file); ++ g_free (preinstall_config_file); ++} ++ ++G_DEFINE_AUTOPTR_CLEANUP_FUNC (PreinstallConfigFile, preinstall_config_file_free); ++ ++static gboolean ++scan_preinstall_config_files (const char *config_dir, ++ GHashTable *configs, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(GFile) conf_dir = NULL; ++ g_autoptr(GFileEnumerator) dir_enum = NULL; ++ g_autoptr(GPtrArray) config_files = NULL; ++ g_autoptr(GError) local_error = NULL; ++ ++ if (!g_file_test (config_dir, G_FILE_TEST_IS_DIR)) ++ { ++ g_info ("Skipping missing preinstall config directory %s", config_dir); ++ return TRUE; ++ } ++ ++ conf_dir = g_file_new_for_path (config_dir); ++ dir_enum = g_file_enumerate_children (conf_dir, ++ G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, ++ G_FILE_QUERY_INFO_NONE, ++ cancellable, ++ &local_error); ++ if (local_error != NULL) ++ { ++ g_info ("Unexpected error retrieving preinstalls from %s: %s", ++ config_dir, ++ local_error->message); ++ ++ g_propagate_error (error, g_steal_pointer (&local_error)); ++ return FALSE; ++ } ++ ++ config_files = g_ptr_array_new_with_free_func ((GDestroyNotify)preinstall_config_file_free); ++ ++ while (TRUE) ++ { ++ GFileInfo *file_info; ++ GFile *path; ++ const char *name; ++ guint32 type; ++ g_autoptr(PreinstallConfigFile) config_file = NULL; ++ ++ if (!g_file_enumerator_iterate (dir_enum, ++ &file_info, ++ &path, ++ cancellable, ++ &local_error)) ++ { ++ g_info ("Unexpected error reading file in %s: %s", ++ config_dir, ++ local_error->message); ++ ++ g_propagate_error (error, g_steal_pointer (&local_error)); ++ return FALSE; ++ } ++ ++ if (file_info == NULL) ++ break; ++ ++ name = g_file_info_get_name (file_info); ++ type = g_file_info_get_file_type (file_info); ++ ++ if (type != G_FILE_TYPE_REGULAR || ++ !g_str_has_suffix (name, FLATPAK_PREINSTALL_FILE_EXT)) ++ continue; ++ ++ config_file = g_new0 (PreinstallConfigFile, 1); ++ config_file->name = g_strdup (name); ++ config_file->file = g_object_ref (path); ++ g_ptr_array_add (config_files, g_steal_pointer (&config_file)); ++ } ++ ++ g_ptr_array_sort (config_files, preinstall_config_file_sort); ++ ++ for (int i = 0; i < config_files->len; i++) ++ { ++ PreinstallConfigFile *config_file = g_ptr_array_index (config_files, i); ++ g_autofree char *path = NULL; ++ g_autoptr(GKeyFile) keyfile = NULL; ++ g_autoptr(GError) load_error = NULL; ++ ++ path = g_file_get_path (config_file->file); ++ ++ g_info ("Parsing config file %s", path); ++ ++ keyfile = g_key_file_new (); ++ ++ if (!g_key_file_load_from_file (keyfile, path, G_KEY_FILE_NONE, &load_error)) ++ g_info ("Parsing config file %s failed: %s", path, load_error->message); ++ ++ flatpak_parse_preinstall_config_file (keyfile, configs); ++ } ++ ++ return TRUE; ++} ++ ++GPtrArray * ++flatpak_get_preinstall_config (const char *default_arch, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(GHashTable) configs = NULL; ++ g_autoptr(GPtrArray) preinstalls = NULL; ++ g_autofree char *config_dir = NULL; ++ g_autofree char *data_dir = NULL; ++ GHashTableIter iter; ++ PreinstallConfig *config; ++ ++ configs = g_hash_table_new_full (g_str_hash, g_str_equal, ++ g_free, (GDestroyNotify)preinstall_config_free); ++ ++ /* scan directories in reverse priority order */ ++ data_dir = g_build_filename (get_data_dir_location (), FLATPAK_PREINSTALL_DIR, NULL); ++ if (!scan_preinstall_config_files (data_dir, configs, cancellable, error)) ++ return NULL; ++ ++ config_dir = g_build_filename (get_config_dir_location (), FLATPAK_PREINSTALL_DIR, NULL); ++ if (!scan_preinstall_config_files (config_dir, configs, cancellable, error)) ++ return NULL; ++ ++ preinstalls = g_ptr_array_new_with_free_func ((GDestroyNotify)flatpak_preinstall_config_free); ++ ++ g_hash_table_iter_init (&iter, configs); ++ while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&config)) ++ { ++ g_autoptr(FlatpakPreinstallConfig) preinstall = NULL; ++ g_autoptr(FlatpakDecomposed) ref = NULL; ++ g_autoptr(GError) local_error = NULL; ++ ++ if (!config->install) ++ { ++ g_info ("Skipping preinstall of %s because it is configured to not install", ++ config->name); ++ continue; ++ } ++ ++ ref = flatpak_decomposed_new_from_parts (config->is_runtime ? ++ FLATPAK_KINDS_RUNTIME : ++ FLATPAK_KINDS_APP, ++ config->name, ++ default_arch, ++ config->branch, ++ &local_error); ++ if (ref == NULL) ++ { ++ g_info ("Skipping preinstall of %s because of problems in the configuration: %s", ++ config->name, ++ local_error->message); ++ continue; ++ } ++ ++ preinstall = g_new0 (FlatpakPreinstallConfig, 1); ++ preinstall->ref = g_steal_pointer (&ref); ++ preinstall->collection_id = g_strdup (config->collection_id); ++ ++ g_info ("Found preinstall ref %s", ++ flatpak_decomposed_get_ref (preinstall->ref)); ++ ++ g_ptr_array_add (preinstalls, g_steal_pointer (&preinstall)); ++ } ++ ++ return g_steal_pointer (&preinstalls); ++} ++ ++gboolean ++flatpak_dir_uninitialized_mark_preinstalled (FlatpakDir *self, ++ const GPtrArray *preinstall_config, ++ GError **error) ++{ ++ const char *existing_preinstalls; ++ g_autoptr(GError) local_error = NULL; ++ ++ existing_preinstalls = flatpak_dir_get_config (self, ++ "preinstalled", ++ &local_error); ++ ++ if (existing_preinstalls != NULL) ++ return TRUE; ++ ++ if (!g_error_matches (local_error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) ++ { ++ g_propagate_error (error, g_steal_pointer (&local_error)); ++ return FALSE; ++ } ++ ++ g_clear_error (&local_error); ++ ++ for (int i = 0; i < preinstall_config->len; i++) ++ { ++ const FlatpakPreinstallConfig *config = g_ptr_array_index (preinstall_config, i); ++ GError **append_error = local_error == NULL ? &local_error : NULL; ++ ++ flatpak_dir_config_append_pattern (self, ++ "preinstalled", ++ flatpak_decomposed_get_ref (config->ref), ++ FALSE, ++ NULL, ++ append_error); ++ } ++ ++ if (local_error) ++ { ++ g_propagate_error (error, g_steal_pointer (&local_error)); ++ return FALSE; ++ } ++ ++ return TRUE; ++} ++ + GPtrArray * + flatpak_get_system_base_dir_locations (GCancellable *cancellable, + GError **error) +@@ -9255,6 +9640,7 @@ flatpak_dir_deploy_install (FlatpakDir *self, + const char **previous_ids, + gboolean reinstall, + gboolean pin_on_deploy, ++ gboolean update_preinstalled_on_deploy, + GCancellable *cancellable, + GError **error) + { +@@ -9360,6 +9746,14 @@ flatpak_dir_deploy_install (FlatpakDir *self, + TRUE, NULL, error)) + goto out; + ++ /* Save preinstalled refs to keep the data on what is user installed and what ++ * is automatically installed. */ ++ if (update_preinstalled_on_deploy && ++ !flatpak_dir_config_append_pattern (self, "preinstalled", ++ flatpak_decomposed_get_ref (ref), ++ FALSE, NULL, error)) ++ goto out; ++ + ret = TRUE; + + commit = flatpak_dir_read_active (self, ref, cancellable); +@@ -9960,6 +10354,7 @@ flatpak_dir_install (FlatpakDir *self, + gboolean reinstall, + gboolean app_hint, + gboolean pin_on_deploy, ++ gboolean update_preinstalled_on_deploy, + FlatpakRemoteState *state, + FlatpakDecomposed *ref, + const char *opt_commit, +@@ -10174,6 +10569,9 @@ flatpak_dir_install (FlatpakDir *self, + if (pin_on_deploy) + helper_flags |= FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PINNED; + ++ if (update_preinstalled_on_deploy) ++ helper_flags |= FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PREINSTALLED; ++ + helper_flags |= FLATPAK_HELPER_DEPLOY_FLAGS_INSTALL_HINT; + + if (!flatpak_dir_system_helper_call_deploy (self, +@@ -10212,6 +10610,7 @@ flatpak_dir_install (FlatpakDir *self, + { + if (!flatpak_dir_deploy_install (self, ref, state->remote_name, opt_subpaths, + opt_previous_ids, reinstall, pin_on_deploy, ++ update_preinstalled_on_deploy, + cancellable, error)) + return FALSE; + +@@ -10502,7 +10901,7 @@ flatpak_dir_install_bundle (FlatpakDir *self, + } + else + { +- if (!flatpak_dir_deploy_install (self, ref, remote, NULL, NULL, FALSE, FALSE, cancellable, error)) ++ if (!flatpak_dir_deploy_install (self, ref, remote, NULL, NULL, FALSE, FALSE, FALSE, cancellable, error)) + return FALSE; + } + +@@ -10933,6 +11332,7 @@ flatpak_dir_uninstall (FlatpakDir *self, + g_autoptr(GBytes) deploy_data = NULL; + gboolean keep_ref = flags & FLATPAK_HELPER_UNINSTALL_FLAGS_KEEP_REF; + gboolean force_remove = flags & FLATPAK_HELPER_UNINSTALL_FLAGS_FORCE_REMOVE; ++ gboolean update_preinstalled = flags & FLATPAK_HELPER_UNINSTALL_FLAGS_UPDATE_PREINSTALLED; + + name = flatpak_decomposed_dup_id (ref); + +@@ -11039,6 +11439,10 @@ flatpak_dir_uninstall (FlatpakDir *self, + if (!flatpak_dir_mark_changed (self, error)) + return FALSE; + ++ if (update_preinstalled && ++ !flatpak_dir_config_remove_pattern (self, "preinstalled", flatpak_decomposed_get_ref (ref), error)) ++ return FALSE; ++ + if (!was_deployed) + { + const char *branch = flatpak_decomposed_get_branch (ref); +diff --git a/common/flatpak-installation.c b/common/flatpak-installation.c +index 3a8677e8..be706174 100644 +--- a/common/flatpak-installation.c ++++ b/common/flatpak-installation.c +@@ -1929,7 +1929,7 @@ flatpak_installation_install_full (FlatpakInstallation *self, + (flags & FLATPAK_INSTALL_FLAGS_NO_PULL) != 0, + (flags & FLATPAK_INSTALL_FLAGS_NO_DEPLOY) != 0, + (flags & FLATPAK_INSTALL_FLAGS_NO_STATIC_DELTAS) != 0, +- FALSE, FALSE, FALSE, state, ++ FALSE, FALSE, FALSE, FALSE, state, + ref, NULL, (const char **) subpaths, NULL, NULL, NULL, NULL, NULL, + progress, cancellable, error)) + return NULL; +diff --git a/common/flatpak-transaction.c b/common/flatpak-transaction.c +index b498e4d4..22b7ad8c 100644 +--- a/common/flatpak-transaction.c ++++ b/common/flatpak-transaction.c +@@ -116,6 +116,7 @@ struct _FlatpakTransactionOperation + gboolean skip; + gboolean update_only_deploy; + gboolean pin_on_deploy; ++ gboolean update_preinstalled_on_deploy; + + gboolean resolved; + char *resolved_commit; +@@ -701,7 +702,8 @@ flatpak_transaction_operation_new (const char *remote, + const char *commit, + GFile *bundle, + FlatpakTransactionOperationType kind, +- gboolean pin_on_deploy) ++ gboolean pin_on_deploy, ++ gboolean update_preinstalled_on_deploy) + { + FlatpakTransactionOperation *self; + +@@ -716,6 +718,7 @@ flatpak_transaction_operation_new (const char *remote, + self->bundle = g_object_ref (bundle); + self->kind = kind; + self->pin_on_deploy = pin_on_deploy; ++ self->update_preinstalled_on_deploy = update_preinstalled_on_deploy; + + return self; + } +@@ -2176,7 +2179,8 @@ flatpak_transaction_add_op (FlatpakTransaction *self, + const char *commit, + GFile *bundle, + FlatpakTransactionOperationType kind, +- gboolean pin_on_deploy) ++ gboolean pin_on_deploy, ++ gboolean update_preinstalled_on_deploy) + { + FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self); + FlatpakTransactionOperation *op; +@@ -2206,7 +2210,8 @@ flatpak_transaction_add_op (FlatpakTransaction *self, + } + + op = flatpak_transaction_operation_new (remote, ref, subpaths, previous_ids, +- commit, bundle, kind, pin_on_deploy); ++ commit, bundle, kind, pin_on_deploy, ++ update_preinstalled_on_deploy); + g_hash_table_insert (priv->last_op_for_ref, flatpak_decomposed_ref (ref), op); + + priv->ops = g_list_prepend (priv->ops, op); +@@ -2314,7 +2319,7 @@ add_related (FlatpakTransaction *self, + related_op = flatpak_transaction_add_op (self, rel->remote, rel->ref, + NULL, NULL, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_UNINSTALL, +- FALSE); ++ FALSE, FALSE); + related_op->non_fatal = TRUE; + related_op->fail_if_op_fails = op; + flatpak_transaction_operation_add_related_to_op (related_op, op); +@@ -2343,7 +2348,7 @@ add_related (FlatpakTransaction *self, + (const char **) rel->subpaths, + NULL, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, +- FALSE); ++ FALSE, FALSE); + related_op->non_fatal = TRUE; + related_op->fail_if_op_fails = op; + flatpak_transaction_operation_add_related_to_op (related_op, op); +@@ -2574,7 +2579,7 @@ add_new_dep_op (FlatpakTransaction *self, + return FALSE; + + *dep_op = flatpak_transaction_add_op (self, dep_remote, dep_ref, NULL, NULL, NULL, NULL, +- FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, FALSE); ++ FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, FALSE, FALSE); + } + else + { +@@ -2584,7 +2589,7 @@ add_new_dep_op (FlatpakTransaction *self, + g_info ("Updating dependency %s of %s", flatpak_decomposed_get_pref (dep_ref), + flatpak_decomposed_get_pref (op->ref)); + *dep_op = flatpak_transaction_add_op (self, dep_remote, dep_ref, NULL, NULL, NULL, NULL, +- FLATPAK_TRANSACTION_OPERATION_UPDATE, FALSE); ++ FLATPAK_TRANSACTION_OPERATION_UPDATE, FALSE, FALSE); + (*dep_op)->non_fatal = TRUE; + } + } +@@ -2680,6 +2685,7 @@ flatpak_transaction_add_ref (FlatpakTransaction *self, + FlatpakImageSource *image_source, + const char *external_metadata, + gboolean pin_on_deploy, ++ gboolean update_preinstalled_on_deploy, + FlatpakTransactionOperation **out_op, + GError **error) + { +@@ -2803,7 +2809,8 @@ flatpak_transaction_add_ref (FlatpakTransaction *self, + } + + op = flatpak_transaction_add_op (self, remote, ref, subpaths, previous_ids, +- commit, bundle, kind, pin_on_deploy); ++ commit, bundle, kind, pin_on_deploy, ++ update_preinstalled_on_deploy); + + if (image_source) + op->image_source = g_object_ref (image_source); +@@ -2861,7 +2868,7 @@ flatpak_transaction_add_install (FlatpakTransaction *self, + + if (!flatpak_transaction_add_ref (self, remote, decomposed, subpaths, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_INSTALL, +- NULL, NULL, NULL, pin_on_deploy, NULL, error)) ++ NULL, NULL, NULL, pin_on_deploy, FALSE, NULL, error)) + return FALSE; + + return TRUE; +@@ -2921,7 +2928,9 @@ flatpak_transaction_add_rebase (FlatpakTransaction *self, + if (dir_ref_is_installed (priv->dir, decomposed, &installed_origin, NULL)) + remote = installed_origin; + +- return flatpak_transaction_add_ref (self, remote, decomposed, subpaths, previous_ids, NULL, FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, NULL, NULL, NULL, FALSE, NULL, error); ++ return flatpak_transaction_add_ref (self, remote, decomposed, subpaths, previous_ids, NULL, ++ FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, ++ NULL, NULL, NULL, FALSE, FALSE, NULL, error); + } + + /** +@@ -2997,25 +3006,22 @@ flatpak_transaction_add_rebase_and_uninstall (FlatpakTransaction *self, + if (!flatpak_transaction_add_ref (self, remote, new_decomposed, subpaths, + previous_ids, NULL, + FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, +- NULL, NULL, NULL, FALSE, &rebase_op, error)) ++ NULL, NULL, NULL, FALSE, FALSE, &rebase_op, error)) + return FALSE; + + if (!flatpak_transaction_add_ref (self, NULL, old_decomposed, NULL, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_UNINSTALL, +- NULL, NULL, NULL, FALSE, &uninstall_op, &local_error)) ++ NULL, NULL, NULL, FALSE, FALSE, &uninstall_op, &local_error)) + { + /* If the user is trying to install an eol-rebased app from scratch, the + * @old_ref can’t be uninstalled because it’s not installed already. + * Silently ignore that. */ +- if (g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) +- { +- g_clear_error (&local_error); +- } +- else ++ if (!g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return FALSE; + } ++ g_clear_error (&local_error); + } + + /* Link the ops together so that the install/update is done first, and if +@@ -3119,6 +3125,134 @@ flatpak_transaction_add_install_flatpakref (FlatpakTransaction *self, + return TRUE; + } + ++/** ++ * flatpak_transaction_add_sync_preinstalled: ++ * @self: a #FlatpakTransaction ++ * @error: return location for a #GError ++ * ++ * Adds preinstall operations to this transaction. This can involve both ++ * installing and removing refs, based on /etc/preinstall.d contents and what ++ * the system had preinstalled before. ++ * ++ * Returns: %TRUE on success; %FALSE with @error set on failure. ++ */ ++gboolean ++flatpak_transaction_add_sync_preinstalled (FlatpakTransaction *self, ++ GError **error) ++{ ++ FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self); ++ g_autoptr(GPtrArray) install_refs = g_ptr_array_new_with_free_func (g_free); ++ g_autoptr(GPtrArray) preinstalled_refs = NULL; ++ g_auto(GStrv) remotes = NULL; ++ g_autoptr(GPtrArray) configs = NULL; ++ ++ remotes = flatpak_dir_list_remotes (priv->dir, NULL, error); ++ if (remotes == NULL) ++ return FALSE; ++ ++ preinstalled_refs = flatpak_dir_get_config_patterns (priv->dir, "preinstalled"); ++ ++ configs = flatpak_get_preinstall_config (priv->default_arch, NULL, error); ++ if (configs == NULL) ++ return FALSE; ++ ++ /* If the system has not had any apps pre-installed (i.e. the key in the ++ * config is missing) we mark all apps we would pre-install as pre-installed. ++ * This makes sure we will uninstall them when the config says that they no ++ * longer should be installed. */ ++ if (!flatpak_dir_uninitialized_mark_preinstalled (priv->dir, configs, NULL)) ++ g_message (_("Warning: Could not mark already installed apps as preinstalled")); ++ ++ /* Find preinstalls that should get installed */ ++ for (int i = 0; i < configs->len; i++) ++ { ++ const FlatpakPreinstallConfig *config = g_ptr_array_index (configs, i); ++ ++ /* Store for later */ ++ g_ptr_array_add (install_refs, flatpak_decomposed_dup_ref (config->ref)); ++ ++ /* Skip over if it's listed as previously preinstalled - it's now under ++ * user's control and we no longer install it again, even if the user ++ * manually removes it. */ ++ if (!priv->reinstall && ++ flatpak_g_ptr_array_contains_string (preinstalled_refs, ++ flatpak_decomposed_get_ref (config->ref))) ++ { ++ g_info ("Preinstall ref %s is marked as already preinstalled; skipping", ++ flatpak_decomposed_get_ref (config->ref)); ++ continue; ++ } ++ ++ for (int j = 0; remotes[j] != NULL; j++) ++ { ++ const char *remote = remotes[j]; ++ g_autoptr(GError) local_error = NULL; ++ g_autofree char *remote_collection_id = NULL; ++ ++ if (flatpak_dir_get_remote_disabled (priv->dir, remote)) ++ continue; ++ ++ remote_collection_id = flatpak_dir_get_remote_collection_id (priv->dir, ++ remote); ++ ++ /* Choose the first match if the collection ID was not specified */ ++ if (config->collection_id != NULL && ++ g_strcmp0 (remote_collection_id, config->collection_id) != 0) ++ continue; ++ ++ g_info ("Adding preinstall of %s from remote %s", ++ flatpak_decomposed_get_ref (config->ref), ++ remote); ++ ++ if (!flatpak_transaction_add_ref (self, remote, config->ref, NULL, NULL, NULL, ++ FLATPAK_TRANSACTION_OPERATION_INSTALL, ++ NULL, NULL, NULL, FALSE, TRUE, NULL, ++ &local_error)) ++ { ++ g_info ("Failed to add preinstall ref %s: %s", ++ flatpak_decomposed_get_ref (config->ref), ++ local_error->message); ++ } ++ } ++ } ++ ++ /* Find previously preinstalled refs that are no longer in the preinstall ++ * list and should now get uninstalled */ ++ for (int i = 0; i < preinstalled_refs->len; i++) ++ { ++ const char *ref = g_ptr_array_index (preinstalled_refs, i); ++ ++ /* No longer in the preinstall.d list, so uninstall */ ++ if (!flatpak_g_ptr_array_contains_string (install_refs, ref)) ++ { ++ g_autoptr(GError) local_error = NULL; ++ g_autoptr(FlatpakDecomposed) decomposed = NULL; ++ ++ decomposed = flatpak_decomposed_new_from_ref (ref, error); ++ if (decomposed == NULL) ++ return FALSE; ++ ++ g_info ("Preinstalled ref %s is no longer listed as wanted in preinstall.d config; uninstalling", ++ flatpak_decomposed_get_ref (decomposed)); ++ ++ if (!flatpak_transaction_add_ref (self, NULL, decomposed, NULL, NULL, NULL, ++ FLATPAK_TRANSACTION_OPERATION_UNINSTALL, ++ NULL, NULL, NULL, FALSE, TRUE, NULL, ++ &local_error)) ++ { ++ if (!g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) ++ { ++ g_propagate_error (error, g_steal_pointer (&local_error)); ++ return FALSE; ++ } ++ g_clear_error (&local_error); ++ } ++ } ++ } ++ ++ return TRUE; ++} ++ + /** + * flatpak_transaction_add_update: + * @self: a #FlatpakTransaction +@@ -3154,7 +3288,7 @@ flatpak_transaction_add_update (FlatpakTransaction *self, + return FALSE; + + /* Note: we implement the merge when subpaths == NULL in flatpak_transaction_add_ref() */ +- return flatpak_transaction_add_ref (self, NULL, decomposed, subpaths, NULL, commit, FLATPAK_TRANSACTION_OPERATION_UPDATE, NULL, NULL, NULL, FALSE, NULL, error); ++ return flatpak_transaction_add_ref (self, NULL, decomposed, subpaths, NULL, commit, FLATPAK_TRANSACTION_OPERATION_UPDATE, NULL, NULL, NULL, FALSE, FALSE, NULL, error); + } + + /** +@@ -3181,7 +3315,7 @@ flatpak_transaction_add_uninstall (FlatpakTransaction *self, + if (decomposed == NULL) + return FALSE; + +- return flatpak_transaction_add_ref (self, NULL, decomposed, NULL, NULL, NULL, FLATPAK_TRANSACTION_OPERATION_UNINSTALL, NULL, NULL, NULL, FALSE, NULL, error); ++ return flatpak_transaction_add_ref (self, NULL, decomposed, NULL, NULL, NULL, FLATPAK_TRANSACTION_OPERATION_UNINSTALL, NULL, NULL, NULL, FALSE, FALSE, NULL, error); + } + + static gboolean +@@ -3293,7 +3427,7 @@ flatpak_transaction_add_auto_install (FlatpakTransaction *self, + + if (!flatpak_transaction_add_ref (self, remote, auto_install_ref, NULL, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, +- NULL, NULL, NULL, FALSE, NULL, ++ NULL, NULL, NULL, FALSE, FALSE, NULL, + &local_error)) + g_info ("Failed to add auto-install ref %s: %s", flatpak_decomposed_get_ref (auto_install_ref), + local_error->message); +@@ -4808,7 +4942,7 @@ flatpak_transaction_resolve_bundles (FlatpakTransaction *self, + + if (!flatpak_transaction_add_ref (self, remote, ref, NULL, NULL, commit, + FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE, +- data->file, NULL, metadata, FALSE, NULL, error)) ++ data->file, NULL, metadata, FALSE, FALSE, NULL, error)) + return FALSE; + } + +@@ -4875,7 +5009,8 @@ flatpak_transaction_resolve_images (FlatpakTransaction *self, + + if (!flatpak_transaction_add_ref (self, remote, ref, NULL, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_INSTALL, +- NULL, image_source, metadata_label, FALSE, &op, error)) ++ NULL, image_source, metadata_label, FALSE, FALSE, ++ &op, error)) + return FALSE; + } + +@@ -4947,6 +5082,7 @@ _run_op_kind (FlatpakTransaction *self, + priv->reinstall, + priv->max_op >= APP_UPDATE, + op->pin_on_deploy, ++ op->update_preinstalled_on_deploy, + remote_state, op->ref, + op->resolved_commit, + (const char **) op->subpaths, +@@ -4986,7 +5122,7 @@ _run_op_kind (FlatpakTransaction *self, + if (flatpak_decomposed_is_app (op->ref)) + *out_needs_triggers = TRUE; + +- if (op->pin_on_deploy) ++ if (op->pin_on_deploy|| op->update_preinstalled_on_deploy) + *out_needs_cache_drop = TRUE; + } + } +@@ -5091,6 +5227,9 @@ _run_op_kind (FlatpakTransaction *self, + if (priv->force_uninstall) + flags |= FLATPAK_HELPER_UNINSTALL_FLAGS_FORCE_REMOVE; + ++ if (op->update_preinstalled_on_deploy) ++ flags |= FLATPAK_HELPER_UNINSTALL_FLAGS_UPDATE_PREINSTALLED; ++ + emit_new_op (self, op, progress); + + res = flatpak_dir_uninstall (priv->dir, op->ref, flags, +@@ -5271,7 +5410,7 @@ add_uninstall_unused_ops (FlatpakTransaction *self, + uninstall_op = flatpak_transaction_add_op (self, origin, unused_ref, + NULL, NULL, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_UNINSTALL, +- FALSE); ++ FALSE, FALSE); + run_operation_last (uninstall_op); + } + } +diff --git a/common/flatpak-transaction.h b/common/flatpak-transaction.h +index dfa3d039..ae305c34 100644 +--- a/common/flatpak-transaction.h ++++ b/common/flatpak-transaction.h +@@ -339,6 +339,9 @@ gboolean flatpak_transaction_add_install_flatpakref (FlatpakTransacti + GBytes *flatpakref_data, + GError **error); + FLATPAK_EXTERN ++gboolean flatpak_transaction_add_sync_preinstalled (FlatpakTransaction *self, ++ GError **error); ++FLATPAK_EXTERN + gboolean flatpak_transaction_add_update (FlatpakTransaction *self, + const char *ref, + const char **subpaths, +diff --git a/doc/flatpak-docs.xml.in b/doc/flatpak-docs.xml.in +index 581bf685..4de8d1f9 100644 +--- a/doc/flatpak-docs.xml.in ++++ b/doc/flatpak-docs.xml.in +@@ -52,6 +52,7 @@ + + + ++ + + + +diff --git a/doc/flatpak-preinstall.xml b/doc/flatpak-preinstall.xml +new file mode 100644 +index 00000000..b774c618 +--- /dev/null ++++ b/doc/flatpak-preinstall.xml +@@ -0,0 +1,276 @@ ++ ++ ++ ++ ++ ++ ++ flatpak preinstall ++ flatpak ++ ++ ++ ++ Developer ++ Kalev ++ Lember ++ klember@redhat.com ++ ++ ++ ++ ++ ++ flatpak preinstall ++ 1 ++ ++ ++ ++ flatpak-preinstall ++ Install flatpaks that are part of the operating system ++ ++ ++ ++ ++ flatpak preinstall ++ OPTION ++ ++ ++ ++ ++ Description ++ ++ ++ This command manages flatpaks that are part of the operating system. If no options are given, running flatpak preinstall will synchronize (install and remove) flatpaks to match the set that the OS vendor has chosen. ++ ++ ++ ++ Preinstalled flatpaks are defined by dropping .preinstall files into the directories /usr/share/flatpak/preinstall.d/ and /etc/flatpak/preinstall.d/. The OS runs flatpak preinstall -y (or its GUI equivalent) on system startup, which then does the actual installation. ++ ++ ++ ++ This system allows the OS vendor to define the list of flatpaks that are installed together with the OS, and also makes it possible for the OS vendor to make changes to the list in the future, which is then applied once flatpak preinstall is run next time. ++ ++ Users can opt out of preinstalled flatpaks by simply uninstalling them, at which point they won't get automatically reinstalled again. ++ ++ ++ ++ ++ File format ++ ++ ++ The .preinstall file is using the same .ini file format that is used for systemd unit files or application .desktop files. ++ ++ ++ ++ [Flatpak Preinstall NAME] ++ ++ ++ The NAME is the fully qualified name of the runtime or application. All the information for a single runtime or application is contained in one [Flatpak Preinstall NAME] group. Multiple groups can be defined in a single file. ++ ++ ++ The following keys can be present in this group: ++ ++ ++ ++ (boolean) ++ ++ Whether this group should be installed. If this key is not specified, the group will be installed. ++ ++ ++ ++ (string) ++ ++ The name of the branch from which to install the application or runtime. If this key is not specified, the "master" branch is used. ++ ++ ++ ++ (boolean) ++ ++ Whether this group refers to a runtime. If this key is not specified, the group is assumed to refer to an application. ++ ++ ++ ++ (string) ++ ++ The collection ID of the remote to use, if it has one. ++ ++ ++ ++ ++ ++ ++ ++ Example ++ ++[Flatpak Preinstall org.gnome.Loupe] ++Branch=stable ++IsRuntime=false ++ ++ ++ ++ ++ Options ++ ++ The following options are understood: ++ ++ ++ ++ ++ ++ ++ ++ Show help options and exit. ++ ++ ++ ++ ++ ++ ++ ++ Uninstall first if already installed. ++ ++ ++ ++ ++ ++ ++ ++ ++ Install the application or runtime in a per-user installation. ++ ++ ++ ++ ++ ++ ++ ++ Install the application or runtime in the default system-wide installation. ++ ++ ++ ++ ++ ++ ++ ++ Install the application or runtime in a system-wide installation ++ specified by NAME among those defined in ++ /etc/flatpak/installations.d/. Using ++ is equivalent to using ++ . ++ ++ ++ ++ ++ ++ ++ ++ Download the latest version, but don't deploy it. ++ ++ ++ ++ ++ ++ ++ ++ Don't download the latest version, deploy whatever is locally available. ++ ++ ++ ++ ++ ++ ++ ++ Don't download related extensions, such as the locale data. ++ ++ ++ ++ ++ ++ ++ ++ Don't verify runtime dependencies when installing. ++ ++ ++ ++ ++ ++ ++ ++ Adds an extra local ostree repo as a source for installation. This is equivalent ++ to using the sideload-repos directories (see ++ flatpak1), ++ but can be done on a per-command basis. Any path added here is used in addition ++ to ones in those directories. ++ ++ ++ ++ ++ ++ ++ ++ For each app being installed, also installs the SDK that was used to build it. ++ Implies ; incompatible with . ++ ++ ++ ++ ++ ++ ++ ++ For each ref being installed, as well as all dependencies, also installs its ++ debug info. Implies ; incompatible with ++ . ++ ++ ++ ++ ++ ++ ++ ++ Automatically answer yes to all questions (or pick the most prioritized answer). This is useful for automation. ++ ++ ++ ++ ++ ++ ++ Produce minimal output and avoid most questions. This is suitable for use in ++ non-interactive situations, e.g. in a build script. ++ ++ ++ ++ ++ ++ ++ ++ ++ Print debug information during command processing. ++ ++ ++ ++ ++ ++ ++ ++ Print OSTree debug information during command processing. ++ ++ ++ ++ ++ ++ ++ Examples ++ ++ ++ $ flatpak preinstall ++ ++ ++ ++ ++ See also ++ ++ ++ flatpak1, ++ ++ ++ ++ +diff --git a/doc/meson.build b/doc/meson.build +index a4cedfbe..9ef8eee1 100644 +--- a/doc/meson.build ++++ b/doc/meson.build +@@ -26,6 +26,7 @@ man1 = [ + 'flatpak-config', + 'flatpak-update', + 'flatpak-uninstall', ++ 'flatpak-preinstall', + 'flatpak-mask', + 'flatpak-pin', + 'flatpak-list', +diff --git a/po/POTFILES.in b/po/POTFILES.in +index b3cb79c0..52600abd 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -30,6 +30,7 @@ app/flatpak-builtins-permission-reset.c + app/flatpak-builtins-permission-set.c + app/flatpak-builtins-permission-show.c + app/flatpak-builtins-pin.c ++app/flatpak-builtins-preinstall.c + app/flatpak-builtins-ps.c + app/flatpak-builtins-remote-add.c + app/flatpak-builtins-remote-delete.c +diff --git a/system-helper/flatpak-system-helper.c b/system-helper/flatpak-system-helper.c +index 1f950b20..623c2fa0 100644 +--- a/system-helper/flatpak-system-helper.c ++++ b/system-helper/flatpak-system-helper.c +@@ -404,6 +404,7 @@ handle_deploy (FlatpakSystemHelper *object, + gboolean local_pull; + gboolean reinstall; + gboolean update_pinned; ++ gboolean update_preinstalled; + g_autofree char *url = NULL; + g_autoptr(OngoingPull) ongoing_pull = NULL; + g_autofree gchar *src_dir = NULL; +@@ -489,6 +490,7 @@ handle_deploy (FlatpakSystemHelper *object, + local_pull = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_LOCAL_PULL) != 0; + reinstall = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_REINSTALL) != 0; + update_pinned = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PINNED) != 0; ++ update_preinstalled = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PREINSTALLED) != 0; + + deploy_dir = flatpak_dir_get_if_deployed (system, ref, NULL, NULL); + +@@ -667,7 +669,8 @@ handle_deploy (FlatpakSystemHelper *object, + if (!flatpak_dir_deploy_install (system, ref, arg_origin, + (const char **) arg_subpaths, + (const char **) arg_previous_ids, +- reinstall, update_pinned, NULL, &error)) ++ reinstall, update_pinned, update_preinstalled, ++ NULL, &error)) + { + flatpak_invocation_return_error (invocation, error, "Error deploying"); + return G_DBUS_METHOD_INVOCATION_HANDLED; +diff --git a/tests/libtest.sh b/tests/libtest.sh +index 7dad594f..69ae0fef 100644 +--- a/tests/libtest.sh ++++ b/tests/libtest.sh +@@ -105,6 +105,7 @@ export FLATPAK_SYSTEM_DIR=${TEST_DATA_DIR}/system + export FLATPAK_SYSTEM_CACHE_DIR=${TEST_DATA_DIR}/system-cache + export FLATPAK_SYSTEM_HELPER_ON_SESSION=1 + export FLATPAK_CONFIG_DIR=${TEST_DATA_DIR}/config ++export FLATPAK_DATA_DIR=${TEST_DATA_DIR}/datadir + export FLATPAK_RUN_DIR=${TEST_DATA_DIR}/run + export FLATPAK_FANCY_OUTPUT=0 + export FLATPAK_FORCE_ALLOW_FUZZY_MATCHING=1 +diff --git a/tests/test-basic.sh b/tests/test-basic.sh +index 76369b35..9f1cfb20 100755 +--- a/tests/test-basic.sh ++++ b/tests/test-basic.sh +@@ -71,7 +71,7 @@ for cmd in install update uninstall list info config repair create-usb \ + remote-modify remote-delete remote-ls remote-info build-init \ + build build-finish build-export build-bundle build-import-bundle \ + build-sign build-update-repo build-commit-from repo kill history \ +- mask; ++ mask preinstall; + do + ${FLATPAK} $cmd --help > help_out + head -2 help_out > help_out2 +diff --git a/tests/test-matrix/meson.build b/tests/test-matrix/meson.build +index fd0b5034..3c20abed 100644 +--- a/tests/test-matrix/meson.build ++++ b/tests/test-matrix/meson.build +@@ -45,3 +45,4 @@ wrapped_tests += {'name' : 'test-unused.sh', 'script' : 'test-unused.sh'} + wrapped_tests += {'name' : 'test-prune.sh', 'script' : 'test-prune.sh'} + wrapped_tests += {'name' : 'test-seccomp.sh', 'script' : 'test-seccomp.sh'} + wrapped_tests += {'name' : 'test-repair.sh', 'script' : 'test-repair.sh'} ++wrapped_tests += {'name' : 'test-preinstall.sh', 'script' : 'test-preinstall.sh'} +diff --git a/tests/test-preinstall.sh b/tests/test-preinstall.sh +new file mode 100755 +index 00000000..23ae094c +--- /dev/null ++++ b/tests/test-preinstall.sh +@@ -0,0 +1,226 @@ ++#!/bin/bash ++# ++# Copyright (C) 2025 Red Hat, Inc ++# ++# This library 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 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the ++# Free Software Foundation, Inc., 59 Temple Place - Suite 330, ++# Boston, MA 02111-1307, USA. ++ ++set -euo pipefail ++ ++. $(dirname $0)/libtest.sh ++ ++mkdir -p $FLATPAK_DATA_DIR/preinstall.d ++mkdir -p $FLATPAK_CONFIG_DIR/preinstall.d ++ ++cat << EOF > hello-install.preinstall ++[Flatpak Preinstall org.test.Hello] ++EOF ++ ++cat << EOF > hello-not-install.preinstall ++[Flatpak Preinstall org.test.Hello] ++Install=false ++EOF ++ ++cat << EOF > hello-install-multi.preinstall ++[Flatpak Preinstall org.test.Hello] ++[Flatpak Preinstall org.test.Hello2] ++EOF ++ ++cat << EOF > hello-install-devel.preinstall ++[Flatpak Preinstall org.test.Hello] ++Branch=devel ++EOF ++ ++cat << EOF > bad.preinstall ++[Wrong Group] ++a=b ++ ++[Flatpak Preinstall ] ++Install=false ++ ++[Flatpak Preinstall] ++Install=true ++EOF ++ ++# Set up the runtimes ++# org.test.Platform//master and org.test.Platform//devel ++# and the apps ++# org.test.Hello//master, org.test.Hello//devel, ++# org.test.Hello2//master, org.test.Hello2//devel ++setup_repo ++make_updated_runtime test org.test.Collection.test devel HELLO_DEVEL org.test.Hello ++make_updated_app test org.test.Collection.test devel HELLO_DEVEL org.test.Hello ++make_updated_app test org.test.Collection.test master HELLO2_MASTER org.test.Hello2 ++make_updated_app test org.test.Collection.test devel HELLO2_DEVEL org.test.Hello2 ++ ++# make the test repo available by adding the test remote ++cat << EOF > test.flatpakrepo ++[Flatpak Repo] ++Url=http://127.0.0.1:${port}/test ++GPGKey=${FL_GPG_BASE64} ++Title=The Title ++Comment=The Comment ++Description=The Description ++Homepage=https://the.homepage/ ++Icon=https://the.icon/ ++EOF ++ ++${FLATPAK} ${U} remote-add test test.flatpakrepo >&2 ++ ++echo "1..9" ++ ++# just checking that the test remote got added ++assert_remote_has_config test url "http://127.0.0.1:${port}/test" ++ ++ok "setup" ++ ++# if we have nothing configured and nothing is marked as preinstalled ++# calling preinstall should be a no-op ++${FLATPAK} ${U} preinstall -y > nothingtodo ++assert_file_has_content nothingtodo "Nothing to do" ++ ++ok "no config" ++ ++# make sure nothing is installed ++${FLATPAK} ${U} list --columns=ref > list-log ++assert_file_empty list-log ++ ++# The preinstall config wants org.test.Hello. ++cp hello-install.preinstall $FLATPAK_DATA_DIR/preinstall.d/ ++ ++${FLATPAK} ${U} preinstall -y >&2 ++ ++# Make sure it and the runtime were installed ++${FLATPAK} ${U} list --columns=ref > list-log ++assert_file_has_content list-log "^org\.test\.Hello/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/devel$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Platform/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Platform/.*/devel$" ++ ++ok "simple preinstall" ++ ++# Make sure calling preinstall with the same config again is a no-op... ++${FLATPAK} ${U} preinstall -y > nothingtodo ++assert_file_has_content nothingtodo "Nothing to do" ++ ++# ...and everything is still installed ++${FLATPAK} ${U} list --columns=ref > list-log ++assert_file_has_content list-log "^org\.test\.Hello/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/devel$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Platform/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Platform/.*/devel$" ++ ++ok "simple preinstall no op" ++ ++${FLATPAK} ${U} uninstall -y org.test.Hello >&2 ++ ++${FLATPAK} ${U} list --columns=ref > list-log ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/devel$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Platform/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Platform/.*/devel$" ++ ++# Make sure calling preinstall with the same config again is a no-op ++# Even if the user uninstalled the app (it is marked as preinstalled) ++${FLATPAK} ${U} preinstall -y > nothingtodo ++assert_file_has_content nothingtodo "Nothing to do" ++ ++# Make sure nothing has changed ++${FLATPAK} ${U} list --columns=ref > list-log ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/devel$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Platform/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Platform/.*/devel$" ++ ++ok "uninstall preinstall" ++ ++${FLATPAK} ${U} install test -y org.test.Hello master >&2 ++ ++${FLATPAK} ${U} list --columns=ref > list-log ++assert_file_has_content list-log "^org\.test\.Hello/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/devel$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Platform/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Platform/.*/devel$" ++ ++# Add a config to /etc which overwrites the config in /usr ($FLATPAK_DATA_DIR) ++# It has the Install=false setting which means it shall not be installed. ++cp hello-not-install.preinstall $FLATPAK_CONFIG_DIR/preinstall.d/ ++ ++${FLATPAK} ${U} preinstall -y >&2 ++ ++# Make sure preinstall removed org.test.Hello as indicated by the config ++${FLATPAK} ${U} list --columns=ref > list-log ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/devel$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Platform/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Platform/.*/devel$" ++ ++ok "preinstall install false" ++ ++# Remove the existing configs ++rm -rf $FLATPAK_CONFIG_DIR/preinstall.d/* ++rm -rf $FLATPAK_DATA_DIR/preinstall.d/* ++ ++# Add a config file which wants org.test.Hello and org.test.Hello2 installed ++cp hello-install-multi.preinstall $FLATPAK_CONFIG_DIR/preinstall.d/ ++ ++${FLATPAK} ${U} preinstall -y >&2 ++ ++# Make sure both apps got installed ++${FLATPAK} ${U} list --columns=ref > list-log ++assert_file_has_content list-log "^org\.test\.Hello/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Hello2/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Platform/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Platform/.*/devel$" ++ ++ok "install multi" ++ ++# Overwrite the branch of org.test.Hello from master to devel ++cp hello-install-devel.preinstall $FLATPAK_CONFIG_DIR/preinstall.d/ ++ ++${FLATPAK} ${U} preinstall -y >&2 ++ ++# Make sure org.test.Hello//devel replaced org.test.Hello//master ++${FLATPAK} ${U} list --columns=ref > list-log ++assert_not_file_has_content list-log "^org\.test\.Hello/.*/master$" ++assert_file_has_content list-log "^org\.test\.Hello/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Hello2/.*/master$" ++assert_not_file_has_content list-log "^org\.test\.Hello2/.*/devel$" ++assert_file_has_content list-log "^org\.test\.Platform/.*/master$" ++assert_file_has_content list-log "^org\.test\.Platform/.*/devel$" ++ ++ok "overwrite branch" ++ ++# Make sure some config file parsing edge cases don't blow up ++cp bad.preinstall $FLATPAK_CONFIG_DIR/preinstall.d/ ++ ++${FLATPAK} ${U} preinstall -y > nothingtodo ++assert_file_has_content nothingtodo "Nothing to do" ++ ++ok "bad config" +\ No newline at end of file +-- +2.47.1 + diff --git a/flatpak-allow-direct-installation-from-oci-images.patch b/flatpak-allow-direct-installation-from-oci-images.patch new file mode 100644 index 0000000000000000000000000000000000000000..76a0475912b68c0ebb0b372f329b311288a21f42 --- /dev/null +++ b/flatpak-allow-direct-installation-from-oci-images.patch @@ -0,0 +1,4160 @@ +From 7255a7ff96d9066c5c8e090f3a6d76de2f38ba50 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Mon, 14 Oct 2024 09:12:39 -0400 +Subject: [PATCH 01/12]image-source: Refactor - add FlatpakImageSource type + +To avoid passing around combinations of a FlaptakOciRegistry with +repository and digest, add a FlatpakImageSource type. + +This also reduces duplicated code where every place that did +this independently retrieved the repository and image config. +--- + app/flatpak-builtins-build-import-bundle.c | 65 +----- + common/flatpak-common-types-private.h | 1 + + common/flatpak-dir.c | 80 +++----- + common/flatpak-image-source-private.h | 57 ++++++ + common/flatpak-image-source.c | 227 +++++++++++++++++++++ + common/flatpak-oci-registry-private.h | 11 +- + common/flatpak-oci-registry.c | 46 ++--- + common/meson.build | 1 + + doc/reference/meson.build | 1 + + system-helper/flatpak-system-helper.c | 57 +----- + 10 files changed, 348 insertions(+), 198 deletions(-) + create mode 100644 common/flatpak-image-source-private.h + create mode 100644 common/flatpak-image-source.c + +diff --git a/app/flatpak-builtins-build-import-bundle.c b/app/flatpak-builtins-build-import-bundle.c +index d5d6fc26..f3508709 100644 +--- a/app/flatpak-builtins-build-import-bundle.c ++++ b/app/flatpak-builtins-build-import-bundle.c +@@ -30,9 +30,10 @@ + #include "libglnx.h" + + #include "flatpak-builtins.h" ++#include "flatpak-image-source-private.h" ++#include "flatpak-oci-registry-private.h" + #include "flatpak-repo-utils-private.h" + #include "flatpak-utils-private.h" +-#include "flatpak-oci-registry-private.h" + + static char *opt_ref; + static gboolean opt_oci = FALSE; +@@ -58,65 +59,17 @@ import_oci (OstreeRepo *repo, GFile *file, + GCancellable *cancellable, GError **error) + { + g_autofree char *commit_checksum = NULL; +- g_autofree char *dir_uri = NULL; + g_autofree char *target_ref = NULL; +- const char *oci_digest; +- g_autoptr(FlatpakOciRegistry) registry = NULL; +- g_autoptr(FlatpakOciVersioned) versioned = NULL; +- g_autoptr(FlatpakOciImage) image_config = NULL; +- FlatpakOciManifest *manifest = NULL; +- g_autoptr(FlatpakOciIndex) index = NULL; +- const FlatpakOciManifestDescriptor *desc; ++ g_autoptr(FlatpakImageSource) image_source = NULL; + GHashTable *labels; + +- dir_uri = g_file_get_uri (file); +- registry = flatpak_oci_registry_new (dir_uri, FALSE, -1, cancellable, error); +- if (registry == NULL) +- return NULL; +- +- index = flatpak_oci_registry_load_index (registry, cancellable, error); +- if (index == NULL) +- return NULL; +- +- if (opt_ref) +- { +- desc = flatpak_oci_index_get_manifest (index, opt_ref); +- if (desc == NULL) +- { +- flatpak_fail (error, _("Ref '%s' not found in registry"), opt_ref); +- return NULL; +- } +- } +- else +- { +- desc = flatpak_oci_index_get_only_manifest (index); +- if (desc == NULL) +- { +- flatpak_fail (error, _("Multiple images in registry, specify a ref with --ref")); +- return NULL; +- } +- } +- +- oci_digest = desc->parent.digest; +- +- versioned = flatpak_oci_registry_load_versioned (registry, NULL, +- oci_digest, NULL, NULL, +- cancellable, error); +- if (versioned == NULL) ++ image_source = flatpak_image_source_new_local (file, opt_ref, cancellable, error); ++ if (image_source == NULL) + return NULL; + +- manifest = FLATPAK_OCI_MANIFEST (versioned); +- +- image_config = flatpak_oci_registry_load_image_config (registry, NULL, +- manifest->config.digest, NULL, +- NULL, cancellable, error); +- if (image_config == NULL) +- return FALSE; +- +- labels = flatpak_oci_image_get_labels (image_config); +- if (labels) +- flatpak_oci_parse_commit_labels (labels, NULL, NULL, NULL, +- &target_ref, NULL, NULL, NULL); ++ labels = flatpak_image_source_get_labels (image_source); ++ flatpak_oci_parse_commit_labels (labels, NULL, NULL, NULL, ++ &target_ref, NULL, NULL, NULL); + if (target_ref == NULL) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, +@@ -124,7 +77,7 @@ import_oci (OstreeRepo *repo, GFile *file, + return NULL; + } + +- commit_checksum = flatpak_pull_from_oci (repo, registry, NULL, oci_digest, NULL, manifest, image_config, ++ commit_checksum = flatpak_pull_from_oci (repo, image_source, NULL, + NULL, target_ref, FLATPAK_PULL_FLAGS_NONE, NULL, NULL, cancellable, error); + if (commit_checksum == NULL) + return NULL; +diff --git a/common/flatpak-common-types-private.h b/common/flatpak-common-types-private.h +index 05130144..d7f3913b 100644 +--- a/common/flatpak-common-types-private.h ++++ b/common/flatpak-common-types-private.h +@@ -53,6 +53,7 @@ typedef enum { + + typedef struct FlatpakDir FlatpakDir; + typedef struct FlatpakDeploy FlatpakDeploy; ++typedef struct _FlatpakImageSource FlatpakImageSource; + typedef struct FlatpakOciRegistry FlatpakOciRegistry; + typedef struct _FlatpakOciManifest FlatpakOciManifest; + typedef struct _FlatpakOciImage FlatpakOciImage; +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index 6936d45f..b0c29a70 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -52,6 +52,7 @@ + #include "flatpak-dir-utils-private.h" + #include "flatpak-error.h" + #include "flatpak-locale-utils-private.h" ++#include "flatpak-image-source-private.h" + #include "flatpak-oci-registry-private.h" + #include "flatpak-ref.h" + #include "flatpak-repo-utils-private.h" +@@ -1049,14 +1050,16 @@ lookup_oci_registry_uri_from_summary (GVariant *summary, + return g_steal_pointer (®istry_uri); + } + +-static FlatpakOciRegistry * +-flatpak_remote_state_new_oci_registry (FlatpakRemoteState *self, ++static FlatpakImageSource * ++flatpak_remote_state_new_image_source (FlatpakRemoteState *self, ++ const char *oci_repository, ++ const char *digest, + const char *token, + GCancellable *cancellable, + GError **error) + { + g_autofree char *registry_uri = NULL; +- g_autoptr(FlatpakOciRegistry) registry = NULL; ++ g_autoptr(FlatpakImageSource) image_source = NULL; + + if (!flatpak_remote_state_ensure_summary (self, error)) + return NULL; +@@ -1065,13 +1068,13 @@ flatpak_remote_state_new_oci_registry (FlatpakRemoteState *self, + if (registry_uri == NULL) + return NULL; + +- registry = flatpak_oci_registry_new (registry_uri, FALSE, -1, NULL, error); +- if (registry == NULL) ++ image_source = flatpak_image_source_new_remote (registry_uri, oci_repository, digest, NULL, error); ++ if (image_source == NULL) + return NULL; + +- flatpak_oci_registry_set_token (registry, token); ++ flatpak_image_source_set_token (image_source, token); + +- return g_steal_pointer (®istry); ++ return g_steal_pointer (&image_source); + } + + static GVariant * +@@ -1083,9 +1086,7 @@ flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, + GCancellable *cancellable, + GError **error) + { +- g_autoptr(FlatpakOciRegistry) registry = NULL; +- g_autoptr(FlatpakOciVersioned) versioned = NULL; +- g_autoptr(FlatpakOciImage) image_config = NULL; ++ g_autoptr(FlatpakImageSource) image_source = NULL; + g_autofree char *oci_digest = NULL; + g_autofree char *latest_rev = NULL; + VarRefInfoRef latest_rev_info; +@@ -1100,10 +1101,6 @@ flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, + g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + g_autoptr(GVariant) metadata_v = NULL; + +- registry = flatpak_remote_state_new_oci_registry (self, token, cancellable, error); +- if (registry == NULL) +- return NULL; +- + /* We extract the rev info from the latest, even if we don't use the latest digest, assuming refs don't move */ + if (!flatpak_remote_state_lookup_ref (self, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) + return NULL; +@@ -1121,25 +1118,11 @@ flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, + + oci_digest = g_strconcat ("sha256:", checksum, NULL); + +- versioned = flatpak_oci_registry_load_versioned (registry, oci_repository, oci_digest, +- NULL, NULL, cancellable, error); +- if (versioned == NULL) +- return NULL; +- +- if (!FLATPAK_IS_OCI_MANIFEST (versioned)) +- { +- flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Image is not a manifest")); +- return NULL; +- } +- +- image_config = flatpak_oci_registry_load_image_config (registry, oci_repository, +- FLATPAK_OCI_MANIFEST (versioned)->config.digest, +- (const char **)FLATPAK_OCI_MANIFEST (versioned)->config.urls, +- NULL, cancellable, error); +- if (image_config == NULL) ++ image_source = flatpak_remote_state_new_image_source (self, oci_repository, oci_digest, token, cancellable, error); ++ if (image_source == NULL) + return NULL; + +- labels = flatpak_oci_image_get_labels (image_config); ++ labels = flatpak_image_source_get_labels (image_source); + if (labels) + flatpak_oci_parse_commit_labels (labels, ×tamp, + &subject, &body, +@@ -5935,7 +5918,7 @@ flatpak_dir_mirror_oci (FlatpakDir *self, + GCancellable *cancellable, + GError **error) + { +- g_autoptr(FlatpakOciRegistry) registry = NULL; ++ g_autoptr(FlatpakImageSource) image_source = NULL; + g_autofree char *oci_digest = NULL; + g_autofree char *latest_rev = NULL; + VarRefInfoRef latest_rev_info; +@@ -5968,15 +5951,15 @@ flatpak_dir_mirror_oci (FlatpakDir *self, + + oci_digest = g_strconcat ("sha256:", rev, NULL); + +- registry = flatpak_remote_state_new_oci_registry (state, token, cancellable, error); +- if (registry == NULL) ++ image_source = flatpak_remote_state_new_image_source (state, oci_repository, oci_digest, token, cancellable, error); ++ if (image_source == NULL) + return FALSE; + + flatpak_progress_start_oci_pull (progress); + + g_info ("Mirroring OCI image %s", oci_digest); + +- res = flatpak_mirror_image_from_oci (dst_registry, registry, oci_repository, oci_digest, state->remote_name, ref, delta_url, self->repo, oci_pull_progress_cb, ++ res = flatpak_mirror_image_from_oci (dst_registry, image_source, state->remote_name, ref, delta_url, self->repo, oci_pull_progress_cb, + progress, cancellable, error); + + if (!res) +@@ -5998,9 +5981,8 @@ flatpak_dir_pull_oci (FlatpakDir *self, + GCancellable *cancellable, + GError **error) + { +- g_autoptr(FlatpakOciRegistry) registry = NULL; +- g_autoptr(FlatpakOciVersioned) versioned = NULL; +- g_autoptr(FlatpakOciImage) image_config = NULL; ++ g_autoptr(FlatpakImageSource) image_source = NULL; ++ FlatpakOciRegistry *registry = NULL; + const char *oci_repository = NULL; + const char *delta_url = NULL; + g_autofree char *oci_digest = NULL; +@@ -6031,23 +6013,8 @@ flatpak_dir_pull_oci (FlatpakDir *self, + if (latest_alt_commit != NULL && strcmp (oci_digest + strlen ("sha256:"), latest_alt_commit) == 0) + return TRUE; + +- registry = flatpak_remote_state_new_oci_registry (state, token, cancellable, error); +- if (registry == NULL) +- return FALSE; +- +- versioned = flatpak_oci_registry_load_versioned (registry, oci_repository, oci_digest, +- NULL, NULL, cancellable, error); +- if (versioned == NULL) +- return FALSE; +- +- if (!FLATPAK_IS_OCI_MANIFEST (versioned)) +- return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Image is not a manifest")); +- +- image_config = flatpak_oci_registry_load_image_config (registry, oci_repository, +- FLATPAK_OCI_MANIFEST (versioned)->config.digest, +- (const char **)FLATPAK_OCI_MANIFEST (versioned)->config.urls, +- NULL, cancellable, error); +- if (image_config == NULL) ++ image_source = flatpak_remote_state_new_image_source (state, oci_repository, oci_digest, token, cancellable, error); ++ if (image_source == NULL) + return FALSE; + + if (repo == NULL) +@@ -6057,7 +6024,7 @@ flatpak_dir_pull_oci (FlatpakDir *self, + + g_info ("Pulling OCI image %s", oci_digest); + +- checksum = flatpak_pull_from_oci (repo, registry, oci_repository, oci_digest, delta_url, FLATPAK_OCI_MANIFEST (versioned), image_config, ++ checksum = flatpak_pull_from_oci (repo, image_source, delta_url, + state->remote_name, ref, flatpak_flags, oci_pull_progress_cb, progress, cancellable, error); + + if (checksum == NULL) +@@ -6073,6 +6040,7 @@ flatpak_dir_pull_oci (FlatpakDir *self, + name = g_file_get_path (file); + } + ++ registry = flatpak_image_source_get_registry (image_source); + (flatpak_dir_log) (self, __FILE__, __LINE__, __FUNCTION__, name, + "pull oci", flatpak_oci_registry_get_uri (registry), ref, NULL, NULL, NULL, + "Pulled %s from %s", ref, flatpak_oci_registry_get_uri (registry)); +diff --git a/common/flatpak-image-source-private.h b/common/flatpak-image-source-private.h +new file mode 100644 +index 00000000..ebd856a4 +--- /dev/null ++++ b/common/flatpak-image-source-private.h +@@ -0,0 +1,57 @@ ++/* ++ * Copyright © 2024 Red Hat, Inc ++ * ++ * This program 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. ++ * ++ * This library is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library. If not, see . ++ * ++ * Authors: ++ * Owen Taylor ++ */ ++ ++#ifndef __FLATPAK_IMAGE_SOURCE_H__ ++#define __FLATPAK_IMAGE_SOURCE_H__ ++ ++#include ++#include ++ ++#include ++ ++#define FLATPAK_TYPE_IMAGE_SOURCE flatpak_image_source_get_type () ++G_DECLARE_FINAL_TYPE (FlatpakImageSource, ++ flatpak_image_source, ++ FLATPAK, IMAGE_SOURCE, ++ GObject) ++ ++FlatpakImageSource *flatpak_image_source_new_local (GFile *file, ++ const char *reference, ++ GCancellable *cancellable, ++ GError **error); ++FlatpakImageSource *flatpak_image_source_new_remote (const char *uri, ++ const char *oci_repository, ++ const char *digest, ++ GCancellable *cancellable, ++ GError **error); ++ ++void flatpak_image_source_set_token (FlatpakImageSource *self, ++ const char *token); ++ ++FlatpakOciRegistry *flatpak_image_source_get_registry (FlatpakImageSource *self); ++const char *flatpak_image_source_get_oci_repository (FlatpakImageSource *self); ++const char *flatpak_image_source_get_digest (FlatpakImageSource *self); ++FlatpakOciManifest *flatpak_image_source_get_manifest (FlatpakImageSource *self); ++size_t flatpak_image_source_get_manifest_size (FlatpakImageSource *self); ++FlatpakOciImage *flatpak_image_source_get_image_config (FlatpakImageSource *self); ++ ++GHashTable *flatpak_image_source_get_labels (FlatpakImageSource *self); ++ ++#endif /* __FLATPAK_IMAGE_SOURCE_H__ */ +diff --git a/common/flatpak-image-source.c b/common/flatpak-image-source.c +new file mode 100644 +index 00000000..22497ccf +--- /dev/null ++++ b/common/flatpak-image-source.c +@@ -0,0 +1,227 @@ ++/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s: ++ * Copyright © 2024 Red Hat, Inc ++ * ++ * This program 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. ++ * ++ * This library is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library. If not, see . ++ * ++ * Authors: ++ * Owen Taylor ++ */ ++ ++#include ++ ++#include "flatpak-image-source-private.h" ++#include "flatpak-oci-registry-private.h" ++ ++struct _FlatpakImageSource ++{ ++ GObject parent; ++ ++ FlatpakOciRegistry *registry; ++ char *repository; ++ char *digest; ++ ++ FlatpakOciManifest *manifest; ++ size_t manifest_size; ++ FlatpakOciImage *image_config; ++}; ++ ++G_DEFINE_TYPE (FlatpakImageSource, flatpak_image_source, G_TYPE_OBJECT) ++ ++static void ++flatpak_image_source_finalize (GObject *object) ++{ ++ FlatpakImageSource *self = FLATPAK_IMAGE_SOURCE (object); ++ ++ g_clear_object (&self->registry); ++ g_clear_pointer (&self->repository, g_free); ++ g_clear_pointer (&self->digest, g_free); ++ g_clear_object (&self->manifest); ++ g_clear_object (&self->image_config); ++ ++ G_OBJECT_CLASS (flatpak_image_source_parent_class)->finalize (object); ++} ++ ++static void ++flatpak_image_source_class_init (FlatpakImageSourceClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ ++ object_class->finalize = flatpak_image_source_finalize; ++} ++ ++static void ++flatpak_image_source_init (FlatpakImageSource *self) ++{ ++} ++ ++static FlatpakImageSource * ++flatpak_image_source_new (FlatpakOciRegistry *registry, ++ const char *repository, ++ const char *digest, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(FlatpakImageSource) self = NULL; ++ g_autoptr(FlatpakOciVersioned) versioned = NULL; ++ GHashTable *labels; ++ ++ self = g_object_new (FLATPAK_TYPE_IMAGE_SOURCE, NULL); ++ self->registry = g_object_ref (registry); ++ self->repository = g_strdup (repository); ++ self->digest = g_strdup (digest); ++ ++ versioned = flatpak_oci_registry_load_versioned (self->registry, self->repository, ++ self->digest, NULL, &self->manifest_size, ++ cancellable, error); ++ if (versioned == NULL) ++ return NULL; ++ ++ if (!FLATPAK_IS_OCI_MANIFEST (versioned)) ++ { ++ flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Image is not a manifest")); ++ return NULL; ++ } ++ ++ self->manifest = FLATPAK_OCI_MANIFEST (g_steal_pointer (&versioned)); ++ ++ if (self->manifest->config.digest == NULL) ++ { ++ flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Image is not a manifest")); ++ return NULL; ++ } ++ ++ self->image_config = flatpak_oci_registry_load_image_config (self->registry, self->repository, ++ self->manifest->config.digest, NULL, ++ NULL, cancellable, error); ++ if (self->image_config == NULL) ++ return NULL; ++ ++ labels = flatpak_image_source_get_labels (self); ++ if (!g_hash_table_contains (labels, "org.flatpak.ref")) ++ { ++ flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("No org.flatpak.ref found in image")); ++ return NULL; ++ } ++ ++ return g_steal_pointer (&self); ++} ++ ++FlatpakImageSource * ++flatpak_image_source_new_local (GFile *file, ++ const char *reference, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autofree char *dir_uri = NULL; ++ g_autofree char *target_ref = NULL; ++ g_autoptr(FlatpakImageSource) image_source = NULL; ++ g_autoptr(FlatpakOciRegistry) registry = NULL; ++ g_autoptr(FlatpakOciIndex) index = NULL; ++ const FlatpakOciManifestDescriptor *desc; ++ ++ dir_uri = g_file_get_uri (file); ++ registry = flatpak_oci_registry_new (dir_uri, FALSE, -1, cancellable, error); ++ if (registry == NULL) ++ return NULL; ++ ++ index = flatpak_oci_registry_load_index (registry, cancellable, error); ++ if (index == NULL) ++ return NULL; ++ ++ if (reference) ++ { ++ desc = flatpak_oci_index_get_manifest (index, reference); ++ if (desc == NULL) ++ { ++ flatpak_fail (error, _("Ref '%s' not found in registry"), reference); ++ return NULL; ++ } ++ } ++ else ++ { ++ desc = flatpak_oci_index_get_only_manifest (index); ++ if (desc == NULL) ++ { ++ flatpak_fail (error, _("Multiple images in registry, specify a ref with --ref")); ++ return NULL; ++ } ++ } ++ ++ return flatpak_image_source_new (registry, NULL, desc->parent.digest, cancellable, error); ++} ++ ++FlatpakImageSource * ++flatpak_image_source_new_remote (const char *uri, ++ const char *oci_repository, ++ const char *digest, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(FlatpakOciRegistry) registry = NULL; ++ ++ registry = flatpak_oci_registry_new (uri, FALSE, -1, cancellable, error); ++ if (!registry) ++ return NULL; ++ ++ return flatpak_image_source_new (registry, oci_repository, digest, cancellable, error); ++} ++ ++void ++flatpak_image_source_set_token (FlatpakImageSource *self, ++ const char *token) ++{ ++ flatpak_oci_registry_set_token (self->registry, token); ++} ++ ++FlatpakOciRegistry * ++flatpak_image_source_get_registry (FlatpakImageSource *self) ++{ ++ return self->registry; ++} ++ ++const char * ++flatpak_image_source_get_oci_repository (FlatpakImageSource *self) ++{ ++ return self->repository; ++} ++ ++const char * ++flatpak_image_source_get_digest (FlatpakImageSource *self) ++{ ++ return self->digest; ++} ++ ++FlatpakOciManifest * ++flatpak_image_source_get_manifest (FlatpakImageSource *self) ++{ ++ return self->manifest; ++} ++ ++size_t ++flatpak_image_source_get_manifest_size (FlatpakImageSource *self) ++{ ++ return self->manifest_size; ++} ++ ++FlatpakOciImage * ++flatpak_image_source_get_image_config (FlatpakImageSource *self) ++{ ++ return self->image_config; ++} ++ ++GHashTable * ++flatpak_image_source_get_labels (FlatpakImageSource *self) ++{ ++ return flatpak_oci_image_get_labels (self->image_config); ++} +diff --git a/common/flatpak-oci-registry-private.h b/common/flatpak-oci-registry-private.h +index 2c0608dc..47ffc7be 100644 +--- a/common/flatpak-oci-registry-private.h ++++ b/common/flatpak-oci-registry-private.h +@@ -183,6 +183,7 @@ GBytes *flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session, + GCancellable *cancellable, + GError **error); + ++ + typedef void (*FlatpakOciPullProgress) (guint64 total_size, + guint64 pulled_size, + guint32 n_layers, +@@ -190,12 +191,8 @@ typedef void (*FlatpakOciPullProgress) (guint64 total_size, + gpointer data); + + char * flatpak_pull_from_oci (OstreeRepo *repo, +- FlatpakOciRegistry *registry, +- const char *oci_repository, +- const char *digest, ++ FlatpakImageSource *image_source, + const char *delta_url, +- FlatpakOciManifest *manifest, +- FlatpakOciImage *image_config, + const char *remote, + const char *ref, + FlatpakPullFlags flags, +@@ -205,9 +202,7 @@ char * flatpak_pull_from_oci (OstreeRepo *repo, + GError **error); + + gboolean flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, +- FlatpakOciRegistry *registry, +- const char *oci_repository, +- const char *digest, ++ FlatpakImageSource *image_source, + const char *remote, + const char *ref, + const char *delta_url, +diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c +index 6d36de2a..11b5f98d 100644 +--- a/common/flatpak-oci-registry.c ++++ b/common/flatpak-oci-registry.c +@@ -28,6 +28,7 @@ + + #include + #include ++#include "flatpak-image-source-private.h" + #include "flatpak-oci-registry-private.h" + #include "flatpak-repo-utils-private.h" + #include "flatpak-utils-base-private.h" +@@ -3454,9 +3455,7 @@ oci_layer_progress (guint64 downloaded_bytes, + + gboolean + flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, +- FlatpakOciRegistry *registry, +- const char *oci_repository, +- const char *digest, ++ FlatpakImageSource *image_source, + const char *remote, + const char *ref, + const char *delta_url, +@@ -3467,8 +3466,11 @@ flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, + GError **error) + { + FlatpakOciPullProgressData progress_data = { progress_cb, progress_user_data }; +- g_autoptr(FlatpakOciVersioned) versioned = NULL; +- FlatpakOciManifest *manifest = NULL; ++ FlatpakOciRegistry *registry = flatpak_image_source_get_registry (image_source); ++ const char *oci_repository = flatpak_image_source_get_oci_repository (image_source); ++ const char *digest = flatpak_image_source_get_digest (image_source); ++ FlatpakOciManifest *manifest = flatpak_image_source_get_manifest (image_source); ++ FlatpakOciImage *image_config = flatpak_image_source_get_image_config (image_source); + g_autoptr(FlatpakOciDescriptor) manifest_desc = NULL; + g_autoptr(FlatpakOciManifest) delta_manifest = NULL; + g_autofree char *old_checksum = NULL; +@@ -3476,36 +3478,16 @@ flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, + g_autoptr(GFile) old_root = NULL; + OstreeRepoCommitState old_state = 0; + g_autofree char *old_diffid = NULL; +- gsize versioned_size; + g_autoptr(FlatpakOciIndex) index = NULL; +- g_autoptr(FlatpakOciImage) image_config = NULL; + int n_layers; + int i; + + if (!flatpak_oci_registry_mirror_blob (dst_registry, registry, oci_repository, TRUE, digest, NULL, NULL, NULL, cancellable, error)) + return FALSE; + +- versioned = flatpak_oci_registry_load_versioned (dst_registry, NULL, digest, NULL, &versioned_size, cancellable, error); +- if (versioned == NULL) +- return FALSE; +- +- if (!FLATPAK_IS_OCI_MANIFEST (versioned)) +- return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Image is not a manifest")); +- +- manifest = FLATPAK_OCI_MANIFEST (versioned); +- +- if (manifest->config.digest == NULL) +- return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Image is not a manifest")); +- + if (!flatpak_oci_registry_mirror_blob (dst_registry, registry, oci_repository, FALSE, manifest->config.digest, (const char **)manifest->config.urls, NULL, NULL, cancellable, error)) + return FALSE; + +- image_config = flatpak_oci_registry_load_image_config (dst_registry, NULL, +- manifest->config.digest, NULL, +- NULL, cancellable, error); +- if (image_config == NULL) +- return FALSE; +- + /* For deltas we ensure that the diffid and regular layers exists and match up */ + n_layers = flatpak_oci_manifest_get_n_layers (manifest); + if (n_layers == 0 || n_layers != flatpak_oci_image_get_n_layers (image_config)) +@@ -3589,7 +3571,8 @@ flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, + if (index == NULL) + index = flatpak_oci_index_new (); + +- manifest_desc = flatpak_oci_descriptor_new (versioned->mediatype, digest, versioned_size); ++ manifest_desc = flatpak_oci_descriptor_new (manifest->parent.mediatype, digest, ++ flatpak_image_source_get_manifest_size (image_source)); + + flatpak_oci_index_add_manifest (index, ref, manifest_desc); + +@@ -3601,12 +3584,8 @@ flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, + + char * + flatpak_pull_from_oci (OstreeRepo *repo, +- FlatpakOciRegistry *registry, +- const char *oci_repository, +- const char *digest, ++ FlatpakImageSource *image_source, + const char *delta_url, +- FlatpakOciManifest *manifest, +- FlatpakOciImage *image_config, + const char *remote, + const char *ref, + FlatpakPullFlags flags, +@@ -3615,6 +3594,11 @@ flatpak_pull_from_oci (OstreeRepo *repo, + GCancellable *cancellable, + GError **error) + { ++ FlatpakOciRegistry *registry = flatpak_image_source_get_registry (image_source); ++ const char *oci_repository = flatpak_image_source_get_oci_repository (image_source); ++ const char *digest = flatpak_image_source_get_digest (image_source); ++ FlatpakOciManifest *manifest = flatpak_image_source_get_manifest (image_source); ++ FlatpakOciImage *image_config = flatpak_image_source_get_image_config (image_source); + gboolean force_disable_deltas = (flags & FLATPAK_PULL_FLAGS_NO_STATIC_DELTAS) != 0; + g_autoptr(OstreeMutableTree) archive_mtree = NULL; + g_autoptr(GFile) archive_root = NULL; +diff --git a/common/meson.build b/common/meson.build +index bd5dbf3d..beed1f06 100644 +--- a/common/meson.build ++++ b/common/meson.build +@@ -172,6 +172,7 @@ sources = [ + 'flatpak-error.c', + 'flatpak-exports.c', + 'flatpak-glib-backports.c', ++ 'flatpak-image-source.c', + 'flatpak-installation.c', + 'flatpak-installed-ref.c', + 'flatpak-instance.c', +diff --git a/doc/reference/meson.build b/doc/reference/meson.build +index a881b0cb..92f1482e 100644 +--- a/doc/reference/meson.build ++++ b/doc/reference/meson.build +@@ -46,6 +46,7 @@ gnome.gtkdoc( + 'flatpak-document-dbus-generated.h', + 'flatpak-enum-types.h', + 'flatpak-exports-private.h', ++ 'flatpak-image-source-private.h', + 'flatpak-installed-ref-private.h', + 'flatpak-json-oci-private.h', + 'flatpak-json-private.h', +diff --git a/system-helper/flatpak-system-helper.c b/system-helper/flatpak-system-helper.c +index a58ab2c6..a4d9708b 100644 +--- a/system-helper/flatpak-system-helper.c ++++ b/system-helper/flatpak-system-helper.c +@@ -39,6 +39,7 @@ + #include "flatpak-dbus-generated.h" + #include "flatpak-dir-private.h" + #include "flatpak-error.h" ++#include "flatpak-image-source-private.h" + #include "flatpak-oci-registry-private.h" + #include "flatpak-progress-private.h" + #include "flatpak-system-helper.h" +@@ -528,15 +529,11 @@ handle_deploy (FlatpakSystemHelper *object, + if (strlen (arg_repo_path) > 0 && is_oci) + { + g_autoptr(GFile) registry_file = g_file_new_for_path (arg_repo_path); +- g_autofree char *registry_uri = g_file_get_uri (registry_file); +- g_autoptr(FlatpakOciRegistry) registry = NULL; +- g_autoptr(FlatpakOciIndex) index = NULL; +- const FlatpakOciManifestDescriptor *desc; +- g_autoptr(FlatpakOciVersioned) versioned = NULL; +- g_autoptr(FlatpakOciImage) image_config = NULL; ++ g_autoptr(FlatpakImageSource) image_source = NULL; + g_autoptr(FlatpakRemoteState) state = NULL; + g_autoptr(GHashTable) remote_refs = NULL; + g_autofree char *checksum = NULL; ++ const char *image_source_digest; + const char *verified_digest; + g_autofree char *upstream_url = NULL; + +@@ -552,50 +549,14 @@ handle_deploy (FlatpakSystemHelper *object, + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- registry = flatpak_oci_registry_new (registry_uri, FALSE, -1, NULL, &error); +- if (registry == NULL) ++ image_source = flatpak_image_source_new_local (registry_file, arg_ref, NULL, &error); ++ if (image_source == NULL) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Can't open child OCI registry: %s", error->message); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- index = flatpak_oci_registry_load_index (registry, NULL, &error); +- if (index == NULL) +- { +- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, +- "Can't open child OCI registry index: %s", error->message); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- +- desc = flatpak_oci_index_get_manifest (index, arg_ref); +- if (desc == NULL) +- { +- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, +- "Can't find ref %s in child OCI registry index", arg_ref); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- +- versioned = flatpak_oci_registry_load_versioned (registry, NULL, desc->parent.digest, (const char **)desc->parent.urls, NULL, +- NULL, &error); +- if (versioned == NULL || !FLATPAK_IS_OCI_MANIFEST (versioned)) +- { +- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, +- "Can't open child manifest"); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- +- image_config = flatpak_oci_registry_load_image_config (registry, NULL, +- FLATPAK_OCI_MANIFEST (versioned)->config.digest, +- (const char **)FLATPAK_OCI_MANIFEST (versioned)->config.urls, +- NULL, NULL, &error); +- if (image_config == NULL) +- { +- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, +- "Can't open child image config"); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- + state = flatpak_dir_get_remote_state (system, arg_origin, FALSE, NULL, &error); + if (state == NULL) + { +@@ -622,15 +583,17 @@ handle_deploy (FlatpakSystemHelper *object, + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- if (!g_str_has_prefix (desc->parent.digest, "sha256:") || +- strcmp (desc->parent.digest + strlen ("sha256:"), verified_digest) != 0) ++ image_source_digest = flatpak_image_source_get_digest (image_source); ++ ++ if (!g_str_has_prefix (image_source_digest, "sha256:") || ++ strcmp (image_source_digest + strlen ("sha256:"), verified_digest) != 0) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "%s: manifest hash in downloaded content does not match ref %s", arg_origin, arg_ref); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- checksum = flatpak_pull_from_oci (flatpak_dir_get_repo (system), registry, NULL, desc->parent.digest, NULL, FLATPAK_OCI_MANIFEST (versioned), image_config, ++ checksum = flatpak_pull_from_oci (flatpak_dir_get_repo (system), image_source, NULL, + arg_origin, arg_ref, FLATPAK_PULL_FLAGS_NONE, NULL, NULL, NULL, &error); + if (checksum == NULL) + { +-- +2.47.1 + +From 5e0cfa2e33b9d99992047a14650784e5fd4c0c05 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Mon, 28 Oct 2024 12:35:33 -0400 +Subject: [PATCH 02/12]image-source: Replace flatpak_oci_parse_commit_labels + with getters + +Instead of having one function with a pile of out arguments in +arbitrary order, add getters to FlatpakImageSource. +--- + app/flatpak-builtins-build-import-bundle.c | 18 ++-- + common/flatpak-dir.c | 25 ++---- + common/flatpak-image-source-private.h | 11 ++- + common/flatpak-image-source.c | 96 ++++++++++++++++++++-- + common/flatpak-json-oci-private.h | 8 -- + common/flatpak-json-oci.c | 66 --------------- + common/flatpak-oci-registry.c | 28 +++---- + 7 files changed, 125 insertions(+), 127 deletions(-) + +diff --git a/app/flatpak-builtins-build-import-bundle.c b/app/flatpak-builtins-build-import-bundle.c +index f3508709..d79fbbde 100644 +--- a/app/flatpak-builtins-build-import-bundle.c ++++ b/app/flatpak-builtins-build-import-bundle.c +@@ -59,30 +59,22 @@ import_oci (OstreeRepo *repo, GFile *file, + GCancellable *cancellable, GError **error) + { + g_autofree char *commit_checksum = NULL; +- g_autofree char *target_ref = NULL; + g_autoptr(FlatpakImageSource) image_source = NULL; +- GHashTable *labels; ++ const char *ref; + + image_source = flatpak_image_source_new_local (file, opt_ref, cancellable, error); + if (image_source == NULL) + return NULL; + +- labels = flatpak_image_source_get_labels (image_source); +- flatpak_oci_parse_commit_labels (labels, NULL, NULL, NULL, +- &target_ref, NULL, NULL, NULL); +- if (target_ref == NULL) +- { +- g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, +- "The OCI image didn't specify a ref, use --ref to specify one"); +- return NULL; +- } ++ ref = flatpak_image_source_get_ref (image_source); + + commit_checksum = flatpak_pull_from_oci (repo, image_source, NULL, +- NULL, target_ref, FLATPAK_PULL_FLAGS_NONE, NULL, NULL, cancellable, error); ++ NULL, ref, FLATPAK_PULL_FLAGS_NONE, ++ NULL, NULL, cancellable, error); + if (commit_checksum == NULL) + return NULL; + +- g_print (_("Importing %s (%s)\n"), target_ref, commit_checksum); ++ g_print (_("Importing %s (%s)\n"), ref, commit_checksum); + + return g_strdup (commit_checksum); + } +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index b0c29a70..e1224170 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -1092,12 +1092,7 @@ flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, + VarRefInfoRef latest_rev_info; + VarMetadataRef metadata; + const char *oci_repository = NULL; +- GHashTable *labels; +- g_autofree char *subject = NULL; +- g_autofree char *body = NULL; +- g_autofree char *manifest_ref = NULL; +- g_autofree char *parent = NULL; +- guint64 timestamp = 0; ++ const char *parent = NULL; + g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + g_autoptr(GVariant) metadata_v = NULL; + +@@ -1122,30 +1117,26 @@ flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, + if (image_source == NULL) + return NULL; + +- labels = flatpak_image_source_get_labels (image_source); +- if (labels) +- flatpak_oci_parse_commit_labels (labels, ×tamp, +- &subject, &body, +- &manifest_ref, NULL, &parent, +- metadata_builder); +- +- +- if (g_strcmp0 (manifest_ref, ref) != 0) ++ if (g_strcmp0 (flatpak_image_source_get_ref (image_source), ref) != 0) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Commit has no requested ref ‘%s’ in ref binding metadata"), ref); + return NULL; + } + ++ flatpak_image_source_build_commit_metadata (image_source, metadata_builder); + metadata_v = g_variant_ref_sink (g_variant_builder_end (metadata_builder)); + ++ parent = flatpak_image_source_get_parent_commit (image_source); ++ + /* This isn't going to be exactly the same as the reconstructed one from the pull, because we don't have the contents, but its useful to get metadata */ + return + g_variant_ref_sink (g_variant_new ("(@a{sv}@ay@a(say)sst@ay@ay)", + metadata_v, + parent ? ostree_checksum_to_bytes_v (parent) : g_variant_new_from_data (G_VARIANT_TYPE ("ay"), NULL, 0, FALSE, NULL, NULL), + g_variant_new_array (G_VARIANT_TYPE ("(say)"), NULL, 0), +- subject, body, +- GUINT64_TO_BE (timestamp), ++ flatpak_image_source_get_commit_subject (image_source), ++ flatpak_image_source_get_commit_body (image_source), ++ GUINT64_TO_BE (flatpak_image_source_get_commit_timestamp (image_source)), + ostree_checksum_to_bytes_v ("0000000000000000000000000000000000000000000000000000000000000000"), + ostree_checksum_to_bytes_v ("0000000000000000000000000000000000000000000000000000000000000000"))); + } +diff --git a/common/flatpak-image-source-private.h b/common/flatpak-image-source-private.h +index ebd856a4..fe8f02ba 100644 +--- a/common/flatpak-image-source-private.h ++++ b/common/flatpak-image-source-private.h +@@ -52,6 +52,15 @@ FlatpakOciManifest *flatpak_image_source_get_manifest (FlatpakImageSource + size_t flatpak_image_source_get_manifest_size (FlatpakImageSource *self); + FlatpakOciImage *flatpak_image_source_get_image_config (FlatpakImageSource *self); + +-GHashTable *flatpak_image_source_get_labels (FlatpakImageSource *self); ++const char *flatpak_image_source_get_ref (FlatpakImageSource *self); ++const char *flatpak_image_source_get_metadata (FlatpakImageSource *self); ++const char *flatpak_image_source_get_commit (FlatpakImageSource *self); ++const char *flatpak_image_source_get_parent_commit (FlatpakImageSource *self); ++guint64 flatpak_image_source_get_commit_timestamp (FlatpakImageSource *self); ++const char *flatpak_image_source_get_commit_subject (FlatpakImageSource *self); ++const char *flatpak_image_source_get_commit_body (FlatpakImageSource *self); ++ ++void flatpak_image_source_build_commit_metadata (FlatpakImageSource *self, ++ GVariantBuilder *metadata_builder); + + #endif /* __FLATPAK_IMAGE_SOURCE_H__ */ +diff --git a/common/flatpak-image-source.c b/common/flatpak-image-source.c +index 22497ccf..feb4fe27 100644 +--- a/common/flatpak-image-source.c ++++ b/common/flatpak-image-source.c +@@ -74,7 +74,6 @@ flatpak_image_source_new (FlatpakOciRegistry *registry, + { + g_autoptr(FlatpakImageSource) self = NULL; + g_autoptr(FlatpakOciVersioned) versioned = NULL; +- GHashTable *labels; + + self = g_object_new (FLATPAK_TYPE_IMAGE_SOURCE, NULL); + self->registry = g_object_ref (registry); +@@ -107,8 +106,7 @@ flatpak_image_source_new (FlatpakOciRegistry *registry, + if (self->image_config == NULL) + return NULL; + +- labels = flatpak_image_source_get_labels (self); +- if (!g_hash_table_contains (labels, "org.flatpak.ref")) ++ if (flatpak_image_source_get_ref (self) == NULL) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("No org.flatpak.ref found in image")); + return NULL; +@@ -220,8 +218,94 @@ flatpak_image_source_get_image_config (FlatpakImageSource *self) + return self->image_config; + } + +-GHashTable * +-flatpak_image_source_get_labels (FlatpakImageSource *self) ++const char * ++flatpak_image_source_get_ref (FlatpakImageSource *self) ++{ ++ GHashTable *labels = flatpak_oci_image_get_labels (self->image_config); ++ ++ return g_hash_table_lookup (labels, "org.flatpak.ref"); ++} ++ ++const char * ++flatpak_image_source_get_metadata (FlatpakImageSource *self) ++{ ++ GHashTable *labels = flatpak_oci_image_get_labels (self->image_config); ++ ++ return g_hash_table_lookup (labels, "org.flatpak.metadata"); ++} ++ ++const char * ++flatpak_image_source_get_commit (FlatpakImageSource *self) ++{ ++ GHashTable *labels = flatpak_oci_image_get_labels (self->image_config); ++ ++ return g_hash_table_lookup (labels, "org.flatpak.commit"); ++} ++ ++const char * ++flatpak_image_source_get_parent_commit (FlatpakImageSource *self) ++{ ++ GHashTable *labels = flatpak_oci_image_get_labels (self->image_config); ++ ++ return g_hash_table_lookup (labels, "org.flatpak.parent-commit"); ++} ++ ++guint64 ++flatpak_image_source_get_commit_timestamp (FlatpakImageSource *self) + { +- return flatpak_oci_image_get_labels (self->image_config); ++ GHashTable *labels = flatpak_oci_image_get_labels (self->image_config); ++ const char *oci_timestamp = g_hash_table_lookup (labels, "org.flatpak.timestamp"); ++ ++ if (oci_timestamp != NULL) ++ return g_ascii_strtoull (oci_timestamp, NULL, 10); ++ else ++ return 0; ++} ++ ++const char * ++flatpak_image_source_get_commit_subject (FlatpakImageSource *self) ++{ ++ GHashTable *labels = flatpak_oci_image_get_labels (self->image_config); ++ ++ return g_hash_table_lookup (labels, "org.flatpak.subject"); ++} ++ ++const char * ++flatpak_image_source_get_commit_body (FlatpakImageSource *self) ++{ ++ GHashTable *labels = flatpak_oci_image_get_labels (self->image_config); ++ ++ return g_hash_table_lookup (labels, "org.flatpak.body"); ++} ++ ++void ++flatpak_image_source_build_commit_metadata (FlatpakImageSource *self, ++ GVariantBuilder *metadata_builder) ++{ ++ GHashTable *labels = flatpak_oci_image_get_labels (self->image_config); ++ GHashTableIter iter; ++ const char *key; ++ const char *value; ++ ++ g_hash_table_iter_init (&iter, labels); ++ while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value)) ++ { ++ g_autoptr(GVariant) data = NULL; ++ uint8_t *bin; ++ size_t bin_len; ++ ++ if (!g_str_has_prefix (key, "org.flatpak.commit-metadata.")) ++ continue; ++ ++ key += strlen ("org.flatpak.commit-metadata."); ++ ++ bin = g_base64_decode (value, &bin_len); ++ data = g_variant_ref_sink (g_variant_new_from_data (G_VARIANT_TYPE ("v"), ++ bin, ++ bin_len, ++ FALSE, ++ g_free, ++ bin)); ++ g_variant_builder_add (metadata_builder, "{s@v}", key, data); ++ } + } +diff --git a/common/flatpak-json-oci-private.h b/common/flatpak-json-oci-private.h +index edb22eeb..87573c17 100644 +--- a/common/flatpak-json-oci-private.h ++++ b/common/flatpak-json-oci-private.h +@@ -246,14 +246,6 @@ void flatpak_oci_add_labels_for_commit (GHashTable *labels, + const char *ref, + const char *commit, + GVariant *commit_data); +-void flatpak_oci_parse_commit_labels (GHashTable *labels, +- guint64 *out_timestamp, +- char **out_subject, +- char **out_body, +- char **out_ref, +- char **out_commit, +- char **out_parent_commit, +- GVariantBuilder *metadata_builder); + + #define FLATPAK_TYPE_OCI_SIGNATURE flatpak_oci_signature_get_type () + G_DECLARE_FINAL_TYPE (FlatpakOciSignature, flatpak_oci_signature, FLATPAK, OCI_SIGNATURE, FlatpakJson) +diff --git a/common/flatpak-json-oci.c b/common/flatpak-json-oci.c +index 42647d11..2b55b9de 100644 +--- a/common/flatpak-json-oci.c ++++ b/common/flatpak-json-oci.c +@@ -927,72 +927,6 @@ flatpak_oci_add_labels_for_commit (GHashTable *labels, + } + } + +-void +-flatpak_oci_parse_commit_labels (GHashTable *labels, +- guint64 *out_timestamp, +- char **out_subject, +- char **out_body, +- char **out_ref, +- char **out_commit, +- char **out_parent_commit, +- GVariantBuilder *metadata_builder) +-{ +- const char *oci_timestamp, *oci_subject, *oci_body, *oci_parent_commit, *oci_commit, *oci_ref; +- GHashTableIter iter; +- gpointer _key, _value; +- +- oci_ref = g_hash_table_lookup (labels, "org.flatpak.ref"); +- +- /* Early return if this is not a flatpak manifest */ +- if (oci_ref == NULL) +- return; +- +- if (oci_ref != NULL && out_ref != NULL && *out_ref == NULL) +- *out_ref = g_strdup (oci_ref); +- +- oci_commit = g_hash_table_lookup (labels, "org.flatpak.commit"); +- if (oci_commit != NULL && out_commit != NULL && *out_commit == NULL) +- *out_commit = g_strdup (oci_commit); +- +- oci_parent_commit = g_hash_table_lookup (labels, "org.flatpak.parent-commit"); +- if (oci_parent_commit != NULL && out_parent_commit != NULL && *out_parent_commit == NULL) +- *out_parent_commit = g_strdup (oci_parent_commit); +- +- oci_timestamp = g_hash_table_lookup (labels, "org.flatpak.timestamp"); +- if (oci_timestamp != NULL && out_timestamp != NULL && *out_timestamp == 0) +- *out_timestamp = g_ascii_strtoull (oci_timestamp, NULL, 10); +- +- oci_subject = g_hash_table_lookup (labels, "org.flatpak.subject"); +- if (oci_subject != NULL && out_subject != NULL && *out_subject == NULL) +- *out_subject = g_strdup (oci_subject); +- +- oci_body = g_hash_table_lookup (labels, "org.flatpak.body"); +- if (oci_body != NULL && out_body != NULL && *out_body == NULL) +- *out_body = g_strdup (oci_body); +- +- if (metadata_builder) +- { +- g_hash_table_iter_init (&iter, labels); +- while (g_hash_table_iter_next (&iter, &_key, &_value)) +- { +- const char *key = _key; +- const char *value = _value; +- guchar *bin; +- gsize bin_len; +- g_autoptr(GVariant) data = NULL; +- +- if (!g_str_has_prefix (key, "org.flatpak.commit-metadata.")) +- continue; +- key += strlen ("org.flatpak.commit-metadata."); +- +- bin = g_base64_decode (value, &bin_len); +- data = g_variant_ref_sink (g_variant_new_from_data (G_VARIANT_TYPE ("v"), bin, bin_len, FALSE, +- g_free, bin)); +- g_variant_builder_add (metadata_builder, "{s@v}", key, data); +- } +- } +-} +- + + G_DEFINE_TYPE (FlatpakOciSignature, flatpak_oci_signature, FLATPAK_TYPE_JSON); + +diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c +index 11b5f98d..ad05c9f0 100644 +--- a/common/flatpak-oci-registry.c ++++ b/common/flatpak-oci-registry.c +@@ -3610,28 +3610,18 @@ flatpak_pull_from_oci (OstreeRepo *repo, + g_autofree char *old_diffid = NULL; + g_autofree char *commit_checksum = NULL; + const char *parent = NULL; +- g_autofree char *subject = NULL; +- g_autofree char *body = NULL; +- g_autofree char *manifest_ref = NULL; ++ const char *manifest_ref = NULL; + g_autofree char *full_ref = NULL; + const char *diffid; +- guint64 timestamp = 0; + FlatpakOciPullProgressData progress_data = { progress_cb, progress_user_data }; + g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + g_autoptr(GVariant) metadata = NULL; +- GHashTable *labels; + int n_layers; + int i; + +- g_assert (ref != NULL); + g_assert (g_str_has_prefix (digest, "sha256:")); + +- labels = flatpak_oci_image_get_labels (image_config); +- if (labels) +- flatpak_oci_parse_commit_labels (labels, ×tamp, +- &subject, &body, +- &manifest_ref, NULL, NULL, +- metadata_builder); ++ manifest_ref = flatpak_image_source_get_ref (image_source); + + if (manifest_ref == NULL) + { +@@ -3639,12 +3629,18 @@ flatpak_pull_from_oci (OstreeRepo *repo, + return NULL; + } + +- if (strcmp (manifest_ref, ref) != 0) ++ if (ref == NULL) ++ { ++ ref = manifest_ref; ++ } ++ else if (g_strcmp0 (manifest_ref, ref) != 0) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Wrong ref (%s) specified for OCI image %s, expected %s"), manifest_ref, digest, ref); + return NULL; + } + ++ flatpak_image_source_build_commit_metadata (image_source, metadata_builder); ++ + g_variant_builder_add (metadata_builder, "{s@v}", "xa.alt-id", + g_variant_new_variant (g_variant_new_string (digest + strlen ("sha256:")))); + +@@ -3818,11 +3814,11 @@ flatpak_pull_from_oci (OstreeRepo *repo, + metadata = g_variant_ref_sink (g_variant_builder_end (metadata_builder)); + if (!ostree_repo_write_commit_with_time (repo, + parent, +- subject, +- body, ++ flatpak_image_source_get_commit_subject (image_source), ++ flatpak_image_source_get_commit_body (image_source), + metadata, + OSTREE_REPO_FILE (archive_root), +- timestamp, ++ flatpak_image_source_get_commit_timestamp (image_source), + &commit_checksum, + cancellable, error)) + goto error; +-- +2.47.1 + +From c13709a1e320b9f7e5ff31b2284e372ba6ee08f4 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 18 Dec 2024 00:32:32 +0100 +Subject: [PATCH 03/12]transaction: Typedef structs directly + +--- + common/flatpak-transaction.c | 12 ++++-------- + 1 file changed, 4 insertions(+), 8 deletions(-) + +diff --git a/common/flatpak-transaction.c b/common/flatpak-transaction.c +index 7339acab..8a16cc1e 100644 +--- a/common/flatpak-transaction.c ++++ b/common/flatpak-transaction.c +@@ -138,15 +138,11 @@ struct _FlatpakTransactionOperation + GPtrArray *related_to_ops; /* (element-type FlatpakTransactionOperation) (nullable) */ + }; + +-typedef struct _FlatpakTransactionPrivate FlatpakTransactionPrivate; +- +-typedef struct _BundleData BundleData; +- +-struct _BundleData ++typedef struct _BundleData + { + GFile *file; + GBytes *gpg_data; +-}; ++} BundleData; + + typedef struct { + FlatpakTransaction *transaction; +@@ -157,7 +153,7 @@ typedef struct { + GVariant *results; + } RequestData; + +-struct _FlatpakTransactionPrivate ++typedef struct _FlatpakTransactionPrivate + { + GObject parent; + +@@ -198,7 +194,7 @@ struct _FlatpakTransactionPrivate + + gboolean needs_resolve; + gboolean needs_tokens; +-}; ++} FlatpakTransactionPrivate; + + enum { + NEW_OPERATION, +-- +2.47.1 + +From 8d6959c27c5b992750cbcc2fefb016a3d58db58c Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 18 Dec 2024 00:38:15 +0100 +Subject: [PATCH 04/12]transaction: Use g_clear_pointer/object functions for + op finalize + +--- + common/flatpak-transaction.c | 45 +++++++++++++++--------------------- + 1 file changed, 18 insertions(+), 27 deletions(-) + +diff --git a/common/flatpak-transaction.c b/common/flatpak-transaction.c +index 8a16cc1e..7039d86e 100644 +--- a/common/flatpak-transaction.c ++++ b/common/flatpak-transaction.c +@@ -593,34 +593,25 @@ flatpak_transaction_operation_finalize (GObject *object) + { + FlatpakTransactionOperation *self = (FlatpakTransactionOperation *) object; + +- g_free (self->remote); +- flatpak_decomposed_unref (self->ref); +- g_free (self->commit); +- g_strfreev (self->subpaths); ++ g_clear_pointer (&self->remote, g_free); ++ g_clear_pointer (&self->ref, flatpak_decomposed_unref); ++ g_clear_pointer (&self->commit, g_free); ++ g_clear_pointer (&self->subpaths, g_strfreev); + g_clear_object (&self->bundle); +- g_free (self->eol); +- g_free (self->eol_rebase); +- if (self->previous_ids) +- g_strfreev (self->previous_ids); +- if (self->external_metadata) +- g_bytes_unref (self->external_metadata); +- g_free (self->resolved_commit); +- if (self->resolved_sideload_path) +- g_object_unref (self->resolved_sideload_path); +- if (self->resolved_metadata) +- g_bytes_unref (self->resolved_metadata); +- if (self->resolved_metakey) +- g_key_file_unref (self->resolved_metakey); +- if (self->resolved_old_metadata) +- g_bytes_unref (self->resolved_old_metadata); +- if (self->resolved_old_metakey) +- g_key_file_unref (self->resolved_old_metakey); +- g_free (self->resolved_token); +- g_list_free (self->run_before_ops); +- if (self->related_to_ops) +- g_ptr_array_unref (self->related_to_ops); +- if (self->summary_metadata) +- g_variant_unref (self->summary_metadata); ++ g_clear_pointer (&self->eol, g_free); ++ g_clear_pointer (&self->eol_rebase, g_free); ++ g_clear_pointer (&self->previous_ids, g_strfreev); ++ g_clear_pointer (&self->external_metadata, g_bytes_unref); ++ g_clear_pointer (&self->resolved_commit, g_free); ++ g_clear_object (&self->resolved_sideload_path); ++ g_clear_pointer (&self->resolved_metadata, g_bytes_unref); ++ g_clear_pointer (&self->resolved_metakey, g_key_file_unref); ++ g_clear_pointer (&self->resolved_old_metadata, g_bytes_unref); ++ g_clear_pointer (&self->resolved_old_metakey, g_key_file_unref); ++ g_clear_pointer (&self->resolved_token, g_free); ++ g_clear_pointer (&self->run_before_ops, g_list_free); ++ g_clear_pointer (&self->related_to_ops, g_ptr_array_unref); ++ g_clear_pointer (&self->summary_metadata, g_variant_unref); + + G_OBJECT_CLASS (flatpak_transaction_operation_parent_class)->finalize (object); + } +-- +2.47.1 + +From b235bb419e87d2a9c13e29a55f9cd32a3089f15a Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Wed, 18 Dec 2024 00:46:33 +0100 +Subject: [PATCH 05/12]image-source: Add flatpak_image_source_new_for_location + +Which allows one to create an image source from a container location. + +It also adds a new FlatpakDockerReference to access different parts of a +docker reference and changes to FlatpakOciIndex to get a manifest for a +specific architecture. + +This will become useful in the next commit when we're going to add +support for installing OCI images. +--- + common/flatpak-docker-reference-private.h | 20 ++++ + common/flatpak-docker-reference.c | 140 ++++++++++++++++++++++ + common/flatpak-image-source-private.h | 3 + + common/flatpak-image-source.c | 122 +++++++++++++++++++ + common/flatpak-json-oci-private.h | 4 +- + common/flatpak-json-oci.c | 21 ++++ + common/meson.build | 1 + + 7 files changed, 310 insertions(+), 1 deletion(-) + create mode 100644 common/flatpak-docker-reference-private.h + create mode 100644 common/flatpak-docker-reference.c + +diff --git a/common/flatpak-docker-reference-private.h b/common/flatpak-docker-reference-private.h +new file mode 100644 +index 00000000..8205064c +--- /dev/null ++++ b/common/flatpak-docker-reference-private.h +@@ -0,0 +1,20 @@ ++#ifndef __FLATPAK_DOCKER_REFERENCE_H__ ++#define __FLATPAK_DOCKER_REFERENCE_H__ ++ ++#include ++ ++typedef struct _FlatpakDockerReference FlatpakDockerReference; ++ ++FlatpakDockerReference *flatpak_docker_reference_parse (const char *reference_str, ++ GError **error); ++ ++const char *flatpak_docker_reference_get_uri (FlatpakDockerReference *reference); ++const char *flatpak_docker_reference_get_repository (FlatpakDockerReference *reference); ++const char *flatpak_docker_reference_get_tag (FlatpakDockerReference *reference); ++const char *flatpak_docker_reference_get_digest (FlatpakDockerReference *reference); ++ ++void flatpak_docker_reference_free (FlatpakDockerReference *reference); ++ ++G_DEFINE_AUTOPTR_CLEANUP_FUNC(FlatpakDockerReference, flatpak_docker_reference_free); ++ ++#endif /* __FLATPAK_DOCKER_REFERENCE_H__ */ +diff --git a/common/flatpak-docker-reference.c b/common/flatpak-docker-reference.c +new file mode 100644 +index 00000000..6df3011f +--- /dev/null ++++ b/common/flatpak-docker-reference.c +@@ -0,0 +1,140 @@ ++/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s: ++ * Copyright © 2024 Red Hat, Inc ++ * ++ * This program 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. ++ * ++ * This library is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library. If not, see . ++ * ++ * Authors: ++ * Owen Taylor ++ */ ++ ++#include "flatpak-docker-reference-private.h" ++#include "flatpak-utils-private.h" ++ ++struct _FlatpakDockerReference ++{ ++ char *uri; ++ char *repository; ++ char *tag; ++ char *digest; ++}; ++ ++/* ++ * Parsing here is loosely based off: ++ * ++ * https://github.com/containers/image/tree/main/docker/reference ++ * ++ * The major simplification is that we require a domain component, and ++ * don't have any default domain. This removes ambiguity between domains and paths ++ * and makes parsing much simpler. We also don't normalize single component ++ * paths (e.g. ubuntu => library/ubuntu.) ++ */ ++ ++#define TAG "[0-9A-Za-z_][0-9A-Za-z_-]{0,127}" ++#define DIGEST "[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}" ++#define REMAINDER_TAG_AND_DIGEST_RE "^(.*?)(:" TAG ")?" "(@" DIGEST ")?$" ++ ++static GRegex * ++get_remainder_tag_and_digest_regex (void) ++{ ++ static gsize regex = 0; ++ ++ if (g_once_init_enter (®ex)) ++ { ++ g_autoptr(GRegex) compiled = g_regex_new (REMAINDER_TAG_AND_DIGEST_RE, ++ G_REGEX_DEFAULT, ++ G_REGEX_MATCH_DEFAULT, ++ NULL); ++ g_once_init_leave (®ex, (gsize)g_steal_pointer (&compiled)); ++ } ++ ++ return (GRegex *)regex; ++} ++ ++FlatpakDockerReference * ++flatpak_docker_reference_parse (const char *reference_str, ++ GError **error) ++{ ++ g_autoptr(FlatpakDockerReference) reference = g_new0 (FlatpakDockerReference, 1); ++ GRegex *regex = get_remainder_tag_and_digest_regex (); ++ g_autoptr(GMatchInfo) match_info = NULL; ++ g_autofree char *tag_match = NULL; ++ g_autofree char *digest_match = NULL; ++ g_autofree char *remainder = NULL; ++ g_autofree char *domain = NULL; ++ gboolean matched; ++ const char *slash; ++ ++ matched = g_regex_match (regex, reference_str, G_REGEX_MATCH_DEFAULT, &match_info); ++ g_assert (matched); ++ ++ tag_match = g_match_info_fetch (match_info, 2); ++ if (tag_match[0] == '\0') ++ reference->tag = NULL; ++ else ++ reference->tag = g_strdup (tag_match + 1); ++ ++ digest_match = g_match_info_fetch (match_info, 3); ++ if (digest_match[0] == '\0') ++ reference->digest = NULL; ++ else ++ reference->digest = g_strdup (digest_match + 1); ++ ++ remainder = g_match_info_fetch (match_info, 1); ++ slash = strchr (remainder, '/'); ++ if (slash == NULL || slash == reference_str || *slash == '\0') ++ { ++ flatpak_fail(error, "Can't parse %s into /", remainder); ++ return NULL; ++ } ++ ++ domain = g_strndup (remainder, slash - remainder); ++ reference->uri = g_strconcat ("https://", domain, NULL); ++ reference->repository = g_strdup (slash + 1); ++ ++ return g_steal_pointer (&reference); ++} ++ ++const char * ++flatpak_docker_reference_get_uri (FlatpakDockerReference *reference) ++{ ++ return reference->uri; ++} ++ ++const char * ++flatpak_docker_reference_get_repository (FlatpakDockerReference *reference) ++{ ++ return reference->repository; ++} ++ ++const char * ++flatpak_docker_reference_get_tag (FlatpakDockerReference *reference) ++{ ++ return reference->tag; ++} ++ ++const char * ++flatpak_docker_reference_get_digest (FlatpakDockerReference *reference) ++{ ++ return reference->digest; ++} ++ ++void ++flatpak_docker_reference_free (FlatpakDockerReference *reference) ++{ ++ g_clear_pointer (&reference->uri, g_free); ++ g_clear_pointer (&reference->repository, g_free); ++ g_clear_pointer (&reference->tag, g_free); ++ g_clear_pointer (&reference->digest, g_free); ++ g_free (reference); ++} +diff --git a/common/flatpak-image-source-private.h b/common/flatpak-image-source-private.h +index fe8f02ba..b1e5d2fa 100644 +--- a/common/flatpak-image-source-private.h ++++ b/common/flatpak-image-source-private.h +@@ -41,6 +41,9 @@ FlatpakImageSource *flatpak_image_source_new_remote (const char *uri, + const char *digest, + GCancellable *cancellable, + GError **error); ++FlatpakImageSource *flatpak_image_source_new_for_location (const char *location, ++ GCancellable *cancellable, ++ GError **error); + + void flatpak_image_source_set_token (FlatpakImageSource *self, + const char *token); +diff --git a/common/flatpak-image-source.c b/common/flatpak-image-source.c +index feb4fe27..0503de5d 100644 +--- a/common/flatpak-image-source.c ++++ b/common/flatpak-image-source.c +@@ -20,6 +20,7 @@ + + #include + ++#include "flatpak-docker-reference-private.h" + #include "flatpak-image-source-private.h" + #include "flatpak-oci-registry-private.h" + +@@ -175,6 +176,127 @@ flatpak_image_source_new_remote (const char *uri, + return flatpak_image_source_new (registry, oci_repository, digest, cancellable, error); + } + ++/* Parse an oci: or oci-archive: image location into a path ++ * and an optional reference ++ */ ++static void ++get_path_and_reference (const char *image_location, ++ GFile **path, ++ char **reference) ++{ ++ g_autofree char *path_str = NULL; ++ const char *bare; ++ const char *colon; ++ ++ colon = strchr (image_location, ':'); ++ g_assert (colon != NULL); ++ ++ bare = colon + 1; ++ colon = strchr (bare, ':'); ++ if (colon) ++ { ++ path_str = g_strndup (bare, colon - bare); ++ *reference = g_strdup (colon + 1); ++ } ++ else ++ { ++ path_str = g_strdup (bare); ++ *reference = NULL; ++ } ++ ++ *path = g_file_new_for_path (path_str); ++} ++ ++FlatpakImageSource * ++flatpak_image_source_new_for_location (const char *location, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ if (g_str_has_prefix (location, "oci:")) ++ { ++ g_autoptr(GFile) path = NULL; ++ g_autofree char *reference = NULL; ++ ++ get_path_and_reference (location, &path, &reference); ++ ++ return flatpak_image_source_new_local (path, reference, cancellable, error); ++ } ++ else if (g_str_has_prefix (location, "docker:")) ++ { ++ g_autoptr(FlatpakOciRegistry) registry = NULL; ++ g_autoptr(FlatpakDockerReference) docker_reference = NULL; ++ g_autofree char *local_digest = NULL; ++ const char *repository = NULL; ++ ++ if (!g_str_has_prefix (location, "docker://")) ++ { ++ flatpak_fail (error, "docker: location must start docker://"); ++ return NULL; ++ } ++ ++ docker_reference = flatpak_docker_reference_parse (location + 9, error); ++ if (docker_reference == NULL) ++ return NULL; ++ ++ registry = flatpak_oci_registry_new (flatpak_docker_reference_get_uri (docker_reference), ++ FALSE, -1, cancellable, error); ++ if (registry == NULL) ++ return NULL; ++ ++ repository = flatpak_docker_reference_get_repository (docker_reference); ++ ++ local_digest = g_strdup (flatpak_docker_reference_get_digest (docker_reference)); ++ if (local_digest == NULL) ++ { ++ g_autoptr(GBytes) bytes = NULL; ++ g_autoptr(FlatpakOciVersioned) versioned = NULL; ++ const char *tag = flatpak_docker_reference_get_tag (docker_reference); ++ ++ if (tag == NULL) ++ tag = "latest"; ++ ++ bytes = flatpak_oci_registry_load_blob (registry, repository, TRUE, tag, ++ NULL, NULL, cancellable, error); ++ if (!bytes) ++ return NULL; ++ ++ versioned = flatpak_oci_versioned_from_json (bytes, NULL, error); ++ if (!versioned) ++ return NULL; ++ ++ if (FLATPAK_IS_OCI_MANIFEST (versioned)) ++ { ++ g_autofree char *checksum = NULL; ++ ++ checksum = g_compute_checksum_for_bytes (G_CHECKSUM_SHA256, bytes); ++ local_digest = g_strconcat ("sha256:", checksum, NULL); ++ } ++ else if (FLATPAK_IS_OCI_INDEX (versioned)) ++ { ++ const char *oci_arch = flatpak_arch_to_oci_arch (flatpak_get_arch ()); ++ FlatpakOciManifestDescriptor *descriptor; ++ ++ descriptor = flatpak_oci_index_get_manifest_for_arch (FLATPAK_OCI_INDEX (versioned), oci_arch); ++ if (descriptor == NULL) ++ { ++ flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, ++ "Can't find manifest for %s in image index", oci_arch); ++ return NULL; ++ } ++ ++ local_digest = g_strdup (descriptor->parent.digest); ++ } ++ } ++ ++ return flatpak_image_source_new (registry, repository, local_digest, cancellable, error); ++ } ++ else ++ { ++ flatpak_fail (error, "unsupported image location: %s", location); ++ return NULL; ++ } ++} ++ + void + flatpak_image_source_set_token (FlatpakImageSource *self, + const char *token) +diff --git a/common/flatpak-json-oci-private.h b/common/flatpak-json-oci-private.h +index 87573c17..e8924ee4 100644 +--- a/common/flatpak-json-oci-private.h ++++ b/common/flatpak-json-oci-private.h +@@ -166,8 +166,10 @@ gboolean flatpak_oci_index_remove_manifest (FlatpakOciIndex + FlatpakOciManifestDescriptor *flatpak_oci_index_get_manifest (FlatpakOciIndex *self, + const char *ref); + FlatpakOciManifestDescriptor *flatpak_oci_index_get_only_manifest (FlatpakOciIndex *self); +-int flatpak_oci_index_get_n_manifests (FlatpakOciIndex *self); ++FlatpakOciManifestDescriptor *flatpak_oci_index_get_manifest_for_arch (FlatpakOciIndex *self, ++ const char *oci_arch); + ++int flatpak_oci_index_get_n_manifests (FlatpakOciIndex *self); + /* Only useful for delta index */ + FlatpakOciDescriptor *flatpak_oci_index_find_delta_for (FlatpakOciIndex *delta_index, + const char *for_digest); +diff --git a/common/flatpak-json-oci.c b/common/flatpak-json-oci.c +index 2b55b9de..3a339a02 100644 +--- a/common/flatpak-json-oci.c ++++ b/common/flatpak-json-oci.c +@@ -577,6 +577,27 @@ flatpak_oci_index_get_only_manifest (FlatpakOciIndex *self) + return NULL; + } + ++FlatpakOciManifestDescriptor * ++flatpak_oci_index_get_manifest_for_arch (FlatpakOciIndex *self, ++ const char *oci_arch) ++{ ++ int i, found = -1; ++ ++ if (self->manifests == NULL) ++ return NULL; ++ ++ for (i = 0; self->manifests[i] != NULL; i++) ++ { ++ if (strcmp (self->manifests[i]->platform.architecture, oci_arch) == 0) ++ return self->manifests[i]; ++ } ++ ++ if (found >= 0) ++ return self->manifests[found]; ++ ++ return NULL; ++} ++ + gboolean + flatpak_oci_index_remove_manifest (FlatpakOciIndex *self, + const char *ref) +diff --git a/common/meson.build b/common/meson.build +index beed1f06..c14337be 100644 +--- a/common/meson.build ++++ b/common/meson.build +@@ -169,6 +169,7 @@ sources = [ + 'flatpak-context.c', + 'flatpak-dir.c', + 'flatpak-dir-utils.c', ++ 'flatpak-docker-reference.c', + 'flatpak-error.c', + 'flatpak-exports.c', + 'flatpak-glib-backports.c', +-- +2.47.1 + +From c9b992fda4ac3cd53c2f315d135823ac6929ad17 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Wed, 18 Dec 2024 00:52:27 +0100 +Subject: [PATCH 06/12]common: Add OCI image installation support + +--- + common/flatpak-dir-private.h | 3 + + common/flatpak-dir.c | 131 ++++++++++-------- + common/flatpak-installation.c | 4 +- + common/flatpak-transaction.c | 183 +++++++++++++++++++++++--- + common/flatpak-transaction.h | 4 + + system-helper/flatpak-system-helper.c | 6 +- + 6 files changed, 252 insertions(+), 79 deletions(-) + +diff --git a/common/flatpak-dir-private.h b/common/flatpak-dir-private.h +index 871f40b6..539476b8 100644 +--- a/common/flatpak-dir-private.h ++++ b/common/flatpak-dir-private.h +@@ -574,6 +574,7 @@ gboolean flatpak_dir_pull (Fla + const char *opt_rev, + const char **subpaths, + GFile *sideload_repo, ++ FlatpakImageSource *image_source, + GBytes *require_metadata, + const char *token, + OstreeRepo *repo, +@@ -694,6 +695,7 @@ gboolean flatpak_dir_install (Fla + const char **subpaths, + const char **previous_ids, + GFile *sideload_repo, ++ FlatpakImageSource *image_source, + GBytes *require_metadata, + const char *token, + FlatpakProgress *progress, +@@ -740,6 +742,7 @@ gboolean flatpak_dir_update (Fla + const char **opt_subpaths, + const char **opt_previous_ids, + GFile *sideload_repo, ++ FlatpakImageSource *image_source, + GBytes *require_metadata, + const char *token, + FlatpakProgress *progress, +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index e1224170..e8ff645b 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -123,7 +123,7 @@ static gboolean flatpak_dir_mirror_oci (FlatpakDir *self, + FlatpakRemoteState *state, + const char *ref, + const char *opt_rev, +- const char *skip_if_current_is, ++ FlatpakImageSource *opt_image_source, + const char *token, + FlatpakProgress *progress, + GCancellable *cancellable, +@@ -5307,7 +5307,7 @@ flatpak_dir_update_appstream (FlatpakDir *self, + if (child_repo == NULL) + return FALSE; + +- if (!flatpak_dir_pull (self, state, used_branch, appstream_commit, NULL, appstream_sideload_path, NULL, NULL, ++ if (!flatpak_dir_pull (self, state, used_branch, appstream_commit, NULL, appstream_sideload_path, NULL, NULL, NULL, + child_repo, FLATPAK_PULL_FLAGS_NONE, 0, + progress, cancellable, error)) + { +@@ -5351,7 +5351,7 @@ flatpak_dir_update_appstream (FlatpakDir *self, + } + + +- if (!flatpak_dir_pull (self, state, used_branch, appstream_commit, NULL, appstream_sideload_path, NULL, NULL, NULL, ++ if (!flatpak_dir_pull (self, state, used_branch, appstream_commit, NULL, appstream_sideload_path, NULL, NULL, NULL, NULL, + FLATPAK_PULL_FLAGS_NONE, OSTREE_REPO_PULL_FLAGS_NONE, progress, + cancellable, error)) + { +@@ -5903,7 +5903,7 @@ flatpak_dir_mirror_oci (FlatpakDir *self, + FlatpakRemoteState *state, + const char *ref, + const char *opt_rev, +- const char *skip_if_current_is, ++ FlatpakImageSource *opt_image_source, + const char *token, + FlatpakProgress *progress, + GCancellable *cancellable, +@@ -5911,40 +5911,42 @@ flatpak_dir_mirror_oci (FlatpakDir *self, + { + g_autoptr(FlatpakImageSource) image_source = NULL; + g_autofree char *oci_digest = NULL; +- g_autofree char *latest_rev = NULL; +- VarRefInfoRef latest_rev_info; +- VarMetadataRef metadata; +- const char *oci_repository = NULL; + const char *delta_url = NULL; +- const char *rev; + gboolean res; + +- /* We use the summary so that we can reuse any cached json */ +- if (!flatpak_remote_state_lookup_ref (state, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) +- return FALSE; +- if (latest_rev == NULL) +- return flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, +- _("Couldn't find latest checksum for ref %s in remote %s"), +- ref, state->remote_name); +- +- rev = opt_rev != NULL ? opt_rev : latest_rev; +- +- if (skip_if_current_is != NULL && strcmp (rev, skip_if_current_is) == 0) ++ if (opt_image_source) + { +- return flatpak_fail_error (error, FLATPAK_ERROR_ALREADY_INSTALLED, +- _("%s commit %s already installed"), +- ref, rev); ++ image_source = g_object_ref (opt_image_source); ++ oci_digest = g_strdup (flatpak_image_source_get_digest (image_source)); + } ++ else ++ { ++ g_autofree char *latest_rev = NULL; ++ VarRefInfoRef latest_rev_info; ++ VarMetadataRef metadata; ++ const char *oci_repository = NULL; ++ const char *rev; + +- metadata = var_ref_info_get_metadata (latest_rev_info); +- oci_repository = var_metadata_lookup_string (metadata, "xa.oci-repository", NULL); +- delta_url = var_metadata_lookup_string (metadata, "xa.delta-url", NULL); ++ /* We use the summary so that we can reuse any cached json */ ++ if (!flatpak_remote_state_lookup_ref (state, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) ++ return FALSE; ++ if (latest_rev == NULL) ++ return flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, ++ _("Couldn't find latest checksum for ref %s in remote %s"), ++ ref, state->remote_name); + +- oci_digest = g_strconcat ("sha256:", rev, NULL); ++ rev = opt_rev != NULL ? opt_rev : latest_rev; + +- image_source = flatpak_remote_state_new_image_source (state, oci_repository, oci_digest, token, cancellable, error); +- if (image_source == NULL) +- return FALSE; ++ metadata = var_ref_info_get_metadata (latest_rev_info); ++ oci_repository = var_metadata_lookup_string (metadata, "xa.oci-repository", NULL); ++ delta_url = var_metadata_lookup_string (metadata, "xa.delta-url", NULL); ++ ++ oci_digest = g_strconcat ("sha256:", rev, NULL); ++ ++ image_source = flatpak_remote_state_new_image_source (state, oci_repository, oci_digest, token, cancellable, error); ++ if (image_source == NULL) ++ return FALSE; ++ } + + flatpak_progress_start_oci_pull (progress); + +@@ -5964,6 +5966,7 @@ flatpak_dir_pull_oci (FlatpakDir *self, + FlatpakRemoteState *state, + const char *ref, + const char *opt_rev, ++ FlatpakImageSource *opt_image_source, + OstreeRepo *repo, + FlatpakPullFlags flatpak_flags, + OstreeRepoPullFlags flags, +@@ -5974,40 +5977,49 @@ flatpak_dir_pull_oci (FlatpakDir *self, + { + g_autoptr(FlatpakImageSource) image_source = NULL; + FlatpakOciRegistry *registry = NULL; +- const char *oci_repository = NULL; + const char *delta_url = NULL; + g_autofree char *oci_digest = NULL; + g_autofree char *checksum = NULL; +- VarRefInfoRef latest_rev_info; + g_autofree char *latest_alt_commit = NULL; +- VarMetadataRef metadata; +- g_autofree char *latest_rev = NULL; + G_GNUC_UNUSED g_autofree char *latest_commit = + flatpak_dir_read_latest (self, state->remote_name, ref, &latest_alt_commit, cancellable, NULL); + g_autofree char *name = NULL; + +- /* We use the summary so that we can reuse any cached json */ +- if (!flatpak_remote_state_lookup_ref (state, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) +- return FALSE; +- if (latest_rev == NULL) +- return flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, +- _("Couldn't find latest checksum for ref %s in remote %s"), +- ref, state->remote_name); ++ if (opt_image_source) ++ { ++ image_source = g_object_ref (opt_image_source); ++ oci_digest = g_strdup (flatpak_image_source_get_digest (image_source)); ++ } ++ else ++ { ++ VarMetadataRef metadata; ++ VarRefInfoRef latest_rev_info; ++ const char *oci_repository = NULL; ++ g_autofree char *latest_rev = NULL; + +- metadata = var_ref_info_get_metadata (latest_rev_info); +- oci_repository = var_metadata_lookup_string (metadata, "xa.oci-repository", NULL); +- delta_url = var_metadata_lookup_string (metadata, "xa.delta-url", NULL); ++ /* We use the summary so that we can reuse any cached json */ ++ if (!flatpak_remote_state_lookup_ref (state, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) ++ return FALSE; ++ if (latest_rev == NULL) ++ return flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, ++ _("Couldn't find latest checksum for ref %s in remote %s"), ++ ref, state->remote_name); ++ ++ metadata = var_ref_info_get_metadata (latest_rev_info); ++ oci_repository = var_metadata_lookup_string (metadata, "xa.oci-repository", NULL); ++ delta_url = var_metadata_lookup_string (metadata, "xa.delta-url", NULL); ++ ++ oci_digest = g_strconcat ("sha256:", opt_rev != NULL ? opt_rev : latest_rev, NULL); + +- oci_digest = g_strconcat ("sha256:", opt_rev != NULL ? opt_rev : latest_rev, NULL); ++ image_source = flatpak_remote_state_new_image_source (state, oci_repository, oci_digest, token, cancellable, error); ++ if (image_source == NULL) ++ return FALSE; ++ } + + /* Short circuit if we've already got this commit */ + if (latest_alt_commit != NULL && strcmp (oci_digest + strlen ("sha256:"), latest_alt_commit) == 0) + return TRUE; + +- image_source = flatpak_remote_state_new_image_source (state, oci_repository, oci_digest, token, cancellable, error); +- if (image_source == NULL) +- return FALSE; +- + if (repo == NULL) + repo = self->repo; + +@@ -6046,6 +6058,7 @@ flatpak_dir_pull (FlatpakDir *self, + const char *opt_rev, + const char **subpaths, + GFile *sideload_repo, ++ FlatpakImageSource *image_source, + GBytes *require_metadata, + const char *token, + OstreeRepo *repo, +@@ -6076,8 +6089,8 @@ flatpak_dir_pull (FlatpakDir *self, + if (repo == NULL && !flatpak_dir_repo_lock (self, &lock, LOCK_SH, cancellable, error)) + return FALSE; + +- if (flatpak_dir_get_remote_oci (self, state->remote_name)) +- return flatpak_dir_pull_oci (self, state, ref, opt_rev, repo, flatpak_flags, ++ if (image_source || flatpak_dir_get_remote_oci (self, state->remote_name)) ++ return flatpak_dir_pull_oci (self, state, ref, opt_rev, image_source, repo, flatpak_flags, + flags, token, progress, cancellable, error); + + if (!ostree_repo_remote_get_url (self->repo, +@@ -9838,6 +9851,7 @@ flatpak_dir_install (FlatpakDir *self, + const char **opt_subpaths, + const char **opt_previous_ids, + GFile *sideload_repo, ++ FlatpakImageSource *image_source, + GBytes *require_metadata, + const char *token, + FlatpakProgress *progress, +@@ -9902,7 +9916,8 @@ flatpak_dir_install (FlatpakDir *self, + + child_repo_path = g_file_get_path (registry_file); + +- if (!flatpak_dir_mirror_oci (self, registry, state, flatpak_decomposed_get_ref (ref), opt_commit, NULL, token, progress, cancellable, error)) ++ if (!flatpak_dir_mirror_oci (self, registry, state, flatpak_decomposed_get_ref (ref), ++ opt_commit, image_source, token, progress, cancellable, error)) + return FALSE; + } + else if (!gpg_verify_summary || !gpg_verify) +@@ -9996,7 +10011,7 @@ flatpak_dir_install (FlatpakDir *self, + + flatpak_flags |= FLATPAK_PULL_FLAGS_SIDELOAD_EXTRA_DATA; + +- if (!flatpak_dir_pull (self, state, flatpak_decomposed_get_ref (ref), opt_commit, subpaths, sideload_repo, require_metadata, token, ++ if (!flatpak_dir_pull (self, state, flatpak_decomposed_get_ref (ref), opt_commit, subpaths, sideload_repo, NULL, require_metadata, token, + child_repo, + flatpak_flags, + 0, +@@ -10071,7 +10086,8 @@ flatpak_dir_install (FlatpakDir *self, + + if (!no_pull) + { +- if (!flatpak_dir_pull (self, state, flatpak_decomposed_get_ref (ref), opt_commit, opt_subpaths, sideload_repo, require_metadata, token, NULL, ++ if (!flatpak_dir_pull (self, state, flatpak_decomposed_get_ref (ref), opt_commit, opt_subpaths, ++ sideload_repo, image_source, require_metadata, token, NULL, + flatpak_flags, OSTREE_REPO_PULL_FLAGS_NONE, + progress, cancellable, error)) + return FALSE; +@@ -10518,6 +10534,7 @@ flatpak_dir_update (FlatpakDir *self, + const char **opt_subpaths, + const char **opt_previous_ids, + GFile *sideload_repo, ++ FlatpakImageSource *image_source, + GBytes *require_metadata, + const char *token, + FlatpakProgress *progress, +@@ -10605,7 +10622,7 @@ flatpak_dir_update (FlatpakDir *self, + child_repo_path = g_file_get_path (registry_file); + + if (!flatpak_dir_mirror_oci (self, registry, state, flatpak_decomposed_get_ref (ref), +- commit, NULL, token, progress, cancellable, error)) ++ commit, image_source, token, progress, cancellable, error)) + return FALSE; + } + else if (!gpg_verify_summary || !gpg_verify) +@@ -10685,7 +10702,7 @@ flatpak_dir_update (FlatpakDir *self, + + flatpak_flags |= FLATPAK_PULL_FLAGS_SIDELOAD_EXTRA_DATA; + if (!flatpak_dir_pull (self, state, flatpak_decomposed_get_ref (ref), +- commit, subpaths, sideload_repo, require_metadata, token, ++ commit, subpaths, sideload_repo, NULL, require_metadata, token, + child_repo, + flatpak_flags, 0, + progress, cancellable, error)) +@@ -10751,7 +10768,7 @@ flatpak_dir_update (FlatpakDir *self, + if (!no_pull) + { + if (!flatpak_dir_pull (self, state, flatpak_decomposed_get_ref (ref), +- commit, subpaths, sideload_repo, require_metadata, token, ++ commit, subpaths, sideload_repo, image_source, require_metadata, token, + NULL, flatpak_flags, OSTREE_REPO_PULL_FLAGS_NONE, + progress, cancellable, error)) + return FALSE; +diff --git a/common/flatpak-installation.c b/common/flatpak-installation.c +index c7d2e1dd..3a8677e8 100644 +--- a/common/flatpak-installation.c ++++ b/common/flatpak-installation.c +@@ -1930,7 +1930,7 @@ flatpak_installation_install_full (FlatpakInstallation *self, + (flags & FLATPAK_INSTALL_FLAGS_NO_DEPLOY) != 0, + (flags & FLATPAK_INSTALL_FLAGS_NO_STATIC_DELTAS) != 0, + FALSE, FALSE, FALSE, state, +- ref, NULL, (const char **) subpaths, NULL, NULL, NULL, NULL, ++ ref, NULL, (const char **) subpaths, NULL, NULL, NULL, NULL, NULL, + progress, cancellable, error)) + return NULL; + +@@ -2098,7 +2098,7 @@ flatpak_installation_update_full (FlatpakInstallation *self, + (flags & FLATPAK_UPDATE_FLAGS_NO_STATIC_DELTAS) != 0, + FALSE, FALSE, FALSE, state, + ref, target_commit, +- (const char **) subpaths, NULL, NULL, NULL, NULL, ++ (const char **) subpaths, NULL, NULL, NULL, NULL, NULL, + progress, cancellable, error)) + return NULL; + +diff --git a/common/flatpak-transaction.c b/common/flatpak-transaction.c +index 7039d86e..ffee385e 100644 +--- a/common/flatpak-transaction.c ++++ b/common/flatpak-transaction.c +@@ -26,7 +26,9 @@ + #include "flatpak-auth-private.h" + #include "flatpak-dir-private.h" + #include "flatpak-error.h" ++#include "flatpak-image-source-private.h" + #include "flatpak-installation-private.h" ++#include "flatpak-oci-registry-private.h" + #include "flatpak-progress-private.h" + #include "flatpak-repo-utils-private.h" + #include "flatpak-transaction-private.h" +@@ -103,6 +105,7 @@ struct _FlatpakTransactionOperation + char **subpaths; + char **previous_ids; + char *commit; ++ FlatpakImageSource *image_source; + GFile *bundle; + GBytes *external_metadata; + FlatpakTransactionOperationType kind; +@@ -115,6 +118,7 @@ struct _FlatpakTransactionOperation + gboolean resolved; + char *resolved_commit; + GFile *resolved_sideload_path; ++ FlatpakImageSource *resolved_image_source; + GBytes *resolved_metadata; + GKeyFile *resolved_metakey; + GBytes *resolved_old_metadata; +@@ -144,6 +148,11 @@ typedef struct _BundleData + GBytes *gpg_data; + } BundleData; + ++typedef struct _ImageData ++{ ++ char *image_location; ++} ImageData; ++ + typedef struct { + FlatpakTransaction *transaction; + const char *remote; +@@ -168,6 +177,7 @@ typedef struct _FlatpakTransactionPrivate + + GList *flatpakrefs; /* GKeyFiles */ + GList *bundles; /* BundleData */ ++ GList *images; /* ImageData */ + + guint next_request_id; + guint active_request_id; +@@ -260,6 +270,23 @@ bundle_data_free (BundleData *data) + g_free (data); + } + ++static ImageData * ++image_data_new (const char *image_location) ++{ ++ ImageData *data = g_new0 (ImageData, 1); ++ ++ data->image_location = g_strdup (image_location); ++ ++ return data; ++} ++ ++static void ++image_data_free (ImageData *data) ++{ ++ g_clear_pointer (&data->image_location, g_free); ++ g_free (data); ++} ++ + static guint progress_signals[LAST_SIGNAL] = { 0 }; + + /** +@@ -612,6 +639,8 @@ flatpak_transaction_operation_finalize (GObject *object) + g_clear_pointer (&self->run_before_ops, g_list_free); + g_clear_pointer (&self->related_to_ops, g_ptr_array_unref); + g_clear_pointer (&self->summary_metadata, g_variant_unref); ++ g_clear_object (&self->image_source); ++ g_clear_object (&self->resolved_image_source); + + G_OBJECT_CLASS (flatpak_transaction_operation_parent_class)->finalize (object); + } +@@ -993,6 +1022,7 @@ flatpak_transaction_finalize (GObject *object) + g_free (priv->parent_window); + g_list_free_full (priv->flatpakrefs, (GDestroyNotify) g_key_file_unref); + g_list_free_full (priv->bundles, (GDestroyNotify) bundle_data_free); ++ g_list_free_full (priv->images, (GDestroyNotify) image_data_free); + g_free (priv->default_arch); + g_hash_table_unref (priv->last_op_for_ref); + g_hash_table_unref (priv->remote_states); +@@ -2603,6 +2633,7 @@ flatpak_transaction_add_ref (FlatpakTransaction *self, + const char *commit, + FlatpakTransactionOperationType kind, + GFile *bundle, ++ FlatpakImageSource *image_source, + const char *external_metadata, + gboolean pin_on_deploy, + FlatpakTransactionOperation **out_op, +@@ -2730,6 +2761,9 @@ flatpak_transaction_add_ref (FlatpakTransaction *self, + op = flatpak_transaction_add_op (self, remote, ref, subpaths, previous_ids, + commit, bundle, kind, pin_on_deploy); + ++ if (image_source) ++ op->image_source = g_object_ref (image_source); ++ + if (external_metadata) + op->external_metadata = g_bytes_new (external_metadata, strlen (external_metadata)); + +@@ -2783,7 +2817,7 @@ flatpak_transaction_add_install (FlatpakTransaction *self, + + if (!flatpak_transaction_add_ref (self, remote, decomposed, subpaths, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_INSTALL, +- NULL, NULL, pin_on_deploy, NULL, error)) ++ NULL, NULL, NULL, pin_on_deploy, NULL, error)) + return FALSE; + + return TRUE; +@@ -2843,7 +2877,7 @@ flatpak_transaction_add_rebase (FlatpakTransaction *self, + if (dir_ref_is_installed (priv->dir, decomposed, &installed_origin, NULL)) + remote = installed_origin; + +- return flatpak_transaction_add_ref (self, remote, decomposed, subpaths, previous_ids, NULL, FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, NULL, NULL, FALSE, NULL, error); ++ return flatpak_transaction_add_ref (self, remote, decomposed, subpaths, previous_ids, NULL, FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, NULL, NULL, NULL, FALSE, NULL, error); + } + + /** +@@ -2919,12 +2953,12 @@ flatpak_transaction_add_rebase_and_uninstall (FlatpakTransaction *self, + if (!flatpak_transaction_add_ref (self, remote, new_decomposed, subpaths, + previous_ids, NULL, + FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, +- NULL, NULL, FALSE, &rebase_op, error)) ++ NULL, NULL, NULL, FALSE, &rebase_op, error)) + return FALSE; + + if (!flatpak_transaction_add_ref (self, NULL, old_decomposed, NULL, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_UNINSTALL, +- NULL, NULL, FALSE, &uninstall_op, &local_error)) ++ NULL, NULL, NULL, FALSE, &uninstall_op, &local_error)) + { + /* If the user is trying to install an eol-rebased app from scratch, the + * @old_ref can’t be uninstalled because it’s not installed already. +@@ -2980,6 +3014,36 @@ flatpak_transaction_add_install_bundle (FlatpakTransaction *self, + return TRUE; + } + ++/** ++ * flatpak_transaction_add_install_image: ++ * @self: a #FlatpakTransaction ++ * @image_location: (nullable): location string to install from. ++ * @error: return location for a #GError ++ * ++ * Install a Flatpak from a container image. The image is specified ++ * ++ * If the reference from the image was previously installed, then ++ * that remote will be used as the remote for the newly installed image. If the ++ * reference was not previously installed, then a remote will be created for the ++ * reference. ++ * ++ * @image_location is specified in containers-transports(5) form. Only a subset ++ * of transports are supported: oci: and docker:. ++ * ++ * Returns: %TRUE on success; %FALSE with @error set on failure. ++ */ ++gboolean ++flatpak_transaction_add_install_image (FlatpakTransaction *self, ++ const char *image_location, ++ GError **error) ++{ ++ FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self); ++ ++ priv->images = g_list_append (priv->images, image_data_new (image_location)); ++ ++ return TRUE; ++} ++ + /** + * flatpak_transaction_add_install_flatpakref: + * @self: a #FlatpakTransaction +@@ -3046,7 +3110,7 @@ flatpak_transaction_add_update (FlatpakTransaction *self, + return FALSE; + + /* Note: we implement the merge when subpaths == NULL in flatpak_transaction_add_ref() */ +- return flatpak_transaction_add_ref (self, NULL, decomposed, subpaths, NULL, commit, FLATPAK_TRANSACTION_OPERATION_UPDATE, NULL, NULL, FALSE, NULL, error); ++ return flatpak_transaction_add_ref (self, NULL, decomposed, subpaths, NULL, commit, FLATPAK_TRANSACTION_OPERATION_UPDATE, NULL, NULL, NULL, FALSE, NULL, error); + } + + /** +@@ -3073,7 +3137,7 @@ flatpak_transaction_add_uninstall (FlatpakTransaction *self, + if (decomposed == NULL) + return FALSE; + +- return flatpak_transaction_add_ref (self, NULL, decomposed, NULL, NULL, NULL, FLATPAK_TRANSACTION_OPERATION_UNINSTALL, NULL, NULL, FALSE, NULL, error); ++ return flatpak_transaction_add_ref (self, NULL, decomposed, NULL, NULL, NULL, FLATPAK_TRANSACTION_OPERATION_UNINSTALL, NULL, NULL, NULL, FALSE, NULL, error); + } + + static gboolean +@@ -3185,7 +3249,7 @@ flatpak_transaction_add_auto_install (FlatpakTransaction *self, + + if (!flatpak_transaction_add_ref (self, remote, auto_install_ref, NULL, NULL, NULL, + FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, +- NULL, NULL, FALSE, NULL, ++ NULL, NULL, NULL, FALSE, NULL, + &local_error)) + g_info ("Failed to add auto-install ref %s: %s", flatpak_decomposed_get_ref (auto_install_ref), + local_error->message); +@@ -3283,6 +3347,7 @@ static gboolean + mark_op_resolved (FlatpakTransactionOperation *op, + const char *commit, + GFile *sideload_path, ++ FlatpakImageSource *image_source, + GBytes *metadata, + GBytes *old_metadata, + GError **error) +@@ -3291,7 +3356,7 @@ mark_op_resolved (FlatpakTransactionOperation *op, + + g_assert (op != NULL); + +- g_assert (commit != NULL); ++ g_assert (commit != NULL || image_source != NULL); + + op->resolved = TRUE; + +@@ -3304,6 +3369,9 @@ mark_op_resolved (FlatpakTransactionOperation *op, + if (sideload_path) + op->resolved_sideload_path = g_object_ref (sideload_path); + ++ if (image_source) ++ op->resolved_image_source = g_object_ref (image_source); ++ + if (metadata) + { + g_autoptr(GKeyFile) metakey = g_key_file_new (); +@@ -3337,13 +3405,14 @@ resolve_op_end (FlatpakTransaction *self, + FlatpakTransactionOperation *op, + const char *checksum, + GFile *sideload_path, ++ FlatpakImageSource *image_source, + GBytes *metadata_bytes, + GError **error) + { + g_autoptr(GBytes) old_metadata_bytes = NULL; + + old_metadata_bytes = load_deployed_metadata (self, op->ref, NULL, NULL); +- if (!mark_op_resolved (op, checksum, sideload_path, metadata_bytes, old_metadata_bytes, error)) ++ if (!mark_op_resolved (op, checksum, sideload_path, image_source, metadata_bytes, old_metadata_bytes, error)) + return FALSE; + emit_eol_and_maybe_skip (self, op); + return TRUE; +@@ -3394,7 +3463,7 @@ resolve_op_from_commit (FlatpakTransaction *self, + flatpak_decomposed_get_ref (eolr_decomposed)); + } + +- return resolve_op_end (self, op, checksum, sideload_path, metadata_bytes, error); ++ return resolve_op_end (self, op, checksum, sideload_path, NULL, metadata_bytes, error); + } + + /* NOTE: In case of non-available summary this returns FALSE with a +@@ -3459,7 +3528,7 @@ try_resolve_op_from_metadata (FlatpakTransaction *self, + } + } + +- return resolve_op_end (self, op, checksum, sideload_path, metadata_bytes, error); ++ return resolve_op_end (self, op, checksum, sideload_path, NULL, metadata_bytes, error); + } + + static gboolean +@@ -3502,7 +3571,7 @@ resolve_ops (FlatpakTransaction *self, + * checksum we got was the version already installed. + */ + g_assert (op->resolved_commit != NULL); +- if (!mark_op_resolved (op, op->resolved_commit, NULL, NULL, NULL, error)) ++ if (!mark_op_resolved (op, op->resolved_commit, NULL, NULL, NULL, NULL, error)) + return FALSE; + continue; + } +@@ -3517,7 +3586,7 @@ resolve_ops (FlatpakTransaction *self, + op->skip = TRUE; + continue; + } +- if (!mark_op_resolved (op, checksum, NULL, metadata_bytes, NULL, error)) ++ if (!mark_op_resolved (op, checksum, NULL, NULL, metadata_bytes, NULL, error)) + return FALSE; + continue; + } +@@ -3525,7 +3594,7 @@ resolve_ops (FlatpakTransaction *self, + if (op->kind == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE) + { + g_assert (op->commit != NULL); +- if (!mark_op_resolved (op, op->commit, NULL, op->external_metadata, NULL, error)) ++ if (!mark_op_resolved (op, op->commit, NULL, NULL, op->external_metadata, NULL, error)) + return FALSE; + continue; + } +@@ -3549,8 +3618,13 @@ resolve_ops (FlatpakTransaction *self, + if (state == NULL) + return FALSE; + ++ if (op->image_source) ++ { ++ if (!mark_op_resolved (op, NULL, NULL, op->image_source, op->external_metadata, NULL, error)) ++ return FALSE; ++ } + /* Should we use local state */ +- if (transaction_is_local_only (self, op->kind)) ++ else if (transaction_is_local_only (self, op->kind)) + { + g_autoptr(GVariant) commit_data = flatpak_dir_read_latest_commit (priv->dir, op->remote, op->ref, + &checksum, NULL, error); +@@ -4688,7 +4762,74 @@ flatpak_transaction_resolve_bundles (FlatpakTransaction *self, + + if (!flatpak_transaction_add_ref (self, remote, ref, NULL, NULL, commit, + FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE, +- data->file, metadata, FALSE, NULL, error)) ++ data->file, NULL, metadata, FALSE, NULL, error)) ++ return FALSE; ++ } ++ ++ return TRUE; ++} ++ ++static gboolean ++flatpak_transaction_resolve_images (FlatpakTransaction *self, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self); ++ GList *l; ++ ++ for (l = priv->images; l != NULL; l = l->next) ++ { ++ ImageData *data = l->data; ++ g_autoptr(FlatpakImageSource) image_source = NULL; ++ g_autofree char *remote = NULL; ++ g_autoptr(FlatpakDecomposed) ref = NULL; ++ const char *ref_label; ++ const char *metadata_label; ++ FlatpakTransactionOperation *op; ++ g_autoptr(GBytes) deploy_data = NULL; ++ ++ image_source = flatpak_image_source_new_for_location (data->image_location, ++ cancellable, error); ++ if (!image_source) ++ return FALSE; ++ ++ ref_label = flatpak_image_source_get_ref (image_source); ++ ref = flatpak_decomposed_new_from_ref (ref_label, error); ++ if (ref == NULL) ++ { ++ g_prefix_error (error, "Cannot parse org.flatpak.ref label: "); ++ return FALSE; ++ } ++ ++ metadata_label = flatpak_image_source_get_metadata (image_source); ++ if (metadata_label == NULL) ++ return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, ++ "Image does not have org.flatpak.metadata label"); ++ ++ deploy_data = flatpak_dir_get_deploy_data (priv->dir, ref, FLATPAK_DEPLOY_VERSION_ANY, cancellable, NULL); ++ if (deploy_data != NULL) ++ remote = g_strdup (flatpak_deploy_data_get_origin (deploy_data)); ++ ++ if (remote == NULL) ++ { ++ gboolean created_remote; ++ g_autofree char *id = flatpak_decomposed_dup_id (ref); ++ ++ remote = flatpak_dir_create_origin_remote (priv->dir, NULL /* url */, id, ++ NULL /* title */, ref_label, ++ NULL /* gpg_data */, NULL /* collection_id */, ++ &created_remote, ++ cancellable, error); ++ if (!remote) ++ return FALSE; ++ ++ if (created_remote) ++ flatpak_installation_drop_caches (priv->installation, NULL, NULL); ++ } ++ ++ if (!flatpak_transaction_add_ref (self, remote, ref, NULL, NULL, NULL, ++ FLATPAK_TRANSACTION_OPERATION_INSTALL, ++ NULL, image_source, metadata_label, FALSE, &op, error)) + return FALSE; + } + +@@ -4747,7 +4888,7 @@ _run_op_kind (FlatpakTransaction *self, + + emit_new_op (self, op, progress); + +- g_assert (op->resolved_commit != NULL); /* We resolved this before */ ++ g_assert (op->resolved_commit != NULL || op->resolved_image_source != NULL); /* We resolved this before */ + + if (op->resolved_metakey && !flatpak_check_required_version (flatpak_decomposed_get_ref (op->ref), + op->resolved_metakey, &local_error)) +@@ -4765,6 +4906,7 @@ _run_op_kind (FlatpakTransaction *self, + (const char **) op->subpaths, + (const char **) op->previous_ids, + op->resolved_sideload_path, ++ op->resolved_image_source, + op->resolved_metadata, + op->resolved_token, + progress->progress_obj, +@@ -4838,6 +4980,7 @@ _run_op_kind (FlatpakTransaction *self, + (const char **) op->subpaths, + (const char **) op->previous_ids, + op->resolved_sideload_path, ++ op->resolved_image_source, + op->resolved_metadata, + op->resolved_token, + progress->progress_obj, +@@ -5149,6 +5292,12 @@ flatpak_transaction_real_run (FlatpakTransaction *self, + return FALSE; + } + ++ if (!flatpak_transaction_resolve_images (self, cancellable, error)) ++ { ++ g_assert (error == NULL || *error != NULL); ++ return FALSE; ++ } ++ + /* Resolve initial ops */ + if (!resolve_all_ops (self, cancellable, error)) + { +diff --git a/common/flatpak-transaction.h b/common/flatpak-transaction.h +index 0b8f2de8..3d3f4ce8 100644 +--- a/common/flatpak-transaction.h ++++ b/common/flatpak-transaction.h +@@ -326,6 +326,10 @@ gboolean flatpak_transaction_add_install_bundle (FlatpakTransaction * + GBytes *gpg_data, + GError **error); + FLATPAK_EXTERN ++gboolean flatpak_transaction_add_install_image (FlatpakTransaction *self, ++ const char *image_location, ++ GError **error); ++FLATPAK_EXTERN + gboolean flatpak_transaction_add_install_flatpakref (FlatpakTransaction *self, + GBytes *flatpakref_data, + GError **error); +diff --git a/system-helper/flatpak-system-helper.c b/system-helper/flatpak-system-helper.c +index a4d9708b..2740b299 100644 +--- a/system-helper/flatpak-system-helper.c ++++ b/system-helper/flatpak-system-helper.c +@@ -640,7 +640,7 @@ handle_deploy (FlatpakSystemHelper *object, + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- if (!flatpak_dir_pull (system, state, arg_ref, NULL, (const char **) arg_subpaths, NULL, NULL, NULL, NULL, ++ if (!flatpak_dir_pull (system, state, arg_ref, NULL, (const char **) arg_subpaths, NULL, NULL, NULL, NULL, NULL, + FLATPAK_PULL_FLAGS_NONE, OSTREE_REPO_PULL_FLAGS_UNTRUSTED, NULL, + NULL, &error)) + { +@@ -874,11 +874,11 @@ handle_deploy_appstream (FlatpakSystemHelper *object, + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- if (!flatpak_dir_pull (system, state, new_branch, NULL, NULL, NULL, NULL, NULL, NULL, ++ if (!flatpak_dir_pull (system, state, new_branch, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + FLATPAK_PULL_FLAGS_NONE, OSTREE_REPO_PULL_FLAGS_UNTRUSTED, NULL, + NULL, &first_error)) + { +- if (!flatpak_dir_pull (system, state, old_branch, NULL, NULL, NULL, NULL, NULL, NULL, ++ if (!flatpak_dir_pull (system, state, old_branch, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + FLATPAK_PULL_FLAGS_NONE, OSTREE_REPO_PULL_FLAGS_UNTRUSTED, NULL, + NULL, &second_error)) + { +-- +2.47.1 + +From 7ef02411fc7209bd1072c13d0e08b95a9c579b0f Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 18 Dec 2024 00:40:45 +0100 +Subject: [PATCH 07/12]builtins/install: Create install transaction in common + function + +--- + app/flatpak-builtins-install.c | 85 +++++++++++++--------------------- + 1 file changed, 33 insertions(+), 52 deletions(-) + +diff --git a/app/flatpak-builtins-install.c b/app/flatpak-builtins-install.c +index d4d9ae30..c5132da6 100644 +--- a/app/flatpak-builtins-install.c ++++ b/app/flatpak-builtins-install.c +@@ -129,6 +129,36 @@ read_gpg_data (GCancellable *cancellable, + return flatpak_read_stream (source_stream, FALSE, error); + } + ++static FlatpakTransaction * ++create_install_transaction (FlatpakDir *dir, ++ GError **error) ++{ ++ g_autoptr(FlatpakTransaction) transaction = NULL; ++ ++ if (opt_noninteractive) ++ transaction = flatpak_quiet_transaction_new (dir, error); ++ else ++ transaction = flatpak_cli_transaction_new (dir, opt_yes, TRUE, opt_arch != NULL, error); ++ ++ if (transaction == NULL) ++ return NULL; ++ ++ flatpak_transaction_set_no_pull (transaction, opt_no_pull); ++ flatpak_transaction_set_no_deploy (transaction, opt_no_deploy); ++ flatpak_transaction_set_disable_static_deltas (transaction, opt_no_static_deltas); ++ flatpak_transaction_set_disable_dependencies (transaction, opt_no_deps); ++ flatpak_transaction_set_disable_related (transaction, opt_no_related); ++ flatpak_transaction_set_disable_auto_pin (transaction, opt_no_auto_pin); ++ flatpak_transaction_set_reinstall (transaction, opt_reinstall); ++ flatpak_transaction_set_auto_install_sdk (transaction, opt_include_sdk); ++ flatpak_transaction_set_auto_install_debug (transaction, opt_include_debug); ++ ++ for (int i = 0; opt_sideload_repos != NULL && opt_sideload_repos[i] != NULL; i++) ++ flatpak_transaction_add_sideload_repo (transaction, opt_sideload_repos[i]); ++ ++ return g_steal_pointer (&transaction); ++} ++ + static gboolean + install_bundle (FlatpakDir *dir, + GOptionContext *context, +@@ -162,26 +192,10 @@ install_bundle (FlatpakDir *dir, + return FALSE; + } + +- if (opt_noninteractive) +- transaction = flatpak_quiet_transaction_new (dir, error); +- else +- transaction = flatpak_cli_transaction_new (dir, opt_yes, TRUE, opt_arch != NULL, error); ++ transaction = create_install_transaction (dir, error); + if (transaction == NULL) + return FALSE; + +- flatpak_transaction_set_no_pull (transaction, opt_no_pull); +- flatpak_transaction_set_no_deploy (transaction, opt_no_deploy); +- flatpak_transaction_set_disable_static_deltas (transaction, opt_no_static_deltas); +- flatpak_transaction_set_disable_dependencies (transaction, opt_no_deps); +- flatpak_transaction_set_disable_related (transaction, opt_no_related); +- flatpak_transaction_set_disable_auto_pin (transaction, opt_no_auto_pin); +- flatpak_transaction_set_reinstall (transaction, opt_reinstall); +- flatpak_transaction_set_auto_install_sdk (transaction, opt_include_sdk); +- flatpak_transaction_set_auto_install_debug (transaction, opt_include_debug); +- +- for (int i = 0; opt_sideload_repos != NULL && opt_sideload_repos[i] != NULL; i++) +- flatpak_transaction_add_sideload_repo (transaction, opt_sideload_repos[i]); +- + if (!flatpak_transaction_add_install_bundle (transaction, file, gpg_data, error)) + return FALSE; + +@@ -241,27 +255,10 @@ install_from (FlatpakDir *dir, + file_data = g_bytes_new_take (g_steal_pointer (&data), data_len); + } + +- if (opt_noninteractive) +- transaction = flatpak_quiet_transaction_new (dir, error); +- else +- transaction = flatpak_cli_transaction_new (dir, opt_yes, TRUE, opt_arch != NULL, error); ++ transaction = create_install_transaction (dir, error); + if (transaction == NULL) + return FALSE; + +- flatpak_transaction_set_no_pull (transaction, opt_no_pull); +- flatpak_transaction_set_no_deploy (transaction, opt_no_deploy); +- flatpak_transaction_set_disable_static_deltas (transaction, opt_no_static_deltas); +- flatpak_transaction_set_disable_dependencies (transaction, opt_no_deps); +- flatpak_transaction_set_disable_related (transaction, opt_no_related); +- flatpak_transaction_set_disable_auto_pin (transaction, opt_no_auto_pin); +- flatpak_transaction_set_reinstall (transaction, opt_reinstall); +- flatpak_transaction_set_default_arch (transaction, opt_arch); +- flatpak_transaction_set_auto_install_sdk (transaction, opt_include_sdk); +- flatpak_transaction_set_auto_install_debug (transaction, opt_include_debug); +- +- for (int i = 0; opt_sideload_repos != NULL && opt_sideload_repos[i] != NULL; i++) +- flatpak_transaction_add_sideload_repo (transaction, opt_sideload_repos[i]); +- + if (!flatpak_transaction_add_install_flatpakref (transaction, file_data, error)) + return FALSE; + +@@ -485,26 +482,10 @@ flatpak_builtin_install (int argc, char **argv, GCancellable *cancellable, GErro + + default_branch = flatpak_dir_get_remote_default_branch (dir, remote); + +- if (opt_noninteractive) +- transaction = flatpak_quiet_transaction_new (dir, error); +- else +- transaction = flatpak_cli_transaction_new (dir, opt_yes, TRUE, opt_arch != NULL, error); ++ transaction = create_install_transaction (dir, error); + if (transaction == NULL) + return FALSE; + +- flatpak_transaction_set_no_pull (transaction, opt_no_pull); +- flatpak_transaction_set_no_deploy (transaction, opt_no_deploy); +- flatpak_transaction_set_disable_static_deltas (transaction, opt_no_static_deltas); +- flatpak_transaction_set_disable_dependencies (transaction, opt_no_deps); +- flatpak_transaction_set_disable_related (transaction, opt_no_related); +- flatpak_transaction_set_disable_auto_pin (transaction, opt_no_auto_pin); +- flatpak_transaction_set_reinstall (transaction, opt_reinstall); +- flatpak_transaction_set_auto_install_sdk (transaction, opt_include_sdk); +- flatpak_transaction_set_auto_install_debug (transaction, opt_include_debug); +- +- for (i = 0; opt_sideload_repos != NULL && opt_sideload_repos[i] != NULL; i++) +- flatpak_transaction_add_sideload_repo (transaction, opt_sideload_repos[i]); +- + for (i = 0; i < n_prefs; i++) + { + const char *pref = prefs[i]; +-- +2.47.1 + +From 920b48c7a53b9bff543827309c974a9c7824068f Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Wed, 18 Dec 2024 00:53:59 +0100 +Subject: [PATCH 08/12]builtins/install: Allow direct installation from OCI + images + +Similar to bundle installs, add: + + flatpak install [--image] docker://registry.example.com/image:latest + flatpak install [--image] oci:/path/to/image + +These is useful for testing purposes and in certain cases when installing +Flatpaks on disconnected systems. +--- + app/flatpak-builtins-install.c | 60 ++++++++++++++++++++++++++++++++-- + app/flatpak-cli-transaction.c | 2 +- + doc/flatpak-install.xml | 24 ++++++++++---- + tests/test-oci-registry.sh | 33 ++++++++++++++++++- + tests/test-oci.sh | 22 ++++++++++++- + 5 files changed, 129 insertions(+), 12 deletions(-) + +diff --git a/app/flatpak-builtins-install.c b/app/flatpak-builtins-install.c +index c5132da6..70474bbe 100644 +--- a/app/flatpak-builtins-install.c ++++ b/app/flatpak-builtins-install.c +@@ -56,6 +56,7 @@ static gboolean opt_include_sdk; + static gboolean opt_include_debug; + static gboolean opt_bundle; + static gboolean opt_from; ++static gboolean opt_image; + static gboolean opt_yes; + static gboolean opt_reinstall; + static gboolean opt_noninteractive; +@@ -76,6 +77,7 @@ static GOptionEntry options[] = { + { "include-debug", 0, 0, G_OPTION_ARG_NONE, &opt_include_debug, N_("Additionally install the debug info for the given refs and their dependencies") }, + { "bundle", 0, 0, G_OPTION_ARG_NONE, &opt_bundle, N_("Assume LOCATION is a .flatpak single-file bundle"), NULL }, + { "from", 0, 0, G_OPTION_ARG_NONE, &opt_from, N_("Assume LOCATION is a .flatpakref application description"), NULL }, ++ { "image", 0, 0, G_OPTION_ARG_NONE, &opt_image, N_("Assume LOCATION is containers-transports(5) reference to an OCI image"), NULL }, + { "gpg-file", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_gpg_file, N_("Check bundle signatures with GPG key from FILE (- for stdin)"), N_("FILE") }, + { "subpath", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_subpaths, N_("Only install this subpath"), N_("PATH") }, + { "assumeyes", 'y', 0, G_OPTION_ARG_NONE, &opt_yes, N_("Automatically answer yes for all questions"), NULL }, +@@ -273,6 +275,52 @@ install_from (FlatpakDir *dir, + return TRUE; + } + ++static gboolean ++install_image (FlatpakDir *dir, ++ GOptionContext *context, ++ int argc, ++ char **argv, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ const char *location; ++ g_autoptr(GBytes) gpg_data = NULL; ++ g_autoptr(FlatpakTransaction) transaction = NULL; ++ ++ if (argc < 2) ++ return usage_error (context, _("Image location must be specified"), error); ++ ++ if (argc > 2) ++ return usage_error (context, _("Too many arguments"), error); ++ ++ location = argv[1]; ++ ++ if (opt_gpg_file != NULL) ++ { ++ /* Override gpg_data from file */ ++ gpg_data = read_gpg_data (cancellable, error); ++ if (gpg_data == NULL) ++ return FALSE; ++ } ++ ++ transaction = create_install_transaction (dir, error); ++ if (transaction == NULL) ++ return FALSE; ++ ++ if (!flatpak_transaction_add_install_image (transaction, location, error)) ++ return FALSE; ++ ++ if (!flatpak_transaction_run (transaction, cancellable, error)) ++ { ++ if (g_error_matches (*error, FLATPAK_ERROR, FLATPAK_ERROR_ABORTED)) ++ g_clear_error (error); /* Don't report on stderr */ ++ ++ return FALSE; ++ } ++ ++ return TRUE; ++} ++ + gboolean + flatpak_builtin_install (int argc, char **argv, GCancellable *cancellable, GError **error) + { +@@ -301,11 +349,14 @@ flatpak_builtin_install (int argc, char **argv, GCancellable *cancellable, GErro + /* Start with the default or specified dir, this is fine for opt_bundle or opt_from */ + dir = g_object_ref (g_ptr_array_index (dirs, 0)); + +- if (!opt_bundle && !opt_from && argc >= 2) ++ if (!opt_bundle && !opt_from && !opt_image && argc >= 2) + { +- if (flatpak_file_arg_has_suffix (argv[1], ".flatpakref")) ++ if (g_str_has_prefix (argv[1], "oci:") || ++ g_str_has_prefix (argv[1], "docker:")) ++ opt_image = TRUE; ++ else if (flatpak_file_arg_has_suffix (argv[1], ".flatpakref")) + opt_from = TRUE; +- if (flatpak_file_arg_has_suffix (argv[1], ".flatpak")) ++ else if (flatpak_file_arg_has_suffix (argv[1], ".flatpak")) + opt_bundle = TRUE; + } + +@@ -315,6 +366,9 @@ flatpak_builtin_install (int argc, char **argv, GCancellable *cancellable, GErro + if (opt_from) + return install_from (dir, context, argc, argv, cancellable, error); + ++ if (opt_image) ++ return install_image (dir, context, argc, argv, cancellable, error); ++ + if (argc < 2) + return usage_error (context, _("At least one REF must be specified"), error); + +diff --git a/app/flatpak-cli-transaction.c b/app/flatpak-cli-transaction.c +index e7c8c90f..8c02c552 100644 +--- a/app/flatpak-cli-transaction.c ++++ b/app/flatpak-cli-transaction.c +@@ -1350,7 +1350,7 @@ transaction_ready_pre_auth (FlatpakTransaction *transaction) + GList *l; + int i; + FlatpakTablePrinter *printer; +- const char *op_shorthand[] = { "i", "u", "i", "r" }; ++ const char *op_shorthand[] = { "i", "u", "i", "r", "i" }; + + /* These caches may no longer be valid once the transaction runs */ + g_clear_pointer (&self->runtime_app_map, g_hash_table_unref); +diff --git a/doc/flatpak-install.xml b/doc/flatpak-install.xml +index ab4bb1fb..f75dedae 100644 +--- a/doc/flatpak-install.xml ++++ b/doc/flatpak-install.xml +@@ -38,7 +38,7 @@ + + flatpak install + OPTION +- --from|--bundle ++ --from|--bundle|--image + LOCATION + + +@@ -85,11 +85,11 @@ + flatpak1). + + +- The alternative form of the command (with or +- ) allows to install directly from a source such as a +- .flatpak single-file bundle or a .flatpakref +- application description. The options are optional if the first argument has the expected +- filename extension. ++ The alternative form of the command (with , , ++ or ) allows to install directly from a source. The source can be a ++ .flatpak single-file bundle, .flatpakref ++ application description, or a reference to an OCI image. The options are optional if the ++ first argument has the expected filename extension or prefix. + + + Note that flatpak allows to have multiple branches of an application and runtimes +@@ -139,6 +139,18 @@ + + + ++ ++ ++ ++ Treat LOCATION as the location of a Flatpak in ++ OCI image format. LOCATION is in the format of ++ containers-transports5. ++ Supported schemes are docker:// and oci:. ++ This is assumed if the argument starts ++ with one of these schemes. ++ ++ ++ + + + +diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh +index da234ded..9503656b 100755 +--- a/tests/test-oci-registry.sh ++++ b/tests/test-oci-registry.sh +@@ -23,7 +23,7 @@ set -euo pipefail + + skip_without_bwrap + +-echo "1..14" ++echo "1..16" + + # Start the fake registry server + +@@ -315,3 +315,34 @@ assert_file_has_content remotes-list "^hello-origin.*[ ]${scheme}://127\.0\.0\. + assert_not_has_file $base/oci/hello-origin.index.gz + + ok "change remote origin via bundle" ++ ++${FLATPAK} ${U} -y uninstall org.test.Hello >&2 ++${FLATPAK} ${U} -y uninstall org.test.Platform >&2 ++ ++${FLATPAK} ${U} list --columns=application,origin > flatpak-list ++assert_not_file_has_content flatpak-list 'org.test.Platform' ++assert_not_file_has_content flatpak-list 'org.test.Hello' ++ ++${FLATPAK} ${U} remotes --show-disabled > remotes-list ++assert_not_file_has_content remotes-list '^platform-origin' ++assert_not_file_has_content remotes-list '^hello-origin' ++ ++ok "clean up" ++ ++# Install from registry via a docker:// location ++# TODO: docker:// locations only support HTTPS ++# This needs https://github.com/flatpak/flatpak/pull/5916 ++ ++if false && [ x${USE_SYSTEMDIR-} != xyes ]; then ++ $FLATPAK --user -y install docker://127.0.0.1/platform-image:latest >&2 ++ ++ ${FLATPAK} ${U} list --columns=application,origin > flatpak-list ++ assert_file_has_content flatpak-list '^org.test.Platform *platform-origin$' ++ ++ ${FLATPAK} ${U} remotes --show-disabled > remotes-list ++ assert_file_has_content remotes-list '^platform-origin' ++ ++ ok "install image from registry" ++else ++ ok "install image from registry # skip Not supported" ++fi +diff --git a/tests/test-oci.sh b/tests/test-oci.sh +index a7baf887..431fa6d6 100755 +--- a/tests/test-oci.sh ++++ b/tests/test-oci.sh +@@ -23,7 +23,7 @@ set -euo pipefail + + skip_without_bwrap + +-echo "1..2" ++echo "1..3" + + setup_repo_no_add oci + +@@ -66,3 +66,23 @@ assert_has_file checked-out/files/bin/hello.sh + assert_has_file checked-out/metadata + + ok "import oci" ++ ++# Trying installing the bundle directly ++ ++${FLATPAK} build-bundle --runtime --oci $FL_GPGARGS repos/oci oci/platform-image org.test.Platform >&2 ++ ++${FLATPAK} --user list --columns=application,origin > flatpak-list ++assert_not_file_has_content flatpak-list 'org.test.Platform' ++ ++${FLATPAK} --user remotes --show-disabled > remotes-list ++assert_not_file_has_content remotes-list '^platform-origin' ++ ++$FLATPAK --user -y install oci:oci/platform-image >&2 ++ ++${FLATPAK} --user list --columns=application,origin > flatpak-list ++assert_file_has_content flatpak-list '^org.test.Platform *platform-origin$' ++ ++${FLATPAK} --user remotes --show-disabled > remotes-list ++assert_file_has_content remotes-list '^platform-origin' ++ ++ok "install oci" +-- +2.47.1 + +From 3996759b7553c41bea63fb282cbf3fa0f9a78e4e Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 20 Dec 2024 00:25:55 +0100 +Subject: [PATCH 09/12]oci-registry: Remove a bunch of double newlines + +--- + common/flatpak-oci-registry-private.h | 1 - + common/flatpak-oci-registry.c | 6 ------ + 2 files changed, 7 deletions(-) + +diff --git a/common/flatpak-oci-registry-private.h b/common/flatpak-oci-registry-private.h +index 47ffc7be..a051b0a6 100644 +--- a/common/flatpak-oci-registry-private.h ++++ b/common/flatpak-oci-registry-private.h +@@ -183,7 +183,6 @@ GBytes *flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session, + GCancellable *cancellable, + GError **error); + +- + typedef void (*FlatpakOciPullProgress) (guint64 total_size, + guint64 pulled_size, + guint32 n_layers, +diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c +index ad05c9f0..e104ab40 100644 +--- a/common/flatpak-oci-registry.c ++++ b/common/flatpak-oci-registry.c +@@ -255,7 +255,6 @@ flatpak_oci_registry_set_token (FlatpakOciRegistry *self, + 0, NULL, NULL); + } + +- + FlatpakOciRegistry * + flatpak_oci_registry_new (const char *uri, + gboolean for_write, +@@ -830,7 +829,6 @@ flatpak_oci_registry_download_blob (FlatpakOciRegistry *self, + return -1; + } + +- + if (!flatpak_open_in_tmpdir_at (self->tmp_dfd, 0600, tmpfile_name, + &out_stream, cancellable, error)) + return -1; +@@ -1357,7 +1355,6 @@ flatpak_oci_layer_writer_reset (FlatpakOciLayerWriter *self) + g_clear_object (&self->compressor); + } + +- + static void + flatpak_oci_layer_writer_finalize (GObject *object) + { +@@ -1725,7 +1722,6 @@ delta_read_byte (GInputStream *in, + return TRUE; + } + +- + static gboolean + delta_read_varuint (GInputStream *in, + guint64 *out, +@@ -2731,7 +2727,6 @@ get_image_metadata (FlatpakOciIndexImage *img, const char *key) + return NULL; + } + +- + static const char * + get_image_ref (FlatpakOciIndexImage *img) + { +@@ -2838,7 +2833,6 @@ flatpak_oci_index_ensure_cached (FlatpakHttpSession *http_session, + + oci_arch = flatpak_arch_to_oci_arch (flatpak_get_arch ()); + +- + query = g_string_new (NULL); + flatpak_uri_encode_query_arg (query, "label:org.flatpak.ref:exists", "1"); + flatpak_uri_encode_query_arg (query, "architecture", oci_arch); +-- +2.47.1 + +From fcad7d86d287d164421d62589a70bb9699a66f08 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 20 Dec 2024 00:27:29 +0100 +Subject: [PATCH 10/12]oci-registry: Allow passing a NULL URI + +--- + common/flatpak-oci-registry.c | 11 +++++++---- + 1 file changed, 7 insertions(+), 4 deletions(-) + +diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c +index e104ab40..a4b914a0 100644 +--- a/common/flatpak-oci-registry.c ++++ b/common/flatpak-oci-registry.c +@@ -142,10 +142,13 @@ flatpak_oci_registry_set_property (GObject *object, + case PROP_URI: + /* Ensure the base uri ends with a / so relative urls work */ + uri = g_value_get_string (value); +- if (g_str_has_suffix (uri, "/")) +- self->uri = g_strdup (uri); +- else +- self->uri = g_strconcat (uri, "/", NULL); ++ if (uri) ++ { ++ if (g_str_has_suffix (uri, "/")) ++ self->uri = g_strdup (uri); ++ else ++ self->uri = g_strconcat (uri, "/", NULL); ++ } + break; + + case PROP_FOR_WRITE: +-- +2.47.1 + +From 96b50f2f632b57c9f6608a7820d282225afef46b Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Wed, 18 Dec 2024 00:53:59 +0100 +Subject: [PATCH 11/12]image-source: Support `oci-archive:` image sources + +Add support for `oci-archive:` image sources by temporarily +unpacking the archive using libarchive. + +Co-authored-by: Sebastian Wick +--- + app/flatpak-builtins-install.c | 1 + + common/flatpak-image-source.c | 50 ++++-- + common/flatpak-oci-registry-private.h | 14 +- + common/flatpak-oci-registry.c | 247 +++++++++++++++++++++++--- + common/flatpak-transaction.c | 2 +- + doc/flatpak-install.xml | 4 +- + tests/test-oci.sh | 24 ++- + 7 files changed, 297 insertions(+), 45 deletions(-) + +diff --git a/app/flatpak-builtins-install.c b/app/flatpak-builtins-install.c +index 70474bbe..e1f7f312 100644 +--- a/app/flatpak-builtins-install.c ++++ b/app/flatpak-builtins-install.c +@@ -352,6 +352,7 @@ flatpak_builtin_install (int argc, char **argv, GCancellable *cancellable, GErro + if (!opt_bundle && !opt_from && !opt_image && argc >= 2) + { + if (g_str_has_prefix (argv[1], "oci:") || ++ g_str_has_prefix (argv[1], "oci-archive:") || + g_str_has_prefix (argv[1], "docker:")) + opt_image = TRUE; + else if (flatpak_file_arg_has_suffix (argv[1], ".flatpakref")) +diff --git a/common/flatpak-image-source.c b/common/flatpak-image-source.c +index 0503de5d..20c94b63 100644 +--- a/common/flatpak-image-source.c ++++ b/common/flatpak-image-source.c +@@ -116,24 +116,15 @@ flatpak_image_source_new (FlatpakOciRegistry *registry, + return g_steal_pointer (&self); + } + +-FlatpakImageSource * +-flatpak_image_source_new_local (GFile *file, +- const char *reference, +- GCancellable *cancellable, +- GError **error) ++static FlatpakImageSource * ++flatpak_image_source_new_local_for_registry (FlatpakOciRegistry *registry, ++ const char *reference, ++ GCancellable *cancellable, ++ GError **error) + { +- g_autofree char *dir_uri = NULL; +- g_autofree char *target_ref = NULL; +- g_autoptr(FlatpakImageSource) image_source = NULL; +- g_autoptr(FlatpakOciRegistry) registry = NULL; + g_autoptr(FlatpakOciIndex) index = NULL; + const FlatpakOciManifestDescriptor *desc; + +- dir_uri = g_file_get_uri (file); +- registry = flatpak_oci_registry_new (dir_uri, FALSE, -1, cancellable, error); +- if (registry == NULL) +- return NULL; +- + index = flatpak_oci_registry_load_index (registry, cancellable, error); + if (index == NULL) + return NULL; +@@ -160,6 +151,23 @@ flatpak_image_source_new_local (GFile *file, + return flatpak_image_source_new (registry, NULL, desc->parent.digest, cancellable, error); + } + ++FlatpakImageSource * ++flatpak_image_source_new_local (GFile *file, ++ const char *reference, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autofree char *dir_uri = NULL; ++ g_autoptr(FlatpakOciRegistry) registry = NULL; ++ ++ dir_uri = g_file_get_uri (file); ++ registry = flatpak_oci_registry_new (dir_uri, FALSE, -1, cancellable, error); ++ if (registry == NULL) ++ return NULL; ++ ++ return flatpak_image_source_new_local_for_registry (registry, reference, cancellable, error); ++} ++ + FlatpakImageSource * + flatpak_image_source_new_remote (const char *uri, + const char *oci_repository, +@@ -221,6 +229,20 @@ flatpak_image_source_new_for_location (const char *location, + + return flatpak_image_source_new_local (path, reference, cancellable, error); + } ++ else if (g_str_has_prefix (location, "oci-archive:")) ++ { ++ g_autoptr(FlatpakOciRegistry) registry = NULL; ++ g_autoptr(GFile) path = NULL; ++ g_autofree char *reference = NULL; ++ ++ get_path_and_reference (location, &path, &reference); ++ ++ registry = flatpak_oci_registry_new_for_archive (path, cancellable, error); ++ if (registry == NULL) ++ return NULL; ++ ++ return flatpak_image_source_new_local_for_registry (registry, reference, cancellable, error); ++ } + else if (g_str_has_prefix (location, "docker:")) + { + g_autoptr(FlatpakOciRegistry) registry = NULL; +diff --git a/common/flatpak-oci-registry-private.h b/common/flatpak-oci-registry-private.h +index a051b0a6..d48c8ed4 100644 +--- a/common/flatpak-oci-registry-private.h ++++ b/common/flatpak-oci-registry-private.h +@@ -50,11 +50,15 @@ typedef struct FlatpakOciLayerWriter FlatpakOciLayerWriter; + + G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakOciLayerWriter, g_object_unref) + +-FlatpakOciRegistry * flatpak_oci_registry_new (const char *uri, +- gboolean for_write, +- int tmp_dfd, +- GCancellable * cancellable, +- GError **error); ++ ++FlatpakOciRegistry * flatpak_oci_registry_new (const char *uri, ++ gboolean for_write, ++ int tmp_dfd, ++ GCancellable *cancellable, ++ GError **error); ++FlatpakOciRegistry * flatpak_oci_registry_new_for_archive (GFile *archive, ++ GCancellable *cancellable, ++ GError **error); + void flatpak_oci_registry_set_token (FlatpakOciRegistry *self, + const char *token); + gboolean flatpak_oci_registry_is_local (FlatpakOciRegistry *self); +diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c +index a4b914a0..ebf000f8 100644 +--- a/common/flatpak-oci-registry.c ++++ b/common/flatpak-oci-registry.c +@@ -27,6 +27,7 @@ + #include "libglnx.h" + + #include ++#include + #include + #include "flatpak-image-source-private.h" + #include "flatpak-oci-registry-private.h" +@@ -69,11 +70,13 @@ struct FlatpakOciRegistry + gboolean valid; + gboolean is_docker; + char *uri; ++ GFile *archive; + int tmp_dfd; + char *token; + + /* Local repos */ + int dfd; ++ GLnxTmpDir *tmp_dir; + + /* Remote repos */ + FlatpakHttpSession *http_session; +@@ -90,6 +93,7 @@ enum { + PROP_0, + + PROP_URI, ++ PROP_ARCHIVE, + PROP_FOR_WRITE, + PROP_TMP_DFD, + }; +@@ -98,6 +102,14 @@ G_DEFINE_TYPE_WITH_CODE (FlatpakOciRegistry, flatpak_oci_registry, G_TYPE_OBJECT + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, + flatpak_oci_registry_initable_iface_init)) + ++static void ++glnx_tmpdir_free (GLnxTmpDir *tmpf) ++{ ++ (void)glnx_tmpdir_delete (tmpf, NULL, NULL); ++ g_free (tmpf); ++} ++G_DEFINE_AUTOPTR_CLEANUP_FUNC(GLnxTmpDir, glnx_tmpdir_free) ++ + static gchar * + parse_relative_uri (GUri *base_uri, + const char *subpath, +@@ -124,6 +136,8 @@ flatpak_oci_registry_finalize (GObject *object) + g_clear_pointer (&self->base_uri, g_uri_unref); + g_free (self->uri); + g_free (self->token); ++ g_clear_object (&self->archive); ++ g_clear_pointer (&self->tmp_dir, glnx_tmpdir_free); + + G_OBJECT_CLASS (flatpak_oci_registry_parent_class)->finalize (object); + } +@@ -151,6 +165,10 @@ flatpak_oci_registry_set_property (GObject *object, + } + break; + ++ case PROP_ARCHIVE: ++ self->archive = g_value_dup_object (value); ++ break; ++ + case PROP_FOR_WRITE: + self->for_write = g_value_get_boolean (value); + break; +@@ -179,6 +197,10 @@ flatpak_oci_registry_get_property (GObject *object, + g_value_set_string (value, self->uri); + break; + ++ case PROP_ARCHIVE: ++ g_value_set_object (value, self->archive); ++ break; ++ + case PROP_FOR_WRITE: + g_value_set_boolean (value, self->for_write); + break; +@@ -209,6 +231,13 @@ flatpak_oci_registry_class_init (FlatpakOciRegistryClass *klass) + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); ++ g_object_class_install_property (object_class, ++ PROP_ARCHIVE, ++ g_param_spec_object ("archive", ++ "", ++ "", ++ G_TYPE_FILE, ++ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (object_class, + PROP_TMP_DFD, + g_param_spec_int ("tmp-dfd", +@@ -277,6 +306,21 @@ flatpak_oci_registry_new (const char *uri, + return oci_registry; + } + ++FlatpakOciRegistry * ++flatpak_oci_registry_new_for_archive (GFile *archive, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ FlatpakOciRegistry *oci_registry; ++ ++ oci_registry = g_initable_new (FLATPAK_TYPE_OCI_REGISTRY, ++ cancellable, error, ++ "archive", archive, ++ NULL); ++ ++ return oci_registry; ++} ++ + static int + local_open_file (int dfd, + const char *subpath, +@@ -457,13 +501,164 @@ verify_oci_version (GBytes *oci_layout_bytes, gboolean *not_json, GCancellable * + return TRUE; + } + ++/* ++ * Code to extract an archive such as a tarfile into a temporary directory ++ * ++ * Based on: https://github.com/libarchive/libarchive/wiki/Examples#A_Complete_Extractor ++ * ++ * We treat ARCHIVE_WARNING as fatal - while this might be too strict, it ++ * will avoid surprises. ++ */ ++ ++static gboolean ++propagate_libarchive_error (GError **error, ++ struct archive *a) ++{ ++ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, ++ "%s", archive_error_string (a)); ++ return FALSE; ++} ++ ++static gboolean ++copy_data (struct archive *ar, ++ struct archive *aw, ++ GError **error) ++{ ++ int r; ++ const void *buff; ++ size_t size; ++ gint64 offset; ++ ++ while (TRUE) ++ { ++ r = archive_read_data_block (ar, &buff, &size, &offset); ++ ++ if (r == ARCHIVE_EOF) ++ return TRUE; ++ ++ if (r == ARCHIVE_RETRY) ++ continue; ++ ++ if (r != ARCHIVE_OK) ++ return propagate_libarchive_error (error, ar); ++ ++ while (TRUE) ++ { ++ r = archive_write_data_block (aw, buff, size, offset); ++ ++ if (r == ARCHIVE_RETRY) ++ continue; ++ ++ if (r == ARCHIVE_OK) ++ break; ++ ++ return propagate_libarchive_error (error, aw); ++ } ++ } ++} ++ ++static gboolean ++unpack_archive (GFile *archive, ++ char *destination, ++ GError **error) ++{ ++ g_autoptr(FlatpakAutoArchiveRead) a = NULL; ++ g_autoptr(FlatpakAutoArchiveWrite) ext = NULL; ++ int flags; ++ int r; ++ ++ flags = 0; ++ flags |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; ++ flags |= ARCHIVE_EXTRACT_SECURE_SYMLINKS; ++ ++ a = archive_read_new (); ++ archive_read_support_format_all (a); ++ archive_read_support_filter_all (a); ++ ++ ext = archive_write_disk_new (); ++ archive_write_disk_set_options (ext, flags); ++ archive_write_disk_set_standard_lookup (ext); ++ ++ r = archive_read_open_filename (a, g_file_get_path(archive), 10240); ++ if (r != ARCHIVE_OK) ++ return propagate_libarchive_error (error, a); ++ ++ while (TRUE) ++ { ++ g_autofree char *target_path = NULL; ++ struct archive_entry *entry; ++ ++ r = archive_read_next_header (a, &entry); ++ if (r == ARCHIVE_EOF) ++ break; ++ ++ if (r != ARCHIVE_OK) ++ return propagate_libarchive_error (error, a); ++ ++ target_path = g_build_filename (destination, archive_entry_pathname (entry), NULL); ++ archive_entry_set_pathname (entry, target_path); ++ ++ r = archive_write_header (ext, entry); ++ if (r != ARCHIVE_OK) ++ return propagate_libarchive_error (error, ext); ++ ++ if (archive_entry_size (entry) > 0) ++ { ++ if (!copy_data (a, ext, error)) ++ return FALSE; ++ } ++ ++ r = archive_write_finish_entry (ext); ++ if (r != ARCHIVE_OK) ++ return propagate_libarchive_error (error, ext); ++ } ++ ++ r = archive_read_close (a); ++ if (r != ARCHIVE_OK) ++ return propagate_libarchive_error (error, a); ++ ++ r = archive_write_close (ext); ++ if (r != ARCHIVE_OK) ++ return propagate_libarchive_error (error, ext); ++ ++ return TRUE; ++} ++ ++static const char * ++get_download_tmpdir (void) ++{ ++ /* We don't use TMPDIR because the downloaded artifacts can be ++ * very big, and we want to prefer /var/tmp to /tmp. ++ */ ++ const char *tmpdir = g_getenv ("FLATPAK_DOWNLOAD_TMPDIR"); ++ if (tmpdir) ++ return tmpdir; ++ ++ return "/var/tmp"; ++} ++ ++static GLnxTmpDir * ++download_tmpdir_new (GError **error) ++{ ++ g_autoptr(GLnxTmpDir) tmp_dir = g_new0 (GLnxTmpDir, 1); ++ glnx_autofd int base_dfd = -1; ++ ++ if (!glnx_opendirat (AT_FDCWD, get_download_tmpdir (), TRUE, &base_dfd, error)) ++ return NULL; ++ ++ if (!glnx_mkdtempat (base_dfd, "oci-XXXXXX", 0700, tmp_dir, error)) ++ return NULL; ++ ++ return g_steal_pointer (&tmp_dir); ++} ++ + static gboolean + flatpak_oci_registry_ensure_local (FlatpakOciRegistry *self, + gboolean for_write, + GCancellable *cancellable, + GError **error) + { +- g_autoptr(GFile) dir = g_file_new_for_uri (self->uri); ++ g_autoptr(GLnxTmpDir) local_tmp_dir = NULL; + glnx_autofd int local_dfd = -1; + int dfd; + g_autoptr(GError) local_error = NULL; +@@ -472,9 +667,28 @@ flatpak_oci_registry_ensure_local (FlatpakOciRegistry *self, + gboolean not_json; + + if (self->dfd != -1) +- dfd = self->dfd; ++ { ++ dfd = self->dfd; ++ } ++ else if (self->archive) ++ { ++ local_tmp_dir = download_tmpdir_new (error); ++ if (!local_tmp_dir) ++ return FALSE; ++ ++ if (!unpack_archive (self->archive, local_tmp_dir->path, error)) ++ return FALSE; ++ ++ if (!glnx_opendirat (AT_FDCWD, local_tmp_dir->path, ++ TRUE, &local_dfd, error)) ++ return FALSE; ++ ++ dfd = local_dfd; ++ } + else + { ++ g_autoptr(GFile) dir = g_file_new_for_uri (self->uri); ++ + if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (dir), + TRUE, &local_dfd, &local_error)) + { +@@ -537,8 +751,11 @@ flatpak_oci_registry_ensure_local (FlatpakOciRegistry *self, + self->token = g_strndup (g_bytes_get_data (token_bytes, NULL), g_bytes_get_size (token_bytes)); + } + +- if (self->dfd == -1 && local_dfd != -1) +- self->dfd = g_steal_fd (&local_dfd); ++ if (self->dfd == -1) ++ { ++ self->dfd = g_steal_fd (&local_dfd); ++ self->tmp_dir = g_steal_pointer (&local_tmp_dir); ++ } + + return TRUE; + } +@@ -589,20 +806,15 @@ flatpak_oci_registry_initable_init (GInitable *initable, + FlatpakOciRegistry *self = FLATPAK_OCI_REGISTRY (initable); + gboolean res; + ++ g_warn_if_fail (self->archive || self->uri); ++ + if (self->tmp_dfd == -1) + { +- /* We don't use TMPDIR because the downloaded artifacts can be +- * very big, and we want to prefer /var/tmp to /tmp. +- */ +- const char *tmpdir = g_getenv ("FLATPAK_DOWNLOAD_TMPDIR"); +- if (tmpdir == NULL) +- tmpdir = "/var/tmp"; +- +- if (!glnx_opendirat (AT_FDCWD, tmpdir, TRUE, &self->tmp_dfd, error)) ++ if (!glnx_opendirat (AT_FDCWD, get_download_tmpdir (), TRUE, &self->tmp_dfd, error)) + return FALSE; + } + +- if (g_str_has_prefix (self->uri, "file:/")) ++ if (self->archive || g_str_has_prefix (self->uri, "file:/")) + res = flatpak_oci_registry_ensure_local (self, self->for_write, cancellable, error); + else + res = flatpak_oci_registry_ensure_remote (self, self->for_write, cancellable, error); +@@ -1332,15 +1544,6 @@ typedef struct + + G_DEFINE_TYPE (FlatpakOciLayerWriter, flatpak_oci_layer_writer, G_TYPE_OBJECT) + +-static gboolean +-propagate_libarchive_error (GError **error, +- struct archive *a) +-{ +- g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, +- "%s", archive_error_string (a)); +- return FALSE; +-} +- + static void + flatpak_oci_layer_writer_reset (FlatpakOciLayerWriter *self) + { +diff --git a/common/flatpak-transaction.c b/common/flatpak-transaction.c +index ffee385e..1aba1daa 100644 +--- a/common/flatpak-transaction.c ++++ b/common/flatpak-transaction.c +@@ -3028,7 +3028,7 @@ flatpak_transaction_add_install_bundle (FlatpakTransaction *self, + * reference. + * + * @image_location is specified in containers-transports(5) form. Only a subset +- * of transports are supported: oci: and docker:. ++ * of transports are supported: oci:, oci-archive:, and docker:. + * + * Returns: %TRUE on success; %FALSE with @error set on failure. + */ +diff --git a/doc/flatpak-install.xml b/doc/flatpak-install.xml +index f75dedae..99aa5469 100644 +--- a/doc/flatpak-install.xml ++++ b/doc/flatpak-install.xml +@@ -145,8 +145,8 @@ + Treat LOCATION as the location of a Flatpak in + OCI image format. LOCATION is in the format of + containers-transports5. +- Supported schemes are docker:// and oci:. +- This is assumed if the argument starts ++ Supported schemes are docker://, oci:, ++ and oci-archive:. This is assumed if the argument starts + with one of these schemes. + + +diff --git a/tests/test-oci.sh b/tests/test-oci.sh +index 431fa6d6..861310a5 100755 +--- a/tests/test-oci.sh ++++ b/tests/test-oci.sh +@@ -23,7 +23,7 @@ set -euo pipefail + + skip_without_bwrap + +-echo "1..3" ++echo "1..4" + + setup_repo_no_add oci + +@@ -85,4 +85,26 @@ assert_file_has_content flatpak-list '^org.test.Platform *platform-origin$' + ${FLATPAK} --user remotes --show-disabled > remotes-list + assert_file_has_content remotes-list '^platform-origin' + ++${FLATPAK} ${U} -y uninstall org.test.Platform >&2 ++ + ok "install oci" ++ ++# Trying installing an OCI archive bundle ++ ++(cd oci/platform-image && tar cf - .) > oci/platform-image.tar ++ ++${FLATPAK} --user list --columns=application,origin > flatpak-list ++assert_not_file_has_content flatpak-list 'org.test.Platform' ++ ++${FLATPAK} --user remotes --show-disabled > remotes-list ++assert_not_file_has_content remotes-list '^platform-origin' ++ ++$FLATPAK --user -y install oci-archive:oci/platform-image.tar >&2 ++ ++${FLATPAK} --user list --columns=application,origin > flatpak-list ++assert_file_has_content flatpak-list '^org.test.Platform *platform-origin$' ++ ++${FLATPAK} --user remotes --show-disabled > remotes-list ++assert_file_has_content remotes-list '^platform-origin' ++ ++ok "install oci archive" +-- +2.47.1 + +From a2fbcf5d1db3ef69b04e7f0e4bc64c0e51dd7ffb Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Tue, 29 Oct 2024 16:59:40 -0400 +Subject: [PATCH 12/12]common: Move delta_url into the FlatpakImageSource + +Instead of passing the delta URL along with the image source, when +we create an image source for a remote registry, if we find a delta +URL in the metadata, set it on the FlatpakImageSource for later use. + +Centralize duplicated code for creating an image source for a remote +repository based on a summary lookup into one place. +--- + app/flatpak-builtins-build-import-bundle.c | 2 +- + common/flatpak-dir.c | 131 +++++++-------------- + common/flatpak-image-source-private.h | 4 + + common/flatpak-image-source.c | 41 +++++++ + common/flatpak-oci-registry-private.h | 2 - + common/flatpak-oci-registry.c | 4 +- + system-helper/flatpak-system-helper.c | 2 +- + 7 files changed, 89 insertions(+), 97 deletions(-) + +diff --git a/app/flatpak-builtins-build-import-bundle.c b/app/flatpak-builtins-build-import-bundle.c +index d79fbbde..d623386f 100644 +--- a/app/flatpak-builtins-build-import-bundle.c ++++ b/app/flatpak-builtins-build-import-bundle.c +@@ -69,7 +69,7 @@ import_oci (OstreeRepo *repo, GFile *file, + ref = flatpak_image_source_get_ref (image_source); + + commit_checksum = flatpak_pull_from_oci (repo, image_source, NULL, +- NULL, ref, FLATPAK_PULL_FLAGS_NONE, ++ ref, FLATPAK_PULL_FLAGS_NONE, + NULL, NULL, cancellable, error); + if (commit_checksum == NULL) + return NULL; +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index e8ff645b..9f398252 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -1077,14 +1077,14 @@ flatpak_remote_state_new_image_source (FlatpakRemoteState *self, + return g_steal_pointer (&image_source); + } + +-static GVariant * +-flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, +- FlatpakDir *dir, +- const char *ref, +- const char *checksum, +- const char *token, +- GCancellable *cancellable, +- GError **error) ++static FlatpakImageSource * ++flatpak_remote_state_fetch_image_source (FlatpakRemoteState *self, ++ FlatpakDir *dir, ++ const char *ref, ++ const char *opt_rev, ++ const char *token, ++ GCancellable *cancellable, ++ GError **error) + { + g_autoptr(FlatpakImageSource) image_source = NULL; + g_autofree char *oci_digest = NULL; +@@ -1092,9 +1092,7 @@ flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, + VarRefInfoRef latest_rev_info; + VarMetadataRef metadata; + const char *oci_repository = NULL; +- const char *parent = NULL; +- g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); +- g_autoptr(GVariant) metadata_v = NULL; ++ const char *delta_url = NULL; + + /* We extract the rev info from the latest, even if we don't use the latest digest, assuming refs don't move */ + if (!flatpak_remote_state_lookup_ref (self, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) +@@ -1110,8 +1108,9 @@ flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, + + metadata = var_ref_info_get_metadata (latest_rev_info); + oci_repository = var_metadata_lookup_string (metadata, "xa.oci-repository", NULL); ++ delta_url = var_metadata_lookup_string (metadata, "xa.delta-url", NULL); + +- oci_digest = g_strconcat ("sha256:", checksum, NULL); ++ oci_digest = g_strconcat ("sha256:", opt_rev ? opt_rev : latest_rev, NULL); + + image_source = flatpak_remote_state_new_image_source (self, oci_repository, oci_digest, token, cancellable, error); + if (image_source == NULL) +@@ -1123,22 +1122,28 @@ flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, + return NULL; + } + +- flatpak_image_source_build_commit_metadata (image_source, metadata_builder); +- metadata_v = g_variant_ref_sink (g_variant_builder_end (metadata_builder)); ++ flatpak_image_source_set_delta_url (image_source, delta_url); ++ ++ return g_steal_pointer (&image_source); ++} ++ + +- parent = flatpak_image_source_get_parent_commit (image_source); ++static GVariant * ++flatpak_remote_state_fetch_commit_object_oci (FlatpakRemoteState *self, ++ FlatpakDir *dir, ++ const char *ref, ++ const char *checksum, ++ const char *token, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(FlatpakImageSource) image_source = NULL; ++ ++ image_source = flatpak_remote_state_fetch_image_source (self, dir, ref, checksum, token, cancellable, error); ++ if (image_source == NULL) ++ return NULL; + +- /* This isn't going to be exactly the same as the reconstructed one from the pull, because we don't have the contents, but its useful to get metadata */ +- return +- g_variant_ref_sink (g_variant_new ("(@a{sv}@ay@a(say)sst@ay@ay)", +- metadata_v, +- parent ? ostree_checksum_to_bytes_v (parent) : g_variant_new_from_data (G_VARIANT_TYPE ("ay"), NULL, 0, FALSE, NULL, NULL), +- g_variant_new_array (G_VARIANT_TYPE ("(say)"), NULL, 0), +- flatpak_image_source_get_commit_subject (image_source), +- flatpak_image_source_get_commit_body (image_source), +- GUINT64_TO_BE (flatpak_image_source_get_commit_timestamp (image_source)), +- ostree_checksum_to_bytes_v ("0000000000000000000000000000000000000000000000000000000000000000"), +- ostree_checksum_to_bytes_v ("0000000000000000000000000000000000000000000000000000000000000000"))); ++ return flatpak_image_source_make_fake_commit (image_source); + } + + static GVariant * +@@ -5910,49 +5915,18 @@ flatpak_dir_mirror_oci (FlatpakDir *self, + GError **error) + { + g_autoptr(FlatpakImageSource) image_source = NULL; +- g_autofree char *oci_digest = NULL; +- const char *delta_url = NULL; + gboolean res; + + if (opt_image_source) +- { +- image_source = g_object_ref (opt_image_source); +- oci_digest = g_strdup (flatpak_image_source_get_digest (image_source)); +- } ++ image_source = g_object_ref (opt_image_source); + else +- { +- g_autofree char *latest_rev = NULL; +- VarRefInfoRef latest_rev_info; +- VarMetadataRef metadata; +- const char *oci_repository = NULL; +- const char *rev; +- +- /* We use the summary so that we can reuse any cached json */ +- if (!flatpak_remote_state_lookup_ref (state, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) +- return FALSE; +- if (latest_rev == NULL) +- return flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, +- _("Couldn't find latest checksum for ref %s in remote %s"), +- ref, state->remote_name); +- +- rev = opt_rev != NULL ? opt_rev : latest_rev; +- +- metadata = var_ref_info_get_metadata (latest_rev_info); +- oci_repository = var_metadata_lookup_string (metadata, "xa.oci-repository", NULL); +- delta_url = var_metadata_lookup_string (metadata, "xa.delta-url", NULL); +- +- oci_digest = g_strconcat ("sha256:", rev, NULL); +- +- image_source = flatpak_remote_state_new_image_source (state, oci_repository, oci_digest, token, cancellable, error); +- if (image_source == NULL) +- return FALSE; +- } ++ image_source = flatpak_remote_state_fetch_image_source (state, self, ref, opt_rev, token, cancellable, error); + + flatpak_progress_start_oci_pull (progress); + +- g_info ("Mirroring OCI image %s", oci_digest); ++ g_info ("Mirroring OCI image %s", flatpak_image_source_get_digest (image_source)); + +- res = flatpak_mirror_image_from_oci (dst_registry, image_source, state->remote_name, ref, delta_url, self->repo, oci_pull_progress_cb, ++ res = flatpak_mirror_image_from_oci (dst_registry, image_source, state->remote_name, ref, self->repo, oci_pull_progress_cb, + progress, cancellable, error); + + if (!res) +@@ -5977,8 +5951,7 @@ flatpak_dir_pull_oci (FlatpakDir *self, + { + g_autoptr(FlatpakImageSource) image_source = NULL; + FlatpakOciRegistry *registry = NULL; +- const char *delta_url = NULL; +- g_autofree char *oci_digest = NULL; ++ const char *oci_digest = NULL; + g_autofree char *checksum = NULL; + g_autofree char *latest_alt_commit = NULL; + G_GNUC_UNUSED g_autofree char *latest_commit = +@@ -5986,35 +5959,11 @@ flatpak_dir_pull_oci (FlatpakDir *self, + g_autofree char *name = NULL; + + if (opt_image_source) +- { +- image_source = g_object_ref (opt_image_source); +- oci_digest = g_strdup (flatpak_image_source_get_digest (image_source)); +- } ++ image_source = g_object_ref (opt_image_source); + else +- { +- VarMetadataRef metadata; +- VarRefInfoRef latest_rev_info; +- const char *oci_repository = NULL; +- g_autofree char *latest_rev = NULL; ++ image_source = flatpak_remote_state_fetch_image_source (state, self, ref, opt_rev, token, cancellable, error); + +- /* We use the summary so that we can reuse any cached json */ +- if (!flatpak_remote_state_lookup_ref (state, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) +- return FALSE; +- if (latest_rev == NULL) +- return flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, +- _("Couldn't find latest checksum for ref %s in remote %s"), +- ref, state->remote_name); +- +- metadata = var_ref_info_get_metadata (latest_rev_info); +- oci_repository = var_metadata_lookup_string (metadata, "xa.oci-repository", NULL); +- delta_url = var_metadata_lookup_string (metadata, "xa.delta-url", NULL); +- +- oci_digest = g_strconcat ("sha256:", opt_rev != NULL ? opt_rev : latest_rev, NULL); +- +- image_source = flatpak_remote_state_new_image_source (state, oci_repository, oci_digest, token, cancellable, error); +- if (image_source == NULL) +- return FALSE; +- } ++ oci_digest = flatpak_image_source_get_digest (image_source); + + /* Short circuit if we've already got this commit */ + if (latest_alt_commit != NULL && strcmp (oci_digest + strlen ("sha256:"), latest_alt_commit) == 0) +@@ -6027,7 +5976,7 @@ flatpak_dir_pull_oci (FlatpakDir *self, + + g_info ("Pulling OCI image %s", oci_digest); + +- checksum = flatpak_pull_from_oci (repo, image_source, delta_url, ++ checksum = flatpak_pull_from_oci (repo, image_source, + state->remote_name, ref, flatpak_flags, oci_pull_progress_cb, progress, cancellable, error); + + if (checksum == NULL) +diff --git a/common/flatpak-image-source-private.h b/common/flatpak-image-source-private.h +index b1e5d2fa..94e4cc1a 100644 +--- a/common/flatpak-image-source-private.h ++++ b/common/flatpak-image-source-private.h +@@ -47,10 +47,13 @@ FlatpakImageSource *flatpak_image_source_new_for_location (const char *locatio + + void flatpak_image_source_set_token (FlatpakImageSource *self, + const char *token); ++void flatpak_image_source_set_delta_url (FlatpakImageSource *self, ++ const char *delta_url); + + FlatpakOciRegistry *flatpak_image_source_get_registry (FlatpakImageSource *self); + const char *flatpak_image_source_get_oci_repository (FlatpakImageSource *self); + const char *flatpak_image_source_get_digest (FlatpakImageSource *self); ++const char *flatpak_image_source_get_delta_url (FlatpakImageSource *self); + FlatpakOciManifest *flatpak_image_source_get_manifest (FlatpakImageSource *self); + size_t flatpak_image_source_get_manifest_size (FlatpakImageSource *self); + FlatpakOciImage *flatpak_image_source_get_image_config (FlatpakImageSource *self); +@@ -66,4 +69,5 @@ const char *flatpak_image_source_get_commit_body (FlatpakImageSource *self) + void flatpak_image_source_build_commit_metadata (FlatpakImageSource *self, + GVariantBuilder *metadata_builder); + ++GVariant *flatpak_image_source_make_fake_commit (FlatpakImageSource *image_source); + #endif /* __FLATPAK_IMAGE_SOURCE_H__ */ +diff --git a/common/flatpak-image-source.c b/common/flatpak-image-source.c +index 20c94b63..9d13cf0d 100644 +--- a/common/flatpak-image-source.c ++++ b/common/flatpak-image-source.c +@@ -31,6 +31,7 @@ struct _FlatpakImageSource + FlatpakOciRegistry *registry; + char *repository; + char *digest; ++ char *delta_url; + + FlatpakOciManifest *manifest; + size_t manifest_size; +@@ -47,6 +48,7 @@ flatpak_image_source_finalize (GObject *object) + g_clear_object (&self->registry); + g_clear_pointer (&self->repository, g_free); + g_clear_pointer (&self->digest, g_free); ++ g_clear_pointer (&self->delta_url, g_free); + g_clear_object (&self->manifest); + g_clear_object (&self->image_config); + +@@ -326,6 +328,20 @@ flatpak_image_source_set_token (FlatpakImageSource *self, + flatpak_oci_registry_set_token (self->registry, token); + } + ++void ++flatpak_image_source_set_delta_url (FlatpakImageSource *self, ++ const char *delta_url) ++{ ++ g_free (self->delta_url); ++ self->delta_url = g_strdup (delta_url); ++} ++ ++const char * ++flatpak_image_source_get_delta_url (FlatpakImageSource *self) ++{ ++ return self->delta_url; ++} ++ + FlatpakOciRegistry * + flatpak_image_source_get_registry (FlatpakImageSource *self) + { +@@ -453,3 +469,28 @@ flatpak_image_source_build_commit_metadata (FlatpakImageSource *self, + g_variant_builder_add (metadata_builder, "{s@v}", key, data); + } + } ++ ++GVariant * ++flatpak_image_source_make_fake_commit (FlatpakImageSource *self) ++{ ++ const char *parent = NULL; ++ g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); ++ g_autoptr(GVariant) metadata_v = NULL; ++ ++ flatpak_image_source_build_commit_metadata (self, metadata_builder); ++ metadata_v = g_variant_ref_sink (g_variant_builder_end (metadata_builder)); ++ ++ parent = flatpak_image_source_get_parent_commit (self); ++ ++ /* This isn't going to be exactly the same as the reconstructed one from the pull, because we don't have the contents, but its useful to get metadata */ ++ return ++ g_variant_ref_sink (g_variant_new ("(@a{sv}@ay@a(say)sst@ay@ay)", ++ metadata_v, ++ parent ? ostree_checksum_to_bytes_v (parent) : g_variant_new_from_data (G_VARIANT_TYPE ("ay"), NULL, 0, FALSE, NULL, NULL), ++ g_variant_new_array (G_VARIANT_TYPE ("(say)"), NULL, 0), ++ flatpak_image_source_get_commit_subject (self), ++ flatpak_image_source_get_commit_body (self), ++ GUINT64_TO_BE (flatpak_image_source_get_commit_timestamp (self)), ++ ostree_checksum_to_bytes_v ("0000000000000000000000000000000000000000000000000000000000000000"), ++ ostree_checksum_to_bytes_v ("0000000000000000000000000000000000000000000000000000000000000000"))); ++} +diff --git a/common/flatpak-oci-registry-private.h b/common/flatpak-oci-registry-private.h +index d48c8ed4..7867c761 100644 +--- a/common/flatpak-oci-registry-private.h ++++ b/common/flatpak-oci-registry-private.h +@@ -195,7 +195,6 @@ typedef void (*FlatpakOciPullProgress) (guint64 total_size, + + char * flatpak_pull_from_oci (OstreeRepo *repo, + FlatpakImageSource *image_source, +- const char *delta_url, + const char *remote, + const char *ref, + FlatpakPullFlags flags, +@@ -208,7 +207,6 @@ gboolean flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, + FlatpakImageSource *image_source, + const char *remote, + const char *ref, +- const char *delta_url, + OstreeRepo *repo, + FlatpakOciPullProgress progress_cb, + gpointer progress_data, +diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c +index ebf000f8..c3dff5d6 100644 +--- a/common/flatpak-oci-registry.c ++++ b/common/flatpak-oci-registry.c +@@ -3658,7 +3658,6 @@ flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, + FlatpakImageSource *image_source, + const char *remote, + const char *ref, +- const char *delta_url, + OstreeRepo *repo, + FlatpakOciPullProgress progress_cb, + gpointer progress_user_data, +@@ -3670,6 +3669,7 @@ flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, + const char *oci_repository = flatpak_image_source_get_oci_repository (image_source); + const char *digest = flatpak_image_source_get_digest (image_source); + FlatpakOciManifest *manifest = flatpak_image_source_get_manifest (image_source); ++ const char *delta_url = flatpak_image_source_get_delta_url (image_source); + FlatpakOciImage *image_config = flatpak_image_source_get_image_config (image_source); + g_autoptr(FlatpakOciDescriptor) manifest_desc = NULL; + g_autoptr(FlatpakOciManifest) delta_manifest = NULL; +@@ -3785,7 +3785,6 @@ flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry, + char * + flatpak_pull_from_oci (OstreeRepo *repo, + FlatpakImageSource *image_source, +- const char *delta_url, + const char *remote, + const char *ref, + FlatpakPullFlags flags, +@@ -3798,6 +3797,7 @@ flatpak_pull_from_oci (OstreeRepo *repo, + const char *oci_repository = flatpak_image_source_get_oci_repository (image_source); + const char *digest = flatpak_image_source_get_digest (image_source); + FlatpakOciManifest *manifest = flatpak_image_source_get_manifest (image_source); ++ const char *delta_url = flatpak_image_source_get_delta_url (image_source); + FlatpakOciImage *image_config = flatpak_image_source_get_image_config (image_source); + gboolean force_disable_deltas = (flags & FLATPAK_PULL_FLAGS_NO_STATIC_DELTAS) != 0; + g_autoptr(OstreeMutableTree) archive_mtree = NULL; +diff --git a/system-helper/flatpak-system-helper.c b/system-helper/flatpak-system-helper.c +index 2740b299..1f950b20 100644 +--- a/system-helper/flatpak-system-helper.c ++++ b/system-helper/flatpak-system-helper.c +@@ -593,7 +593,7 @@ handle_deploy (FlatpakSystemHelper *object, + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- checksum = flatpak_pull_from_oci (flatpak_dir_get_repo (system), image_source, NULL, ++ checksum = flatpak_pull_from_oci (flatpak_dir_get_repo (system), image_source, + arg_origin, arg_ref, FLATPAK_PULL_FLAGS_NONE, NULL, NULL, NULL, &error); + if (checksum == NULL) + { +-- +2.47.1 + diff --git a/flatpak-enable-collection-ids-for-oci-remotes.patch b/flatpak-enable-collection-ids-for-oci-remotes.patch new file mode 100644 index 0000000000000000000000000000000000000000..f40a7ad56d7bb55621404855ac0ada88bc99e336 --- /dev/null +++ b/flatpak-enable-collection-ids-for-oci-remotes.patch @@ -0,0 +1,53 @@ +From 5c84fa21cc590811936d36dd8b122025a4340f85 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Wed, 30 Oct 2024 14:27:44 -0400 +Subject: [PATCH] Enable collection IDs for OCI remotes + +We want to use collection IDs to specify what remote to install from +when processing /etc/flatpak/preinstall.d; in order for this to work +for OCI remotes, we need to permit collection IDs. + + - In flatpakrepo files, don't require a GPGKey for a OCI remote + with a collection - we don't have signature verification for GPG remotes. + - Don't validate that the collection ID appears in the summary - + the image index doesn't currently contain an image ID +--- + common/flatpak-dir.c | 6 +++++- + common/flatpak-repo-utils.c | 5 ++++- + 2 files changed, 9 insertions(+), 2 deletions(-) + +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index 6936d45f89..b0937eaa65 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -12995,9 +12995,13 @@ _flatpak_dir_get_remote_state (FlatpakDir *self, + return NULL; + } + ++ /* For OCI remotes, the collection ID is local configuration only: ++ * In the future we could add it to the index format. ++ */ + if (state->collection_id != NULL && + state->summary != NULL && +- !_validate_summary_for_collection_id (state->summary, state->collection_id, error)) ++ !(flatpak_dir_get_remote_oci (self, state->remote_name) || ++ _validate_summary_for_collection_id (state->summary, state->collection_id, error))) + return NULL; + + if (flatpak_dir_get_remote_oci (self, remote_or_uri)) +diff --git a/common/flatpak-repo-utils.c b/common/flatpak-repo-utils.c +index 63dc9981e7..52508d2df1 100644 +--- a/common/flatpak-repo-utils.c ++++ b/common/flatpak-repo-utils.c +@@ -2929,7 +2929,10 @@ flatpak_parse_repofile (const char *remote_name, + FLATPAK_REPO_COLLECTION_ID_KEY); + if (collection_id != NULL) + { +- if (gpg_key == NULL) ++ /* We don't support signatures for OCI remotes, but Collection ID's are ++ * still useful for preinstallation. ++ */ ++ if (gpg_key == NULL && !g_str_has_prefix (uri, "oci+")) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Collection ID requires GPG key to be provided")); + return NULL; diff --git a/flatpak-implement-etc-containers-certs-for-oci-registries.patch b/flatpak-implement-etc-containers-certs-for-oci-registries.patch new file mode 100644 index 0000000000000000000000000000000000000000..361ecb752b64c0a2fb76a5959cace48653ec3b9b --- /dev/null +++ b/flatpak-implement-etc-containers-certs-for-oci-registries.patch @@ -0,0 +1,2003 @@ +From cec97aac1c9fad9b5bc18d1166b63edb13ca2bcc Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Tue, 17 Dec 2024 15:35:21 +0100 +Subject: [PATCH 01/09] tests/oci-registry-client.py: Drop python2 + compatibility + +--- + tests/oci-registry-client.py | 15 ++++++--------- + 1 file changed, 6 insertions(+), 9 deletions(-) + +diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py +index 7f1eeb54..b9ca63d7 100644 +--- a/tests/oci-registry-client.py ++++ b/tests/oci-registry-client.py +@@ -2,12 +2,9 @@ + + import sys + +-if sys.version_info[0] >= 3: +- import http.client as http_client +- import urllib.parse as urllib_parse +-else: +- import http.client as http_client +- import urllib as urllib_parse ++import http.client ++import urllib.parse ++ + + if sys.argv[2] == 'add': + detach_icons = '--detach-icons' in sys.argv +@@ -16,8 +13,8 @@ if sys.argv[2] == 'add': + params = {'d': sys.argv[5]} + if detach_icons: + params['detach-icons'] = 1 +- query = urllib_parse.urlencode(params) +- conn = http_client.HTTPConnection(sys.argv[1]) ++ query = urllib.parse.urlencode(params) ++ conn = http.client.HTTPConnection(sys.argv[1]) + path = "/testing/{repo}/{tag}?{query}".format(repo=sys.argv[3], + tag=sys.argv[4], + query=query) +@@ -28,7 +25,7 @@ if sys.argv[2] == 'add': + print("Failed: status={}".format(response.status), file=sys.stderr) + sys.exit(1) + elif sys.argv[2] == 'delete': +- conn = http_client.HTTPConnection(sys.argv[1]) ++ conn = http.client.HTTPConnection(sys.argv[1]) + path = "/testing/{repo}/{ref}".format(repo=sys.argv[3], + ref=sys.argv[4]) + conn.request("DELETE", path) +-- +2.47.1 + +From d8ce35c9d1c0b1c83127b07abe9b3479170cc8f6 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Tue, 17 Dec 2024 15:41:53 +0100 +Subject: [PATCH 02/09] tests/oci-registry-client.py: Parse URL parameter + +--- + tests/oci-registry-client.py | 9 +++++++-- + tests/test-oci-registry.sh | 2 +- + 2 files changed, 8 insertions(+), 3 deletions(-) + +diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py +index b9ca63d7..cd835609 100644 +--- a/tests/oci-registry-client.py ++++ b/tests/oci-registry-client.py +@@ -6,6 +6,11 @@ import http.client + import urllib.parse + + ++def get_conn(url): ++ parsed = urllib.parse.urlparse(url) ++ return http.client.HTTPConnection(host=parsed.hostname, port=parsed.port) ++ ++ + if sys.argv[2] == 'add': + detach_icons = '--detach-icons' in sys.argv + if detach_icons: +@@ -14,7 +19,7 @@ if sys.argv[2] == 'add': + if detach_icons: + params['detach-icons'] = 1 + query = urllib.parse.urlencode(params) +- conn = http.client.HTTPConnection(sys.argv[1]) ++ conn = get_conn(sys.argv[1]) + path = "/testing/{repo}/{tag}?{query}".format(repo=sys.argv[3], + tag=sys.argv[4], + query=query) +@@ -25,7 +30,7 @@ if sys.argv[2] == 'add': + print("Failed: status={}".format(response.status), file=sys.stderr) + sys.exit(1) + elif sys.argv[2] == 'delete': +- conn = http.client.HTTPConnection(sys.argv[1]) ++ conn = get_conn(sys.argv[1]) + path = "/testing/{repo}/{ref}".format(repo=sys.argv[3], + ref=sys.argv[4]) + conn.request("DELETE", path) +diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh +index 8eb154f5..51a6142f 100755 +--- a/tests/test-oci-registry.sh ++++ b/tests/test-oci-registry.sh +@@ -29,7 +29,7 @@ echo "1..14" + + httpd oci-registry-server.py . + port=$(cat httpd-port) +-client="python3 $test_srcdir/oci-registry-client.py 127.0.0.1:$port" ++client="python3 $test_srcdir/oci-registry-client.py http://127.0.0.1:$port" + + setup_repo_no_add oci + +-- +2.47.1 + +From 0757171aa07f3b8d390881e0765de09ed77d4825 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Tue, 17 Dec 2024 15:47:07 +0100 +Subject: [PATCH 03/09] tests/oci-registry-client.py: Convert to argparse + +--- + tests/oci-registry-client.py | 57 +++++++++++++++++++++++------------- + tests/test-oci-registry.sh | 2 +- + 2 files changed, 38 insertions(+), 21 deletions(-) + mode change 100644 => 100755 tests/oci-registry-client.py + +diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py +old mode 100644 +new mode 100755 +index cd835609..5654062b +--- a/tests/oci-registry-client.py ++++ b/tests/oci-registry-client.py +@@ -1,45 +1,62 @@ + #!/usr/bin/python3 + ++import argparse + import sys + + import http.client + import urllib.parse + + +-def get_conn(url): +- parsed = urllib.parse.urlparse(url) ++def get_conn(args): ++ parsed = urllib.parse.urlparse(args.url) + return http.client.HTTPConnection(host=parsed.hostname, port=parsed.port) + + +-if sys.argv[2] == 'add': +- detach_icons = '--detach-icons' in sys.argv +- if detach_icons: +- sys.argv.remove('--detach-icons') +- params = {'d': sys.argv[5]} +- if detach_icons: +- params['detach-icons'] = 1 ++def run_add(args): ++ params = {"d": args.oci_dir} ++ if args.detach_icons: ++ params["detach-icons"] = "1" + query = urllib.parse.urlencode(params) +- conn = get_conn(sys.argv[1]) +- path = "/testing/{repo}/{tag}?{query}".format(repo=sys.argv[3], +- tag=sys.argv[4], +- query=query) ++ conn = get_conn(args) ++ path = "/testing/{repo}/{tag}?{query}".format( ++ repo=args.repo, tag=args.tag, query=query ++ ) + conn.request("POST", path) + response = conn.getresponse() + if response.status != 200: + print(response.read(), file=sys.stderr) + print("Failed: status={}".format(response.status), file=sys.stderr) + sys.exit(1) +-elif sys.argv[2] == 'delete': +- conn = get_conn(sys.argv[1]) +- path = "/testing/{repo}/{ref}".format(repo=sys.argv[3], +- ref=sys.argv[4]) ++ ++ ++def run_delete(args): ++ conn = get_conn(args) ++ path = "/testing/{repo}/{ref}".format(repo=args.repo, ref=args.ref) + conn.request("DELETE", path) + response = conn.getresponse() + if response.status != 200: + print(response.read(), file=sys.stderr) + print("Failed: status={}".format(response.status), file=sys.stderr) + sys.exit(1) +-else: +- print("Usage: oci-registry-client.py [add|remove] ARGS", file=sys.stderr) +- sys.exit(1) + ++ ++parser = argparse.ArgumentParser() ++parser.add_argument("--url", required=True) ++ ++subparsers = parser.add_subparsers() ++subparsers.required = True ++ ++add_parser = subparsers.add_parser("add") ++add_parser.add_argument("repo") ++add_parser.add_argument("tag") ++add_parser.add_argument("oci_dir") ++add_parser.add_argument("--detach-icons", action="store_true", default=False) ++add_parser.set_defaults(func=run_add) ++ ++delete_parser = subparsers.add_parser("delete") ++delete_parser.add_argument("repo") ++delete_parser.add_argument("ref") ++delete_parser.set_defaults(func=run_delete) ++ ++args = parser.parse_args() ++args.func(args) +diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh +index 51a6142f..bc2f138b 100755 +--- a/tests/test-oci-registry.sh ++++ b/tests/test-oci-registry.sh +@@ -29,7 +29,7 @@ echo "1..14" + + httpd oci-registry-server.py . + port=$(cat httpd-port) +-client="python3 $test_srcdir/oci-registry-client.py http://127.0.0.1:$port" ++client="python3 $test_srcdir/oci-registry-client.py --url=http://127.0.0.1:$port" + + setup_repo_no_add oci + +-- +2.47.1 + +From f38197c03dc77b9192c6dbc59e175b3cb640614a Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Thu, 22 Aug 2024 02:43:13 -0400 +Subject: [PATCH 04/09] tests/oci-registry-server.py: Clean up Python style + +--- + .flake8 | 2 + + tests/oci-registry-server.py | 170 ++++++++++++++++++----------------- + 2 files changed, 88 insertions(+), 84 deletions(-) + create mode 100644 .flake8 + +diff --git a/.flake8 b/.flake8 +new file mode 100644 +index 00000000..7da1f960 +--- /dev/null ++++ b/.flake8 +@@ -0,0 +1,2 @@ ++[flake8] ++max-line-length = 100 +diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py +index 33c3b646..3050a883 100755 +--- a/tests/oci-registry-server.py ++++ b/tests/oci-registry-server.py +@@ -13,65 +13,61 @@ import http.server as http_server + repositories = {} + icons = {} + ++ + def get_index(): + results = [] + for repo_name in sorted(repositories.keys()): + repo = repositories[repo_name] +- results.append({ +- 'Name': repo_name, +- 'Images': repo['images'], +- 'Lists': [], +- }) ++ results.append( ++ { ++ "Name": repo_name, ++ "Images": repo["images"], ++ "Lists": [], ++ } ++ ) ++ ++ return json.dumps({"Registry": "/", "Results": results}, indent=4) + +- return json.dumps({ +- 'Registry': '/', +- 'Results': results +- }, indent=4) + + def cache_icon(data_uri): +- prefix = 'data:image/png;base64,' ++ prefix = "data:image/png;base64," + assert data_uri.startswith(prefix) +- data = base64.b64decode(data_uri[len(prefix):]) ++ data = base64.b64decode(data_uri[len(prefix) :]) + h = hashlib.sha256() + h.update(data) + digest = h.hexdigest() +- filename = digest + '.png' ++ filename = digest + ".png" + icons[filename] = data + +- return '/icons/' + filename ++ return "/icons/" + filename ++ + + serial = 0 + server_start_time = int(time.time()) + ++ + def get_etag(): +- return str(server_start_time) + '-' + str(serial) ++ return str(server_start_time) + "-" + str(serial) ++ + + def modified(): + global serial + serial += 1 + +-def parse_http_date(date): +- parsed = parsedate(date) +- if parsed is not None: +- return timegm(parsed) +- else: +- return None + + class RequestHandler(http_server.BaseHTTPRequestHandler): + def check_route(self, route): +- parts = self.path.split('?', 1) +- path = parts[0].split('/') +- +- result = [] ++ parts = self.path.split("?", 1) ++ path = parts[0].split("/") + +- route_path = route.split('/') ++ route_path = route.split("/") + print((route_path, path)) + if len(route_path) != len(path): + return False + + matches = {} + for i in range(1, len(route_path)): +- if route_path[i][0] == '@': ++ if route_path[i][0] == "@": + matches[route_path[i][1:]] = path[i] + elif route_path[i] != path[i]: + return False +@@ -92,24 +88,25 @@ class RequestHandler(http_server.BaseHTTPRequestHandler): + + add_headers = {} + +- if self.check_route('/v2/@repo_name/blobs/@digest'): +- repo_name = self.matches['repo_name'] +- digest = self.matches['digest'] +- response_file = repositories[repo_name]['blobs'][digest] +- elif self.check_route('/v2/@repo_name/manifests/@ref'): +- repo_name = self.matches['repo_name'] +- ref = self.matches['ref'] +- response_file = repositories[repo_name]['manifests'][ref] +- elif self.check_route('/index/static') or self.check_route('/index/dynamic'): ++ if self.check_route("/v2/@repo_name/blobs/@digest"): ++ repo_name = self.matches["repo_name"] ++ digest = self.matches["digest"] ++ response_file = repositories[repo_name]["blobs"][digest] ++ elif self.check_route("/v2/@repo_name/manifests/@ref"): ++ repo_name = self.matches["repo_name"] ++ ref = self.matches["ref"] ++ response_file = repositories[repo_name]["manifests"][ref] ++ elif self.check_route("/index/static") or self.check_route("/index/dynamic"): + etag = get_etag() + if self.headers.get("If-None-Match") == etag: + response = 304 + else: + response_string = get_index() +- add_headers['Etag'] = etag +- elif self.check_route('/icons/@filename') : +- response_string = icons[self.matches['filename']] +- response_content_type = 'image/png' ++ add_headers["Etag"] = etag ++ elif self.check_route("/icons/@filename"): ++ response_string = icons[self.matches["filename"]] ++ assert isinstance(response_string, bytes) ++ response_content_type = "image/png" + else: + response = 404 + +@@ -121,86 +118,89 @@ class RequestHandler(http_server.BaseHTTPRequestHandler): + self.send_header("Content-Type", response_content_type) + + if response == 200 or response == 304: +- self.send_header('Cache-Control', 'no-cache') ++ self.send_header("Cache-Control", "no-cache") + + self.end_headers() + + if response == 200: + if response_file: +- with open(response_file, 'rb') as f: ++ with open(response_file, "rb") as f: + response_string = f.read() + + if isinstance(response_string, bytes): + self.wfile.write(response_string) + else: +- self.wfile.write(response_string.encode('utf-8')) ++ assert isinstance(response_string, str) ++ self.wfile.write(response_string.encode("utf-8")) + + def do_HEAD(self): + return self.do_GET() + + def do_POST(self): +- if self.check_route('/testing/@repo_name/@tag'): +- repo_name = self.matches['repo_name'] +- tag = self.matches['tag'] +- d = self.query['d'][0] +- detach_icons = 'detach-icons' in self.query ++ if self.check_route("/testing/@repo_name/@tag"): ++ repo_name = self.matches["repo_name"] ++ tag = self.matches["tag"] ++ d = self.query["d"][0] ++ detach_icons = "detach-icons" in self.query + + repo = repositories.setdefault(repo_name, {}) +- blobs = repo.setdefault('blobs', {}) +- manifests = repo.setdefault('manifests', {}) +- images = repo.setdefault('images', []) ++ blobs = repo.setdefault("blobs", {}) ++ manifests = repo.setdefault("manifests", {}) ++ images = repo.setdefault("images", []) + +- with open(os.path.join(d, 'index.json')) as f: ++ with open(os.path.join(d, "index.json")) as f: + index = json.load(f) + +- manifest_digest = index['manifests'][0]['digest'] +- manifest_path = os.path.join(d, 'blobs', *manifest_digest.split(':')) ++ manifest_digest = index["manifests"][0]["digest"] ++ manifest_path = os.path.join(d, "blobs", *manifest_digest.split(":")) + manifests[manifest_digest] = manifest_path + manifests[tag] = manifest_path + + with open(manifest_path) as f: + manifest = json.load(f) + +- config_digest = manifest['config']['digest'] +- config_path = os.path.join(d, 'blobs', *config_digest.split(':')) ++ config_digest = manifest["config"]["digest"] ++ config_path = os.path.join(d, "blobs", *config_digest.split(":")) + + with open(config_path) as f: + config = json.load(f) + +- for dig in os.listdir(os.path.join(d, 'blobs', 'sha256')): +- digest = 'sha256:' + dig +- path = os.path.join(d, 'blobs', 'sha256', dig) ++ for dig in os.listdir(os.path.join(d, "blobs", "sha256")): ++ digest = "sha256:" + dig ++ path = os.path.join(d, "blobs", "sha256", dig) + if digest != manifest_digest: + blobs[digest] = path + + if detach_icons: + for size in (64, 128): +- annotation = 'org.freedesktop.appstream.icon-{}'.format(size) +- icon = manifest.get('annotations', {}).get(annotation) ++ annotation = "org.freedesktop.appstream.icon-{}".format(size) ++ icon = manifest.get("annotations", {}).get(annotation) + if icon: + path = cache_icon(icon) +- manifest['annotations'][annotation] = path ++ manifest["annotations"][annotation] = path + else: +- icon = config.get('config', {}).get('Labels', {}).get(annotation) ++ icon = ( ++ config.get("config", {}).get("Labels", {}).get(annotation) ++ ) + if icon: + path = cache_icon(icon) +- config['config']['Labels'][annotation] = path ++ config["config"]["Labels"][annotation] = path + + image = { + "Tags": [tag], + "Digest": manifest_digest, + "MediaType": "application/vnd.oci.image.manifest.v1+json", +- "OS": config['os'], +- "Architecture": config['architecture'], +- "Annotations": manifest.get('annotations', {}), +- "Labels": config.get('config', {}).get('Labels', {}), ++ "OS": config["os"], ++ "Architecture": config["architecture"], ++ "Annotations": manifest.get("annotations", {}), ++ "Labels": config.get("config", {}).get("Labels", {}), + } + + # Delete old versions + for i in images: +- if tag in i['Tags']: ++ if tag in i["Tags"]: + images.remove(i) +- del manifests[i['Digest']] ++ del manifests[i["Digest"]] + + images.append(image) + +@@ -214,26 +214,26 @@ class RequestHandler(http_server.BaseHTTPRequestHandler): + return + + def do_DELETE(self): +- if self.check_route('/testing/@repo_name/@ref'): +- repo_name = self.matches['repo_name'] +- ref = self.matches['ref'] ++ if self.check_route("/testing/@repo_name/@ref"): ++ repo_name = self.matches["repo_name"] ++ ref = self.matches["ref"] + + repo = repositories.setdefault(repo_name, {}) +- blobs = repo.setdefault('blobs', {}) +- manifests = repo.setdefault('manifests', {}) +- images = repo.setdefault('images', []) ++ repo.setdefault("blobs", {}) ++ manifests = repo.setdefault("manifests", {}) ++ images = repo.setdefault("images", []) + + image = None + for i in images: +- if i['Digest'] == ref or ref in i['Tags']: ++ if i["Digest"] == ref or ref in i["Tags"]: + image = i + break + + assert image + + images.remove(image) +- del manifests[image['Digest']] +- for t in image['Tags']: ++ del manifests[image["Digest"]] ++ for t in image["Tags"]: + del manifests[t] + + modified() +@@ -245,22 +245,24 @@ class RequestHandler(http_server.BaseHTTPRequestHandler): + self.end_headers() + return + ++ + def run(dir): + RequestHandler.protocol_version = "HTTP/1.0" +- httpd = http_server.HTTPServer( ("127.0.0.1", 0), RequestHandler) ++ httpd = http_server.HTTPServer(("127.0.0.1", 0), RequestHandler) + host, port = httpd.socket.getsockname()[:2] +- with open("httpd-port", 'w') as file: ++ with open("httpd-port", "w") as file: + file.write("%d" % port) + try: +- os.write(3, bytes("Started\n", 'utf-8')); +- except: ++ os.write(3, bytes("Started\n", "utf-8")) ++ except OSError: + pass +- print("Serving HTTP on port %d" % port); ++ print("Serving HTTP on port %d" % port) + if dir: + os.chdir(dir) + httpd.serve_forever() + +-if __name__ == '__main__': ++ ++if __name__ == "__main__": + dir = None + if len(sys.argv) >= 2 and len(sys.argv[1]) > 0: + dir = sys.argv[1] +-- +2.47.1 + +From 47ff75860097afe0b07758b2457a52f0e6b94bd7 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Thu, 22 Aug 2024 02:47:19 -0400 +Subject: [PATCH 05/09] tests/oci-registry-server.py: Convert to argparse + +--- + tests/oci-registry-server.py | 15 ++++++++------- + tests/test-oci-registry.sh | 2 +- + 2 files changed, 9 insertions(+), 8 deletions(-) + +diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py +index 3050a883..65421f34 100755 +--- a/tests/oci-registry-server.py ++++ b/tests/oci-registry-server.py +@@ -1,5 +1,6 @@ + #!/usr/bin/python3 + ++import argparse + import base64 + import hashlib + import json +@@ -246,7 +247,7 @@ class RequestHandler(http_server.BaseHTTPRequestHandler): + return + + +-def run(dir): ++def run(args): + RequestHandler.protocol_version = "HTTP/1.0" + httpd = http_server.HTTPServer(("127.0.0.1", 0), RequestHandler) + host, port = httpd.socket.getsockname()[:2] +@@ -257,14 +258,14 @@ def run(dir): + except OSError: + pass + print("Serving HTTP on port %d" % port) +- if dir: +- os.chdir(dir) ++ if args.dir: ++ os.chdir(args.dir) + httpd.serve_forever() + + + if __name__ == "__main__": +- dir = None +- if len(sys.argv) >= 2 and len(sys.argv[1]) > 0: +- dir = sys.argv[1] ++ parser = argparse.ArgumentParser() ++ parser.add_argument("--dir") ++ args = parser.parse_args() + +- run(dir) ++ run(args) +diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh +index bc2f138b..12036358 100755 +--- a/tests/test-oci-registry.sh ++++ b/tests/test-oci-registry.sh +@@ -27,7 +27,7 @@ echo "1..14" + + # Start the fake registry server + +-httpd oci-registry-server.py . ++httpd oci-registry-server.py --dir=. + port=$(cat httpd-port) + client="python3 $test_srcdir/oci-registry-client.py --url=http://127.0.0.1:$port" + +-- +2.47.1 + +From c2c2f3679608a32339dadb233dc11633f22b0793 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Tue, 17 Dec 2024 16:39:16 +0100 +Subject: [PATCH 06/09] tests/oci-registry-server.py: Always get bytes for the + response + +And sent the Content-Length header. +--- + tests/oci-registry-server.py | 46 +++++++++++++++++++----------------- + 1 file changed, 24 insertions(+), 22 deletions(-) + +diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py +index 65421f34..13bf50b3 100755 +--- a/tests/oci-registry-server.py ++++ b/tests/oci-registry-server.py +@@ -5,7 +5,6 @@ import base64 + import hashlib + import json + import os +-import sys + import time + + from urllib.parse import parse_qs +@@ -27,7 +26,7 @@ def get_index(): + } + ) + +- return json.dumps({"Registry": "/", "Results": results}, indent=4) ++ return json.dumps({"Registry": "/", "Results": results}, indent=4).encode("UTF-8") + + + def cache_icon(data_uri): +@@ -62,7 +61,6 @@ class RequestHandler(http_server.BaseHTTPRequestHandler): + path = parts[0].split("/") + + route_path = route.split("/") +- print((route_path, path)) + if len(route_path) != len(path): + return False + +@@ -82,34 +80,47 @@ class RequestHandler(http_server.BaseHTTPRequestHandler): + return True + + def do_GET(self): +- response = 200 +- response_string = None ++ response = 404 ++ response_string = b"" + response_content_type = "application/octet-stream" +- response_file = None + + add_headers = {} + ++ def get_file_contents(repo_name, type, ref): ++ try: ++ path = repositories[repo_name][type][ref] ++ with open(path, "rb") as f: ++ return 200, f.read() ++ except KeyError: ++ return 404, b"" ++ + if self.check_route("/v2/@repo_name/blobs/@digest"): + repo_name = self.matches["repo_name"] + digest = self.matches["digest"] +- response_file = repositories[repo_name]["blobs"][digest] ++ response, response_string = get_file_contents(repo_name, "blobs", digest) + elif self.check_route("/v2/@repo_name/manifests/@ref"): + repo_name = self.matches["repo_name"] + ref = self.matches["ref"] +- response_file = repositories[repo_name]["manifests"][ref] ++ response, response_string = get_file_contents(repo_name, "manifests", ref) + elif self.check_route("/index/static") or self.check_route("/index/dynamic"): + etag = get_etag() ++ add_headers["Etag"] = etag + if self.headers.get("If-None-Match") == etag: + response = 304 ++ response_string = b"" + else: ++ response = 200 + response_string = get_index() +- add_headers["Etag"] = etag + elif self.check_route("/icons/@filename"): ++ response = 200 + response_string = icons[self.matches["filename"]] +- assert isinstance(response_string, bytes) + response_content_type = "image/png" +- else: +- response = 404 ++ ++ assert isinstance(response, int) ++ assert isinstance(response_string, bytes) ++ assert isinstance(response_content_type, str) ++ ++ add_headers["Content-Length"] = len(response_string) + + self.send_response(response) + for k, v in list(add_headers.items()): +@@ -123,16 +134,7 @@ class RequestHandler(http_server.BaseHTTPRequestHandler): + + self.end_headers() + +- if response == 200: +- if response_file: +- with open(response_file, "rb") as f: +- response_string = f.read() +- +- if isinstance(response_string, bytes): +- self.wfile.write(response_string) +- else: +- assert isinstance(response_string, str) +- self.wfile.write(response_string.encode("utf-8")) ++ self.wfile.write(response_string) + + def do_HEAD(self): + return self.do_GET() +-- +2.47.1 + +From 85379a0fe6b79d216a29497980ce2c8e0ab46997 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Tue, 17 Dec 2024 16:41:38 +0100 +Subject: [PATCH 07/09] tests/oci-registry: Add support for SSL to client and + server + +--- + tests/oci-registry-client.py | 19 ++++++++++++++++++- + tests/oci-registry-server.py | 22 +++++++++++++++++++++- + 2 files changed, 39 insertions(+), 2 deletions(-) + +diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py +index 5654062b..c4707c07 100755 +--- a/tests/oci-registry-client.py ++++ b/tests/oci-registry-client.py +@@ -1,6 +1,7 @@ + #!/usr/bin/python3 + + import argparse ++import ssl + import sys + + import http.client +@@ -9,7 +10,20 @@ import urllib.parse + + def get_conn(args): + parsed = urllib.parse.urlparse(args.url) +- return http.client.HTTPConnection(host=parsed.hostname, port=parsed.port) ++ if parsed.scheme == "http": ++ return http.client.HTTPConnection(host=parsed.hostname, port=parsed.port) ++ elif parsed.scheme == "https": ++ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ++ context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 ++ if args.cert: ++ context.load_cert_chain(certfile=args.cert, keyfile=args.key) ++ if args.cacert: ++ context.load_verify_locations(cafile=args.cacert) ++ return http.client.HTTPSConnection( ++ host=parsed.hostname, port=parsed.port, context=context ++ ) ++ else: ++ assert False, "Bad scheme: " + parsed.scheme + + + def run_add(args): +@@ -42,6 +56,9 @@ def run_delete(args): + + parser = argparse.ArgumentParser() + parser.add_argument("--url", required=True) ++parser.add_argument("--cacert") ++parser.add_argument("--cert") ++parser.add_argument("--key") + + subparsers = parser.add_subparsers() + subparsers.required = True +diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py +index 13bf50b3..2bbe8c6e 100755 +--- a/tests/oci-registry-server.py ++++ b/tests/oci-registry-server.py +@@ -5,6 +5,7 @@ import base64 + import hashlib + import json + import os ++import ssl + import time + + from urllib.parse import parse_qs +@@ -252,6 +253,19 @@ class RequestHandler(http_server.BaseHTTPRequestHandler): + def run(args): + RequestHandler.protocol_version = "HTTP/1.0" + httpd = http_server.HTTPServer(("127.0.0.1", 0), RequestHandler) ++ ++ if args.cert: ++ context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ++ context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 ++ context.load_cert_chain(certfile=args.cert, keyfile=args.key) ++ ++ if args.mtls_cacert: ++ context.load_verify_locations(cafile=args.mtls_cacert) ++ # In a real application, we'd need to check the CN against authorized users ++ context.verify_mode = ssl.CERT_REQUIRED ++ ++ httpd.socket = context.wrap_socket(httpd.socket, server_side=True) ++ + host, port = httpd.socket.getsockname()[:2] + with open("httpd-port", "w") as file: + file.write("%d" % port) +@@ -259,7 +273,10 @@ def run(args): + os.write(3, bytes("Started\n", "utf-8")) + except OSError: + pass +- print("Serving HTTP on port %d" % port) ++ if args.cert: ++ print("Serving HTTPS on port %d" % port) ++ else: ++ print("Serving HTTP on port %d" % port) + if args.dir: + os.chdir(args.dir) + httpd.serve_forever() +@@ -268,6 +285,9 @@ def run(args): + if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--dir") ++ parser.add_argument("--cert") ++ parser.add_argument("--key") ++ parser.add_argument("--mtls-cacert") + args = parser.parse_args() + + run(args) +-- +2.47.1 + +From 68b3fdcc0b00ee1080a1a3cd15dff33f1de6c0bf Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Fri, 23 Aug 2024 09:48:26 -0400 +Subject: [PATCH 08/09] common: Implement /etc/containers/certs.d for OCI + registries + +Docker and podman can be configured to use mutual TLS authentication +to the registry by dropping files into system-wide and user +directories. Implement this in a largely compatible way. + +(Because of the limitations of our underlying libraries, we +can't support multiple certificates within the same host config, +but I don't expect anybody actually needs that.) + +The certs.d handling is extended so that certificates are separately +looked up when downloading the look-aside index. This is mostly +to simplify our tests, so we can use one web server for both - +in actual operation, we expect the indexes to be unauthenticated. + +Also for testing purposes, FLATPAK_CONTAINER_CERTS_D is supported +to override the standard search path. + +Co-authored-by: Sebastian Wick +--- + .github/workflows/check.yml | 2 +- + common/flatpak-oci-registry.c | 86 +++++-- + common/flatpak-utils-http-private.h | 12 + + common/flatpak-utils-http.c | 368 +++++++++++++++++++++++++++- + doc/flatpak-remote.xml | 6 + + tests/httpcache.c | 2 +- + 6 files changed, 440 insertions(+), 36 deletions(-) + +diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml +index dbf44c1d..6cce2a6f 100644 +--- a/.github/workflows/check.yml ++++ b/.github/workflows/check.yml +@@ -52,7 +52,7 @@ jobs: + libjson-glib-dev shared-mime-info desktop-file-utils libpolkit-agent-1-dev libpolkit-gobject-1-dev \ + libseccomp-dev libsoup2.4-dev libcurl4-openssl-dev libsystemd-dev libxml2-utils libgpgme11-dev gobject-introspection \ + libgirepository1.0-dev libappstream-dev libdconf-dev clang socat meson libdbus-1-dev e2fslibs-dev bubblewrap xdg-dbus-proxy \ +- python3-pip meson ninja-build libyaml-dev libstemmer-dev gperf itstool libmalcontent-0-dev ++ python3-pip meson ninja-build libyaml-dev libstemmer-dev gperf itstool libmalcontent-0-dev openssl + # One of the tests wants this + sudo mkdir /tmp/flatpak-com.example.App-OwnedByRoot + - name: Check out flatpak +diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c +index ad595e53..6d36de2a 100644 +--- a/common/flatpak-oci-registry.c ++++ b/common/flatpak-oci-registry.c +@@ -76,7 +76,8 @@ struct FlatpakOciRegistry + + /* Remote repos */ + FlatpakHttpSession *http_session; +- GUri *base_uri; ++ GUri *base_uri; ++ FlatpakCertificates *certificates; + }; + + typedef struct +@@ -353,31 +354,29 @@ choose_alt_uri (GUri *base_uri, + } + + static GBytes * +-remote_load_file (FlatpakHttpSession *http_session, +- GUri *base, +- const char *subpath, +- const char **alt_uris, +- const char *token, +- char **out_content_type, +- GCancellable *cancellable, +- GError **error) ++remote_load_file (FlatpakOciRegistry *self, ++ const char *subpath, ++ const char **alt_uris, ++ char **out_content_type, ++ GCancellable *cancellable, ++ GError **error) + { + g_autoptr(GBytes) bytes = NULL; + g_autofree char *uri_s = NULL; + +- uri_s = choose_alt_uri (base, alt_uris); ++ uri_s = choose_alt_uri (self->base_uri, alt_uris); + if (uri_s == NULL) + { +- uri_s = parse_relative_uri (base, subpath, error); ++ uri_s = parse_relative_uri (self->base_uri, subpath, error); + if (uri_s == NULL) + return NULL; + } + +- bytes = flatpak_load_uri (http_session, +- uri_s, FLATPAK_HTTP_FLAGS_ACCEPT_OCI, +- token, +- NULL, NULL, out_content_type, +- cancellable, error); ++ bytes = flatpak_load_uri_full (self->http_session, ++ uri_s, self->certificates, FLATPAK_HTTP_FLAGS_ACCEPT_OCI, ++ NULL, self->token, ++ NULL, NULL, NULL, out_content_type, NULL, ++ cancellable, error); + if (bytes == NULL) + return NULL; + +@@ -395,7 +394,7 @@ flatpak_oci_registry_load_file (FlatpakOciRegistry *self, + if (self->dfd != -1) + return local_load_file (self->dfd, subpath, cancellable, error); + else +- return remote_load_file (self->http_session, self->base_uri, subpath, alt_uris, self->token, out_content_type, cancellable, error); ++ return remote_load_file (self, subpath, alt_uris, out_content_type, cancellable, error); + } + + static JsonNode * +@@ -548,6 +547,7 @@ flatpak_oci_registry_ensure_remote (FlatpakOciRegistry *self, + GError **error) + { + g_autoptr(GUri) baseuri = NULL; ++ g_autoptr(GError) local_error = NULL; + + if (for_write) + { +@@ -568,6 +568,13 @@ flatpak_oci_registry_ensure_remote (FlatpakOciRegistry *self, + self->is_docker = TRUE; + self->base_uri = g_steal_pointer (&baseuri); + ++ self->certificates = flatpak_get_certificates_for_uri (self->uri, &local_error); ++ if (local_error) ++ { ++ g_propagate_error (error, g_steal_pointer (&local_error)); ++ return FALSE; ++ } ++ + return TRUE; + } + +@@ -834,6 +841,7 @@ flatpak_oci_registry_download_blob (FlatpakOciRegistry *self, + return -1; + + if (!flatpak_download_http_uri (self->http_session, uri_s, ++ self->certificates, + FLATPAK_HTTP_FLAGS_ACCEPT_OCI, + out_stream, + self->token, +@@ -925,7 +933,8 @@ flatpak_oci_registry_mirror_blob (FlatpakOciRegistry *self, + + out_stream = g_unix_output_stream_new (tmpf.fd, FALSE); + +- if (!flatpak_download_http_uri (source_registry->http_session, uri_s, ++ if (!flatpak_download_http_uri (source_registry->http_session, ++ uri_s, source_registry->certificates, + FLATPAK_HTTP_FLAGS_ACCEPT_OCI, out_stream, + self->token, + progress_cb, user_data, +@@ -1055,6 +1064,7 @@ get_token_for_www_auth (FlatpakOciRegistry *self, + + body = flatpak_load_uri_full (self->http_session, + auth_uri_s, ++ self->certificates, + FLATPAK_HTTP_FLAGS_NOCHECK_STATUS, + auth, NULL, + NULL, NULL, +@@ -1146,7 +1156,7 @@ flatpak_oci_registry_get_token (FlatpakOciRegistry *self, + if (uri_s == NULL) + return NULL; + +- body = flatpak_load_uri_full (self->http_session, uri_s, ++ body = flatpak_load_uri_full (self->http_session, uri_s, self->certificates, + FLATPAK_HTTP_FLAGS_ACCEPT_OCI | FLATPAK_HTTP_FLAGS_HEAD | FLATPAK_HTTP_FLAGS_NOCHECK_STATUS, + NULL, NULL, + NULL, NULL, +@@ -2080,11 +2090,11 @@ flatpak_oci_registry_find_delta_manifest (FlatpakOciRegistry *registry, + g_autofree char *uri_s = parse_relative_uri (registry->base_uri, delta_manifest_url, NULL); + + if (uri_s != NULL) +- bytes = flatpak_load_uri (registry->http_session, +- uri_s, FLATPAK_HTTP_FLAGS_ACCEPT_OCI, +- registry->token, +- NULL, NULL, NULL, +- cancellable, NULL); ++ bytes = flatpak_load_uri_full (registry->http_session, ++ uri_s, registry->certificates, FLATPAK_HTTP_FLAGS_ACCEPT_OCI, ++ NULL, registry->token, ++ NULL, NULL, NULL, NULL, NULL, ++ cancellable, NULL); + if (bytes != NULL) + { + g_autoptr(FlatpakOciVersioned) versioned = +@@ -2760,6 +2770,7 @@ flatpak_oci_index_ensure_cached (FlatpakHttpSession *http_session, + g_autofree char *tag = NULL; + const char *oci_arch = NULL; + gboolean success = FALSE; ++ g_autoptr(FlatpakCertificates) certificates = NULL; + g_autoptr(GError) local_error = NULL; + GUri *tmp_uri; + +@@ -2844,8 +2855,16 @@ flatpak_oci_index_ensure_cached (FlatpakHttpSession *http_session, + + query_uri_s = g_uri_to_string_partial (query_uri, G_URI_HIDE_PASSWORD); + ++ certificates = flatpak_get_certificates_for_uri (query_uri_s, &local_error); ++ if (local_error) ++ { ++ g_propagate_error (error, g_steal_pointer (&local_error)); ++ return FALSE; ++ } ++ + success = flatpak_cache_http_uri (http_session, + query_uri_s, ++ certificates, + FLATPAK_HTTP_FLAGS_STORE_COMPRESSED, + AT_FDCWD, index_path, + NULL, NULL, +@@ -3082,6 +3101,7 @@ flatpak_oci_index_make_summary (GFile *index, + static gboolean + add_icon_image (FlatpakHttpSession *http_session, + const char *index_uri, ++ FlatpakCertificates *certificates, + int icons_dfd, + GHashTable *used_icons, + const char *subdir, +@@ -3132,7 +3152,7 @@ add_icon_image (FlatpakHttpSession *http_session, + if (icon_uri_s == NULL) + return FALSE; + +- if (!flatpak_cache_http_uri (http_session, icon_uri_s, ++ if (!flatpak_cache_http_uri (http_session, icon_uri_s, certificates, + 0 /* flags */, + icons_dfd, icon_path, + NULL, NULL, +@@ -3152,6 +3172,7 @@ add_icon_image (FlatpakHttpSession *http_session, + static void + add_image_to_appstream (FlatpakHttpSession *http_session, + const char *index_uri, ++ FlatpakCertificates *certificates, + FlatpakXml *appstream_root, + int icons_dfd, + GHashTable *used_icons, +@@ -3243,6 +3264,7 @@ add_image_to_appstream (FlatpakHttpSession *http_session, + { + if (!add_icon_image (http_session, + index_uri, ++ certificates, + icons_dfd, + used_icons, + icon_sizes[i].subdir, id, icon_data, +@@ -3338,6 +3360,8 @@ flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session, + g_autoptr(FlatpakXml) appstream_root = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GHashTable) used_icons = NULL; ++ g_autoptr(FlatpakCertificates) certificates = NULL; ++ g_autoptr(GError) local_error = NULL; + int i; + + const char *oci_arch = flatpak_arch_to_oci_arch (arch); +@@ -3351,6 +3375,14 @@ flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session, + + appstream_root = flatpak_appstream_xml_new (); + ++ certificates = flatpak_get_certificates_for_uri (index_uri, &local_error); ++ if (local_error) ++ { ++ g_print ("Failed to load certificates for %s: %s", ++ index_uri, local_error->message); ++ g_clear_error (&local_error); ++ } ++ + for (i = 0; response->results != NULL && response->results[i] != NULL; i++) + { + FlatpakOciIndexRepository *r = response->results[i]; +@@ -3361,7 +3393,7 @@ flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session, + FlatpakOciIndexImage *image = r->images[j]; + if (g_strcmp0 (image->architecture, oci_arch) == 0) + add_image_to_appstream (http_session, +- index_uri, ++ index_uri, certificates, + appstream_root, icons_dfd, used_icons, + r, image, + cancellable); +@@ -3377,7 +3409,7 @@ flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session, + FlatpakOciIndexImage *image = list->images[k]; + if (g_strcmp0 (image->architecture, oci_arch) == 0) + add_image_to_appstream (http_session, +- index_uri, ++ index_uri, certificates, + appstream_root, icons_dfd, used_icons, + r, image, + cancellable); +diff --git a/common/flatpak-utils-http-private.h b/common/flatpak-utils-http-private.h +index 2c89ba40..a930ee79 100644 +--- a/common/flatpak-utils-http-private.h ++++ b/common/flatpak-utils-http-private.h +@@ -39,6 +39,15 @@ void flatpak_http_session_free (FlatpakHttpSession* http_session); + + G_DEFINE_AUTOPTR_CLEANUP_FUNC(FlatpakHttpSession, flatpak_http_session_free) + ++typedef struct FlatpakCertificates FlatpakCertificates; ++ ++FlatpakCertificates* flatpak_get_certificates_for_uri (const char *uri, ++ GError **error); ++FlatpakCertificates * flatpak_certificates_copy (FlatpakCertificates *other); ++void flatpak_certificates_free (FlatpakCertificates *certificates); ++ ++G_DEFINE_AUTOPTR_CLEANUP_FUNC(FlatpakCertificates, flatpak_certificates_free) ++ + typedef enum { + FLATPAK_HTTP_FLAGS_NONE = 0, + FLATPAK_HTTP_FLAGS_ACCEPT_OCI = 1 << 0, +@@ -52,6 +61,7 @@ typedef void (*FlatpakLoadUriProgress) (guint64 downloaded_bytes, + + GBytes * flatpak_load_uri_full (FlatpakHttpSession *http_session, + const char *uri, ++ FlatpakCertificates *certificates, + FlatpakHTTPFlags flags, + const char *auth, + const char *token, +@@ -73,6 +83,7 @@ GBytes * flatpak_load_uri (FlatpakHttpSession *http_session, + GError **error); + gboolean flatpak_download_http_uri (FlatpakHttpSession *http_session, + const char *uri, ++ FlatpakCertificates *certificates, + FlatpakHTTPFlags flags, + GOutputStream *out, + const char *token, +@@ -82,6 +93,7 @@ gboolean flatpak_download_http_uri (FlatpakHttpSession *http_session, + GError **error); + gboolean flatpak_cache_http_uri (FlatpakHttpSession *http_session, + const char *uri, ++ FlatpakCertificates *certificates, + FlatpakHTTPFlags flags, + int dest_dfd, + const char *dest_subpath, +diff --git a/common/flatpak-utils-http.c b/common/flatpak-utils-http.c +index 27de9c7a..e8d5d724 100644 +--- a/common/flatpak-utils-http.c ++++ b/common/flatpak-utils-http.c +@@ -73,6 +73,17 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (SoupURI, soup_uri_free) + + G_DEFINE_QUARK (flatpak_http_error, flatpak_http_error) + ++/* Holds information about CA and client certificates found in ++ * system-wide and per-user certificate directories as documented ++ * in container-certs.d(5). ++ */ ++struct FlatpakCertificates ++{ ++ char *ca_cert_file; ++ char *client_cert_file; ++ char *client_key_file; ++}; ++ + /* Information about the cache status of a file. + Encoded in an xattr on the cached file, or a file on the side if xattrs don't work. + */ +@@ -95,6 +106,7 @@ typedef struct + FlatpakHTTPFlags flags; + const char *auth; + const char *token; ++ FlatpakCertificates *certificates; + FlatpakLoadUriProgress progress; + GCancellable *cancellable; + gpointer user_data; +@@ -231,6 +243,157 @@ check_http_status (guint status_code, + return FALSE; + } + ++FlatpakCertificates* ++flatpak_get_certificates_for_uri (const char *uri, ++ GError **error) ++{ ++ g_autoptr(FlatpakCertificates) certificates = NULL; ++ g_autoptr(GUri) parsed_uri = NULL; ++ g_autofree char *hostport = NULL; ++ const char *system_certs_d = NULL; ++ g_autofree char *certs_path_str = NULL; ++ g_auto(GStrv) certs_path = NULL; ++ ++ certificates = g_new0 (FlatpakCertificates, 1); ++ ++ parsed_uri = g_uri_parse (uri, G_URI_FLAGS_PARSE_RELAXED, error); ++ if (!parsed_uri) ++ return NULL; ++ ++ if (!g_uri_get_host (parsed_uri)) ++ return NULL; ++ ++ if (g_uri_get_port (parsed_uri) != -1) ++ hostport = g_strdup_printf ("%s:%d", g_uri_get_host (parsed_uri), g_uri_get_port (parsed_uri)); ++ else ++ hostport = g_strdup (g_uri_get_host (parsed_uri)); ++ ++ system_certs_d = g_getenv ("FLATPAK_SYSTEM_CERTS_D"); ++ if (system_certs_d == NULL || system_certs_d[0] == '\0') ++ system_certs_d = "/etc/containers/certs.d:/etc/docker/certs.d"; ++ ++ /* containers/image hardcodes ~/.config and doesn't honor XDG_CONFIG_HOME */ ++ certs_path_str = g_strconcat (g_get_user_config_dir(), "/containers/certs.d:", ++ system_certs_d, NULL); ++ certs_path = g_strsplit (certs_path_str, ":", -1); ++ ++ for (int i = 0; certs_path[i]; i++) ++ { ++ g_autoptr(GFile) certs_dir = g_file_new_for_path (certs_path[i]); ++ g_autoptr(GFile) host_dir = g_file_get_child (certs_dir, hostport); ++ g_autoptr(GFileEnumerator) enumerator; ++ g_autoptr(GError) local_error = NULL; ++ ++ enumerator = g_file_enumerate_children (host_dir, G_FILE_ATTRIBUTE_STANDARD_NAME, ++ G_FILE_QUERY_INFO_NONE, ++ NULL, &local_error); ++ if (enumerator == NULL) ++ { ++ /* This matches libpod - missing certificate directory or a permission ++ * error causes the directory to be skipped; any other error is fatal ++ */ ++ if (g_error_matches(local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) || ++ g_error_matches(local_error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) ++ { ++ g_clear_error (&local_error); ++ continue; ++ } ++ else ++ { ++ g_propagate_error (error, g_steal_pointer (&local_error)); ++ return NULL; ++ } ++ } ++ ++ while (TRUE) ++ { ++ GFile *child; ++ g_autofree char *basename = NULL; ++ ++ if (!g_file_enumerator_iterate (enumerator, NULL, &child, NULL, error)) ++ return NULL; ++ ++ if (child == NULL) ++ break; ++ ++ basename = g_file_get_basename (child); ++ ++ /* In libpod, all CA certificates are added to the CA certificate ++ * database. We just use the first in readdir order. ++ */ ++ if (g_str_has_suffix (basename, ".crt") && certificates->ca_cert_file == NULL) ++ certificates->ca_cert_file = g_file_get_path (child); ++ ++ if (g_str_has_suffix (basename, ".cert")) ++ { ++ g_autofree char *nosuffix = g_strndup (basename, strlen (basename) - 5); ++ g_autofree char *key_basename = g_strconcat (nosuffix, ".key", NULL); ++ g_autoptr(GFile) key_file = g_file_get_child (host_dir, key_basename); ++ ++ if (!g_file_query_exists (key_file, NULL)) ++ { ++ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, ++ "missing key %s for client cert %s. " ++ "Note that CA certificates should use the extension .crt", ++ g_file_peek_path (key_file), ++ g_file_peek_path (child)); ++ return NULL; ++ } ++ ++ /* In libpod, all client certificates are added, and then the go TLS ++ * code selects the best based on TLS negotation. We just pick the first ++ * in readdir order ++ * */ ++ if (certificates->client_cert_file == NULL) ++ { ++ certificates->client_cert_file = g_file_get_path (child); ++ certificates->client_key_file = g_file_get_path (key_file); ++ } ++ } ++ ++ if (g_str_has_suffix (basename, ".key")) ++ { ++ g_autofree char *nosuffix = g_strndup (basename, strlen (basename) - 4); ++ g_autofree char *cert_basename = g_strconcat (nosuffix, ".cert", NULL); ++ g_autoptr(GFile) cert_file = g_file_get_child (host_dir, cert_basename); ++ ++ if (!g_file_query_exists (cert_file, NULL)) ++ { ++ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, ++ "missing client certificate %s for key %s", ++ g_file_peek_path (cert_file), ++ g_file_peek_path (child)); ++ return NULL; ++ } ++ } ++ } ++ } ++ ++ return g_steal_pointer (&certificates); ++} ++ ++FlatpakCertificates * ++flatpak_certificates_copy (FlatpakCertificates *other) ++{ ++ FlatpakCertificates *certificates = g_new0 (FlatpakCertificates, 1); ++ ++ certificates->ca_cert_file = g_strdup (other->ca_cert_file); ++ certificates->client_cert_file = g_strdup (other->client_cert_file); ++ certificates->client_key_file = g_strdup (other->client_key_file); ++ ++ return certificates; ++} ++ ++void ++flatpak_certificates_free (FlatpakCertificates *certificates) ++{ ++ g_clear_pointer (&certificates->ca_cert_file, g_free); ++ g_clear_pointer (&certificates->client_cert_file, g_free); ++ g_clear_pointer (&certificates->client_key_file, g_free); ++ ++ g_free (certificates); ++} ++ + #if defined(HAVE_CURL) + + /************************************************************************ +@@ -477,6 +640,18 @@ flatpak_download_http_uri_once (FlatpakHttpSession *session, + curl_easy_setopt (curl, CURLOPT_WRITEDATA, (void *)data); + curl_easy_setopt (curl, CURLOPT_HEADERDATA, (void *)data); + ++ if (data->certificates) ++ { ++ if (data->certificates->ca_cert_file) ++ curl_easy_setopt (curl, CURLOPT_CAINFO, data->certificates->ca_cert_file); ++ ++ if (data->certificates->client_cert_file) ++ { ++ curl_easy_setopt (curl, CURLOPT_SSLCERT, data->certificates->client_cert_file); ++ curl_easy_setopt (curl, CURLOPT_SSLKEY, data->certificates->client_key_file); ++ } ++ } ++ + if (data->flags & FLATPAK_HTTP_FLAGS_HEAD) + curl_easy_setopt (curl, CURLOPT_NOBODY, 1L); + else +@@ -576,6 +751,73 @@ flatpak_download_http_uri_once (FlatpakHttpSession *session, + * Soup implementation * + ***********************************************************************/ + ++/* ++ * The implementation of /etc/containers/certs.d for Soup is made tricky ++ * because the CA certificate database in Soup is global to the session, ++ * but we share a single sesssion between different hosts that might ++ * need different custom CA certs based on what's configured in certs.d. ++ * ++ * So what we do is make the FlatpakSoupSession multiplex multiple ++ * SoupSessions, depending on the certificates we use. The most common ++ * case, is of course, a single session with no custom certificates. ++ */ ++ ++typedef struct ++{ ++ char *user_agent; ++ GHashTable *soup_sessions; ++} FlatpakSoupSession; ++ ++static guint ++certificates_hash(const FlatpakCertificates *certificates) ++{ ++ guint hash = 0; ++ ++ if (certificates && certificates->ca_cert_file) ++ hash |= 13 * g_str_hash (certificates->ca_cert_file); ++ if (certificates && certificates->client_cert_file) ++ hash |= 17 * g_str_hash (certificates->client_cert_file); ++ if (certificates && certificates->client_key_file) ++ hash |= 23 * g_str_hash (certificates->client_key_file); ++ ++ return hash; ++} ++ ++static gboolean ++certificates_equal (const FlatpakCertificates *a, ++ const FlatpakCertificates *b) ++{ ++ if (a && b) ++ { ++ return (g_strcmp0(a->ca_cert_file, b->ca_cert_file) == 0 && ++ g_strcmp0(a->client_cert_file, b->client_cert_file) == 0 && ++ g_strcmp0(a->client_key_file, b->client_key_file) == 0); ++ } ++ else ++ return a == b; ++} ++ ++static void ++certificates_free (FlatpakCertificates *certificates) ++{ ++ if (certificates) ++ flatpak_certificates_free (certificates); ++} ++ ++static FlatpakSoupSession * ++flatpak_create_soup_session (const char *user_agent) ++{ ++ FlatpakSoupSession *session = g_new0 (FlatpakSoupSession, 1); ++ ++ session->user_agent = g_strdup (user_agent); ++ session->soup_sessions = g_hash_table_new_full ((GHashFunc)certificates_hash, ++ (GEqualFunc)certificates_equal, ++ (GDestroyNotify)certificates_free, ++ (GDestroyNotify)g_object_unref); ++ ++ return session; ++} ++ + static gboolean + check_soup_transfer_error (SoupMessage *msg, GError **error) + { +@@ -768,18 +1010,114 @@ load_uri_callback (GObject *source_object, + load_uri_read_cb, data); + } + ++/* Inline class for providing a pre-configured client certificate; from ++ * libsoup/examples/get.c. By Colin Walters. ++ */ ++struct _FlatpakTlsInteraction ++{ ++ GTlsInteraction parent_instance; ++ ++ GTlsCertificate *cert; ++}; ++ ++struct _FlatpakTlsInteractionClass ++{ ++ GTlsInteractionClass parent_class; ++}; ++ ++G_DECLARE_FINAL_TYPE (FlatpakTlsInteraction, ++ flatpak_tls_interaction, ++ FLATPAK, TLS_INTERACTION, ++ GTlsInteraction) ++ ++G_DEFINE_TYPE (FlatpakTlsInteraction, ++ flatpak_tls_interaction, ++ G_TYPE_TLS_INTERACTION) ++ ++static GTlsInteractionResult ++flatpak_tls_interaction_request_certificate (GTlsInteraction *interaction, ++ GTlsConnection *connection, ++ GTlsCertificateRequestFlags flags, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ FlatpakTlsInteraction *self = FLATPAK_TLS_INTERACTION (interaction); ++ ++ g_tls_connection_set_certificate (connection, self->cert); ++ ++ return G_TLS_INTERACTION_HANDLED; ++} ++ ++static void ++flatpak_tls_interaction_finalize (GObject *object) ++{ ++ FlatpakTlsInteraction *self = FLATPAK_TLS_INTERACTION (object); ++ ++ g_clear_object (&self->cert); ++ ++ G_OBJECT_CLASS (flatpak_tls_interaction_parent_class)->finalize (object); ++} ++ ++static void ++flatpak_tls_interaction_init (FlatpakTlsInteraction *interaction) ++{ ++} ++ ++static void ++flatpak_tls_interaction_class_init (FlatpakTlsInteractionClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ GTlsInteractionClass *interaction_class = G_TLS_INTERACTION_CLASS (klass); ++ ++ object_class->finalize = flatpak_tls_interaction_finalize; ++ ++ interaction_class->request_certificate = flatpak_tls_interaction_request_certificate; ++} ++ ++static FlatpakTlsInteraction * ++flatpak_tls_interaction_new (GTlsCertificate *cert) ++{ ++ FlatpakTlsInteraction *self = g_object_new (flatpak_tls_interaction_get_type (), NULL); ++ ++ self->cert = g_object_ref (cert); ++ ++ return self; ++} ++ + static SoupSession * +-flatpak_create_soup_session (const char *user_agent) ++get_soup_session (FlatpakSoupSession *session, FlatpakCertificates *certificates, GError **error) + { + SoupSession *soup_session; + const char *http_proxy; + +- soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, user_agent, +- SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE, ++ soup_session = g_hash_table_lookup (session->soup_sessions, certificates); ++ if (soup_session) ++ return soup_session; ++ ++ soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, session->user_agent, + SOUP_SESSION_USE_THREAD_CONTEXT, TRUE, + SOUP_SESSION_TIMEOUT, FLATPAK_HTTP_TIMEOUT_SECS, + SOUP_SESSION_IDLE_TIMEOUT, FLATPAK_HTTP_TIMEOUT_SECS, + NULL); ++ if (certificates && certificates->ca_cert_file) ++ g_object_set (soup_session, SOUP_SESSION_SSL_CA_FILE, certificates->ca_cert_file, NULL); ++ else ++ g_object_set (soup_session, SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE, NULL); ++ ++ if (certificates && certificates->client_cert_file) ++ { ++ g_autoptr(GTlsCertificate) client_cert = NULL; ++ g_autoptr(GTlsInteraction) interaction = NULL; ++ ++ client_cert = g_tls_certificate_new_from_files (certificates->client_cert_file, ++ certificates->client_key_file, error); ++ if (!client_cert) ++ return NULL; ++ ++ interaction = G_TLS_INTERACTION (flatpak_tls_interaction_new (client_cert)); ++ g_object_set (soup_session, SOUP_SESSION_TLS_INTERACTION, interaction, NULL); ++ } ++ + http_proxy = g_getenv ("http_proxy"); + if (http_proxy) + { +@@ -793,6 +1131,10 @@ flatpak_create_soup_session (const char *user_agent) + if (g_getenv ("OSTREE_DEBUG_HTTP")) + soup_session_add_feature (soup_session, (SoupSessionFeature *) soup_logger_new (SOUP_LOGGER_LOG_BODY, 500)); + ++ g_hash_table_replace (session->soup_sessions, ++ certificates ? flatpak_certificates_copy (certificates) : NULL, ++ soup_session); ++ + return soup_session; + } + +@@ -805,9 +1147,11 @@ flatpak_create_http_session (const char *user_agent) + void + flatpak_http_session_free (FlatpakHttpSession* http_session) + { +- SoupSession *soup_session = (SoupSession *)http_session; ++ FlatpakSoupSession *session = (FlatpakSoupSession *)http_session; + +- g_object_unref (soup_session); ++ g_hash_table_destroy (session->soup_sessions); ++ g_free (session->user_agent); ++ g_free (session); + } + + static gboolean +@@ -816,12 +1160,16 @@ flatpak_download_http_uri_once (FlatpakHttpSession *http_session, + const char *uri, + GError **error) + { +- SoupSession *soup_session = (SoupSession *)http_session; ++ SoupSession *soup_session; + g_autoptr(SoupRequestHTTP) request = NULL; + SoupMessage *m; + + g_info ("Loading %s using libsoup", uri); + ++ soup_session = get_soup_session ((FlatpakSoupSession *)http_session, data->certificates, error); ++ if (!soup_session) ++ return FALSE; ++ + request = soup_session_request_http (soup_session, + (data->flags & FLATPAK_HTTP_FLAGS_HEAD) != 0 ? "HEAD" : "GET", + uri, error); +@@ -927,6 +1275,7 @@ flatpak_http_should_retry_request (const GError *error, + GBytes * + flatpak_load_uri_full (FlatpakHttpSession *http_session, + const char *uri, ++ FlatpakCertificates *certificates, + FlatpakHTTPFlags flags, + const char *auth, + const char *token, +@@ -965,6 +1314,7 @@ flatpak_load_uri_full (FlatpakHttpSession *http_session, + data.last_progress_time = g_get_monotonic_time (); + data.cancellable = cancellable; + data.flags = flags; ++ data.certificates = certificates; + data.auth = auth; + data.token = token; + +@@ -1018,7 +1368,7 @@ flatpak_load_uri (FlatpakHttpSession *http_session, + GCancellable *cancellable, + GError **error) + { +- return flatpak_load_uri_full (http_session, uri, flags, NULL, token, ++ return flatpak_load_uri_full (http_session, uri, NULL, flags, NULL, token, + progress, user_data, NULL, out_content_type, NULL, + cancellable, error); + } +@@ -1026,6 +1376,7 @@ flatpak_load_uri (FlatpakHttpSession *http_session, + gboolean + flatpak_download_http_uri (FlatpakHttpSession *http_session, + const char *uri, ++ FlatpakCertificates *certificates, + FlatpakHTTPFlags flags, + GOutputStream *out, + const char *token, +@@ -1047,6 +1398,7 @@ flatpak_download_http_uri (FlatpakHttpSession *http_session, + data.user_data = user_data; + data.last_progress_time = g_get_monotonic_time (); + data.cancellable = cancellable; ++ data.certificates = certificates; + data.flags = flags; + data.token = token; + +@@ -1384,6 +1736,7 @@ set_cache_http_data_from_headers (CacheHttpData *cache_data, + gboolean + flatpak_cache_http_uri (FlatpakHttpSession *http_session, + const char *uri, ++ FlatpakCertificates *certificates, + FlatpakHTTPFlags flags, + int dest_dfd, + const char *dest_subpath, +@@ -1444,6 +1797,7 @@ flatpak_cache_http_uri (FlatpakHttpSession *http_session, + data.last_progress_time = g_get_monotonic_time (); + data.cancellable = cancellable; + data.flags = flags; ++ data.certificates = certificates; + + data.cache_data = cache_data; + +diff --git a/doc/flatpak-remote.xml b/doc/flatpak-remote.xml +index 798f5c39..47a5a42a 100644 +--- a/doc/flatpak-remote.xml ++++ b/doc/flatpak-remote.xml +@@ -80,6 +80,12 @@ + is a Flatpak extension that indicates that the remote is not an ostree + repository, but is rather an URL to an index of OCI images that are stored + within a container image registry. ++ ++ ++ For OCI remotes, client and CA certificates are read from ++ /etc/containers/certs.d and ++ ~/.config/containers/certs.d as documented in ++ containers-certs.d5. + + + +diff --git a/tests/httpcache.c b/tests/httpcache.c +index a4550fb0..f6f9de64 100644 +--- a/tests/httpcache.c ++++ b/tests/httpcache.c +@@ -32,7 +32,7 @@ main (int argc, char *argv[]) + + + if (!flatpak_cache_http_uri (session, +- url, ++ url, NULL, + flags, + AT_FDCWD, dest, + NULL, NULL, NULL, &error)) +-- +2.47.1 + +From cf555f02fcc1cf410fdad7607ff83a6764864a14 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Tue, 17 Dec 2024 17:47:35 +0100 +Subject: [PATCH 09/09] tests: Add tests for https OCI remotes + +--- + tests/libtest.sh | 17 +++++-- + tests/test-matrix/meson.build | 6 ++- + tests/test-oci-registry.sh | 90 ++++++++++++++++++++++++++++++----- + tests/test-wrapper.sh | 6 +++ + tests/update-test-matrix | 2 +- + 5 files changed, 101 insertions(+), 20 deletions(-) + +diff --git a/tests/libtest.sh b/tests/libtest.sh +index d63810e1..7dad594f 100644 +--- a/tests/libtest.sh ++++ b/tests/libtest.sh +@@ -332,12 +332,16 @@ make_runtime () { + } + + httpd () { +- COMMAND=${1:-web-server.py} +- DIR=${2:-repos} ++ if [ $# -eq 0 ] ; then ++ set web-server.py repos ++ fi ++ ++ COMMAND=$1 ++ shift + + rm -f httpd-pipe + mkfifo httpd-pipe +- PYTHONUNBUFFERED=1 $(dirname $0)/$COMMAND "$DIR" 3> httpd-pipe 2>&1 | tee -a httpd-log >&2 & ++ PYTHONUNBUFFERED=1 $(dirname $0)/$COMMAND "$@" 3> httpd-pipe 2>&1 | tee -a httpd-log >&2 & + read < httpd-pipe + } + +@@ -589,10 +593,15 @@ skip_without_libsystemd () { + fi + } + ++FLATPAK_SYSTEM_CERTS_D=$(pwd)/certs.d ++export FLATPAK_SYSTEM_CERTS_D ++ + sed s#@testdir@#${test_builddir}# ${test_srcdir}/session.conf.in > session.conf + dbus-daemon --fork --config-file=session.conf --print-address=3 --print-pid=4 \ + 3> dbus-session-bus-address 4> dbus-session-bus-pid +-export DBUS_SESSION_BUS_ADDRESS="$(cat dbus-session-bus-address)" ++ ++DBUS_SESSION_BUS_ADDRESS="$(cat dbus-session-bus-address)" ++export DBUS_SESSION_BUS_ADDRESS + DBUS_SESSION_BUS_PID="$(cat dbus-session-bus-pid)" + + if ! /bin/kill -0 "$DBUS_SESSION_BUS_PID"; then +diff --git a/tests/test-matrix/meson.build b/tests/test-matrix/meson.build +index 15176048..fd0b5034 100644 +--- a/tests/test-matrix/meson.build ++++ b/tests/test-matrix/meson.build +@@ -17,8 +17,10 @@ wrapped_tests += {'name' : 'test-sideload@system.wrap', 'script' : 'test-sideloa + wrapped_tests += {'name' : 'test-bundle@user.wrap', 'script' : 'test-bundle.sh'} + wrapped_tests += {'name' : 'test-bundle@system.wrap', 'script' : 'test-bundle.sh'} + wrapped_tests += {'name' : 'test-bundle@system-norevokefs.wrap', 'script' : 'test-bundle.sh'} +-wrapped_tests += {'name' : 'test-oci-registry@user.wrap', 'script' : 'test-oci-registry.sh'} +-wrapped_tests += {'name' : 'test-oci-registry@system.wrap', 'script' : 'test-oci-registry.sh'} ++wrapped_tests += {'name' : 'test-oci-registry@user,http.wrap', 'script' : 'test-oci-registry.sh'} ++wrapped_tests += {'name' : 'test-oci-registry@user,https.wrap', 'script' : 'test-oci-registry.sh'} ++wrapped_tests += {'name' : 'test-oci-registry@system,http.wrap', 'script' : 'test-oci-registry.sh'} ++wrapped_tests += {'name' : 'test-oci-registry@system,https.wrap', 'script' : 'test-oci-registry.sh'} + wrapped_tests += {'name' : 'test-update-remote-configuration@newsummary.wrap', 'script' : 'test-update-remote-configuration.sh'} + wrapped_tests += {'name' : 'test-update-remote-configuration@oldsummary.wrap', 'script' : 'test-update-remote-configuration.sh'} + wrapped_tests += {'name' : 'test-update-portal@user.wrap', 'script' : 'test-update-portal.sh'} +diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh +index 12036358..da234ded 100755 +--- a/tests/test-oci-registry.sh ++++ b/tests/test-oci-registry.sh +@@ -27,9 +27,73 @@ echo "1..14" + + # Start the fake registry server + +-httpd oci-registry-server.py --dir=. ++if [ x${USE_HTTPS} = xyes ] ; then ++ cat > openssl.config <&2 ++${FLATPAK} remote-add ${U} oci-registry "oci+${scheme}://127.0.0.1:${port}" >&2 + + # Check that the images we expect are listed + +@@ -144,7 +208,7 @@ fi + assert_has_file $base/oci/oci-registry.index.gz + assert_has_file $base/oci/oci-registry.summary + assert_has_dir $base/appstream/oci-registry +-${FLATPAK} remote-modify ${U} --url=http://127.0.0.1:${port} oci-registry >&2 ++${FLATPAK} remote-modify ${U} --url=${scheme}://127.0.0.1:${port} oci-registry >&2 + assert_not_has_file $base/oci/oci-registry.index.gz + assert_not_has_file $base/oci/oci-registry.summary + assert_not_has_dir $base/appstream/oci-registry +@@ -153,7 +217,7 @@ ok "change remote to non-OCI" + + # Change it back and refetch + +-${FLATPAK} remote-modify ${U} --url=oci+http://127.0.0.1:${port} oci-registry >&2 ++${FLATPAK} remote-modify ${U} --url=oci+${scheme}://127.0.0.1:${port} oci-registry >&2 + ${FLATPAK} update ${U} --appstream oci-registry >&2 + + # Delete the remote, check that everything was removed +@@ -177,7 +241,7 @@ ok "delete remote" + cat << EOF > runtime-repo.flatpakrepo + [Flatpak Repo] + Version=1 +-Url=oci+http://localhost:${port} ++Url=oci+${scheme}://localhost:${port} + Title=The OCI Title + EOF + +@@ -186,7 +250,7 @@ cat << EOF > org.test.Platform.flatpakref + Title=Test Platform + Name=org.test.Platform + Branch=master +-Url=oci+http://127.0.0.1:${port} ++Url=oci+${scheme}://127.0.0.1:${port} + IsRuntime=true + RuntimeRepo=file://$(pwd)/runtime-repo.flatpakrepo + EOF +@@ -214,12 +278,12 @@ ok "prune origin remote" + + # Install from a (non-OCI) bundle, check that the repo-url is respected + +-${FLATPAK} build-bundle --runtime --repo-url "oci+http://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Platform.flatpak org.test.Platform >&2 ++${FLATPAK} build-bundle --runtime --repo-url "oci+${scheme}://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Platform.flatpak org.test.Platform >&2 + + ${FLATPAK} ${U} install -y --bundle org.test.Platform.flatpak >&2 + + ${FLATPAK} remotes -d > remotes-list +-assert_file_has_content remotes-list "^platform-origin.*[ ]oci+http://127\.0\.0\.1:${port}" ++assert_file_has_content remotes-list "^platform-origin.*[ ]oci+${scheme}://127\.0\.0\.1:${port}" + + assert_has_file $base/oci/platform-origin.index.gz + +@@ -227,12 +291,12 @@ ok "install via bundle" + + # Install an app from a bundle + +-${FLATPAK} build-bundle --repo-url "oci+http://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Hello.flatpak org.test.Hello >&2 ++${FLATPAK} build-bundle --repo-url "oci+${scheme}://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Hello.flatpak org.test.Hello >&2 + + ${FLATPAK} ${U} install -y --bundle org.test.Hello.flatpak >&2 + + ${FLATPAK} remotes -d > remotes-list +-assert_file_has_content remotes-list "^hello-origin.*[ ]oci+http://127\.0\.0\.1:${port}" ++assert_file_has_content remotes-list "^hello-origin.*[ ]oci+${scheme}://127\.0\.0\.1:${port}" + + assert_has_file $base/oci/hello-origin.index.gz + +@@ -241,12 +305,12 @@ ok "app install via bundle" + # Install an updated app bundle with a different origin + + make_updated_app oci +-${FLATPAK} build-bundle --repo-url "http://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Hello.flatpak org.test.Hello >&2 ++${FLATPAK} build-bundle --repo-url "${scheme}://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Hello.flatpak org.test.Hello >&2 + + ${FLATPAK} ${U} install -y --bundle org.test.Hello.flatpak >&2 + + ${FLATPAK} remotes -d > remotes-list +-assert_file_has_content remotes-list "^hello-origin.*[ ]http://127\.0\.0\.1:${port}" ++assert_file_has_content remotes-list "^hello-origin.*[ ]${scheme}://127\.0\.0\.1:${port}" + + assert_not_has_file $base/oci/hello-origin.index.gz + +diff --git a/tests/test-wrapper.sh b/tests/test-wrapper.sh +index be624256..2dacc1bc 100755 +--- a/tests/test-wrapper.sh ++++ b/tests/test-wrapper.sh +@@ -30,6 +30,12 @@ for feature in $(echo $1 | sed "s/^.*@\(.*\).wrap/\1/" | tr "," "\n"); do + annotations) + export USE_OCI_ANNOTATIONS=yes + ;; ++ https) ++ export USE_HTTPS=yes ++ ;; ++ http) ++ export USE_HTTPS=no ++ ;; + *) + echo unsupported test feature $feature + exit 1 +diff --git a/tests/update-test-matrix b/tests/update-test-matrix +index 2aff6f00..3a51d0ba 100755 +--- a/tests/update-test-matrix ++++ b/tests/update-test-matrix +@@ -23,7 +23,7 @@ TEST_MATRIX_SOURCE=( + 'tests/test-extensions.sh' \ + 'tests/test-bundle.sh{user+system+system-norevokefs}' \ + 'tests/test-oci.sh' \ +- 'tests/test-oci-registry.sh{user+system}' \ ++ 'tests/test-oci-registry.sh{{user+system},{http+https}}' \ + 'tests/test-update-remote-configuration.sh{newsummary+oldsummary}' \ + 'tests/test-override.sh' \ + 'tests/test-update-portal.sh{user+system}' \ +-- +2.47.1 + diff --git a/flatpak-pass-token-to-flatpak-image-source-new-remote.patch b/flatpak-pass-token-to-flatpak-image-source-new-remote.patch new file mode 100644 index 0000000000000000000000000000000000000000..2eb9686ee21e6d1577ef200408ab085618424b1b --- /dev/null +++ b/flatpak-pass-token-to-flatpak-image-source-new-remote.patch @@ -0,0 +1,104 @@ +commit f0bc60dc0b34669e64d48e723a5e84c0b90b281d +Author: Owen W. Taylor +Date: Wed Feb 5 12:29:43 2025 -0500 + + Pass token to flatpak_image_source_new_remote() + + Since flatpak_image_source_new_remote() already tries to load files + from the registry, having a separate flatpak_image_source_set_token() + doesn't work - when the token is set, it's already too late to + be passed along with the initial requests. + +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index 26ec176f..3621dd3b 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -1225,12 +1225,10 @@ flatpak_remote_state_new_image_source (FlatpakRemoteState *self, + if (registry_uri == NULL) + return NULL; + +- image_source = flatpak_image_source_new_remote (registry_uri, oci_repository, digest, NULL, error); ++ image_source = flatpak_image_source_new_remote (registry_uri, oci_repository, digest, token, NULL, error); + if (image_source == NULL) + return NULL; + +- flatpak_image_source_set_token (image_source, token); +- + return g_steal_pointer (&image_source); + } + +@@ -6473,6 +6471,9 @@ flatpak_dir_mirror_oci (FlatpakDir *self, + else + image_source = flatpak_remote_state_fetch_image_source (state, self, ref, opt_rev, token, cancellable, error); + ++ if (!image_source) ++ return FALSE; ++ + flatpak_progress_start_oci_pull (progress); + + g_info ("Mirroring OCI image %s", flatpak_image_source_get_digest (image_source)); +@@ -6514,6 +6515,9 @@ flatpak_dir_pull_oci (FlatpakDir *self, + else + image_source = flatpak_remote_state_fetch_image_source (state, self, ref, opt_rev, token, cancellable, error); + ++ if (!image_source) ++ return FALSE; ++ + oci_digest = flatpak_image_source_get_digest (image_source); + + /* Short circuit if we've already got this commit */ +diff --git a/common/flatpak-image-source-private.h b/common/flatpak-image-source-private.h +index 597a8174..5f9604d8 100644 +--- a/common/flatpak-image-source-private.h ++++ b/common/flatpak-image-source-private.h +@@ -45,14 +45,13 @@ FlatpakImageSource *flatpak_image_source_new_local (GFile *file, + FlatpakImageSource *flatpak_image_source_new_remote (const char *uri, + const char *oci_repository, + const char *digest, ++ const char *token, + GCancellable *cancellable, + GError **error); + FlatpakImageSource *flatpak_image_source_new_for_location (const char *location, + GCancellable *cancellable, + GError **error); + +-void flatpak_image_source_set_token (FlatpakImageSource *self, +- const char *token); + void flatpak_image_source_set_delta_url (FlatpakImageSource *self, + const char *delta_url); + +diff --git a/common/flatpak-image-source.c b/common/flatpak-image-source.c +index a31f1084..1fc0eeb0 100644 +--- a/common/flatpak-image-source.c ++++ b/common/flatpak-image-source.c +@@ -180,6 +180,7 @@ FlatpakImageSource * + flatpak_image_source_new_remote (const char *uri, + const char *oci_repository, + const char *digest, ++ const char *token, + GCancellable *cancellable, + GError **error) + { +@@ -189,6 +190,8 @@ flatpak_image_source_new_remote (const char *uri, + if (!registry) + return NULL; + ++ flatpak_oci_registry_set_token (registry, token); ++ + return flatpak_image_source_new (registry, oci_repository, digest, cancellable, error); + } + +@@ -327,13 +330,6 @@ flatpak_image_source_new_for_location (const char *location, + } + } + +-void +-flatpak_image_source_set_token (FlatpakImageSource *self, +- const char *token) +-{ +- flatpak_oci_registry_set_token (self->registry, token); +-} +- + void + flatpak_image_source_set_delta_url (FlatpakImageSource *self, + const char *delta_url) diff --git a/flatpak-support-sideload-repositories-for-oci-remotes.patch b/flatpak-support-sideload-repositories-for-oci-remotes.patch new file mode 100644 index 0000000000000000000000000000000000000000..114007fd3efccf46e9fc2824fcf155a816a7efa8 --- /dev/null +++ b/flatpak-support-sideload-repositories-for-oci-remotes.patch @@ -0,0 +1,1676 @@ +From 74c8d6ee0bf170fbd4accda2d422afde14712408 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Tue, 29 Oct 2024 17:16:08 -0400 +Subject: [PATCH 01/02] Don't return a VarRefInfoRef from + flatpak_remote_state_lookup_ref() + +The memory management of returning a VarRefInfoRef is tricky - it +points to data owned by the summary or the sideload repo. External +consumers were always retrieving a copy of the summary metadata, +so make the public function do that. +--- + common/flatpak-dir-private.h | 14 ++++----- + common/flatpak-dir.c | 61 +++++++++++++++++++++++++++++------- + common/flatpak-transaction.c | 14 +++------ + 3 files changed, 61 insertions(+), 28 deletions(-) + +diff --git a/common/flatpak-dir-private.h b/common/flatpak-dir-private.h +index 539476b8..3456660b 100644 +--- a/common/flatpak-dir-private.h ++++ b/common/flatpak-dir-private.h +@@ -126,13 +126,13 @@ gboolean flatpak_remote_state_ensure_subsummary_all_arches (FlatpakRemoteState * + GError **error); + gboolean flatpak_remote_state_allow_ref (FlatpakRemoteState *self, + const char *ref); +-gboolean flatpak_remote_state_lookup_ref (FlatpakRemoteState *self, +- const char *ref, +- char **out_checksum, +- guint64 *out_timestamp, +- VarRefInfoRef *out_info, +- GFile **out_sideload_path, +- GError **error); ++gboolean flatpak_remote_state_lookup_ref (FlatpakRemoteState *self, ++ const char *ref, ++ char **out_checksum, ++ guint64 *out_timestamp, ++ GVariant **out_summary_metadata, ++ GFile **out_sideload_path, ++ GError **error); + GPtrArray *flatpak_remote_state_match_subrefs (FlatpakRemoteState *self, + FlatpakDecomposed *ref); + GFile *flatpak_remote_state_lookup_sideload_checksum (FlatpakRemoteState *self, +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index 9f398252..99889aa5 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -755,14 +755,14 @@ get_summary_for_ref (FlatpakRemoteState *self, + /* Returns TRUE if the ref is found in the summary or cache. + * out_checksum and out_variant are only set when the ref is found. + */ +-gboolean +-flatpak_remote_state_lookup_ref (FlatpakRemoteState *self, +- const char *ref, +- char **out_checksum, +- guint64 *out_timestamp, +- VarRefInfoRef *out_info, +- GFile **out_sideload_path, +- GError **error) ++static gboolean ++flatpak_remote_state_lookup_ref_internal (FlatpakRemoteState *self, ++ const char *ref, ++ char **out_checksum, ++ guint64 *out_timestamp, ++ VarRefInfoRef *out_info, ++ GFile **out_sideload_path, ++ GError **error) + { + if (!flatpak_remote_state_allow_ref (self, ref)) + { +@@ -822,12 +822,50 @@ flatpak_remote_state_lookup_ref (FlatpakRemoteState *self, + return FALSE; + + if (out_sideload_path) +- *out_sideload_path = g_object_ref (ostree_repo_get_path (ss->repo)); ++ { ++ if (ss) ++ *out_sideload_path = g_object_ref (ostree_repo_get_path (ss->repo)); ++ else ++ *out_sideload_path = NULL; ++ } + } + + return TRUE; + } + ++/* Returns TRUE if the ref is found in the summary or cache. ++ * out parameters are only set if the ref is found. ++ */ ++gboolean ++flatpak_remote_state_lookup_ref (FlatpakRemoteState *self, ++ const char *ref, ++ char **out_checksum, ++ guint64 *out_timestamp, ++ GVariant **out_summary_metadata, ++ GFile **out_sideload_path, ++ GError **error) ++{ ++ ++ if (out_summary_metadata) ++ { ++ VarRefInfoRef info; ++ ++ if (flatpak_remote_state_lookup_ref_internal (self, ref, out_checksum, out_timestamp, ++ &info, out_sideload_path, error)) ++ { ++ *out_summary_metadata = var_metadata_dup_to_gvariant (var_ref_info_get_metadata (info)); ++ return TRUE; ++ } ++ else ++ return FALSE; ++ } ++ else ++ { ++ return flatpak_remote_state_lookup_ref_internal (self, ref, out_checksum, out_timestamp, ++ NULL, out_sideload_path, error); ++ } ++} ++ + GPtrArray * + flatpak_remote_state_match_subrefs (FlatpakRemoteState *self, + FlatpakDecomposed *ref) +@@ -995,7 +1033,6 @@ flatpak_remote_state_load_data (FlatpakRemoteState *self, + /* Look up from sideload */ + g_autofree char *checksum = NULL; + guint64 timestamp; +- VarRefInfoRef info; + FlatpakSideloadState *ss = NULL; + g_autoptr(GVariant) commit_data = NULL; + g_autoptr(GVariant) commit_metadata = NULL; +@@ -1006,7 +1043,7 @@ flatpak_remote_state_load_data (FlatpakRemoteState *self, + /* Use sideload refs if any */ + + if (!flatpak_remote_state_resolve_sideloaded_ref (self, ref, &checksum, ×tamp, +- &info, &ss, error)) ++ NULL, &ss, error)) + return FALSE; + + if (!ostree_repo_load_commit (ss->repo, checksum, &commit_data, NULL, error)) +@@ -1095,7 +1132,7 @@ flatpak_remote_state_fetch_image_source (FlatpakRemoteState *self, + const char *delta_url = NULL; + + /* We extract the rev info from the latest, even if we don't use the latest digest, assuming refs don't move */ +- if (!flatpak_remote_state_lookup_ref (self, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) ++ if (!flatpak_remote_state_lookup_ref_internal (self, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) + return NULL; + + if (latest_rev == NULL) +diff --git a/common/flatpak-transaction.c b/common/flatpak-transaction.c +index 1aba1daa..2dd35f53 100644 +--- a/common/flatpak-transaction.c ++++ b/common/flatpak-transaction.c +@@ -3482,7 +3482,6 @@ try_resolve_op_from_metadata (FlatpakTransaction *self, + guint64 installed_size = 0; + const char *metadata = NULL; + VarMetadataRef sparse_cache; +- VarRefInfoRef info; + g_autofree char *summary_checksum = NULL; + + /* Ref has to match the actual commit in the summary */ +@@ -3499,9 +3498,8 @@ try_resolve_op_from_metadata (FlatpakTransaction *self, + + metadata_bytes = g_bytes_new (metadata, strlen (metadata)); + +- if (flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref), +- NULL, NULL, &info, NULL, NULL)) +- op->summary_metadata = var_metadata_dup_to_gvariant (var_ref_info_get_metadata (info)); ++ flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref), ++ NULL, NULL, &op->summary_metadata, NULL, NULL); + + op->installed_size = installed_size; + op->download_size = download_size; +@@ -3704,13 +3702,11 @@ resolve_ops (FlatpakTransaction *self, + * Note, we don't have a token here, so this will not work for authenticated apps. + * We handle this by catching the 401 http status and retrying. */ + g_autoptr(GVariant) commit_data = NULL; +- VarRefInfoRef ref_info; + + /* OCI needs this to get the oci repository for the ref to request the token, so lets always set it here */ +- if (op->summary_metadata == NULL && +- flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref), +- NULL, NULL, &ref_info, NULL, NULL)) +- op->summary_metadata = var_metadata_dup_to_gvariant (var_ref_info_get_metadata (ref_info)); ++ if (op->summary_metadata == NULL) ++ flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref), ++ NULL, NULL, &op->summary_metadata, NULL, NULL); + + commit_data = flatpak_remote_state_load_ref_commit (state, priv->dir, + flatpak_decomposed_get_ref (op->ref), +-- +2.47.1 + +From 751944405e027daf968741a3fbeec188ccf07ab1 Mon Sep 17 00:00:00 2001 +From: "Owen W. Taylor" +Date: Tue, 29 Oct 2024 17:19:38 -0400 +Subject: [PATCH 02/02] Support sideload repositories for OCI remotes + +For OCI remotes, the existing sideload repository system doesn't +work: identity for OCI remotes is done by manifest digest (disguised +as a fake commit ID internally), instead of by ostree commit, so +we have no way of knowing whether a sideloaded image matches the +summary. + +Allow specifying a new form of sideload repository with: + + --sideload-repo=oci: + +The desired use case for this is preinstalling Flatpaks during OS +install, and for this, binding the entire repository to a single +collection ID is both inconvenient and not useful, so OCI sideload +repostories don't have a defined collection ID - they just apply to +all OCI remotes. (And, because of this, they are restricted to +the command line.) + +This is implemented in a straightforward way by adding, throughout +the FlatpakTransaction and FlatpakDir code, in parallel, +to GFile *sideload_path, FlatpakImageSource *image_source. + +The new FlatpakImageCollection type represents a set of +FlatpakImageSource loaded from the image sideload repository. +--- + app/flatpak-builtins-install.c | 17 +- + app/flatpak-builtins-remote-info.c | 2 +- + app/flatpak-builtins-update.c | 4 +- + app/flatpak-builtins-utils.c | 33 ++ + app/flatpak-builtins-utils.h | 6 + + common/flatpak-common-types-private.h | 13 +- + common/flatpak-dir-private.h | 12 +- + common/flatpak-dir.c | 362 +++++++++++++++++----- + common/flatpak-image-collection-private.h | 50 +++ + common/flatpak-image-collection.c | 154 +++++++++ + common/flatpak-image-source-private.h | 7 + + common/flatpak-image-source.c | 25 +- + common/flatpak-transaction.c | 74 ++++- + common/flatpak-transaction.h | 5 + + common/meson.build | 1 + + doc/flatpak-install.xml | 9 + + doc/flatpak-update.xml | 9 + + doc/reference/meson.build | 1 + + 18 files changed, 667 insertions(+), 117 deletions(-) + create mode 100644 common/flatpak-image-collection-private.h + create mode 100644 common/flatpak-image-collection.c + +diff --git a/app/flatpak-builtins-install.c b/app/flatpak-builtins-install.c +index e1f7f312..80a43152 100644 +--- a/app/flatpak-builtins-install.c ++++ b/app/flatpak-builtins-install.c +@@ -132,8 +132,9 @@ read_gpg_data (GCancellable *cancellable, + } + + static FlatpakTransaction * +-create_install_transaction (FlatpakDir *dir, +- GError **error) ++create_install_transaction (FlatpakDir *dir, ++ GCancellable *cancellable, ++ GError **error) + { + g_autoptr(FlatpakTransaction) transaction = NULL; + +@@ -155,8 +156,8 @@ create_install_transaction (FlatpakDir *dir, + flatpak_transaction_set_auto_install_sdk (transaction, opt_include_sdk); + flatpak_transaction_set_auto_install_debug (transaction, opt_include_debug); + +- for (int i = 0; opt_sideload_repos != NULL && opt_sideload_repos[i] != NULL; i++) +- flatpak_transaction_add_sideload_repo (transaction, opt_sideload_repos[i]); ++ if (!setup_sideload_repositories (transaction, opt_sideload_repos, cancellable, error)) ++ return FALSE; + + return g_steal_pointer (&transaction); + } +@@ -194,7 +195,7 @@ install_bundle (FlatpakDir *dir, + return FALSE; + } + +- transaction = create_install_transaction (dir, error); ++ transaction = create_install_transaction (dir, cancellable, error); + if (transaction == NULL) + return FALSE; + +@@ -257,7 +258,7 @@ install_from (FlatpakDir *dir, + file_data = g_bytes_new_take (g_steal_pointer (&data), data_len); + } + +- transaction = create_install_transaction (dir, error); ++ transaction = create_install_transaction (dir, cancellable, error); + if (transaction == NULL) + return FALSE; + +@@ -303,7 +304,7 @@ install_image (FlatpakDir *dir, + return FALSE; + } + +- transaction = create_install_transaction (dir, error); ++ transaction = create_install_transaction (dir, cancellable, error); + if (transaction == NULL) + return FALSE; + +@@ -537,7 +538,7 @@ flatpak_builtin_install (int argc, char **argv, GCancellable *cancellable, GErro + + default_branch = flatpak_dir_get_remote_default_branch (dir, remote); + +- transaction = create_install_transaction (dir, error); ++ transaction = create_install_transaction (dir, cancellable, error); + if (transaction == NULL) + return FALSE; + +diff --git a/app/flatpak-builtins-remote-info.c b/app/flatpak-builtins-remote-info.c +index 8dee2af7..89fd5c83 100644 +--- a/app/flatpak-builtins-remote-info.c ++++ b/app/flatpak-builtins-remote-info.c +@@ -151,7 +151,7 @@ flatpak_builtin_remote_info (int argc, char **argv, GCancellable *cancellable, G + if (opt_commit) + commit = g_strdup (opt_commit); + else if (!flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (ref), +- &commit, NULL, NULL, NULL, error)) ++ &commit, NULL, NULL, NULL, NULL, error)) + { + g_assert (error == NULL || *error != NULL); + return FALSE; +diff --git a/app/flatpak-builtins-update.c b/app/flatpak-builtins-update.c +index b5184a33..5465dc8c 100644 +--- a/app/flatpak-builtins-update.c ++++ b/app/flatpak-builtins-update.c +@@ -150,8 +150,8 @@ flatpak_builtin_update (int argc, + if (opt_arch) + flatpak_transaction_set_default_arch (transaction, opt_arch); + +- for (i = 0; opt_sideload_repos != NULL && opt_sideload_repos[i] != NULL; i++) +- flatpak_transaction_add_sideload_repo (transaction, opt_sideload_repos[i]); ++ if (!setup_sideload_repositories (transaction, opt_sideload_repos, cancellable, error)) ++ return FALSE; + + g_ptr_array_insert (transactions, 0, transaction); + } +diff --git a/app/flatpak-builtins-utils.c b/app/flatpak-builtins-utils.c +index e539a8c2..01c2cd05 100644 +--- a/app/flatpak-builtins-utils.c ++++ b/app/flatpak-builtins-utils.c +@@ -1450,3 +1450,36 @@ ensure_remote_state_all_arches (FlatpakDir *dir, + + return TRUE; + } ++ ++gboolean ++setup_sideload_repositories (FlatpakTransaction *transaction, ++ char **opt_sideload_repos, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ for (int i = 0; opt_sideload_repos != NULL && opt_sideload_repos[i] != NULL; i++) ++ { ++ const char *repo = opt_sideload_repos[i]; ++ if (g_str_has_prefix (repo, "oci:") || g_str_has_prefix (repo, "oci-archive:")) ++ { ++ if (!flatpak_transaction_add_sideload_image_collection (transaction, repo, cancellable, error)) ++ return FALSE; ++ } ++ else if (g_str_has_prefix (repo, "file:")) ++ { ++ g_autoptr(GFile) file = g_file_new_for_uri (repo); ++ const char *path = flatpak_file_get_path_cached (file); ++ flatpak_transaction_add_sideload_repo (transaction, path); ++ } ++ else ++ { ++ if (g_regex_match_simple ("^[A-Za-z][A-Za-z0-9+.-]*:", repo, ++ G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT)) ++ return flatpak_fail (error, _("Unknown scheme in sideload location %s"), repo); ++ ++ flatpak_transaction_add_sideload_repo (transaction, repo); ++ } ++ } ++ ++ return TRUE; ++} +diff --git a/app/flatpak-builtins-utils.h b/app/flatpak-builtins-utils.h +index 257a6b95..cd24abf7 100644 +--- a/app/flatpak-builtins-utils.h ++++ b/app/flatpak-builtins-utils.h +@@ -27,6 +27,7 @@ + #include "flatpak-utils-private.h" + #include "flatpak-dir-private.h" + #include "flatpak-permission-dbus-generated.h" ++#include "flatpak-transaction.h" + + /* AS_CHECK_VERSION was introduced in 0.14.0; we still support 0.12.0, so + * behave as though versions without this macro are arbitrarily old */ +@@ -209,4 +210,9 @@ gboolean ensure_remote_state_all_arches (FlatpakDir *dir, + GCancellable *cancellable, + GError **error); + ++gboolean setup_sideload_repositories (FlatpakTransaction *transaction, ++ char **opt_sideload_repos, ++ GCancellable *cancellable, ++ GError **error); ++ + #endif /* __FLATPAK_BUILTINS_UTILS_H__ */ +diff --git a/common/flatpak-common-types-private.h b/common/flatpak-common-types-private.h +index d7f3913b..6d41f12c 100644 +--- a/common/flatpak-common-types-private.h ++++ b/common/flatpak-common-types-private.h +@@ -51,11 +51,12 @@ typedef enum { + FLATPAK_RUN_FLAG_PARENT_SHARE_PIDS = (1 << 21), + } FlatpakRunFlags; + +-typedef struct FlatpakDir FlatpakDir; +-typedef struct FlatpakDeploy FlatpakDeploy; +-typedef struct _FlatpakImageSource FlatpakImageSource; +-typedef struct FlatpakOciRegistry FlatpakOciRegistry; +-typedef struct _FlatpakOciManifest FlatpakOciManifest; +-typedef struct _FlatpakOciImage FlatpakOciImage; ++typedef struct FlatpakDir FlatpakDir; ++typedef struct FlatpakDeploy FlatpakDeploy; ++typedef struct FlatpakImageCollection FlatpakImageCollection; ++typedef struct _FlatpakImageSource FlatpakImageSource; ++typedef struct FlatpakOciRegistry FlatpakOciRegistry; ++typedef struct _FlatpakOciManifest FlatpakOciManifest; ++typedef struct _FlatpakOciImage FlatpakOciImage; + + #endif /* __FLATPAK_COMMON_TYPES_H__ */ +diff --git a/common/flatpak-dir-private.h b/common/flatpak-dir-private.h +index 3456660b..ffcff5ff 100644 +--- a/common/flatpak-dir-private.h ++++ b/common/flatpak-dir-private.h +@@ -87,6 +87,7 @@ typedef struct + { + char *remote_name; + gboolean is_file_uri; ++ gboolean is_oci; + char *collection_id; + + /* New format summary */ +@@ -107,6 +108,7 @@ typedef struct + int refcount; + gint32 default_token_type; + GPtrArray *sideload_repos; ++ GPtrArray *sideload_image_collections; + } FlatpakRemoteState; + + FlatpakRemoteState *flatpak_remote_state_ref (FlatpakRemoteState *remote_state); +@@ -132,11 +134,14 @@ gboolean flatpak_remote_state_lookup_ref (FlatpakRemoteState *self, + guint64 *out_timestamp, + GVariant **out_summary_metadata, + GFile **out_sideload_path, ++ FlatpakImageSource **out_image_Source, + GError **error); + GPtrArray *flatpak_remote_state_match_subrefs (FlatpakRemoteState *self, + FlatpakDecomposed *ref); +-GFile *flatpak_remote_state_lookup_sideload_checksum (FlatpakRemoteState *self, +- char *checksum); ++void flatpak_remote_state_lookup_sideload_checksum (FlatpakRemoteState *self, ++ char *checksum, ++ GFile **out_sideload_path, ++ FlatpakImageSource **out_image_source); + gboolean flatpak_remote_state_lookup_cache (FlatpakRemoteState *self, + const char *ref, + guint64 *download_size, +@@ -163,6 +168,8 @@ GVariant *flatpak_remote_state_load_ref_commit (FlatpakRemoteState *self, + GError **error); + void flatpak_remote_state_add_sideload_dir (FlatpakRemoteState *self, + GFile *path); ++void flatpak_remote_state_add_sideload_image_collection (FlatpakRemoteState *self, ++ FlatpakImageCollection *image_collection); + + + G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakDir, g_object_unref) +@@ -982,6 +989,7 @@ gboolean flatpak_dir_find_latest_rev (Fla + char **out_rev, + guint64 *out_timestamp, + GFile **out_sideload_path, ++ FlatpakImageSource **out_image_source, + GCancellable *cancellable, + GError **error); + FlatpakDecomposed * flatpak_dir_get_remote_auto_install_authenticator_ref (FlatpakDir *self, +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index 99889aa5..1c304525 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -52,6 +52,7 @@ + #include "flatpak-dir-utils-private.h" + #include "flatpak-error.h" + #include "flatpak-locale-utils-private.h" ++#include "flatpak-image-collection-private.h" + #include "flatpak-image-source-private.h" + #include "flatpak-oci-registry-private.h" + #include "flatpak-ref.h" +@@ -375,6 +376,7 @@ flatpak_remote_state_new (void) + + state->refcount = 1; + state->sideload_repos = g_ptr_array_new_with_free_func ((GDestroyNotify)flatpak_sideload_state_free); ++ state->sideload_image_collections = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref); + state->subsummaries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)variant_maybe_unref); + return state; + } +@@ -408,6 +410,7 @@ flatpak_remote_state_unref (FlatpakRemoteState *remote_state) + g_clear_pointer (&remote_state->allow_refs, g_regex_unref); + g_clear_pointer (&remote_state->deny_refs, g_regex_unref); + g_clear_pointer (&remote_state->sideload_repos, g_ptr_array_unref); ++ g_clear_pointer (&remote_state->sideload_image_collections, g_ptr_array_unref); + + g_free (remote_state); + } +@@ -470,6 +473,13 @@ flatpak_remote_state_add_sideload_repo (FlatpakRemoteState *self, + } + } + ++void ++flatpak_remote_state_add_sideload_image_collection (FlatpakRemoteState *self, ++ FlatpakImageCollection *image_collection) ++{ ++ g_ptr_array_add (self->sideload_image_collections, g_object_ref (image_collection)); ++} ++ + static void add_sideload_subdirs (GPtrArray *res, + GFile *parent, + gboolean recurse); +@@ -656,31 +666,57 @@ get_timestamp_from_ref_info (VarRefInfoRef info) + } + + +-GFile * +-flatpak_remote_state_lookup_sideload_checksum (FlatpakRemoteState *self, +- char *checksum) ++void ++flatpak_remote_state_lookup_sideload_checksum (FlatpakRemoteState *self, ++ char *checksum, ++ GFile **out_sideload_path, ++ FlatpakImageSource **out_image_source) + { +- for (int i = 0; i < self->sideload_repos->len; i++) ++ if (out_sideload_path) ++ *out_sideload_path = NULL; ++ if (out_image_source) ++ *out_image_source = NULL; ++ ++ if (self->is_oci) + { +- FlatpakSideloadState *ss = g_ptr_array_index (self->sideload_repos, i); +- OstreeRepoCommitState commit_state; ++ if (out_image_source) ++ { ++ const char *digest = g_strconcat ("sha256:", checksum, NULL); + +- if (ostree_repo_load_commit (ss->repo, checksum, NULL, &commit_state, NULL) && +- commit_state == OSTREE_REPO_COMMIT_STATE_NORMAL) +- return g_object_ref (ostree_repo_get_path (ss->repo)); ++ for (int i = 0; i < self->sideload_image_collections->len; i++) ++ { ++ FlatpakImageCollection *collection = g_ptr_array_index (self->sideload_image_collections, i); ++ g_autoptr(FlatpakImageSource) image_source = flatpak_image_collection_lookup_digest (collection, digest); ++ if (image_source) ++ *out_image_source = g_steal_pointer (&image_source); ++ } ++ } + } ++ else if (out_sideload_path) ++ { ++ for (int i = 0; i < self->sideload_repos->len; i++) ++ { ++ FlatpakSideloadState *ss = g_ptr_array_index (self->sideload_repos, i); ++ OstreeRepoCommitState commit_state; + +- return NULL; ++ if (ostree_repo_load_commit (ss->repo, checksum, NULL, &commit_state, NULL) && ++ commit_state == OSTREE_REPO_COMMIT_STATE_NORMAL) ++ { ++ *out_sideload_path = g_object_ref (ostree_repo_get_path (ss->repo)); ++ return; ++ } ++ } ++ } + } + + static gboolean +-flatpak_remote_state_resolve_sideloaded_ref (FlatpakRemoteState *self, +- const char *ref, +- char **out_checksum, +- guint64 *out_timestamp, +- VarRefInfoRef *out_info, +- FlatpakSideloadState **out_sideload_state, +- GError **error) ++flatpak_remote_state_resolve_sideloaded_ref_repos (FlatpakRemoteState *self, ++ const char *ref, ++ char **out_checksum, ++ guint64 *out_timestamp, ++ VarRefInfoRef *out_info, ++ FlatpakSideloadState **out_sideload_state, ++ GError **error) + { + g_autofree char *latest_checksum = NULL; + guint64 latest_timestamp = 0; +@@ -725,6 +761,73 @@ flatpak_remote_state_resolve_sideloaded_ref (FlatpakRemoteState *self, + return TRUE; + } + ++static gboolean ++flatpak_remote_state_resolve_sideloaded_ref_images (FlatpakRemoteState *self, ++ const char *ref, ++ char **out_checksum, ++ guint64 *out_timestamp, ++ VarRefInfoRef *out_info, ++ FlatpakImageSource **out_image_source, ++ GError **error) ++{ ++ g_autoptr(FlatpakImageSource) image_source = NULL; ++ ++ for (int i = 0; i < self->sideload_image_collections->len; i++) ++ { ++ FlatpakImageCollection *collection = g_ptr_array_index (self->sideload_image_collections, i); ++ image_source = flatpak_image_collection_lookup_ref (collection, ref); ++ if (image_source) ++ break; ++ } ++ ++ if (image_source) ++ { ++ if (out_checksum) ++ { ++ const char *digest = flatpak_image_source_get_digest (image_source); ++ g_assert (g_str_has_prefix (digest, "sha256:")); ++ *out_checksum = g_strdup (digest + 7); ++ } ++ ++ if (out_timestamp) ++ *out_timestamp = flatpak_image_source_get_commit_timestamp (image_source); ++ } ++ ++ if (image_source == NULL) ++ return flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, ++ _("No such ref '%s' in remote %s"), ++ ref, self->remote_name); ++ ++ if (out_image_source) ++ *out_image_source = g_steal_pointer (&image_source); ++ ++ return TRUE; ++} ++ ++static gboolean ++flatpak_remote_state_resolve_sideloaded_ref (FlatpakRemoteState *self, ++ const char *ref, ++ char **out_checksum, ++ guint64 *out_timestamp, ++ VarRefInfoRef *out_info, ++ FlatpakSideloadState **out_sideload_state, ++ FlatpakImageSource **out_image_source, ++ GError **error) ++{ ++ if (out_sideload_state) ++ *out_sideload_state = NULL; ++ if (out_image_source) ++ *out_image_source = NULL; ++ ++ if (self->is_oci) ++ return flatpak_remote_state_resolve_sideloaded_ref_images (self, ref, out_checksum, out_timestamp, out_info, ++ out_image_source, error); ++ else ++ return flatpak_remote_state_resolve_sideloaded_ref_repos (self, ref, out_checksum, out_timestamp, out_info, ++ out_sideload_state, error); ++ ++} ++ + static GVariant * + get_summary_for_ref (FlatpakRemoteState *self, + const char *ref) +@@ -754,6 +857,9 @@ get_summary_for_ref (FlatpakRemoteState *self, + + /* Returns TRUE if the ref is found in the summary or cache. + * out_checksum and out_variant are only set when the ref is found. ++ * ++ * NOTE: The _internal() variant has the odd constraint that *out_info is only ++ * valid if *out_image_source is NULL. + */ + static gboolean + flatpak_remote_state_lookup_ref_internal (FlatpakRemoteState *self, +@@ -762,8 +868,11 @@ flatpak_remote_state_lookup_ref_internal (FlatpakRemoteState *self, + guint64 *out_timestamp, + VarRefInfoRef *out_info, + GFile **out_sideload_path, ++ FlatpakImageSource **out_image_source, + GError **error) + { ++ g_assert (out_info == NULL || out_image_source != NULL); ++ + if (!flatpak_remote_state_allow_ref (self, ref)) + { + return flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, +@@ -786,26 +895,7 @@ flatpak_remote_state_lookup_ref_internal (FlatpakRemoteState *self, + ref, self->remote_name); + + /* Even if its available in the summary we want to install it from a sideload repo if available */ +- +- if (out_sideload_path) +- { +- g_autoptr(GFile) found_sideload_path = NULL; +- +- for (int i = 0; i < self->sideload_repos->len; i++) +- { +- FlatpakSideloadState *ss = g_ptr_array_index (self->sideload_repos, i); +- OstreeRepoCommitState commit_state; +- +- if (ostree_repo_load_commit (ss->repo, checksum, NULL, &commit_state, NULL) && +- commit_state == OSTREE_REPO_COMMIT_STATE_NORMAL) +- { +- found_sideload_path = g_object_ref (ostree_repo_get_path (ss->repo)); +- break; +- } +- } +- +- *out_sideload_path = g_steal_pointer (&found_sideload_path); +- } ++ flatpak_remote_state_lookup_sideload_checksum (self, checksum, out_sideload_path, out_image_source); + + if (out_info) + *out_info = info; +@@ -818,7 +908,7 @@ flatpak_remote_state_lookup_ref_internal (FlatpakRemoteState *self, + { + FlatpakSideloadState *ss = NULL; + +- if (!flatpak_remote_state_resolve_sideloaded_ref (self, ref, out_checksum, out_timestamp, out_info, &ss, error)) ++ if (!flatpak_remote_state_resolve_sideloaded_ref (self, ref, out_checksum, out_timestamp, out_info, &ss, out_image_source, error)) + return FALSE; + + if (out_sideload_path) +@@ -843,17 +933,26 @@ flatpak_remote_state_lookup_ref (FlatpakRemoteState *self, + guint64 *out_timestamp, + GVariant **out_summary_metadata, + GFile **out_sideload_path, ++ FlatpakImageSource **out_image_source, + GError **error) + { + + if (out_summary_metadata) + { ++ g_autoptr(FlatpakImageSource) local_image_source; + VarRefInfoRef info; + + if (flatpak_remote_state_lookup_ref_internal (self, ref, out_checksum, out_timestamp, +- &info, out_sideload_path, error)) ++ &info, out_sideload_path, &local_image_source, error)) + { +- *out_summary_metadata = var_metadata_dup_to_gvariant (var_ref_info_get_metadata (info)); ++ if (local_image_source) ++ *out_summary_metadata = flatpak_image_source_make_summary_metadata (local_image_source); ++ else ++ *out_summary_metadata = var_metadata_dup_to_gvariant (var_ref_info_get_metadata (info)); ++ ++ if (out_image_source) ++ *out_image_source = g_steal_pointer (&local_image_source); ++ + return TRUE; + } + else +@@ -862,7 +961,7 @@ flatpak_remote_state_lookup_ref (FlatpakRemoteState *self, + else + { + return flatpak_remote_state_lookup_ref_internal (self, ref, out_checksum, out_timestamp, +- NULL, out_sideload_path, error); ++ NULL, out_sideload_path, out_image_source, error); + } + } + +@@ -1034,6 +1133,7 @@ flatpak_remote_state_load_data (FlatpakRemoteState *self, + g_autofree char *checksum = NULL; + guint64 timestamp; + FlatpakSideloadState *ss = NULL; ++ g_autoptr(FlatpakImageSource) image_source = NULL; + g_autoptr(GVariant) commit_data = NULL; + g_autoptr(GVariant) commit_metadata = NULL; + const char *xa_metadata = NULL; +@@ -1043,13 +1143,23 @@ flatpak_remote_state_load_data (FlatpakRemoteState *self, + /* Use sideload refs if any */ + + if (!flatpak_remote_state_resolve_sideloaded_ref (self, ref, &checksum, ×tamp, +- NULL, &ss, error)) ++ NULL, &ss, &image_source, error)) + return FALSE; + +- if (!ostree_repo_load_commit (ss->repo, checksum, &commit_data, NULL, error)) +- return FALSE; ++ if (image_source) ++ { ++ g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); ++ flatpak_image_source_build_commit_metadata (image_source, metadata_builder); ++ commit_metadata = g_variant_builder_end (metadata_builder); ++ } ++ else ++ { ++ if (!ostree_repo_load_commit (ss->repo, checksum, &commit_data, NULL, error)) ++ return FALSE; ++ ++ commit_metadata = g_variant_get_child_value (commit_data, 0); ++ } + +- commit_metadata = g_variant_get_child_value (commit_data, 0); + g_variant_lookup (commit_metadata, "xa.metadata", "&s", &xa_metadata); + if (xa_metadata == NULL) + return flatpak_fail (error, "No xa.metadata in sideload commit %s ref %s", checksum, ref); +@@ -1124,15 +1234,11 @@ flatpak_remote_state_fetch_image_source (FlatpakRemoteState *self, + GError **error) + { + g_autoptr(FlatpakImageSource) image_source = NULL; +- g_autofree char *oci_digest = NULL; + g_autofree char *latest_rev = NULL; + VarRefInfoRef latest_rev_info; +- VarMetadataRef metadata; +- const char *oci_repository = NULL; +- const char *delta_url = NULL; + + /* We extract the rev info from the latest, even if we don't use the latest digest, assuming refs don't move */ +- if (!flatpak_remote_state_lookup_ref_internal (self, ref, &latest_rev, NULL, &latest_rev_info, NULL, error)) ++ if (!flatpak_remote_state_lookup_ref_internal (self, ref, &latest_rev, NULL, &latest_rev_info, NULL, &image_source, error)) + return NULL; + + if (latest_rev == NULL) +@@ -1143,23 +1249,31 @@ flatpak_remote_state_fetch_image_source (FlatpakRemoteState *self, + return NULL; + } + +- metadata = var_ref_info_get_metadata (latest_rev_info); +- oci_repository = var_metadata_lookup_string (metadata, "xa.oci-repository", NULL); +- delta_url = var_metadata_lookup_string (metadata, "xa.delta-url", NULL); ++ if (image_source == NULL) ++ { ++ VarMetadataRef metadata; ++ g_autofree char *oci_digest = NULL; ++ const char *oci_repository = NULL; ++ const char *delta_url = NULL; + +- oci_digest = g_strconcat ("sha256:", opt_rev ? opt_rev : latest_rev, NULL); ++ metadata = var_ref_info_get_metadata (latest_rev_info); ++ oci_repository = var_metadata_lookup_string (metadata, "xa.oci-repository", NULL); ++ delta_url = var_metadata_lookup_string (metadata, "xa.delta-url", NULL); + +- image_source = flatpak_remote_state_new_image_source (self, oci_repository, oci_digest, token, cancellable, error); +- if (image_source == NULL) +- return NULL; ++ oci_digest = g_strconcat ("sha256:", opt_rev ? opt_rev : latest_rev, NULL); + +- if (g_strcmp0 (flatpak_image_source_get_ref (image_source), ref) != 0) +- { +- flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Commit has no requested ref ‘%s’ in ref binding metadata"), ref); +- return NULL; +- } ++ image_source = flatpak_remote_state_new_image_source (self, oci_repository, oci_digest, token, cancellable, error); ++ if (image_source == NULL) ++ return NULL; ++ ++ if (g_strcmp0 (flatpak_image_source_get_ref (image_source), ref) != 0) ++ { ++ flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Commit has no requested ref ‘%s’ in ref binding metadata"), ref); ++ return NULL; ++ } + +- flatpak_image_source_set_delta_url (image_source, delta_url); ++ flatpak_image_source_set_delta_url (image_source, delta_url); ++ } + + return g_steal_pointer (&image_source); + } +@@ -1299,7 +1413,7 @@ flatpak_remote_state_load_ref_commit (FlatpakRemoteState *self, + + if (opt_commit == NULL) + { +- if (!flatpak_remote_state_lookup_ref (self, ref, &commit, NULL, NULL, NULL, error)) ++ if (!flatpak_remote_state_lookup_ref (self, ref, &commit, NULL, NULL, NULL, NULL, error)) + return NULL; + + if (commit == NULL) +@@ -1317,12 +1431,26 @@ flatpak_remote_state_load_ref_commit (FlatpakRemoteState *self, + if (ostree_repo_load_commit (dir->repo, commit, &commit_data, NULL, NULL)) + goto out; + +- for (int i = 0; i < self->sideload_repos->len; i++) ++ if (self->is_oci) + { +- FlatpakSideloadState *ss = g_ptr_array_index (self->sideload_repos, i); ++ g_autoptr(FlatpakImageSource) image_source; + +- if (ostree_repo_load_commit (ss->repo, commit, &commit_data, NULL, NULL)) +- goto out; ++ flatpak_remote_state_lookup_sideload_checksum (self, commit, NULL, &image_source); ++ if (image_source) ++ { ++ commit_data = flatpak_image_source_make_fake_commit (image_source); ++ goto out; ++ } ++ } ++ else ++ { ++ for (int i = 0; i < self->sideload_repos->len; i++) ++ { ++ FlatpakSideloadState *ss = g_ptr_array_index (self->sideload_repos, i); ++ ++ if (ostree_repo_load_commit (ss->repo, commit, &commit_data, NULL, NULL)) ++ goto out; ++ } + } + + if (flatpak_dir_get_remote_oci (dir, self->remote_name)) +@@ -5034,6 +5162,7 @@ flatpak_dir_find_latest_rev (FlatpakDir *self, + char **out_rev, + guint64 *out_timestamp, + GFile **out_sideload_path, ++ FlatpakImageSource **out_image_source, + GCancellable *cancellable, + GError **error) + { +@@ -5041,7 +5170,7 @@ flatpak_dir_find_latest_rev (FlatpakDir *self, + + g_return_val_if_fail (out_rev != NULL, FALSE); + +- if (!flatpak_remote_state_lookup_ref (state, ref, &latest_rev, out_timestamp, NULL, out_sideload_path, error)) ++ if (!flatpak_remote_state_lookup_ref (state, ref, &latest_rev, out_timestamp, NULL, out_sideload_path, out_image_source, error)) + return FALSE; + if (latest_rev == NULL) + return flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, +@@ -5286,10 +5415,10 @@ flatpak_dir_update_appstream (FlatpakDir *self, + used_branch = new_branch; + if (!is_oci) + { +- if (!flatpak_dir_find_latest_rev (self, state, used_branch, NULL, &appstream_commit, NULL, &appstream_sideload_path, cancellable, &first_error)) ++ if (!flatpak_dir_find_latest_rev (self, state, used_branch, NULL, &appstream_commit, NULL, &appstream_sideload_path, NULL, cancellable, &first_error)) + { + used_branch = old_branch; +- if (!flatpak_dir_find_latest_rev (self, state, used_branch, NULL, &appstream_commit, NULL, &appstream_sideload_path, cancellable, &second_error)) ++ if (!flatpak_dir_find_latest_rev (self, state, used_branch, NULL, &appstream_commit, NULL, &appstream_sideload_path, NULL, cancellable, &second_error)) + { + g_prefix_error (&first_error, "Error updating appstream2: "); + g_prefix_error (&second_error, "Error updating appstream: "); +@@ -6095,7 +6224,7 @@ flatpak_dir_pull (FlatpakDir *self, + { + rev = g_strdup (opt_rev); + } +- else if (!flatpak_remote_state_lookup_ref (state, ref, &rev, NULL, NULL, NULL, error)) ++ else if (!flatpak_remote_state_lookup_ref (state, ref, &rev, NULL, NULL, NULL, NULL, error)) + { + g_assert (error == NULL || *error != NULL); + return FALSE; +@@ -10489,7 +10618,7 @@ flatpak_dir_check_for_update (FlatpakDir *self, + else + { + if (!flatpak_dir_find_latest_rev (self, state, flatpak_decomposed_get_ref (ref), checksum_or_latest, &latest_rev, +- NULL, NULL, cancellable, error)) ++ NULL, NULL, NULL, cancellable, error)) + return NULL; + } + +@@ -12771,6 +12900,7 @@ _flatpak_dir_get_remote_state (FlatpakDir *self, + if (!ostree_repo_remote_get_url (self->repo, remote_or_uri, &url, error)) + return NULL; + ++ state->is_oci = flatpak_dir_get_remote_oci (self, remote_or_uri); + state->default_token_type = flatpak_dir_get_remote_default_token_type (self, remote_or_uri); + } + +@@ -13104,6 +13234,56 @@ populate_hash_table_from_refs_map (GHashTable *ret_all_refs, + } + + ++static void ++populate_hash_table_from_image_collection (GHashTable *ret_all_refs, ++ GHashTable *ref_timestamps, ++ FlatpakImageCollection *image_collection, ++ const char *opt_collection_id, ++ FlatpakRemoteState *state) ++{ ++ g_autoptr(GPtrArray) sources = flatpak_image_collection_get_sources (image_collection); ++ ++ for (guint i = 0; i < sources->len; i++) ++ { ++ FlatpakImageSource *image_source = g_ptr_array_index (sources, i); ++ const char *ref_name = flatpak_image_source_get_ref (image_source); ++ const char *digest = flatpak_image_source_get_digest (image_source); ++ const char *checksum; ++ guint64 *new_timestamp = NULL; ++ g_autoptr(FlatpakDecomposed) decomposed = NULL; ++ ++ if (!flatpak_remote_state_allow_ref (state, ref_name)) ++ continue; ++ ++ g_assert (g_str_has_prefix (digest, "sha256:")); ++ checksum = digest + 7; ++ ++ decomposed = flatpak_decomposed_new_from_col_ref (ref_name, opt_collection_id, NULL); ++ if (decomposed == NULL) ++ continue; ++ ++ if (ref_timestamps) ++ { ++ guint64 timestamp = flatpak_image_source_get_commit_timestamp (image_source); ++ gpointer value; ++ ++ if (g_hash_table_lookup_extended (ref_timestamps, ref_name, NULL, &value)) ++ { ++ guint64 *old_timestamp = value; ++ if (*old_timestamp >= timestamp) ++ continue; /* New timestamp is older, skip this commit */ ++ } ++ ++ new_timestamp = g_memdup2 (×tamp, sizeof (guint64)); ++ } ++ ++ g_hash_table_replace (ret_all_refs, g_steal_pointer (&decomposed), g_strdup (checksum)); ++ if (new_timestamp) ++ g_hash_table_replace (ref_timestamps, g_strdup (ref_name), new_timestamp); ++ } ++} ++ ++ + /* This tries to list all available remote refs but also tries to keep + * working when offline, so it looks in sideloaded repos. Also it uses + * in-memory cached summaries which ostree doesn't. */ +@@ -13175,25 +13355,37 @@ flatpak_dir_list_all_remote_refs (FlatpakDir *self, + ref_map = var_summary_get_ref_map (summary); + populate_hash_table_from_refs_map (ret_all_refs, NULL, ref_map, main_collection_id, state); + } +- else if (state->collection_id) ++ else + { + g_autoptr(GHashTable) ref_mtimes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + /* No main summary, add just all sideloded refs, with the latest version of each checksum */ + +- for (int i = 0; i < state->sideload_repos->len; i++) ++ if (state->collection_id) + { +- FlatpakSideloadState *ss = g_ptr_array_index (state->sideload_repos, i); ++ for (int i = 0; i < state->sideload_repos->len; i++) ++ { ++ FlatpakSideloadState *ss = g_ptr_array_index (state->sideload_repos, i); + +- summary = var_summary_from_gvariant (ss->summary); +- exts = var_summary_get_metadata (summary); ++ summary = var_summary_from_gvariant (ss->summary); ++ exts = var_summary_get_metadata (summary); + +- if (var_metadata_lookup (exts, "ostree.summary.collection-map", NULL, &v)) +- { +- VarCollectionMapRef map = var_collection_map_from_variant (v); ++ if (var_metadata_lookup (exts, "ostree.summary.collection-map", NULL, &v)) ++ { ++ VarCollectionMapRef map = var_collection_map_from_variant (v); + +- if (var_collection_map_lookup (map, state->collection_id, NULL, &ref_map)) +- populate_hash_table_from_refs_map (ret_all_refs, ref_mtimes, ref_map, NULL, state); ++ if (var_collection_map_lookup (map, state->collection_id, NULL, &ref_map)) ++ populate_hash_table_from_refs_map (ret_all_refs, ref_mtimes, ref_map, NULL, state); ++ } ++ } ++ } ++ ++ if (state->is_oci) ++ { ++ for (int i = 0; i < state->sideload_image_collections->len; i++) ++ { ++ FlatpakImageCollection *collection = g_ptr_array_index (state->sideload_image_collections, i); ++ populate_hash_table_from_image_collection (ret_all_refs, ref_mtimes, collection, NULL, state); + } + } + } +@@ -15773,7 +15965,7 @@ flatpak_dir_find_remote_related_for_metadata (FlatpakDir *self, + if (extension_ref == NULL) + continue; + +- if (flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (extension_ref), &checksum, NULL, NULL, NULL, NULL)) ++ if (flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (extension_ref), &checksum, NULL, NULL, NULL, NULL, NULL)) + { + if (flatpak_filters_allow_ref (NULL, masked, flatpak_decomposed_get_ref (extension_ref))) + add_related (self, related, state->remote_name, extension, extension_ref, checksum, +@@ -15788,7 +15980,7 @@ flatpak_dir_find_remote_related_for_metadata (FlatpakDir *self, + g_autofree char *subref_checksum = NULL; + + if (flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (subref_ref), +- &subref_checksum, NULL, NULL, NULL, NULL) && ++ &subref_checksum, NULL, NULL, NULL, NULL, NULL) && + flatpak_filters_allow_ref (NULL, masked, flatpak_decomposed_get_ref (subref_ref))) + add_related (self, related, state->remote_name, extension, subref_ref, subref_checksum, + no_autodownload, download_if, autoprune_unless, autodelete, locale_subset); +diff --git a/common/flatpak-image-collection-private.h b/common/flatpak-image-collection-private.h +new file mode 100644 +index 00000000..2848f95f +--- /dev/null ++++ b/common/flatpak-image-collection-private.h +@@ -0,0 +1,50 @@ ++/* ++ * Copyright © 2024 Red Hat, Inc ++ * ++ * This program 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. ++ * ++ * This library is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library. If not, see . ++ * ++ * Authors: ++ * Owen Taylor ++ */ ++ ++#ifndef __FLATPAK_IMAGE_COLLECTION_H__ ++#define __FLATPAK_IMAGE_COLLECTION_H__ ++ ++#include ++#include ++ ++#include ++#include ++ ++#define FLATPAK_TYPE_IMAGE_COLLECTION flatpak_image_collection_get_type () ++#define FLATPAK_IMAGE_COLLECTION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FLATPAK_TYPE_IMAGE_COLLECTION, FlatpakImageCollection)) ++#define FLATPAK_IS_IMAGE_COLLECTION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), FLATPAK_TYPE_IMAGE_COLLECTION)) ++ ++GType flatpak_image_collection_get_type (void); ++ ++G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakImageCollection, g_object_unref) ++ ++ ++FlatpakImageCollection *flatpak_image_collection_new (const char *location, ++ GCancellable *cancellable, ++ GError **error); ++ ++FlatpakImageSource *flatpak_image_collection_lookup_ref (FlatpakImageCollection *self, ++ const char *ref); ++FlatpakImageSource *flatpak_image_collection_lookup_digest (FlatpakImageCollection *self, ++ const char *digest); ++ ++GPtrArray *flatpak_image_collection_get_sources (FlatpakImageCollection *self); ++ ++#endif /* __FLATPAK_IMAGE_COLLECTION_H__ */ +diff --git a/common/flatpak-image-collection.c b/common/flatpak-image-collection.c +new file mode 100644 +index 00000000..bdb41838 +--- /dev/null ++++ b/common/flatpak-image-collection.c +@@ -0,0 +1,154 @@ ++/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s: ++ * Copyright © 2024 Red Hat, Inc ++ * ++ * This program 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. ++ * ++ * This library is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library. If not, see . ++ * ++ * Authors: ++ * Owen Taylor ++ */ ++ ++#include ++ ++#include "flatpak-image-collection-private.h" ++#include "flatpak-oci-registry-private.h" ++ ++struct FlatpakImageCollection ++{ ++ GObject parent; ++ ++ GPtrArray *sources; ++}; ++ ++typedef struct ++{ ++ GObjectClass parent_class; ++} FlatpakImageCollectionClass; ++ ++G_DEFINE_TYPE (FlatpakImageCollection, flatpak_image_collection, G_TYPE_OBJECT) ++ ++ ++static void ++flatpak_image_collection_finalize (GObject *object) ++{ ++ FlatpakImageCollection *self = FLATPAK_IMAGE_COLLECTION (object); ++ ++ g_ptr_array_free (self->sources, TRUE); ++ ++ G_OBJECT_CLASS (flatpak_image_collection_parent_class)->finalize (object); ++} ++ ++static void ++flatpak_image_collection_class_init (FlatpakImageCollectionClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ ++ object_class->finalize = flatpak_image_collection_finalize; ++} ++ ++static void ++flatpak_image_collection_init (FlatpakImageCollection *self) ++{ ++ self->sources = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref); ++} ++ ++FlatpakImageCollection * ++flatpak_image_collection_new (const char *location, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(FlatpakImageCollection) self = NULL; ++ g_autoptr(FlatpakOciRegistry) registry = NULL; ++ g_autoptr(FlatpakOciIndex) index = NULL; ++ gsize i; ++ ++ self = g_object_new (FLATPAK_TYPE_IMAGE_COLLECTION, NULL); ++ ++ if (g_str_has_prefix (location, "oci:")) ++ { ++ g_autoptr(GFile) dir = g_file_new_for_path (location + 4); ++ g_autofree char *uri = g_file_get_uri (dir); ++ ++ registry = flatpak_oci_registry_new (uri, FALSE, -1, cancellable, error); ++ if (registry == NULL) ++ return NULL; ++ } ++ else if (g_str_has_prefix (location, "oci-archive:")) ++ { ++ g_autoptr(GFile) file = g_file_new_for_path (location + 12); ++ registry = flatpak_oci_registry_new_for_archive (file, cancellable, error); ++ if (registry == NULL) ++ return NULL; ++ } ++ else ++ { ++ flatpak_fail (error, "Can't parse image collection location %s", location); ++ return NULL; ++ } ++ ++ index = flatpak_oci_registry_load_index (registry, cancellable, error); ++ if (index == NULL) ++ return NULL; ++ ++ for (i = 0; index->manifests[i] != NULL; i++) ++ { ++ g_autoptr(GError) local_error = NULL; ++ FlatpakOciManifestDescriptor *descriptor = index->manifests[i]; ++ g_autoptr(FlatpakImageSource) image_source = flatpak_image_source_new (registry, NULL, ++ descriptor->parent.digest, ++ cancellable, &local_error); ++ if (image_source == NULL) ++ { ++ g_info ("Can't load manifest in image collection: %s", local_error->message); ++ continue; ++ } ++ ++ g_ptr_array_add (self->sources, g_steal_pointer (&image_source)); ++ } ++ ++ return g_steal_pointer (&self); ++} ++ ++FlatpakImageSource * ++flatpak_image_collection_lookup_ref (FlatpakImageCollection *self, ++ const char *ref) ++{ ++ for (guint i = 0; i < self->sources->len; i++) ++ { ++ FlatpakImageSource *source = g_ptr_array_index (self->sources, i); ++ if (strcmp (flatpak_image_source_get_ref (source), ref) == 0) ++ return g_object_ref (source); ++ } ++ ++ return NULL; ++} ++ ++FlatpakImageSource * ++flatpak_image_collection_lookup_digest (FlatpakImageCollection *self, ++ const char *digest) ++{ ++ for (guint i = 0; i < self->sources->len; i++) ++ { ++ FlatpakImageSource *source = g_ptr_array_index (self->sources, i); ++ if (strcmp (flatpak_image_source_get_digest (source), digest) == 0) ++ return g_object_ref (source); ++ } ++ ++ return NULL; ++} ++ ++GPtrArray * ++flatpak_image_collection_get_sources (FlatpakImageCollection *self) ++{ ++ return g_ptr_array_ref (self->sources); ++} +diff --git a/common/flatpak-image-source-private.h b/common/flatpak-image-source-private.h +index 94e4cc1a..597a8174 100644 +--- a/common/flatpak-image-source-private.h ++++ b/common/flatpak-image-source-private.h +@@ -32,6 +32,12 @@ G_DECLARE_FINAL_TYPE (FlatpakImageSource, + FLATPAK, IMAGE_SOURCE, + GObject) + ++FlatpakImageSource *flatpak_image_source_new (FlatpakOciRegistry *registry, ++ const char *repository, ++ const char *digest, ++ GCancellable *cancellable, ++ GError **error); ++ + FlatpakImageSource *flatpak_image_source_new_local (GFile *file, + const char *reference, + GCancellable *cancellable, +@@ -70,4 +76,5 @@ void flatpak_image_source_build_commit_metadata (FlatpakImageSource *self, + GVariantBuilder *metadata_builder); + + GVariant *flatpak_image_source_make_fake_commit (FlatpakImageSource *image_source); ++GVariant *flatpak_image_source_make_summary_metadata (FlatpakImageSource *self); + #endif /* __FLATPAK_IMAGE_SOURCE_H__ */ +diff --git a/common/flatpak-image-source.c b/common/flatpak-image-source.c +index 9d13cf0d..a31f1084 100644 +--- a/common/flatpak-image-source.c ++++ b/common/flatpak-image-source.c +@@ -68,7 +68,7 @@ flatpak_image_source_init (FlatpakImageSource *self) + { + } + +-static FlatpakImageSource * ++FlatpakImageSource * + flatpak_image_source_new (FlatpakOciRegistry *registry, + const char *repository, + const char *digest, +@@ -78,6 +78,12 @@ flatpak_image_source_new (FlatpakOciRegistry *registry, + g_autoptr(FlatpakImageSource) self = NULL; + g_autoptr(FlatpakOciVersioned) versioned = NULL; + ++ if (!g_str_has_prefix (digest, "sha256:")) ++ { ++ flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Only sha256 image checksums are supported")); ++ return NULL; ++ } ++ + self = g_object_new (FLATPAK_TYPE_IMAGE_SOURCE, NULL); + self->registry = g_object_ref (registry); + self->repository = g_strdup (repository); +@@ -494,3 +500,20 @@ flatpak_image_source_make_fake_commit (FlatpakImageSource *self) + ostree_checksum_to_bytes_v ("0000000000000000000000000000000000000000000000000000000000000000"), + ostree_checksum_to_bytes_v ("0000000000000000000000000000000000000000000000000000000000000000"))); + } ++ ++GVariant * ++flatpak_image_source_make_summary_metadata (FlatpakImageSource *self) ++{ ++ g_autoptr(GVariantBuilder) ref_metadata_builder = NULL; ++ ++ ref_metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); ++ ++ if (self->repository) ++ g_variant_builder_add (ref_metadata_builder, "{sv}", "xa.oci-repository", ++ g_variant_new_string (self->repository)); ++ if (self->delta_url) ++ g_variant_builder_add (ref_metadata_builder, "{sv}", "xa.delta-url", ++ g_variant_new_string (self->delta_url)); ++ ++ return g_variant_ref_sink (g_variant_builder_end (ref_metadata_builder)); ++} +diff --git a/common/flatpak-transaction.c b/common/flatpak-transaction.c +index 2dd35f53..b498e4d4 100644 +--- a/common/flatpak-transaction.c ++++ b/common/flatpak-transaction.c +@@ -26,6 +26,8 @@ + #include "flatpak-auth-private.h" + #include "flatpak-dir-private.h" + #include "flatpak-error.h" ++#include "flatpak-image-collection-private.h" ++#include "flatpak-image-collection-private.h" + #include "flatpak-image-source-private.h" + #include "flatpak-installation-private.h" + #include "flatpak-oci-registry-private.h" +@@ -172,6 +174,7 @@ typedef struct _FlatpakTransactionPrivate + GHashTable *remote_states; /* (element-type utf8 FlatpakRemoteState) */ + GPtrArray *extra_dependency_dirs; + GPtrArray *extra_sideload_repos; ++ GPtrArray *sideload_image_collections; + GList *ops; + GPtrArray *added_origin_remotes; + +@@ -524,6 +527,38 @@ flatpak_transaction_add_sideload_repo (FlatpakTransaction *self, + g_strdup (path)); + } + ++/** ++ * flatpak_transaction_add_sideload_image_collection: ++ * @self: a #FlatpakTransaction ++ * @location: source of images for installation ++ * ++ * Adds a set of images to be used as source for installation. This is similar ++ * to flatpak_transaction_add_sideload_repo(), but the Flatpaks are stored ++ * as OCI images rather than ostree commits, and the images are used for ++ * all OCI remotes without regard to collection ID. ++ * ++ * Currently @location should be either 'oci:' or 'oci-archive:'. ++ * Additional schemes may be added in the future. ++ * ++ * Since: 1.7.1 ++ */ ++gboolean ++flatpak_transaction_add_sideload_image_collection (FlatpakTransaction *self, ++ const char *location, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self); ++ g_autoptr(FlatpakImageCollection) collection = NULL; ++ ++ collection = flatpak_image_collection_new (location, cancellable, error); ++ if (collection == NULL) ++ return FALSE; ++ ++ g_ptr_array_add (priv->sideload_image_collections, g_steal_pointer (&collection)); ++ return TRUE; ++} ++ + /** + * flatpak_transaction_add_default_dependency_sources: + * @self: a #FlatpakTransaction +@@ -1033,6 +1068,7 @@ flatpak_transaction_finalize (GObject *object) + + g_ptr_array_free (priv->extra_dependency_dirs, TRUE); + g_ptr_array_free (priv->extra_sideload_repos, TRUE); ++ g_ptr_array_free (priv->sideload_image_collections, TRUE); + + G_OBJECT_CLASS (flatpak_transaction_parent_class)->finalize (object); + } +@@ -1508,6 +1544,7 @@ flatpak_transaction_init (FlatpakTransaction *self) + priv->added_origin_remotes = g_ptr_array_new_with_free_func (g_free); + priv->extra_dependency_dirs = g_ptr_array_new_with_free_func (g_object_unref); + priv->extra_sideload_repos = g_ptr_array_new_with_free_func (g_free); ++ priv->sideload_image_collections = g_ptr_array_new_with_free_func (g_object_unref); + priv->can_run = TRUE; + } + +@@ -2085,6 +2122,13 @@ flatpak_transaction_ensure_remote_state (FlatpakTransaction *self, + g_autoptr(GFile) f = g_file_new_for_path (path); + flatpak_remote_state_add_sideload_dir (state, f); + } ++ ++ for (int i = 0; i < priv->sideload_image_collections->len; i++) ++ { ++ FlatpakImageCollection *collection = g_ptr_array_index (priv->sideload_image_collections, i); ++ flatpak_remote_state_add_sideload_image_collection (state, collection); ++ } ++ + } + + if (opt_arch != NULL && +@@ -2368,7 +2412,7 @@ search_for_dependency (FlatpakTransaction *self, + continue; + } + +- if (flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (runtime_ref), NULL, NULL, NULL, NULL, NULL)) ++ if (flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (runtime_ref), NULL, NULL, NULL, NULL, NULL, NULL)) + g_ptr_array_add (found, g_strdup (remote)); + } + +@@ -3243,7 +3287,7 @@ flatpak_transaction_add_auto_install (FlatpakTransaction *self, + g_autoptr(FlatpakRemoteState) state = flatpak_transaction_ensure_remote_state (self, FLATPAK_TRANSACTION_OPERATION_UPDATE, remote, NULL, NULL); + + if (state != NULL && +- flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (auto_install_ref), NULL, NULL, NULL, NULL, NULL)) ++ flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (auto_install_ref), NULL, NULL, NULL, NULL, NULL, NULL)) + { + g_info ("Auto adding install of %s from remote %s", flatpak_decomposed_get_ref (auto_install_ref), remote); + +@@ -3424,6 +3468,7 @@ resolve_op_from_commit (FlatpakTransaction *self, + FlatpakTransactionOperation *op, + const char *checksum, + GFile *sideload_path, ++ FlatpakImageSource *image_source, + GVariant *commit_data, + GError **error) + { +@@ -3463,7 +3508,7 @@ resolve_op_from_commit (FlatpakTransaction *self, + flatpak_decomposed_get_ref (eolr_decomposed)); + } + +- return resolve_op_end (self, op, checksum, sideload_path, NULL, metadata_bytes, error); ++ return resolve_op_end (self, op, checksum, sideload_path, image_source, metadata_bytes, error); + } + + /* NOTE: In case of non-available summary this returns FALSE with a +@@ -3474,6 +3519,7 @@ try_resolve_op_from_metadata (FlatpakTransaction *self, + FlatpakTransactionOperation *op, + const char *checksum, + GFile *sideload_path, ++ FlatpakImageSource *image_source, + FlatpakRemoteState *state, + GError **error) + { +@@ -3487,7 +3533,7 @@ try_resolve_op_from_metadata (FlatpakTransaction *self, + /* Ref has to match the actual commit in the summary */ + if ((state->summary == NULL && state->index == NULL) || + !flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref), +- &summary_checksum, NULL, NULL, NULL, NULL) || ++ &summary_checksum, NULL, NULL, NULL, NULL, NULL) || + strcmp (summary_checksum, checksum) != 0) + return FALSE; + +@@ -3499,7 +3545,7 @@ try_resolve_op_from_metadata (FlatpakTransaction *self, + metadata_bytes = g_bytes_new (metadata, strlen (metadata)); + + flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref), +- NULL, NULL, &op->summary_metadata, NULL, NULL); ++ NULL, NULL, &op->summary_metadata, NULL, NULL, NULL); + + op->installed_size = installed_size; + op->download_size = download_size; +@@ -3629,36 +3675,39 @@ resolve_ops (FlatpakTransaction *self, + if (commit_data == NULL) + return FALSE; + +- if (!resolve_op_from_commit (self, op, checksum, NULL, commit_data, error)) ++ if (!resolve_op_from_commit (self, op, checksum, NULL, NULL, commit_data, error)) + return FALSE; + } + else + { + g_autoptr(GError) local_error = NULL; + g_autoptr(GFile) sideload_path = NULL; ++ g_autoptr(FlatpakImageSource) image_source = NULL; + + if (op->commit != NULL) + { + checksum = g_strdup (op->commit); + /* Check if this is available offline and if so, use that */ +- sideload_path = flatpak_remote_state_lookup_sideload_checksum (state, op->commit); ++ flatpak_remote_state_lookup_sideload_checksum (state, op->commit, &sideload_path, &image_source); + } + else + { + g_autofree char *latest_checksum = NULL; + g_autoptr(GFile) latest_sideload_path = NULL; ++ g_autoptr(FlatpakImageSource) latest_image_source = NULL; + g_autofree char *local_checksum = NULL; + guint64 latest_timestamp; + g_autoptr(GVariant) local_commit_data = flatpak_dir_read_latest_commit (priv->dir, op->remote, op->ref, + &local_checksum, NULL, NULL); + + if (flatpak_dir_find_latest_rev (priv->dir, state, flatpak_decomposed_get_ref (op->ref), op->commit, +- &latest_checksum, &latest_timestamp, &latest_sideload_path, ++ &latest_checksum, &latest_timestamp, &latest_sideload_path, &latest_image_source, + cancellable, &local_error)) + { + /* If we found the latest in a sideload repo, it may be older that what is locally available, check timestamps. + * Note: If the timestamps are equal (timestamp granularity issue), assume we want to update */ +- if (latest_sideload_path != NULL && local_commit_data && latest_timestamp != 0 && ++ if ((latest_sideload_path != NULL || latest_image_source != NULL) && ++ local_commit_data && latest_timestamp != 0 && + ostree_commit_get_timestamp (local_commit_data) > latest_timestamp) + { + g_info ("Installed commit %s newer than sideloaded %s, ignoring", local_checksum, latest_checksum); +@@ -3669,6 +3718,7 @@ resolve_ops (FlatpakTransaction *self, + /* Otherwise, use whatever we found */ + checksum = g_steal_pointer (&latest_checksum); + sideload_path = g_steal_pointer (&latest_sideload_path); ++ image_source = g_steal_pointer (&latest_image_source); + } + } + else +@@ -3689,7 +3739,7 @@ resolve_ops (FlatpakTransaction *self, + } + + /* First try to resolve via metadata (if remote is available and its metadata matches the commit version) */ +- if (!try_resolve_op_from_metadata (self, op, checksum, sideload_path, state, &local_error)) ++ if (!try_resolve_op_from_metadata (self, op, checksum, sideload_path, image_source, state, &local_error)) + { + if (local_error) + { +@@ -3706,7 +3756,7 @@ resolve_ops (FlatpakTransaction *self, + /* OCI needs this to get the oci repository for the ref to request the token, so lets always set it here */ + if (op->summary_metadata == NULL) + flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref), +- NULL, NULL, &op->summary_metadata, NULL, NULL); ++ NULL, NULL, &op->summary_metadata, NULL, NULL, NULL); + + commit_data = flatpak_remote_state_load_ref_commit (state, priv->dir, + flatpak_decomposed_get_ref (op->ref), +@@ -3732,7 +3782,7 @@ resolve_ops (FlatpakTransaction *self, + return FALSE; + } + +- if (!resolve_op_from_commit (self, op, checksum, sideload_path, commit_data, error)) ++ if (!resolve_op_from_commit (self, op, checksum, sideload_path, image_source, commit_data, error)) + return FALSE; + } + } +diff --git a/common/flatpak-transaction.h b/common/flatpak-transaction.h +index 3d3f4ce8..dfa3d039 100644 +--- a/common/flatpak-transaction.h ++++ b/common/flatpak-transaction.h +@@ -272,6 +272,11 @@ FLATPAK_EXTERN + void flatpak_transaction_add_sideload_repo (FlatpakTransaction *self, + const char *path); + FLATPAK_EXTERN ++gboolean flatpak_transaction_add_sideload_image_collection (FlatpakTransaction *self, ++ const char *location, ++ GCancellable *cancellable, ++ GError **error); ++FLATPAK_EXTERN + void flatpak_transaction_add_default_dependency_sources (FlatpakTransaction *self); + FLATPAK_EXTERN + gboolean flatpak_transaction_run (FlatpakTransaction *transaction, +diff --git a/common/meson.build b/common/meson.build +index c14337be..37911c4c 100644 +--- a/common/meson.build ++++ b/common/meson.build +@@ -173,6 +173,7 @@ sources = [ + 'flatpak-error.c', + 'flatpak-exports.c', + 'flatpak-glib-backports.c', ++ 'flatpak-image-collection.c', + 'flatpak-image-source.c', + 'flatpak-installation.c', + 'flatpak-installed-ref.c', +diff --git a/doc/flatpak-install.xml b/doc/flatpak-install.xml +index 99aa5469..985b94b2 100644 +--- a/doc/flatpak-install.xml ++++ b/doc/flatpak-install.xml +@@ -84,6 +84,15 @@ + system-wide basis (see + flatpak1). + ++ ++ For OCI remotes, sideload repositories can also be specified as ++ or , ++ where path points to an OCI image layout or archive of an OCI image layout. ++ (See containers-transports5.) ++ When specified in this fashion, the images found in the image layout are available for all OCI remotes, ++ without regard to collection ID. For this reason, there is no directory equivalent - ++ it can only be specified on the command line. ++ + + The alternative form of the command (with , , + or ) allows to install directly from a source. The source can be a +diff --git a/doc/flatpak-update.xml b/doc/flatpak-update.xml +index feaaf278..096ee310 100644 +--- a/doc/flatpak-update.xml ++++ b/doc/flatpak-update.xml +@@ -75,6 +75,15 @@ + system-wide basis (see + flatpak1). + ++ ++ For OCI remotes, sideload repositories can also be specified as ++ or , ++ where path points to an OCI image layout or archive of an OCI image layout. ++ (See containers-transports5.) ++ When specified in this fashion, the images found in the image layout are available for all OCI remotes, ++ without regard to collection ID. For this reason, there is no directory equivalent - ++ it can only be specified on the command line. ++ + + Note that updating a runtime is different from installing + a different branch, and runtime updates are expected to keep +diff --git a/doc/reference/meson.build b/doc/reference/meson.build +index 92f1482e..0854030e 100644 +--- a/doc/reference/meson.build ++++ b/doc/reference/meson.build +@@ -46,6 +46,7 @@ gnome.gtkdoc( + 'flatpak-document-dbus-generated.h', + 'flatpak-enum-types.h', + 'flatpak-exports-private.h', ++ 'flatpak-image-collection-private.h', + 'flatpak-image-source-private.h', + 'flatpak-installed-ref-private.h', + 'flatpak-json-oci-private.h', +-- +2.47.1 + diff --git a/flatpak.spec b/flatpak.spec index 7abc0486ba7cd981a9ba162a6e23d949cba92a5e..af2f2420e2ca1085c53fe08b799cc8d0f215c250 100644 --- a/flatpak.spec +++ b/flatpak.spec @@ -1,7 +1,6 @@ %define anolis_release 1 - -%global appstream_version 0.15.3 -%global bubblewrap_version 0.8.0 +%global appstream_version 1.0.0~ +%global bubblewrap_version 0.10.0 %global glib_version 2.46.0 %global gpgme_version 1.8.0 %global libcurl_version 7.29.0 @@ -9,8 +8,11 @@ %global wayland_protocols_version 1.32 %global wayland_scanner_version 1.15 +# Disable parental control for RHEL builds +%bcond malcontent %[!0%{?rhel}] + Name: flatpak -Version: 1.15.8 +Version: 1.16.0 Release: %{anolis_release}%{?dist} Summary: Application deployment framework for desktop apps @@ -20,7 +22,32 @@ Source0: https://github.com/flatpak/flatpak/releases/download/%{version}/ # systemd-sysusers config. Only used for the %%pre macro. Must be kept in sync # with the config from upstream sources. -Source1: flatpak.sysusers.conf +Source2: flatpak.sysusers.conf + +# Implement /etc/containers/certs.d for OCI registries +# https://github.com/flatpak/flatpak/pull/5916 +Patch0: flatpak-implement-etc-containers-certs-for-oci-registries.patch +# Allow direct installation from OCI images +# https://github.com/flatpak/flatpak/pull/5972 +Patch1: flatpak-allow-direct-installation-from-oci-images.patch +# Support sideload repositories for OCI remotes +# https://github.com/owtaylor/flatpak/commits/oci-sideload +Patch2: flatpak-support-sideload-repositories-for-oci-remotes.patch +# Add support for preinstalling flatpaks +# https://github.com/flatpak/flatpak/pull/5832 +Patch3: flatpak-add-support-for-preinstalling-flatpaks.patch +# Enable collection IDs for OCI remotes +# https://github.com/flatpak/flatpak/pull/6083 +Patch4: flatpak-enable-collection-ids-for-oci-remotes.patch + +# Fix crash and installatcion of OCI images +Patch5: flatpak-pass-token-to-flatpak-image-source-new-remote.patch + +# ostree not on i686 for RHEL 10 +# https://github.com/containers/composefs/pull/229#issuecomment-1838735764 +%if 0%{?rhel} >= 10 +ExcludeArch: %{ix86} +%endif BuildRequires: pkgconfig(appstream) >= %{appstream_version} BuildRequires: pkgconfig(dconf) @@ -36,7 +63,9 @@ BuildRequires: pkgconfig(libcurl) >= %{libcurl_version} BuildRequires: pkgconfig(libsystemd) BuildRequires: pkgconfig(libxml-2.0) >= 2.4 BuildRequires: pkgconfig(libzstd) >= 0.8.1 +%if %{with malcontent} BuildRequires: pkgconfig(malcontent-0) +%endif BuildRequires: pkgconfig(ostree-1) >= %{ostree_version} BuildRequires: pkgconfig(polkit-gobject-1) BuildRequires: pkgconfig(wayland-client) @@ -63,19 +92,25 @@ BuildRequires: /usr/bin/xsltproc %{?sysusers_requires_compat} -Requires: appstream >= %{appstream_version} +Requires: appstream%{?_isa} >= %{appstream_version} Requires: bubblewrap >= %{bubblewrap_version} -Requires: glib2 >= %{glib_version} -Requires: libcurl >= %{libcurl_version} -Requires: librsvg2 -Requires: ostree-libs >= %{ostree_version} +Requires: glib2%{?_isa} >= %{glib_version} +Requires: libcurl%{?_isa} >= %{libcurl_version} +Requires: librsvg2%{?_isa} +Requires: ostree-libs%{?_isa} >= %{ostree_version} Requires: /usr/bin/fusermount3 Requires: /usr/bin/xdg-dbus-proxy +# https://fedoraproject.org/wiki/SELinux/IndependentPolicy Requires: (flatpak-selinux = %{?epoch:%{epoch}:}%{version}-%{release} if selinux-policy-targeted) -Requires: %{name}-session-helper = %{?epoch:%{epoch}:}%{version}-%{release} +Requires: %{name}-session-helper%{?_isa} = %{?epoch:%{epoch}:}%{version}-%{release} Recommends: p11-kit-server +# Make sure the document portal is installed +%if 0%{?fedora} || 0%{?rhel} > 7 Recommends: xdg-desktop-portal > 0.10 +%else +Requires: xdg-desktop-portal > 0.10 +%endif %description flatpak is a system for building, distributing and running sandboxed desktop @@ -84,8 +119,8 @@ more information. %package devel Summary: Development files for %{name} -Requires: %{name} = %{?epoch:%{epoch}:}%{version}-%{release} -Requires: %{name}-libs = %{?epoch:%{epoch}:}%{version}-%{release} +Requires: %{name}%{?_isa} = %{?epoch:%{epoch}:}%{version}-%{release} +Requires: %{name}-libs%{?_isa} = %{?epoch:%{epoch}:}%{version}-%{release} %description devel This package contains the pkg-config file and development headers for %{name}. @@ -93,7 +128,9 @@ This package contains the pkg-config file and development headers for %{name}. %package libs Summary: Libraries for %{name} Requires: bubblewrap >= %{bubblewrap_version} -Requires: ostree-libs >= %{ostree_version} +# We can assume ostree is installed on ostree systems +# So do not enforce it on non-ostree ones +Requires: ostree-libs%{?_isa} >= %{ostree_version} %description libs This package contains libflatpak. @@ -120,22 +157,15 @@ that's used by %{name} and other packages. %package tests Summary: Tests for %{name} -Requires: %{name} = %{?epoch:%{epoch}:}%{version}-%{release} -Requires: %{name}-libs = %{?epoch:%{epoch}:}%{version}-%{release} -Requires: %{name}-session-helper = %{?epoch:%{epoch}:}%{version}-%{release} +Requires: %{name}%{?_isa} = %{?epoch:%{epoch}:}%{version}-%{release} +Requires: %{name}-libs%{?_isa} = %{?epoch:%{epoch}:}%{version}-%{release} +Requires: %{name}-session-helper%{?_isa} = %{?epoch:%{epoch}:}%{version}-%{release} Requires: bubblewrap >= %{bubblewrap_version} -Requires: ostree >= %{ostree_version} +Requires: ostree%{?_isa} >= %{ostree_version} %description tests This package contains installed tests for %{name}. -%package doc -Summary: Documentation files for %{name} -Requires: %{name}-libs = %{version}-%{release} -BuildArch: noarch - -%description doc -The %{name}-doc package contains documentation files for %{name}. %prep %autosetup -p1 @@ -147,7 +177,11 @@ The %{name}-doc package contains documentation files for %{name}. -Dsystem_bubblewrap=/usr/bin/bwrap \ -Dsystem_dbus_proxy=/usr/bin/xdg-dbus-proxy \ -Dtmpfilesdir=%{_tmpfilesdir} \ +%if %{with malcontent} -Dmalcontent=enabled \ +%else + -Dmalcontent=disabled \ +%endif -Dwayland_security_context=enabled \ %{nil} %meson_build @@ -160,16 +194,38 @@ install -pm 644 NEWS README.md %{buildroot}/%{_pkgdocdir} install -d %{buildroot}%{_localstatedir}/lib/flatpak install -d %{buildroot}%{_sysconfdir}/flatpak/remotes.d +%if 0%{?fedora} +install -D -t %{buildroot}%{_unitdir} %{SOURCE1} +%endif + %find_lang %{name} %pre -%sysusers_create_compat %{SOURCE1} +%sysusers_create_compat %{SOURCE2} + + +%if 0%{?fedora} +%post +%systemd_post flatpak-add-fedora-repos.service +%endif %post selinux %selinux_modules_install %{_datadir}/selinux/packages/flatpak.pp.bz2 +%if 0%{?fedora} +%preun +%systemd_preun flatpak-add-fedora-repos.service +%endif + + +%if 0%{?fedora} +%postun +%systemd_postun_with_restart flatpak-add-fedora-repos.service +%endif + + %postun selinux if [ $1 -eq 0 ]; then %selinux_modules_uninstall %{_datadir}/selinux/packages/flatpak.pp.bz2 @@ -212,6 +268,7 @@ fi %{_sysconfdir}/dbus-1/system.d/org.freedesktop.Flatpak.SystemHelper.conf %dir %{_sysconfdir}/flatpak %{_sysconfdir}/flatpak/remotes.d +%{_sysconfdir}/profile.d/flatpak.csh %{_sysconfdir}/profile.d/flatpak.sh %{_sysusersdir}/%{name}.conf %{_unitdir}/flatpak-system-helper.service @@ -221,6 +278,10 @@ fi %{_systemd_user_env_generator_dir}/60-flatpak %{_tmpfilesdir}/%{name}.conf +%if 0%{?fedora} +%{_unitdir}/flatpak-add-fedora-repos.service +%endif + %files devel %{_datadir}/gir-1.0/Flatpak-1.0.gir %{_datadir}/gtk-doc/ @@ -248,10 +309,11 @@ fi %{_datadir}/installed-tests %{_libexecdir}/installed-tests -%files doc -%doc README.md NEWS %changelog +* Wed Feb 12 2025 Liwei Ge - 1.16.0-1 +- Sync up from CentOS upstream flatpak-1.16.0-5 + * Fri May 10 2024 mgb01105731 - 1.15.8-1 - update to 1.15.8