summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile-libostree-defines.am1
-rw-r--r--Makefile-libostree.am4
-rw-r--r--Makefile-tests.am7
-rw-r--r--Makefile.am2
-rw-r--r--apidoc/ostree-experimental-sections.txt8
-rw-r--r--src/libostree/libostree-experimental.sym2
-rw-r--r--src/libostree/ostree-autocleanups.h1
-rw-r--r--src/libostree/ostree-core-private.h3
-rw-r--r--src/libostree/ostree-repo-finder-mount.c547
-rw-r--r--src/libostree/ostree-repo-finder-mount.h55
-rw-r--r--src/libostree/ostree-repo-pull.c4
-rw-r--r--src/libostree/ostree.h1
-rw-r--r--tests/.gitignore1
-rw-r--r--tests/test-mock-gio.c400
-rw-r--r--tests/test-mock-gio.h127
-rw-r--r--tests/test-repo-finder-mount.c497
16 files changed, 1657 insertions, 3 deletions
diff --git a/Makefile-libostree-defines.am b/Makefile-libostree-defines.am
index ac5eaa00..7586f7b9 100644
--- a/Makefile-libostree-defines.am
+++ b/Makefile-libostree-defines.am
@@ -44,6 +44,7 @@ libostree_public_headers += \
src/libostree/ostree-remote.h \
src/libostree/ostree-repo-finder.h \
src/libostree/ostree-repo-finder-config.h \
+ src/libostree/ostree-repo-finder-mount.h \
$(NULL)
endif
diff --git a/Makefile-libostree.am b/Makefile-libostree.am
index 4b968abe..01aa8663 100644
--- a/Makefile-libostree.am
+++ b/Makefile-libostree.am
@@ -156,11 +156,13 @@ libostree_1_la_SOURCES += \
src/libostree/ostree-remote.h \
src/libostree/ostree-repo-finder.h \
src/libostree/ostree-repo-finder-config.h \
+ src/libostree/ostree-repo-finder-mount.h \
$(NULL)
else # if ENABLE_EXPERIMENTAL_API
libostree_1_la_SOURCES += \
src/libostree/ostree-repo-finder.c \
src/libostree/ostree-repo-finder-config.c \
+ src/libostree/ostree-repo-finder-mount.c \
$(NULL)
endif
@@ -237,7 +239,7 @@ OSTree_1_0_gir_INCLUDES = Gio-2.0
OSTree_1_0_gir_CFLAGS = $(libostree_1_la_CFLAGS)
OSTree_1_0_gir_LIBS = libostree-1.la
OSTree_1_0_gir_SCANNERFLAGS = --warn-all --identifier-prefix=Ostree --symbol-prefix=ostree
-OSTree_1_0_gir_FILES = $(libostreeinclude_HEADERS) $(filter-out %-private.h %/ostree-soup-uri.h %/ostree-repo-finder.h %/ostree-repo-finder-config.h,$(libostree_1_la_SOURCES))
+OSTree_1_0_gir_FILES = $(libostreeinclude_HEADERS) $(filter-out %-private.h %/ostree-soup-uri.h %/ostree-repo-finder.h %/ostree-repo-finder-config.h %/ostree-repo-finder-mount.h,$(libostree_1_la_SOURCES))
INTROSPECTION_GIRS += OSTree-1.0.gir
gir_DATA += OSTree-1.0.gir
typelib_DATA += OSTree-1.0.typelib
diff --git a/Makefile-tests.am b/Makefile-tests.am
index 1cdf8826..09c85818 100644
--- a/Makefile-tests.am
+++ b/Makefile-tests.am
@@ -196,6 +196,7 @@ _installed_or_uninstalled_test_programs = tests/test-varint tests/test-ot-unix-u
if ENABLE_EXPERIMENTAL_API
test_programs += \
tests/test-repo-finder-config \
+ tests/test-repo-finder-mount \
$(NULL)
endif
@@ -210,7 +211,7 @@ common_tests_cflags = $(ostree_bin_shared_cflags) $(OT_INTERNAL_GIO_UNIX_CFLAGS)
common_tests_ldadd = $(ostree_bin_shared_ldadd) $(OT_INTERNAL_GIO_UNIX_LIBS)
noinst_LTLIBRARIES += libostreetest.la
-libostreetest_la_SOURCES = tests/libostreetest.c
+libostreetest_la_SOURCES = tests/libostreetest.c tests/test-mock-gio.c tests/test-mock-gio.h
libostreetest_la_CFLAGS = $(common_tests_cflags) -I $(srcdir)/tests
libostreetest_la_LIBADD = $(common_tests_ldadd)
@@ -229,6 +230,10 @@ tests_test_repo_finder_config_SOURCES = tests/test-repo-finder-config.c
tests_test_repo_finder_config_CFLAGS = $(TESTS_CFLAGS)
tests_test_repo_finder_config_LDADD = $(TESTS_LDADD)
+tests_test_repo_finder_mount_SOURCES = tests/test-repo-finder-mount.c
+tests_test_repo_finder_mount_CFLAGS = $(TESTS_CFLAGS)
+tests_test_repo_finder_mount_LDADD = $(TESTS_LDADD)
+
tests_test_mutable_tree_CFLAGS = $(TESTS_CFLAGS)
tests_test_mutable_tree_LDADD = $(TESTS_LDADD)
diff --git a/Makefile.am b/Makefile.am
index 53b505e3..0939d4b7 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -29,7 +29,7 @@ AM_CPPFLAGS += -DDATADIR='"$(datadir)"' -DLIBEXECDIR='"$(libexecdir)"' \
-DOSTREE_COMPILATION \
-DG_LOG_DOMAIN=\"OSTree\" \
-DOSTREE_GITREV='"$(OSTREE_GITREV)"' \
- -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_40 -DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_40 \
+ -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_40 -DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_50 \
-DSOUP_VERSION_MIN_REQUIRED=SOUP_VERSION_2_40 -DSOUP_VERSION_MAX_ALLOWED=SOUP_VERSION_2_48
AM_CFLAGS += -std=gnu99 $(WARN_CFLAGS)
AM_DISTCHECK_CONFIGURE_FLAGS += \
diff --git a/apidoc/ostree-experimental-sections.txt b/apidoc/ostree-experimental-sections.txt
index 93210d56..c977b56c 100644
--- a/apidoc/ostree-experimental-sections.txt
+++ b/apidoc/ostree-experimental-sections.txt
@@ -58,6 +58,14 @@ ostree_repo_finder_config_get_type
</SECTION>
<SECTION>
+<FILE>ostree-repo-finder-mount</FILE>
+OstreeRepoFinderMount
+ostree_repo_finder_mount_new
+<SUBSECTION Standard>
+ostree_repo_finder_mount_get_type
+</SECTION>
+
+<SECTION>
<FILE>ostree-misc-experimental</FILE>
ostree_repo_get_collection_id
ostree_repo_set_collection_id
diff --git a/src/libostree/libostree-experimental.sym b/src/libostree/libostree-experimental.sym
index 79fcdbe9..e70c80bb 100644
--- a/src/libostree/libostree-experimental.sym
+++ b/src/libostree/libostree-experimental.sym
@@ -50,6 +50,8 @@ global:
ostree_repo_finder_config_get_type;
ostree_repo_finder_config_new;
ostree_repo_finder_get_type;
+ ostree_repo_finder_mount_get_type;
+ ostree_repo_finder_mount_new;
ostree_repo_finder_resolve_async;
ostree_repo_finder_resolve_all_async;
ostree_repo_finder_resolve_all_finish;
diff --git a/src/libostree/ostree-autocleanups.h b/src/libostree/ostree-autocleanups.h
index ac0aa1fb..dd3c9778 100644
--- a/src/libostree/ostree-autocleanups.h
+++ b/src/libostree/ostree-autocleanups.h
@@ -65,6 +65,7 @@ G_DEFINE_AUTO_CLEANUP_FREE_FUNC (OstreeCollectionRefv, ostree_collection_ref_fre
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRemote, ostree_remote_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinder, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderConfig, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderMount, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderResult, ostree_repo_finder_result_free)
G_DEFINE_AUTO_CLEANUP_FREE_FUNC (OstreeRepoFinderResultv, ostree_repo_finder_result_freev, NULL)
#endif /* OSTREE_ENABLE_EXPERIMENTAL_API */
diff --git a/src/libostree/ostree-core-private.h b/src/libostree/ostree-core-private.h
index 4c117110..a56fdc0b 100644
--- a/src/libostree/ostree-core-private.h
+++ b/src/libostree/ostree-core-private.h
@@ -190,6 +190,9 @@ G_DEFINE_AUTO_CLEANUP_FREE_FUNC (OstreeRepoFinderResultv, ostree_repo_finder_res
#include "ostree-repo-finder-config.h"
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderConfig, g_object_unref)
+
+#include "ostree-repo-finder-mount.h"
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderMount, g_object_unref)
#endif
G_END_DECLS
diff --git a/src/libostree/ostree-repo-finder-mount.c b/src/libostree/ostree-repo-finder-mount.c
new file mode 100644
index 00000000..ffe31e99
--- /dev/null
+++ b/src/libostree/ostree-repo-finder-mount.c
@@ -0,0 +1,547 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2017 Endless Mobile, 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.
+ *
+ * Authors:
+ * - Philip Withnall <withnall@endlessm.com>
+ */
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <libglnx.h>
+#include <stdlib.h>
+
+#include "ostree-autocleanups.h"
+#include "ostree-remote-private.h"
+#include "ostree-repo-finder.h"
+#include "ostree-repo-finder-mount.h"
+
+/**
+ * SECTION:ostree-repo-finder-mount
+ * @title: OstreeRepoFinderMount
+ * @short_description: Finds remote repositories from ref names by looking at
+ * mounted removable volumes
+ * @stability: Unstable
+ * @include: libostree/ostree-repo-finder-mount.h
+ *
+ * #OstreeRepoFinderMount is an implementation of #OstreeRepoFinder which looks
+ * refs up in well-known locations on any mounted removable volumes.
+ *
+ * For an #OstreeCollectionRef, (`C`, `R`), it checks whether `.ostree/repos/C/R`
+ * exists and is an OSTree repository on each mounted removable volume. Collection
+ * IDs and ref names are not escaped when building the path, so if either
+ * contains `/` in its name, the repository will be checked for in a
+ * subdirectory of `.ostree/repos`. Non-removable volumes are ignored.
+ *
+ * For each repository which is found, a result will be returned for the
+ * intersection of the refs being searched for, and the refs in `refs/heads` and
+ * `refs/mirrors` in the repository on the removable volume.
+ *
+ * Symlinks are followed when resolving the refs, so a volume might contain a
+ * single OSTree at some arbitrary path, with a number of refs linking to it
+ * from `.ostree/repos`. Any symlink which points outside the volume’s file
+ * system will be ignored. Repositories are deduplicated in the results.
+ *
+ * The volume monitor used to find mounted volumes can be overridden by setting
+ * #OstreeRepoFinderMount:monitor. By default, g_volume_monitor_get() is used.
+ *
+ * Since: 2017.8
+ */
+
+typedef GList/*<owned GObject>*/ ObjectList;
+
+static void
+object_list_free (ObjectList *list)
+{
+ g_list_free_full (list, g_object_unref);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (ObjectList, object_list_free)
+
+static void ostree_repo_finder_mount_iface_init (OstreeRepoFinderInterface *iface);
+
+struct _OstreeRepoFinderMount
+{
+ GObject parent_instance;
+
+ GVolumeMonitor *monitor; /* owned */
+};
+
+G_DEFINE_TYPE_WITH_CODE (OstreeRepoFinderMount, ostree_repo_finder_mount, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (OSTREE_TYPE_REPO_FINDER, ostree_repo_finder_mount_iface_init))
+
+typedef struct
+{
+ gchar *uri;
+ gchar *keyring;
+} UriAndKeyring;
+
+static void
+uri_and_keyring_free (UriAndKeyring *data)
+{
+ g_free (data->uri);
+ g_free (data->keyring);
+ g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (UriAndKeyring, uri_and_keyring_free)
+
+static UriAndKeyring *
+uri_and_keyring_new (const gchar *uri,
+ const gchar *keyring)
+{
+ g_autoptr(UriAndKeyring) data = NULL;
+
+ data = g_new0 (UriAndKeyring, 1);
+ data->uri = g_strdup (uri);
+ data->keyring = g_strdup (keyring);
+
+ return g_steal_pointer (&data);
+}
+
+static guint
+uri_and_keyring_hash (gconstpointer key)
+{
+ const UriAndKeyring *_key = key;
+
+ return g_str_hash (_key->uri) ^ g_str_hash (_key->keyring);
+}
+
+static gboolean
+uri_and_keyring_equal (gconstpointer a,
+ gconstpointer b)
+{
+ const UriAndKeyring *_a = a, *_b = b;
+
+ return g_str_equal (_a->uri, _b->uri) && g_str_equal (_a->keyring, _b->keyring);
+}
+
+/* This must return a valid remote name (suitable for use in a refspec). */
+static gchar *
+uri_and_keyring_to_name (UriAndKeyring *data)
+{
+ g_autofree gchar *escaped_uri = g_uri_escape_string (data->uri, NULL, FALSE);
+ g_autofree gchar *escaped_keyring = g_uri_escape_string (data->keyring, NULL, FALSE);
+
+ /* FIXME: Need a better separator than `_`, since it’s not escaped in the input. */
+ g_autofree gchar *out = g_strdup_printf ("%s_%s", escaped_uri, escaped_keyring);
+
+ for (gsize i = 0; out[i] != '\0'; i++)
+ {
+ if (out[i] == '%')
+ out[i] = '_';
+ }
+
+ g_return_val_if_fail (ostree_validate_remote_name (out, NULL), NULL);
+
+ return g_steal_pointer (&out);
+}
+
+static gint
+results_compare_cb (gconstpointer a,
+ gconstpointer b)
+{
+ const OstreeRepoFinderResult *result_a = *((const OstreeRepoFinderResult **) a);
+ const OstreeRepoFinderResult *result_b = *((const OstreeRepoFinderResult **) b);
+
+ return ostree_repo_finder_result_compare (result_a, result_b);
+}
+
+static void
+ostree_repo_finder_mount_resolve_async (OstreeRepoFinder *finder,
+ const OstreeCollectionRef * const *refs,
+ OstreeRepo *parent_repo,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (finder);
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(ObjectList) mounts = NULL;
+ g_autoptr(GPtrArray) results = NULL; /* (element-type OstreeRepoFinderResult) */
+ GList *l;
+ const gint priority = 50; /* arbitrarily chosen */
+
+ task = g_task_new (finder, cancellable, callback, user_data);
+ g_task_set_source_tag (task, ostree_repo_finder_mount_resolve_async);
+
+ mounts = g_volume_monitor_get_mounts (self->monitor);
+ results = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_repo_finder_result_free);
+
+ g_debug ("%s: Found %u mounts", G_STRFUNC, g_list_length (mounts));
+
+ for (l = mounts; l != NULL; l = l->next)
+ {
+ GMount *mount = G_MOUNT (l->data);
+ g_autofree gchar *mount_name = NULL;
+ g_autoptr(GFile) mount_root = NULL;
+ g_autofree gchar *mount_root_path = NULL;
+ glnx_fd_close int mount_root_dfd = -1;
+ struct stat mount_root_stbuf;
+ glnx_fd_close int repos_dfd = -1;
+ gsize i;
+ g_autoptr(GHashTable) repo_to_refs = NULL; /* (element-type UriAndKeyring GHashTable) */
+ GHashTable *supported_ref_to_checksum; /* (element-type OstreeCollectionRef utf8) */
+ GHashTableIter iter;
+ UriAndKeyring *repo;
+ g_autoptr(GError) local_error = NULL;
+
+ mount_name = g_mount_get_name (mount);
+
+ /* Check the mount’s general properties. */
+ if (g_mount_is_shadowed (mount))
+ {
+ g_debug ("Ignoring mount ‘%s’ as it’s shadowed.", mount_name);
+ continue;
+ }
+
+ /* Check if it contains a .ostree/repos directory. */
+ mount_root = g_mount_get_root (mount);
+ mount_root_path = g_file_get_path (mount_root);
+
+ if (!glnx_opendirat (AT_FDCWD, mount_root_path, TRUE, &mount_root_dfd, &local_error))
+ {
+ g_debug ("Ignoring mount ‘%s’ as ‘%s’ directory can’t be opened: %s",
+ mount_name, mount_root_path, local_error->message);
+ continue;
+ }
+
+ if (!glnx_opendirat (mount_root_dfd, ".ostree/repos", TRUE, &repos_dfd, &local_error))
+ {
+ if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ g_debug ("Ignoring mount ‘%s’ as ‘%s/.ostree/repos’ directory doesn’t exist.",
+ mount_name, mount_root_path);
+ else
+ g_debug ("Ignoring mount ‘%s’ as ‘%s/.ostree/repos’ directory can’t be opened: %s",
+ mount_name, mount_root_path, local_error->message);
+
+ continue;
+ }
+
+ /* stat() the mount root so we can later check whether the resolved
+ * repositories for individual refs are on the same device (to avoid the
+ * symlinks for them pointing outside the mount root). */
+ if (!glnx_fstat (mount_root_dfd, &mount_root_stbuf, &local_error))
+ {
+ g_debug ("Ignoring mount ‘%s’ as querying info of ‘%s’ failed: %s",
+ mount_name, mount_root_path, local_error->message);
+ continue;
+ }
+
+ /* Check whether a subdirectory exists for any of the @refs we’re looking
+ * for. If so, and it’s a symbolic link, dereference it so multiple links
+ * to the same repository (containing multiple refs) are coalesced.
+ * Otherwise, include it as a result by itself. */
+ repo_to_refs = g_hash_table_new_full (uri_and_keyring_hash, uri_and_keyring_equal,
+ (GDestroyNotify) uri_and_keyring_free, (GDestroyNotify) g_hash_table_unref);
+
+ for (i = 0; refs[i] != NULL; i++)
+ {
+ struct stat stbuf;
+ g_autofree gchar *collection_and_ref = NULL;
+ g_autofree gchar *repo_dir_path = NULL;
+ g_autofree gchar *resolved_repo_uri = NULL;
+ g_autofree gchar *keyring = NULL;
+ g_autoptr(UriAndKeyring) resolved_repo = NULL;
+
+ collection_and_ref = g_build_filename (refs[i]->collection_id, refs[i]->ref_name, NULL);
+ repo_dir_path = g_build_filename (mount_root_path, ".ostree", "repos",
+ collection_and_ref, NULL);
+
+ if (!glnx_fstatat (repos_dfd, collection_and_ref, &stbuf, AT_NO_AUTOMOUNT, &local_error))
+ {
+ g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as querying info of ‘%s’ failed: %s",
+ refs[i]->collection_id, refs[i]->ref_name, mount_name, repo_dir_path, local_error->message);
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ if ((stbuf.st_mode & S_IFMT) != S_IFDIR)
+ {
+ g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as ‘%s’ is of type %u, not a directory.",
+ refs[i]->collection_id, refs[i]->ref_name, mount_name, repo_dir_path, (stbuf.st_mode & S_IFMT));
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ /* Check the resolved repository path is below the mount point. Do not
+ * allow ref symlinks to point somewhere outside of the mounted
+ * volume. */
+ if (stbuf.st_dev != mount_root_stbuf.st_dev)
+ {
+ g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as it’s on a different file system from the mount.",
+ refs[i]->collection_id, refs[i]->ref_name, mount_name);
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ /* Exclude repositories which resolve to @parent_repo. */
+ g_autofree char *canonical_repo_dir_path = realpath (repo_dir_path, NULL);
+ g_autofree gchar *parent_repo_path = g_file_get_path (ostree_repo_get_path (parent_repo));
+ g_autofree char *canonical_parent_repo_path = realpath (parent_repo_path, NULL);
+
+ if (g_strcmp0 (canonical_repo_dir_path, canonical_parent_repo_path) == 0)
+ {
+ g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as its repository was the one we are resolving for: %s",
+ refs[i]->collection_id, refs[i]->ref_name, mount_name, canonical_parent_repo_path);
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ /* Grab the given ref and a checksum for it from the repo.
+ * FIXME: Ideally, there would be some ostree_repo_open_at() which we
+ * could use to keep the openat() chain going. See
+ * https://github.com/ostreedev/ostree/pull/820. */
+ g_autoptr(OstreeRepo) repo = NULL;
+ g_autoptr(GFile) repo_dir_file = g_file_new_for_path (repo_dir_path);
+ repo = ostree_repo_new (repo_dir_file);
+
+ if (!ostree_repo_open (repo, cancellable, &local_error))
+ {
+ g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as its repository could not be opened: %s",
+ refs[i]->collection_id, refs[i]->ref_name, mount_name, local_error->message);
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ g_autoptr(GHashTable) repo_refs = NULL; /* (element-type OstreeCollectionRef utf8) */
+
+ if (!ostree_repo_list_collection_refs (repo, refs[i]->collection_id, &repo_refs, cancellable, &local_error))
+ {
+ g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as its refs could not be listed: %s",
+ refs[i]->collection_id, refs[i]->ref_name, mount_name, local_error->message);
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ const gchar *checksum = g_hash_table_lookup (repo_refs, refs[i]);
+
+ if (checksum == NULL)
+ {
+ g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ as its repository doesn’t contain the ref.",
+ refs[i]->collection_id, refs[i]->ref_name, mount_name);
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ /* Finally, look up the GPG keyring for this ref. */
+ keyring = ostree_repo_resolve_keyring_for_collection (parent_repo, refs[i]->collection_id,
+ cancellable, &local_error);
+
+ if (keyring == NULL)
+ {
+ g_debug ("Ignoring ref (%s, %s) on mount ‘%s’ due to missing keyring: %s",
+ refs[i]->collection_id, refs[i]->ref_name, mount_name, local_error->message);
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ /* There is a valid repo at (or pointed to by)
+ * $mount_root/.ostree/repos/$refs[i]->collection_id/$refs[i]->ref_name.
+ * Add it to the results, keyed by the canonicalised repository URI
+ * to deduplicate the results. */
+ resolved_repo_uri = g_strconcat ("file://", canonical_repo_dir_path, NULL);
+ g_debug ("Resolved ref (%s, %s) on mount ‘%s’ to repo URI ‘%s’ with keyring ‘%s’.",
+ refs[i]->collection_id, refs[i]->ref_name, mount_name, resolved_repo_uri, keyring);
+
+ resolved_repo = uri_and_keyring_new (resolved_repo_uri, keyring);
+
+ supported_ref_to_checksum = g_hash_table_lookup (repo_to_refs, resolved_repo);
+
+ if (supported_ref_to_checksum == NULL)
+ {
+ supported_ref_to_checksum = g_hash_table_new_full (ostree_collection_ref_hash,
+ ostree_collection_ref_equal,
+ NULL, g_free);
+ g_hash_table_insert (repo_to_refs, g_steal_pointer (&resolved_repo), supported_ref_to_checksum /* transfer */);
+ }
+
+ g_hash_table_insert (supported_ref_to_checksum, (gpointer) refs[i], g_strdup (checksum));
+ }
+
+ /* Aggregate the results. */
+ g_hash_table_iter_init (&iter, repo_to_refs);
+
+ while (g_hash_table_iter_next (&iter, (gpointer *) &repo, (gpointer *) &supported_ref_to_checksum))
+ {
+ g_autoptr(OstreeRemote) remote = NULL;
+
+ /* Build an #OstreeRemote. Use the escaped URI, since remote->name
+ * is used in file paths, so needs to not contain special characters. */
+ g_autofree gchar *name = uri_and_keyring_to_name (repo);
+ remote = ostree_remote_new (name);
+
+ g_clear_pointer (&remote->keyring, g_free);
+ remote->keyring = g_strdup (repo->keyring);
+
+ g_key_file_set_string (remote->options, remote->group, "url", repo->uri);
+ g_key_file_set_boolean (remote->options, remote->group, "gpg-verify", TRUE);
+ g_key_file_set_boolean (remote->options, remote->group, "gpg-verify-summary", TRUE);
+
+ /* Set the timestamp in the #OstreeRepoFinderResult to 0 because
+ * the code in ostree_repo_pull_from_remotes_async() will be able to
+ * check it just as quickly as we can here; so don’t duplicate the
+ * code. */
+ g_ptr_array_add (results, ostree_repo_finder_result_new (remote, finder, priority, supported_ref_to_checksum, 0));
+ }
+ }
+
+ g_ptr_array_sort (results, results_compare_cb);
+
+ g_task_return_pointer (task, g_steal_pointer (&results), (GDestroyNotify) g_ptr_array_unref);
+}
+
+static GPtrArray *
+ostree_repo_finder_mount_resolve_finish (OstreeRepoFinder *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, self), NULL);
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ostree_repo_finder_mount_init (OstreeRepoFinderMount *self)
+{
+ /* Nothing to see here. */
+}
+
+static void
+ostree_repo_finder_mount_constructed (GObject *object)
+{
+ OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (object);
+
+ G_OBJECT_CLASS (ostree_repo_finder_mount_parent_class)->constructed (object);
+
+ if (self->monitor == NULL)
+ self->monitor = g_volume_monitor_get ();
+}
+
+typedef enum
+{
+ PROP_MONITOR = 1,
+} OstreeRepoFinderMountProperty;
+
+static void
+ostree_repo_finder_mount_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (object);
+
+ switch ((OstreeRepoFinderMountProperty) property_id)
+ {
+ case PROP_MONITOR:
+ g_value_set_object (value, self->monitor);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+ostree_repo_finder_mount_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (object);
+
+ switch ((OstreeRepoFinderMountProperty) property_id)
+ {
+ case PROP_MONITOR:
+ /* Construct-only. */
+ g_assert (self->monitor == NULL);
+ self->monitor = g_value_dup_object (value);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+ostree_repo_finder_mount_dispose (GObject *object)
+{
+ OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (object);
+
+ g_clear_object (&self->monitor);
+
+ G_OBJECT_CLASS (ostree_repo_finder_mount_parent_class)->dispose (object);
+}
+
+static void
+ostree_repo_finder_mount_class_init (OstreeRepoFinderMountClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = ostree_repo_finder_mount_get_property;
+ object_class->set_property = ostree_repo_finder_mount_set_property;
+ object_class->constructed = ostree_repo_finder_mount_constructed;
+ object_class->dispose = ostree_repo_finder_mount_dispose;
+
+ /**
+ * OstreeRepoFinderMount:monitor:
+ *
+ * Volume monitor to use to look up mounted volumes when queried.
+ *
+ * Since: 2017.8
+ */
+ g_object_class_install_property (object_class, PROP_MONITOR,
+ g_param_spec_object ("monitor",
+ "Volume Monitor",
+ "Volume monitor to use "
+ "to look up mounted "
+ "volumes when queried.",
+ G_TYPE_VOLUME_MONITOR,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+ostree_repo_finder_mount_iface_init (OstreeRepoFinderInterface *iface)
+{
+ iface->resolve_async = ostree_repo_finder_mount_resolve_async;
+ iface->resolve_finish = ostree_repo_finder_mount_resolve_finish;
+}
+
+/**
+ * ostree_repo_finder_mount_new:
+ * @monitor: (nullable) (transfer none): volume monitor to use, or %NULL to use
+ * the system default
+ *
+ * Create a new #OstreeRepoFinderMount, using the given @monitor to look up
+ * volumes. If @monitor is %NULL, the monitor from g_volume_monitor_get() will
+ * be used.
+ *
+ * Returns: (transfer full): a new #OstreeRepoFinderMount
+ * Since: 2017.8
+ */
+OstreeRepoFinderMount *
+ostree_repo_finder_mount_new (GVolumeMonitor *monitor)
+{
+ g_return_val_if_fail (monitor == NULL || G_IS_VOLUME_MONITOR (monitor), NULL);
+
+ return g_object_new (OSTREE_TYPE_REPO_FINDER_MOUNT,
+ "monitor", monitor,
+ NULL);
+}
diff --git a/src/libostree/ostree-repo-finder-mount.h b/src/libostree/ostree-repo-finder-mount.h
new file mode 100644
index 00000000..d844dd44
--- /dev/null
+++ b/src/libostree/ostree-repo-finder-mount.h
@@ -0,0 +1,55 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2017 Endless Mobile, 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.
+ *
+ * Authors:
+ * - Philip Withnall <withnall@endlessm.com>
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib-object.h>
+
+#include "ostree-repo-finder.h"
+#include "ostree-types.h"
+
+G_BEGIN_DECLS
+
+#define OSTREE_TYPE_REPO_FINDER_MOUNT (ostree_repo_finder_mount_get_type ())
+
+/* Manually expanded version of the following, omitting autoptr support (for GLib < 2.44):
+_OSTREE_PUBLIC
+G_DECLARE_FINAL_TYPE (OstreeRepoFinderMount, ostree_repo_finder_mount, OSTREE, REPO_FINDER_MOUNT, GObject) */
+
+_OSTREE_PUBLIC
+GType ostree_repo_finder_mount_get_type (void);
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+typedef struct _OstreeRepoFinderMount OstreeRepoFinderMount;
+typedef struct { GObjectClass parent_class; } OstreeRepoFinderMountClass;
+
+static inline OstreeRepoFinderMount *OSTREE_REPO_FINDER_MOUNT (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, ostree_repo_finder_mount_get_type (), OstreeRepoFinderMount); }
+static inline gboolean OSTREE_IS_REPO_FINDER_MOUNT (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, ostree_repo_finder_mount_get_type ()); }
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+_OSTREE_PUBLIC
+OstreeRepoFinderMount *ostree_repo_finder_mount_new (GVolumeMonitor *monitor);
+
+G_END_DECLS
diff --git a/src/libostree/ostree-repo-pull.c b/src/libostree/ostree-repo-pull.c
index 56e4839e..ecc9b72c 100644
--- a/src/libostree/ostree-repo-pull.c
+++ b/src/libostree/ostree-repo-pull.c
@@ -42,6 +42,7 @@
#ifdef OSTREE_ENABLE_EXPERIMENTAL_API
#include "ostree-repo-finder.h"
#include "ostree-repo-finder-config.h"
+#include "ostree-repo-finder-mount.h"
#endif /* OSTREE_ENABLE_EXPERIMENTAL_API */
#include <gio/gunixinputstream.h>
@@ -4087,6 +4088,7 @@ ostree_repo_find_remotes_async (OstreeRepo *self,
GMainContext *context;
OstreeRepoFinder *default_finders[4] = { NULL, };
g_autoptr(OstreeRepoFinder) finder_config = NULL;
+ g_autoptr(OstreeRepoFinder) finder_mount = NULL;
g_return_if_fail (OSTREE_IS_REPO (self));
g_return_if_fail (is_valid_collection_ref_array (refs));
@@ -4106,8 +4108,10 @@ ostree_repo_find_remotes_async (OstreeRepo *self,
if (finders == NULL)
{
finder_config = OSTREE_REPO_FINDER (ostree_repo_finder_config_new ());
+ finder_mount = OSTREE_REPO_FINDER (ostree_repo_finder_mount_new (NULL));
default_finders[0] = finder_config;
+ default_finders[1] = finder_mount;
finders = default_finders;
}
diff --git a/src/libostree/ostree.h b/src/libostree/ostree.h
index 8d547041..0fe2a23e 100644
--- a/src/libostree/ostree.h
+++ b/src/libostree/ostree.h
@@ -39,6 +39,7 @@
#include <ostree-ref.h>
#include <ostree-repo-finder.h>
#include <ostree-repo-finder-config.h>
+#include <ostree-repo-finder-mount.h>
#endif /* OSTREE_ENABLE_EXPERIMENTAL_API */
#include <ostree-autocleanups.h>
diff --git a/tests/.gitignore b/tests/.gitignore
index bf31dd01..f3bdb177 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -15,4 +15,5 @@ test-ot-opt-utils
test-ot-tool-util
test-ot-unix-utils
test-repo-finder-config
+test-repo-finder-mount
test-rollsum-cli
diff --git a/tests/test-mock-gio.c b/tests/test-mock-gio.c
new file mode 100644
index 00000000..f6d4f2a8
--- /dev/null
+++ b/tests/test-mock-gio.c
@@ -0,0 +1,400 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2017 Endless Mobile, 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.
+ *
+ * Authors:
+ * - Philip Withnall <withnall@endlessm.com>
+ */
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <libglnx.h>
+
+#include "test-mock-gio.h"
+
+/**
+ * SECTION:mock-gio
+ * @title: Mock GIO volume interfaces
+ * @short_description: Mock implementations of GIO volume, mount and drive
+ * interfaces
+ * @stability: Unstable
+ * @include: tests/test-mock-gio.h
+ *
+ * A set of classes implementing GIO interfaces for volumes, mounts, drives
+ * and volume monitoring, which return mock data to the caller when used. These
+ * are designed for use in unit tests, to mock up removable drives when testing
+ * code which monitors such drives being added and removed and then queries
+ * properties of them.
+ *
+ * By returning mock drive locations to the caller, for example, the contents of
+ * a removable drive may be mocked up using temporary files.
+ *
+ * Currently, all the mock data returned by these classes to callers is static,
+ * set at construction time.
+ *
+ * Since: 2017.8
+ */
+
+/* Mock volume monitor class. This returns a static set of data to the caller,
+ * which it was initialised with. */
+struct _OstreeMockVolumeMonitor
+{
+ GVolumeMonitor parent_instance;
+
+ GList *mounts; /* (element-type OstreeMockMount) */
+ GList *volumes; /* (element-type OstreeMockVolume) */
+};
+
+G_DEFINE_TYPE (OstreeMockVolumeMonitor, ostree_mock_volume_monitor, G_TYPE_VOLUME_MONITOR)
+
+static GList *
+ostree_mock_volume_monitor_get_mounts (GVolumeMonitor *monitor)
+{
+ OstreeMockVolumeMonitor *self = OSTREE_MOCK_VOLUME_MONITOR (monitor);
+ return g_list_copy_deep (self->mounts, (GCopyFunc) g_object_ref, NULL);
+}
+
+static GList *
+ostree_mock_volume_monitor_get_volumes (GVolumeMonitor *monitor)
+{
+ OstreeMockVolumeMonitor *self = OSTREE_MOCK_VOLUME_MONITOR (monitor);
+ return g_list_copy_deep (self->volumes, (GCopyFunc) g_object_ref, NULL);
+}
+
+static void
+ostree_mock_volume_monitor_init (OstreeMockVolumeMonitor *self)
+{
+ /* Nothing to see here. */
+}
+
+static void
+ostree_mock_volume_monitor_dispose (GObject *object)
+{
+ OstreeMockVolumeMonitor *self = OSTREE_MOCK_VOLUME_MONITOR (object);
+
+ g_list_free_full (self->volumes, g_object_unref);
+ self->volumes = NULL;
+
+ g_list_free_full (self->mounts, g_object_unref);
+ self->mounts = NULL;
+
+ G_OBJECT_CLASS (ostree_mock_volume_monitor_parent_class)->dispose (object);
+}
+
+static void
+ostree_mock_volume_monitor_class_init (OstreeMockVolumeMonitorClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GVolumeMonitorClass *monitor_class = G_VOLUME_MONITOR_CLASS (klass);
+
+ object_class->dispose = ostree_mock_volume_monitor_dispose;
+
+ monitor_class->get_mounts = ostree_mock_volume_monitor_get_mounts;
+ monitor_class->get_volumes = ostree_mock_volume_monitor_get_volumes;
+}
+
+/**
+ * ostree_mock_volume_monitor_new:
+ * @mounts: (element-type GMount) (transfer none): list of current #GMounts
+ * @volumes: (element-type GVolume) (transfer none): list of current #GVolumes
+ *
+ * Create a new mock #GVolumeMonitor which will return the given static lists of
+ * #GMounts and #GVolumes to any caller of g_volume_monitor_get_mounts() or
+ * g_volume_monitor_get_volumes().
+ *
+ * Typically, the elements of @mounts will be #OstreeMockMount objects and the
+ * elements of @volumes will be #OstreeMockVolume objects; but this does not
+ * have to be the case.
+ *
+ * Returns: (transfer full): a new #GVolumeMonitor object
+ * Since: 2017.8
+ */
+GVolumeMonitor *
+ostree_mock_volume_monitor_new (GList *mounts,
+ GList *volumes)
+{
+ g_autoptr(OstreeMockVolumeMonitor) monitor = NULL;
+
+ monitor = g_object_new (OSTREE_TYPE_MOCK_VOLUME_MONITOR, NULL);
+ monitor->mounts = g_list_copy_deep (mounts, (GCopyFunc) g_object_ref, NULL);
+ monitor->volumes = g_list_copy_deep (volumes, (GCopyFunc) g_object_ref, NULL);
+
+ return g_steal_pointer (&monitor);
+}
+
+/* Mock volume class. This returns a static set of data to the caller, which it
+ * was initialised with. */
+struct _OstreeMockVolume
+{
+ GObject parent_instance;
+
+ gchar *name;
+ GDrive *drive; /* (owned) (nullable) */
+ GMount *mount; /* (owned) (nullable) */
+};
+
+static void ostree_mock_volume_iface_init (GVolumeIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (OstreeMockVolume, ostree_mock_volume, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_VOLUME, ostree_mock_volume_iface_init))
+
+static gchar *
+ostree_mock_volume_get_name (GVolume *volume)
+{
+ OstreeMockVolume *self = OSTREE_MOCK_VOLUME (volume);
+ return g_strdup (self->name);
+}
+
+static GDrive *
+ostree_mock_volume_get_drive (GVolume *volume)
+{
+ OstreeMockVolume *self = OSTREE_MOCK_VOLUME (volume);
+ return (self->drive != NULL) ? g_object_ref (self->drive) : NULL;
+}
+
+static GMount *
+ostree_mock_volume_get_mount (GVolume *volume)
+{
+ OstreeMockVolume *self = OSTREE_MOCK_VOLUME (volume);
+ return (self->mount != NULL) ? g_object_ref (self->mount) : NULL;
+}
+
+static void
+ostree_mock_volume_init (OstreeMockVolume *self)
+{
+ /* Nothing to see here. */
+}
+
+static void
+ostree_mock_volume_dispose (GObject *object)
+{
+ OstreeMockVolume *self = OSTREE_MOCK_VOLUME (object);
+
+ g_clear_pointer (&self->name, g_free);
+ g_clear_object (&self->drive);
+ g_clear_object (&self->mount);
+
+ G_OBJECT_CLASS (ostree_mock_volume_parent_class)->dispose (object);
+}
+
+static void
+ostree_mock_volume_class_init (OstreeMockVolumeClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = ostree_mock_volume_dispose;
+}
+
+static void
+ostree_mock_volume_iface_init (GVolumeIface *iface)
+{
+ iface->get_name = ostree_mock_volume_get_name;
+ iface->get_drive = ostree_mock_volume_get_drive;
+ iface->get_mount = ostree_mock_volume_get_mount;
+}
+
+/**
+ * ostree_mock_volume_new:
+ * @name: volume name
+ * @drive: (transfer none) (nullable): drive for the volume, or %NULL if none
+ * should be associated
+ * @mount: (transfer none) (nullable): mount for the volume, or %NULL if it’s
+ * not mounted
+ *
+ * Create a new mock #GVolume which will return the given static @name, @drive
+ * and @mount to any caller of its getter methods. There is currently no
+ * provision for changing these values dynamically. There is also currently no
+ * provision for mocking the other getters of #GVolume.
+ *
+ * Typically, @drive will be an #OstreeMockDrive object and @mount will be an
+ * #OstreeMockMount object; but this does not have to be the case.
+ *
+ * Returns: (transfer full): a new #GVolume object
+ * Since: 2017.8
+ */
+OstreeMockVolume *
+ostree_mock_volume_new (const gchar *name,
+ GDrive *drive,
+ GMount *mount)
+{
+ g_autoptr(OstreeMockVolume) volume = NULL;
+
+ volume = g_object_new (OSTREE_TYPE_MOCK_VOLUME, NULL);
+ volume->name = g_strdup (name);
+ volume->drive = (drive != NULL) ? g_object_ref (drive) : NULL;
+ volume->mount = (mount != NULL) ? g_object_ref (mount) : NULL;
+
+ return g_steal_pointer (&volume);
+}
+
+/* Mock drive class. This returns a static set of data to the caller, which it
+ * was initialised with. */
+struct _OstreeMockDrive
+{
+ GObject parent_instance;
+
+ gboolean is_removable;
+};
+
+static void ostree_mock_drive_iface_init (GDriveIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (OstreeMockDrive, ostree_mock_drive, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_DRIVE, ostree_mock_drive_iface_init))
+
+#if GLIB_CHECK_VERSION(2, 50, 0)
+static gboolean
+ostree_mock_drive_is_removable (GDrive *drive)
+{
+ OstreeMockDrive *self = OSTREE_MOCK_DRIVE (drive);
+ return self->is_removable;
+}
+#endif
+
+static void
+ostree_mock_drive_init (OstreeMockDrive *self)
+{
+ /* Nothing to see here. */
+}
+
+static void
+ostree_mock_drive_class_init (OstreeMockDriveClass *klass)
+{
+ /* Nothing to see here. */
+}
+
+static void
+ostree_mock_drive_iface_init (GDriveIface *iface)
+{
+#if GLIB_CHECK_VERSION(2, 50, 0)
+ iface->is_removable = ostree_mock_drive_is_removable;
+#endif
+}
+
+/**
+ * ostree_mock_drive_new:
+ * @is_removable: %TRUE if the drive is removable; %FALSE otherwise
+ *
+ * Create a new mock #GDrive which will return the given static @is_removable to
+ * any caller of its getter methods. There is currently no provision for mocking
+ * the other getters of #GDrive.
+ *
+ * Returns: (transfer full): a new #GDrive object
+ * Since: 2017.8
+ */
+OstreeMockDrive *
+ostree_mock_drive_new (gboolean is_removable)
+{
+ g_autoptr(OstreeMockDrive) drive = NULL;
+
+ drive = g_object_new (OSTREE_TYPE_MOCK_DRIVE, NULL);
+ drive->is_removable = is_removable;
+
+ return g_steal_pointer (&drive);
+}
+
+/* Mock mount class. This returns a static set of data to the caller, which it
+ * was initialised with. */
+struct _OstreeMockMount
+{
+ GObject parent_instance;
+
+ gchar *name; /* (owned) */
+ GFile *root; /* (owned) */
+};
+
+static void ostree_mock_mount_iface_init (GMountIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (OstreeMockMount, ostree_mock_mount, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_MOUNT, ostree_mock_mount_iface_init))
+
+static gchar *
+ostree_mock_mount_get_name (GMount *mount)
+{
+ OstreeMockMount *self = OSTREE_MOCK_MOUNT (mount);
+ return g_strdup (self->name);
+}
+
+static GFile *
+ostree_mock_mount_get_root (GMount *mount)
+{
+ OstreeMockMount *self = OSTREE_MOCK_MOUNT (mount);
+ return g_object_ref (self->root);
+}
+
+static void
+ostree_mock_mount_init (OstreeMockMount *self)
+{
+ /* Nothing to see here. */
+}
+
+static void
+ostree_mock_mount_dispose (GObject *object)
+{
+ OstreeMockMount *self = OSTREE_MOCK_MOUNT (object);
+
+ g_clear_pointer (&self->name, g_free);
+ g_clear_object (&self->root);
+
+ G_OBJECT_CLASS (ostree_mock_mount_parent_class)->dispose (object);
+}
+
+static void
+ostree_mock_mount_class_init (OstreeMockMountClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = ostree_mock_mount_dispose;
+}
+
+static void
+ostree_mock_mount_iface_init (GMountIface *iface)
+{
+ iface->get_name = ostree_mock_mount_get_name;
+ iface->get_root = ostree_mock_mount_get_root;
+}
+
+/**
+ * ostree_mock_mount_new:
+ * @name: mount name
+ * @root: (transfer none): root path for the mounted file system
+ *
+ * Create a new mock #GMount which will return the given static @name and @root
+ * to any caller of its getter methods. There is currently no provision for
+ * mocking the other getters of #GMount.
+ *
+ * Typically, @root will point to a temporary directory where a mocked file
+ * system is present; but this does not have to be the case.
+ *
+ * Returns: (transfer full): a new #GMount object
+ * Since: 2017.8
+ */
+OstreeMockMount *
+ostree_mock_mount_new (const gchar *name,
+ GFile *root)
+{
+ g_autoptr(OstreeMockMount) mount = NULL;
+
+ mount = g_object_new (OSTREE_TYPE_MOCK_MOUNT, NULL);
+ mount->name = g_strdup (name);
+ mount->root = g_object_ref (root);
+
+ return g_steal_pointer (&mount);
+}
diff --git a/tests/test-mock-gio.h b/tests/test-mock-gio.h
new file mode 100644
index 00000000..9f544759
--- /dev/null
+++ b/tests/test-mock-gio.h
@@ -0,0 +1,127 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2017 Endless Mobile, 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.
+ *
+ * Authors:
+ * - Philip Withnall <withnall@endlessm.com>
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <libglnx.h>
+
+#include "ostree-types.h"
+
+G_BEGIN_DECLS
+
+#define OSTREE_TYPE_MOCK_VOLUME_MONITOR (ostree_mock_volume_monitor_get_type ())
+
+/* Manually expanded version of the following, omitting autoptr support (for GLib < 2.44):
+G_GNUC_INTERNAL
+G_DECLARE_FINAL_TYPE (OstreeMockVolumeMonitor, ostree_mock_volume_monitor, OSTREE, MOCK_VOLUME_MONITOR, GVolumeMonitor) */
+
+G_GNUC_INTERNAL
+GType ostree_mock_volume_monitor_get_type (void);
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+typedef struct _OstreeMockVolumeMonitor OstreeMockVolumeMonitor;
+typedef struct { GVolumeMonitorClass parent_class; } OstreeMockVolumeMonitorClass;
+
+static inline OstreeMockVolumeMonitor *OSTREE_MOCK_VOLUME_MONITOR (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, ostree_mock_volume_monitor_get_type (), OstreeMockVolumeMonitor); }
+static inline gboolean OSTREE_IS_MOCK_VOLUME_MONITOR (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, ostree_mock_volume_monitor_get_type ()); }
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeMockVolumeMonitor, g_object_unref)
+
+G_GNUC_INTERNAL
+GVolumeMonitor *ostree_mock_volume_monitor_new (GList *mounts,
+ GList *volumes);
+
+#define OSTREE_TYPE_MOCK_VOLUME (ostree_mock_volume_get_type ())
+
+/* Manually expanded version of the following, omitting autoptr support (for GLib < 2.44):
+G_GNUC_INTERNAL
+G_DECLARE_FINAL_TYPE (OstreeMockVolume, ostree_mock_volume, OSTREE, MOCK_VOLUME, GObject) */
+
+G_GNUC_INTERNAL
+GType ostree_mock_volume_get_type (void);
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+typedef struct _OstreeMockVolume OstreeMockVolume;
+typedef struct { GObjectClass parent_class; } OstreeMockVolumeClass;
+
+static inline OstreeMockVolume *OSTREE_MOCK_VOLUME (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, ostree_mock_volume_get_type (), OstreeMockVolume); }
+static inline gboolean OSTREE_IS_MOCK_VOLUME (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, ostree_mock_volume_get_type ()); }
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeMockVolume, g_object_unref)
+
+G_GNUC_INTERNAL
+OstreeMockVolume *ostree_mock_volume_new (const gchar *name,
+ GDrive *drive,
+ GMount *mount);
+
+#define OSTREE_TYPE_MOCK_DRIVE (ostree_mock_drive_get_type ())
+
+/* Manually expanded version of the following, omitting autoptr support (for GLib < 2.44):
+G_GNUC_INTERNAL
+G_DECLARE_FINAL_TYPE (OstreeMockDrive, ostree_mock_drive, OSTREE, MOCK_DRIVE, GObject) */
+
+G_GNUC_INTERNAL
+GType ostree_mock_drive_get_type (void);
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+typedef struct _OstreeMockDrive OstreeMockDrive;
+typedef struct { GObjectClass parent_class; } OstreeMockDriveClass;
+
+static inline OstreeMockDrive *OSTREE_MOCK_DRIVE (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, ostree_mock_drive_get_type (), OstreeMockDrive); }
+static inline gboolean OSTREE_IS_MOCK_DRIVE (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, ostree_mock_drive_get_type ()); }
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeMockDrive, g_object_unref)
+
+G_GNUC_INTERNAL
+OstreeMockDrive *ostree_mock_drive_new (gboolean is_removable);
+
+#define OSTREE_TYPE_MOCK_MOUNT (ostree_mock_mount_get_type ())
+
+/* Manually expanded version of the following, omitting autoptr support (for GLib < 2.44):
+G_GNUC_INTERNAL
+G_DECLARE_FINAL_TYPE (OstreeMockMount, ostree_mock_mount, OSTREE, MOCK_MOUNT, GObject) */
+
+G_GNUC_INTERNAL
+GType ostree_mock_mount_get_type (void);
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+typedef struct _OstreeMockMount OstreeMockMount;
+typedef struct { GObjectClass parent_class; } OstreeMockMountClass;
+
+static inline OstreeMockMount *OSTREE_MOCK_MOUNT (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, ostree_mock_mount_get_type (), OstreeMockMount); }
+static inline gboolean OSTREE_IS_MOCK_MOUNT (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, ostree_mock_mount_get_type ()); }
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeMockMount, g_object_unref)
+
+G_GNUC_INTERNAL
+OstreeMockMount *ostree_mock_mount_new (const gchar *name,
+ GFile *root);
+
+G_END_DECLS
diff --git a/tests/test-repo-finder-mount.c b/tests/test-repo-finder-mount.c
new file mode 100644
index 00000000..c84feb52
--- /dev/null
+++ b/tests/test-repo-finder-mount.c
@@ -0,0 +1,497 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright © 2017 Endless Mobile, 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.
+ *
+ * Authors:
+ * - Philip Withnall <withnall@endlessm.com>
+ */
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <locale.h>
+
+#include "libostreetest.h"
+#include "ostree-autocleanups.h"
+#include "ostree-remote-private.h"
+#include "ostree-repo-finder.h"
+#include "ostree-repo-finder-mount.h"
+#include "ostree-types.h"
+#include "test-mock-gio.h"
+
+/* Test fixture. Creates a temporary directory and repository. */
+typedef struct
+{
+ OstreeRepo *parent_repo;
+ int working_dfd; /* owned */
+ GFile *working_dir; /* owned */
+} Fixture;
+
+static void
+setup (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_autofree gchar *tmp_name = NULL;
+ g_autoptr(GError) error = NULL;
+
+ tmp_name = g_strdup ("test-repo-finder-mount-XXXXXX");
+ glnx_mkdtempat_open_in_system (tmp_name, 0700, &fixture->working_dfd, &error);
+ g_assert_no_error (error);
+
+ g_test_message ("Using temporary directory: %s", tmp_name);
+
+ glnx_shutil_mkdir_p_at (fixture->working_dfd, "repo", 0700, NULL, &error);
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+ g_clear_error (&error);
+ g_assert_no_error (error);
+
+ g_autoptr(GFile) tmp_dir = g_file_new_for_path (g_get_tmp_dir ());
+ fixture->working_dir = g_file_get_child (tmp_dir, tmp_name);
+
+ fixture->parent_repo = ot_test_setup_repo (NULL, &error);
+ g_assert_no_error (error);
+}
+
+static void
+teardown (Fixture *fixture,
+ gconstpointer test_data)
+{
+ glnx_fd_close int parent_repo_dfd = -1;
+ g_autoptr(GError) error = NULL;
+
+ /* Recursively remove the temporary directory. */
+ glnx_shutil_rm_rf_at (fixture->working_dfd, ".", NULL, NULL);
+
+ close (fixture->working_dfd);
+ fixture->working_dfd = -1;
+
+ /* The repo also needs its source files to be removed. This is the inverse
+ * of setup_test_repository() in libtest.sh. */
+ g_autofree gchar *parent_repo_path = g_file_get_path (ostree_repo_get_path (fixture->parent_repo));
+ glnx_opendirat (-1, parent_repo_path, TRUE, &parent_repo_dfd, &error);
+ g_assert_no_error (error);
+
+ glnx_shutil_rm_rf_at (parent_repo_dfd, "../files", NULL, NULL);
+ glnx_shutil_rm_rf_at (parent_repo_dfd, "../repo", NULL, NULL);
+
+ g_clear_object (&fixture->working_dir);
+ g_clear_object (&fixture->parent_repo);
+}
+
+/* Test the object constructor works at a basic level. */
+static void
+test_repo_finder_mount_init (void)
+{
+ g_autoptr(OstreeRepoFinderMount) finder = NULL;
+ g_autoptr(GVolumeMonitor) monitor = NULL;
+
+ /* Default #GVolumeMonitor. */
+ finder = ostree_repo_finder_mount_new (NULL);
+ g_clear_object (&finder);
+
+ /* Explicit #GVolumeMonitor. */
+ monitor = ostree_mock_volume_monitor_new (NULL, NULL);
+ finder = ostree_repo_finder_mount_new (monitor);
+ g_clear_object (&finder);
+}
+
+static void
+result_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **result_out = user_data;
+ *result_out = g_object_ref (result);
+}
+
+/* Test that no remotes are found if the #GVolumeMonitor returns no mounts. */
+static void
+test_repo_finder_mount_no_mounts (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_autoptr(OstreeRepoFinderMount) finder = NULL;
+ g_autoptr(GVolumeMonitor) monitor = NULL;
+ g_autoptr(GMainContext) context = NULL;
+ g_autoptr(GAsyncResult) result = NULL;
+ g_autoptr(GPtrArray) results = NULL; /* (element-type OstreeRepoFinderResult) */
+ g_autoptr(GError) error = NULL;
+ const OstreeCollectionRef ref1 = { "org.example.Collection1", "exampleos/x86_64/standard" };
+ const OstreeCollectionRef ref2 = { "org.example.Collection1", "exampleos/x86_64/buildmaster/standard" };
+ const OstreeCollectionRef ref3 = { "org.example.Collection2", "exampleos/x86_64/standard" };
+ const OstreeCollectionRef ref4 = { "org.example.Collection2", "exampleos/arm64/standard" };
+ const OstreeCollectionRef * const refs[] = { &ref1, &ref2, &ref3, &ref4, NULL };
+
+ context = g_main_context_new ();
+ g_main_context_push_thread_default (context);
+
+ monitor = ostree_mock_volume_monitor_new (NULL, NULL);
+ finder = ostree_repo_finder_mount_new (monitor);
+
+ ostree_repo_finder_resolve_async (OSTREE_REPO_FINDER (finder), refs,
+ fixture->parent_repo,
+ NULL, result_cb, &result);
+
+ while (result == NULL)
+ g_main_context_iteration (context, TRUE);
+
+ results = ostree_repo_finder_resolve_finish (OSTREE_REPO_FINDER (finder),
+ result, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (results);
+ g_assert_cmpuint (results->len, ==, 0);
+
+ g_main_context_pop_thread_default (context);
+}
+
+/* Create a .ostree/repos directory under the given @mount_root, or abort. */
+static gboolean
+assert_create_repos_dir (Fixture *fixture,
+ const gchar *mount_root_name,
+ int *out_repos_dfd,
+ GMount **out_mount)
+{
+ glnx_fd_close int repos_dfd = -1;
+ g_autoptr(GError) error = NULL;
+
+ g_autofree gchar *path = g_build_filename (mount_root_name, ".ostree", "repos", NULL);
+ glnx_shutil_mkdir_p_at_open (fixture->working_dfd, path, 0700, &repos_dfd, NULL, &error);
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+ g_clear_error (&error);
+ g_assert_no_error (error);
+
+ *out_repos_dfd = glnx_steal_fd (&repos_dfd);
+ g_autoptr(GFile) mount_root = g_file_get_child (fixture->working_dir, mount_root_name);
+ *out_mount = G_MOUNT (ostree_mock_mount_new (mount_root_name, mount_root));
+
+ return TRUE;
+}
+
+/* Create a new repository in @repo_dir with its collection ID unset, and
+ * containing the refs given in @... (which must be %NULL-terminated). Each
+ * #OstreeCollectionRef in @... is followed by a gchar** return address for the
+ * checksum committed for that ref. Return the new repository. */
+static OstreeRepo *
+assert_create_remote_va (Fixture *fixture,
+ GFile *repo_dir,
+ va_list args)
+{
+ g_autoptr(GError) error = NULL;
+
+ g_autoptr(OstreeRepo) repo = ostree_repo_new (repo_dir);
+ ostree_repo_create (repo, OSTREE_REPO_MODE_ARCHIVE_Z2, NULL, &error);
+ g_assert_no_error (error);
+
+ /* Set up the refs from @.... */
+ for (const OstreeCollectionRef *ref = va_arg (args, const OstreeCollectionRef *);
+ ref != NULL;
+ ref = va_arg (args, const OstreeCollectionRef *))
+ {
+ g_autofree gchar *checksum = NULL;
+ g_autoptr(OstreeMutableTree) mtree = NULL;
+ g_autoptr(OstreeRepoFile) repo_file = NULL;
+ gchar **out_checksum = va_arg (args, gchar **);
+
+ mtree = ostree_mutable_tree_new ();
+ ostree_repo_write_dfd_to_mtree (repo, AT_FDCWD, ".", mtree, NULL, NULL, &error);
+ g_assert_no_error (error);
+ ostree_repo_write_mtree (repo, mtree, (GFile **) &repo_file, NULL, &error);
+ g_assert_no_error (error);
+
+ ostree_repo_write_commit (repo, NULL /* no parent */, ref->ref_name, ref->ref_name,
+ NULL /* no metadata */, repo_file, &checksum,
+ NULL, &error);
+ g_assert_no_error (error);
+
+ if (ref->collection_id != NULL)
+ ostree_repo_set_collection_ref_immediate (repo, ref, checksum, NULL, &error);
+ else
+ ostree_repo_set_ref_immediate (repo, NULL, ref->ref_name, checksum, NULL, &error);
+ g_assert_no_error (error);
+
+ if (out_checksum != NULL)
+ *out_checksum = g_steal_pointer (&checksum);
+ }
+
+ /* Update the summary. */
+ ostree_repo_regenerate_summary (repo, NULL /* no metadata */, NULL, &error);
+ g_assert_no_error (error);
+
+ return g_steal_pointer (&repo);
+}
+
+static OstreeRepo *
+assert_create_repo_dir (Fixture *fixture,
+ int repos_dfd,
+ GMount *repos_mount,
+ const OstreeCollectionRef *ref,
+ gchar **out_uri,
+ ...) G_GNUC_NULL_TERMINATED;
+
+/* Create a @ref directory under the given @repos_dfd, or abort. Create a new
+ * repository in it with the refs given in @..., as per assert_create_remote_va().
+ * Return the URI of the repository. */
+static OstreeRepo *
+assert_create_repo_dir (Fixture *fixture,
+ int repos_dfd,
+ GMount *repos_mount,
+ const OstreeCollectionRef *ref,
+ gchar **out_uri,
+ ...)
+{
+ glnx_fd_close int ref_dfd = -1;
+ g_autoptr(OstreeRepo) repo = NULL;
+ g_autoptr(GError) error = NULL;
+ va_list args;
+
+ g_autofree gchar *path = g_build_filename (ref->collection_id, ref->ref_name, NULL);
+ glnx_shutil_mkdir_p_at_open (repos_dfd, path, 0700, &ref_dfd, NULL, &error);
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+ g_clear_error (&error);
+ g_assert_no_error (error);
+
+ g_autoptr(GFile) mount_root = g_mount_get_root (repos_mount);
+ g_autoptr(GFile) repos_dir = g_file_get_child (mount_root, ".ostree/repos");
+ g_autoptr(GFile) repo_dir = g_file_get_child (repos_dir, path);
+
+ va_start (args, out_uri);
+ repo = assert_create_remote_va (fixture, repo_dir, args);
+ va_end (args);
+
+ *out_uri = g_file_get_uri (repo_dir);
+
+ return g_steal_pointer (&repo);
+}
+
+/* Create a @ref symlink under the given @repos_dfd, pointing to
+ * @symlink_target, or abort. */
+static int
+assert_create_repo_symlink (int repos_dfd,
+ const OstreeCollectionRef *ref,
+ const gchar *symlink_target_path)
+{
+ glnx_fd_close int symlink_target_dfd = -1;
+ g_autoptr(GError) error = NULL;
+
+ /* The @ref_parent_dir is not necessarily @collection_dir, since @ref may
+ * contain slashes. */
+ g_autofree gchar *path = g_build_filename (ref->collection_id, ref->ref_name, NULL);
+ g_autofree gchar *path_parent = g_path_get_dirname (path);
+
+ glnx_shutil_mkdir_p_at (repos_dfd, path_parent, 0700, NULL, &error);
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+ g_clear_error (&error);
+ g_assert_no_error (error);
+
+ if (TEMP_FAILURE_RETRY (symlinkat (symlink_target_path, repos_dfd, path)) != 0)
+ {
+ g_autoptr(GError) error = NULL;
+ glnx_throw_errno_prefix (&error, "symlinkat");
+ g_assert_no_error (error);
+ }
+
+ /* Return a dir FD for the symlink target. */
+ glnx_opendirat (repos_dfd, path, TRUE, &symlink_target_dfd, &error);
+ g_assert_no_error (error);
+
+ return glnx_steal_fd (&symlink_target_dfd);
+}
+
+/* Add configuration for a remote named @remote_name, at @remote_uri, with a
+ * remote collection ID of @collection_id, to the given @repo. */
+static void
+assert_create_remote_config (OstreeRepo *repo,
+ const gchar *remote_name,
+ const gchar *remote_uri,
+ const gchar *collection_id)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) options = NULL;
+
+ if (collection_id != NULL)
+ options = g_variant_new_parsed ("@a{sv} { 'collection-id': <%s> }",
+ collection_id);
+
+ ostree_repo_remote_add (repo, remote_name, remote_uri, options, NULL, &error);
+ g_assert_no_error (error);
+}
+
+/* Test resolving the refs against a collection of mock volumes, some of which
+ * are mounted, some of which are removable, some of which contain valid or
+ * invalid repo information on the file system, etc. */
+static void
+test_repo_finder_mount_mixed_mounts (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_autoptr(OstreeRepoFinderMount) finder = NULL;
+ g_autoptr(GVolumeMonitor) monitor = NULL;
+ g_autoptr(GMainContext) context = NULL;
+ g_autoptr(GAsyncResult) result = NULL;
+ g_autoptr(GPtrArray) results = NULL; /* (element-type OstreeRepoFinderResult) */
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GList) mounts = NULL; /* (element-type OstreeMockMount) */
+ g_autoptr(GMount) non_removable_mount = NULL;
+ g_autoptr(GMount) no_repos_mount = NULL;
+ g_autoptr(GMount) repo1_mount = NULL;
+ g_autoptr(GMount) repo2_mount = NULL;
+ g_autoptr(GFile) non_removable_root = NULL;
+ glnx_fd_close int no_repos_repos = -1;
+ glnx_fd_close int repo1_repos = -1;
+ glnx_fd_close int repo2_repos = -1;
+ g_autoptr(OstreeRepo) repo1_repo_a = NULL, repo1_repo_b = NULL;
+ g_autoptr(OstreeRepo) repo2_repo_a = NULL;
+ g_autofree gchar *repo1_repo_a_uri = NULL, *repo1_repo_b_uri = NULL;
+ g_autofree gchar *repo2_repo_a_uri = NULL;
+ g_autofree gchar *repo1_ref0_checksum = NULL, *repo1_ref1_checksum = NULL, *repo1_ref2_checksum = NULL;
+ g_autofree gchar *repo2_ref0_checksum = NULL, *repo2_ref1_checksum = NULL, *repo2_ref2_checksum = NULL;
+ g_autofree gchar *repo1_ref5_checksum = NULL;
+ gsize i;
+ const OstreeCollectionRef ref0 = { "org.example.Collection1", "exampleos/x86_64/ref0" };
+ const OstreeCollectionRef ref1 = { "org.example.Collection1", "exampleos/x86_64/ref1" };
+ const OstreeCollectionRef ref2 = { "org.example.Collection1", "exampleos/x86_64/ref2" };
+ const OstreeCollectionRef ref3 = { "org.example.Collection1", "exampleos/x86_64/ref3" };
+ const OstreeCollectionRef ref4 = { "org.example.UnconfiguredCollection", "exampleos/x86_64/ref4" };
+ const OstreeCollectionRef ref5 = { "org.example.Collection3", "exampleos/x86_64/ref0" };
+ const OstreeCollectionRef * const refs[] = { &ref0, &ref1, &ref2, &ref3, &ref4, &ref5, NULL };
+
+ context = g_main_context_new ();
+ g_main_context_push_thread_default (context);
+
+ /* Build the various mock drives/volumes/mounts, and some repositories with
+ * refs within them. We use "/" under the assumption that it’s on a separate
+ * file system from /tmp, so it’s an example of a symlink pointing outside
+ * its mount point. */
+ non_removable_root = g_file_get_child (fixture->working_dir, "non-removable-mount");
+ non_removable_mount = G_MOUNT (ostree_mock_mount_new ("non-removable", non_removable_root));
+
+ assert_create_repos_dir (fixture, "no-repos-mount", &no_repos_repos, &no_repos_mount);
+
+ assert_create_repos_dir (fixture, "repo1-mount", &repo1_repos, &repo1_mount);
+ repo1_repo_a = assert_create_repo_dir (fixture, repo1_repos, repo1_mount, refs[0], &repo1_repo_a_uri,
+ refs[0], &repo1_ref0_checksum,
+ refs[2], &repo1_ref2_checksum,
+ refs[5], &repo1_ref5_checksum,
+ NULL);
+ repo1_repo_b = assert_create_repo_dir (fixture, repo1_repos, repo1_mount, refs[1], &repo1_repo_b_uri,
+ refs[1], &repo1_ref1_checksum,
+ NULL);
+ assert_create_repo_symlink (repo1_repos, refs[2], "ref0"); /* repo1_repo_a */
+ assert_create_repo_symlink (repo1_repos, refs[5], "../../../org.example.Collection1/exampleos/x86_64/ref0"); /* repo1_repo_a */
+
+ assert_create_repos_dir (fixture, "repo2-mount", &repo2_repos, &repo2_mount);
+ repo2_repo_a = assert_create_repo_dir (fixture, repo2_repos, repo2_mount, refs[0], &repo2_repo_a_uri,
+ refs[0], &repo2_ref0_checksum,
+ refs[1], &repo2_ref1_checksum,
+ refs[2], &repo2_ref2_checksum,
+ refs[3], NULL,
+ NULL);
+ assert_create_repo_symlink (repo2_repos, refs[1], "ref0"); /* repo2_repo_a */
+ assert_create_repo_symlink (repo2_repos, refs[2], "ref1"); /* repo2_repo_b */
+ assert_create_repo_symlink (repo2_repos, refs[3], "/");
+
+ mounts = g_list_prepend (mounts, non_removable_mount);
+ mounts = g_list_prepend (mounts, no_repos_mount);
+ mounts = g_list_prepend (mounts, repo1_mount);
+ mounts = g_list_prepend (mounts, repo2_mount);
+
+ monitor = ostree_mock_volume_monitor_new (mounts, NULL);
+ finder = ostree_repo_finder_mount_new (monitor);
+
+ assert_create_remote_config (fixture->parent_repo, "remote1", "https://nope1", "org.example.Collection1");
+ assert_create_remote_config (fixture->parent_repo, "remote2", "https://nope2", "org.example.Collection2");
+ /* don’t configure org.example.UnconfiguredCollection */
+ assert_create_remote_config (fixture->parent_repo, "remote3", "https://nope3", "org.example.Collection3");
+
+ /* Resolve the refs. */
+ ostree_repo_finder_resolve_async (OSTREE_REPO_FINDER (finder), refs,
+ fixture->parent_repo,
+ NULL, result_cb, &result);
+
+ while (result == NULL)
+ g_main_context_iteration (context, TRUE);
+
+ results = ostree_repo_finder_resolve_finish (OSTREE_REPO_FINDER (finder),
+ result, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (results);
+ g_assert_cmpuint (results->len, ==, 4);
+
+ /* Check that the results are correct: the invalid refs should have been
+ * ignored, and the valid results canonicalised and deduplicated. */
+ for (i = 0; i < results->len; i++)
+ {
+ g_autofree gchar *uri = NULL;
+ const gchar *keyring;
+ const OstreeRepoFinderResult *result = g_ptr_array_index (results, i);
+
+ uri = g_key_file_get_string (result->remote->options, result->remote->group, "url", &error);
+ g_assert_no_error (error);
+ keyring = result->remote->keyring;
+
+ if (g_strcmp0 (uri, repo1_repo_a_uri) == 0 &&
+ g_strcmp0 (keyring, "remote1.trustedkeys.gpg") == 0)
+ {
+ g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 2);
+ g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[0]), ==, repo1_ref0_checksum);
+ g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[2]), ==, repo1_ref2_checksum);
+ }
+ else if (g_strcmp0 (uri, repo1_repo_a_uri) == 0 &&
+ g_strcmp0 (keyring, "remote3.trustedkeys.gpg") == 0)
+ {
+ g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 1);
+ g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[5]), ==, repo1_ref5_checksum);
+ }
+ else if (g_strcmp0 (uri, repo1_repo_b_uri) == 0 &&
+ g_strcmp0 (keyring, "remote1.trustedkeys.gpg") == 0)
+ {
+ g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 1);
+ g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[1]), ==, repo1_ref1_checksum);
+ }
+ else if (g_strcmp0 (uri, repo2_repo_a_uri) == 0 &&
+ g_strcmp0 (keyring, "remote1.trustedkeys.gpg") == 0)
+ {
+ g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 3);
+ g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[0]), ==, repo2_ref0_checksum);
+ g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[1]), ==, repo2_ref1_checksum);
+ g_assert_cmpstr (g_hash_table_lookup (result->ref_to_checksum, refs[2]), ==, repo2_ref2_checksum);
+ }
+ else
+ {
+ g_test_message ("Unknown result ‘%s’ with keyring ‘%s’.",
+ result->remote->name, result->remote->keyring);
+ g_assert_not_reached ();
+ }
+ }
+
+ g_main_context_pop_thread_default (context);
+}
+
+int main (int argc, char **argv)
+{
+ setlocale (LC_ALL, "");
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/repo-finder-mount/init", test_repo_finder_mount_init);
+ g_test_add ("/repo-finder-mount/no-mounts", Fixture, NULL, setup,
+ test_repo_finder_mount_no_mounts, teardown);
+ g_test_add ("/repo-finder-mount/mixed-mounts", Fixture, NULL, setup,
+ test_repo_finder_mount_mixed_mounts, teardown);
+
+ return g_test_run();
+}