--- /dev/null
+/* -*- 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 <fcntl.h>
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <libglnx.h>
+
+#include "ostree-remote-private.h"
+#include "ostree-repo.h"
+#include "ostree-repo-private.h"
+#include "ostree-repo-finder.h"
+#include "ostree-repo-finder-config.h"
+
+/**
+ * SECTION:ostree-repo-finder-config
+ * @title: OstreeRepoFinderConfig
+ * @short_description: Finds remote repositories from ref names using the local
+ * repository configuration files
+ * @stability: Unstable
+ * @include: libostree/ostree-repo-finder-config.h
+ *
+ * #OstreeRepoFinderConfig is an implementation of #OstreeRepoFinder which looks
+ * refs up in locally configured remotes and returns remote URIs.
+ * Duplicate remote URIs are combined into a single #OstreeRepoFinderResult
+ * which lists multiple refs.
+ *
+ * For all the locally configured remotes which have an `collection-id` specified
+ * (see [ostree.repo-config(5)](man:ostree.repo-config(5))), it finds the
+ * intersection of their refs and the set of refs to resolve. If the
+ * intersection is non-empty, that remote is returned as a result. Remotes which
+ * do not have their `collection-id` key configured are ignored.
+ *
+ * Since: 2017.8
+ */
+
+static void ostree_repo_finder_config_iface_init (OstreeRepoFinderInterface *iface);
+
+struct _OstreeRepoFinderConfig
+{
+ GObject parent_instance;
+};
+
+G_DEFINE_TYPE_WITH_CODE (OstreeRepoFinderConfig, ostree_repo_finder_config, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (OSTREE_TYPE_REPO_FINDER, ostree_repo_finder_config_iface_init))
+
+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_config_resolve_async (OstreeRepoFinder *finder,
+ const OstreeCollectionRef * const *refs,
+ OstreeRepo *parent_repo,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GPtrArray) results = NULL;
+ const gint priority = 100; /* arbitrarily chosen; lower than the others */
+ gsize i, j;
+ g_autoptr(GHashTable) repo_name_to_refs = NULL; /* (element-type utf8 GHashTable) */
+ GHashTable *supported_ref_to_checksum; /* (element-type OstreeCollectionRef utf8) */
+ GHashTableIter iter;
+ const gchar *remote_name;
+ g_auto(GStrv) remotes = NULL;
+ gsize n_remotes = 0;
+
+ task = g_task_new (finder, cancellable, callback, user_data);
+ g_task_set_source_tag (task, ostree_repo_finder_config_resolve_async);
+ results = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_repo_finder_result_free);
+ repo_name_to_refs = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
+ (GDestroyNotify) g_hash_table_unref);
+
+ /* List all remotes in this #OstreeRepo and see which of their ref lists
+ * intersect with @refs. */
+ remotes = ostree_repo_remote_list (parent_repo, (guint *) &n_remotes);
+
+ g_debug ("%s: Checking %" G_GSIZE_FORMAT " remotes", G_STRFUNC, n_remotes);
+
+ for (i = 0; i < n_remotes; i++)
+ {
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GHashTable) remote_refs = NULL; /* (element-type utf8 utf8) */
+ const gchar *checksum;
+ g_autofree gchar *remote_collection_id = NULL;
+
+ remote_name = remotes[i];
+
+ if (!ostree_repo_get_remote_option (parent_repo, remote_name, "collection-id",
+ NULL, &remote_collection_id, &local_error) ||
+ !ostree_validate_collection_id (remote_collection_id, &local_error))
+ {
+ g_debug ("Ignoring remote ‘%s’ due to no valid collection ID being configured for it: %s",
+ remote_name, local_error->message);
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ if (!ostree_repo_remote_list_refs (parent_repo, remote_name, &remote_refs,
+ cancellable, &local_error))
+ {
+ g_debug ("Ignoring remote ‘%s’ due to error loading its refs: %s",
+ remote_name, local_error->message);
+ g_clear_error (&local_error);
+ continue;
+ }
+
+ for (j = 0; refs[j] != NULL; j++)
+ {
+ if (g_strcmp0 (refs[j]->collection_id, remote_collection_id) == 0 &&
+ g_hash_table_lookup_extended (remote_refs, refs[j]->ref_name, NULL, (gpointer *) &checksum))
+ {
+ /* The requested ref is listed in the refs for this remote. Add
+ * the remote to the results, and the ref to its
+ * @supported_ref_to_checksum. */
+ g_debug ("Resolved ref (%s, %s) to remote ‘%s’.",
+ refs[j]->collection_id, refs[j]->ref_name, remote_name);
+
+ supported_ref_to_checksum = g_hash_table_lookup (repo_name_to_refs, remote_name);
+
+ 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_name_to_refs, (gpointer) remote_name, supported_ref_to_checksum /* transfer */);
+ }
+
+ g_hash_table_insert (supported_ref_to_checksum,
+ (gpointer) refs[j], g_strdup (checksum));
+ }
+ }
+ }
+
+ /* Aggregate the results. */
+ g_hash_table_iter_init (&iter, repo_name_to_refs);
+
+ while (g_hash_table_iter_next (&iter, (gpointer *) &remote_name, (gpointer *) &supported_ref_to_checksum))
+ {
+ g_autoptr(GError) local_error = NULL;
+ OstreeRemote *remote;
+
+ /* We don’t know what last-modified timestamp the remote has without
+ * making expensive HTTP queries, so leave that information blank. We
+ * assume that the configuration which says the refs and commits in
+ * @supported_ref_to_checksum are in the repository is correct; the code
+ * in ostree_repo_find_remotes_async() will check that. */
+ remote = _ostree_repo_get_remote_inherited (parent_repo, remote_name, &local_error);
+ if (remote == NULL)
+ {
+ g_debug ("Configuration for remote ‘%s’ could not be found. Ignoring.",
+ remote_name);
+ continue;
+ }
+
+ 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_config_resolve_finish (OstreeRepoFinder *finder,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, finder), NULL);
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ostree_repo_finder_config_init (OstreeRepoFinderConfig *self)
+{
+ /* Nothing to see here. */
+}
+
+static void
+ostree_repo_finder_config_class_init (OstreeRepoFinderConfigClass *klass)
+{
+ /* Nothing to see here. */
+}
+
+static void
+ostree_repo_finder_config_iface_init (OstreeRepoFinderInterface *iface)
+{
+ iface->resolve_async = ostree_repo_finder_config_resolve_async;
+ iface->resolve_finish = ostree_repo_finder_config_resolve_finish;
+}
+
+/**
+ * ostree_repo_finder_config_new:
+ *
+ * Create a new #OstreeRepoFinderConfig.
+ *
+ * Returns: (transfer full): a new #OstreeRepoFinderConfig
+ * Since: 2017.8
+ */
+OstreeRepoFinderConfig *
+ostree_repo_finder_config_new (void)
+{
+ return g_object_new (OSTREE_TYPE_REPO_FINDER_CONFIG, NULL);
+}
--- /dev/null
+/* -*- 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 <locale.h>
+#include <string.h>
+
+#include "libostreetest.h"
+#include "ostree-autocleanups.h"
+#include "ostree-repo-finder.h"
+#include "ostree-repo-finder-config.h"
+
+/* Test fixture. Creates a temporary directory. */
+typedef struct
+{
+ OstreeRepo *parent_repo; /* owned */
+ 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-config-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);
+ 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_config_init (void)
+{
+ g_autoptr(OstreeRepoFinderConfig) finder = NULL;
+
+ /* Default everything. */
+ finder = ostree_repo_finder_config_new ();
+}
+
+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 there are no config files in the refs
+ * directory. */
+static void
+test_repo_finder_config_no_configs (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_autoptr(OstreeRepoFinderConfig) finder = 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.Os", "exampleos/x86_64/standard" };
+ const OstreeCollectionRef ref2 = { "org.example.Os", "exampleos/x86_64/buildmaster/standard" };
+ const OstreeCollectionRef * const refs[] = { &ref1, &ref2, NULL };
+
+ context = g_main_context_new ();
+ g_main_context_push_thread_default (context);
+
+ finder = ostree_repo_finder_config_new ();
+
+ 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);
+}
+
+/* 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);
+}
+
+static gchar *assert_create_remote (Fixture *fixture,
+ const gchar *collection_id,
+ ...) G_GNUC_NULL_TERMINATED;
+
+/* Create a new repository in a temporary directory with its collection ID set
+ * to @collection_id, and containing the refs given in @... (which must be
+ * %NULL-terminated). Return the `file://` URI of the new repository. */
+static gchar *
+assert_create_remote (Fixture *fixture,
+ const gchar *collection_id,
+ ...)
+{
+ va_list args;
+ g_autoptr(GError) error = NULL;
+ const gchar *repo_name = (collection_id != NULL) ? collection_id : "no-collection";
+
+ glnx_shutil_mkdir_p_at (fixture->working_dfd, repo_name, 0700, NULL, &error);
+ g_assert_no_error (error);
+
+ g_autoptr(GFile) repo_path = g_file_get_child (fixture->working_dir, repo_name);
+ g_autoptr(OstreeRepo) repo = ostree_repo_new (repo_path);
+ ostree_repo_set_collection_id (repo, collection_id, &error);
+ g_assert_no_error (error);
+ ostree_repo_create (repo, OSTREE_REPO_MODE_ARCHIVE_Z2, NULL, &error);
+ g_assert_no_error (error);
+
+ /* Set up the refs from @.... */
+ va_start (args, collection_id);
+
+ for (const gchar *ref_name = va_arg (args, const gchar *);
+ ref_name != NULL;
+ ref_name = va_arg (args, const gchar *))
+ {
+ OstreeCollectionRef collection_ref = { (gchar *) collection_id, (gchar *) ref_name };
+ g_autofree gchar *checksum = NULL;
+ g_autoptr(OstreeMutableTree) mtree = NULL;
+ g_autoptr(OstreeRepoFile) repo_file = NULL;
+
+ 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_name, ref_name,
+ NULL /* no metadata */, repo_file, &checksum,
+ NULL, &error);
+ g_assert_no_error (error);
+
+ if (collection_id != NULL)
+ ostree_repo_set_collection_ref_immediate (repo, &collection_ref, checksum, NULL, &error);
+ else
+ ostree_repo_set_ref_immediate (repo, NULL, ref_name, checksum, NULL, &error);
+ g_assert_no_error (error);
+ }
+
+ va_end (args);
+
+ /* Update the summary. */
+ ostree_repo_regenerate_summary (repo, NULL /* no metadata */, NULL, &error);
+ g_assert_no_error (error);
+
+ return g_file_get_uri (repo_path);
+}
+
+/* Test resolving the refs against a collection of config files, which contain
+ * valid, invalid or duplicate repo information. */
+static void
+test_repo_finder_config_mixed_configs (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_autoptr(OstreeRepoFinderConfig) finder = NULL;
+ g_autoptr(GMainContext) context = NULL;
+ g_autoptr(GAsyncResult) result = NULL;
+ g_autoptr(GPtrArray) results = NULL; /* (element-type OstreeRepoFinderResult) */
+ g_autoptr(GError) error = NULL;
+ gsize i;
+ const OstreeCollectionRef ref0 = { "org.example.Collection0", "exampleos/x86_64/ref0" };
+ const OstreeCollectionRef ref1 = { "org.example.Collection0", "exampleos/x86_64/ref1" };
+ const OstreeCollectionRef ref2 = { "org.example.Collection1", "exampleos/x86_64/ref1" };
+ const OstreeCollectionRef ref3 = { "org.example.Collection1", "exampleos/x86_64/ref2" };
+ const OstreeCollectionRef ref4 = { "org.example.Collection2", "exampleos/x86_64/ref3" };
+ const OstreeCollectionRef * const refs[] = { &ref0, &ref1, &ref2, &ref3, &ref4, NULL };
+
+ context = g_main_context_new ();
+ g_main_context_push_thread_default (context);
+
+ /* Put together various ref configuration files. */
+ g_autofree gchar *collection0_uri = assert_create_remote (fixture, "org.example.Collection0",
+ "exampleos/x86_64/ref0",
+ "exampleos/x86_64/ref1",
+ NULL);
+ g_autofree gchar *collection1_uri = assert_create_remote (fixture, "org.example.Collection1",
+ "exampleos/x86_64/ref2",
+ NULL);
+ g_autofree gchar *no_collection_uri = assert_create_remote (fixture, NULL,
+ "exampleos/x86_64/ref3",
+ NULL);
+
+ assert_create_remote_config (fixture->parent_repo, "remote0", collection0_uri, "org.example.Collection0");
+ assert_create_remote_config (fixture->parent_repo, "remote1", collection1_uri, "org.example.Collection1");
+ assert_create_remote_config (fixture->parent_repo, "remote0-copy", collection0_uri, "org.example.Collection0");
+ assert_create_remote_config (fixture->parent_repo, "remote1-bad-copy", collection1_uri, "org.example.NotCollection1");
+ assert_create_remote_config (fixture->parent_repo, "remote2", no_collection_uri, NULL);
+
+ finder = ostree_repo_finder_config_new ();
+
+ /* 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, ==, 3);
+
+ /* 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++)
+ {
+ const OstreeRepoFinderResult *result = g_ptr_array_index (results, i);
+
+ if (g_strcmp0 (ostree_remote_get_name (result->remote), "remote0") == 0 ||
+ g_strcmp0 (ostree_remote_get_name (result->remote), "remote0-copy") == 0)
+ {
+ g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 2);
+ g_assert_true (g_hash_table_contains (result->ref_to_checksum, &ref0));
+ g_assert_true (g_hash_table_contains (result->ref_to_checksum, &ref1));
+ }
+ else if (g_strcmp0 (ostree_remote_get_name (result->remote), "remote1") == 0)
+ {
+ g_assert_cmpuint (g_hash_table_size (result->ref_to_checksum), ==, 1);
+ g_assert_true (g_hash_table_contains (result->ref_to_checksum, &ref3));
+ }
+ else
+ {
+ 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-config/init", test_repo_finder_config_init);
+ g_test_add ("/repo-finder-config/no-configs", Fixture, NULL, setup,
+ test_repo_finder_config_no_configs, teardown);
+ g_test_add ("/repo-finder-config/mixed-configs", Fixture, NULL, setup,
+ test_repo_finder_config_mixed_configs, teardown);
+
+ return g_test_run();
+}