/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2012-2024 Matthias Klumpp * Copyright (C) 2014-2016 Richard Hughes * * Licensed under the GNU Lesser General Public License Version 2.1 * * 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.1 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 . */ #include "as-utils.h" #include "as-utils-private.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef G_OS_WIN32 #include #else #include #endif #include #include #include "as-version.h" #include "as-resources.h" #include "as-category.h" #include "as-metadata.h" #include "as-component-private.h" #include "as-desktop-env-data.h" #include "as-zstd-decompressor.h" /** * SECTION:as-utils * @short_description: Helper functions that are used inside libappstream * @include: appstream.h * * Functions which are used in libappstream and might be useful for others * as well. */ /** * as_utils_error_quark: * * Return value: An error quark. * * Since: 0.14.0 **/ G_DEFINE_QUARK (as-utils-error-quark, as_utils_error) /** * as_get_resource_safe: * * A threadsafe variant to obtain our GResource. */ static GResource * as_get_resource_safe (void) { static GResource *resource = NULL; if (g_once_init_enter (&resource)) { GResource *res = as_get_resource (); g_once_init_leave (&resource, res); } g_assert (resource != NULL); return resource; } /** * as_markup_strsplit_words: * @text: the text to split. * @line_len: the maximum length of the output line * * Splits up a long line into an array of smaller strings, each being no longer * than @line_len. Words are not split. * * Returns: (transfer full): lines, or %NULL in event of an error * * Since: 0.14.0 **/ gchar ** as_markup_strsplit_words (const gchar *text, guint line_len) { GPtrArray *lines; g_autoptr(GString) curline = NULL; g_auto(GStrv) tokens = NULL; gsize curline_char_count = 0; /* sanity check */ if (text == NULL) return NULL; if (text[0] == '\0') return g_strsplit (text, " ", -1); if (line_len == 0) return NULL; lines = g_ptr_array_new (); curline = g_string_new (""); /* tokenize the string */ tokens = g_strsplit (text, " ", -1); for (guint i = 0; tokens[i] != NULL; i++) { gsize token_unilen = g_utf8_strlen (tokens[i], -1); gboolean token_has_linebreak = g_strstr_len (tokens[i], -1, "\n") != NULL; /* current line plus new token is okay */ if (curline_char_count + token_unilen < line_len) { /* we can't just check for a suffix \n here, as tokens may contain internal linebreaks */ if (token_has_linebreak) { if (tokens[i][0] == '\0') g_string_append_c (curline, ' '); else g_string_append_printf (curline, "%s ", tokens[i]); g_ptr_array_add (lines, g_strdup (curline->str)); g_string_truncate (curline, 0); curline_char_count = 0; } else { g_string_append_printf (curline, "%s ", tokens[i]); curline_char_count += token_unilen + 1; } continue; } /* too long, so remove space, add newline and dump */ if (curline->len > 0) { g_string_truncate (curline, curline->len - 1); curline_char_count -= 1; } g_string_append (curline, "\n"); g_ptr_array_add (lines, g_strdup (curline->str)); g_string_truncate (curline, 0); curline_char_count = 0; if (token_has_linebreak) { g_ptr_array_add (lines, g_strdup (tokens[i])); } else { g_string_append_printf (curline, "%s ", tokens[i]); curline_char_count += token_unilen + 1; } } /* any incomplete line? */ if (curline->len > 0) { g_string_truncate (curline, curline->len - 1); g_string_append (curline, "\n"); g_ptr_array_add (lines, g_strdup (curline->str)); } /* any superfluous linebreak at the start? * (this may happen if text is just one, very long word with no spaces) */ if (lines->len > 0) { gchar *first_line = (gchar *) g_ptr_array_index (lines, 0); if (!g_str_has_prefix (text, "\n") && g_strcmp0 (first_line, "\n") == 0) g_ptr_array_remove_index (lines, 0); } g_ptr_array_add (lines, NULL); return (gchar **) g_ptr_array_free (lines, FALSE); } /** * as_sanitize_text_spaces: * @text: The text to sanitize. * * Sanitize a text string by removing extra whitespaces and all * linebreaks. This is ideal to run on a text obtained from XML * prior to passing it through to a word-wrapping function. * * Returns: The sanitized string. */ gchar * as_sanitize_text_spaces (const gchar *text) { g_auto(GStrv) strv = NULL; if (text == NULL) return NULL; strv = g_strsplit (text, "\n", -1); for (guint i = 0; strv[i] != NULL; ++i) g_strstrip (strv[i]); return g_strstrip (g_strjoinv (" ", strv)); } /** * as_markup_convert: * @markup: the XML markup to transform. * @to_kind: The markup style to convert into. * @error: A #GError or %NULL. * * Converts XML description markup into other forms of text. * * Returns: (transfer full): a newly allocated string, or %NULL on error. **/ gchar * as_markup_convert (const gchar *markup, AsMarkupKind to_kind, GError **error) { xmlDoc *doc = NULL; xmlNode *root; xmlNode *iter; gboolean ret = TRUE; gchar *formatted = NULL; GString *str = NULL; g_autofree gchar *xmldata = NULL; if (markup == NULL) return NULL; /* is this actually markup */ if (g_strrstr (markup, "<") == NULL) { formatted = g_strdup (markup); goto out; } /* if we already have XML, do nothing */ if (to_kind == AS_MARKUP_KIND_XML) goto out; /* make XML parser happy by providing a root element */ xmldata = g_strdup_printf ("%s", markup); doc = xmlParseDoc ((xmlChar *) xmldata); if (doc == NULL) { ret = FALSE; goto out; } root = xmlDocGetRootElement (doc); if (root == NULL) { /* document was empty */ ret = FALSE; goto out; } str = g_string_new (""); for (iter = root->children; iter != NULL; iter = iter->next) { xmlNode *iter2; /* discard spaces */ if (iter->type != XML_ELEMENT_NODE) continue; if (g_strcmp0 ((gchar *) iter->name, "p") == 0) { g_autofree gchar *clean_text = NULL; g_autofree gchar *text_content = as_xml_get_node_value_raw (iter); /* Apparently the element is empty, which is odd. But we better add it instead * of completely ignoring it. */ if (text_content == NULL) text_content = g_strdup (""); /* remove extra whitespaces and linebreaks */ clean_text = as_sanitize_text_spaces (text_content); if (str->len > 0) g_string_append (str, "\n"); if (to_kind == AS_MARKUP_KIND_MARKDOWN) { g_auto(GStrv) spl = as_markup_strsplit_words (clean_text, 100); if (spl != NULL) { for (guint i = 0; spl[i] != NULL; i++) g_string_append (str, spl[i]); } } else { g_string_append_printf (str, "%s\n", clean_text); } } else if ((g_strcmp0 ((gchar *) iter->name, "ul") == 0) || (g_strcmp0 ((gchar *) iter->name, "ol") == 0)) { g_autofree gchar *item_c = NULL; gboolean is_ordered_list = g_strcmp0 ((gchar *) iter->name, "ol") == 0; guint entry_no = 0; /* set item style for unordered lists */ if (!is_ordered_list) { if (to_kind == AS_MARKUP_KIND_MARKDOWN) item_c = g_strdup ("*"); else item_c = g_strdup ("•"); } /* iterate over itemize contents */ for (iter2 = iter->children; iter2 != NULL; iter2 = iter2->next) { if (iter2->type != XML_ELEMENT_NODE) continue; if (g_strcmp0 ((gchar *) iter2->name, "li") == 0) { g_auto(GStrv) spl = NULL; g_autofree gchar *clean_item = NULL; g_autofree gchar *item_content = as_xml_get_node_value_raw ( iter2); entry_no++; /* Apparently the item text is empty, which is odd. * Let's add an empty entry, instead of ignoring it entirely. */ if (item_content == NULL) item_content = g_strdup (""); /* remove extra whitespaces and linebreaks */ clean_item = as_sanitize_text_spaces (item_content); /* set item number for ordered list */ if (is_ordered_list) { g_free (item_c); item_c = g_strdup_printf ("%u.", entry_no); } /* break to 100 chars, leaving room for the dot/indent */ spl = as_markup_strsplit_words (clean_item, 100 - 4); if (spl != NULL) { g_string_append_printf (str, " %s %s", item_c, spl[0]); for (guint i = 1; spl[i] != NULL; i++) g_string_append_printf (str, " %s", spl[i]); } } else { /* only
  • is valid in lists */ ret = FALSE; goto out; } } } } /* success */ if (str->len > 0) g_string_truncate (str, str->len - 1); out: if (doc != NULL) xmlFreeDoc (doc); if (!ret) formatted = g_strdup (markup); if (str != NULL) { if (!ret) g_string_free (str, TRUE); else formatted = g_string_free (str, FALSE); } return formatted; } /** * as_is_empty: * @str: The string to test. * * Test if a C string is %NULL or empty (contains only terminating null character). * * Returns: %TRUE if string was empty. */ gboolean as_is_empty (const gchar *str) { if ((str == NULL) || (str[0] == '\0')) return TRUE; return FALSE; } /** * as_iso8601_to_datetime: * * Helper function to work around a bug in g_date_time_new_from_iso8601. * Can be dropped when the bug gets resolved upstream: * https://bugzilla.gnome.org/show_bug.cgi?id=760983 **/ GDateTime * as_iso8601_to_datetime (const gchar *iso_date) { guint dmy[] = { 0, 0, 0 }; /* nothing set */ if (iso_date == NULL || iso_date[0] == '\0') return NULL; /* try to parse complete ISO8601 date */ if (g_strstr_len (iso_date, -1, "T") != NULL) { g_autoptr(GTimeZone) tz_utc = g_time_zone_new_utc (); GDateTime *res = g_date_time_new_from_iso8601 (iso_date, tz_utc); if (res != NULL) return res; } /* g_date_time_new_from_iso8601() blows goats and won't * accept a valid ISO8601 formatted date without a * time value - try and parse this case */ if (sscanf (iso_date, "%u-%u-%u", &dmy[0], &dmy[1], &dmy[2]) != 3) return NULL; /* create valid object */ return g_date_time_new_utc (dmy[0], dmy[1], dmy[2], 0, 0, 0); } /** * as_str_verify_integer: * @str: The string to test. * @min_value: Minimal value to be considered valid, e.g. %G_MININT64 * @max_value: Maximum value to be considered valie, e.g. %G_MAXINT64 * * Verify that a string is an integer in the given range. * Unlike strtoll(), this function will only pass if the whole string * consists of numbers, and will not succeed if the string has a text suffix. * * Returns: %TRUE if verified correctly according to the set conditions, %FALSE otherwise. */ gboolean as_str_verify_integer (const gchar *str, gint64 min_value, gint64 max_value) { gchar *endptr; gint64 res; g_return_val_if_fail (min_value < max_value, FALSE); if (as_is_empty (str)) return FALSE; res = g_ascii_strtoll (str, &endptr, 10); /* check if we used the whole string for conversion */ if (endptr[0] != '\0') return FALSE; return res >= min_value && res <= max_value; } /** * as_utils_delete_dir_recursive: * @dirname: Directory to remove * * Remove directory and all its children (like rm -r does). * * Returns: %TRUE if operation was successful */ gboolean as_utils_delete_dir_recursive (const gchar *dirname) { GError *error = NULL; gboolean ret = FALSE; GFile *dir; GFileEnumerator *enr; g_autoptr(GFileInfo) info = NULL; g_return_val_if_fail (dirname != NULL, FALSE); if (!g_file_test (dirname, G_FILE_TEST_IS_DIR)) return TRUE; dir = g_file_new_for_path (dirname); enr = g_file_enumerate_children (dir, "standard::name", G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error); if (error != NULL) goto out; if (enr == NULL) goto out; info = g_file_enumerator_next_file (enr, NULL, &error); if (error != NULL) goto out; while (info != NULL) { g_autofree gchar *path = g_build_filename (dirname, g_file_info_get_name (info), NULL); if (g_file_test (path, G_FILE_TEST_IS_DIR)) as_utils_delete_dir_recursive (path); else g_remove (path); g_object_unref (info); info = g_file_enumerator_next_file (enr, NULL, &error); if (error != NULL) goto out; } if (g_file_test (dirname, G_FILE_TEST_EXISTS)) g_rmdir (dirname); ret = TRUE; out: g_object_unref (dir); if (enr != NULL) g_object_unref (enr); if (error != NULL) { g_critical ("Could not remove directory: %s", error->message); g_error_free (error); } return ret; } /** * as_utils_find_files_matching: */ GPtrArray * as_utils_find_files_matching (const gchar *dir, const gchar *pattern, gboolean recursive, GError **error) { GPtrArray *list; GFileInfo *file_info; GFileEnumerator *enumerator = NULL; GFile *fdir; GError *tmp_error = NULL; g_return_val_if_fail (dir != NULL, NULL); g_return_val_if_fail (pattern != NULL, NULL); list = g_ptr_array_new_with_free_func (g_free); fdir = g_file_new_for_path (dir); enumerator = g_file_enumerate_children (fdir, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, 0, NULL, &tmp_error); if (tmp_error != NULL) goto out; while ((file_info = g_file_enumerator_next_file (enumerator, NULL, &tmp_error)) != NULL) { g_autofree gchar *path = NULL; if (tmp_error != NULL) { g_object_unref (file_info); break; } if (g_file_info_get_is_hidden (file_info)) { g_object_unref (file_info); continue; } path = g_build_filename (dir, g_file_info_get_name (file_info), NULL); if ((!g_file_test (path, G_FILE_TEST_IS_REGULAR)) && (recursive)) { GPtrArray *subdir_list; guint i; subdir_list = as_utils_find_files_matching (path, pattern, recursive, &tmp_error); /* if there was an error, exit */ if (subdir_list == NULL) { g_ptr_array_unref (list); list = NULL; g_object_unref (file_info); break; } for (i = 0; i < subdir_list->len; i++) g_ptr_array_add ( list, g_strdup ((gchar *) g_ptr_array_index (subdir_list, i))); g_ptr_array_unref (subdir_list); } else { if (!as_is_empty (pattern)) { if (!g_pattern_match_simple (pattern, g_file_info_get_name (file_info))) { g_object_unref (file_info); continue; } } g_ptr_array_add (list, path); path = NULL; } g_object_unref (file_info); } out: g_object_unref (fdir); if (enumerator != NULL) g_object_unref (enumerator); if (tmp_error != NULL) { if (error == NULL) g_debug ("Error while searching for files in %s: %s", dir, tmp_error->message); else g_propagate_error (error, tmp_error); g_error_free (tmp_error); g_ptr_array_unref (list); return NULL; } return list; } /** * as_utils_find_files: */ GPtrArray * as_utils_find_files (const gchar *dir, gboolean recursive, GError **error) { GPtrArray *res = NULL; g_return_val_if_fail (dir != NULL, NULL); res = as_utils_find_files_matching (dir, "", recursive, error); return res; } /** * as_utils_is_root: */ gboolean as_utils_is_root (void) { #ifdef G_OS_WIN32 PSID sid = NULL; SID_IDENTIFIER_AUTHORITY auth = SECURITY_NT_AUTHORITY; BOOL is_admin = FALSE; if (AllocateAndInitializeSid (&auth, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid)) { if (!CheckTokenMembership (NULL, sid, &is_admin)) { is_admin = FALSE; } FreeSid (sid); } return is_admin; #else uid_t vuid; vuid = getuid (); return (vuid == ((uid_t) 0)); #endif } /** * as_utils_is_writable: * @path: the path to check. * * Checks if a path is writable. */ gboolean as_utils_is_writable (const gchar *path) { g_autoptr(GFile) file = NULL; g_autoptr(GFileInfo) file_info = NULL; file = g_file_new_for_path (path); file_info = g_file_query_info (file, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (file_info && g_file_info_has_attribute (file_info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE)) return g_file_info_get_attribute_boolean (file_info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE); return FALSE; } /** * as_get_current_locale_posix: * * Returns the current locale string in the POSIX format. * * Returns: (transfer full): A locale string, free with g_free() */ gchar * as_get_current_locale_posix (void) { const gchar *const *locale_names; const gchar *locale = NULL; /* use LANGUAGE, LC_ALL, LC_MESSAGES and LANG */ locale_names = g_get_language_names (); if (g_strstr_len (locale_names[0], -1, "_") == NULL) { /* The locale doesn't have a region code - see if LANG has more to offer. */ const gchar *env_lang = g_getenv ("LANG"); if ((env_lang != NULL) && (g_strstr_len (env_lang, -1, "_") != NULL)) locale = env_lang; } if (locale == NULL) locale = locale_names[0]; if (locale == NULL) locale = g_strdup ("C"); /* return active locale without UTF-8 suffix, UTF-8 is default in AppStream */ return as_locale_strip_encoding (locale); } /** * as_get_current_locale_bcp47: * * Returns the current locale string in the IETF BCP 47 format * for use in AppStream XML. * * Returns: (transfer full): A locale string, free with g_free() */ gchar * as_get_current_locale_bcp47 (void) { g_autofree gchar *locale = as_get_current_locale_posix (); return as_utils_posix_locale_to_bcp47 (locale); } /** * as_utils_posix_locale_to_bcp47: * * Converts a POSIX locale string to the corresponding IETF BCP47 format. * If the given locale is already in BCP47 format, no change will be done. * * Returns: (transfer full): A locale string, free with g_free() */ gchar * as_utils_posix_locale_to_bcp47 (const gchar *locale) { g_autoptr(GString) bcp47 = NULL; gboolean has_variant = FALSE; guint i; if (locale == NULL) return NULL; bcp47 = g_string_sized_new (strlen (locale)); for (i = 0; locale[i] != '\0'; i++) { if (locale[i] == '_') { g_string_append_c (bcp47, '-'); } else if (locale[i] == '@') { has_variant = TRUE; break; } else { g_string_append_c (bcp47, locale[i]); } } if (has_variant) { if (g_str_has_prefix (locale + i, "@cyrillic")) g_string_append (bcp47, "-Cyrl"); else if (g_str_has_prefix (locale + i, "@devanagari")) g_string_append (bcp47, "-Deva"); else if (g_str_has_prefix (locale + i, "@latin")) g_string_append (bcp47, "-Latn"); else if (g_str_has_prefix (locale + i, "@shaw")) g_string_append (bcp47, "-Shaw"); else if (!g_str_has_prefix (locale + i, "@euro")) { g_string_append_c (bcp47, '-'); g_string_append (bcp47, locale + i + 1); } } return g_string_free (g_steal_pointer (&bcp47), FALSE); } /** * as_locale_is_posix: * @locale: (nullable): The locale to test. * * Guess if the given locale is in POSIX format, or whether it is * in another format, e.g. BCP47. * * Returns: %TRUE if we have a POSIX locale, %FALSE otherwise. */ gboolean as_locale_is_posix (const gchar *locale) { /* we assume NULL is valid, for compatibility */ if (locale == NULL) return TRUE; for (guint i = 0; locale[i] != '\0'; i++) { if (locale[i] == '-') return FALSE; if (locale[i] == '_') return TRUE; if (locale[i] == '@') return TRUE; } /* if we are here, the string contained no hyphens (which would indicate BCP47), * but also no underscores or @ (which would indicate POSIX), so it may be something * like "de", for which we simply assume it is a POSIX locale. */ return TRUE; } /** * as_locale_is_bcp47: * @locale: (nullable): The locale to test. * * Guess if the given locale is in BCP47 format. * * Returns: %TRUE if we likely have a BCP47 locale, %FALSE otherwise. */ gboolean as_locale_is_bcp47 (const gchar *locale) { /* we assume NULL is valid, for compatibility */ if (locale == NULL) return TRUE; for (guint i = 0; locale[i] != '\0'; i++) { if (locale[i] == '-') return TRUE; if (locale[i] == '_') return FALSE; if (locale[i] == '@') return FALSE; } /* if we are here, the string contained no hyphens (which would indicate BCP47), * but also no underscores or @ (which would indicate POSIX), so it may be something * like "de", for which we simply assume it is a BCP47 locale. */ return TRUE; } /** * as_ptr_array_to_strv: * @array: (element-type utf8) * * Returns: (transfer full): strv of the string array */ gchar ** as_ptr_array_to_strv (GPtrArray *array) { gchar **value; const gchar *value_temp; g_return_val_if_fail (array != NULL, NULL); /* copy the array to a strv */ value = g_new0 (gchar *, array->len + 1); for (guint i = 0; i < array->len; i++) { value_temp = (const gchar *) g_ptr_array_index (array, i); value[i] = g_strdup (value_temp); } return value; } /** * as_strv_to_ptr_array: * @strv: The strings to convert. * @ignore_empty: %TRUE if empty entries should be ignored. * @copy: %TRUE if a deep copy should be created. * * Returns: (transfer full): strv of the string array */ GPtrArray * as_strv_to_ptr_array (gchar **strv, gboolean ignore_empty, gboolean copy) { GPtrArray *array; g_return_val_if_fail (strv != NULL, NULL); array = copy ? g_ptr_array_new_with_free_func (g_free) : g_ptr_array_new (); for (guint i = 0; strv[i] != NULL; ++i) { if (ignore_empty && as_is_empty (strv[i])) continue; if (copy) g_ptr_array_add (array, g_strdup (strv[i])); else g_ptr_array_add (array, strv[i]); } return array; } /** * as_gstring_replace: * @string: a #GString * @find: the string to find in @string * @replace: the string to insert in place of @find * @limit: the maximum instances of @find to replace with @replace, or `0` for no limit * * Replaces the string @find with the string @replace in a #GString up to * @limit times. If the number of instances of @find in the #GString is * less than @limit, all instances are replaced. If @limit is `0`, * all instances of @find are replaced. * * Returns: the number of find and replace operations performed. **/ guint as_gstring_replace (GString *string, const gchar *find, const gchar *replace, guint limit) { #if GLIB_CHECK_VERSION(2, 68, 0) return g_string_replace (string, find, replace, limit); #else /* note: This is a direct copy from GLib upstream (with whitespace * fixed spaces to tabs and with style fixed). Once we can depend on * GLib 2.68, this copy should be dropped and g_string_replace() used * instead. * * GLib is licensed under the LGPL-2.1+. */ gsize f_len, r_len, pos; gchar *cur, *next; guint n = 0; g_return_val_if_fail (string != NULL, 0); g_return_val_if_fail (find != NULL, 0); g_return_val_if_fail (replace != NULL, 0); f_len = strlen (find); r_len = strlen (replace); cur = string->str; while ((next = strstr (cur, find)) != NULL) { pos = next - string->str; g_string_erase (string, pos, f_len); g_string_insert (string, pos, replace); cur = string->str + pos + r_len; n++; } return n; #endif /* !GLIB_CHECK_VERSION(2,68.0) */ } /** * as_str_replace: * @str: The string to operate on * @old_str: The old value to replace. * @new_str: The new value to replace @old_str with. * @limit: the maximum instances of @find to replace with @new_str, or `0` for * no limit * * Performs search & replace on the given string. * * Returns: A new string with the characters replaced. */ gchar * as_str_replace (const gchar *str, const gchar *old_str, const gchar *new_str, guint limit) { GString *gstr; gstr = g_string_new (str); as_gstring_replace (gstr, old_str, new_str, limit); return g_string_free (gstr, FALSE); } /** * as_touch_location: * @fname: The file or directory to touch. * * Change mtime of a filesystem location. */ gboolean as_touch_location (const gchar *fname) { struct stat sb; struct utimbuf new_times; if (stat (fname, &sb) < 0) return FALSE; new_times.actime = sb.st_atime; new_times.modtime = time (NULL); if (utime (fname, &new_times) < 0) return FALSE; return TRUE; } /** * as_reset_umask: * * Reset umask potentially set by the user to a default value, * so we can create files with the correct permissions. */ void as_reset_umask (void) { umask (0022); } /** * as_copy_file: * * Copy a file. */ gboolean as_copy_file (const gchar *source, const gchar *destination, GError **error) { FILE *fsrc, *fdest; int a; fsrc = fopen (source, "rb"); if (fsrc == NULL) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Could not copy file: %s", g_strerror (errno)); return FALSE; } fdest = fopen (destination, "wb"); if (fdest == NULL) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Could not copy file: %s", g_strerror (errno)); fclose (fsrc); return FALSE; } while (TRUE) { a = fgetc (fsrc); if (!feof (fsrc)) fputc (a, fdest); else break; } fclose (fdest); fclose (fsrc); return TRUE; } /** * as_is_cruft_locale: * * Checks whether the given locale is valid or a cruft or dummy * locale. */ gboolean as_is_cruft_locale (const gchar *locale) { if (locale == NULL) return FALSE; if (g_strcmp0 (locale, "x-test") == 0) return TRUE; if (g_strcmp0 (locale, "xx") == 0) return TRUE; return FALSE; } /** * as_locale_strip_encoding: * * Remove the encoding from a locale string. * The function returns a newly allocated string. */ gchar * as_locale_strip_encoding (const gchar *locale) { return as_str_replace (locale, ".UTF-8", "", 1); } /** * as_get_current_arch: * * Get the current architecture as vendor strings * (e.g. "amd64" instead of "x86_64"). * * Returns: (transfer full): The current OS architecture as string */ gchar * as_get_current_arch (void) { #ifdef G_OS_WIN32 SYSTEM_INFO si; GetSystemInfo (&si); switch (si.wProcessorArchitecture) { case PROCESSOR_ARCHITECTURE_AMD64: return g_strdup ("amd64"); case PROCESSOR_ARCHITECTURE_ARM: return g_strdup ("arm"); case PROCESSOR_ARCHITECTURE_ARM64: return g_strdup ("arm64"); case PROCESSOR_ARCHITECTURE_IA64: return g_strdup ("ia64"); case PROCESSOR_ARCHITECTURE_INTEL: return g_strdup ("i386"); default: return g_strdup ("unknown"); } #else gchar *arch; struct utsname uts; uname (&uts); if (g_strcmp0 (uts.machine, "x86_64") == 0) { arch = g_strdup ("amd64"); } else if (g_pattern_match_simple ("i?86", uts.machine)) { arch = g_strdup ("i386"); } else if (g_strcmp0 (uts.machine, "aarch64")) { arch = g_strdup ("arm64"); } else { arch = g_strdup (uts.machine); } return arch; #endif } /** * as_arch_compatible: * @arch1: Architecture 1 * @arch2: Architecture 2 * * Compares two architectures and returns %TRUE if they are compatible. */ gboolean as_arch_compatible (const gchar *arch1, const gchar *arch2) { if (g_strcmp0 (arch1, arch2) == 0) return TRUE; if (g_strcmp0 (arch1, "all") == 0) return TRUE; if (g_strcmp0 (arch2, "all") == 0) return TRUE; return FALSE; } /** * as_utils_locale_to_language: * @locale: (nullable): the BCP47 or POSIX locale string. * * Get language part from a locale string in either BCP47 or POSIX format. */ gchar * as_utils_locale_to_language (const gchar *locale) { gchar *tmp; gchar *country_code; /* invalid */ if (locale == NULL) return NULL; /* return the part before the - for BCP47 locale */ country_code = g_strdup (locale); tmp = g_strstr_len (country_code, -1, "-"); if (tmp != NULL) { *tmp = '\0'; } else { /* return the part before the _ for POSIX locale (not always 2 chars!) */ tmp = g_strstr_len (country_code, -1, "_"); if (tmp != NULL) *tmp = '\0'; } /* return the part before any "@" for locale with modifiers like "ca@valencia" */ tmp = g_strstr_len (country_code, -1, "@"); if (tmp != NULL) *tmp = '\0'; return country_code; } /** * as_ptr_array_find_string: * @array: gchar* array * @value: string to find * * Finds a string in a pointer array. * * Returns: (nullable): the const string, or %NULL if not found **/ const gchar * as_ptr_array_find_string (GPtrArray *array, const gchar *value) { for (guint i = 0; i < array->len; i++) { const gchar *tmp = g_ptr_array_index (array, i); if (g_strcmp0 (tmp, value) == 0) return tmp; } return NULL; } /** * as_hash_table_keys_to_array: * @table: The hash table. * @array: The pointer array. * * Add keys of a hash table to a pointer array. * The hash table keys must be strings. */ void as_hash_table_string_keys_to_array (GHashTable *table, GPtrArray *array) { GHashTableIter iter; gpointer value; g_hash_table_iter_init (&iter, table); while (g_hash_table_iter_next (&iter, NULL, &value)) { const gchar *str = (const gchar *) value; g_ptr_array_add (array, g_strdup (str)); } } /** * as_utils_locale_is_compatible: * @locale1: (nullable): a BCP47 or POSIX locale string, or %NULL * @locale2: (nullable): a BCP47 or POSIX locale string, or %NULL * * Calculates if one locale is compatible with another. * When doing the calculation the locale and language code is taken into * account if possible. * * Returns: %TRUE if the locale is compatible. * * Since: 0.9.5 **/ gboolean as_utils_locale_is_compatible (const gchar *locale1, const gchar *locale2) { g_autofree gchar *lang1 = as_utils_locale_to_language (locale1); g_autofree gchar *lang2 = as_utils_locale_to_language (locale2); /* we've specified "don't care" and locale unspecified */ if (locale1 == NULL && locale2 == NULL) return TRUE; /* forward */ if (locale1 == NULL && locale2 != NULL) { const gchar *const *locales = g_get_language_names (); return g_strv_contains (locales, locale2) || g_strv_contains (locales, lang2); } /* backwards */ if (locale1 != NULL && locale2 == NULL) { const gchar *const *locales = g_get_language_names (); return g_strv_contains (locales, locale1) || g_strv_contains (locales, lang1); } /* both specified */ if (g_strcmp0 (locale1, locale2) == 0) return TRUE; if (g_strcmp0 (locale1, lang2) == 0) return TRUE; if (g_strcmp0 (lang1, locale2) == 0) return TRUE; return FALSE; } /** * as_utils_search_token_valid: * @token: the search token * * Checks the search token if it is valid. Valid tokens are at least 3 chars in * length and do not contain markup. * * Returns: %TRUE is the search token was valid. **/ gboolean as_utils_search_token_valid (const gchar *token) { guint i; for (i = 0; token[i] != '\0'; i++) { if (token[i] == '<' || token[i] == '>' || token[i] == '(' || token[i] == ')') return FALSE; } if (i < 3) return FALSE; return TRUE; } /** * as_utils_is_category_name: * @category_name: a XDG category name, e.g. "ProjectManagement" * * Searches the known list of registered XDG category names. * See https://specifications.freedesktop.org/menu-spec/latest/category-registry.html * for a reference. * * Returns: %TRUE if the category name is valid * * Since: 0.9.7 **/ gboolean as_utils_is_category_name (const gchar *category_name) { g_autoptr(GBytes) data = NULL; g_autofree gchar *key = NULL; GResource *resource = as_get_resource_safe (); /* custom spec-extensions are generally valid if prefixed correctly */ if (g_str_has_prefix (category_name, "X-")) return TRUE; /* safeguard against accidentally matching comments */ if (g_str_has_prefix (category_name, "#")) return FALSE; /* load the readonly data section and look for the category name */ data = g_resource_lookup_data (resource, "/org/freedesktop/appstream/xdg-category-names.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL); if (data == NULL) return FALSE; key = g_strdup_printf ("\n%s\n", category_name); return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL; } /** * as_utils_category_name_is_bad: * @category_name: a XDG category name, e.g. "ProjectManagement" * * We want to ignore certain low-quality categories like "GTK", "Qt" * or "GUI" that convey no meaning to the user at all, * as well as any custom-defined categories. * * This functiuon checks for those, adn should be used in * conjunction with %as_utils_is_category_name. * * It is not invalid to use the categories in desktop-entry files, * but they should not end up in AppStream catalog metadata, and * should ideally not be used in MetaInfo files as well. * * Returns: %TRUE if the category should be ignored. **/ gboolean as_utils_category_name_is_bad (const gchar *category_name) { if (as_str_equal0 (category_name, "GTK")) return TRUE; if (as_str_equal0 (category_name, "Qt")) return TRUE; if (as_str_equal0 (category_name, "KDE")) return TRUE; if (as_str_equal0 (category_name, "GNOME")) return TRUE; if (as_str_equal0 (category_name, "Motif")) return TRUE; if (as_str_equal0 (category_name, "Java")) return TRUE; if (as_str_equal0 (category_name, "GUI")) return TRUE; if (as_str_equal0 (category_name, "Application")) return TRUE; if (as_str_equal0 (category_name, "XFCE")) return TRUE; if (as_str_equal0 (category_name, "DDE")) return TRUE; /* we want to ignore custom categories */ if (g_str_has_prefix (category_name, "X-")) return TRUE; if (g_str_has_prefix (category_name, "x-")) return TRUE; return FALSE; } /** * as_utils_is_tld: * @tld: a top-level domain without dot, e.g. "de", "org", "name" * * Searches the known list of TLDs we allow for AppStream IDs. * This excludes internationalized names. * * Returns: %TRUE if the TLD is valid * * Since: 0.9.8 **/ gboolean as_utils_is_tld (const gchar *tld) { g_autoptr(GBytes) data = NULL; g_autofree gchar *key = NULL; GResource *resource = as_get_resource_safe (); /* safeguard against accidentally matching comments */ if (as_is_empty (tld) || g_str_has_prefix (tld, "#")) return FALSE; /* load the readonly data section and look for the TLD */ data = g_resource_lookup_data (resource, "/org/freedesktop/appstream/iana-filtered-tld-list.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL); if (data == NULL) return FALSE; key = g_strdup_printf ("\n%s\n", tld); return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL; } /** * as_utils_is_desktop_environment: * @de_id: a desktop environment id. * * Checks if the submitted desktop environment ID is * known and valid. * * Returns: %TRUE if the desktop-id is valid * * Since: 0.10.0 **/ gboolean as_utils_is_desktop_environment (const gchar *de_id) { /* handle invalid */ if (de_id == NULL || de_id[0] == '\0') return FALSE; for (guint i = 0; as_desktop_env_data[i].id != NULL; i++) { if (as_str_equal0 (as_desktop_env_data[i].id, de_id)) return TRUE; } return FALSE; } /** * as_utils_get_desktop_environment_name: * @de_id: a desktop environment id. * * Get a human-readable, translated name of the desktop environment * represented by the given ID. * * Returns: A localized name of the DE, or %NULL if none available. * * Since: 1.0.0 **/ const gchar * as_utils_get_desktop_environment_name (const gchar *de_id) { /* handle invalid */ if (de_id == NULL || de_id[0] == '\0') return NULL; for (guint i = 0; as_desktop_env_data[i].id != NULL; i++) { if (as_str_equal0 (as_desktop_env_data[i].id, de_id)) return as_desktop_env_data[i].name; } return NULL; } /** * as_utils_is_gui_environment_style: * @env_style: a GUI environment style ID, e.g. "pantheon:dark" * * Checks if the given identifier is a valid, known GUI environment style. * * Returns: %TRUE if the environment-style ID is is valid * * Since: 1.0.0 **/ gboolean as_utils_is_gui_environment_style (const gchar *env_style) { /* handle invalid */ if (env_style == NULL || env_style[0] == '\0') return FALSE; for (guint i = 0; as_gui_env_style_data[i].id != NULL; i++) { if (as_str_equal0 (as_gui_env_style_data[i].id, env_style)) return TRUE; } return FALSE; } /** * as_utils_get_gui_environment_style_name: * @env_style: a GUI environment style ID, e.g. "pantheon:dark" * * Get a human-readable, translated name of the combination * of GUI environment and style. E.g. "plasma:dark" becomes "Plasma (Dark)". * * Returns: A localized name of the environment style, or %NULL if none available. * * Since: 1.0.0 **/ const gchar * as_utils_get_gui_environment_style_name (const gchar *env_style) { /* handle invalid */ if (env_style == NULL || env_style[0] == '\0') return NULL; for (guint i = 0; as_gui_env_style_data[i].id != NULL; i++) { if (as_str_equal0 (as_gui_env_style_data[i].id, env_style)) return as_gui_env_style_data[i].name; } return NULL; } /** * as_utils_is_platform_triplet_arch: * @arch: an architecture ID. * * Check if the given string is a valid architecture part * of a platform triplet. * * Returns: %TRUE if architecture is valid * * Since: 0.14.0 **/ gboolean as_utils_is_platform_triplet_arch (const gchar *arch) { g_autoptr(GBytes) data = NULL; g_autofree gchar *key = NULL; GResource *resource = NULL; if (as_is_empty (arch)) return FALSE; /* "any" is always a valid value */ if (g_strcmp0 (arch, "any") == 0) return TRUE; /* safeguard against accidentally matching comments */ if (g_str_has_prefix (arch, "#")) return FALSE; resource = as_get_resource_safe (); /* load the readonly data section */ data = g_resource_lookup_data (resource, "/org/freedesktop/appstream/platform_arch.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL); if (data == NULL) return FALSE; key = g_strdup_printf ("\n%s\n", arch); return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL; } /** * as_utils_is_platform_triplet_oskernel: * @os: an OS/kernel ID. * * Check if the given string is a valid OS/kernel part * of a platform triplet. * * Returns: %TRUE if kernel ID is valid * * Since: 0.14.0 **/ gboolean as_utils_is_platform_triplet_oskernel (const gchar *os) { g_autoptr(GBytes) data = NULL; g_autofree gchar *key = NULL; GResource *resource; if (as_is_empty (os)) return FALSE; /* "any" is always a valid value */ if (g_strcmp0 (os, "any") == 0) return TRUE; /* safeguard against accidentally matching comments */ if (g_str_has_prefix (os, "#")) return FALSE; resource = as_get_resource_safe (); /* load the readonly data section */ data = g_resource_lookup_data (resource, "/org/freedesktop/appstream/platform_os.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL); if (data == NULL) return FALSE; key = g_strdup_printf ("\n%s\n", os); return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL; } /** * as_utils_is_platform_triplet_osenv: * @env: an OS/environment ID. * * Check if the given string is a valid OS/environment part * of a platform triplet. * * Returns: %TRUE if environment ID is valid * * Since: 0.14.0 **/ gboolean as_utils_is_platform_triplet_osenv (const gchar *env) { g_autoptr(GBytes) data = NULL; g_autofree gchar *key = NULL; GResource *resource; if (as_is_empty (env)) return FALSE; /* "any" is always a valid value */ if (g_strcmp0 (env, "any") == 0) return TRUE; /* safeguard against accidentally matching comments */ if (g_str_has_prefix (env, "#")) return FALSE; resource = as_get_resource_safe (); /* load the readonly data section */ data = g_resource_lookup_data (resource, "/org/freedesktop/appstream/platform_env.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL); if (data == NULL) return FALSE; key = g_strdup_printf ("\n%s\n", env); return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL; } /** * as_utils_is_platform_triplet: * @triplet: a platform triplet. * * Test if the given string is a valid platform triplet recognized by * AppStream. * * Returns: %TRUE if triplet is valid. * * Since: 0.14.0 **/ gboolean as_utils_is_platform_triplet (const gchar *triplet) { g_auto(GStrv) parts = NULL; if (triplet == NULL) return FALSE; parts = g_strsplit (triplet, "-", 3); if (g_strv_length (parts) != 3) return FALSE; if (!as_utils_is_platform_triplet_arch (parts[0])) return FALSE; if (!as_utils_is_platform_triplet_oskernel (parts[1])) return FALSE; if (!as_utils_is_platform_triplet_osenv (parts[2])) return FALSE; return TRUE; } /** * as_utils_is_reference_registry: * @regname: a potential registry ID. * * Check if the given string is a valid ID for an * external identifier or scientific registry. * * Returns: %TRUE if registry is valid * * Since: 1.0.0 **/ gboolean as_utils_is_reference_registry (const gchar *regname) { g_autoptr(GBytes) data = NULL; g_autofree gchar *key = NULL; GResource *resource = NULL; if (as_is_empty (regname)) return FALSE; /* safeguard against accidentally matching comments */ if (g_str_has_prefix (regname, "#")) return FALSE; resource = as_get_resource_safe (); /* load the readonly data section */ data = g_resource_lookup_data (resource, "/org/freedesktop/appstream/reference-registries.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL); if (data == NULL) return FALSE; key = g_strdup_printf ("\n%s\n", regname); return g_strstr_len (g_bytes_get_data (data, NULL), -1, key) != NULL; } /** * as_utils_sort_components_into_categories: * @cpts: (element-type AsComponent): List of components. * @categories: (element-type AsCategory): List of categories to sort components into. * @check_duplicates: Whether to check for duplicates. * * Sorts all components in @cpts into the #AsCategory categories listed in @categories. */ void as_utils_sort_components_into_categories (GPtrArray *cpts, GPtrArray *categories, gboolean check_duplicates) { for (guint i = 0; i < cpts->len; i++) { guint j; AsComponent *cpt = AS_COMPONENT (g_ptr_array_index (cpts, i)); for (j = 0; j < categories->len; j++) { guint k; GPtrArray *children; gboolean added_to_main = FALSE; AsCategory *main_cat = AS_CATEGORY (g_ptr_array_index (categories, j)); if (as_component_is_member_of_category (cpt, main_cat)) { if (!check_duplicates || !as_category_has_component (main_cat, cpt)) { as_category_add_component (main_cat, cpt); added_to_main = TRUE; } } /* fortunately, categories are only nested one level deep in all known cases. * if this will ever change, we will need to adjust this code to go through * a whole tree of categories, eww... */ children = as_category_get_children (main_cat); for (k = 0; k < children->len; k++) { AsCategory *subcat = AS_CATEGORY (g_ptr_array_index (children, k)); /* skip duplicates */ if (check_duplicates && as_category_has_component (subcat, cpt)) continue; if (as_component_is_member_of_category (cpt, subcat)) { as_category_add_component (subcat, cpt); if (!added_to_main) { if (!check_duplicates || !as_category_has_component (main_cat, cpt)) { as_category_add_component (main_cat, cpt); } } } } } } } static inline const gchar * _as_fix_data_id_part (const gchar *tmp) { if (tmp == NULL || tmp[0] == '\0') return AS_DATA_ID_WILDCARD; return tmp; } /** * as_utils_build_data_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. * * Since: 0.14.0 */ gchar * as_utils_build_data_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 we have a package in system scope, the origin is "os", as they share the same namespace * and we can not have multiple versions of the same software installed on the system. * The data ID is needed to deduplicate entries */ if (scope == AS_COMPONENT_SCOPE_SYSTEM && bundle_kind == AS_BUNDLE_KIND_PACKAGE) origin = "os"; 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); /* build the data-id */ return g_strdup_printf ("%s/%s/%s/%s/%s", _as_fix_data_id_part (scope_str), _as_fix_data_id_part (bundle_str), _as_fix_data_id_part (origin), _as_fix_data_id_part (cid), _as_fix_data_id_part (branch)); } /** * as_utils_data_id_valid: * @data_id: a component data ID * * Checks if a data ID is valid i.e. has the correct number of * sections. * * Returns: %TRUE if the ID is valid * * Since: 0.14.0 */ gboolean as_utils_data_id_valid (const gchar *data_id) { guint i; guint sections = 1; if (data_id == NULL) return FALSE; for (i = 0; data_id[i] != '\0'; i++) { if (data_id[i] == '/') sections++; } return sections == AS_DATA_ID_PARTS_COUNT; } /** * as_utils_data_id_get_cid: * @data_id: The data-id. * * Get the component-id part of the data-id. */ gchar * as_utils_data_id_get_cid (const gchar *data_id) { g_auto(GStrv) parts = NULL; parts = g_strsplit (data_id, "/", 5); if (g_strv_length (parts) != 5) return NULL; return g_strdup (parts[3]); } static inline guint _as_utils_data_id_find_part (const gchar *str) { guint i; for (i = 0; str[i] != '/' && str[i] != '\0'; i++) ; return i; } static inline gboolean _as_utils_data_id_is_wildcard_part (const gchar *str, guint len) { return len == 1 && str[0] == '*'; } /** * as_utils_data_id_match: * @data_id1: a data ID * @data_id2: another data ID * @match_flags: a #AsDataIdMatchFlags bitfield, e.g. %AS_DATA_ID_MATCH_FLAG_ID * * Checks two data IDs for equality allowing globs to match, whilst also * allowing clients to whitelist sections that have to match. * * Returns: %TRUE if the IDs should be considered equal. * * Since: 0.14.0 */ gboolean as_utils_data_id_match (const gchar *data_id1, const gchar *data_id2, AsDataIdMatchFlags match_flags) { guint last1 = 0; guint last2 = 0; guint len1; guint len2; /* trivial */ if (data_id1 == data_id2) return TRUE; /* invalid */ if (!as_utils_data_id_valid (data_id1) || !as_utils_data_id_valid (data_id2)) return g_strcmp0 (data_id1, data_id2) == 0; /* look at each part */ for (guint i = 0; i < AS_DATA_ID_PARTS_COUNT; i++) { const gchar *tmp1 = data_id1 + last1; const gchar *tmp2 = data_id2 + last2; /* find the slash or the end of the string */ len1 = _as_utils_data_id_find_part (tmp1); len2 = _as_utils_data_id_find_part (tmp2); /* either string was a wildcard */ if (match_flags & (1 << i) && !_as_utils_data_id_is_wildcard_part (tmp1, len1) && !_as_utils_data_id_is_wildcard_part (tmp2, len2)) { /* are substrings the same */ if (len1 != len2) return FALSE; if (memcmp (tmp1, tmp2, len1) != 0) return FALSE; } /* advance to next section */ last1 += len1 + 1; last2 += len2 + 1; } return TRUE; } /** * as_utils_data_id_equal: * @data_id1: a data ID * @data_id2: another data ID * * Checks two component data IDs for equality allowing globs to match. * * Returns: %TRUE if the ID's should be considered equal. * * Since: 0.14.0 */ gboolean as_utils_data_id_equal (const gchar *data_id1, const gchar *data_id2) { return as_utils_data_id_match (data_id1, data_id2, AS_DATA_ID_MATCH_FLAG_SCOPE | AS_DATA_ID_MATCH_FLAG_BUNDLE_KIND | AS_DATA_ID_MATCH_FLAG_ORIGIN | AS_DATA_ID_MATCH_FLAG_ID | AS_DATA_ID_MATCH_FLAG_BRANCH); } /** * as_utils_data_id_hash: * @data_id: a data ID * * Converts a data-id to a hash value. * * This function implements the widely used DJB hash on the ID subset of the * data-id string. * * It can be passed to g_hash_table_new() as the hash_func parameter, * when using non-NULL strings or unique_ids as keys in a GHashTable. * * Returns: a hash value corresponding to the key * * Since: 0.14.0 */ guint as_utils_data_id_hash (const gchar *data_id) { gsize i; guint hash = 5381; guint section_cnt = 0; /* not a unique ID */ if (!as_utils_data_id_valid (data_id)) return g_str_hash (data_id); /* only include the component-id */ for (i = 0; data_id[i] != '\0'; i++) { if (data_id[i] == '/') { if (++section_cnt > 3) break; continue; } if (section_cnt < 3) continue; hash = (guint) ((hash << 5) + hash) + (guint) (data_id[i]); } return hash; } /** * as_utils_get_component_bundle_kind: * * Check which bundling system the component uses. */ AsBundleKind as_utils_get_component_bundle_kind (AsComponent *cpt) { GPtrArray *bundles; AsBundleKind bundle_kind = AS_BUNDLE_KIND_UNKNOWN; /* determine bundle - what should we do if there are multiple bundles of different types * defined for one component? */ if (as_component_has_package (cpt) || as_component_get_kind (cpt) == AS_COMPONENT_KIND_OPERATING_SYSTEM) bundle_kind = AS_BUNDLE_KIND_PACKAGE; bundles = as_component_get_bundles (cpt); if (bundles->len > 0) bundle_kind = as_bundle_get_kind (AS_BUNDLE (g_ptr_array_index (bundles, 0))); /* assume "package" for system-wide components from metainfo files */ if (bundle_kind == AS_BUNDLE_KIND_UNKNOWN && as_component_get_scope (cpt) == AS_COMPONENT_SCOPE_SYSTEM && as_component_get_origin_kind (cpt) == AS_ORIGIN_KIND_METAINFO) return AS_BUNDLE_KIND_PACKAGE; return bundle_kind; } /** * as_utils_build_data_id_for_cpt: * @cpt: The component to build the ID for. * * Builds the unique metadata ID for component @cpt. */ gchar * as_utils_build_data_id_for_cpt (AsComponent *cpt) { AsBundleKind bundle_kind; /* determine bundle - what should we do if there are multiple bundles of different types * defined for one component? */ bundle_kind = as_utils_get_component_bundle_kind (cpt); /* build the data-id */ return as_utils_build_data_id (as_component_get_scope (cpt), bundle_kind, as_component_get_origin (cpt), as_component_get_id (cpt), as_component_get_branch (cpt)); } /** * as_utils_dns_to_rdns: * * Create a reverse-DNS ID based on a preexisting URL. */ gchar * as_utils_dns_to_rdns (const gchar *url, const gchar *suffix) { g_autofree gchar *tmp = NULL; gchar *pos = NULL; GString *new_cid = NULL; g_auto(GStrv) parts = NULL; guint i; tmp = g_strstr_len (url, -1, "://"); if (tmp == NULL) tmp = g_strdup (url); else tmp = g_strdup (tmp + 3); pos = g_strstr_len (tmp, -1, "/"); if (pos != NULL) pos[0] = '\0'; parts = g_strsplit (tmp, ".", -1); if (parts == NULL) return NULL; new_cid = g_string_new (suffix); for (i = 0; parts[i] != NULL; i++) { if (g_strcmp0 (parts[i], "www") != 0) { g_string_prepend_c (new_cid, '.'); g_string_prepend (new_cid, parts[i]); } } if (suffix == NULL) g_string_truncate (new_cid, new_cid->len - 1); return g_string_free (new_cid, FALSE); } /** * as_sort_components_by_score_cb: * * Helper method to sort result arrays by the #AsComponent match score * with higher scores appearing higher in the list. */ static gint as_sort_components_by_score_cb (gconstpointer a, gconstpointer b) { guint s1, s2; AsComponent *cpt1 = *((AsComponent **) a); AsComponent *cpt2 = *((AsComponent **) b); s1 = as_component_get_sort_score (cpt1); s2 = as_component_get_sort_score (cpt2); if (s1 > s2) return -1; if (s1 < s2) return 1; return 0; } /** * as_sort_components_by_score: * * Sort components by their (search) match score. */ void as_sort_components_by_score (GPtrArray *cpts) { g_ptr_array_sort (cpts, as_sort_components_by_score_cb); } /** * as_object_ptr_array_absorb: * * Append contents from source array of GObjects to destination array, * transferring ownership to the destination and removing values * from the source (effectively moving the data). * The source array will be empty afterwards. * * This function assumes that a GDestroyNotify function is set on the * GPtrArray if GLib < 2.58. */ void as_object_ptr_array_absorb (GPtrArray *dest, GPtrArray *src) { while (src->len != 0) g_ptr_array_add (dest, g_ptr_array_steal_index_fast (src, 0)); } /** * as_ptr_array_to_str: * * Convert a string GPtrArray to a single string with * the array values separated by a separator. */ gchar * as_ptr_array_to_str (GPtrArray *array, const gchar *separator) { GString *str; if ((array == NULL) || (array->len == 0)) return NULL; str = g_string_new (""); for (guint i = 0; i < array->len; i++) { g_string_append_printf (str, "%s%s", (const gchar *) g_ptr_array_index (array, i), separator); } if (str->len >= 1) g_string_truncate (str, str->len - 1); return g_string_free (str, FALSE); } /** * as_filebasename_from_uri: * * Get the file basename from an URI. * This is the last component of the path, with any query or fragment * stripped off. * * Returns: The filename. */ gchar * as_filebasename_from_uri (const gchar *uri) { gchar *tmp; gchar *bname; if (uri == NULL) return NULL; bname = g_path_get_basename (uri); tmp = g_strstr_len (bname, -1, "?"); if (tmp != NULL) tmp[0] = '\0'; tmp = g_strstr_len (bname, -1, "#"); if (tmp != NULL) tmp[0] = '\0'; return bname; } /** * as_strstripnl: * @string: a string to remove surrounding whitespaces and newlines * * Removes newlines and whitespaces surrounding a string. * * This function doesn't allocate or reallocate any memory; * it modifies @string in place. * * As opposed to g_strstrip() this function also removes newlines * from the start and end of strings. * * Returns: @string */ gchar * as_strstripnl (gchar *string) { gsize len; guchar *start; if (string == NULL) return NULL; /* remove trailing whitespaces/newlines */ len = strlen (string); while (len--) { const guchar c = string[len]; if (g_ascii_isspace (c) || (c == '\n')) string[len] = '\0'; else break; } /* remove leading whitespaces/newlines */ for (start = (guchar *) string; *start && (g_ascii_isspace (*start) || ((*start) == '\n')); start++) ; memmove (string, start, strlen ((gchar *) start) + 1); return string; } /** * as_ref_string_release: * @rstr: a #GRefString to release. * * This function works exactly like %g_ref_string_release, except * that it does not throw an error if %NULL is passed to it. */ void as_ref_string_release (GRefString *rstr) { if (rstr == NULL) return; g_ref_string_release (rstr); } /** * as_ref_string_assign_safe: * @rstr_ptr: (out): a #AsRefString * @str: a string, or a #AsRefString * * This function unrefs and clears @rstr_ptr if set, then sets @rstr if * non-NULL. If @rstr and @rstr_ptr are the same string the action is ignored. * * This function should be used when @str cannot be guaranteed to be a * refcounted string and is suitable for use in existing object setters. */ void as_ref_string_assign_safe (GRefString **rstr_ptr, const gchar *str) { g_return_if_fail (rstr_ptr != NULL); if (*rstr_ptr != NULL) { g_ref_string_release (*rstr_ptr); *rstr_ptr = NULL; } if (str != NULL) *rstr_ptr = g_ref_string_new_intern (str); } /** * as_ref_string_assign_transfer: * @rstr_ptr: (out): a #AsRefString * @new_rstr: a #AsRefString * * Clear the previous refstring in @rstr_ptr and move the new string @new_rstr in its place, * without increasing its reference count again. */ void as_ref_string_assign_transfer (GRefString **rstr_ptr, GRefString *new_rstr) { g_return_if_fail (rstr_ptr != NULL); if (*rstr_ptr != NULL) { g_ref_string_release (*rstr_ptr); *rstr_ptr = NULL; } if (new_rstr != NULL) *rstr_ptr = new_rstr; } /** * as_utils_extract_tarball: * * Internal helper function to extract a tarball with tar. */ gboolean as_utils_extract_tarball (const gchar *filename, const gchar *target_dir, GError **error) { g_autoptr(GFile) tarz_file = NULL; g_autoptr(GInputStream) tarz_stream = NULL; g_autoptr(GInputStream) tar_stream = NULL; g_autoptr(GConverter) conv = NULL; g_autoptr(GSubprocess) tar_process = NULL; GOutputStream *tar_stdin = NULL; gssize bytes_read; gchar buffer[4096]; GError *tmp_error = NULL; /* read the (possibly compressed) tarball */ tarz_file = g_file_new_for_path (filename); tarz_stream = G_INPUT_STREAM (g_file_read (tarz_file, NULL, error)); if (tarz_stream == NULL) return FALSE; if (g_str_has_suffix (filename, "tar.zst")) { /* decompress the Zstd stream */ conv = G_CONVERTER (as_zstd_decompressor_new ()); tar_stream = g_converter_input_stream_new (tarz_stream, conv); } else if (g_str_has_suffix (filename, "tar.gz")) { /* decompress the GZip stream */ conv = G_CONVERTER (g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP)); tar_stream = g_converter_input_stream_new (tarz_stream, conv); } else { tar_stream = g_object_ref (tarz_stream); } /* set up the tar subprocess that will extract the tar data */ tar_process = g_subprocess_new (G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE, &tmp_error, "tar", "-x", "-C", target_dir, NULL); if (tar_process == NULL) { g_propagate_prefixed_error (error, tmp_error, "Unable to launch tar process: "); return FALSE; } tar_stdin = g_subprocess_get_stdin_pipe (tar_process); /* read decompressed data from the tarball and write it to the tar subprocess */ while ((bytes_read = g_input_stream_read (tar_stream, buffer, sizeof (buffer), NULL, &tmp_error)) > 0) { if (!g_output_stream_write_all (tar_stdin, buffer, bytes_read, NULL, NULL, error)) { g_prefix_error (error, "Error writing to tar subprocess: "); return FALSE; } } /* close the input stream to tar */ if (!g_output_stream_close (tar_stdin, NULL, error)) { g_prefix_error (error, "Error closing tar subprocess stdin: "); return FALSE; } if (tmp_error != NULL) { g_propagate_prefixed_error (error, tmp_error, "Error reading tarball: "); return FALSE; } /* wait for the subprocess to finish and check for errors */ if (!g_subprocess_wait_check (tar_process, NULL, &tmp_error)) { g_autoptr(GBytes) stderr_bytes = NULL; g_autoptr(GString) error_msg = NULL; const gchar *stderr_data; gsize stderr_size; GInputStream *tar_stderr = NULL; tar_stderr = g_subprocess_get_stderr_pipe (tar_process); stderr_bytes = g_input_stream_read_bytes (tar_stderr, 4096, NULL, NULL); stderr_data = g_bytes_get_data (stderr_bytes, &stderr_size); error_msg = g_string_new ("Tarball extraction failed"); if (stderr_size > 0) { g_string_append_printf (error_msg, " with the following error: %s", stderr_data); } else { g_string_append (error_msg, "."); } if (tmp_error != NULL) { g_set_error (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "%s Code: %i. %s", error_msg->str, g_subprocess_get_exit_status (tar_process), tmp_error->message); g_error_free (tmp_error); } else { g_set_error (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "%s Code: %i", error_msg->str, g_subprocess_get_exit_status (tar_process)); } return FALSE; } return TRUE; } /** * as_metadata_location_get_prefix: */ static const gchar * as_metadata_location_get_prefix (AsMetadataLocation location) { if (location == AS_METADATA_LOCATION_SHARED) return "/usr/share"; if (location == AS_METADATA_LOCATION_CACHE) return "/var/cache"; if (location == AS_METADATA_LOCATION_STATE) return "/var/lib"; if (location == AS_METADATA_LOCATION_USER) return g_get_user_data_dir (); return NULL; } /** * as_utils_install_metadata_file_internal: */ static gboolean as_utils_install_metadata_file_internal (const gchar *filename, const gchar *origin, const gchar *dir, const gchar *destdir, gboolean is_yaml, GError **error) { gchar *tmp; g_autofree gchar *basename = NULL; g_autofree gchar *path_dest = NULL; g_autofree gchar *path_parent = NULL; g_autoptr(GFile) file_dest = NULL; g_autoptr(GFile) file_src = NULL; /* create directory structure */ path_parent = g_strdup_printf ("%s%s", destdir, dir); if (g_mkdir_with_parents (path_parent, 0755) != 0) { g_set_error (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "Failed to create %s", path_parent); return FALSE; } /* calculate the new destination */ file_src = g_file_new_for_path (filename); basename = g_path_get_basename (filename); if (origin != NULL) { g_autofree gchar *basename_new = NULL; tmp = g_strstr_len (basename, -1, "."); if (tmp == NULL) { g_set_error (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "Name of metadata catalog file is invalid %s", basename); return FALSE; } basename_new = g_strdup_printf ("%s%s", origin, tmp); /* replace the fedora.xml.gz into %{origin}.xml.gz */ path_dest = g_build_filename (path_parent, basename_new, NULL); } else { path_dest = g_build_filename (path_parent, basename, NULL); } /* actually copy file */ file_dest = g_file_new_for_path (path_dest); if (!g_file_copy (file_src, file_dest, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, error)) return FALSE; /* update the origin for XML files */ if (origin != NULL && !is_yaml) { g_autoptr(AsMetadata) mdata = as_metadata_new (); as_metadata_set_locale (mdata, "ALL"); as_metadata_set_format_style (mdata, AS_FORMAT_STYLE_CATALOG); if (!as_metadata_parse_file (mdata, file_dest, AS_FORMAT_KIND_XML, error)) return FALSE; as_metadata_set_origin (mdata, origin); if (!as_metadata_save_catalog (mdata, path_dest, AS_FORMAT_KIND_XML, error)) return FALSE; } g_chmod (path_dest, 0755); return TRUE; } /** * as_utils_install_icon_tarball: */ static gboolean as_utils_install_icon_tarball (AsMetadataLocation location, const gchar *filename, const gchar *origin, const gchar *size_id, const gchar *destdir, GError **error) { g_autofree gchar *dir = NULL; dir = g_strdup_printf ("%s%s/swcatalog/icons/%s/%s", destdir, as_metadata_location_get_prefix (location), origin, size_id); if (g_mkdir_with_parents (dir, 0755) != 0) { g_set_error (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "Failed to create %s", dir); return FALSE; } if (!as_utils_extract_tarball (filename, dir, error)) return FALSE; return TRUE; } /** * as_utils_install_metadata_file: * @location: the #AsMetadataLocation, e.g. %AS_METADATA_LOCATION_CACHE * @filename: the full path of the file to install * @origin: the origin to use for the installation, or %NULL * @destdir: the destdir to use, or %NULL * @error: A #GError or %NULL * * Installs an AppStream MetaInfo, AppStream Metadata Catalog or AppStream Icon tarball file * to the right place on the filesystem. * Please note that this function does almost no validation and may guess missing values such * as icon sizes and origin names. * Ensure your metadata is good before installing it. * * Returns: %TRUE for success, %FALSE if error is set * * Since: 0.14.0 **/ gboolean as_utils_install_metadata_file (AsMetadataLocation location, const gchar *filename, const gchar *origin, const gchar *destdir, GError **error) { gboolean ret = FALSE; g_autofree gchar *basename = NULL; g_autofree gchar *path = NULL; const gchar *icons_size_id = NULL; const gchar *icons_size_ids[] = { "48x48", "48x48@2", "64x64", "64x64@2", "128x128", "128x128@2", NULL }; /* default value */ if (destdir == NULL) destdir = ""; if (location == AS_METADATA_LOCATION_USER) destdir = ""; if (location == AS_METADATA_LOCATION_UNKNOWN) { g_set_error_literal (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "Installation location was unknown."); return FALSE; } switch (as_metadata_file_guess_style (filename)) { case AS_FORMAT_STYLE_CATALOG: if (g_strstr_len (filename, -1, ".yml.gz") != NULL) { path = g_build_filename (as_metadata_location_get_prefix (location), "swcatalog", "yaml", NULL); ret = as_utils_install_metadata_file_internal (filename, origin, path, destdir, TRUE, error); } else { path = g_build_filename (as_metadata_location_get_prefix (location), "swcatalog", "xml", NULL); ret = as_utils_install_metadata_file_internal (filename, origin, path, destdir, FALSE, error); } break; case AS_FORMAT_STYLE_METAINFO: if (location == AS_METADATA_LOCATION_CACHE || location == AS_METADATA_LOCATION_STATE) { g_set_error_literal ( error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "System cache and state locations are unsupported for MetaInfo files"); return FALSE; } path = g_build_filename (as_metadata_location_get_prefix (location), "metainfo", NULL); ret = as_utils_install_metadata_file_internal (filename, NULL, path, destdir, FALSE, error); break; default: basename = g_path_get_basename (filename); if (g_str_has_suffix (basename, ".tar.gz")) { gchar *tmp; g_autofree gchar *tmp2 = NULL; /* we may have an icon tarball */ /* guess icon size */ for (guint i = 0; icons_size_ids[i] != NULL; i++) { if (g_strstr_len (basename, -1, icons_size_ids[i]) != NULL) { icons_size_id = icons_size_ids[i]; break; } } if (icons_size_id == NULL) { g_set_error_literal ( error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "Unable to find valid icon size in icon tarball name."); return FALSE; } /* install icons if we know the origin name */ if (origin != NULL) { ret = as_utils_install_icon_tarball (location, filename, origin, icons_size_id, destdir, error); break; } /* guess origin */ tmp2 = g_strdup_printf ("_icons-%s.tar.gz", icons_size_id); tmp = g_strstr_len (basename, -1, tmp2); if (tmp != NULL) { *tmp = '\0'; ret = as_utils_install_icon_tarball (location, filename, basename, icons_size_id, destdir, error); break; } } /* unrecognised */ g_set_error_literal (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "Can not process files of this type."); break; } return ret; } /** * as_get_user_cache_dir: * * Obtain the user-specific data cache directory for AppStream. * * Since: 0.14.2 */ gchar * as_get_user_cache_dir (GError **error) { const gchar *cache_root = g_get_user_cache_dir (); if (cache_root == NULL) { gchar *tmp_dir = g_dir_make_tmp ("appstream-XXXXXX", error); if (tmp_dir == NULL) { /* something went very wrong here, we could neither get a user cache dir, nor * access to a temporary directory in /tmp */ return NULL; } return tmp_dir; } else { gchar *cache_dir = g_build_filename (cache_root, "appstream", NULL); g_mkdir_with_parents (cache_dir, 0755); return cache_dir; } } /** * as_unichar_accepted: * * Test if the unicode character is in the accepted set for * string values in AppStream. * * We permit any printable, non-spacing, format or zero-width space characters, as * well as enclosing marks and U+00AD SOFT HYPHEN */ gboolean as_unichar_accepted (gunichar c) { return g_unichar_isprint (c) || g_unichar_iszerowidth (c) || c == 173; } /** * as_random_alnum_string: * @len: Length of the generated string. * * Create a random alphanumeric (only ASCII letters and numbers) * string that can be used for tests and filenames. * * Returns: A random alphanumeric string. */ gchar * as_random_alnum_string (gssize len) { gchar *ret; static char alnum_plain_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "1234567890"; ret = g_new0 (gchar, len + 1); for (gssize i = 0; i < len; i++) ret[i] = alnum_plain_chars[g_random_int_range (0, strlen (alnum_plain_chars))]; return ret; } /** * as_utils_find_stock_icon_filename_full: * @root_dir: the directory to search in, including prefix. * @icon_name: the stock icon search name, e.g. "microphone.svg" or "kate" * @icon_size: the icon size, e.g. 64 or 128. If size is 0, the first found icon is returned. * @icon_scale: the icon scaling factor, 1 for non HiDPI displays * @error: a #GError or %NULL * * Finds an icon filename in the filesystem that matches the given specifications. * This function may return a bigger icon than requested, which is suitable to be scaled down * to the actually requested size. * If no icon with the right scale factor is found, %NULL is returned. * * This algorithm does not implement the full Freedesktop icon theme specification, * instead is is designed to find 99% of all application icons quickly and * efficiently. It is not explicitly designed to find non-application stock * icons as well. * It also deliberately does not support legacy icon search locations and formats. * It will however work on incomplete directory trees with missing icon theme definition files, * by using a heuristic to find the right icon. * If you need more features, and have a complete icon theme definition installed, use the * icon-finding functions provided by GTK+ and Qt instead. * * If @icon_name is an absolute path, it will be returned unconditionally, as long * as the icon it references exists on the filesystem. * * Returns: (transfer full): a newly allocated %NULL terminated string * * Since: 0.14.5 **/ gchar * as_utils_find_stock_icon_filename_full (const gchar *root_dir, const gchar *icon_name, guint icon_size, guint icon_scale, GError **error) { guint min_size_idx = 0; const gchar *supported_ext[] = { ".png", ".svg", ".svgz", "", NULL }; const struct { guint size; const gchar *size_str; } sizes[] = { {48, "48x48" }, { 64, "64x64" }, { 96, "96x96" }, { 128, "128x128" }, { 256, "256x256" }, { 512, "512x512" }, { 0, "scalable"}, { 0, NULL } }; const gchar *types[] = { "actions", "animations", "apps", "categories", "devices", "emblems", "emotes", "filesystems", "intl", "mimetypes", "places", "status", "stock", NULL }; g_autofree gchar *prefix = NULL; g_return_val_if_fail (icon_name != NULL, NULL); /* fallbacks & sanitizations */ if (root_dir == NULL) root_dir = ""; if (icon_scale <= 0) icon_scale = 1; if (icon_size > 512) icon_size = 512; /* is this an absolute path */ if (icon_name[0] == '/') { g_autofree gchar *tmp = NULL; tmp = g_build_filename (root_dir, icon_name, NULL); if (!g_file_test (tmp, G_FILE_TEST_EXISTS)) { g_set_error (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "specified icon '%s' does not exist", icon_name); return NULL; } return g_strdup (tmp); } /* detect prefix */ prefix = g_build_filename (root_dir, "usr", NULL); if (!g_file_test (prefix, G_FILE_TEST_EXISTS)) { g_free (prefix); prefix = g_strdup (root_dir); } if (!g_file_test (prefix, G_FILE_TEST_EXISTS)) { g_set_error (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "Failed to find icon '%s' in %s", icon_name, prefix); return NULL; } /* select minimum size */ for (guint i = 0; sizes[i].size_str != NULL; i++) { if (sizes[i].size >= icon_size) { min_size_idx = i; break; } } /* hicolor icon theme search */ for (guint i = min_size_idx; sizes[i].size_str != NULL; i++) { g_autofree gchar *size = NULL; if (icon_scale == 1) size = g_strdup (sizes[i].size_str); else size = g_strdup_printf ("%s@%i", sizes[i].size_str, icon_scale); for (guint m = 0; types[m] != NULL; m++) { for (guint j = 0; supported_ext[j] != NULL; j++) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf ("%s/share/icons/" "hicolor/%s/%s/%s%s", prefix, size, types[m], icon_name, supported_ext[j]); if (g_file_test (tmp, G_FILE_TEST_EXISTS)) return g_strdup (tmp); } } } /* breeze icon theme search, for KDE Plasma compatibility */ for (guint i = min_size_idx; sizes[i].size_str != NULL; i++) { g_autofree gchar *size = NULL; if (icon_scale == 1) size = g_strdup (sizes[i].size_str); else size = g_strdup_printf ("%s@%i", sizes[i].size_str, icon_scale); for (guint m = 0; types[m] != NULL; m++) { for (guint j = 0; supported_ext[j] != NULL; j++) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf ("%s/share/icons/" "breeze/%s/%s/%s%s", prefix, types[m], size, icon_name, supported_ext[j]); if (g_file_test (tmp, G_FILE_TEST_EXISTS)) return g_strdup (tmp); } } } /* failed */ g_set_error (error, AS_UTILS_ERROR, AS_UTILS_ERROR_FAILED, "Failed to find icon %s", icon_name); return NULL; } /** * as_utils_guess_scope_from_path: * @path: The filename to test. * * Guess the #AsComponentScope that applies to a given path. * * Returns: the #AsComponentScope * * Since: 0.15.0 */ AsComponentScope as_utils_guess_scope_from_path (const gchar *path) { if (g_str_has_prefix (path, "/home") || g_str_has_prefix (path, g_get_home_dir ())) return AS_COMPONENT_SCOPE_USER; return AS_COMPONENT_SCOPE_SYSTEM; } /** * as_make_usertag_key: * @ns: the namespace * @tag: the tag * * Helper for release and component user tags. */ gchar * as_make_usertag_key (const gchar *ns, const gchar *tag) { if (ns == NULL) ns = ""; return g_strconcat (ns, "::", tag, NULL); } /** * as_utils_get_tag_search_weight: * @tag_name: A tag name in a component element, e.g. "name" or "summary" or "keyword" * * Retrieve the raw search token weight for the given tag name that AppStream uses * internally for searching. * This can be used to implement separate, but compatible search logic. * * Returns: The tag weight used in (fulltext) searches. 0 for lowest weight/unused. */ guint16 as_utils_get_tag_search_weight (const gchar *tag_name) { if (as_str_equal0 (tag_name, "id")) return AS_SEARCH_TOKEN_MATCH_ID; if (as_str_equal0 (tag_name, "name")) return AS_SEARCH_TOKEN_MATCH_NAME; if (as_str_equal0 (tag_name, "keyword")) return AS_SEARCH_TOKEN_MATCH_KEYWORD; if (as_str_equal0 (tag_name, "summary")) return AS_SEARCH_TOKEN_MATCH_SUMMARY; if (as_str_equal0 (tag_name, "description")) return AS_SEARCH_TOKEN_MATCH_DESCRIPTION; if (as_str_equal0 (tag_name, "origin")) return AS_SEARCH_TOKEN_MATCH_ORIGIN; if (as_str_equal0 (tag_name, "pkgname")) return AS_SEARCH_TOKEN_MATCH_PKGNAME; if (as_str_equal0 (tag_name, "mediatype")) return AS_SEARCH_TOKEN_MATCH_MEDIATYPE; return AS_SEARCH_TOKEN_MATCH_NONE; }