/* -*- 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 . */ #include "as-desktop-entry.h" /** * SECTION:as-desktop-entry * @short_description: Parser for XDG Desktop Entry data. * @include: appstream.h * */ #include #include "as-utils.h" #include "as-utils-private.h" #include "as-metadata.h" #include "as-component.h" #include "as-component-private.h" #include "as-validator-issue.h" #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP /** * as_strequal_casefold: */ static gboolean as_strequal_casefold (const gchar *a, const gchar *b) { g_autofree gchar *str1 = NULL; g_autofree gchar *str2 = NULL; if (a != NULL) str1 = g_utf8_casefold (a, -1); if (b != NULL) str2 = g_utf8_casefold (b, -1); return g_strcmp0 (str1, str2) == 0; } /** * as_desktop_entry_add_issue: */ static void as_desktop_entry_add_issue (GPtrArray *issues, const gchar *tag, const gchar *format, ...) { va_list args; g_autofree gchar *buffer = NULL; AsValidatorIssue *issue = NULL; /* do nothing if we shouldn't record issues */ if (issues == NULL) return; if (format != NULL) { va_start (args, format); buffer = g_strdup_vprintf (format, args); va_end (args); } issue = as_validator_issue_new (); as_validator_issue_set_tag (issue, tag); as_validator_issue_set_hint (issue, buffer); g_ptr_array_add (issues, issue); } /** * as_get_locale_from_key: */ static gchar * as_get_locale_from_key (const gchar *key) { gchar *tmp1; gchar *tmp2; gchar *delim; g_autofree gchar *locale = NULL; tmp1 = g_strstr_len (key, -1, "["); if (tmp1 == NULL) return g_strdup ("C"); tmp2 = g_strstr_len (tmp1, -1, "]"); /* this is a bug in the file */ if (tmp2 == NULL) return g_strdup ("C"); locale = g_strdup (tmp1 + 1); locale[tmp2 - tmp1 - 1] = '\0'; /* drop UTF-8 suffixes */ if (g_str_has_suffix (locale, ".utf-8") || g_str_has_suffix (locale, ".UTF-8")) locale[strlen (locale) - 6] = '\0'; /* filter out cruft */ if (as_is_cruft_locale (locale)) return NULL; delim = g_strrstr (locale, "."); if (delim != NULL) { gchar *tmp; g_autofree gchar *enc = NULL; /* looks like we need to drop another encoding suffix * (but we need to make sure it actually is one) */ tmp = delim + 1; if (tmp != NULL) enc = g_utf8_strdown (tmp, -1); if ((enc != NULL) && (g_str_has_prefix (enc, "iso"))) { delim[0] = '\0'; } } return g_steal_pointer (&locale); } /** * as_add_filtered_categories: * * Filter out some useless categories which we don't want to have in the * AppStream metadata. * Add the remaining ones to the new #AsComponent. */ static void as_add_filtered_categories (gchar **cats, AsComponent *cpt, GPtrArray *issues) { guint i; for (i = 0; cats[i] != NULL; i++) { const gchar *cat = cats[i]; /* check for invalid */ if (g_strcmp0 (cat, "") == 0) continue; /* ignore low-quality and custom categories */ if (as_utils_category_name_is_bad (cat)) continue; /* add the category if it is valid */ if (as_utils_is_category_name (cat)) as_component_add_category (cpt, cat); else as_desktop_entry_add_issue (issues, "desktop-entry-category-invalid", cat); } } /** * as_get_desktop_entry_value: */ static gchar * as_get_desktop_entry_value (GKeyFile *df, GPtrArray *issues, const gchar *key) { g_autofree gchar *str = NULL; const gchar *str_iter; GString *sane_str; gboolean has_invalid_chars = FALSE; g_autoptr(GError) error = NULL; str = g_key_file_get_string (df, DESKTOP_GROUP, key, &error); if (error != NULL) as_desktop_entry_add_issue (issues, "desktop-entry-bad-data", error->message); if (str == NULL) return NULL; /* some dumb .desktop files contain non-printable characters. If we are in XML mode, * this will hard-break some XML readers at a later point, so we need to clean this up * and replace these characters with a nice questionmark, so someone will notice and hopefully * fix the issue at the source */ sane_str = g_string_new (""); str_iter = str; while (*str_iter != '\0') { gunichar c = g_utf8_get_char (str_iter); if (as_unichar_accepted (c)) g_string_append_unichar (sane_str, c); else { g_string_append (sane_str, "�"); has_invalid_chars = TRUE; } str_iter = g_utf8_next_char (str_iter); } if (has_invalid_chars) as_desktop_entry_add_issue (issues, "desktop-entry-value-invalid-chars", key); return g_string_free (sane_str, FALSE); } /** * as_check_desktop_string: */ static void as_check_desktop_string (GPtrArray *issues, const gchar *field, const gchar *str) { if (issues == NULL) return; if ((g_str_has_prefix (str, "\"") && g_str_has_suffix (str, "\"")) || (g_str_has_prefix (str, "'") && g_str_has_suffix (str, "'"))) as_desktop_entry_add_issue (issues, "desktop-entry-value-quoted", "%s: %s", field, str); } /** * as_get_external_desktop_translations: */ static GPtrArray * as_get_external_desktop_translations (GKeyFile *kf, const gchar *text, const gchar *locale, AsTranslateDesktopTextFn de_l10n_fn, gpointer user_data) { GPtrArray *l10n; if (de_l10n_fn == NULL) return NULL; if (g_strcmp0 (locale, "C") != 0) return NULL; l10n = de_l10n_fn (kf, text, user_data); if (G_UNLIKELY (l10n->len % 2 != 0)) { /* NOTE: We could use g_return_val_if_fail here, but we could just as well write a more descriptive message */ g_critical ( "Invalid amount of list entries in external desktop translation l10n listing. " "Make sure you return locale names in even, and translations in odd indices. " "This is a programmer error."); return NULL; } return l10n; } /** * as_desktop_entry_parse_data: */ gboolean as_desktop_entry_parse_data (AsComponent *cpt, const gchar *data, gssize data_len, AsFormatVersion fversion, gboolean ignore_nodisplay, GPtrArray *issues, AsTranslateDesktopTextFn de_l10n_fn, gpointer user_data, GError **error) { g_autoptr(GKeyFile) df = NULL; g_auto(GStrv) keys = NULL; gboolean ignore_cpt = FALSE; GError *tmp_error = NULL; gchar *tmp; gboolean had_name, had_summary, had_categories, had_mimetypes; g_autofree gchar *desktop_basename = g_strdup (as_component_get_id (cpt)); if (desktop_basename == NULL) { g_set_error_literal ( error, AS_METADATA_ERROR, AS_METADATA_ERROR_PARSE, "Unable to determine component-id for component from desktop-entry data."); return FALSE; } df = g_key_file_new (); g_key_file_load_from_data (df, data, data_len, G_KEY_FILE_KEEP_TRANSLATIONS, &tmp_error); if (tmp_error != NULL) { g_propagate_error (error, tmp_error); return FALSE; } /* check this is a valid desktop file */ if (!g_key_file_has_group (df, DESKTOP_GROUP)) { g_set_error (error, AS_METADATA_ERROR, AS_METADATA_ERROR_PARSE, "Data in '%s' does not contain a valid Desktop Entry.", as_component_get_id (cpt)); return FALSE; } /* Type */ tmp = g_key_file_get_string (df, DESKTOP_GROUP, "Type", NULL); if (!as_strequal_casefold (tmp, "application")) { g_free (tmp); /* not an application, so we can't proceed, but also no error */ return FALSE; } g_free (tmp); /* NoDisplay */ tmp = g_key_file_get_string (df, DESKTOP_GROUP, "NoDisplay", NULL); if (as_strequal_casefold (tmp, "true")) { /* we may read the application data, but it will be ignored in its current form */ ignore_cpt = TRUE; if (!ignore_nodisplay) { g_free (tmp); return FALSE; } } g_free (tmp); /* X-AppStream-Ignore */ tmp = g_key_file_get_string (df, DESKTOP_GROUP, "X-AppStream-Ignore", NULL); if (as_strequal_casefold (tmp, "true")) { g_free (tmp); /* this file should be ignored, we can't return a component (but this is also no error) */ return FALSE; } g_free (tmp); /* Hidden */ tmp = g_key_file_get_string (df, DESKTOP_GROUP, "Hidden", NULL); if (as_strequal_casefold (tmp, "true")) { ignore_cpt = TRUE; as_desktop_entry_add_issue (issues, "desktop-entry-hidden-set", NULL); if (!ignore_nodisplay) { g_free (tmp); return FALSE; } } g_free (tmp); /* OnlyShowIn */ tmp = g_key_file_get_string (df, DESKTOP_GROUP, "OnlyShowIn", NULL); if (tmp != NULL) { if (as_is_empty (tmp)) as_desktop_entry_add_issue (issues, "desktop-entry-empty-onlyshowin", NULL); /* We want to ignore all desktop-entry files which were made desktop-exclusive * via OnlyShowIn (those are usually configuration apps and control center modules) * Only exception is if a metainfo file was present. */ g_free (tmp); ignore_cpt = TRUE; if (!ignore_nodisplay) return FALSE; } /* create the new component we synthesize for this desktop entry */ as_component_set_kind (cpt, AS_COMPONENT_KIND_DESKTOP_APP); as_component_set_ignored (cpt, ignore_cpt); as_component_set_origin_kind (cpt, AS_ORIGIN_KIND_DESKTOP_ENTRY); /* strip .desktop suffix if the reverse-domain-name scheme is followed and we build for * a recent AppStream version */ { g_auto(GStrv) parts = g_strsplit (desktop_basename, ".", 3); if (g_strv_length (parts) == 3) { if (as_utils_is_tld (parts[0]) && g_str_has_suffix (desktop_basename, ".desktop")) { g_autofree gchar *id_raw = NULL; /* remove .desktop suffix */ id_raw = g_strdup (desktop_basename); id_raw[strlen (id_raw) - 8] = '\0'; as_component_set_id (cpt, id_raw); } } else { /* for components synthesized from desktop-entry files, that we can't guess * a rDNS ID for, we need a lower-cased ID instead */ g_autofree gchar *cid_low = g_utf8_strdown (desktop_basename, -1); as_component_set_id (cpt, cid_low); } } had_name = !as_is_empty (as_component_get_name (cpt)); had_summary = !as_is_empty (as_component_get_name (cpt)); had_categories = as_component_get_categories (cpt)->len > 0; had_mimetypes = as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_MEDIATYPE) != NULL; keys = g_key_file_get_keys (df, DESKTOP_GROUP, NULL, NULL); for (guint i = 0; keys[i] != NULL; i++) { g_autofree gchar *locale_posix = NULL; g_autofree gchar *locale = NULL; g_autofree gchar *val = NULL; gchar *key = keys[i]; if (g_strcmp0 (key, "Type") == 0) continue; g_strstrip (key); locale_posix = as_get_locale_from_key (key); /* skip invalid stuff */ if (locale_posix == NULL) continue; locale = as_utils_posix_locale_to_bcp47 (locale_posix); val = as_get_desktop_entry_value (df, issues, key); if (val == NULL) continue; if (g_str_has_prefix (key, "Name")) { g_autoptr(GPtrArray) l10n_data = NULL; if (had_name) continue; as_component_set_name (cpt, val, locale); as_check_desktop_string (issues, key, val); l10n_data = as_get_external_desktop_translations (df, val, locale, de_l10n_fn, user_data); if (l10n_data != NULL) { for (guint j = 0; j < l10n_data->len; j += 2) as_component_set_name ( cpt, g_ptr_array_index (l10n_data, j), g_ptr_array_index (l10n_data, j + 1)); } } else if (g_str_has_prefix (key, "Comment")) { g_autoptr(GPtrArray) l10n_data = NULL; if (had_summary) continue; as_component_set_summary (cpt, val, locale); as_check_desktop_string (issues, key, val); l10n_data = as_get_external_desktop_translations (df, val, locale, de_l10n_fn, user_data); if (l10n_data != NULL) { for (guint j = 0; j < l10n_data->len; j += 2) as_component_set_name ( cpt, g_ptr_array_index (l10n_data, j), g_ptr_array_index (l10n_data, j + 1)); } } else if (g_strcmp0 (key, "Categories") == 0) { g_auto(GStrv) cats = NULL; if (had_categories) continue; cats = g_strsplit (val, ";", -1); as_add_filtered_categories (cats, cpt, issues); } else if (g_str_has_prefix (key, "Keywords")) { g_auto(GStrv) kws = NULL; g_autoptr(GPtrArray) kws_list = NULL; g_autoptr(GPtrArray) l10n_data = NULL; /* skip adding keywords if the metainfo file or previous component has * already had some for this locale */ if (g_hash_table_contains (as_component_get_keywords_table (cpt), locale)) continue; kws = g_strsplit (val, ";", -1); kws_list = as_strv_to_ptr_array (kws, TRUE, TRUE); as_component_set_keywords (cpt, kws_list, locale, FALSE); l10n_data = as_get_external_desktop_translations (df, val, locale, de_l10n_fn, user_data); if (l10n_data != NULL) { for (guint j = 0; j < l10n_data->len; j += 2) { g_auto(GStrv) tmp_strv = NULL; g_autoptr(GPtrArray) e_kws = NULL; const gchar *e_locale = g_ptr_array_index (l10n_data, j); gchar *e_value = g_ptr_array_index (l10n_data, j + 1); tmp_strv = g_strsplit (e_value, ";", -1); e_kws = as_strv_to_ptr_array (tmp_strv, TRUE, TRUE); as_component_set_keywords (cpt, e_kws, e_locale, FALSE); } } } else if (g_strcmp0 (key, "MimeType") == 0) { g_auto(GStrv) mts = NULL; g_autoptr(AsProvided) prov = NULL; if (had_mimetypes) continue; mts = g_strsplit (val, ";", -1); if (mts == NULL) continue; prov = as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_MEDIATYPE); if (prov == NULL) { prov = as_provided_new (); as_provided_set_kind (prov, AS_PROVIDED_KIND_MEDIATYPE); } else { g_object_ref (prov); } for (guint j = 0; mts[j] != NULL; j++) { if (g_strcmp0 (mts[j], "") == 0) continue; as_provided_add_item (prov, mts[j]); } as_component_add_provided (cpt, prov); } else if (g_strcmp0 (key, "Icon") == 0) { g_autoptr(AsIcon) icon = NULL; icon = as_icon_new (); if (g_str_has_prefix (val, "/")) { as_icon_set_kind (icon, AS_ICON_KIND_LOCAL); as_icon_set_filename (icon, val); } else { gchar *dot; as_icon_set_kind (icon, AS_ICON_KIND_STOCK); /* work around stock icons being suffixed */ dot = g_strstr_len (val, -1, "."); if (dot != NULL && (g_strcmp0 (dot, ".png") == 0 || g_strcmp0 (dot, ".xpm") == 0 || g_strcmp0 (dot, ".svg") == 0 || g_strcmp0 (dot, ".svgz") == 0)) { *dot = '\0'; } as_icon_set_name (icon, val); } as_component_add_icon (cpt, icon); } } /* add this .desktop file as launchable entry, if we don't have one set already */ if (as_component_get_launchable (cpt, AS_LAUNCHABLE_KIND_DESKTOP_ID) == NULL) { g_autoptr(AsLaunchable) launch = as_launchable_new (); as_launchable_set_kind (launch, AS_LAUNCHABLE_KIND_DESKTOP_ID); as_launchable_add_entry (launch, desktop_basename); as_component_add_launchable (cpt, launch); /* we have the lowest priority */ as_component_set_priority (cpt, -G_MAXINT); } return TRUE; } /** * as_desktop_entry_parse_file: * * Parse a .desktop file. */ gboolean as_desktop_entry_parse_file (AsComponent *cpt, GFile *file, AsFormatVersion fversion, gboolean ignore_nodisplay, GPtrArray *issues, AsTranslateDesktopTextFn de_l10n_fn, gpointer user_data, GError **error) { g_autofree gchar *file_basename = NULL; g_autoptr(GInputStream) file_stream = NULL; g_autoptr(GString) dedata = NULL; gssize len; const gsize buffer_size = 1024 * 32; g_autofree gchar *buffer = NULL; file_stream = G_INPUT_STREAM (g_file_read (file, NULL, error)); if (file_stream == NULL) return FALSE; file_basename = g_file_get_basename (file); dedata = g_string_new (""); buffer = g_malloc (buffer_size); while ((len = g_input_stream_read (file_stream, buffer, buffer_size, NULL, error)) > 0) { g_string_append_len (dedata, buffer, len); } /* check if there was an error */ if (len < 0) return FALSE; /* parse desktop entry */ as_component_set_id (cpt, file_basename); return as_desktop_entry_parse_data (cpt, dedata->str, dedata->len, fversion, ignore_nodisplay, issues, de_l10n_fn, user_data, error); }