1
0
Fork 0
gnome-software/lib/gs-utils.c
Daniel Baumann 68ee05b3fd
Adding upstream version 48.2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 21:00:23 +02:00

1873 lines
50 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
* vi:set noexpandtab tabstop=8 shiftwidth=8:
*
* Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com>
* Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
/**
* SECTION:gs-utils
* @title: GsUtils
* @include: gnome-software.h
* @stability: Unstable
* @short_description: Utilities that plugins can use
*
* These functions provide useful functionality that makes it easy to
* add new plugin functions.
*/
#include "config.h"
#include <errno.h>
#include <fnmatch.h>
#include <math.h>
#include <string.h>
#include <glib/gi18n-lib.h>
#include <glib/gstdio.h>
#include <json-glib/json-glib.h>
#if defined(__linux__)
#include <sys/sysinfo.h>
#elif defined(__FreeBSD__)
#include <sys/types.h>
#include <sys/sysctl.h>
#endif
#ifdef HAVE_POLKIT
#include <polkit/polkit.h>
#endif
#include "gs-app.h"
#include "gs-app-private.h"
#include "gs-utils.h"
#include "gs-plugin.h"
#define MB_IN_BYTES (1024 * 1024)
/**
* gs_mkdir_parent:
* @path: A full pathname
* @error: A #GError, or %NULL
*
* Creates any required directories, including any parent directories.
*
* Returns: %TRUE for success
**/
gboolean
gs_mkdir_parent (const gchar *path, GError **error)
{
g_autofree gchar *parent = NULL;
parent = g_path_get_dirname (path);
if (g_mkdir_with_parents (parent, 0755) == -1) {
g_set_error (error,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_FAILED,
"Failed to create '%s': %s",
parent, g_strerror (errno));
return FALSE;
}
return TRUE;
}
/**
* gs_utils_get_file_age:
* @file: A #GFile
*
* Gets a file age.
*
* Returns: The time in seconds since the file was modified, or %G_MAXUINT64 for error
*/
guint64
gs_utils_get_file_age (GFile *file)
{
guint64 now;
guint64 mtime;
g_autoptr(GFileInfo) info = NULL;
info = g_file_query_info (file,
G_FILE_ATTRIBUTE_TIME_MODIFIED,
G_FILE_QUERY_INFO_NONE,
NULL,
NULL);
if (info == NULL)
return G_MAXUINT64;
mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
now = (guint64) g_get_real_time () / G_USEC_PER_SEC;
if (mtime > now)
return G_MAXUINT64;
if (now - mtime > G_MAXUINT64)
return G_MAXUINT64;
return (guint) (now - mtime);
}
static gchar *
gs_utils_filename_array_return_newest (GPtrArray *array)
{
const gchar *filename_best = NULL;
guint age_lowest = G_MAXUINT;
guint i;
for (i = 0; i < array->len; i++) {
const gchar *fn = g_ptr_array_index (array, i);
g_autoptr(GFile) file = g_file_new_for_path (fn);
guint64 age_tmp = gs_utils_get_file_age (file);
if (age_tmp < age_lowest) {
age_lowest = age_tmp;
filename_best = fn;
}
}
return g_strdup (filename_best);
}
/**
* gs_utils_get_cache_filename:
* @kind: A cache kind, e.g. "fwupd" or "screenshots/123x456"
* @resource: A resource, e.g. "system.bin" or "http://foo.bar/baz.bin"
* @flags: Some #GsUtilsCacheFlags, e.g. %GS_UTILS_CACHE_FLAG_WRITEABLE
* @error: A #GError, or %NULL
*
* Returns a filename that points into the cache.
* This may be per-system or per-user, the latter being more likely
* when %GS_UTILS_CACHE_FLAG_WRITEABLE is specified in @flags.
*
* If %GS_UTILS_CACHE_FLAG_USE_HASH is set in @flags then the returned filename
* will contain the hashed version of @resource.
*
* If there is more than one match, the file that has been modified last is
* returned.
*
* If a plugin requests a file to be saved in the cache it is the plugins
* responsibility to remove the file when it is no longer valid or is too old
* -- gnome-software will not ever clean the cache for the plugin.
* For this reason it is a good idea to use the plugin name as @kind.
*
* This function can only fail if %GS_UTILS_CACHE_FLAG_ENSURE_EMPTY or
* %GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY are passed in @flags.
*
* Returns: The full path and filename, which may or may not exist, or %NULL
**/
gchar *
gs_utils_get_cache_filename (const gchar *kind,
const gchar *resource,
GsUtilsCacheFlags flags,
GError **error)
{
const gchar *tmp;
g_autofree gchar *basename = NULL;
g_autofree gchar *cachedir = NULL;
g_autoptr(GFile) cachedir_file = NULL;
g_autoptr(GPtrArray) candidates = g_ptr_array_new_with_free_func (g_free);
g_autoptr(GError) local_error = NULL;
/* in the self tests */
tmp = g_getenv ("GS_SELF_TEST_CACHEDIR");
if (tmp != NULL) {
cachedir = g_build_filename (tmp, kind, NULL);
cachedir_file = g_file_new_for_path (cachedir);
if ((flags & GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY) &&
!g_file_make_directory_with_parents (cachedir_file, NULL, &local_error) &&
!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
g_propagate_error (error, g_steal_pointer (&local_error));
return NULL;
}
return g_build_filename (cachedir, resource, NULL);;
}
/* get basename */
if (flags & GS_UTILS_CACHE_FLAG_USE_HASH) {
g_autofree gchar *basename_tmp = g_path_get_basename (resource);
g_autofree gchar *hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1,
resource, -1);
basename = g_strdup_printf ("%s-%s", hash, basename_tmp);
} else {
basename = g_path_get_basename (resource);
}
/* not writable, so try the system cache first */
if (!(flags & GS_UTILS_CACHE_FLAG_WRITEABLE)) {
g_autofree gchar *cachefn = NULL;
cachefn = g_build_filename (LOCALSTATEDIR,
"cache",
"gnome-software",
kind,
basename,
NULL);
if (g_file_test (cachefn, G_FILE_TEST_EXISTS)) {
g_ptr_array_add (candidates,
g_steal_pointer (&cachefn));
}
}
/* create the cachedir in a per-release location, creating
* if it does not already exist */
cachedir = g_build_filename (g_get_user_cache_dir (),
"gnome-software",
kind,
NULL);
cachedir_file = g_file_new_for_path (cachedir);
if (g_file_query_exists (cachedir_file, NULL) &&
flags & GS_UTILS_CACHE_FLAG_ENSURE_EMPTY) {
if (!gs_utils_rmtree (cachedir, error))
return NULL;
}
if ((flags & GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY) &&
!g_file_query_exists (cachedir_file, NULL) &&
!g_file_make_directory_with_parents (cachedir_file, NULL, error))
return NULL;
g_ptr_array_add (candidates, g_build_filename (cachedir, basename, NULL));
/* common case: we only have one option */
if (candidates->len == 1)
return g_strdup (g_ptr_array_index (candidates, 0));
/* return the newest (i.e. one with least age) */
return gs_utils_filename_array_return_newest (candidates);
}
/**
* gs_utils_get_user_hash:
* @error: A #GError, or %NULL
*
* This SHA1 hash is composed of the contents of machine-id and your
* username and is also salted with a hardcoded value.
*
* This provides an identifier that can be used to identify a specific
* user on a machine, allowing them to cast only one vote or perform
* one review on each app.
*
* There is no known way to calculate the machine ID or username from
* the machine hash and there should be no privacy issue.
*
* Returns: The user hash, or %NULL on error
*/
gchar *
gs_utils_get_user_hash (GError **error)
{
g_autofree gchar *data = NULL;
g_autofree gchar *salted = NULL;
if (!g_file_get_contents ("/etc/machine-id", &data, NULL, error))
return NULL;
salted = g_strdup_printf ("gnome-software[%s:%s]",
g_get_user_name (), data);
return g_compute_checksum_for_string (G_CHECKSUM_SHA1, salted, -1);
}
/**
* gs_utils_get_permission:
* @id: A PolicyKit ID, e.g. "org.gnome.Desktop"
* @cancellable: A #GCancellable, or %NULL
* @error: A #GError, or %NULL
*
* Gets a permission object for an ID.
*
* Returns: a #GPermission, or %NULL if this if not possible.
**/
GPermission *
gs_utils_get_permission (const gchar *id, GCancellable *cancellable, GError **error)
{
#ifdef HAVE_POLKIT
g_autoptr(GPermission) permission = NULL;
permission = polkit_permission_new_sync (id, NULL, cancellable, error);
if (permission == NULL) {
g_prefix_error (error, "failed to create permission %s: ", id);
gs_utils_error_convert_gio (error);
return NULL;
}
return g_steal_pointer (&permission);
#else
g_set_error (error,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_NOT_SUPPORTED,
"no PolicyKit, so can't return GPermission for %s", id);
return NULL;
#endif
}
/**
* gs_utils_get_permission_async:
* @id: a polkit action ID, for example `org.freedesktop.packagekit.trigger-offline-update`
* @cancellable: (nullable): a #GCancellable, or %NULL
* @callback: callback for when the asynchronous operation is complete
* @user_data: data to pass to @callback
*
* Asynchronously gets a #GPermission object representing the given polkit
* action @id.
*
* Since: 42
*/
void
gs_utils_get_permission_async (const gchar *id,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_return_if_fail (id != NULL);
g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
#ifdef HAVE_POLKIT
polkit_permission_new (id, NULL, cancellable, callback, user_data);
#else
g_task_report_new_error (NULL, callback, user_data, gs_utils_get_permission_async,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_NOT_SUPPORTED,
"no PolicyKit, so can't return GPermission for %s", id);
#endif
}
/**
* gs_utils_get_permission_finish:
* @result: result of the asynchronous operation
* @error: return location for a #GError, or %NULL
*
* Finish an asynchronous operation started with gs_utils_get_permission_async().
*
* Returns: (transfer full): a #GPermission representing the given action ID
* Since: 42
*/
GPermission *
gs_utils_get_permission_finish (GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
#ifdef HAVE_POLKIT
return polkit_permission_new_finish (result, error);
#else
return g_task_propagate_pointer (G_TASK (result), error);
#endif
}
/**
* gs_utils_get_content_type:
* @file: A GFile
* @cancellable: A #GCancellable, or %NULL
* @error: A #GError, or %NULL
*
* Gets the standard content type for a file.
*
* Returns: the content type, or %NULL, e.g. "text/plain"
*/
gchar *
gs_utils_get_content_type (GFile *file,
GCancellable *cancellable,
GError **error)
{
const gchar *tmp;
g_autoptr(GFileInfo) info = NULL;
/* get content type */
info = g_file_query_info (file,
G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
G_FILE_QUERY_INFO_NONE,
cancellable,
error);
if (info == NULL)
return NULL;
tmp = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
if (tmp == NULL)
return NULL;
return g_strdup (tmp);
}
/**
* gs_utils_get_content_type_async:
* @file: a #GFile
* @cancellable: a #GCancellable, or %NULL
* @callback: callback for when the asynchronous operation is complete
* @user_data: data to pass to @callback
*
* Asynchronously get the standard content type for the @file.
* Finish the operation with @gs_utils_get_content_type_finish.
*
* Since: 47
*/
void
gs_utils_get_content_type_async (GFile *file,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_return_if_fail (G_IS_FILE (file));
g_file_query_info_async (file,
G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
G_FILE_QUERY_INFO_NONE,
G_PRIORITY_DEFAULT,
cancellable,
callback,
user_data);
}
/**
* gs_utils_get_content_type_finish:
* @file: a #GFile
* @result: result of the asynchronous operation
* @error: return location for a #GError, or %NULL
*
* Finish an asynchronous operation started with gs_utils_get_content_type_async().
*
* Returns: the content type, or %NULL, e.g. "text/plain"
*
* Since: 47
**/
gchar *
gs_utils_get_content_type_finish (GFile *file,
GAsyncResult *result,
GError **error)
{
const gchar *tmp;
g_autoptr(GFileInfo) info = NULL;
info = g_file_query_info_finish (file, result, error);
if (info == NULL)
return NULL;
tmp = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
if (tmp == NULL)
return NULL;
return g_strdup (tmp);
}
/**
* gs_utils_strv_fnmatch:
* @strv: A NUL-terminated list of strings
* @str: A string
*
* Matches a string against a list of globs.
*
* Returns: %TRUE if the list matches
*/
gboolean
gs_utils_strv_fnmatch (gchar **strv, const gchar *str)
{
guint i;
/* empty */
if (strv == NULL)
return FALSE;
/* look at each one */
for (i = 0; strv[i] != NULL; i++) {
if (fnmatch (strv[i], str, 0) == 0)
return TRUE;
}
return FALSE;
}
/**
* gs_utils_sort_key:
* @str: A string to convert to a sort key
*
* Useful to sort strings in a locale-sensitive, presentational way.
* Case is ignored and utf8 collation is used (e.g. accents are ignored).
*
* Returns: a newly allocated string sort key
*/
gchar *
gs_utils_sort_key (const gchar *str)
{
g_autofree gchar *casefolded = g_utf8_casefold (str, -1);
return g_utf8_collate_key (casefolded, -1);
}
/**
* gs_utils_sort_strcmp:
* @str1: (nullable): A string to compare
* @str2: (nullable): A string to compare
*
* Compares two strings in a locale-sensitive, presentational way.
* Case is ignored and utf8 collation is used (e.g. accents are ignored). %NULL
* is sorted before all non-%NULL strings, and %NULLs compare equal.
*
* Returns: < 0 if str1 is before str2, 0 if equal, > 0 if str1 is after str2
*/
gint
gs_utils_sort_strcmp (const gchar *str1, const gchar *str2)
{
g_autofree gchar *key1 = (str1 != NULL) ? gs_utils_sort_key (str1) : NULL;
g_autofree gchar *key2 = (str2 != NULL) ? gs_utils_sort_key (str2) : NULL;
return g_strcmp0 (key1, key2);
}
/**
* gs_utils_get_desktop_app_info:
* @id: A desktop ID, e.g. "gimp.desktop"
*
* Gets a a #GDesktopAppInfo taking into account the kde4- prefix.
* If the given @id doesn not have a ".desktop" suffix, it will add one to it
* for convenience.
*
* Returns: a #GDesktopAppInfo for a specific ID, or %NULL
*/
GDesktopAppInfo *
gs_utils_get_desktop_app_info (const gchar *id)
{
GDesktopAppInfo *app_info;
g_autofree gchar *desktop_id = NULL;
/* for convenience, if the given id doesn't have the required .desktop
* suffix, we add it here */
if (!g_str_has_suffix (id, ".desktop")) {
desktop_id = g_strconcat (id, ".desktop", NULL);
id = desktop_id;
}
/* try to get the standard app-id */
app_info = g_desktop_app_info_new (id);
/* KDE is a special project because it believes /usr/share/applications
* isn't KDE enough. For this reason we support falling back to the
* "kde4-" prefixed ID to avoid educating various self-righteous
* upstreams about the correct ID to use in the AppData file. */
if (app_info == NULL) {
g_autofree gchar *kde_id = NULL;
kde_id = g_strdup_printf ("%s-%s", "kde4", id);
app_info = g_desktop_app_info_new (kde_id);
}
return app_info;
}
/**
* gs_utils_symlink:
* @target: the full path of the symlink to create
* @linkpath: where the symlink should point to
* @error: A #GError, or %NULL
*
* Creates a symlink that can cross filesystem boundaries.
* Any parent directories needed for target to exist are also created.
*
* Returns: %TRUE for success
**/
gboolean
gs_utils_symlink (const gchar *target, const gchar *linkpath, GError **error)
{
if (!gs_mkdir_parent (target, error))
return FALSE;
if (symlink (target, linkpath) != 0) {
g_set_error (error,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_WRITE_FAILED,
"failed to create symlink from %s to %s",
linkpath, target);
return FALSE;
}
return TRUE;
}
/**
* gs_utils_unlink:
* @filename: A full pathname to delete
* @error: A #GError, or %NULL
*
* Deletes a file from disk.
*
* Returns: %TRUE for success
**/
gboolean
gs_utils_unlink (const gchar *filename, GError **error)
{
if (g_unlink (filename) != 0) {
gint err = errno;
g_set_error (error,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_DELETE_FAILED,
_("Failed to delete file “%s”: %s"), filename, g_strerror (err));
return FALSE;
}
return TRUE;
}
static gboolean
gs_utils_rmtree_real (const gchar *directory, GError **error)
{
const gchar *filename;
g_autoptr(GDir) dir = NULL;
/* try to open */
dir = g_dir_open (directory, 0, error);
if (dir == NULL)
return FALSE;
/* find each */
while ((filename = g_dir_read_name (dir))) {
g_autofree gchar *src = NULL;
src = g_build_filename (directory, filename, NULL);
if (g_file_test (src, G_FILE_TEST_IS_DIR) &&
!g_file_test (src, G_FILE_TEST_IS_SYMLINK)) {
if (!gs_utils_rmtree_real (src, error))
return FALSE;
} else {
if (g_unlink (src) != 0) {
gint err = errno;
g_set_error (error,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_DELETE_FAILED,
_("Failed to delete file “%s”: %s"), src, g_strerror (err));
return FALSE;
}
}
}
if (g_rmdir (directory) != 0) {
gint err = errno;
g_set_error (error,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_DELETE_FAILED,
_("Failed to delete directory “%s”: %s"), directory, g_strerror (err));
return FALSE;
}
return TRUE;
}
/**
* gs_utils_rmtree:
* @directory: A full directory pathname to delete
* @error: A #GError, or %NULL
*
* Deletes a directory from disk and all its contents.
*
* Returns: %TRUE for success
**/
gboolean
gs_utils_rmtree (const gchar *directory, GError **error)
{
g_debug ("recursively removing directory '%s'", directory);
return gs_utils_rmtree_real (directory, error);
}
static gdouble
pnormaldist (gdouble qn)
{
static gdouble b[11] = { 1.570796288, 0.03706987906, -0.8364353589e-3,
-0.2250947176e-3, 0.6841218299e-5, 0.5824238515e-5,
-0.104527497e-5, 0.8360937017e-7, -0.3231081277e-8,
0.3657763036e-10, 0.6936233982e-12 };
gdouble w1, w3;
guint i;
if (qn < 0 || qn > 1)
return 0; // This is an error case
if (qn == 0.5)
return 0;
w1 = qn;
if (qn > 0.5)
w1 = 1.0 - w1;
w3 = -log (4.0 * w1 * (1.0 - w1));
w1 = b[0];
for (i = 1; i < 11; i++)
w1 = w1 + (b[i] * pow (w3, i));
if (qn > 0.5)
return sqrt (w1 * w3);
else
return -sqrt (w1 * w3);
}
static gdouble
wilson_score (gdouble value, gdouble n, gdouble power)
{
gdouble z, phat;
if (value == 0)
return 0;
z = pnormaldist (1 - power / 2);
phat = value / n;
return (phat + z * z / (2 * n) -
z * sqrt ((phat * (1 - phat) + z * z / (4 * n)) / n)) /
(1 + z * z / n);
}
/**
* gs_utils_get_wilson_rating:
* @star1: The number of 1 star reviews
* @star2: The number of 2 star reviews
* @star3: The number of 3 star reviews
* @star4: The number of 4 star reviews
* @star5: The number of 5 star reviews
*
* Returns the lower bound of Wilson score confidence interval for a
* Bernoulli parameter. This ensures small numbers of ratings don't give overly
* high scores.
* See https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval
* for details.
*
* Returns: Wilson rating percentage, or -1 for error
**/
gint
gs_utils_get_wilson_rating (guint64 star1,
guint64 star2,
guint64 star3,
guint64 star4,
guint64 star5)
{
gdouble val;
guint64 star_sum = star1 + star2 + star3 + star4 + star5;
if (star_sum == 0)
return -1;
/* get score */
val = (wilson_score ((gdouble) star1, (gdouble) star_sum, 0.2) * -2);
val += (wilson_score ((gdouble) star2, (gdouble) star_sum, 0.2) * -1);
val += (wilson_score ((gdouble) star4, (gdouble) star_sum, 0.2) * 1);
val += (wilson_score ((gdouble) star5, (gdouble) star_sum, 0.2) * 2);
/* normalize from -2..+2 to 0..5 */
val += 3;
/* multiply to a percentage */
val *= 20;
/* return rounded up integer */
return (gint) ceil (val);
}
/**
* gs_utils_error_add_app_id:
* @error: a #GError
* @app: a #GsApp
*
* Adds app unique ID prefix to the error.
*
* Since: 3.30
**/
void
gs_utils_error_add_app_id (GError **error, GsApp *app)
{
g_return_if_fail (GS_APP (app));
if (error == NULL || *error == NULL)
return;
g_prefix_error (error, "{%s} ", gs_app_get_unique_id (app));
}
/**
* gs_utils_error_add_origin_id:
* @error: a #GError
* @origin: a #GsApp
*
* Adds origin unique ID prefix to the error.
*
* Since: 3.30
**/
void
gs_utils_error_add_origin_id (GError **error, GsApp *origin)
{
g_return_if_fail (GS_APP (origin));
if (error == NULL || *error == NULL)
return;
g_prefix_error (error, "[%s] ", gs_app_get_unique_id (origin));
}
/**
* gs_utils_error_strip_app_id:
* @error: a #GError
*
* Removes a possible app ID prefix from the error, and returns the removed
* app ID.
*
* Returns: A newly allocated string with the app ID
*
* Since: 3.30
**/
gchar *
gs_utils_error_strip_app_id (GError *error)
{
g_autofree gchar *app_id = NULL;
g_autofree gchar *msg = NULL;
if (error == NULL || error->message == NULL)
return FALSE;
if (g_str_has_prefix (error->message, "{")) {
const gchar *endp = strstr (error->message + 1, "} ");
if (endp != NULL) {
app_id = g_strndup (error->message + 1,
endp - (error->message + 1));
msg = g_strdup (endp + 2);
}
}
if (msg != NULL) {
g_free (error->message);
error->message = g_steal_pointer (&msg);
}
return g_steal_pointer (&app_id);
}
/**
* gs_utils_error_strip_origin_id:
* @error: a #GError
*
* Removes a possible origin ID prefix from the error, and returns the removed
* origin ID.
*
* Returns: A newly allocated string with the origin ID
*
* Since: 3.30
**/
gchar *
gs_utils_error_strip_origin_id (GError *error)
{
g_autofree gchar *origin_id = NULL;
g_autofree gchar *msg = NULL;
if (error == NULL || error->message == NULL)
return FALSE;
if (g_str_has_prefix (error->message, "[")) {
const gchar *endp = strstr (error->message + 1, "] ");
if (endp != NULL) {
origin_id = g_strndup (error->message + 1,
endp - (error->message + 1));
msg = g_strdup (endp + 2);
}
}
if (msg != NULL) {
g_free (error->message);
error->message = g_steal_pointer (&msg);
}
return g_steal_pointer (&origin_id);
}
/**
* gs_utils_error_convert_gdbus:
* @perror: a pointer to a #GError, or %NULL
*
* Converts the #GDBusError to an error with a GsPluginError domain.
*
* Returns: %TRUE if the error was converted, or already correct
**/
gboolean
gs_utils_error_convert_gdbus (GError **perror)
{
GError *error = perror != NULL ? *perror : NULL;
/* not set */
if (error == NULL)
return FALSE;
if (error->domain == GS_PLUGIN_ERROR)
return TRUE;
if (error->domain != G_DBUS_ERROR)
return FALSE;
switch (error->code) {
case G_DBUS_ERROR_FAILED:
case G_DBUS_ERROR_NO_REPLY:
case G_DBUS_ERROR_TIMEOUT:
error->code = GS_PLUGIN_ERROR_FAILED;
break;
case G_DBUS_ERROR_IO_ERROR:
case G_DBUS_ERROR_NAME_HAS_NO_OWNER:
case G_DBUS_ERROR_NOT_SUPPORTED:
case G_DBUS_ERROR_SERVICE_UNKNOWN:
case G_DBUS_ERROR_UNKNOWN_INTERFACE:
case G_DBUS_ERROR_UNKNOWN_METHOD:
case G_DBUS_ERROR_UNKNOWN_OBJECT:
case G_DBUS_ERROR_UNKNOWN_PROPERTY:
error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
break;
case G_DBUS_ERROR_NO_MEMORY:
error->code = GS_PLUGIN_ERROR_NO_SPACE;
break;
case G_DBUS_ERROR_ACCESS_DENIED:
case G_DBUS_ERROR_AUTH_FAILED:
error->code = GS_PLUGIN_ERROR_NO_SECURITY;
break;
case G_DBUS_ERROR_NO_NETWORK:
error->code = GS_PLUGIN_ERROR_NO_NETWORK;
break;
case G_DBUS_ERROR_INVALID_FILE_CONTENT:
error->code = GS_PLUGIN_ERROR_INVALID_FORMAT;
break;
default:
g_warning ("can't reliably fixup error code %i in domain %s: %s",
error->code, g_quark_to_string (error->domain),
error->message);
error->code = GS_PLUGIN_ERROR_FAILED;
break;
}
error->domain = GS_PLUGIN_ERROR;
return TRUE;
}
/**
* gs_utils_error_convert_gio:
* @perror: a pointer to a #GError, or %NULL
*
* Converts the #GIOError to an error with a GsPluginError domain.
*
* Returns: %TRUE if the error was converted, or already correct
**/
gboolean
gs_utils_error_convert_gio (GError **perror)
{
GError *error = perror != NULL ? *perror : NULL;
/* not set */
if (error == NULL)
return FALSE;
if (error->domain == GS_PLUGIN_ERROR)
return TRUE;
if (error->domain != G_IO_ERROR)
return FALSE;
switch (error->code) {
case G_IO_ERROR_FAILED:
case G_IO_ERROR_NOT_FOUND:
case G_IO_ERROR_EXISTS:
error->code = GS_PLUGIN_ERROR_FAILED;
break;
case G_IO_ERROR_TIMED_OUT:
error->code = GS_PLUGIN_ERROR_TIMED_OUT;
break;
case G_IO_ERROR_NOT_SUPPORTED:
error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
break;
case G_IO_ERROR_CANCELLED:
error->code = GS_PLUGIN_ERROR_CANCELLED;
break;
case G_IO_ERROR_NO_SPACE:
error->code = GS_PLUGIN_ERROR_NO_SPACE;
break;
case G_IO_ERROR_PERMISSION_DENIED:
error->code = GS_PLUGIN_ERROR_NO_SECURITY;
break;
case G_IO_ERROR_HOST_NOT_FOUND:
case G_IO_ERROR_HOST_UNREACHABLE:
case G_IO_ERROR_CONNECTION_REFUSED:
case G_IO_ERROR_PROXY_FAILED:
case G_IO_ERROR_PROXY_AUTH_FAILED:
case G_IO_ERROR_PROXY_NOT_ALLOWED:
error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED;
break;
case G_IO_ERROR_NETWORK_UNREACHABLE:
error->code = GS_PLUGIN_ERROR_NO_NETWORK;
break;
default:
g_warning ("can't reliably fixup error code %i in domain %s: %s",
error->code, g_quark_to_string (error->domain),
error->message);
error->code = GS_PLUGIN_ERROR_FAILED;
break;
}
error->domain = GS_PLUGIN_ERROR;
return TRUE;
}
/**
* gs_utils_error_convert_gresolver:
* @perror: a pointer to a #GError, or %NULL
*
* Converts the #GResolverError to an error with a GsPluginError domain.
*
* Returns: %TRUE if the error was converted, or already correct
**/
gboolean
gs_utils_error_convert_gresolver (GError **perror)
{
GError *error = perror != NULL ? *perror : NULL;
/* not set */
if (error == NULL)
return FALSE;
if (error->domain == GS_PLUGIN_ERROR)
return TRUE;
if (error->domain != G_RESOLVER_ERROR)
return FALSE;
switch (error->code) {
case G_RESOLVER_ERROR_INTERNAL:
error->code = GS_PLUGIN_ERROR_FAILED;
break;
case G_RESOLVER_ERROR_NOT_FOUND:
case G_RESOLVER_ERROR_TEMPORARY_FAILURE:
error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED;
break;
default:
g_warning ("can't reliably fixup error code %i in domain %s: %s",
error->code, g_quark_to_string (error->domain),
error->message);
error->code = GS_PLUGIN_ERROR_FAILED;
break;
}
error->domain = GS_PLUGIN_ERROR;
return TRUE;
}
/**
* gs_utils_error_convert_gdk_pixbuf:
* @perror: a pointer to a #GError, or %NULL
*
* Converts the #GdkPixbufError to an error with a GsPluginError domain.
*
* Returns: %TRUE if the error was converted, or already correct
**/
gboolean
gs_utils_error_convert_gdk_pixbuf (GError **perror)
{
GError *error = perror != NULL ? *perror : NULL;
/* not set */
if (error == NULL)
return FALSE;
if (error->domain == GS_PLUGIN_ERROR)
return TRUE;
if (error->domain != GDK_PIXBUF_ERROR)
return FALSE;
switch (error->code) {
case GDK_PIXBUF_ERROR_UNSUPPORTED_OPERATION:
case GDK_PIXBUF_ERROR_UNKNOWN_TYPE:
error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
break;
case GDK_PIXBUF_ERROR_FAILED:
error->code = GS_PLUGIN_ERROR_FAILED;
break;
case GDK_PIXBUF_ERROR_CORRUPT_IMAGE:
error->code = GS_PLUGIN_ERROR_INVALID_FORMAT;
break;
default:
g_warning ("can't reliably fixup error code %i in domain %s: %s",
error->code, g_quark_to_string (error->domain),
error->message);
error->code = GS_PLUGIN_ERROR_FAILED;
break;
}
error->domain = GS_PLUGIN_ERROR;
return TRUE;
}
/**
* gs_utils_error_convert_appstream:
* @perror: a pointer to a #GError, or %NULL
*
* Converts the various AppStream error types to an error with a GsPluginError
* domain.
*
* Returns: %TRUE if the error was converted, or already correct
**/
gboolean
gs_utils_error_convert_appstream (GError **perror)
{
GError *error = perror != NULL ? *perror : NULL;
/* not set */
if (error == NULL)
return FALSE;
if (error->domain == GS_PLUGIN_ERROR)
return TRUE;
/* custom to this plugin */
if (error->domain == AS_METADATA_ERROR) {
switch (error->code) {
case AS_METADATA_ERROR_PARSE:
case AS_METADATA_ERROR_FORMAT_UNEXPECTED:
case AS_METADATA_ERROR_NO_COMPONENT:
error->code = GS_PLUGIN_ERROR_INVALID_FORMAT;
break;
case AS_METADATA_ERROR_FAILED:
default:
error->code = GS_PLUGIN_ERROR_FAILED;
break;
}
} else if (error->domain == AS_POOL_ERROR) {
switch (error->code) {
case AS_POOL_ERROR_FAILED:
default:
error->code = GS_PLUGIN_ERROR_FAILED;
break;
}
} else if (error->domain == G_FILE_ERROR) {
switch (error->code) {
case G_FILE_ERROR_EXIST:
case G_FILE_ERROR_ACCES:
case G_FILE_ERROR_PERM:
error->code = GS_PLUGIN_ERROR_NO_SECURITY;
break;
case G_FILE_ERROR_NOSPC:
error->code = GS_PLUGIN_ERROR_NO_SPACE;
break;
default:
error->code = GS_PLUGIN_ERROR_FAILED;
break;
}
} else {
g_warning ("can't reliably fixup error code %i in domain %s: %s",
error->code, g_quark_to_string (error->domain),
error->message);
error->code = GS_PLUGIN_ERROR_FAILED;
}
error->domain = GS_PLUGIN_ERROR;
return TRUE;
}
/**
* gs_utils_get_url_scheme:
* @url: A URL, e.g. "appstream://gimp.desktop"
*
* Gets the scheme from the URL string.
*
* Returns: the URL scheme, e.g. "appstream"
*/
gchar *
gs_utils_get_url_scheme (const gchar *url)
{
g_autoptr(GUri) uri = NULL;
/* no data */
if (url == NULL)
return NULL;
/* create URI from URL */
uri = g_uri_parse (url, SOUP_HTTP_URI_FLAGS, NULL);
if (!uri)
return NULL;
/* success */
return g_strdup (g_uri_get_scheme (uri));
}
/**
* gs_utils_get_url_path:
* @url: A URL, e.g. "appstream://gimp.desktop"
*
* Gets the path from the URL string, removing any leading slashes.
*
* Returns: the URL path, e.g. "gimp.desktop"
*/
gchar *
gs_utils_get_url_path (const gchar *url)
{
g_autoptr(GUri) uri = NULL;
const gchar *host;
const gchar *path;
uri = g_uri_parse (url, SOUP_HTTP_URI_FLAGS, NULL);
if (!uri)
return NULL;
/* foo://bar -> scheme: foo, host: bar, path: / */
/* foo:bar -> scheme: foo, host: (empty string), path: /bar */
host = g_uri_get_host (uri);
path = g_uri_get_path (uri);
if (host != NULL && *host != '\0')
path = host;
/* trim any leading slashes */
while (*path == '/')
path++;
/* success */
return g_strdup (path);
}
/**
* gs_user_agent:
*
* Gets the user agent to use for remote requests.
*
* Returns: the user-agent, e.g. "gnome-software/3.22.1"
*/
const gchar *
gs_user_agent (void)
{
return PACKAGE_NAME "/" PACKAGE_VERSION;
}
/**
* gs_utils_append_key_value:
* @str: A #GString
* @align_len: The alignment of the @value compared to the @key
* @key: The text to use as a title
* @value: The text to use as a value
*
* Adds a line to an existing string, padding the key to a set number of spaces.
*
* Since: 3.26
*/
void
gs_utils_append_key_value (GString *str, gsize align_len,
const gchar *key, const gchar *value)
{
gsize len = 0;
g_return_if_fail (str != NULL);
g_return_if_fail (value != NULL);
if (key != NULL) {
len = strlen (key) + 2;
g_string_append (str, key);
g_string_append (str, ": ");
}
for (gsize i = len; i < align_len + 1; i++)
g_string_append (str, " ");
g_string_append (str, value);
g_string_append (str, "\n");
}
guint
gs_utils_get_memory_total (void)
{
#if defined(__linux__)
struct sysinfo si = { 0 };
sysinfo (&si);
if (si.mem_unit > 0)
return si.totalram / MB_IN_BYTES / si.mem_unit;
return 0;
#elif defined(__FreeBSD__)
unsigned long physmem;
sysctl ((int[]){ CTL_HW, HW_PHYSMEM }, 2, &physmem, &(size_t){ sizeof (physmem) }, NULL, 0);
return physmem / MB_IN_BYTES;
#else
#error "Please implement gs_utils_get_memory_total for your system."
#endif
}
/**
* gs_utils_unique_id_compat_convert:
* @data_id: (nullable): A string that may be a unique component ID
*
* Converts the unique ID string from its legacy 6-part form into
* a new-style 5-part AppStream data-id.
* Does nothing if the string is already valid.
*
* See !583 for the history of this conversion.
*
* Returns: (nullable): A newly allocated string with the new-style data-id, or %NULL if input was no valid ID.
*
* Since: 40
**/
gchar*
gs_utils_unique_id_compat_convert (const gchar *data_id)
{
g_auto(GStrv) parts = NULL;
if (data_id == NULL)
return NULL;
/* check for the most common case first: data-id is already valid */
if (as_utils_data_id_valid (data_id))
return g_strdup (data_id);
parts = g_strsplit (data_id, "/", -1);
if (g_strv_length (parts) != 6)
return NULL;
return g_strdup_printf ("%s/%s/%s/%s/%s",
parts[0],
parts[1],
parts[2],
parts[4],
parts[5]);
}
static const gchar *
_fix_data_id_part (const gchar *value)
{
if (!value || !*value)
return "*";
return value;
}
/**
* gs_utils_build_unique_id:
* @scope: Scope of the metadata as #AsComponentScope e.g. %AS_COMPONENT_SCOPE_SYSTEM
* @bundle_kind: Bundling system providing this data, e.g. 'package' or 'flatpak'
* @origin: Origin string, e.g. 'os' or 'gnome-apps-nightly'
* @cid: AppStream component ID, e.g. 'org.freedesktop.appstream.cli'
* @branch: Branch, e.g. '3-20' or 'master'
*
* Builds an identifier string unique to the individual dataset using the supplied information.
* It's similar to as_utils_build_data_id(), except it respects the @origin for the packages.
*
* Returns: (transfer full): a unique ID, free with g_free(), when no longer needed.
*
* Since: 41
*/
gchar *
gs_utils_build_unique_id (AsComponentScope scope,
AsBundleKind bundle_kind,
const gchar *origin,
const gchar *cid,
const gchar *branch)
{
const gchar *scope_str = NULL;
const gchar *bundle_str = NULL;
if (scope != AS_COMPONENT_SCOPE_UNKNOWN)
scope_str = as_component_scope_to_string (scope);
if (bundle_kind != AS_BUNDLE_KIND_UNKNOWN)
bundle_str = as_bundle_kind_to_string (bundle_kind);
return g_strdup_printf ("%s/%s/%s/%s/%s",
_fix_data_id_part (scope_str),
_fix_data_id_part (bundle_str),
_fix_data_id_part (origin),
_fix_data_id_part (cid),
_fix_data_id_part (branch));
}
static void
gs_pixbuf_blur_private (GdkPixbuf *src, GdkPixbuf *dest, guint radius, guint8 *div_kernel_size)
{
gint width, height, src_rowstride, dest_rowstride, n_channels;
guchar *p_src, *p_dest, *c1, *c2;
gint x, y, i, i1, i2, width_minus_1, height_minus_1, radius_plus_1;
gint r, g, b;
guchar *p_dest_row, *p_dest_col;
width = gdk_pixbuf_get_width (src);
height = gdk_pixbuf_get_height (src);
n_channels = gdk_pixbuf_get_n_channels (src);
radius_plus_1 = radius + 1;
/* horizontal blur */
p_src = gdk_pixbuf_get_pixels (src);
p_dest = gdk_pixbuf_get_pixels (dest);
src_rowstride = gdk_pixbuf_get_rowstride (src);
dest_rowstride = gdk_pixbuf_get_rowstride (dest);
width_minus_1 = width - 1;
for (y = 0; y < height; y++) {
/* calc the initial sums of the kernel */
r = g = b = 0;
for (i = -radius; i <= (gint) radius; i++) {
c1 = p_src + (CLAMP (i, 0, width_minus_1) * n_channels);
r += c1[0];
g += c1[1];
b += c1[2];
}
p_dest_row = p_dest;
for (x = 0; x < width; x++) {
/* set as the mean of the kernel */
p_dest_row[0] = div_kernel_size[r];
p_dest_row[1] = div_kernel_size[g];
p_dest_row[2] = div_kernel_size[b];
p_dest_row += n_channels;
/* the pixel to add to the kernel */
i1 = x + radius_plus_1;
if (i1 > width_minus_1)
i1 = width_minus_1;
c1 = p_src + (i1 * n_channels);
/* the pixel to remove from the kernel */
i2 = x - radius;
if (i2 < 0)
i2 = 0;
c2 = p_src + (i2 * n_channels);
/* calc the new sums of the kernel */
r += c1[0] - c2[0];
g += c1[1] - c2[1];
b += c1[2] - c2[2];
}
p_src += src_rowstride;
p_dest += dest_rowstride;
}
/* vertical blur */
p_src = gdk_pixbuf_get_pixels (dest);
p_dest = gdk_pixbuf_get_pixels (src);
src_rowstride = gdk_pixbuf_get_rowstride (dest);
dest_rowstride = gdk_pixbuf_get_rowstride (src);
height_minus_1 = height - 1;
for (x = 0; x < width; x++) {
/* calc the initial sums of the kernel */
r = g = b = 0;
for (i = -radius; i <= (gint) radius; i++) {
c1 = p_src + (CLAMP (i, 0, height_minus_1) * src_rowstride);
r += c1[0];
g += c1[1];
b += c1[2];
}
p_dest_col = p_dest;
for (y = 0; y < height; y++) {
/* set as the mean of the kernel */
p_dest_col[0] = div_kernel_size[r];
p_dest_col[1] = div_kernel_size[g];
p_dest_col[2] = div_kernel_size[b];
p_dest_col += dest_rowstride;
/* the pixel to add to the kernel */
i1 = y + radius_plus_1;
if (i1 > height_minus_1)
i1 = height_minus_1;
c1 = p_src + (i1 * src_rowstride);
/* the pixel to remove from the kernel */
i2 = y - radius;
if (i2 < 0)
i2 = 0;
c2 = p_src + (i2 * src_rowstride);
/* calc the new sums of the kernel */
r += c1[0] - c2[0];
g += c1[1] - c2[1];
b += c1[2] - c2[2];
}
p_src += n_channels;
p_dest += n_channels;
}
}
/**
* gs_utils_pixbuf_blur:
* @src: the GdkPixbuf.
* @radius: the pixel radius for the gaussian blur, typical values are 1..3
* @iterations: Amount to blur the image, typical values are 1..5
*
* Blurs an image. Warning, this method is s..l..o..w... for large images.
**/
void
gs_utils_pixbuf_blur (GdkPixbuf *src, guint radius, guint iterations)
{
gint kernel_size;
gint i;
g_autofree guchar *div_kernel_size = NULL;
g_autoptr(GdkPixbuf) tmp = NULL;
tmp = gdk_pixbuf_new (gdk_pixbuf_get_colorspace (src),
gdk_pixbuf_get_has_alpha (src),
gdk_pixbuf_get_bits_per_sample (src),
gdk_pixbuf_get_width (src),
gdk_pixbuf_get_height (src));
kernel_size = 2 * radius + 1;
div_kernel_size = g_new (guchar, 256 * kernel_size);
for (i = 0; i < 256 * kernel_size; i++)
div_kernel_size[i] = (guchar) (i / kernel_size);
while (iterations-- > 0)
gs_pixbuf_blur_private (src, tmp, radius, div_kernel_size);
}
/**
* gs_utils_get_file_size:
* @filename: a file name to get the size of; it can be a file or a directory
* @include_func: (nullable) (scope call): optional callback to limit what files to count
* @user_data: user data passed to the @include_func
* @cancellable: (nullable): an optional #GCancellable or %NULL
*
* Gets the size of the file or a directory identified by @filename.
*
* When the @include_func is not %NULL, it can limit which files are included
* in the resulting size. When it's %NULL, all files and subdirectories are included.
*
* Returns: disk size of the @filename; or 0 when not found
*
* Since: 41
**/
guint64
gs_utils_get_file_size (const gchar *filename,
GsFileSizeIncludeFunc include_func,
gpointer user_data,
GCancellable *cancellable)
{
guint64 size = 0;
g_return_val_if_fail (filename != NULL, 0);
if (g_file_test (filename, G_FILE_TEST_IS_DIR)) {
GSList *dirs_to_do = NULL;
gsize base_len = strlen (filename);
/* The `include_func()` expects a path relative to the `filename`, without
a leading dir separator. As the `dirs_to_do` contains the full path,
constructed with `g_build_filename()`, the added dir separator needs
to be skipped, when it's not part of the `filename` already. */
if (!g_str_has_suffix (filename, G_DIR_SEPARATOR_S))
base_len++;
dirs_to_do = g_slist_prepend (dirs_to_do, g_strdup (filename));
while (dirs_to_do != NULL && !g_cancellable_is_cancelled (cancellable)) {
g_autofree gchar *path = NULL;
g_autoptr(GDir) dir = NULL;
/* Steal the top `path` out of the `dirs_to_do`. */
path = dirs_to_do->data;
dirs_to_do = g_slist_remove (dirs_to_do, path);
dir = g_dir_open (path, 0, NULL);
if (dir) {
const gchar *name;
while (name = g_dir_read_name (dir), name != NULL && !g_cancellable_is_cancelled (cancellable)) {
g_autofree gchar *full_path = g_build_filename (path, name, NULL);
GStatBuf st;
if (g_stat (full_path, &st) == 0 && (include_func == NULL ||
include_func (full_path + base_len,
g_file_test (full_path, G_FILE_TEST_IS_SYMLINK) ? G_FILE_TEST_IS_SYMLINK :
S_ISDIR (st.st_mode) ? G_FILE_TEST_IS_DIR :
G_FILE_TEST_IS_REGULAR,
user_data))) {
if (S_ISDIR (st.st_mode)) {
/* Skip symlinks, they can point to a shared storage */
if (!g_file_test (full_path, G_FILE_TEST_IS_SYMLINK))
dirs_to_do = g_slist_prepend (dirs_to_do, g_steal_pointer (&full_path));
} else {
size += st.st_size;
}
}
}
}
}
g_slist_free_full (dirs_to_do, g_free);
} else {
GStatBuf st;
if (g_stat (filename, &st) == 0)
size = st.st_size;
}
return size;
}
#define METADATA_ETAG_ATTRIBUTE "xattr::gnome-software::etag"
/**
* gs_utils_get_file_etag:
* @file: a file to get the ETag for
* @last_modified_date_out: (out callee-allocates) (transfer full) (optional) (nullable):
* return location for the last modified date of the file (%NULL to ignore),
* or %NULL if unknown
* @cancellable: (nullable): an optional #GCancellable or %NULL
*
* Gets the ETag for the @file, previously stored by
* gs_utils_set_file_etag().
*
* Returns: (nullable) (transfer full): The ETag stored for the @file,
* or %NULL, when the file does not exist, no ETag is stored for it
* or other error occurs.
*
* Since: 43
**/
gchar *
gs_utils_get_file_etag (GFile *file,
GDateTime **last_modified_date_out,
GCancellable *cancellable)
{
g_autoptr(GFileInfo) info = NULL;
const gchar *attributes;
g_autoptr(GError) local_error = NULL;
g_return_val_if_fail (G_IS_FILE (file), NULL);
g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
if (last_modified_date_out == NULL)
attributes = METADATA_ETAG_ATTRIBUTE;
else
attributes = METADATA_ETAG_ATTRIBUTE "," G_FILE_ATTRIBUTE_TIME_MODIFIED;
info = g_file_query_info (file, attributes, G_FILE_QUERY_INFO_NONE, cancellable, &local_error);
if (info == NULL) {
g_debug ("Error getting attribute %s for file %s: %s",
METADATA_ETAG_ATTRIBUTE, g_file_peek_path (file), local_error->message);
if (last_modified_date_out != NULL)
*last_modified_date_out = NULL;
return NULL;
}
if (last_modified_date_out != NULL)
*last_modified_date_out = g_file_info_get_modification_date_time (info);
return g_strdup (g_file_info_get_attribute_string (info, METADATA_ETAG_ATTRIBUTE));
}
/**
* gs_utils_set_file_etag:
* @file: a file to get the ETag for
* @etag: (nullable): an ETag to set
* @cancellable: (nullable): an optional #GCancellable or %NULL
*
* Sets the ETag for the @file. When the @etag is %NULL or an empty
* string, then unsets the ETag for the @file. The ETag can be read
* back with gs_utils_get_file_etag().
*
* The @file should exist, otherwise the function fails.
*
* Returns: whether succeeded.
*
* Since: 42
**/
gboolean
gs_utils_set_file_etag (GFile *file,
const gchar *etag,
GCancellable *cancellable)
{
g_autoptr(GError) local_error = NULL;
g_return_val_if_fail (G_IS_FILE (file), FALSE);
g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
if (etag == NULL || *etag == '\0') {
if (!g_file_set_attribute (file, METADATA_ETAG_ATTRIBUTE, G_FILE_ATTRIBUTE_TYPE_INVALID,
NULL, G_FILE_QUERY_INFO_NONE, cancellable, &local_error)) {
g_debug ("Error clearing attribute %s on file %s: %s",
METADATA_ETAG_ATTRIBUTE, g_file_peek_path (file), local_error->message);
return FALSE;
}
return TRUE;
}
if (!g_file_set_attribute_string (file, METADATA_ETAG_ATTRIBUTE, etag, G_FILE_QUERY_INFO_NONE, cancellable, &local_error)) {
g_debug ("Error setting attribute %s to %s on file %s: %s",
METADATA_ETAG_ATTRIBUTE, etag, g_file_peek_path (file), local_error->message);
return FALSE;
}
return TRUE;
}
/**
* gs_utils_get_upgrade_background:
* @version: (nullable): version string of the upgrade (which must be non-empty
* if provided), or %NULL if unknown
*
* Get the path to a background image to display as the background for a banner
* advertising an upgrade to the given @version.
*
* If a path is returned, its guaranteed to exist on the file system.
*
* Vendors can drop their customised backgrounds in this directory for them to
* be used by gnome-software. See `doc/vendor-customisation.md`.
*
* Returns: (transfer full) (type filename) (nullable): path to an upgrade
* background image to use, or %NULL if a suitable one didnt exist
* Since: 42
*/
gchar *
gs_utils_get_upgrade_background (const gchar *version)
{
g_autofree gchar *filename = NULL;
g_autofree gchar *os_id = g_get_os_info (G_OS_INFO_KEY_ID);
g_return_val_if_fail (version == NULL || *version != '\0', NULL);
if (version != NULL) {
filename = g_strdup_printf (DATADIR "/gnome-software/backgrounds/%s-%s.png", os_id, version);
if (g_file_test (filename, G_FILE_TEST_EXISTS))
return g_steal_pointer (&filename);
g_clear_pointer (&filename, g_free);
}
filename = g_strdup_printf (DATADIR "/gnome-software/backgrounds/%s.png", os_id);
if (g_file_test (filename, G_FILE_TEST_EXISTS))
return g_steal_pointer (&filename);
g_clear_pointer (&filename, g_free);
return NULL;
}
/**
* gs_utils_app_sort_name:
* @app1: a #GsApp
* @app2: another #GsApp
* @user_data: data passed to the sort function
*
* Comparison function to sort apps in increasing alphabetical order of name.
*
* This is suitable for passing to gs_app_list_sort().
*
* Returns: a strcmp()-style sort value comparing @app1 to @app2
* Since: 43
*/
gint
gs_utils_app_sort_name (GsApp *app1,
GsApp *app2,
gpointer user_data)
{
return gs_utils_sort_strcmp (gs_app_get_name (app1), gs_app_get_name (app2));
}
/**
* gs_utils_app_sort_match_value:
* @app1: a #GsApp
* @app2: another #GsApp
* @user_data: data passed to the sort function
*
* Comparison function to sort apps in decreasing order of match value
* (#GsApp:match-value).
*
* This is suitable for passing to gs_app_list_sort().
*
* Returns: a strcmp()-style sort value comparing @app1 to @app2
* Since: 43
*/
gint
gs_utils_app_sort_match_value (GsApp *app1,
GsApp *app2,
gpointer user_data)
{
return gs_app_get_match_value (app2) - gs_app_get_match_value (app1);
}
/**
* gs_utils_app_sort_priority:
* @app1: a #GsApp
* @app2: another #GsApp
* @user_data: data passed to the sort function
*
* Comparison function to sort apps in increasing order of their priority
* (#GsApp:priority).
*
* This is suitable for passing to gs_app_list_sort().
*
* Returns: a strcmp()-style sort value comparing @app1 to @app2
* Since: 43
*/
gint
gs_utils_app_sort_priority (GsApp *app1,
GsApp *app2,
gpointer user_data)
{
return gs_app_compare_priority (app1, app2);
}
/**
* gs_utils_gstring_replace:
* @str: a #GString to replace the text in
* @find: a text to find
* @replace: a text to replace the found text with
*
* Replaces all @find occurrences in @str with @replace.
*
* Since: 45
**/
void
gs_utils_gstring_replace (GString *str,
const gchar *find,
const gchar *replace)
{
#if AS_CHECK_VERSION(1, 0, 0)
as_gstring_replace (str, find, replace, 0);
#else
as_gstring_replace2 (str, find, replace, 0);
#endif
}
static gint
get_app_kind_rank (GsApp *app)
{
switch (gs_app_get_kind (app)) {
case AS_COMPONENT_KIND_DESKTOP_APP:
return 2;
case AS_COMPONENT_KIND_WEB_APP:
return 3;
case AS_COMPONENT_KIND_RUNTIME:
return 4;
case AS_COMPONENT_KIND_ADDON:
return 5;
case AS_COMPONENT_KIND_CODEC:
case AS_COMPONENT_KIND_FONT:
return 6;
case AS_COMPONENT_KIND_INPUT_METHOD:
return 7;
default:
if (gs_app_get_special_kind (app) == GS_APP_SPECIAL_KIND_OS_UPDATE)
return 1;
else
return 8;
}
}
/**
* gs_utils_app_sort_kind:
* @app1: a #GsApp
* @app2: another #GsApp
*
* Comparison function to sort apps by Appstream kind, then by
* increasing alphabetical order of name.
*
* This is useful for sorting apps with multiple kinds (E.g Updates /
* Updated pages), as opposed to category pages where all apps are of
* the same kind.
*
* Returns: < 0 if app1 is before app2, 0 if equal, > 0 if app1 is after app2
*
* Since: 47
**/
gint
gs_utils_app_sort_kind (GsApp *app1, GsApp *app2)
{
gint rank1, rank2;
rank1 = get_app_kind_rank (app1);
rank2 = get_app_kind_rank (app2);
/* sort apps by name if they are of same kind */
if (rank1 == rank2)
return gs_utils_app_sort_name (app1, app2, NULL);
return rank1 < rank2 ? -1 : 1;
}
/**
* gs_utils_compare_versions:
* @ver1: the first version string
* @ver2: the second version string
*
* Compares @ver1 and @ver2, return value as `strcmp()`, that is, a number
* below zero, when the @ver1 is before @ver2 zero, when @ver1 is the same
* as @ver2, and a number above zero, when @ver1 is lower than @ver2.
*
* Returns: a compare result of the two version string comparison
*
* Since: 48
**/
gint
gs_utils_compare_versions (const gchar *ver1,
const gchar *ver2)
{
int rc;
if (ver1 == NULL || ver2 == NULL)
return ver1 == ver2 ? 0 : ver1 == NULL ? -1 : 1;
/* compare with epoch */
rc = as_vercmp (ver1, ver2, AS_VERCMP_FLAG_NONE);
if (rc > 0) {
/* the version can sometimes end with a non-version string, common
in both strings, which can confuse the comparison, thus try without
the suffix, if such exists and is not a number. For example:
"2:2.1-61.fc40" and "2:2.1-61.1.fc40" is reported as a downgrade,
but without the common suffix ".fc40" it's correctly reported
as an update.
FIXME: Eventually this needs to be factored out into a plugin-specific
implementation so we can use
https://github.com/PackageKit/PackageKit/issues/826. */
size_t lenv1, lenv2;
lenv1 = strlen (ver1);
lenv2 = strlen (ver2);
for (size_t i = 0; i < lenv1 && i < lenv2; i++) {
if (ver1[lenv1 - i - 1] != ver2[lenv2 - i - 1] ||
ver1[lenv1 - i - 1] == '.' ||
ver1[lenv1 - i - 1] == '-') {
if (i > 0 && !g_ascii_isdigit (ver1[lenv1 - i])) {
g_autofree gchar *cut_v1 = g_strndup (ver1, lenv1 - i - 1);
g_autofree gchar *cut_v2 = g_strndup (ver2, lenv2 - i - 1);
rc = as_vercmp (cut_v1, cut_v2, AS_VERCMP_FLAG_NONE);
}
break;
}
}
}
return rc;
}