/* 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;
}