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