lib/repo-finder: Add mount based OstreeRepoFinder implementation

This is a basic implementation of OstreeRepoFinder which resolves ref
names to remote URIs by looking for them on any currently mounted
removable storage volumes. The idea is to support OS and app updates via
USB stick.

Unit tests are included.

This bumps libostree’s maximum GLib dependency from 2.44 to 2.50 for
g_drive_is_removable(). If GLib 2.50 is not available, the call which
needs it will be omitted and the OstreeRepoFinderMount implementation
will scan all volumes (not just removable ones); this is a performance
hit, but not a functionality hit.

Signed-off-by: Philip Withnall <withnall@endlessm.com>

Closes: #924
Approved by: cgwalters
This commit is contained in:
Philip Withnall 2017-04-19 00:07:51 +01:00 committed by Atomic Bot
parent d15f83c922
commit ae335f24dc
16 changed files with 1657 additions and 3 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 += \

View File

@ -57,6 +57,14 @@ ostree_repo_finder_config_new
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

View File

@ -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;

View File

@ -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 */

View File

@ -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

View File

@ -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 volumes 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 its 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 mounts general properties. */
if (g_mount_is_shadowed (mount))
{
g_debug ("Ignoring mount %s as its 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 cant 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 doesnt exist.",
mount_name, mount_root_path);
else
g_debug ("Ignoring mount %s as %s/.ostree/repos directory cant 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 were looking
* for. If so, and its 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 its 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 doesnt 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 dont 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);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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>

1
tests/.gitignore vendored
View File

@ -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

400
tests/test-mock-gio.c Normal file
View File

@ -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 its
* 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);
}

127
tests/test-mock-gio.h Normal file
View File

@ -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

View File

@ -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 its on a separate
* file system from /tmp, so its 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");
/* dont 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();
}