/* -*- 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-utils-metainfo * @short_description: Helper functions for working with MetaInfo data in appstream-compose. * @include: appstream-compose.h */ #include "config.h" #include "asc-utils-metainfo.h" #include "as-utils-private.h" #include "as-spdx.h" #include "as-desktop-entry.h" /* Maximum amount of releases present in output data */ #define MAX_RELEASE_INFO_COUNT 4 /** * asc_parse_metainfo_data: * @cres: an #AscResult instance. * @mdata: an #AsMetadata instance used for parsing. * @bytes: the data to parse. * @mi_basename: the basename of the MetaInfo file we are loading. * * Parses metaInfo XML into a new #AsComponent and records any issues in * an #AscResult. * * Returns: (transfer full): A new #AsComponent or %NULL if we refused to accept this data. **/ AsComponent * asc_parse_metainfo_data (AscResult *cres, AsMetadata *mdata, GBytes *bytes, const gchar *mi_basename) { g_autoptr(GError) error = NULL; AsComponent *cpt; g_return_val_if_fail (mi_basename != NULL, NULL); if (!as_metadata_parse_bytes (mdata, bytes, AS_FORMAT_KIND_XML, &error)) { g_autofree gchar *cid_guess = g_strdup (mi_basename); /* try to guess the component-ID */ if (g_str_has_suffix (cid_guess, ".metainfo.xml")) cid_guess[strlen (cid_guess) - 13] = '\0'; else if (g_str_has_suffix (cid_guess, ".appdata.xml")) cid_guess[strlen (cid_guess) - 12] = '\0'; else if (g_str_has_suffix (cid_guess, ".xml")) cid_guess[strlen (cid_guess) - 4] = '\0'; asc_result_add_hint_by_cid (cres, cid_guess, "metainfo-parsing-error", "fname", mi_basename, "error", error->message, NULL); return NULL; } cpt = as_metadata_get_component (mdata); if (cpt == NULL) return NULL; /* check if we have a component-id, a component without ID is invalid */ if (as_is_empty (as_component_get_id (cpt))) { asc_result_add_hint (cres, NULL, "metainfo-no-id", "fname", mi_basename, NULL); return NULL; } /* we at least read enough data to consider this component */ if (!asc_result_add_component (cres, cpt, bytes, NULL)) return NULL; /* check if we can actually legally use this metadata */ if (!as_license_is_metadata_license (as_component_get_metadata_license (cpt))) { asc_result_add_hint (cres, cpt, "metainfo-license-invalid", "license", as_component_get_metadata_license (cpt), NULL); return NULL; } /* quit immediately if we have an unknown component type */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_UNKNOWN) { asc_result_add_hint_simple (cres, cpt, "metainfo-unknown-type"); return NULL; } return g_object_ref (cpt); } /** * asc_parse_metainfo_data_simple: * @cres: an #AscResult instance. * @bytes: the data to parse. * @mi_basename: the basename of the MetaInfo file we are loading. * * Parses metaInfo XML into a new #AsComponent and records any issues in * an #AscResult. * * Returns: (transfer full): A new #AsComponent or %NULL if we refused to accept this data. **/ AsComponent * asc_parse_metainfo_data_simple (AscResult *cres, GBytes *bytes, const gchar *mi_basename) { g_autoptr(AsMetadata) mdata = as_metadata_new (); as_metadata_set_locale (mdata, "ALL"); as_metadata_set_format_style (mdata, AS_FORMAT_STYLE_METAINFO); return asc_parse_metainfo_data (cres, mdata, bytes, mi_basename); } /** * asc_process_metainfo_releases: * @cres: an #AscResult instance. * @unit: an #AscUnit where release information could be read from. * @cpt: the #AsComponent to read release data for. * @mi_filename: the metadata filename for the component, as found in the passed unit. * @allow_net: set to %TRUE if network access should be allowed. * @acurl: the #AsCurl to use for network access. * @used_reldata: (out) (optional): Receive the release data that was used, if set. * * Reads an external release description file, either from a network location or from * the given unit. Also performs further processing on the release information, like sorting * releases or pruning old ones. **/ void asc_process_metainfo_releases (AscResult *cres, AscUnit *unit, AsComponent *cpt, const gchar *mi_filename, gboolean allow_net, AsCurl *acurl, GBytes **used_reldata) { g_autoptr(GError) local_error = NULL; g_autoptr(GBytes) relmd_bytes = NULL; AsReleaseList *releases = as_component_get_releases_plain (cpt); /* download external release metadata or fetch local release data */ if (as_release_list_get_kind (releases) == AS_RELEASE_LIST_KIND_EXTERNAL) { g_autofree gchar *relmd_uri = NULL; as_release_list_clear (releases); if (allow_net && as_release_list_get_url (releases) != NULL) { /* get the release data from a network location */ const gchar *releases_url = as_release_list_get_url (releases); relmd_bytes = as_curl_download_bytes (acurl, releases_url, &local_error); if (relmd_bytes == NULL) { asc_result_add_hint (cres, NULL, "metainfo-releases-download-failed", "url", releases_url, "msg", local_error->message, NULL); goto out; } relmd_uri = g_strdup (releases_url); } else { /* we need to find local release information */ g_autofree gchar *relfile_path = NULL; g_autofree gchar *relfile_name = NULL; g_autofree gchar *tmp = NULL; relfile_name = g_strconcat (as_component_get_id (cpt), ".releases.xml", NULL); tmp = g_path_get_dirname (mi_filename); relfile_path = g_build_filename (tmp, "releases", relfile_name, NULL); relmd_bytes = asc_unit_read_data (unit, relfile_path, &local_error); if (relmd_bytes == NULL) { asc_result_add_hint (cres, NULL, "file-read-error", "fname", relfile_path, "msg", local_error->message, NULL); goto out; } relmd_uri = g_steal_pointer (&relfile_path); } if (!as_release_list_load_from_bytes (releases, NULL, relmd_bytes, &local_error)) { asc_result_add_hint (cres, NULL, "metainfo-releases-read-failed", "path", relmd_uri, "msg", local_error->message, NULL); goto out; } } /* limit the amount of releases that we add to the output metadata. * since releases are sorted with the newest one at the top, we will only * remove the older ones. */ if (as_component_get_kind (cpt) != AS_COMPONENT_KIND_OPERATING_SYSTEM) { if (as_release_list_len (releases) > MAX_RELEASE_INFO_COUNT) as_release_list_set_size (releases, MAX_RELEASE_INFO_COUNT); } out: if (used_reldata != NULL) *used_reldata = g_steal_pointer (&relmd_bytes); } /** * asc_validate_metainfo_data_for_component: * @cres: an #AscResult instance. * @validator: an #AsValidator to validate with. * @cpt: the loaded #AsComponent which we are validating * @bytes: the data @cpt was constructed from. * @mi_basename: the basename of the MetaInfo file we are analyzing. * @relmd_bytes: (nullable): the release metadata for this component. * * Validates MetaInfo data for the given component and stores the validation result as issue hints * in the given #AscResult. * Both the result as well as the validator's state may be modified by this function. **/ void asc_validate_metainfo_data_for_component (AscResult *cres, AsValidator *validator, AsComponent *cpt, GBytes *bytes, const gchar *mi_basename, GBytes *relmd_bytes) { GHashTable *issues_files; GHashTableIter hiter; gpointer hkey, hvalue; /* don't check web URLs for validity, we catch those issues differently */ as_validator_set_allow_net (validator, FALSE); /* remove release data from a potential previous use of this validator */ as_validator_clear_release_data (validator); /* add release data if we have any */ if (relmd_bytes != NULL) { g_autoptr(GError) tmp_error = NULL; g_autofree gchar *release_name = g_strconcat (as_component_get_id (cpt), ".releases.xml", NULL); if (!as_validator_add_release_bytes (validator, release_name, relmd_bytes, &tmp_error)) g_warning ("Failed to add release metadata for %s: %s", as_component_get_id (cpt), tmp_error->message); } /* validate */ as_validator_validate_bytes (validator, bytes); /* convert & register found issues */ issues_files = as_validator_get_issues_per_file (validator); g_hash_table_iter_init (&hiter, issues_files); while (g_hash_table_iter_next (&hiter, &hkey, &hvalue)) { const gchar *filename = (const gchar *) hkey; const GPtrArray *issues = (const GPtrArray *) hvalue; if (as_is_empty (filename)) filename = mi_basename; for (guint i = 0; i < issues->len; i++) { g_autofree gchar *asv_tag = NULL; g_autofree gchar *location = NULL; glong line; const gchar *issue_hint; AsValidatorIssue *issue = AS_VALIDATOR_ISSUE ( g_ptr_array_index (issues, i)); /* we have a special hint tag for legacy metadata, * with its proper "error" priority */ if (g_strcmp0 (as_validator_issue_get_tag (issue), "metainfo-ancient") == 0) { asc_result_add_hint_simple (cres, cpt, "ancient-metadata"); continue; } /* create a tag for asgen out of the AppStream validator tag by prefixing it */ asv_tag = g_strconcat ("asv-", as_validator_issue_get_tag (issue), NULL); line = as_validator_issue_get_line (issue); if (line >= 0) location = g_strdup_printf ("%s:%ld", filename, line); else location = g_strdup (filename); /* we don't need to do much here, with the tag generated here, * the hint registry will automatically assign the right explanation * text and severity to the issue. */ issue_hint = as_validator_issue_get_hint (issue); if (issue_hint == NULL) issue_hint = ""; asc_result_add_hint (cres, cpt, asv_tag, "location", location, "hint", issue_hint, NULL); } } } /** * asc_parse_desktop_entry_data: * @cres: an #AscResult instance. * @cpt: (nullable): an existing #AsComponent which we are adding data from this desktop file to, or NULL to create a new one * @bytes: the data @cpt was constructed from. * @de_basename: the basename of the desktop-entry file we are parsing. * @ignore_nodisplay: set %TRUE if fields which cause the desktop-entry file to be purposefully hidden should be ignored (useful when passing an existing #AsComponent). * @fversion: AppStream format version * @de_l10n_fn: (scope call): callback for custom desktop-entry field localization functions. * @user_data: user data for @de_l10n_fn * * Parse a XDG desktop-entry file into either a new component or for extending an existing component. * * Returns: (nullable) (transfer full): A new #AsComponent or reference to the existing #AsComponent passed as parameter in case * we parsed anything. %NULL if we refused to accept this data. */ AsComponent * asc_parse_desktop_entry_data (AscResult *cres, AsComponent *cpt, GBytes *bytes, const gchar *de_basename, gboolean ignore_nodisplay, AsFormatVersion fversion, AscTranslateDesktopTextFn de_l10n_fn, gpointer user_data) { g_autoptr(AsComponent) ncpt = NULL; g_autoptr(GPtrArray) issues = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *prev_cid = NULL; gboolean cpt_is_new = FALSE; const gchar *data; gsize data_len; gboolean ret; /* NOTE: The cid needs to be the exact desktop-entry name briefly, as as_desktop_entry_parse_data * uses it to synthesize a Launchable. We can reset it later. */ if (cpt != NULL) { ncpt = g_object_ref (cpt); prev_cid = g_strdup (as_component_get_id (cpt)); as_component_set_id (ncpt, de_basename); cpt_is_new = FALSE; } else { ncpt = as_component_new (); as_component_set_id (ncpt, de_basename); cpt_is_new = TRUE; } /* we shouldn't even try to use cpt from this point forward */ cpt = NULL; /* prepare */ issues = g_ptr_array_new_with_free_func (g_object_unref); data = g_bytes_get_data (bytes, &data_len); /* sanity check */ if (data_len == 0) { asc_result_add_hint_by_cid (cres, de_basename, "desktop-file-error", "msg", "Desktop file was empty or nonexistent. " "Please ensure that it isn't a symbolic link to " "contents of a different package.", NULL); return NULL; } /* actually parse the desktop-entry data now */ ret = as_desktop_entry_parse_data (ncpt, data, data_len, fversion, ignore_nodisplay, issues, de_l10n_fn, user_data, &error); if (error != NULL) { asc_result_add_hint_by_cid (cres, de_basename, "desktop-file-error", "msg", error->message, NULL); return NULL; } if (!ret) return NULL; if (!ignore_nodisplay && cpt == NULL) { /* If we shouldn't ignore NoDisplay & Co. and have no preexisting component, we * will also skip any kind of settings or console app. * Otherwise we would sythesize a component with usually extremely low-quality * metadata for it (and additionally flood the software catalog with tons of settings apps * for various DEs). This means any kind of settings app needs a metainfo file in order to be * added to the software catalogs. */ GPtrArray *categories = as_component_get_categories (ncpt); for (guint i = 0; i < categories->len; i++) { const gchar *tmp = g_ptr_array_index (categories, i); if (g_strcmp0 (tmp, "Settings") == 0) return NULL; if (g_strcmp0 (tmp, "DesktopSettings") == 0) return NULL; if (g_strcmp0 (tmp, "ConsoleOnly") == 0) return NULL; } } /* the desktop-entry parsing function may have overridden this, so we reset the original value if we had one */ if (prev_cid != NULL) as_component_set_id (ncpt, prev_cid); /* reset component priority to neutral (may be lowest) */ as_component_set_priority (ncpt, 0); /* add new component to the results set */ if (cpt_is_new && !asc_result_add_component (cres, ncpt, bytes, NULL)) return NULL; /* register all the hints we may have found */ for (guint i = 0; i < issues->len; i++) { AsValidatorIssue *issue = AS_VALIDATOR_ISSUE (g_ptr_array_index (issues, i)); g_autofree gchar *asv_tag = NULL; const gchar *issue_hint = NULL; const gchar *orig_tag = NULL; orig_tag = as_validator_issue_get_tag (issue); if ((g_strcmp0 (orig_tag, "desktop-entry-hidden-set") == 0) || (g_strcmp0 (orig_tag, "desktop-entry-empty-onlyshowin") == 0)) { /* these tags get special treatment with links & co */ asv_tag = g_strdup (orig_tag); } else { asv_tag = g_strconcat ("asv-", orig_tag, NULL); } issue_hint = as_validator_issue_get_hint (issue); if (issue_hint == NULL) issue_hint = ""; asc_result_add_hint (cres, ncpt, asv_tag, "location", de_basename, "hint", issue_hint, NULL); } return g_steal_pointer (&ncpt); }