63d7ff8d5f
Determines a revision argument to either be a SHA256 checksum or a version metadata value. The revision string may have a "revision=" prefix to denote a SHA256 checksum, or a "version=" prefix to denote a version metadata value. If the revision string lacks either prefix, the function attempts to infer the type of revision. The prefixes are case-insensitive.
561 lines
16 KiB
C
561 lines
16 KiB
C
/*
|
|
* Copyright (C) 2015 Red Hat, Inc.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "rpmostreed-utils.h"
|
|
#include "rpmostreed-errors.h"
|
|
#include "libgsystem.h"
|
|
|
|
#include <libglnx.h>
|
|
|
|
static void
|
|
append_to_object_path (GString *str,
|
|
const gchar *s)
|
|
{
|
|
guint n;
|
|
|
|
for (n = 0; s[n] != '\0'; n++)
|
|
{
|
|
gint c = s[n];
|
|
/* D-Bus spec sez:
|
|
*
|
|
* Each element must only contain the ASCII characters "[A-Z][a-z][0-9]_-"
|
|
*/
|
|
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')
|
|
{
|
|
g_string_append_c (str, c);
|
|
}
|
|
else if (c == '-' || c == '/')
|
|
{
|
|
/* Swap / or - for _ to keep names easier to read */
|
|
g_string_append_c (str, '_');
|
|
}
|
|
else
|
|
{
|
|
/* Escape bytes not in [A-Z][a-z][0-9] as _<hex-with-two-digits> */
|
|
g_string_append_printf (str, "_%02x", c & 0xFF);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* rpmostreed_generate_object_path:
|
|
* @base: The base object path (without trailing '/').
|
|
* @part...: UTF-8 strings.
|
|
*
|
|
* Appends @s to @base in a way such that only characters that can be
|
|
* used in a D-Bus object path will be used. E.g. a character not in
|
|
* <literal>[A-Z][a-z][0-9]_</literal> will be escaped as _HEX where
|
|
* HEX is a two-digit hexadecimal number.
|
|
*
|
|
* Note that his mapping is not bijective - e.g. you cannot go back
|
|
* to the original string.
|
|
*
|
|
* Returns: An allocated string that must be freed with g_free ().
|
|
*/
|
|
gchar *
|
|
rpmostreed_generate_object_path (const gchar *base,
|
|
const gchar *part,
|
|
...)
|
|
{
|
|
gchar *result;
|
|
va_list va;
|
|
|
|
va_start (va, part);
|
|
result = rpmostreed_generate_object_path_from_va (base, part, va);
|
|
va_end (va);
|
|
|
|
return result;
|
|
}
|
|
|
|
gchar *
|
|
rpmostreed_generate_object_path_from_va (const gchar *base,
|
|
const gchar *part,
|
|
va_list va)
|
|
{
|
|
GString *path;
|
|
|
|
g_return_val_if_fail (base != NULL, NULL);
|
|
g_return_val_if_fail (g_variant_is_object_path (base), NULL);
|
|
g_return_val_if_fail (!g_str_has_suffix (base, "/"), NULL);
|
|
|
|
path = g_string_new (base);
|
|
|
|
while (part != NULL)
|
|
{
|
|
if (!g_utf8_validate (part, -1, NULL))
|
|
{
|
|
g_string_free (path, TRUE);
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
g_string_append_c (path, '/');
|
|
append_to_object_path (path, part);
|
|
part = va_arg (va, const gchar *);
|
|
}
|
|
}
|
|
|
|
return g_string_free (path, FALSE);
|
|
}
|
|
|
|
/**
|
|
* rpmostreed_refspec_parse_partial:
|
|
* @new_provided_refspec: The provided refspec
|
|
* @base_refspec: The refspec string to base on.
|
|
* @out_refspec: Pointer to the new refspec
|
|
* @error: Pointer to an error pointer.
|
|
*
|
|
* Takes a refspec string and adds any missing bits based on the
|
|
* base_refspec argument. Errors if a full valid refspec can't
|
|
* be derived.
|
|
*
|
|
* Returns: True on success.
|
|
*/
|
|
gboolean
|
|
rpmostreed_refspec_parse_partial (const gchar *new_provided_refspec,
|
|
gchar *base_refspec,
|
|
gchar **out_refspec,
|
|
GError **error)
|
|
{
|
|
|
|
g_autofree gchar *ref = NULL;
|
|
g_autofree gchar *remote = NULL;
|
|
g_autofree gchar *origin_ref = NULL;
|
|
g_autofree gchar *origin_remote = NULL;
|
|
GError *parse_error = NULL;
|
|
|
|
gboolean ret = FALSE;
|
|
|
|
/* Allow just switching remotes */
|
|
if (g_str_has_suffix (new_provided_refspec, ":"))
|
|
{
|
|
remote = g_strdup (new_provided_refspec);
|
|
remote[strlen (remote) - 1] = '\0';
|
|
}
|
|
else
|
|
{
|
|
if (!ostree_parse_refspec (new_provided_refspec, &remote,
|
|
&ref, &parse_error))
|
|
{
|
|
g_set_error_literal (error, RPM_OSTREED_ERROR,
|
|
RPM_OSTREED_ERROR_INVALID_REFSPEC,
|
|
parse_error->message);
|
|
g_clear_error (&parse_error);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (base_refspec != NULL)
|
|
{
|
|
if (!ostree_parse_refspec (base_refspec, &origin_remote,
|
|
&origin_ref, &parse_error))
|
|
goto out;
|
|
}
|
|
|
|
if (ref == NULL)
|
|
{
|
|
if (origin_ref)
|
|
{
|
|
ref = g_strdup (origin_ref);
|
|
}
|
|
|
|
else
|
|
{
|
|
g_set_error (error, RPM_OSTREED_ERROR,
|
|
RPM_OSTREED_ERROR_INVALID_REFSPEC,
|
|
"Could not determine default ref to pull.");
|
|
goto out;
|
|
}
|
|
|
|
}
|
|
else if (remote == NULL)
|
|
{
|
|
if (origin_remote)
|
|
{
|
|
remote = g_strdup (origin_remote);
|
|
}
|
|
else
|
|
{
|
|
g_set_error (error, RPM_OSTREED_ERROR,
|
|
RPM_OSTREED_ERROR_INVALID_REFSPEC,
|
|
"Could not determine default remote to pull.");
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (g_strcmp0 (origin_remote, remote) == 0 &&
|
|
g_strcmp0 (origin_ref, ref) == 0)
|
|
{
|
|
g_set_error (error, RPM_OSTREED_ERROR,
|
|
RPM_OSTREED_ERROR_INVALID_REFSPEC,
|
|
"Old and new refs are equal: %s:%s",
|
|
remote, ref);
|
|
goto out;
|
|
}
|
|
|
|
*out_refspec = g_strconcat (remote, ":", ref, NULL);
|
|
ret = TRUE;
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
void
|
|
rpmostreed_reboot (GCancellable *cancellable, GError **error)
|
|
{
|
|
gs_subprocess_simple_run_sync (NULL, GS_SUBPROCESS_STREAM_DISPOSITION_INHERIT,
|
|
cancellable, error,
|
|
"systemctl", "reboot", NULL);
|
|
}
|
|
|
|
/**
|
|
* rpmostreed_repo_pull_ancestry:
|
|
* @repo: Repo
|
|
* @refspec: Repository branch
|
|
* @visitor: (allow-none): Visitor function to call on each commit
|
|
* @visitor_data: (allow-none): User data for @visitor
|
|
* @progress: (allow-none): Progress
|
|
* @cancellable: Cancellable
|
|
* @error: Error
|
|
*
|
|
* Downloads an ancestry of commit objects starting from @refspec.
|
|
*
|
|
* If a @visitor function pointer is given, commit objects are downloaded
|
|
* in batches and the @visitor function is called for each commit object.
|
|
* The @visitor function can stop the recursion, such as when looking for
|
|
* a particular commit.
|
|
*
|
|
* Returns: %TRUE on success, %FALSE on failure
|
|
*/
|
|
gboolean
|
|
rpmostreed_repo_pull_ancestry (OstreeRepo *repo,
|
|
const char *refspec,
|
|
RpmostreedCommitVisitor visitor,
|
|
gpointer visitor_data,
|
|
OstreeAsyncProgress *progress,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
OstreeRepoPullFlags flags;
|
|
GVariantDict options;
|
|
GVariant *refs_value;
|
|
const char *refs_array[] = { NULL, NULL };
|
|
g_autofree char *remote = NULL;
|
|
g_autofree char *ref = NULL;
|
|
g_autofree char *checksum = NULL;
|
|
int depth, ii;
|
|
gboolean ret = FALSE;
|
|
|
|
g_return_val_if_fail (OSTREE_IS_REPO (repo), FALSE);
|
|
g_return_val_if_fail (refspec != NULL, FALSE);
|
|
|
|
if (!ostree_parse_refspec (refspec, &remote, &ref, error))
|
|
goto out;
|
|
|
|
/* If no visitor function was provided then we won't be short-circuiting
|
|
* the recursion, so pull everything in one shot. Otherwise pull commits
|
|
* in increasingly large batches. */
|
|
depth = (visitor != NULL) ? 10 : -1;
|
|
|
|
flags = OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY;
|
|
|
|
/* It's important to use the ref name instead of a checksum on the first
|
|
* pass because we want to search from the latest available commit on the
|
|
* remote server, which is not necessarily what the ref name is currently
|
|
* pointing at in our local repo. */
|
|
refs_array[0] = ref;
|
|
|
|
while (TRUE)
|
|
{
|
|
/* Floating reference, transferred to dictionary. */
|
|
refs_value = g_variant_new_strv ((const char * const *) refs_array, -1);
|
|
|
|
g_variant_dict_init (&options, NULL);
|
|
g_variant_dict_insert (&options, "depth", "i", depth);
|
|
g_variant_dict_insert (&options, "flags", "i", flags);
|
|
g_variant_dict_insert_value (&options, "refs", refs_value);
|
|
|
|
if (!ostree_repo_pull_with_options (repo, remote,
|
|
g_variant_dict_end (&options),
|
|
progress, cancellable, error))
|
|
goto out;
|
|
|
|
/* First pass only. Now we can resolve the ref to a checksum. */
|
|
if (checksum == NULL)
|
|
{
|
|
if (!ostree_repo_resolve_rev (repo, ref, FALSE, &checksum, error))
|
|
goto out;
|
|
}
|
|
|
|
/* If depth is negative (no visitor), this loop is skipped. */
|
|
for (ii = 0; ii < depth && checksum != NULL; ii++)
|
|
{
|
|
g_autoptr(GVariant) commit = NULL;
|
|
gboolean stop = FALSE;
|
|
|
|
if (!ostree_repo_load_commit (repo, checksum, &commit, NULL, error))
|
|
goto out;
|
|
|
|
if (!visitor (repo, checksum, commit, visitor_data, &stop, error))
|
|
goto out;
|
|
|
|
g_clear_pointer (&checksum, g_free);
|
|
|
|
if (!stop)
|
|
checksum = ostree_commit_get_parent (commit);
|
|
}
|
|
|
|
/* Break if no visitor, or visitor told us to stop. */
|
|
if (depth < 0 || checksum == NULL)
|
|
break;
|
|
|
|
/* Pull the next batch of commits, twice as many. */
|
|
refs_array[0] = checksum;
|
|
depth = depth * 2;
|
|
}
|
|
|
|
ret = TRUE;
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
typedef struct {
|
|
const char *version;
|
|
char *checksum;
|
|
} VersionVisitorClosure;
|
|
|
|
static gboolean
|
|
version_visitor (OstreeRepo *repo,
|
|
const char *checksum,
|
|
GVariant *commit,
|
|
gpointer user_data,
|
|
gboolean *out_stop,
|
|
GError **error)
|
|
{
|
|
VersionVisitorClosure *closure = user_data;
|
|
g_autoptr(GVariant) metadict = NULL;
|
|
const char *version = NULL;
|
|
|
|
metadict = g_variant_get_child_value (commit, 0);
|
|
if (g_variant_lookup (metadict, "version", "&s", &version))
|
|
{
|
|
if (g_str_equal (version, closure->version))
|
|
{
|
|
closure->checksum = g_strdup (checksum);
|
|
*out_stop = TRUE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* rpmostreed_repo_lookup_version:
|
|
* @repo: Repo
|
|
* @refspec: Repository branch
|
|
* @version: Version to look for
|
|
* @progress: (allow-none): Progress
|
|
* @cancellable: Cancellable
|
|
* @out_checksum: (out) (allow-none): Commit checksum, or %NULL
|
|
* @error: Error
|
|
*
|
|
* Tries to determine the commit checksum for @version on @refspec.
|
|
* This may require pulling commit objects from a remote repository.
|
|
*
|
|
* Returns: %TRUE on success, %FALSE on failure
|
|
*/
|
|
gboolean
|
|
rpmostreed_repo_lookup_version (OstreeRepo *repo,
|
|
const char *refspec,
|
|
const char *version,
|
|
OstreeAsyncProgress *progress,
|
|
GCancellable *cancellable,
|
|
char **out_checksum,
|
|
GError **error)
|
|
{
|
|
VersionVisitorClosure closure = { version, NULL };
|
|
gboolean ret = FALSE;
|
|
|
|
g_return_val_if_fail (OSTREE_IS_REPO (repo), FALSE);
|
|
g_return_val_if_fail (refspec != NULL, FALSE);
|
|
g_return_val_if_fail (version != NULL, FALSE);
|
|
|
|
if (!rpmostreed_repo_pull_ancestry (repo, refspec,
|
|
version_visitor, &closure,
|
|
progress, cancellable, error))
|
|
goto out;
|
|
|
|
if (closure.checksum == NULL)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
|
|
"Version %s not found in %s", version, refspec);
|
|
goto out;
|
|
}
|
|
|
|
if (out_checksum != NULL)
|
|
*out_checksum = g_steal_pointer (&closure.checksum);
|
|
|
|
g_free (closure.checksum);
|
|
|
|
ret = TRUE;
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* rpmostreed_repo_lookup_cached_version:
|
|
* @repo: Repo
|
|
* @refspec: Repository branch
|
|
* @version: Version to look for
|
|
* @cancellable: Cancellable
|
|
* @out_checksum: (out) (allow-none): Commit checksum, or %NULL
|
|
* @error: Error
|
|
*
|
|
* Similar to rpmostreed_repo_lookup_version(), except without pulling
|
|
* from a remote repository. It traverses whatever commits are available
|
|
* locally in @repo.
|
|
*
|
|
* Returns: %TRUE on success, %FALSE on failure
|
|
*/
|
|
gboolean
|
|
rpmostreed_repo_lookup_cached_version (OstreeRepo *repo,
|
|
const char *refspec,
|
|
const char *version,
|
|
GCancellable *cancellable,
|
|
char **out_checksum,
|
|
GError **error)
|
|
{
|
|
VersionVisitorClosure closure = { version, NULL };
|
|
g_autofree char *checksum = NULL;
|
|
gboolean ret = FALSE;
|
|
|
|
g_return_val_if_fail (OSTREE_IS_REPO (repo), FALSE);
|
|
g_return_val_if_fail (refspec != NULL, FALSE);
|
|
g_return_val_if_fail (version != NULL, FALSE);
|
|
|
|
if (!ostree_repo_resolve_rev (repo, refspec, FALSE, &checksum, error))
|
|
goto out;
|
|
|
|
while (checksum != NULL)
|
|
{
|
|
g_autoptr(GVariant) commit = NULL;
|
|
gboolean stop = FALSE;
|
|
|
|
if (!ostree_repo_load_commit (repo, checksum, &commit, NULL, error))
|
|
goto out;
|
|
|
|
if (!version_visitor (repo, checksum, commit, &closure, &stop, error))
|
|
goto out;
|
|
|
|
g_clear_pointer (&checksum, g_free);
|
|
|
|
if (!stop)
|
|
checksum = ostree_commit_get_parent (commit);
|
|
}
|
|
|
|
if (closure.checksum == NULL)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
|
|
"Version %s not cached in %s", version, refspec);
|
|
goto out;
|
|
}
|
|
|
|
if (out_checksum != NULL)
|
|
*out_checksum = g_steal_pointer (&closure.checksum);
|
|
|
|
g_free (closure.checksum);
|
|
|
|
ret = TRUE;
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* rpmostreed_parse_revision:
|
|
* @revision: Revision string
|
|
* @out_checksum: (out) (allow-none): Commit checksum, or %NULL
|
|
* @out_version: (out) (allow-none): Version value, or %NULL
|
|
* @error: Error
|
|
*
|
|
* Determines @revision to either be a SHA256 checksum or a version metadata
|
|
* value, sets one of @out_checksum or @out_version appropriately, and then
|
|
* returns %TRUE.
|
|
*
|
|
* The @revision string may have a "revision=" prefix to denote a SHA256
|
|
* checksum, or a "version=" prefix to denote a version metadata value. If
|
|
* the @revision string lacks either prefix, the function attempts to infer
|
|
* the type of revision. The prefixes are case-insensitive.
|
|
*
|
|
* The only possible error is if a "revision=" prefix is given, but the
|
|
* rest of the string is not a valid SHA256 checksum. In that case the
|
|
* function sets @error and returns %FALSE.
|
|
*
|
|
* Returns: %TRUE on success, %FALSE of failure
|
|
*/
|
|
gboolean
|
|
rpmostreed_parse_revision (const char *revision,
|
|
char **out_checksum,
|
|
char **out_version,
|
|
GError **error)
|
|
{
|
|
const char *checksum = NULL;
|
|
const char *version = NULL;
|
|
gboolean ret = FALSE;
|
|
|
|
g_return_val_if_fail (revision != NULL, FALSE);
|
|
|
|
if (g_ascii_strncasecmp (revision, "revision=", 9) == 0)
|
|
{
|
|
checksum = revision + 9;
|
|
|
|
/* Since this claims to be a checksum, fail if it isn't. */
|
|
if (!ostree_validate_checksum_string (checksum, error))
|
|
goto out;
|
|
}
|
|
else if (g_ascii_strncasecmp (revision, "version=", 8) == 0)
|
|
{
|
|
version = revision + 8;
|
|
}
|
|
else if (ostree_validate_checksum_string (revision, NULL))
|
|
{
|
|
/* If it looks like a checksum, assume it is. */
|
|
checksum = revision;
|
|
}
|
|
else
|
|
{
|
|
/* Treat anything else as a version metadata value. */
|
|
version = revision;
|
|
}
|
|
|
|
if (out_checksum != NULL)
|
|
*out_checksum = g_strdup (checksum);
|
|
|
|
if (out_version != NULL)
|
|
*out_version = g_strdup (version);
|
|
|
|
ret = TRUE;
|
|
|
|
out:
|
|
return ret;
|
|
}
|