/* * Copyright © 2017 Endless Mobile, Inc. * * SPDX-License-Identifier: LGPL-2.0+ * * 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, see . * * Authors: * - Philip Withnall */ #include "config.h" #include #include #include #include #include "ostree-autocleanups.h" #include "ostree-core.h" #include "ostree-remote-private.h" #include "ostree-repo-finder.h" #include "ostree-repo.h" static void ostree_repo_finder_default_init (OstreeRepoFinderInterface *iface); G_DEFINE_INTERFACE (OstreeRepoFinder, ostree_repo_finder, G_TYPE_OBJECT) static void ostree_repo_finder_default_init (OstreeRepoFinderInterface *iface) { /* Nothing to see here. */ } /* Validate the given struct contains a valid collection ID and ref name, and that * the collection ID is non-%NULL. */ static gboolean is_valid_collection_ref (const OstreeCollectionRef *ref) { return (ref != NULL && ostree_validate_rev (ref->ref_name, NULL) && ostree_validate_collection_id (ref->collection_id, NULL)); } /* Validate @refs is non-%NULL, non-empty, and contains only valid collection * and ref names. */ static gboolean is_valid_collection_ref_array (const OstreeCollectionRef * const *refs) { gsize i; if (refs == NULL || *refs == NULL) return FALSE; for (i = 0; refs[i] != NULL; i++) { if (!is_valid_collection_ref (refs[i])) return FALSE; } return TRUE; } /* Validate @ref_to_checksum is non-%NULL, non-empty, and contains only valid * OstreeCollectionRefs as keys and only valid commit checksums as values. */ static gboolean is_valid_collection_ref_map (GHashTable *ref_to_checksum) { GHashTableIter iter; const OstreeCollectionRef *ref; const gchar *checksum; if (ref_to_checksum == NULL || g_hash_table_size (ref_to_checksum) == 0) return FALSE; g_hash_table_iter_init (&iter, ref_to_checksum); while (g_hash_table_iter_next (&iter, (gpointer *) &ref, (gpointer *) &checksum)) { g_assert (ref != NULL); g_assert (checksum != NULL); if (!is_valid_collection_ref (ref)) return FALSE; if (!ostree_validate_checksum_string (checksum, NULL)) return FALSE; } return TRUE; } static void resolve_cb (GObject *obj, GAsyncResult *result, gpointer user_data); /** * ostree_repo_finder_resolve_async: * @self: an #OstreeRepoFinder * @refs: (array zero-terminated=1): non-empty array of collection–ref pairs to find remotes for * @parent_repo: (transfer none): the local repository which the refs are being resolved for, * which provides configuration information and GPG keys * @cancellable: (nullable): a #GCancellable, or %NULL * @callback: asynchronous completion callback * @user_data: data to pass to @callback * * Find reachable remote URIs which claim to provide any of the given @refs. The * specific method for finding the remotes depends on the #OstreeRepoFinder * implementation. * * Any remote which is found and which claims to support any of the given @refs * will be returned in the results. It is possible that a remote claims to * support a given ref, but turns out not to — it is not possible to verify this * until ostree_repo_pull_from_remotes_async() is called. * * The returned results will be sorted with the most useful first — this is * typically the remote which claims to provide the most @refs, at the lowest * latency. * * Each result contains a mapping of @refs to the checksums of the commits * which the result provides. If the result provides the latest commit for a ref * across all of the results, the checksum will be set. Otherwise, if the * result provides an outdated commit, or doesn’t provide a given ref at all, * the checksum will not be set. Results which provide none of the requested * @refs may be listed with an empty refs map. * * Pass the results to ostree_repo_pull_from_remotes_async() to pull the given * @refs from those remotes. * * Since: 2018.6 */ void ostree_repo_finder_resolve_async (OstreeRepoFinder *self, const OstreeCollectionRef * const *refs, OstreeRepo *parent_repo, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; OstreeRepoFinder *finders[2] = { NULL, }; g_return_if_fail (OSTREE_IS_REPO_FINDER (self)); g_return_if_fail (is_valid_collection_ref_array (refs)); g_return_if_fail (OSTREE_IS_REPO (parent_repo)); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, ostree_repo_finder_resolve_async); finders[0] = self; ostree_repo_finder_resolve_all_async (finders, refs, parent_repo, cancellable, resolve_cb, g_steal_pointer (&task)); } static void resolve_cb (GObject *obj, GAsyncResult *result, gpointer user_data) { g_autoptr(GTask) task = NULL; g_autoptr(GPtrArray) results = NULL; g_autoptr(GError) local_error = NULL; task = G_TASK (user_data); results = ostree_repo_finder_resolve_all_finish (result, &local_error); g_assert ((local_error == NULL) != (results == NULL)); if (local_error != NULL) g_task_return_error (task, g_steal_pointer (&local_error)); else g_task_return_pointer (task, g_steal_pointer (&results), (GDestroyNotify) g_ptr_array_unref); } /** * ostree_repo_finder_resolve_finish: * @self: an #OstreeRepoFinder * @result: #GAsyncResult from the callback * @error: return location for a #GError * * Get the results from a ostree_repo_finder_resolve_async() operation. * * Returns: (transfer full) (element-type OstreeRepoFinderResult): array of zero * or more results * Since: 2018.6 */ GPtrArray * ostree_repo_finder_resolve_finish (OstreeRepoFinder *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (OSTREE_IS_REPO_FINDER (self), NULL); g_return_val_if_fail (g_task_is_valid (result, self), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); return g_task_propagate_pointer (G_TASK (result), error); } static gint sort_results_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); } typedef struct { gsize n_finders_pending; GPtrArray *results; } ResolveAllData; static void resolve_all_data_free (ResolveAllData *data) { g_assert (data->n_finders_pending == 0); g_clear_pointer (&data->results, g_ptr_array_unref); g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (ResolveAllData, resolve_all_data_free) static void resolve_all_cb (GObject *obj, GAsyncResult *result, gpointer user_data); static void resolve_all_finished_one (GTask *task); /** * ostree_repo_finder_resolve_all_async: * @finders: (array zero-terminated=1): non-empty array of #OstreeRepoFinders * @refs: (array zero-terminated=1): non-empty array of collection–ref pairs to find remotes for * @parent_repo: (transfer none): the local repository which the refs are being resolved for, * which provides configuration information and GPG keys * @cancellable: (nullable): a #GCancellable, or %NULL * @callback: asynchronous completion callback * @user_data: data to pass to @callback * * A version of ostree_repo_finder_resolve_async() which queries one or more * @finders in parallel and combines the results. * * Since: 2018.6 */ void ostree_repo_finder_resolve_all_async (OstreeRepoFinder * const *finders, const OstreeCollectionRef * const *refs, OstreeRepo *parent_repo, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; g_autoptr(ResolveAllData) data = NULL; gsize i; g_autoptr(GString) refs_str = NULL; g_autoptr(GString) finders_str = NULL; g_return_if_fail (finders != NULL && finders[0] != NULL); g_return_if_fail (is_valid_collection_ref_array (refs)); g_return_if_fail (OSTREE_IS_REPO (parent_repo)); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); refs_str = g_string_new (""); for (i = 0; refs[i] != NULL; i++) { if (i != 0) g_string_append (refs_str, ", "); g_string_append_printf (refs_str, "(%s, %s)", refs[i]->collection_id, refs[i]->ref_name); } finders_str = g_string_new (""); for (i = 0; finders[i] != NULL; i++) { if (i != 0) g_string_append (finders_str, ", "); g_string_append (finders_str, g_type_name (G_TYPE_FROM_INSTANCE (finders[i]))); } g_debug ("%s: Resolving refs [%s] with finders [%s]", G_STRFUNC, refs_str->str, finders_str->str); task = g_task_new (NULL, cancellable, callback, user_data); g_task_set_source_tag (task, ostree_repo_finder_resolve_all_async); data = g_new0 (ResolveAllData, 1); data->n_finders_pending = 1; /* while setting up the loop */ data->results = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_repo_finder_result_free); g_task_set_task_data (task, data, (GDestroyNotify) resolve_all_data_free); /* Start all the asynchronous queries in parallel. */ for (i = 0; finders[i] != NULL; i++) { OstreeRepoFinder *finder = OSTREE_REPO_FINDER (finders[i]); OstreeRepoFinderInterface *iface; iface = OSTREE_REPO_FINDER_GET_IFACE (finder); g_assert (iface->resolve_async != NULL); iface->resolve_async (finder, refs, parent_repo, cancellable, resolve_all_cb, g_object_ref (task)); data->n_finders_pending++; } resolve_all_finished_one (task); data = NULL; /* passed to the GTask above */ } /* Modifies both arrays in place. */ static void array_concatenate_steal (GPtrArray *array, GPtrArray *to_concatenate) /* (transfer full) */ { g_autoptr(GPtrArray) array_to_concatenate = to_concatenate; gsize i; for (i = 0; i < array_to_concatenate->len; i++) { /* Sanity check that the arrays do not contain any %NULL elements * (particularly NULL terminators). */ g_assert (g_ptr_array_index (array_to_concatenate, i) != NULL); g_ptr_array_add (array, g_steal_pointer (&g_ptr_array_index (array_to_concatenate, i))); } g_ptr_array_set_free_func (array_to_concatenate, NULL); g_ptr_array_set_size (array_to_concatenate, 0); } static void resolve_all_cb (GObject *obj, GAsyncResult *result, gpointer user_data) { OstreeRepoFinder *finder; OstreeRepoFinderInterface *iface; g_autoptr(GTask) task = NULL; g_autoptr(GPtrArray) results = NULL; g_autoptr(GError) local_error = NULL; ResolveAllData *data; finder = OSTREE_REPO_FINDER (obj); iface = OSTREE_REPO_FINDER_GET_IFACE (finder); task = G_TASK (user_data); data = g_task_get_task_data (task); results = iface->resolve_finish (finder, result, &local_error); g_assert ((local_error == NULL) != (results == NULL)); if (local_error != NULL) g_debug ("Error resolving refs to repository URI using %s: %s", g_type_name (G_TYPE_FROM_INSTANCE (finder)), local_error->message); else array_concatenate_steal (data->results, g_steal_pointer (&results)); resolve_all_finished_one (task); } static void resolve_all_finished_one (GTask *task) { ResolveAllData *data; data = g_task_get_task_data (task); data->n_finders_pending--; if (data->n_finders_pending == 0) { gsize i; g_autoptr(GString) results_str = NULL; g_ptr_array_sort (data->results, sort_results_cb); results_str = g_string_new (""); for (i = 0; i < data->results->len; i++) { const OstreeRepoFinderResult *result = g_ptr_array_index (data->results, i); if (i != 0) g_string_append (results_str, ", "); g_string_append (results_str, ostree_remote_get_name (result->remote)); } if (i == 0) g_string_append (results_str, "(none)"); g_debug ("%s: Finished, results: %s", G_STRFUNC, results_str->str); g_task_return_pointer (task, g_steal_pointer (&data->results), (GDestroyNotify) g_ptr_array_unref); } } /** * ostree_repo_finder_resolve_all_finish: * @result: #GAsyncResult from the callback * @error: return location for a #GError * * Get the results from a ostree_repo_finder_resolve_all_async() operation. * * Returns: (transfer full) (element-type OstreeRepoFinderResult): array of zero * or more results * Since: 2018.6 */ GPtrArray * ostree_repo_finder_resolve_all_finish (GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (result, NULL), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); return g_task_propagate_pointer (G_TASK (result), error); } G_DEFINE_BOXED_TYPE (OstreeRepoFinderResult, ostree_repo_finder_result, ostree_repo_finder_result_dup, ostree_repo_finder_result_free) /** * ostree_repo_finder_result_new: * @remote: (transfer none): an #OstreeRemote containing the transport details * for the result * @finder: (transfer none): the #OstreeRepoFinder instance which produced the * result * @priority: static priority of the result, where higher numbers indicate lower * priority * @ref_to_checksum: (element-type OstreeCollectionRef utf8) (transfer none): * map of collection–ref pairs to checksums provided by this result * @ref_to_timestamp: (element-type OstreeCollectionRef guint64) (nullable) * (transfer none): map of collection–ref pairs to timestamps provided by this * result * @summary_last_modified: Unix timestamp (seconds since the epoch, UTC) when * the summary file for the result was last modified, or `0` if this is unknown * * Create a new #OstreeRepoFinderResult instance. The semantics for the arguments * are as described in the #OstreeRepoFinderResult documentation. * * Returns: (transfer full): a new #OstreeRepoFinderResult * Since: 2018.6 */ OstreeRepoFinderResult * ostree_repo_finder_result_new (OstreeRemote *remote, OstreeRepoFinder *finder, gint priority, GHashTable *ref_to_checksum, GHashTable *ref_to_timestamp, guint64 summary_last_modified) { g_autoptr(OstreeRepoFinderResult) result = NULL; g_return_val_if_fail (remote != NULL, NULL); g_return_val_if_fail (OSTREE_IS_REPO_FINDER (finder), NULL); g_return_val_if_fail (is_valid_collection_ref_map (ref_to_checksum), NULL); result = g_new0 (OstreeRepoFinderResult, 1); result->remote = ostree_remote_ref (remote); result->finder = g_object_ref (finder); result->priority = priority; result->ref_to_checksum = g_hash_table_ref (ref_to_checksum); result->ref_to_timestamp = ref_to_timestamp != NULL ? g_hash_table_ref (ref_to_timestamp) : NULL; result->summary_last_modified = summary_last_modified; return g_steal_pointer (&result); } /** * ostree_repo_finder_result_dup: * @result: (transfer none): an #OstreeRepoFinderResult to copy * * Copy an #OstreeRepoFinderResult. * * Returns: (transfer full): a newly allocated copy of @result * Since: 2018.6 */ OstreeRepoFinderResult * ostree_repo_finder_result_dup (OstreeRepoFinderResult *result) { g_return_val_if_fail (result != NULL, NULL); return ostree_repo_finder_result_new (result->remote, result->finder, result->priority, result->ref_to_checksum, result->ref_to_timestamp, result->summary_last_modified); } /** * ostree_repo_finder_result_compare: * @a: an #OstreeRepoFinderResult * @b: an #OstreeRepoFinderResult * * Compare two #OstreeRepoFinderResult instances to work out which one is better * to pull from, and hence needs to be ordered before the other. * * Returns: <0 if @a is ordered before @b, 0 if they are ordered equally, * >0 if @b is ordered before @a * Since: 2018.6 */ gint ostree_repo_finder_result_compare (const OstreeRepoFinderResult *a, const OstreeRepoFinderResult *b) { guint a_n_refs, b_n_refs; g_return_val_if_fail (a != NULL, 0); g_return_val_if_fail (b != NULL, 0); /* FIXME: Check if this is really the ordering we want. For example, we * probably don’t want a result with 0 refs to be ordered before one with >0 * refs, just because its priority is higher. */ if (a->priority != b->priority) return a->priority - b->priority; if (a->summary_last_modified != 0 && b->summary_last_modified != 0 && a->summary_last_modified != b->summary_last_modified) return a->summary_last_modified - b->summary_last_modified; gpointer value; GHashTableIter iter; a_n_refs = b_n_refs = 0; g_hash_table_iter_init (&iter, a->ref_to_checksum); while (g_hash_table_iter_next (&iter, NULL, &value)) if (value != NULL) a_n_refs++; g_hash_table_iter_init (&iter, b->ref_to_checksum); while (g_hash_table_iter_next (&iter, NULL, &value)) if (value != NULL) b_n_refs++; if (a_n_refs != b_n_refs) return (gint) a_n_refs - (gint) b_n_refs; return g_strcmp0 (a->remote->name, b->remote->name); } /** * ostree_repo_finder_result_free: * @result: (transfer full): an #OstreeRepoFinderResult * * Free the given @result. * * Since: 2018.6 */ void ostree_repo_finder_result_free (OstreeRepoFinderResult *result) { g_return_if_fail (result != NULL); /* This may be NULL iff the result is freed half-way through find_remotes_cb() * in ostree-repo-pull.c, and at no other time. */ g_clear_pointer (&result->ref_to_checksum, g_hash_table_unref); g_clear_pointer (&result->ref_to_timestamp, g_hash_table_unref); g_object_unref (result->finder); ostree_remote_unref (result->remote); g_free (result); } /** * ostree_repo_finder_result_freev: * @results: (array zero-terminated=1) (transfer full): an #OstreeRepoFinderResult * * Free the given @results array, freeing each element and the container. * * Since: 2018.6 */ void ostree_repo_finder_result_freev (OstreeRepoFinderResult **results) { gsize i; for (i = 0; results[i] != NULL; i++) ostree_repo_finder_result_free (results[i]); g_free (results); }