/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2016-2024 Matthias Klumpp * * 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 . */ /** * SECTION:asc-compose * @short_description: Compose catalog metadata easily. * @include: appstream-compose.h */ #include "config.h" #include "asc-compose.h" #include #include #include "as-utils-private.h" #include "as-yaml.h" #include "as-curl.h" #include "asc-globals-private.h" #include "asc-utils.h" #include "asc-hint.h" #include "asc-result.h" #include "asc-utils-metainfo.h" #include "asc-utils-l10n.h" #include "asc-utils-screenshots.h" #include "asc-utils-fonts.h" #include "asc-image.h" typedef struct { GPtrArray *units; GPtrArray *results; AscUnit *locale_unit; GHashTable *allowed_cids; GRefString *prefix; GRefString *origin; gchar *media_baseurl; AsFormatKind format; guint min_l10n_percentage; GPtrArray *custom_allowed; gssize max_scr_size_bytes; gchar *cainfo; AscComposeFlags flags; AscIconPolicy *icon_policy; gchar *data_result_dir; gchar *icons_result_dir; gchar *media_result_dir; gchar *hints_result_dir; GHashTable *known_cids; GMutex mutex; AscCheckMetadataEarlyFn check_md_early_fn; gpointer check_md_early_fn_udata; AscTranslateDesktopTextFn de_l10n_fn; gpointer de_l10n_fn_udata; } AscComposePrivate; G_DEFINE_TYPE_WITH_PRIVATE (AscCompose, asc_compose, G_TYPE_OBJECT) #define GET_PRIVATE(o) (asc_compose_get_instance_private (o)) static void asc_compose_init (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); priv->units = g_ptr_array_new_with_free_func (g_object_unref); priv->results = g_ptr_array_new_with_free_func (g_object_unref); priv->allowed_cids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); priv->known_cids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); priv->custom_allowed = g_ptr_array_new_with_free_func (g_free); g_mutex_init (&priv->mutex); /* defaults */ priv->format = AS_FORMAT_KIND_XML; as_ref_string_assign_safe (&priv->prefix, "/usr"); priv->min_l10n_percentage = 25; priv->max_scr_size_bytes = -1; priv->flags = ASC_COMPOSE_FLAG_USE_THREADS | ASC_COMPOSE_FLAG_ALLOW_NET | ASC_COMPOSE_FLAG_VALIDATE | ASC_COMPOSE_FLAG_STORE_SCREENSHOTS | ASC_COMPOSE_FLAG_ALLOW_SCREENCASTS | ASC_COMPOSE_FLAG_PROCESS_FONTS | ASC_COMPOSE_FLAG_PROCESS_TRANSLATIONS; /* the icon policy will initialize with default settings */ priv->icon_policy = asc_icon_policy_new (); } static void asc_compose_finalize (GObject *object) { AscCompose *compose = ASC_COMPOSE (object); AscComposePrivate *priv = GET_PRIVATE (compose); g_ptr_array_unref (priv->units); g_ptr_array_unref (priv->results); g_ptr_array_unref (priv->custom_allowed); g_free (priv->cainfo); g_hash_table_unref (priv->allowed_cids); g_hash_table_unref (priv->known_cids); as_ref_string_release (priv->prefix); as_ref_string_release (priv->origin); g_free (priv->media_baseurl); g_free (priv->data_result_dir); g_free (priv->icons_result_dir); g_free (priv->media_result_dir); g_free (priv->hints_result_dir); if (priv->locale_unit != NULL) g_object_unref (priv->locale_unit); g_mutex_clear (&priv->mutex); G_OBJECT_CLASS (asc_compose_parent_class)->finalize (object); } static void asc_compose_class_init (AscComposeClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = asc_compose_finalize; } /** * asc_compose_reset: * @compose: an #AscCompose instance. * * Reset the results, units and run-specific settings so the * instance can be reused for another metadata generation run. **/ void asc_compose_reset (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); g_hash_table_remove_all (priv->allowed_cids); g_ptr_array_set_size (priv->units, 0); g_ptr_array_set_size (priv->results, 0); g_hash_table_remove_all (priv->known_cids); } /** * asc_compose_add_unit: * @compose: an #AscCompose instance. * @unit: The #AscUnit to add * * Add an #AscUnit as data source for metadata processing. **/ void asc_compose_add_unit (AscCompose *compose, AscUnit *unit) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); /* sanity check */ for (guint i = 0; i < priv->units->len; i++) { if (unit == g_ptr_array_index (priv->units, i)) { g_critical ("Not adding unit duplicate for processing!"); return; } } g_ptr_array_add (priv->units, g_object_ref (unit)); } /** * asc_compose_add_allowed_cid: * @compose: an #AscCompose instance. * @component_id: The component-id to whitelist * * Adds a component ID to the allowlist. If the list is not empty, only * components in the list will be added to the metadata output. **/ void asc_compose_add_allowed_cid (AscCompose *compose, const gchar *component_id) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); g_hash_table_add (priv->allowed_cids, g_strdup (component_id)); } /** * asc_compose_get_prefix: * @compose: an #AscCompose instance. * * Get the directory prefix used for processing. */ const gchar * asc_compose_get_prefix (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->prefix; } /** * asc_compose_set_prefix: * @compose: an #AscCompose instance. * @prefix: a directory prefix, e.g. "/usr" * * Set the directory prefix the to-be-processed units are using. */ void asc_compose_set_prefix (AscCompose *compose, const gchar *prefix) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); /* do a bit of sanitizing: "no prefix" means the prefix directory is the root directory */ if (prefix == NULL || g_strcmp0 (prefix, "") == 0) as_ref_string_assign_safe (&priv->prefix, "/"); else as_ref_string_assign_safe (&priv->prefix, prefix); } /** * asc_compose_get_origin: * @compose: an #AscCompose instance. * * Get the metadata origin field. */ const gchar * asc_compose_get_origin (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->origin; } /** * asc_compose_set_origin: * @compose: an #AscCompose instance. * @origin: the origin. * * Set the metadata origin field (e.g. "debian" or "flathub") */ void asc_compose_set_origin (AscCompose *compose, const gchar *origin) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); g_autofree gchar *tmp = NULL; tmp = g_markup_escape_text (origin, -1); as_ref_string_assign_safe (&priv->origin, tmp); } /** * asc_compose_get_format: * @compose: an #AscCompose instance. * * get the format type we are generating. */ AsFormatKind asc_compose_get_format (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->format; } /** * asc_compose_set_format: * @compose: an #AscCompose instance. * @kind: The format, e.g. %AS_FORMAT_KIND_XML * * Set the format kind of the catalog metadata that we should generate. */ void asc_compose_set_format (AscCompose *compose, AsFormatKind kind) { AscComposePrivate *priv = GET_PRIVATE (compose); priv->format = kind; } /** * asc_compose_get_media_baseurl: * @compose: an #AscCompose instance. * * Get the media base URL to be used for the generated data, * or %NULL if no media is cached. */ const gchar * asc_compose_get_media_baseurl (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->media_baseurl; } /** * asc_compose_set_media_baseurl: * @compose: an #AscCompose instance. * @url: (nullable): the media base URL. * * Set the media base URL for the generated metadata. Can be %NULL if no media * should be cached and the original URLs should be kept. */ void asc_compose_set_media_baseurl (AscCompose *compose, const gchar *url) { AscComposePrivate *priv = GET_PRIVATE (compose); as_assign_string_safe (priv->media_baseurl, url); } /** * asc_compose_get_flags: * @compose: an #AscCompose instance. * * Get the flags controlling compose behavior. */ AscComposeFlags asc_compose_get_flags (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->flags; } /** * asc_compose_set_flags: * @compose: an #AscCompose instance. * @flags: The compose flags bitfield. * * Set compose flags bitfield that controls the enabled features * for this #AscCompose. */ void asc_compose_set_flags (AscCompose *compose, AscComposeFlags flags) { AscComposePrivate *priv = GET_PRIVATE (compose); priv->flags = flags; } /** * asc_compose_add_flags: * @compose: an #AscCompose instance. * @flags: The compose flags to add. * * Add compose flags. */ void asc_compose_add_flags (AscCompose *compose, AscComposeFlags flags) { AscComposePrivate *priv = GET_PRIVATE (compose); as_flags_add (priv->flags, flags); } /** * asc_compose_remove_flags: * @compose: an #AscCompose instance. * @flags: The compose flags to remove. * * Remove compose flags. */ void asc_compose_remove_flags (AscCompose *compose, AscComposeFlags flags) { AscComposePrivate *priv = GET_PRIVATE (compose); as_flags_remove (priv->flags, flags); } /** * asc_compose_get_icon_policy: * @compose: an #AscCompose instance. * * Get the policy for how icons should be distributed to * any AppStream clients. * * Returns: (transfer none): an #AscIconPolicy */ AscIconPolicy * asc_compose_get_icon_policy (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->icon_policy; } /** * asc_compose_set_icon_policy: * @compose: an #AscCompose instance. * @policy: (not nullable): an #AscIconPolicy instance * * Set an icon policy object, overriding the existing one. */ void asc_compose_set_icon_policy (AscCompose *compose, AscIconPolicy *policy) { AscComposePrivate *priv = GET_PRIVATE (compose); g_return_if_fail (policy != NULL); g_object_unref (priv->icon_policy); priv->icon_policy = g_object_ref (policy); } /** * asc_compose_get_cainfo: * @compose: an #AscCompose instance. * * Get the CA file used to verify peers with, or %NULL for default. */ const gchar * asc_compose_get_cainfo (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->cainfo; } /** * asc_compose_set_cainfo: * @compose: an #AscCompose instance. * @cainfo: a valid file path * * Set a CA file holding one or more certificates to verify peers with * for download operations performed by this #AscCompose. */ void asc_compose_set_cainfo (AscCompose *compose, const gchar *cainfo) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); as_assign_string_safe (priv->cainfo, cainfo); } /** * asc_compose_get_data_result_dir: * @compose: an #AscCompose instance. * * Get the data result directory. */ const gchar * asc_compose_get_data_result_dir (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->data_result_dir; } /** * asc_compose_set_data_result_dir: * @compose: an #AscCompose instance. * @dir: the metadata save location. * * Set an output location where generated metadata should be saved. * If this is set to %NULL, no metadata will be saved. */ void asc_compose_set_data_result_dir (AscCompose *compose, const gchar *dir) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); as_assign_string_safe (priv->data_result_dir, dir); } /** * asc_compose_get_icons_result_dir: * @compose: an #AscCompose instance. * * Get the icon result directory. */ const gchar * asc_compose_get_icons_result_dir (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->icons_result_dir; } /** * asc_compose_set_icons_result_dir: * @compose: an #AscCompose instance. * @dir: the icon storage location. * * Set an output location where plain icons for the processed metadata * are stored. */ void asc_compose_set_icons_result_dir (AscCompose *compose, const gchar *dir) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); as_assign_string_safe (priv->icons_result_dir, dir); } /** * asc_compose_get_media_result_dir: * @compose: an #AscCompose instance. * * Get the media result directory, that can be served on a webserver. */ const gchar * asc_compose_get_media_result_dir (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->media_result_dir; } /** * asc_compose_set_media_result_dir: * @compose: an #AscCompose instance. * @dir: the media storage location. * * Set an output location to store media (screenshots, icons, ...) that * will be served on a webserver via the URL set as media baseurl. */ void asc_compose_set_media_result_dir (AscCompose *compose, const gchar *dir) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); as_assign_string_safe (priv->media_result_dir, dir); } /** * asc_compose_get_hints_result_dir: * @compose: an #AscCompose instance. * * Get hints report output directory. */ const gchar * asc_compose_get_hints_result_dir (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->hints_result_dir; } /** * asc_compose_set_hints_result_dir: * @compose: an #AscCompose instance. * @dir: the hints data directory. * * Set an output location for HTML reports of issues generated * during a compose run. */ void asc_compose_set_hints_result_dir (AscCompose *compose, const gchar *dir) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); as_assign_string_safe (priv->hints_result_dir, dir); } /** * asc_compose_remove_custom_allowed: * @compose: an #AscCompose instance. * @key_id: the custom key to drop from the allowed list. * * Remove a key from the allowlist used to filter the `` tag entries. */ void asc_compose_remove_custom_allowed (AscCompose *compose, const gchar *key_id) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); for (guint i = 0; i < priv->custom_allowed->len; i++) { if (g_strcmp0 (g_ptr_array_index (priv->custom_allowed, i), key_id) == 0) { g_ptr_array_remove_index_fast (priv->custom_allowed, i); break; } } } /** * asc_compose_add_custom_allowed: * @compose: an #AscCompose instance. * @key_id: the custom key to add to the allowed list. * * Add a key to the allowlist that is used to filter custom tag values. */ void asc_compose_add_custom_allowed (AscCompose *compose, const gchar *key_id) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); g_ptr_array_add (priv->custom_allowed, g_strdup (key_id)); } /** * asc_compose_get_max_screenshot_size: * @compose: an #AscCompose instance. * * Get the maximum size a screenshot video or image can have. * A size < 0 may be returned for no limit, setting a limit of 0 * will disable screenshots. */ gssize asc_compose_get_max_screenshot_size (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->max_scr_size_bytes; } /** * asc_compose_set_max_screenshot_size: * @compose: an #AscCompose instance. * @size_bytes: maximum size of a screenshot image or video in bytes * * Set the maximum size a screenshot video or image can have. * A size < 0 may be set to allow unlimited sizes, setting a limit of 0 * will disable screenshot caching entirely. */ void asc_compose_set_max_screenshot_size (AscCompose *compose, gssize size_bytes) { AscComposePrivate *priv = GET_PRIVATE (compose); priv->max_scr_size_bytes = size_bytes; } /** * asc_compose_set_check_metadata_early_func: * @compose: an #AscCompose instance. * @func: (scope notified): the #AscCheckMetainfoLoadResultFn function to be called * @user_data: user data for @func * * Set an custom callback to be run when most of the metadata has been loaded, * but no expensive operations (like downloads or icon rendering) have been done yet. * This can be used to ignore unwanted components early on. * * The callback function may be called from any thread, so it needs to ensure thread safety on its own. */ void asc_compose_set_check_metadata_early_func (AscCompose *compose, AscCheckMetadataEarlyFn func, gpointer user_data) { AscComposePrivate *priv = GET_PRIVATE (compose); priv->check_md_early_fn = func; priv->check_md_early_fn_udata = user_data; } /** * asc_compose_set_desktop_entry_l10n_func: * @compose: an #AscCompose instance. * @func: (scope notified): the #AscTranslateDesktopTextFn function to be called * @user_data: user data for @func * * Set a custom desktop-entry field localization functions to be run for specialized * desktop-entry localization schemes such as used in Ubuntu. * * The callback function may be called from any thread, so it needs to ensure thread safety on its own. */ void asc_compose_set_desktop_entry_l10n_func (AscCompose *compose, AscTranslateDesktopTextFn func, gpointer user_data) { AscComposePrivate *priv = GET_PRIVATE (compose); priv->de_l10n_fn = func; priv->de_l10n_fn_udata = user_data; } /** * asc_compose_get_locale_unit: * @compose: an #AscCompose instance. * * Get the unit we use for locale processing * * Return: (transfer none) (nullable): The unit used for locale processing, or %NULL for default. */ AscUnit * asc_compose_get_locale_unit (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->locale_unit; } /** * asc_compose_set_locale_unit: * @compose: an #AscCompose instance. * @locale_unit: the unit used for locale processing. * * Set a specific unit that is used for fetching locale information. * This may be useful in case a special language pack layout is used, * but is generally not necessary to be set explicitly, as locale * will be found in the unit where the metadata is by default. */ void asc_compose_set_locale_unit (AscCompose *compose, AscUnit *locale_unit) { AscComposePrivate *priv = GET_PRIVATE (compose); if (priv->locale_unit == locale_unit) return; if (priv->locale_unit != NULL) g_object_unref (priv->locale_unit); priv->locale_unit = g_object_ref (locale_unit); } /** * asc_compose_get_results: * @compose: an #AscCompose instance. * * Get the results of the last processing run. * * Returns: (transfer none) (element-type AscResult): The results */ GPtrArray * asc_compose_get_results (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); return priv->results; } /** * asc_compose_fetch_components: * @compose: an #AscCompose instance. * * Get the results components extracted in the last data processing run. * * Returns: (transfer container) (element-type AsComponent): The components */ GPtrArray * asc_compose_fetch_components (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); GPtrArray *cpts_result = g_ptr_array_new_with_free_func (g_object_unref); for (guint i = 0; i < priv->results->len; i++) { g_autoptr(GPtrArray) cpts = NULL; AscResult *res = ASC_RESULT (g_ptr_array_index (priv->results, i)); cpts = asc_result_fetch_components (res); for (guint j = 0; j < cpts->len; j++) g_ptr_array_add (cpts_result, g_object_ref (AS_COMPONENT (g_ptr_array_index (cpts, j)))); } return cpts_result; } /** * asc_compose_has_errors: * @compose: an #AscCompose instance. * * Check if the last run generated any errors (which will cause metadata to be ignored). * * Returns: %TRUE if we had errors. */ gboolean asc_compose_has_errors (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); for (guint i = 0; i < priv->results->len; i++) { g_autoptr(GPtrArray) hints = NULL; AscResult *res = ASC_RESULT (g_ptr_array_index (priv->results, i)); hints = asc_result_fetch_hints_all (res); for (guint j = 0; j < hints->len; j++) { AscHint *hint = ASC_HINT (g_ptr_array_index (hints, j)); if (asc_hint_is_error (hint)) return TRUE; } } return FALSE; } typedef struct { AscUnit *unit; AscResult *result; GHashTable *files_units_map; /* no ref */ } AscComposeTask; static AscComposeTask * asc_compose_task_new (AscUnit *unit) { AscComposeTask *ctask; ctask = g_new0 (AscComposeTask, 1); ctask->unit = g_object_ref (unit); ctask->result = asc_result_new (); return ctask; } static void asc_compose_task_free (AscComposeTask *ctask) { g_object_unref (ctask->unit); g_object_unref (ctask->result); g_free (ctask); } /** * asc_compose_find_icon_filename: */ static gchar * asc_compose_find_icon_filename (AscCompose *compose, AscUnit *unit, const gchar *icon_name, guint icon_size, guint icon_scale) { AscComposePrivate *priv = GET_PRIVATE (compose); guint min_size_idx = 0; guint min_ext_idx = 0; gboolean vector_relaxed = FALSE; gboolean scale_relaxed = FALSE; /* clang-format off */ const gchar *supported_ext[] = { ".png", ".svg", ".svgz", "", NULL }; const struct { guint size; const gchar *size_str; } sizes[] = { { 48, "48x48" }, { 32, "32x32" }, { 64, "64x64" }, { 96, "96x96" }, { 128, "128x128" }, { 256, "256x256" }, { 512, "512x512" }, { 0, "scalable"}, { 0, NULL } }; const gchar *types[] = { "actions", "apps", "applets", "categories", "devices", "emblems", "emotes", "filesystems", "mimetypes", "places", "preferences", "status", "stock", NULL }; /* clang-format on */ g_return_val_if_fail (icon_name != NULL, NULL); /* fallbacks & sanitizations */ 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 (priv->prefix, icon_name, NULL); if (!asc_unit_file_exists (unit, tmp)) return NULL; return g_strdup (tmp); } /* select minimum size */ for (guint i = 0; sizes[i].size_str != NULL; i++) { if (sizes[i].size >= icon_size) { min_size_idx = i; break; } } while (TRUE) { /* 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 = min_ext_idx; supported_ext[j] != NULL; j++) { g_autofree gchar *tmp = NULL; /* skip bitmaps if we only want vector graphics anyway */ if ((scale_relaxed || vector_relaxed) && as_str_equal0 (supported_ext[j], ".png")) continue; tmp = g_strdup_printf ("%s/share/icons/" "hicolor/%s/%s/%s%s", priv->prefix, size, types[m], icon_name, supported_ext[j]); if (asc_unit_file_exists (unit, tmp)) 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_printf ("%i", sizes[i].size); else size = g_strdup_printf ("%i@%i", sizes[i].size, icon_scale); for (guint m = 0; types[m] != NULL; m++) { for (guint j = min_ext_idx; supported_ext[j] != NULL; j++) { g_autofree gchar *tmp = NULL; /* skip bitmaps if we only want vector graphica anyway */ if ((scale_relaxed || vector_relaxed) && as_str_equal0 (supported_ext[j], ".png")) continue; tmp = g_strdup_printf ("%s/share/icons/" "breeze/%s/%s/%s%s", priv->prefix, types[m], size, icon_name, supported_ext[j]); if (asc_unit_file_exists (unit, tmp)) return g_strdup (tmp); } } } if (vector_relaxed) { if (scale_relaxed || icon_scale == 1) { break; } else { /* check if we can scale up a vector graphic to match the requested size */ min_size_idx = 0; min_ext_idx = 1; icon_size = icon_size * icon_scale; icon_scale = 1; } } else { if (g_str_has_suffix (icon_name, ".png")) break; /* try again, searching for vector graphics that we can scale up */ vector_relaxed = TRUE; min_size_idx = 0; /* start at index 1, where the SVG icons are */ min_ext_idx = 1; } } /* failed to find any icon */ return NULL; } static void asc_compose_process_icons (AscCompose *compose, AscResult *cres, AsComponent *cpt, AscUnit *unit, const gchar *icon_export_dir) { AscComposePrivate *priv = GET_PRIVATE (compose); GPtrArray *icons = NULL; g_autoptr(AsIcon) stock_icon = NULL; const gchar *icon_name = NULL; AscIconPolicyIter iter; guint size; guint scale_factor; AscIconState icon_state; /* do nothing if we have no icons to process */ icons = as_component_get_icons (cpt); if (icons == NULL || icons->len == 0) return; /* find a suitable stock icon as template */ for (guint i = 0; i < icons->len; i++) { AsIcon *icon = AS_ICON (g_ptr_array_index (icons, i)); if (as_icon_get_kind (icon) == AS_ICON_KIND_STOCK) { stock_icon = icon; break; } /* we cheat here to accomodate for apps which used the "local" icon type wrong */ if (as_icon_get_kind (icon) == AS_ICON_KIND_LOCAL) stock_icon = icon; } /* drop all preexisting icons */ if (stock_icon != NULL) stock_icon = g_object_ref (stock_icon); g_ptr_array_set_size (as_component_get_icons (cpt), 0); if (stock_icon == NULL) { asc_result_add_hint_simple (cres, cpt, "no-stock-icon"); return; } icon_name = as_icon_get_name (stock_icon); if (as_is_empty (icon_name) || icon_name[0] == ' ') { /* and invalid stock icon is like having none at all */ asc_result_add_hint_simple (cres, cpt, "no-stock-icon"); return; } asc_icon_policy_iter_init (&iter, priv->icon_policy); while (asc_icon_policy_iter_next (&iter, &size, &scale_factor, &icon_state)) { g_autofree gchar *icon_fname = NULL; g_autofree gchar *res_icon_fname = NULL; g_autofree gchar *res_icon_size_str = NULL; g_autofree gchar *res_icon_sizedir = NULL; g_autofree gchar *res_icon_basename = NULL; g_autoptr(AscImage) img = NULL; g_autoptr(AsIcon) icon = NULL; g_autoptr(GBytes) img_bytes = NULL; gboolean is_vector_icon = FALSE; const void *img_data; gsize img_len; g_autoptr(GError) error = NULL; /* skip icon if its size should be skipped */ if (icon_state == ASC_ICON_STATE_IGNORED) continue; icon_fname = asc_compose_find_icon_filename (compose, unit, icon_name, size, scale_factor); if (icon_fname == NULL) { /* only a 64x64px icon is mandatory, everything else is optional */ if (size == 64 && scale_factor == 1) { asc_result_add_hint (cres, cpt, "icon-not-found", "icon_fname", icon_name, NULL); return; } continue; } is_vector_icon = g_str_has_suffix (icon_fname, ".svgz") || g_str_has_suffix (icon_fname, ".svg"); img_bytes = asc_unit_read_data (unit, icon_fname, &error); if (img_bytes == NULL) { asc_result_add_hint (cres, cpt, "file-read-error", "fname", icon_fname, "msg", error->message, NULL); return; } img_data = g_bytes_get_data (img_bytes, &img_len); img = asc_image_new_from_data (img_data, img_len, is_vector_icon ? size * scale_factor : 0, g_str_has_suffix (icon_fname, ".svgz"), ASC_IMAGE_LOAD_FLAG_ALWAYS_RESIZE, &error); if (img == NULL) { asc_result_add_hint (cres, cpt, "file-read-error", "fname", icon_fname, "msg", error->message, NULL); return; } /* we only take exact-ish size matches for 48x48px */ if (size == 48 && asc_image_get_width (img) > 48) continue; res_icon_size_str = (scale_factor == 1) ? g_strdup_printf ("%ix%i", size, size) : g_strdup_printf ("%ix%i@%i", size, size, scale_factor); res_icon_sizedir = g_build_filename (icon_export_dir, res_icon_size_str, NULL); g_mkdir_with_parents (res_icon_sizedir, 0755); res_icon_basename = g_strdup_printf ("%s.png", as_component_get_id (cpt)); res_icon_fname = g_build_filename (res_icon_sizedir, res_icon_basename, NULL); /* scale & save the image */ g_debug ("Saving icon: %s", res_icon_fname); if (!asc_image_save_filename (img, res_icon_fname, size * scale_factor, size * scale_factor, ASC_IMAGE_SAVE_FLAG_OPTIMIZE, &error)) { asc_result_add_hint (cres, cpt, "icon-write-error", "fname", icon_fname, "msg", error->message, NULL); return; } /* create a remote reference if we have data for it */ if (priv->media_result_dir != NULL && icon_state != ASC_ICON_STATE_CACHED_ONLY) { g_autofree gchar *icons_media_urlpart_dir = NULL; g_autofree gchar *icon_media_urlpart_fname = NULL; g_autofree gchar *icons_media_path = NULL; g_autofree gchar *icon_media_fname = NULL; g_autoptr(AsIcon) remote_icon = NULL; icons_media_urlpart_dir = g_strdup_printf ( "%s/%s/%s", asc_result_gcid_for_component (cres, cpt), "icons", res_icon_size_str); icon_media_urlpart_fname = g_strdup_printf ("%s/%s", icons_media_urlpart_dir, res_icon_basename); icons_media_path = g_build_filename (priv->media_result_dir, icons_media_urlpart_dir, NULL); icon_media_fname = g_build_filename (icons_media_path, res_icon_basename, NULL); g_mkdir_with_parents (icons_media_path, 0755); g_debug ("Adding media pool icon: %s", icon_media_fname); if (!as_copy_file (res_icon_fname, icon_media_fname, &error)) { g_warning ("Unable to write media pool icon: %s", icon_media_fname); asc_result_add_hint (cres, cpt, "icon-write-error", "fname", icon_fname, "msg", error->message, NULL); return; } /* add remote icon to metadata */ remote_icon = as_icon_new (); as_icon_set_kind (remote_icon, AS_ICON_KIND_REMOTE); as_icon_set_width (remote_icon, size); as_icon_set_height (remote_icon, size); as_icon_set_scale (remote_icon, scale_factor); /* We can only make use of the media-baseurl-using partial URLs if screenshot storage * is also enabled, because otherwise screenshots will use full URLs which conflicts * with the media baseurl (as it is unconditionally prefixed to *all* media URLs */ if (as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS) && !as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_NO_PARTIAL_URLS)) { as_icon_set_url (remote_icon, icon_media_urlpart_fname); } else { g_autofree gchar *icon_remote_url = g_strconcat ( priv->media_baseurl, "/", icon_media_urlpart_fname, NULL); /* if priv->media_result_dir is set, media_baseurl will be set too (checked before each run) */ as_icon_set_url (remote_icon, icon_remote_url); } as_component_add_icon (cpt, remote_icon); } /* add icon to metadata */ if (icon_state != ASC_ICON_STATE_REMOTE_ONLY) { icon = as_icon_new (); as_icon_set_kind (icon, AS_ICON_KIND_CACHED); as_icon_set_width (icon, size); as_icon_set_height (icon, size); as_icon_set_scale (icon, scale_factor); as_icon_set_name (icon, res_icon_basename); as_component_add_icon (cpt, icon); } } /* fix some stock icon mistakes and add the stock icon back */ if (as_icon_get_kind (stock_icon) == AS_ICON_KIND_STOCK) { g_autofree gchar *tmp = NULL; gsize icon_name_len = strlen (icon_name); tmp = g_strdup (icon_name); if (g_str_has_suffix (icon_name, ".png") || g_str_has_suffix (icon_name, ".svg")) tmp[icon_name_len - 4] = '\0'; else if (g_str_has_suffix (icon_name, ".svgz")) tmp[icon_name_len - 5] = '\0'; as_icon_set_width (stock_icon, 0); as_icon_set_height (stock_icon, 0); as_icon_set_scale (stock_icon, 1); as_icon_set_name (stock_icon, tmp); as_component_add_icon (cpt, stock_icon); } } /** * as_compose_yaml_write_handler_cb: * * Helper function to store the emitted YAML document. */ static int as_compose_yaml_write_handler_cb (void *ptr, unsigned char *buffer, size_t size) { GString *str; str = (GString *) ptr; g_string_append_len (str, (const gchar *) buffer, size); return 1; } static gboolean asc_compose_component_known (AscCompose *compose, AsComponent *cpt) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); return g_hash_table_contains (priv->known_cids, as_component_get_id (cpt)); } /** * asc_evaluate_custom_entry_cb: * * Helper function for asc_compose_finalize_components() */ static gboolean asc_evaluate_custom_entry_cb (gpointer key_p, gpointer value_p, gpointer user_data) { const gchar *key = (const gchar *) key_p; GPtrArray *whitelist = (GPtrArray *) user_data; for (guint i = 0; i < whitelist->len; i++) { if (g_strcmp0 (g_ptr_array_index (whitelist, i), key) == 0) return FALSE; /* do not delete, key is in whitelist */ } /* remove key that was not allowed */ return TRUE; } static void asc_compose_finalize_components (AscCompose *compose, AscResult *cres) { AscComposePrivate *priv = GET_PRIVATE (compose); AsValueFlags value_flags; g_autoptr(GPtrArray) final_cpts = NULL; g_autoptr(AsContext) default_context = NULL; /* create a default context */ default_context = as_context_new (); as_context_set_locale (default_context, "C"); as_context_set_origin (default_context, priv->origin); as_context_set_style (default_context, AS_FORMAT_STYLE_CATALOG); value_flags = as_context_get_value_flags (default_context) | AS_VALUE_FLAG_NO_TRANSLATION_FALLBACK; as_context_set_value_flags (default_context, value_flags); final_cpts = asc_result_fetch_components (cres); for (guint i = 0; i < final_cpts->len; i++) { AsContext *cpt_ctx = NULL; AsComponent *cpt = AS_COMPONENT (g_ptr_array_index (final_cpts, i)); AsComponentKind ckind = as_component_get_kind (cpt); /* add bundle data if we have any */ if (asc_result_get_bundle_kind (cres) != AS_BUNDLE_KIND_UNKNOWN) { AsBundleKind bundle_kind = asc_result_get_bundle_kind (cres); g_ptr_array_set_size (as_component_get_bundles (cpt), 0); as_component_set_pkgname (cpt, NULL); if (bundle_kind == AS_BUNDLE_KIND_PACKAGE) { as_component_set_pkgname (cpt, asc_result_get_bundle_id (cres)); } else { g_autoptr(AsBundle) bundle = as_bundle_new (); as_bundle_set_kind (bundle, bundle_kind); as_bundle_set_id (bundle, asc_result_get_bundle_id (cres)); } } cpt_ctx = as_component_get_context (cpt); if (cpt_ctx == NULL) { as_component_set_context (cpt, default_context); } else { as_context_set_locale (cpt_ctx, "C"); as_context_set_value_flags (cpt_ctx, value_flags); } if (ckind == AS_COMPONENT_KIND_UNKNOWN) { if (!asc_result_add_hint_simple (cres, cpt, "metainfo-unknown-type")) continue; } /* filter custom entries */ if (!as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_PROPAGATE_CUSTOM)) { if (priv->custom_allowed->len == 0) { GHashTable *custom_entries = as_component_get_custom (cpt); /* no custom entries permitted in output */ g_hash_table_remove_all (custom_entries); } else { GHashTable *custom_entries = as_component_get_custom (cpt); g_hash_table_foreach_remove (custom_entries, asc_evaluate_custom_entry_cb, priv->custom_allowed); } } /* only perform the next checks if we don't have a merge-component * (which is by definition incomplete and only is required to have its ID present) */ if (as_component_get_merge_kind (cpt) != AS_MERGE_KIND_NONE) continue; /* strip out release artifacts unless we were told to propagate them */ if (!as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_PROPAGATE_ARTIFACTS)) { AsReleaseList *releases = as_component_get_releases_plain (cpt); for (guint j = 0; j < as_release_list_len (releases); j++) { AsRelease *rel = as_release_list_index (releases, j); g_ptr_array_set_size (as_release_get_artifacts (rel), 0); } } if (as_is_empty (as_component_get_name (cpt))) if (!asc_result_add_hint_simple (cres, cpt, "metainfo-no-name")) continue; if (as_is_empty (as_component_get_summary (cpt))) if (!asc_result_add_hint_simple (cres, cpt, "metainfo-no-summary")) continue; /* ensure that everything that should have an icon has one */ if (as_component_get_icons (cpt)->len == 0) { if (ckind == AS_COMPONENT_KIND_DESKTOP_APP) { if (!asc_result_add_hint_simple (cres, cpt, "gui-app-without-icon")) continue; } else if (ckind == AS_COMPONENT_KIND_WEB_APP) { if (!asc_result_add_hint_simple (cres, cpt, "web-app-without-icon")) continue; } else if (ckind == AS_COMPONENT_KIND_FONT) { if (!asc_result_add_hint_simple (cres, cpt, "font-without-icon")) continue; } else if (ckind == AS_COMPONENT_KIND_OPERATING_SYSTEM) { if (!asc_result_add_hint_simple (cres, cpt, "os-without-icon")) continue; } } if (ckind == AS_COMPONENT_KIND_DESKTOP_APP || ckind == AS_COMPONENT_KIND_CONSOLE_APP || ckind == AS_COMPONENT_KIND_WEB_APP) { /* desktop-application components are required to have a category */ if (ckind != AS_COMPONENT_KIND_CONSOLE_APP) { if (as_component_get_categories (cpt)->len <= 0) if (!asc_result_add_hint_simple (cres, cpt, "no-valid-category")) continue; } if (as_is_empty (as_component_get_description (cpt))) { if (!asc_result_add_hint (cres, cpt, "description-missing", "kind", as_component_kind_to_string (ckind), NULL)) continue; } } } } static void asc_compose_process_task_cb (AscComposeTask *ctask, AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autofree gchar *metainfo_dir = NULL; g_autofree gchar *app_dir = NULL; g_autofree gchar *share_dir = NULL; g_autoptr(AsMetadata) mdata = NULL; g_autoptr(AsValidator) validator = NULL; g_autoptr(GPtrArray) mi_fnames = NULL; g_autoptr(GHashTable) de_fname_map = NULL; g_autoptr(GPtrArray) found_cpts = NULL; g_autoptr(AsCurl) acurl = NULL; g_autoptr(GError) tmp_error = NULL; gboolean has_fonts = FALSE; gboolean filter_cpts = FALSE; GPtrArray *contents = NULL; /* propagate unit bundle ID */ asc_result_set_bundle_id (ctask->result, asc_unit_get_bundle_id (ctask->unit)); asc_result_set_bundle_kind (ctask->result, asc_unit_get_bundle_kind (ctask->unit)); /* configure metadata loader */ mdata = as_metadata_new (); as_metadata_set_locale (mdata, "ALL"); as_metadata_set_format_style (mdata, AS_FORMAT_STYLE_METAINFO); /* create validator */ validator = as_validator_new (); /* Curl interface for this task */ acurl = as_curl_new (&tmp_error); if (acurl == NULL) { g_critical ("Unable to initialize networking: %s", tmp_error->message); g_error_free (g_steal_pointer (&tmp_error)); } if (priv->cainfo != NULL) as_curl_set_cainfo (acurl, priv->cainfo); /* give unit a hint as to which paths we want to read */ share_dir = g_build_filename (priv->prefix, "share", NULL); asc_unit_add_relevant_path (ctask->unit, share_dir); /* open our unit for reading */ if (!asc_unit_open (ctask->unit, &tmp_error)) { g_warning ("Failed to open unit: %s", tmp_error->message); asc_result_add_hint (ctask->result, NULL, "unit-read-error", "name", asc_unit_get_bundle_id (ctask->unit), "msg", tmp_error->message, NULL); return; } contents = asc_unit_get_contents (ctask->unit); /* collect interesting data for this unit */ metainfo_dir = g_build_filename (share_dir, "metainfo", NULL); app_dir = g_build_filename (share_dir, "applications", NULL); mi_fnames = g_ptr_array_new_with_free_func (g_free); de_fname_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); g_debug ("Looking for metainfo files in: %s", metainfo_dir); for (guint i = 0; i < contents->len; i++) { const gchar *fname = g_ptr_array_index (contents, i); if (g_str_has_prefix (fname, metainfo_dir) && (g_str_has_suffix (fname, ".metainfo.xml") || g_str_has_suffix (fname, ".appdata.xml"))) { g_ptr_array_add (mi_fnames, g_strdup (fname)); continue; } if (g_str_has_prefix (fname, app_dir) && g_str_has_suffix (fname, ".desktop")) { g_hash_table_insert (de_fname_map, g_path_get_basename (fname), g_strdup (fname)); continue; } } /* check if we need to filter components */ filter_cpts = g_hash_table_size (priv->allowed_cids) > 0; /* process metadata */ for (guint i = 0; i < mi_fnames->len; i++) { g_autoptr(GBytes) mi_bytes = NULL; g_autoptr(GBytes) rel_bytes = NULL; g_autoptr(GError) local_error = NULL; g_autoptr(AsComponent) cpt = NULL; g_autofree gchar *mi_basename = NULL; const gchar *mi_fname = g_ptr_array_index (mi_fnames, i); mi_basename = g_path_get_basename (mi_fname); g_debug ("Processing: %s", mi_fname); mi_bytes = asc_unit_read_data (ctask->unit, mi_fname, &local_error); if (mi_bytes == NULL) { asc_result_add_hint_by_cid (ctask->result, mi_basename, "file-read-error", "fname", mi_fname, "msg", local_error->message, NULL); g_debug ("Failed '%s': %s", mi_basename, local_error->message); continue; } as_metadata_clear_components (mdata); cpt = asc_parse_metainfo_data (ctask->result, mdata, mi_bytes, mi_basename); if (cpt == NULL) { g_debug ("Rejected: %s", mi_basename); continue; } /* filter out this component if it isn't on the allowlist */ if (filter_cpts) { if (!g_hash_table_contains (priv->allowed_cids, as_component_get_id (cpt))) { asc_result_remove_component (ctask->result, cpt); continue; } } /* check if we have a duplicate */ if (asc_compose_component_known (compose, cpt)) { asc_result_add_hint_simple (ctask->result, cpt, "duplicate-component"); continue; } else { g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); g_hash_table_add (priv->known_cids, g_strdup (as_component_get_id (cpt))); } /* process any release information of this component and download release data if needed */ asc_process_metainfo_releases ( ctask->result, ctask->unit, cpt, mi_fname, as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_ALLOW_NET), acurl, &rel_bytes); /* validate the data */ if (as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_VALIDATE)) { asc_validate_metainfo_data_for_component (ctask->result, validator, cpt, mi_bytes, mi_basename, rel_bytes); } /* legacy support: Synthesize launchable entry if none was set, * but only if we actually need to do that. * At the moment we determine whether a .desktop file is needed by checking * if the metainfo file defines an icon (which is commonly provided by the .desktop * file instead of the metainfo file). * This heuristic is, of course, not ideal, which is why everything should have a launchable tag. */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) { AsLaunchable *launchable = as_component_get_launchable ( cpt, AS_LAUNCHABLE_KIND_DESKTOP_ID); if (launchable == NULL) { AsIcon *stock_icon = NULL; GPtrArray *icons = as_component_get_icons (cpt); for (guint k = 0; k < icons->len; k++) { AsIcon *icon = AS_ICON (g_ptr_array_index (icons, k)); if (as_icon_get_kind (icon) == AS_ICON_KIND_STOCK) { stock_icon = icon; break; } } if (stock_icon == NULL) { g_autoptr(AsLaunchable) launch = as_launchable_new (); g_autofree gchar *synth_desktop_id = NULL; if (g_str_has_suffix (as_component_get_id (cpt), ".desktop")) synth_desktop_id = g_strdup ( as_component_get_id (cpt)); else synth_desktop_id = g_strdup_printf ( "%s.desktop", as_component_get_id (cpt)); as_launchable_set_kind (launch, AS_LAUNCHABLE_KIND_DESKTOP_ID); as_launchable_add_entry (launch, synth_desktop_id); as_component_add_launchable (cpt, launch); } } } /* find an accompanying desktop-entry file, if one exists */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) { AsLaunchable *launchable = as_component_get_launchable ( cpt, AS_LAUNCHABLE_KIND_DESKTOP_ID); if (launchable != NULL) { GPtrArray *launch_entries = as_launchable_get_entries (launchable); for (guint j = 0; j < launch_entries->len; j++) { const gchar *de_basename = g_ptr_array_index ( launch_entries, j); const gchar *de_fname = g_hash_table_lookup (de_fname_map, de_basename); if (de_fname == NULL) { asc_result_add_hint ( ctask->result, cpt, "missing-launchable-desktop-file", "desktop_id", de_basename, NULL); continue; } if (j == 0) { /* add data from the first desktop-entry file to this app */ g_autoptr(AsComponent) de_cpt = NULL; g_autoptr(GBytes) de_bytes = NULL; g_debug ("Reading: %s", de_fname); de_bytes = asc_unit_read_data (ctask->unit, de_fname, &local_error); if (de_bytes == NULL) { asc_result_add_hint (ctask->result, cpt, "file-read-error", "fname", de_fname, "msg", local_error->message, NULL); g_error_free ( g_steal_pointer (&local_error)); g_hash_table_remove (de_fname_map, de_basename); continue; } de_cpt = asc_parse_desktop_entry_data ( ctask->result, cpt, de_bytes, de_basename, TRUE, /* ignore NoDisplay & Co. */ AS_FORMAT_VERSION_LATEST, priv->de_l10n_fn, priv->de_l10n_fn_udata); if (de_cpt != NULL) { /* update component hash based on new source data */ asc_result_update_component_gcid ( ctask->result, cpt, de_bytes); } } g_hash_table_remove (de_fname_map, de_basename); } /* end launch entry loop */ } } /* end of desktop-entry support */ } /* end of metadata parsing loop */ /* process the remaining .desktop files */ if (as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_PROCESS_UNPAIRED_DESKTOP)) { GHashTableIter ht_iter; gpointer ht_key; gpointer ht_value; g_hash_table_iter_init (&ht_iter, de_fname_map); while (g_hash_table_iter_next (&ht_iter, &ht_key, &ht_value)) { const gchar *de_fname = (const gchar *) ht_value; const gchar *de_basename = (const gchar *) ht_key; g_autoptr(AsComponent) de_cpt = NULL; g_autoptr(GBytes) de_bytes = NULL; g_autoptr(GError) local_error = NULL; g_debug ("Reading orphan desktop-entry: %s", de_fname); de_bytes = asc_unit_read_data (ctask->unit, de_fname, &local_error); if (de_bytes == NULL) { asc_result_add_hint_by_cid (ctask->result, de_basename, "file-read-error", "fname", de_fname, "msg", local_error->message, NULL); g_error_free (g_steal_pointer (&local_error)); continue; } /* synthesize component from desktop entry. The component will be auto-added * to the results set if it is valid. */ de_cpt = asc_parse_desktop_entry_data ( ctask->result, NULL, /* existing component */ de_bytes, de_basename, FALSE, /* don't ignore NoDisplay & Co. */ AS_FORMAT_VERSION_LATEST, priv->de_l10n_fn, priv->de_l10n_fn_udata); if (de_cpt != NULL && !asc_result_is_ignored (ctask->result, de_cpt)) asc_result_add_hint_simple (ctask->result, de_cpt, "no-metainfo"); } } /* allow external function to alter the detected components early on before we do expensive processing */ if (priv->check_md_early_fn != NULL) priv->check_md_early_fn (ctask->result, ctask->unit, priv->check_md_early_fn_udata); /* process translation status */ if (as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_PROCESS_TRANSLATIONS)) { if (priv->locale_unit == NULL) { asc_read_translation_status (ctask->result, ctask->unit, priv->prefix, priv->min_l10n_percentage); } else { asc_read_translation_status (ctask->result, priv->locale_unit, priv->prefix, priv->min_l10n_percentage); } } /* process icons and screenshots */ found_cpts = asc_result_fetch_components (ctask->result); for (guint i = 0; i < found_cpts->len; i++) { AsComponent *cpt = AS_COMPONENT (g_ptr_array_index (found_cpts, i)); /* icons */ if (!as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_IGNORE_ICONS)) { g_autofree gchar *icons_export_dir = NULL; if (priv->icons_result_dir == NULL) icons_export_dir = g_build_filename ( asc_globals_get_tmp_dir (), asc_result_gcid_for_component (ctask->result, cpt), "icons", NULL); else icons_export_dir = g_strdup (priv->icons_result_dir); asc_compose_process_icons (compose, ctask->result, cpt, ctask->unit, icons_export_dir); /* skip the next steps if the component has been ignored */ if (asc_result_is_ignored (ctask->result, cpt)) continue; } /* screenshots, but only if we allow network access */ if (as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_ALLOW_NET) && acurl != NULL) asc_process_screenshots ( ctask->result, cpt, acurl, priv->media_result_dir, as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_NO_PARTIAL_URLS) ? priv->media_baseurl : NULL, priv->max_scr_size_bytes, as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_ALLOW_SCREENCASTS), as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS)); if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_FONT) has_fonts = TRUE; } /* handle all font components present in this unit */ if (has_fonts && as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_PROCESS_FONTS)) { asc_process_fonts (ctask->result, ctask->unit, priv->prefix, priv->media_result_dir, priv->icons_result_dir, priv->icon_policy, priv->flags); } /* clean up superfluous hints in case we were filtering the results, as some rejected * components may have generated errors while we were inspecting them */ if (filter_cpts) { const gchar **cids = asc_result_get_component_ids_with_hints (ctask->result); for (guint i = 0; cids[i] != NULL; i++) { if (!g_hash_table_contains (priv->allowed_cids, cids[i])) { /* we want to catch out-of-scope XML errors as well, which have a CID * guessed from the filename, and don't include any .desktop suffix. */ if (!g_str_has_suffix (cids[i], ".desktop")) { g_autofree gchar *cid_desktop = g_strconcat (cids[i], ".desktop", NULL); if (g_hash_table_contains (priv->allowed_cids, cid_desktop)) continue; } asc_result_remove_hints_for_cid (ctask->result, cids[i]); } } } /* postprocess components and add remaining values and hints */ if (!as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_NO_FINAL_CHECK)) asc_compose_finalize_components (compose, ctask->result); asc_unit_close (ctask->unit); } static gboolean asc_compose_export_hints_data_yaml (AscCompose *compose, GError **error) { AscComposePrivate *priv = GET_PRIVATE (compose); yaml_emitter_t emitter; yaml_event_t event; gboolean res = FALSE; g_auto(GStrv) all_hint_tags = NULL; g_autofree gchar *yaml_fname = NULL; g_autoptr(GString) yaml_result = g_string_new (""); /* don't export anything if export dir isn't set */ if (priv->hints_result_dir == NULL) return TRUE; yaml_emitter_initialize (&emitter); yaml_emitter_set_indent (&emitter, 2); yaml_emitter_set_unicode (&emitter, TRUE); yaml_emitter_set_width (&emitter, 100); yaml_emitter_set_output (&emitter, as_compose_yaml_write_handler_cb, yaml_result); /* emit start event */ yaml_stream_start_event_initialize (&event, YAML_UTF8_ENCODING); if (!yaml_emitter_emit (&emitter, &event)) { g_set_error_literal (error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "Failed to initialize YAML emitter."); yaml_emitter_delete (&emitter); return FALSE; } /* new document for the tag list */ yaml_document_start_event_initialize (&event, NULL, NULL, NULL, FALSE); res = yaml_emitter_emit (&emitter, &event); g_assert (res); all_hint_tags = asc_globals_get_hint_tags (); as_yaml_sequence_start (&emitter); for (guint i = 0; all_hint_tags[i] != NULL; i++) { const gchar *tag = all_hint_tags[i]; /* main dict start */ as_yaml_mapping_start (&emitter); as_yaml_emit_entry (&emitter, "Tag", tag); as_yaml_emit_entry ( &emitter, "Severity", as_issue_severity_to_string (asc_globals_hint_tag_severity (tag))); as_yaml_emit_entry (&emitter, "Explanation", asc_globals_hint_tag_explanation (tag)); /* main dict end */ as_yaml_mapping_end (&emitter); } as_yaml_sequence_end (&emitter); /* finalize the tag list document */ yaml_document_end_event_initialize (&event, 1); res = yaml_emitter_emit (&emitter, &event); g_assert (res); /* new document for the actual issue hints */ yaml_document_start_event_initialize (&event, NULL, NULL, NULL, FALSE); res = yaml_emitter_emit (&emitter, &event); g_assert (res); as_yaml_sequence_start (&emitter); for (guint i = 0; i < priv->results->len; i++) { g_autofree const gchar **hints_cids = NULL; AscResult *result = ASC_RESULT (g_ptr_array_index (priv->results, i)); hints_cids = asc_result_get_component_ids_with_hints (result); if (hints_cids == NULL) continue; as_yaml_mapping_start (&emitter); as_yaml_emit_entry (&emitter, "Unit", asc_result_get_bundle_id (result)); as_yaml_emit_scalar (&emitter, "Hints"); as_yaml_sequence_start (&emitter); for (guint j = 0; hints_cids[j] != NULL; j++) { GPtrArray *hints = asc_result_get_hints (result, hints_cids[j]); as_yaml_mapping_start (&emitter); as_yaml_emit_scalar (&emitter, hints_cids[j]); as_yaml_sequence_start (&emitter); for (guint k = 0; k < hints->len; k++) { GPtrArray *vars; AscHint *hint = ASC_HINT (g_ptr_array_index (hints, k)); as_yaml_mapping_start (&emitter); as_yaml_emit_entry (&emitter, "tag", asc_hint_get_tag (hint)); vars = asc_hint_get_explanation_vars_list (hint); as_yaml_emit_scalar (&emitter, "variables"); as_yaml_mapping_start (&emitter); for (guint l = 0; l < vars->len; l += 2) { as_yaml_emit_entry (&emitter, g_ptr_array_index (vars, l), g_ptr_array_index (vars, l + 1)); } as_yaml_mapping_end (&emitter); /* end hint mapping */ as_yaml_mapping_end (&emitter); } as_yaml_sequence_end (&emitter); as_yaml_mapping_end (&emitter); } as_yaml_sequence_end (&emitter); as_yaml_mapping_end (&emitter); } as_yaml_sequence_end (&emitter); /* finalize the hints document */ yaml_document_end_event_initialize (&event, 1); res = yaml_emitter_emit (&emitter, &event); g_assert (res); /* end stream */ yaml_stream_end_event_initialize (&event); res = yaml_emitter_emit (&emitter, &event); g_assert (res); yaml_emitter_flush (&emitter); yaml_emitter_delete (&emitter); g_mkdir_with_parents (priv->hints_result_dir, 0755); yaml_fname = g_strdup_printf ("%s/%s.hints.yaml", priv->hints_result_dir, priv->origin); return g_file_set_contents (yaml_fname, yaml_result->str, yaml_result->len, error); } static gboolean asc_compose_export_hints_data_html (AscCompose *compose, GError **error) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GString) html = NULL; g_autofree gchar *html_fname = NULL; /* create header */ html = g_string_new (""); g_string_append (html, "\n" "\n"); g_string_append (html, "\n"); g_string_append (html, "\n"); g_string_append (html, "\n"); g_string_append_printf (html, "Compose issue hints for \"%s\"\n", priv->origin); g_string_append (html, "\n\n\n"); g_string_append (html, "\n"); g_string_append (html, "\n"); for (guint i = 0; i < priv->results->len; i++) { g_autofree const gchar **hints_cids = NULL; g_autofree gchar *bundle_hstr = NULL; AscResult *result = ASC_RESULT (g_ptr_array_index (priv->results, i)); hints_cids = asc_result_get_component_ids_with_hints (result); if (hints_cids == NULL) continue; g_string_append_printf ( html, "

Compose issue hints for \"%s\"

\n", priv->origin); g_string_append (html, "
"); bundle_hstr = g_markup_escape_text (asc_result_get_bundle_id (result), -1); g_string_append_printf (html, "

Unit: %s

\n
\n", bundle_hstr); for (guint j = 0; hints_cids[j] != NULL; j++) { g_autofree gchar *cid_hstr = NULL; GPtrArray *hints = asc_result_get_hints (result, hints_cids[j]); cid_hstr = g_markup_escape_text (hints_cids[j], -1); g_string_append_printf (html, "

%s #

\n", cid_hstr, cid_hstr, cid_hstr); g_string_append (html, "
    \n"); for (guint k = 0; k < hints->len; k++) { g_autofree gchar *explanation = NULL; const gchar *label_style; AsIssueSeverity severity; AscHint *hint = ASC_HINT (g_ptr_array_index (hints, k)); severity = asc_hint_get_severity (hint); switch (severity) { case AS_ISSUE_SEVERITY_ERROR: label_style = "label-error"; break; case AS_ISSUE_SEVERITY_WARNING: label_style = "label-warning"; break; case AS_ISSUE_SEVERITY_INFO: label_style = "label-info"; break; case AS_ISSUE_SEVERITY_PEDANTIC: label_style = "label-neutral"; break; default: label_style = "label-neutral"; } explanation = asc_hint_format_explanation (hint); g_string_append_printf ( html, "
  • \n %s %s\n", asc_hint_get_tag (hint), label_style, as_issue_severity_to_string (severity)); g_string_append_printf (html, "

    %s

    \n
  • \n", explanation); } g_string_append (html, "
\n"); } } g_string_append (html, "
\n"); g_string_append (html, "\n"); g_string_append (html, "\n"); g_mkdir_with_parents (priv->hints_result_dir, 0755); html_fname = g_strdup_printf ("%s/%s.hints.html", priv->hints_result_dir, priv->origin); return g_file_set_contents (html_fname, html->str, html->len, error); } static gboolean asc_compose_save_metadata_result (AscCompose *compose, gboolean *results_not_empty, GError **error) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(AsMetadata) mdata = NULL; g_autofree gchar *data_basename = NULL; g_autofree gchar *data_fname = NULL; mdata = as_metadata_new (); as_metadata_set_format_style (mdata, AS_FORMAT_STYLE_CATALOG); as_metadata_set_format_version (mdata, AS_FORMAT_VERSION_LATEST); /* Set baseurl only if one is set and we actually store any screenshot media. If no screenshot media * is stored, upstream's URLs are used and having a media base URL makes no sense. * In addition, we *must not* set a media-baseurl if *any* represented component uses full URLs. */ if (priv->media_baseurl != NULL && as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS) && !as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_NO_PARTIAL_URLS)) as_metadata_set_media_baseurl (mdata, priv->media_baseurl); if (priv->format == AS_FORMAT_KIND_YAML) data_basename = g_strdup_printf ("%s.yml.gz", priv->origin); else data_basename = g_strdup_printf ("%s.xml.gz", priv->origin); if (g_mkdir_with_parents (priv->data_result_dir, 0755)) { g_set_error (error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "failed to create %s: %s", priv->data_result_dir, strerror (errno)); return FALSE; } if (results_not_empty != NULL) *results_not_empty = FALSE; for (guint i = 0; i < priv->results->len; i++) { g_autoptr(GPtrArray) cpts = NULL; AscResult *result = ASC_RESULT (g_ptr_array_index (priv->results, i)); cpts = asc_result_fetch_components (result); for (guint j = 0; j < cpts->len; j++) as_metadata_add_component (mdata, AS_COMPONENT (g_ptr_array_index (cpts, j))); if (cpts->len > 0 && results_not_empty != NULL) *results_not_empty = TRUE; } data_fname = g_build_filename (priv->data_result_dir, data_basename, NULL); return as_metadata_save_catalog (mdata, data_fname, priv->format, error); } /** * asc_compose_finalize_results: * @compose: an #AscCompose instance. * * Perform final validation of generated data. * Calling this function is not necessary, unless the final check was explicitly * disabled using the %ASC_COMPOSE_FLAG_NO_FINAL_CHECK flag. */ void asc_compose_finalize_results (AscCompose *compose) { AscComposePrivate *priv = GET_PRIVATE (compose); for (guint i = 0; i < priv->results->len; i++) { AscResult *cres = ASC_RESULT (g_ptr_array_index (priv->results, i)); asc_compose_finalize_components (compose, cres); } } /** * asc_compose_finalize_result: * @compose: an #AscCompose instance. * @result: the #AscResult to finalize * * Perform final validation of generated data for the specified * result container. */ void asc_compose_finalize_result (AscCompose *compose, AscResult *result) { asc_compose_finalize_components (compose, result); } /** * asc_compose_run: * @compose: an #AscCompose instance. * @cancellable: a #GCancellable. * @error: A #GError or %NULL. * * Process the registered units and generate catalog metadata from * found components. * * Returns: (transfer none) (element-type AscResult): The results, or %NULL on error */ GPtrArray * asc_compose_run (AscCompose *compose, GCancellable *cancellable, GError **error) { AscComposePrivate *priv = GET_PRIVATE (compose); g_autoptr(GPtrArray) tasks = NULL; gboolean temp_dir_created = FALSE; gboolean results_generated = FALSE; /* ensure icon output dir is set, hint and data output dirs are optional */ if (priv->icons_result_dir == NULL && !as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_IGNORE_ICONS)) { g_set_error_literal (error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, _("Icon output directory is not set.")); return NULL; } if (priv->media_baseurl == NULL && priv->media_result_dir != NULL) { g_set_error_literal (error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, _("Media result directory is set, but media base URL is not. A media base URL is needed " "to export media that is served via the media URL.")); return NULL; } if (as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_NO_PARTIAL_URLS) && priv->media_baseurl == NULL) { g_set_error_literal (error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, _("Partial URLs are not allowed, but no base URL is set to create full URLs.")); return NULL; } if (priv->media_result_dir == NULL) { AscIconPolicyIter ip_iter; guint icon_size; guint scale_factor; AscIconState icon_state; asc_icon_policy_iter_init (&ip_iter, priv->icon_policy); while ( asc_icon_policy_iter_next (&ip_iter, &icon_size, &scale_factor, &icon_state)) { if (icon_state == ASC_ICON_STATE_IGNORED) continue; if (icon_state == ASC_ICON_STATE_REMOTE_ONLY || icon_state == ASC_ICON_STATE_CACHED_REMOTE) { g_debug ( "No media export directory set, but icon %ix%i@%i is set for " "remote delivery. Disabling remote for this icon type.", icon_size, icon_size, scale_factor); asc_icon_policy_set_policy (priv->icon_policy, icon_size, scale_factor, ASC_ICON_STATE_CACHED_ONLY); } } if (as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS)) { g_debug ("No media export directory set, but screenshots are supposed to " "be stored. Disabling screenshot storage."); as_flags_remove (priv->flags, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS); } } if (g_file_test (asc_globals_get_tmp_dir (), G_FILE_TEST_EXISTS)) { g_debug ("Will use existing directory '%s' for temporary data (and will not delete " "it later).", asc_globals_get_tmp_dir ()); temp_dir_created = FALSE; } else { g_debug ("Will use temporary directory '%s' and delete it after this run.", asc_globals_get_tmp_dir ()); temp_dir_created = TRUE; } tasks = g_ptr_array_new_with_free_func ((GDestroyNotify) asc_compose_task_free); for (guint i = 0; i < priv->units->len; i++) { AscComposeTask *ctask; AscUnit *unit = g_ptr_array_index (priv->units, i); ctask = asc_compose_task_new (unit); g_ptr_array_add (tasks, ctask); } if (as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_USE_THREADS)) { GThreadPool *tpool = NULL; tpool = g_thread_pool_new ((GFunc) asc_compose_process_task_cb, compose, -1, /* max threads */ FALSE, /* exclusive */ error); if (tpool == NULL) return NULL; /* launch all processing tasks in parallel */ for (guint i = 0; i < tasks->len; i++) g_thread_pool_push (tpool, g_ptr_array_index (tasks, i), NULL); /* shutdown thread pool, wait for all tasks to complete */ g_thread_pool_free (tpool, FALSE, TRUE); } else { /* run everything in sequence */ for (guint i = 0; i < tasks->len; i++) asc_compose_process_task_cb ( (AscComposeTask *) g_ptr_array_index (tasks, i), compose); } /* collect results */ for (guint i = 0; i < tasks->len; i++) { AscComposeTask *ctask = g_ptr_array_index (tasks, i); g_ptr_array_add (priv->results, g_object_ref (ctask->result)); } /* write result */ if (priv->data_result_dir != NULL) { if (!asc_compose_save_metadata_result (compose, &results_generated, error)) return NULL; } /* check if we (unexpectedly) had no results */ if (g_hash_table_size (priv->allowed_cids) > 0 && !results_generated) { /* we had filters set but generated no results - this was most certainly not intended */ AscComposeTask *ctask = g_ptr_array_index (tasks, 0); asc_result_add_hint_simple (ctask->result, NULL, "filters-but-no-output"); } /* write hints */ if (priv->hints_result_dir != NULL) { if (!asc_compose_export_hints_data_yaml (compose, error)) return NULL; if (!asc_compose_export_hints_data_html (compose, error)) return NULL; } /* clean up */ if (temp_dir_created) { g_debug ("Removing temporary directory '%s'", asc_globals_get_tmp_dir ()); if (!as_utils_delete_dir_recursive (asc_globals_get_tmp_dir ())) g_debug ("Failed to remove temporary directory."); } return priv->results; } /** * asc_compose_new: * * Creates a new #AscCompose. **/ AscCompose * asc_compose_new (void) { AscCompose *compose; compose = g_object_new (ASC_TYPE_COMPOSE, NULL); return ASC_COMPOSE (compose); }