/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s: * Copyright © 2019 Red Hat, Inc * * This program 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 . * * Authors: * Matthias Clasen */ #include "config.h" #include "flatpak-appdata-private.h" #include "flatpak-utils-private.h" #include #include typedef struct { char *id; GHashTable *names; GHashTable *comments; char *version; char *license; char *content_rating_type; GHashTable *content_rating; /* (element-type interned-utf8 interned-utf8) */ } Component; typedef struct { GPtrArray *components; GString *text; gboolean in_text; gboolean in_component; gboolean in_content_rating; gboolean in_developer; char *lang; guint64 timestamp; const char *id; /* interned */ } ParserData; static void component_free (gpointer data) { Component *component = data; g_hash_table_unref (component->names); g_hash_table_unref (component->comments); g_free (component->id); g_free (component->version); g_free (component->license); g_free (component->content_rating_type); g_clear_pointer (&component->content_rating, g_hash_table_unref); g_free (component); } static Component * component_new (void) { Component *component = g_new0 (Component, 1); component->names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); component->comments = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); return component; } static void parser_data_free (ParserData *data) { g_ptr_array_unref (data->components); g_string_free (data->text, TRUE); g_free (data->lang); g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (ParserData, parser_data_free) static ParserData * parser_data_new (void) { ParserData *data = g_new0 (ParserData, 1); data->components = g_ptr_array_new_with_free_func (component_free); data->text = g_string_new (""); return data; } static void start_element (GMarkupParseContext *context, const char *element_name, const char **attribute_names, const char **attribute_values, gpointer user_data, GError **error) { ParserData *data = user_data; g_assert (data->text->len == 0); g_assert (data->lang == NULL); if (g_str_equal (element_name, "component")) { g_ptr_array_add (data->components, component_new ()); } else if (g_str_equal (element_name, "id")) { data->in_text = TRUE; } else if ((!data->in_developer && g_str_equal (element_name, "name")) || g_str_equal (element_name, "summary")) { const char *lang = NULL; if (g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "xml:lang", &lang, G_MARKUP_COLLECT_INVALID)) { if (lang) data->lang = g_strdup (lang); else data->lang = g_strdup ("C"); data->in_text = TRUE; } } else if (g_str_equal (element_name, "project_license")) { data->in_text = TRUE; } else if (g_str_equal (element_name, "release")) { const char *timestamp; const char *date; const char *version; Component *component = NULL; g_assert (data->components->len > 0); component = g_ptr_array_index (data->components, data->components->len - 1); if (g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, G_MARKUP_COLLECT_STRING, "version", &version, G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "timestamp", ×tamp, G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "date", &date, G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "date_eol", NULL, G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "urgency", NULL, G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "type", NULL, G_MARKUP_COLLECT_INVALID)) { guint64 ts = 0; if (timestamp) ts = g_ascii_strtoull (timestamp, NULL, 10); else if (date) { g_autoptr(GTimeZone) tz = g_time_zone_new_utc (); g_autoptr(GDateTime) dt = g_date_time_new_from_iso8601 (date, tz); if (!dt) { int d, m, y; if (sscanf (date, "%u-%u-%u", &d, &m, &y) == 3) dt = g_date_time_new_utc (d, m, y, 0, 0, 0); } if (dt) ts = g_date_time_to_unix (dt); } else g_warning ("Ignoring release element without timestamp or date"); if (ts > data->timestamp) { data->timestamp = ts; g_free (component->version); component->version = g_strdup (version); } } } else if (g_str_equal (element_name, "content_rating")) { /* https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-content_rating */ Component *component = NULL; g_assert (data->components->len > 0); component = g_ptr_array_index (data->components, data->components->len - 1); if (component->content_rating == NULL) { const gchar *type = NULL; if (g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, G_MARKUP_COLLECT_STRING, "type", &type, G_MARKUP_COLLECT_INVALID)) { component->content_rating_type = g_strdup (type); component->content_rating = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); data->in_content_rating = TRUE; } else { g_warning ("Ignoring content rating missing type attribute"); } } else { g_warning ("Ignoring duplicate content rating"); } } else if (data->in_content_rating && g_str_equal (element_name, "content_attribute")) { /* https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-content_rating */ Component *component = NULL; const gchar *id = NULL; g_assert (data->components->len > 0); component = g_ptr_array_index (data->components, data->components->len - 1); g_assert (component->content_rating != NULL); if (g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, G_MARKUP_COLLECT_STRING, "id", &id, G_MARKUP_COLLECT_INVALID)) { /* FIXME: We use interned strings here (for keys and, below, for * values) as the set of OARS keys and values is small and bounded. * In future (once we can depend on GLib 2.58), we could switch to * using #GRefString to avoid expanding the interned string hash * table. */ data->id = g_intern_string (id); data->in_text = TRUE; } else { g_warning ("Ignoring content attribute missing id attribute"); } } else if (g_str_equal (element_name, "developer")) { data->in_developer = TRUE; } } static void end_element (GMarkupParseContext *context, const char *element_name, gpointer user_data, GError **error) { ParserData *data = user_data; g_autofree char *text = NULL; Component *component = NULL; const char *parent = NULL; const GSList *elements; elements = g_markup_parse_context_get_element_stack (context); if (elements->next) parent = (const char *) elements->next->data; g_assert (data->components->len > 0); component = g_ptr_array_index (data->components, data->components->len - 1); if (data->in_text) { text = g_strdup (data->text->str); g_string_truncate (data->text, 0); data->in_text = FALSE; } /* avoid picking up elements from e.g. */ if (g_str_equal (element_name, "id") && g_str_equal (parent, "component")) { component->id = g_steal_pointer (&text); } else if (!data->in_developer && g_str_equal (element_name, "name")) { g_hash_table_insert (component->names, g_steal_pointer (&data->lang), g_steal_pointer (&text)); } else if (g_str_equal (element_name, "summary")) { g_hash_table_insert (component->comments, g_steal_pointer (&data->lang), g_steal_pointer (&text)); } else if (g_str_equal (element_name, "project_license")) { component->license = g_steal_pointer (&text); } else if (g_str_equal (element_name, "content_rating")) { data->in_content_rating = FALSE; } else if (data->in_content_rating && g_str_equal (element_name, "content_attribute")) { /* https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-content_rating */ g_assert (component->content_rating != NULL); g_hash_table_insert (component->content_rating, (gpointer) data->id, (gpointer) g_intern_string (text)); } else if (g_str_equal (element_name, "developer")) { data->in_developer = FALSE; } } static void text (GMarkupParseContext *context, const char *text, gsize text_len, gpointer user_data, GError **error) { ParserData *data = user_data; if (data->in_text) g_string_append_len (data->text, text, text_len); } gboolean flatpak_parse_appdata (const char *appdata_xml, const char *app_id, GHashTable **names, GHashTable **comments, char **version, char **license, char **content_rating_type, GHashTable **content_rating) { g_autoptr(GMarkupParseContext) context = NULL; GMarkupParser parser = { start_element, end_element, text, NULL, NULL }; g_autoptr(ParserData) data = parser_data_new (); g_autoptr(GError) error = NULL; int i; g_autofree char *legacy_id = NULL; context = g_markup_parse_context_new (&parser, G_MARKUP_TREAT_CDATA_AS_TEXT, data, NULL); if (!g_markup_parse_context_parse (context, appdata_xml, -1, &error)) { g_warning ("Failed to parse appdata: %s", error->message); return FALSE; } legacy_id = g_strconcat (app_id, ".desktop", NULL); for (i = 0; i < data->components->len; i++) { Component *component = g_ptr_array_index (data->components, i); if (g_str_equal (component->id, app_id) || g_str_equal (component->id, legacy_id)) { *names = g_hash_table_ref (component->names); *comments = g_hash_table_ref (component->comments); *version = g_steal_pointer (&component->version); *license = g_steal_pointer (&component->license); if (content_rating_type != NULL) *content_rating_type = g_steal_pointer (&component->content_rating_type); if (content_rating != NULL) *content_rating = g_steal_pointer (&component->content_rating); return TRUE; } } g_warning ("No matching appdata for %s", app_id); return FALSE; }