[gnome-software/wip/hughsie/steam2] Add Steam support



commit 8832be242a3aa683901c319aead140ed994484df
Author: Richard Hughes <richard hughsie com>
Date:   Wed Mar 2 16:38:20 2016 +0000

    Add Steam support

 configure.ac                   |    1 +
 contrib/gnome-software.spec.in |    1 +
 src/plugins/Makefile.am        |   10 +
 src/plugins/gs-html-utils.c    |  246 +++++++++
 src/plugins/gs-html-utils.h    |   34 ++
 src/plugins/gs-plugin-steam.c  | 1079 ++++++++++++++++++++++++++++++++++++++++
 src/plugins/gs-self-test.c     |   39 ++
 7 files changed, 1410 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 7436e8e..2aa7c4b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -65,6 +65,7 @@ PKG_CHECK_MODULES(APPSTREAM, appstream-glib >= 0.5.11)
 PKG_CHECK_MODULES(GDK_PIXBUF, gdk-pixbuf-2.0 >= 2.31.5)
 PKG_CHECK_MODULES(JSON_GLIB, json-glib-1.0 >= 1.1.1)
 PKG_CHECK_MODULES(SQLITE, sqlite3)
+PKG_CHECK_MODULES(ICNS, libicns)
 PKG_CHECK_MODULES(SOUP, libsoup-2.4 >= 2.51.92)
 PKG_CHECK_MODULES(GSETTINGS_DESKTOP_SCHEMAS, gsettings-desktop-schemas >= 3.11.5)
 PKG_CHECK_MODULES(GNOME_DESKTOP, gnome-desktop-3.0 >= 3.17.92)
diff --git a/contrib/gnome-software.spec.in b/contrib/gnome-software.spec.in
index 8b7e866..ff58e2c 100644
--- a/contrib/gnome-software.spec.in
+++ b/contrib/gnome-software.spec.in
@@ -28,6 +28,7 @@ BuildRequires: libnotify-devel
 BuildRequires: PackageKit-glib-devel >= 0.8.10
 BuildRequires: libsoup-devel
 BuildRequires: gnome-desktop3-devel
+BuildRequires: libicns-devel
 BuildRequires: gsettings-desktop-schemas-devel
 BuildRequires: libappstream-glib-devel
 BuildRequires: fwupd-devel
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index a2964b8..f248317 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -42,6 +42,7 @@ plugin_LTLIBRARIES =                                  \
        libgs_plugin_fedora_tagger_usage.la             \
        libgs_plugin_shell-extensions.la                \
        libgs_plugin_epiphany.la                        \
+       libgs_plugin_steam.la                           \
        libgs_plugin_icons.la
 
 if HAVE_PACKAGEKIT
@@ -69,6 +70,14 @@ if HAVE_LIMBA
 plugin_LTLIBRARIES += libgs_plugin_limba.la
 endif
 
+libgs_plugin_steam_la_SOURCES =                                \
+       gs-html-utils.c                                 \
+       gs-html-utils.h                                 \
+       gs-plugin-steam.c
+libgs_plugin_steam_la_LIBADD = $(GS_PLUGIN_LIBS) $(ICNS_LIBS)
+libgs_plugin_steam_la_LDFLAGS = -module -avoid-version
+libgs_plugin_steam_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+
 libgs_plugin_dummy_la_SOURCES = gs-plugin-dummy.c
 libgs_plugin_dummy_la_LIBADD = $(GS_PLUGIN_LIBS)
 libgs_plugin_dummy_la_LDFLAGS = -module -avoid-version
@@ -236,6 +245,7 @@ check_PROGRAMS =                                    \
        gs-self-test
 
 gs_self_test_SOURCES =                                 \
+       gs-html-utils.c                                 \
        gs-moduleset.c                                  \
        gs-self-test.c
 
diff --git a/src/plugins/gs-html-utils.c b/src/plugins/gs-html-utils.c
new file mode 100644
index 0000000..b33d9e3
--- /dev/null
+++ b/src/plugins/gs-html-utils.c
@@ -0,0 +1,246 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2014 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib.h>
+#include <appstream-glib.h>
+
+#include "gs-html-utils.h"
+
+typedef enum {
+       GS_HTML_UTILS_ACTION_IGNORE,
+       GS_HTML_UTILS_ACTION_PARA,
+       GS_HTML_UTILS_ACTION_LI,
+       GS_HTML_UTILS_ACTION_LAST
+} GsHtmlUtilsAction;
+
+typedef struct {
+       GsHtmlUtilsAction        action;
+       GString                 *str;
+} GsHtmlUtilsHelper;
+
+/**
+ * gs_html_utils_start_cb:
+ **/
+static void
+gs_html_utils_start_cb (GMarkupParseContext *context,
+                         const gchar *element_name,
+                         const gchar **attribute_names,
+                         const gchar **attribute_values,
+                         gpointer user_data,
+                         GError **error)
+{
+       GsHtmlUtilsHelper *helper = (GsHtmlUtilsHelper *) user_data;
+       if (g_strcmp0 (element_name, "book") == 0)
+               return;
+       if (g_strcmp0 (element_name, "li") == 0) {
+               helper->action = GS_HTML_UTILS_ACTION_LI;
+               return;
+       }
+       if (g_strcmp0 (element_name, "p") == 0) {
+               helper->action = GS_HTML_UTILS_ACTION_PARA;
+               return;
+       }
+       if (g_strcmp0 (element_name, "ul") == 0 ||
+           g_strcmp0 (element_name, "ol") == 0) {
+               g_string_append (helper->str, "<ul>");
+               return;
+       }
+       g_warning ("unhandled START %s", element_name);
+}
+
+/**
+ * gs_html_utils_end_cb:
+ **/
+static void
+gs_html_utils_end_cb (GMarkupParseContext *context,
+                       const gchar *element_name,
+                       gpointer user_data,
+                       GError **error)
+{
+       GsHtmlUtilsHelper *helper = (GsHtmlUtilsHelper *) user_data;
+       if (g_strcmp0 (element_name, "book") == 0 ||
+           g_strcmp0 (element_name, "li") == 0) {
+               return;
+       }
+       if (g_strcmp0 (element_name, "p") == 0) {
+               helper->action = GS_HTML_UTILS_ACTION_IGNORE;
+               return;
+       }
+       if (g_strcmp0 (element_name, "ul") == 0 ||
+           g_strcmp0 (element_name, "ol") == 0) {
+               g_string_append (helper->str, "</ul>");
+               return;
+       }
+       g_warning ("unhandled END %s", element_name);
+}
+
+/**
+ * gs_html_utils_text_cb:
+ **/
+static void
+gs_html_utils_text_cb (GMarkupParseContext *context,
+                        const gchar *text,
+                        gsize text_len,
+                        gpointer user_data,
+                        GError **error)
+{
+       GsHtmlUtilsHelper *helper = (GsHtmlUtilsHelper *) user_data;
+       g_autofree gchar *tmp = NULL;
+       g_auto(GStrv) split = NULL;
+       guint i;
+       gchar *strip;
+
+       if (helper->action == GS_HTML_UTILS_ACTION_IGNORE)
+               return;
+
+       /* only add valid lines */
+       tmp = g_markup_escape_text (text, text_len);
+       split = g_strsplit (tmp, "\n", -1);
+       for (i = 0; split[i] != NULL; i++) {
+               strip = g_strstrip (split[i]);
+               if (strip[0] == '\0')
+                       continue;
+               if (strlen (strip) < 15)
+                       continue;
+               switch (helper->action) {
+               case GS_HTML_UTILS_ACTION_PARA:
+                       g_string_append_printf (helper->str, "<p>%s</p>", strip);
+                       break;
+               case GS_HTML_UTILS_ACTION_LI:
+                       g_string_append_printf (helper->str, "<li>%s</li>", strip);
+                       break;
+               default:
+                       break;
+               }
+       }
+}
+
+/**
+ * gs_html_utils_strreplace:
+ **/
+static void
+gs_html_utils_strreplace (GString *str, const gchar *search, const gchar *replace)
+{
+       g_auto(GStrv) split = NULL;
+       g_autofree gchar *new = NULL;
+
+       /* optimise */
+       if (g_strstr_len (str->str, -1, search) == NULL)
+               return;
+       split = g_strsplit (str->str, search, -1);
+       new = g_strjoinv (replace, split);
+       g_string_assign (str, new);
+}
+
+/**
+ * gs_html_utils_erase:
+ *
+ * Replaces any tag with whitespace.
+ **/
+static void
+gs_html_utils_erase (GString *str, const gchar *start, const gchar *end)
+{
+       guint i, j;
+       guint start_len = strlen (start);
+       guint end_len = strlen (end);
+       for (i = 0; str->str[i] != '\0'; i++) {
+               if (memcmp (&str->str[i], start, start_len) != 0)
+                       continue;
+               for (j = i; i < str->len; j++) {
+                       if (memcmp (&str->str[j], end, end_len) != 0)
+                               continue;
+                       /* delete this section and restart the search */
+                       g_string_erase (str, i, (j - i) + end_len);
+                       i = -1;
+                       break;
+               }
+       }
+}
+
+/**
+ * gs_html_utils_parse_description:
+ **/
+gchar *
+gs_html_utils_parse_description (const gchar *html, GError **error)
+{
+       GMarkupParseContext *ctx;
+       GsHtmlUtilsHelper helper;
+       GMarkupParser parser = {
+               gs_html_utils_start_cb,
+               gs_html_utils_end_cb,
+               gs_html_utils_text_cb,
+               NULL,
+               NULL };
+       g_autofree gchar *tmp = NULL;
+       g_autoptr(GString) str = NULL;
+
+       /* set up XML parser */
+       helper.action = GS_HTML_UTILS_ACTION_PARA;
+       helper.str = g_string_new ("");
+       ctx = g_markup_parse_context_new (&parser, G_MARKUP_TREAT_CDATA_AS_TEXT, &helper, NULL);
+
+       /* ensure this has at least one se of quotes */
+       str = g_string_new ("");
+       g_string_append_printf (str, "<book>%s</book>", html);
+
+       /* convert win32 line endings */
+       g_strdelimit (str->str, "\r", '\n');
+
+       /* tidy up non-compliant HTML5 */
+       gs_html_utils_erase (str, "<img", ">");
+       gs_html_utils_erase (str, "<br", ">");
+
+       /* kill anything that's not wanted */
+       gs_html_utils_erase (str, "<h1", "</h1>");
+       gs_html_utils_erase (str, "<h2", "</h2>");
+       gs_html_utils_erase (str, "<span", "</span>");
+       gs_html_utils_erase (str, "<a", ">");
+       gs_html_utils_erase (str, "</a", ">");
+
+       /* use UTF-8 */
+       gs_html_utils_strreplace (str, "<i>", "");
+       gs_html_utils_strreplace (str, "</i>", "");
+       gs_html_utils_strreplace (str, "<u>", "");
+       gs_html_utils_strreplace (str, "</u>", "");
+       gs_html_utils_strreplace (str, "<b>", "");
+       gs_html_utils_strreplace (str, "</b>", "");
+       gs_html_utils_strreplace (str, "<blockquote>", "");
+       gs_html_utils_strreplace (str, "</blockquote>", "");
+       gs_html_utils_strreplace (str, "<strong>", "");
+       gs_html_utils_strreplace (str, "</strong>", "");
+       gs_html_utils_strreplace (str, "&trade;", "™");
+       gs_html_utils_strreplace (str, "&reg;", "®");
+
+//g_print ("%s\n", str->str);
+
+       /* parse */
+       if (!g_markup_parse_context_parse (ctx, str->str, -1, error))
+               return NULL;
+
+       /* return only valid AppStream markup */
+       return as_markup_convert_full (helper.str->str,
+                                      AS_MARKUP_CONVERT_FORMAT_APPSTREAM,
+                                      AS_MARKUP_CONVERT_FLAG_IGNORE_ERRORS,
+                                      error);
+}
diff --git a/src/plugins/gs-html-utils.h b/src/plugins/gs-html-utils.h
new file mode 100644
index 0000000..35cc262
--- /dev/null
+++ b/src/plugins/gs-html-utils.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2014 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_STEAM_DESCRIPTION_H
+#define __GS_STEAM_DESCRIPTION_H
+
+#include <glib-object.h>
+
+gchar          *gs_html_utils_parse_description        (const gchar    *html,
+                                                        GError         **error);
+
+
+G_END_DECLS
+
+#endif /* __GS_STEAM_DESCRIPTION_H */
+
diff --git a/src/plugins/gs-plugin-steam.c b/src/plugins/gs-plugin-steam.c
new file mode 100644
index 0000000..45e8477
--- /dev/null
+++ b/src/plugins/gs-plugin-steam.c
@@ -0,0 +1,1079 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015-2016 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+
+#include <gs-plugin.h>
+#include <string.h>
+#include <icns.h>
+
+#include "gs-html-utils.h"
+#include "gs-utils.h"
+
+/**
+ * gs_plugin_get_name:
+ */
+const gchar *
+gs_plugin_get_name (void)
+{
+       return "steam";
+}
+
+/**
+ * gs_plugin_steam_html_download:
+ */
+static gboolean
+gs_plugin_steam_html_download (GsPlugin *plugin,
+                              const gchar *uri,
+                              gchar **data,
+                              gsize *data_len,
+                              GError **error)
+{
+       guint status_code;
+       g_autoptr(GInputStream) stream = NULL;
+       g_autoptr(SoupMessage) msg = NULL;
+
+       /* create the GET data */
+       msg = soup_message_new (SOUP_METHOD_GET, uri);
+       if (msg == NULL) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "%s is not a valid URL", uri);
+               return FALSE;
+       }
+
+       /* set sync request */
+       status_code = soup_session_send_message (plugin->soup_session, msg);
+       if (status_code != SOUP_STATUS_OK) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Failed to download icon %s: %s",
+                            uri, soup_status_get_phrase (status_code));
+               return FALSE;
+       }
+
+       /* return data */
+       if (data != NULL)
+               *data = g_memdup (msg->response_body->data,
+                                 msg->response_body->length);
+       if (data_len != NULL)
+               *data_len = msg->response_body->length;
+       return TRUE;
+}
+
+/**
+ * gs_plugin_get_deps:
+ */
+const gchar **
+gs_plugin_get_deps (GsPlugin *plugin)
+{
+       static const gchar *deps[] = {
+               "appstream",            /* need metadata */
+               NULL };
+       return deps;
+}
+
+typedef enum {
+       GS_PLUGIN_STEAM_TOKEN_START             = 0x00,
+       GS_PLUGIN_STEAM_TOKEN_STRING            = 0x01,
+       GS_PLUGIN_STEAM_TOKEN_INTEGER           = 0x02,
+       GS_PLUGIN_STEAM_TOKEN_END               = 0x08,
+       GS_PLUGIN_STEAM_TOKEN_LAST,
+} GsPluginSteamToken;
+
+/**
+ * gs_plugin_steam_token_kind_to_str:
+ **/
+static const gchar *
+gs_plugin_steam_token_kind_to_str (guint8 data)
+{
+       static gchar tmp[2] = { 0x00, 0x00 };
+
+       if (data == GS_PLUGIN_STEAM_TOKEN_START)
+               return "[SRT]";
+       if (data == GS_PLUGIN_STEAM_TOKEN_STRING)
+               return "[STR]";
+       if (data == GS_PLUGIN_STEAM_TOKEN_INTEGER)
+               return "[INT]";
+       if (data == GS_PLUGIN_STEAM_TOKEN_END)
+               return "[END]";
+
+       /* guess */
+       if (data == 0x03)
+               return "[ETX]";
+       if (data == 0x04)
+               return "[EOT]";
+       if (data == 0x05)
+               return "[ENQ]";
+       if (data == 0x06)
+               return "[ACK]";
+       if (data == 0x07)
+               return "[BEL]";
+       if (data == 0x09)
+               return "[SMI]";
+
+       /* printable */
+       if (g_ascii_isprint (data)) {
+               tmp[0] = data;
+               return tmp;
+       }
+       return "[?]";
+}
+
+/**
+ * gs_plugin_steam_consume_uint32:
+ **/
+static guint32
+gs_plugin_steam_consume_uint32 (guint8 *data, gsize data_len, guint *idx)
+{
+       guint32 tmp = *((guint32 *) &data[*idx + 1]);
+       *idx += 4;
+       return tmp;
+}
+
+/**
+ * gs_plugin_steam_consume_string:
+ **/
+static const gchar *
+gs_plugin_steam_consume_string (guint8 *data, gsize data_len, guint *idx)
+{
+       const gchar *tmp;
+
+       /* this may be an empty string */
+       tmp = (const gchar *) &data[*idx+1];
+       if (tmp[0] == '\0') {
+               (*idx)++;
+               return NULL;
+       }
+       *idx += strlen (tmp) + 1;
+       return tmp;
+}
+
+/**
+ * gs_plugin_steam_find_next_sync_point:
+ **/
+static void
+gs_plugin_steam_find_next_sync_point (guint8 *data, gsize data_len, guint *idx)
+{
+       guint i;
+       for (i = *idx; i < data_len; i++) {
+               if (memcmp (&data[i], "\0\x02\0common\0", 8) == 0) {
+                       *idx = i - 1;
+                       return;
+               }
+       }
+       *idx = 0xfffffffe;
+}
+
+/**
+ * gs_plugin_steam_add_app:
+ **/
+static GHashTable *
+gs_plugin_steam_add_app (GPtrArray *apps)
+{
+       GHashTable *app;
+       app = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                    g_free, (GDestroyNotify) g_variant_unref);
+       g_ptr_array_add (apps, app);
+       return app;
+}
+
+/**
+ * gs_plugin_steam_parse_appinfo_file:
+ **/
+static GPtrArray *
+gs_plugin_steam_parse_appinfo_file (const gchar *filename, GError **error)
+{
+       GPtrArray *apps;
+       GHashTable *app = NULL;
+       const gchar *tmp;
+       guint8 *data = NULL;
+       gsize data_len = 0;
+       guint i = 0;
+       gboolean debug =  g_getenv ("GS_PLUGIN_STEAM_DEBUG") != NULL;
+
+       /* load file */
+       if (!g_file_get_contents (filename, (gchar **) &data, &data_len, error))
+               return NULL;
+
+       /* a GPtrArray of GHashTable */
+       apps = g_ptr_array_new_with_free_func ((GDestroyNotify) g_hash_table_unref);
+
+       /* find the first application and avoid header */
+       gs_plugin_steam_find_next_sync_point (data, data_len, &i);
+       for (i = i + 1; i < data_len; i++) {
+               if (debug)
+                       g_debug ("%04x {0x%02x} %s", i, data[i], gs_plugin_steam_token_kind_to_str (data[i]));
+               if (data[i] == GS_PLUGIN_STEAM_TOKEN_START) {
+
+                       /* this is a new application/game */
+                       if (data[i+1] == 0x02) {
+                               /* reset */
+                               app = gs_plugin_steam_add_app (apps);
+                               i++;
+                               continue;
+                       }
+
+                       /* new group */
+                       if (g_ascii_isprint (data[i+1])) {
+                               tmp = gs_plugin_steam_consume_string (data, data_len, &i);
+                               if (debug)
+                                       g_debug ("[%s] {", tmp);
+                               continue;
+                       }
+
+                       /* something went wrong */
+                       if (debug)
+                               g_debug ("CORRUPTION DETECTED");
+                       gs_plugin_steam_find_next_sync_point (data, data_len, &i);
+                       continue;
+               }
+               if (data[i] == GS_PLUGIN_STEAM_TOKEN_END) {
+                       if (debug)
+                               g_debug ("}");
+                       continue;
+               }
+               if (data[i] == GS_PLUGIN_STEAM_TOKEN_STRING) {
+                       const gchar *value;
+                       tmp = gs_plugin_steam_consume_string (data, data_len, &i);
+                       value = gs_plugin_steam_consume_string (data, data_len, &i);
+                       if (debug)
+                               g_debug ("\t%s=%s", tmp, value);
+                       if (tmp != NULL && value != NULL) {
+                               if (g_hash_table_lookup (app, tmp) != NULL)
+                                       continue;
+                               g_hash_table_insert (app,
+                                                    g_strdup (tmp),
+                                                    g_variant_new_string (value));
+                       }
+                       continue;
+               }
+               if (data[i] == GS_PLUGIN_STEAM_TOKEN_INTEGER) {
+                       guint32 value;
+                       tmp = gs_plugin_steam_consume_string (data, data_len, &i);
+                       value = gs_plugin_steam_consume_uint32 (data, data_len, &i);
+                       if (debug)
+                               g_debug ("\t%s=%i", tmp, value);
+                       if (tmp != NULL) {
+                               if (g_hash_table_lookup (app, tmp) != NULL)
+                                       continue;
+                               g_hash_table_insert (app,
+                                                    g_strdup (tmp),
+                                                    g_variant_new_uint32 (value));
+                       }
+                       continue;
+               }
+       }
+
+       return apps;
+}
+
+/**
+ * gs_plugin_steam_dump_apps:
+ **/
+static void
+gs_plugin_steam_dump_apps (GPtrArray *apps)
+{
+       guint i;
+       GHashTable *app;
+
+       for (i = 0; i < apps->len; i++) {
+               g_autoptr(GList) keys = NULL;
+               GList *l;
+               app = g_ptr_array_index (apps, i);
+               keys = g_hash_table_get_keys (app);
+               for (l = keys; l != NULL; l = l->next) {
+                       const gchar *tmp;
+                       GVariant *value;
+                       tmp = l->data;
+                       value = g_hash_table_lookup (app, tmp);
+                       if (g_strcmp0 (g_variant_get_type_string (value), "s") == 0)
+                               g_print ("%s=%s\n", tmp, g_variant_get_string (value, NULL));
+                       else if (g_strcmp0 (g_variant_get_type_string (value), "u") == 0)
+                               g_print ("%s=%u\n", tmp, g_variant_get_uint32 (value));
+               }
+               g_print ("\n");
+       }
+}
+
+/**
+ * gs_plugin_steam_capture:
+ *
+ * Returns: A string between @start and @end, or %NULL
+ **/
+static gchar *
+gs_plugin_steam_capture (const gchar *html,
+                        const gchar *start,
+                        const gchar *end,
+                        guint *offset)
+{
+       guint i;
+       guint j;
+       guint start_len;
+       guint end_len;
+
+       /* find @start */
+       start_len = strlen (start);
+       for (i = *offset; html[i] != '\0'; i++) {
+               if (memcmp (&html[i], start, start_len) != 0)
+                       continue;
+               /* find @end */
+               end_len = strlen (end);
+               for (j = i + start_len; html[j] != '\0'; j++) {
+                       if (memcmp (&html[j], end, end_len) != 0)
+                               continue;
+                       *offset = j + end_len;
+                       return g_strndup (&html[i + start_len],
+                                         j - i - start_len);
+               }
+       }
+       return NULL;
+}
+
+/**
+ * gs_plugin_steam_update_screenshots:
+ **/
+static gboolean
+gs_plugin_steam_update_screenshots (AsApp *app, const gchar *html, GError **error)
+{
+       const gchar *gameid_str;
+       gchar *tmp1;
+       guint i = 0;
+       guint idx = 0;
+
+       /* find all the screenshots */
+       gameid_str = as_app_get_metadata_item (app, "X-Steam-GameID");
+       while ((tmp1 = gs_plugin_steam_capture (html, "data-screenshotid=\"", "\"", &i))) {
+               g_autoptr(AsImage) im = NULL;
+               g_autoptr(AsScreenshot) ss = NULL;
+               g_autofree gchar *cdn_uri = NULL;
+
+               /* create an image */
+               im = as_image_new ();
+               as_image_set_kind (im, AS_IMAGE_KIND_SOURCE);
+               cdn_uri = g_strdup_printf ("http://cdn.akamai.steamstatic.com/steam/apps/%s/%s";, gameid_str, 
tmp1);
+               as_image_set_url (im, cdn_uri);
+
+               /* create screenshot with no caption */
+               ss = as_screenshot_new ();
+               as_screenshot_set_kind (ss, idx == 0 ? AS_SCREENSHOT_KIND_DEFAULT :
+                                                      AS_SCREENSHOT_KIND_NORMAL);
+               as_screenshot_add_image (ss, im);
+               as_app_add_screenshot (app, ss);
+               g_free (tmp1);
+
+               /* limit this to a sane number */
+               if (idx++ >= 4)
+                       break;
+       }
+       return TRUE;
+}
+
+/**
+ * gs_plugin_steam_update_description:
+ **/
+static gboolean
+gs_plugin_steam_update_description (AsApp *app, const gchar *html, GError **error)
+{
+       guint i = 0;
+       g_autofree gchar *desc = NULL;
+       g_autofree gchar *subsect = NULL;
+       g_autoptr(GError) error_local = NULL;
+
+       /* get the game description div section */
+       subsect = gs_plugin_steam_capture (html,
+                       "<div id=\"game_area_description\" class=\"game_area_description\">",
+                       "</div>", &i);
+
+       /* fall back gracefully */
+       if (subsect == NULL) {
+               subsect = gs_plugin_steam_capture (html,
+                               "<meta name=\"Description\" content=\"",
+                               "\">", &i);
+       }
+       if (subsect == NULL) {
+               g_warning ("Failed to get description for %s [%s]",
+                          as_app_get_name (app, NULL),
+                          as_app_get_id (app));
+               return TRUE;
+       }
+       desc = gs_html_utils_parse_description (subsect, &error_local);
+       if (desc == NULL) {
+               g_warning ("Failed to parse description for %s [%s]: %s",
+                          as_app_get_name (app, NULL),
+                          as_app_get_id (app),
+                          error_local->message);
+               return TRUE;
+       }
+       as_app_set_description (app, NULL, desc);
+       return TRUE;
+}
+
+/**
+ * gs_plugin_steam_new_pixbuf_from_icns:
+ **/
+static GdkPixbuf *
+gs_plugin_steam_new_pixbuf_from_icns (const gchar *fn, GError **error)
+{
+       GdkPixbuf *pb = NULL;
+       FILE *datafile;
+       guint i;
+       icns_family_t *icon_family = NULL;
+       icns_image_t im;
+       int rc;
+       icns_type_t preference[] = {
+               ICNS_128X128_32BIT_DATA,
+               ICNS_256x256_32BIT_ARGB_DATA,
+               ICNS_48x48_32BIT_DATA,
+               0 };
+
+       /* open file */
+       datafile = fopen (fn, "rb");
+       rc = icns_read_family_from_file (datafile, &icon_family);
+       if (rc != 0) {
+               g_set_error (error, 1, 0, "Failed to read icon %s", fn);
+               return NULL;
+       }
+
+       /* libicns 'helpfully' frees the @arg */
+       im.imageData = NULL;
+
+       /* get the best sized icon */
+       for (i = 0; preference[i] != 0; i++) {
+               rc = icns_get_image32_with_mask_from_family (icon_family,
+                                                            preference[i],
+                                                            &im);
+               if (rc == 0) {
+                       gchar buf[5];
+                       icns_type_str (preference[i], buf);
+                       g_debug ("using ICNS %s for %s", buf, fn);
+                       break;
+               }
+       }
+       if (im.imageData == NULL) {
+               g_set_error (error, 1, 0, "Failed to get icon %s", fn);
+               return NULL;
+       }
+
+       /* create the pixbuf */
+       pb = gdk_pixbuf_new_from_data (im.imageData,
+                                      GDK_COLORSPACE_RGB,
+                                      TRUE,
+                                      im.imagePixelDepth,
+                                      im.imageWidth,
+                                      im.imageHeight,
+                                      im.imageWidth * im.imageChannels,
+                                      NULL, //??
+                                      NULL);
+       g_assert (pb != NULL);
+
+       fclose (datafile);
+       return pb;
+}
+
+/**
+ * gs_plugin_steam_download_icon:
+ **/
+static gboolean
+gs_plugin_steam_download_icon (GsPlugin *plugin, AsApp *app, const gchar *uri, GError **error)
+{
+       const gchar *gameid_str;
+       gsize data_len;
+       g_autofree gchar *cache_basename = NULL;
+       g_autofree gchar *cache_fn = NULL;
+       g_autofree gchar *cache_png = NULL;
+       g_autofree gchar *data = NULL;
+       g_autoptr(AsIcon) icon = NULL;
+       g_autoptr(GdkPixbuf) pb = NULL;
+
+       /* download icons from the cdn */
+       gameid_str = as_app_get_metadata_item (app, "X-Steam-GameID");
+       cache_basename = g_path_get_basename (uri);
+       cache_fn = g_build_filename (g_get_user_cache_dir (),
+                                    "gnome-software",
+                                    "steam",
+                                    cache_basename,
+                                    NULL);
+       if (g_file_test (cache_fn, G_FILE_TEST_EXISTS)) {
+               if (!g_file_get_contents (cache_fn, &data, &data_len, error))
+                       return FALSE;
+       } else {
+               if (!gs_plugin_steam_html_download (plugin, uri, &data, &data_len, error))
+                       return FALSE;
+               if (!gs_mkdir_parent (cache_fn, error))
+                       return FALSE;
+               if (!g_file_set_contents (cache_fn, data, data_len, error))
+                       return FALSE;
+       }
+
+       /* check the icns file is not just a png/ico/jpg file in disguise */
+       if (memcmp (data + 1, "\x89PNG", 4) == 0 ||
+           memcmp (data, "\x00\x00\x01\x00", 4) == 0 ||
+           memcmp (data, "\xff\xd8\xff", 3) == 0) {
+               g_debug ("using fallback for %s\n", cache_fn);
+               pb = gdk_pixbuf_new_from_file (cache_fn, error);
+               if (pb == NULL)
+                       return FALSE;
+       } else {
+               pb = gs_plugin_steam_new_pixbuf_from_icns (cache_fn, error);
+               if (pb == NULL)
+                       return FALSE;
+       }
+
+       /* too small? */
+       if (gdk_pixbuf_get_width (pb) < 48 ||
+           gdk_pixbuf_get_height (pb) < 48) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "icon is too small %ix%i",
+                            gdk_pixbuf_get_width (pb),
+                            gdk_pixbuf_get_height (pb));
+               return FALSE;
+       }
+
+       /* save to cache */
+       memcpy (cache_basename + 40, ".png\0", 5);
+       cache_png = g_build_filename (g_get_user_cache_dir (),
+                                     "gnome-software",
+                                     "steam",
+                                     cache_basename,
+                                     NULL);
+       if (!gdk_pixbuf_save (pb, cache_png, "png", error, NULL))
+               return FALSE;
+
+       /* add an icon */
+       icon = as_icon_new ();
+       as_icon_set_kind (icon, AS_ICON_KIND_LOCAL);
+       as_icon_set_filename (icon, cache_png);
+       as_app_add_icon (app, icon);
+       return TRUE;
+}
+
+/**
+ * gs_plugin_steam_update_store_app:
+ **/
+static gboolean
+gs_plugin_steam_update_store_app (GsPlugin *plugin,
+                                 AsStore *store,
+                                 GHashTable *app,
+                                 GError **error)
+{
+       const gchar *name;
+       GVariant *tmp;
+       guint32 gameid;
+       gchar *app_id;
+       g_autofree gchar *cache_basename = NULL;
+       g_autofree gchar *cache_fn = NULL;
+       g_autofree gchar *gameid_str = NULL;
+       g_autofree gchar *html = NULL;
+       g_autofree gchar *uri = NULL;
+       g_autoptr(AsApp) item = NULL;
+
+       /* this is the key */
+       tmp = g_hash_table_lookup (app, "gameid");
+       if (tmp == NULL)
+               return TRUE;
+       gameid = g_variant_get_uint32 (tmp);
+
+       /* valve use the name as the application ID, not the gameid */
+       tmp = g_hash_table_lookup (app, "name");
+       if (tmp == NULL)
+               return TRUE;
+       name = g_variant_get_string (tmp, NULL);
+       app_id = g_strdup_printf ("%s.desktop", name);
+
+       /* already exists */
+       if (as_store_get_app_by_id (store, app_id) != NULL) {
+               g_debug ("already exists %i, skipping", gameid);
+               return TRUE;
+       }
+
+       /* create application with the gameid as the key */
+       g_debug ("parsing steam %i", gameid);
+       item = as_app_new ();
+       as_app_set_project_license (item, "Steam");
+       as_app_set_id (item, app_id);
+       as_app_set_name (item, NULL, name);
+       as_app_add_category (item, "Game");
+       as_app_add_kudo_kind (item, AS_KUDO_KIND_MODERN_TOOLKIT);
+       as_app_set_comment (item, NULL, "Available on Steam");
+
+       /* this is for the GNOME Software plugin */
+       gameid_str = g_strdup_printf ("%" G_GUINT32_FORMAT, gameid);
+       as_app_add_metadata (item, "X-Steam-GameID", gameid_str);
+
+       /* ban certains apps based on the name */
+       if (g_strstr_len (name, -1, "Dedicated Server") != NULL)
+               as_app_add_veto (item, "Dedicated Server");
+
+       /* oslist */
+       tmp = g_hash_table_lookup (app, "oslist");
+       if (tmp == NULL) {
+               as_app_add_veto (item, "No operating systems listed");
+       } else if (g_strstr_len (g_variant_get_string (tmp, NULL), -1, "linux") == NULL) {
+               as_app_add_veto (item, "No Linux support");
+       }
+
+       /* url: homepage */
+       tmp = g_hash_table_lookup (app, "homepage");
+       if (tmp != NULL)
+               as_app_add_url (item, AS_URL_KIND_HOMEPAGE, g_variant_get_string (tmp, NULL));
+
+       /* developer name */
+       tmp = g_hash_table_lookup (app, "developer");
+       if (tmp != NULL)
+               as_app_set_developer_name (item, NULL, g_variant_get_string (tmp, NULL));
+
+       /* type */
+       tmp = g_hash_table_lookup (app, "type");
+       if (tmp != NULL) {
+               const gchar *kind = g_variant_get_string (tmp, NULL);
+               if (g_strcmp0 (kind, "DLC") == 0 ||
+                   g_strcmp0 (kind, "Config") == 0 ||
+                   g_strcmp0 (kind, "Tool") == 0)
+                       as_app_add_veto (item, "type is %s", kind);
+       }
+
+       /* don't bother saving apps with failures */
+       if (as_app_get_vetos(item)->len > 0)
+               return TRUE;
+
+       /* icons */
+       tmp = g_hash_table_lookup (app, "clienticns");
+       if (tmp != NULL) {
+               g_autoptr(GError) error_local = NULL;
+               g_autofree gchar *ic_uri = NULL;
+               ic_uri = g_strdup_printf 
("https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/%i/%s.icns";,
+                                         gameid, g_variant_get_string (tmp, NULL));
+               if (!gs_plugin_steam_download_icon (plugin, item, ic_uri, &error_local)) {
+                       g_warning ("Failed to parse clienticns: %s",
+                                  error_local->message);
+               }
+       }
+
+       /* try clienticon */
+       if (as_app_get_icons(item)->len == 0) {
+               tmp = g_hash_table_lookup (app, "clienticon");
+               if (tmp != NULL) {
+                       g_autoptr(GError) error_local = NULL;
+                       g_autofree gchar *ic_uri = NULL;
+                       ic_uri = g_strdup_printf 
("http://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/%i/%s.ico";,
+                                                 gameid, g_variant_get_string (tmp, NULL));
+                       if (!gs_plugin_steam_download_icon (plugin, item, ic_uri, &error_local)) {
+                               g_warning ("Failed to parse clienticon: %s",
+                                          error_local->message);
+                       }
+               }
+       }
+
+       /* fall back to a resized logo */
+       if (as_app_get_icons(item)->len == 0) {
+               tmp = g_hash_table_lookup (app, "logo");
+               if (tmp != NULL) {
+                       AsIcon *icon = NULL;
+                       g_autofree gchar *ic_uri = NULL;
+                       ic_uri = g_strdup_printf 
("http://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/%i/%s.jpg";,
+                                                 gameid, g_variant_get_string (tmp, NULL));
+                       icon = as_icon_new ();
+                       as_icon_set_kind (icon, AS_ICON_KIND_REMOTE);
+                       as_icon_set_url (icon, ic_uri);
+                       as_app_add_icon (item, icon);
+               }
+       }
+
+       /* size */
+       tmp = g_hash_table_lookup (app, "maxsize");
+       if (tmp != NULL) {
+               /* string when over 16Gb... :/ */
+               if (g_strcmp0 (g_variant_get_type_string (tmp), "u") == 0) {
+                       g_autofree gchar *val = NULL;
+                       val = g_strdup_printf ("%" G_GUINT32_FORMAT,
+                                              g_variant_get_uint32 (tmp));
+                       as_app_add_metadata (item, "X-Steam-Size", val);
+               } else {
+                       as_app_add_metadata (item, "X-Steam-Size",
+                                            g_variant_get_string (tmp, NULL));
+               }
+       }
+
+       /* download page from the store */
+       cache_basename = g_strdup_printf ("%s.html", gameid_str);
+       cache_fn = g_build_filename (g_get_user_cache_dir (),
+                                    "gnome-software",
+                                    "steam",
+                                    cache_basename,
+                                    NULL);
+       if (g_file_test (cache_fn, G_FILE_TEST_EXISTS)) {
+               if (!g_file_get_contents (cache_fn, &html, NULL, error))
+                       return FALSE;
+       } else {
+               uri = g_strdup_printf ("http://store.steampowered.com/app/%s/";, gameid_str);
+               if (!gs_plugin_steam_html_download (plugin, uri, &html, NULL, error))
+                       return FALSE;
+               if (!gs_mkdir_parent (cache_fn, error))
+                       return FALSE;
+               if (!g_file_set_contents (cache_fn, html, -1, error))
+                       return FALSE;
+       }
+
+       /* get screenshots and descriptions */
+       if (!gs_plugin_steam_update_screenshots (item, html, error))
+               return FALSE;
+       if (!gs_plugin_steam_update_description (item, html, error))
+               return FALSE;
+
+       /* add */
+       as_store_add_app (store, item);
+       return TRUE;
+}
+
+/**
+ * gs_plugin_steam_update_store:
+ */
+static gboolean
+gs_plugin_steam_update_store (GsPlugin *plugin, AsStore *store, GPtrArray *apps, GError **error)
+{
+       guint i;
+       GHashTable *app;
+
+       for (i = 0; i < apps->len; i++) {
+               app = g_ptr_array_index (apps, i);
+               if (!gs_plugin_steam_update_store_app (plugin, store, app, error))
+                       return FALSE;
+       }
+       return TRUE;
+}
+
+/**
+ * gs_plugin_refresh:
+ */
+gboolean
+gs_plugin_refresh (GsPlugin *plugin,
+                  guint cache_age,
+                  GsPluginRefreshFlags flags,
+                  GCancellable *cancellable,
+                  GError **error)
+{
+       g_autoptr(AsStore) store = NULL;
+       g_autoptr(GFile) file = NULL;
+       g_autoptr(GPtrArray) apps = NULL;
+       g_autofree gchar *fn = NULL;
+       g_autofree gchar *fn_xml = NULL;
+
+       /* check if exists */
+       fn = g_build_filename (g_get_user_data_dir (),
+                              "Steam", "appcache", "appinfo.vdf", NULL);
+       if (!g_file_test (fn, G_FILE_TEST_EXISTS)) {
+               g_debug ("no %s, so skipping", fn);
+               return TRUE;
+       }
+
+       /* test cache age */
+       fn_xml = g_build_filename (g_get_user_data_dir (),
+                                  "app-info", "xmls", "steam.xml.gz", NULL);
+       file = g_file_new_for_path (fn_xml);
+       if (cache_age > 0) {
+               guint tmp;
+               tmp = gs_utils_get_file_age (file);
+               if (tmp < cache_age) {
+                       g_debug ("%s is only %i seconds old, so ignoring refresh",
+                                fn_xml, tmp);
+                       return TRUE;
+               }
+       }
+
+       /* parse it */
+       apps = gs_plugin_steam_parse_appinfo_file (fn, error);
+       if (apps == NULL)
+               return FALSE;
+
+       /* debug */
+       if (g_getenv ("GS_PLUGIN_STEAM_DEBUG") != NULL)
+               gs_plugin_steam_dump_apps (apps);
+
+       /* load existing AppStream XML */
+       store = as_store_new ();
+       if (g_file_query_exists (file, cancellable)) {
+               if (!as_store_from_file (store, file, NULL, cancellable, error))
+                       return FALSE;
+       }
+
+       /* update any new applications */
+       if (!gs_plugin_steam_update_store (plugin, store, apps, error))
+               return FALSE;
+
+       /* save new file */
+       return as_store_to_file (store, file,
+                                AS_NODE_TO_XML_FLAG_FORMAT_INDENT |
+                                AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE,
+                                NULL,
+                                error);
+}
+
+/**
+ * gs_plugin_steam_load_app_manifest:
+ */
+static GHashTable *
+gs_plugin_steam_load_app_manifest (const gchar *fn, GError **error)
+{
+       GHashTable *manifest = NULL;
+       guint i;
+       guint j;
+       g_autofree gchar *data = NULL;
+       g_auto(GStrv) lines = NULL;
+
+       /* get file */
+       if (!g_file_get_contents (fn, &data, NULL, error))
+               return NULL;
+
+       /* parse each line */
+       manifest = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+       lines = g_strsplit (data, "\n", -1);
+       for (i = 0; lines[i] != NULL; i++) {
+               gboolean is_key = TRUE;
+               const gchar *tmp = lines[i];
+               g_autoptr(GString) key = g_string_new ("");
+               g_autoptr(GString) value = g_string_new ("");
+               for (j = 0; tmp[j] != '\0'; j++) {
+
+                       /* alphanum, so either key or value */
+                       if (g_ascii_isalnum (tmp[j])) {
+                               g_string_append_c (is_key ? key : value, tmp[j]);
+                               continue;
+                       }
+
+                       /* first whitespace after the key */
+                       if (g_ascii_isspace (tmp[j]) && key->len > 0)
+                               is_key = FALSE;
+               }
+               if (g_getenv ("GS_PLUGIN_STEAM_DEBUG") != NULL)
+                       g_debug ("manifest %s=%s", key->str, value->str);
+               if (key->len == 0 || value->len == 0)
+                       continue;
+               g_hash_table_insert (manifest, g_strdup (key->str), g_strdup (value->str));
+       }
+       return manifest;
+}
+
+typedef enum {
+       GS_STEAM_STATE_FLAG_INVALID             = 0,
+       GS_STEAM_STATE_FLAG_UNINSTALLED         = 1 << 0,
+       GS_STEAM_STATE_FLAG_UPDATE_REQUIRED     = 1 << 1,
+       GS_STEAM_STATE_FLAG_FULLY_INSTALLED     = 1 << 2,
+       GS_STEAM_STATE_FLAG_ENCRYPTED           = 1 << 3,
+       GS_STEAM_STATE_FLAG_LOCKED              = 1 << 4,
+       GS_STEAM_STATE_FLAG_FILES_MISSING       = 1 << 5,
+       GS_STEAM_STATE_FLAG_APP_RUNNING         = 1 << 6,
+       GS_STEAM_STATE_FLAG_FILES_CORRUPT       = 1 << 7,
+       GS_STEAM_STATE_FLAG_UPDATE_RUNNING      = 1 << 8,
+       GS_STEAM_STATE_FLAG_UPDATE_PAUSED       = 1 << 9,
+       GS_STEAM_STATE_FLAG_UPDATE_STARTED      = 1 << 10,
+       GS_STEAM_STATE_FLAG_UNINSTALLING        = 1 << 11,
+       GS_STEAM_STATE_FLAG_BACKUP_RUNNING      = 1 << 12,
+       /* not sure what happened here... */
+       GS_STEAM_STATE_FLAG_RECONFIGURING       = 1 << 16,
+       GS_STEAM_STATE_FLAG_VALIDATING          = 1 << 17,
+       GS_STEAM_STATE_FLAG_ADDING_FILES        = 1 << 18,
+       GS_STEAM_STATE_FLAG_PREALLOCATING       = 1 << 19,
+       GS_STEAM_STATE_FLAG_DOWNLOADING         = 1 << 20,
+       GS_STEAM_STATE_FLAG_STAGING             = 1 << 21,
+       GS_STEAM_STATE_FLAG_COMMITTING          = 1 << 22,
+       GS_STEAM_STATE_FLAG_UPDATE_STOPPING     = 1 << 23,
+       GS_STEAM_STATE_FLAG_LAST
+} GsSteamStateFlags;
+
+/**
+ * gs_plugin_steam_refine_app:
+ */
+static gboolean
+gs_plugin_steam_refine_app (GsPlugin *plugin,
+                           GsApp *app,
+                           GsPluginRefineFlags flags,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       const gchar *gameid;
+       const gchar *tmp;
+       g_autofree gchar *manifest_basename = NULL;
+       g_autofree gchar *fn = NULL;
+       g_autoptr(GHashTable) manifest = NULL;
+
+       /* check is us */
+       gameid = gs_app_get_metadata_item (app, "X-Steam-GameID");
+       if (gameid == NULL)
+               return TRUE;
+
+       /* is this true? */
+       gs_app_set_kind (app, AS_ID_KIND_DESKTOP);
+
+       /* size */
+       tmp = gs_app_get_metadata_item (app, "X-Steam-Size");
+       if (tmp != NULL) {
+               guint64 sz;
+               sz = g_ascii_strtoull (tmp, NULL, 10);
+               if (sz > 0)
+                       gs_app_set_size (app, sz);
+       }
+
+       /* check manifest */
+       manifest_basename = g_strdup_printf ("appmanifest_%s.acf", gameid);
+       fn = g_build_filename (g_get_user_data_dir (),
+                              "Steam",
+                              "steamapps",
+                              manifest_basename,
+                              NULL);
+       if (!g_file_test (fn, G_FILE_TEST_EXISTS)) {
+               /* can never have been installed */
+               gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+               return TRUE;
+       }
+       manifest = gs_plugin_steam_load_app_manifest (fn, error);
+       if (manifest == NULL)
+               return FALSE;
+
+       /* this is better than the download size */
+       tmp = g_hash_table_lookup (manifest, "SizeOnDisk");
+       if (tmp != NULL) {
+               guint64 sz;
+               sz = g_ascii_strtoull (tmp, NULL, 10);
+               if (sz > 0)
+                       gs_app_set_size (app, sz);
+       }
+
+       /* set state */
+       tmp = g_hash_table_lookup (manifest, "StateFlags");
+       if (tmp != NULL) {
+               guint64 state_flags;
+
+               /* set state */
+               state_flags = g_ascii_strtoull (tmp, NULL, 10);
+               if (state_flags & GS_STEAM_STATE_FLAG_DOWNLOADING ||
+                   state_flags & GS_STEAM_STATE_FLAG_PREALLOCATING ||
+                   state_flags & GS_STEAM_STATE_FLAG_ADDING_FILES ||
+                   state_flags & GS_STEAM_STATE_FLAG_COMMITTING ||
+                   state_flags & GS_STEAM_STATE_FLAG_STAGING)
+                       gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+               else if (state_flags & GS_STEAM_STATE_FLAG_UNINSTALLING)
+                       gs_app_set_state (app, AS_APP_STATE_REMOVING);
+               else if (state_flags & GS_STEAM_STATE_FLAG_FULLY_INSTALLED)
+                       gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+               else if (state_flags & GS_STEAM_STATE_FLAG_UNINSTALLED)
+                       gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+       }
+
+       /* set install date */
+       tmp = g_hash_table_lookup (manifest, "LastUpdated");
+       if (tmp != NULL) {
+               guint64 ts;
+               ts = g_ascii_strtoull (tmp, NULL, 10);
+               if (ts > 0)
+                       gs_app_set_install_date (app, ts);
+       }
+
+       return TRUE;
+}
+
+/**
+ * gs_plugin_refine:
+ */
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+                 GList **list,
+                 GsPluginRefineFlags flags,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       GList *l;
+       GsApp *app;
+
+       for (l = *list; l != NULL; l = l->next) {
+               app = GS_APP (l->data);
+               if (!gs_plugin_steam_refine_app (plugin, app, flags,
+                                                cancellable, error))
+                       return FALSE;
+       }
+       return TRUE;
+}
+
+/**
+ * gs_plugin_app_install:
+ */
+gboolean
+gs_plugin_app_install (GsPlugin *plugin, GsApp *app,
+                      GCancellable *cancellable, GError **error)
+{
+       const gchar *gameid;
+       g_autofree gchar *cmdline = NULL;
+
+       /* check is us */
+       gameid = gs_app_get_metadata_item (app, "X-Steam-GameID");
+       if (gameid == NULL)
+               return TRUE;
+
+       /* this is async as steam is a different process: FIXME: use D-Bus */
+       gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+       cmdline = g_strdup_printf ("steam steam://install/%s", gameid);
+       return g_spawn_command_line_sync (cmdline, NULL, NULL, NULL, error);
+}
+
+/**
+ * gs_plugin_app_remove:
+ */
+gboolean
+gs_plugin_app_remove (GsPlugin *plugin, GsApp *app,
+                     GCancellable *cancellable, GError **error)
+{
+       const gchar *gameid;
+       g_autofree gchar *cmdline = NULL;
+
+       /* check is us */
+       gameid = gs_app_get_metadata_item (app, "X-Steam-GameID");
+       if (gameid == NULL)
+               return TRUE;
+
+       /* this is async as steam is a different process: FIXME: use D-Bus */
+       gs_app_set_state (app, AS_APP_STATE_REMOVING);
+       cmdline = g_strdup_printf ("steam steam://uninstall/%s", gameid);
+       return g_spawn_command_line_sync (cmdline, NULL, NULL, NULL, error);
+}
+
+/**
+ * gs_plugin_launch:
+ */
+gboolean
+gs_plugin_launch (GsPlugin *plugin, GsApp *app,
+                 GCancellable *cancellable, GError **error)
+{
+       const gchar *gameid;
+       g_autofree gchar *cmdline = NULL;
+
+       /* check is us */
+       gameid = gs_app_get_metadata_item (app, "X-Steam-GameID");
+       if (gameid == NULL)
+               return TRUE;
+
+       /* this is async as steam is a different process: FIXME: use D-Bus */
+       cmdline = g_strdup_printf ("steam steam://run/%s", gameid);
+       return g_spawn_command_line_sync (cmdline, NULL, NULL, NULL, error);
+}
diff --git a/src/plugins/gs-self-test.c b/src/plugins/gs-self-test.c
index 6b45897..c81fad4 100644
--- a/src/plugins/gs-self-test.c
+++ b/src/plugins/gs-self-test.c
@@ -27,6 +27,44 @@
 #include <gtk/gtk.h>
 
 #include "gs-moduleset.h"
+#include "gs-html-utils.h"
+
+static void
+html_utils_func (void)
+{
+       const gchar *input;
+       g_autofree gchar *out_complex = NULL;
+       g_autofree gchar *out_list = NULL;
+       g_autofree gchar *out_simple = NULL;
+       g_autoptr(GError) error = NULL;
+
+       /* simple, from meta */
+       input = "This game is simply awesome&trade; in every way!";
+       out_simple = gs_html_utils_parse_description (input, &error);
+       g_assert_no_error (error);
+       g_assert_cmpstr (out_simple, ==, "<p>This game is simply awesome™ in every way!</p>");
+
+       /* complex non-compliant HTML, from div */
+       input = "  <h1>header</h1>"
+               "  <p>First line of the <i>description</i> is okay...</p>"
+               "  <img src=\"moo.png\">"
+               "  <img src=\"png\">"
+               "  <p>Second <strong>line</strong> is <a href=\"#moo\">even</a> better!</p>";
+       out_complex = gs_html_utils_parse_description (input, &error);
+       g_print ("\n\n%s\n\n", out_complex);
+       g_assert_no_error (error);
+       g_assert_cmpstr (out_complex, ==, "<p>First line of the description is okay...</p>"
+                                         "<p>Second line is even better!</p>");
+
+       /* complex list */
+       input = "  <ul>"
+               "  <li>First line of the list</li>"
+               "  <li>Second line of the list</li>"
+               "  </ul>";
+       out_list = gs_html_utils_parse_description (input, &error);
+       g_assert_no_error (error);
+       g_assert_cmpstr (out_list, ==, "<ul><li>First line of the list</li><li>Second line of the 
list</li></ul>");
+}
 
 static void
 moduleset_func (void)
@@ -76,6 +114,7 @@ main (int argc, char **argv)
 
        /* tests go here */
        g_test_add_func ("/moduleset", moduleset_func);
+       g_test_add_func ("/html-utils", html_utils_func);
 
        return g_test_run ();
 }



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]