[retro-gtk/wip/aplazas/bml] Add RetroBml



commit cddcbdaeebd5a7cfeb454135315eddd8eaa3d5fd
Author: Adrien Plazas <kekun plazas laposte net>
Date:   Thu Nov 15 12:37:14 2018 +0100

    Add RetroBml
    
    This allows to parse the BML file format used by the Higan shader
    format.

 retro-gtk/meson.build         |   1 +
 retro-gtk/retro-bml-private.h |  37 ++++
 retro-gtk/retro-bml.c         | 382 ++++++++++++++++++++++++++++++++++++++++++
 tests/meson.build             |   8 +
 tests/test-bml.c              | 179 ++++++++++++++++++++
 tests/test-bml.gresource.xml  |   6 +
 tests/test.bml                |  21 +++
 7 files changed, 634 insertions(+)
---
diff --git a/retro-gtk/meson.build b/retro-gtk/meson.build
index f54fdd8..1dd5be2 100644
--- a/retro-gtk/meson.build
+++ b/retro-gtk/meson.build
@@ -7,6 +7,7 @@ retro_gtk_resources = gnome.compile_resources(
 
 retro_gtk_sources = [
   retro_gtk_resources[0],
+  'retro-bml.c',
   'retro-cairo-display.c',
   'retro-controller.c',
   'retro-controller-codes.c',
diff --git a/retro-gtk/retro-bml-private.h b/retro-gtk/retro-bml-private.h
new file mode 100644
index 0000000..302cea4
--- /dev/null
+++ b/retro-gtk/retro-bml-private.h
@@ -0,0 +1,37 @@
+// This file is part of retro-gtk. License: GPL-3.0+.
+
+#pragma once
+
+#if !defined(__RETRO_GTK_INSIDE__) && !defined(RETRO_GTK_COMPILATION)
+# error "Only <retro-gtk.h> can be included directly."
+#endif
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define RETRO_BML_ERROR (retro_bml_error_quark ())
+
+typedef enum
+{
+  RETRO_BML_ERROR_NOT_NAME,
+  RETRO_BML_ERROR_NOT_QUOTED_VALUE,
+  RETRO_BML_ERROR_NOT_VALUE,
+} RetroBmlError;
+
+GQuark retro_bml_error_quark (void);
+
+#define RETRO_TYPE_BML (retro_bml_get_type())
+
+G_DECLARE_FINAL_TYPE (RetroBml, retro_bml, RETRO, BML, GObject)
+
+RetroBml *retro_bml_new (void);
+void retro_bml_parse_file (RetroBml  *self,
+                           GFile     *file,
+                           GError   **error);
+GNode *retro_bml_get_root (RetroBml  *self);
+gchar *retro_bml_node_get_name (GNode *node);
+gchar *retro_bml_node_get_value (GNode *node);
+GHashTable *retro_bml_node_get_attributes (GNode *node);
+
+G_END_DECLS
diff --git a/retro-gtk/retro-bml.c b/retro-gtk/retro-bml.c
new file mode 100644
index 0000000..723eba1
--- /dev/null
+++ b/retro-gtk/retro-bml.c
@@ -0,0 +1,382 @@
+// This file is part of retro-gtk. License: GPL-3.0+.
+
+#include "retro-bml-private.h"
+
+/* This parses the BML markup language from Higan. It is used by the Higan
+ * shader format that is supported by retro-gtk.
+ */
+
+struct _RetroBml
+{
+  GObject parent_instance;
+  GNode *root;
+};
+
+typedef struct
+{
+  guint depth;
+  gchar *name;
+  gchar *value;
+  GHashTable *attributes;
+} Data;
+
+G_DEFINE_TYPE (RetroBml, retro_bml, G_TYPE_OBJECT)
+
+static void
+free_data (Data *data)
+{
+  g_free (data->name);
+  g_free (data->value);
+  if (data->attributes)
+    g_hash_table_unref (data->attributes);
+
+  g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (Data, free_data);
+
+static gboolean
+free_node_data (GNode    *node,
+                gpointer  user_data)
+{
+  free_data (node->data);
+
+  return FALSE;
+}
+
+RetroBml *
+retro_bml_new (void)
+{
+  return g_object_new (RETRO_TYPE_BML, NULL);
+}
+
+static void
+retro_bml_finalize (GObject *object)
+{
+  RetroBml *self = (RetroBml *)object;
+
+  g_node_traverse (self->root, G_IN_ORDER, G_TRAVERSE_ALL, -1, free_node_data, NULL);
+  g_node_destroy (self->root);
+
+  G_OBJECT_CLASS (retro_bml_parent_class)->finalize (object);
+}
+
+static void
+retro_bml_class_init (RetroBmlClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = retro_bml_finalize;
+}
+
+static void
+retro_bml_init (RetroBml *self)
+{
+  self->root = g_node_new (g_new0 (Data, 1));
+}
+
+static guint
+count_whitespaces (gchar *line)
+{
+  gchar *p;
+
+  for (p = line; g_ascii_isspace (*p); p++);
+
+  return p - line;
+}
+
+static gboolean
+is_valid (char c)
+{
+  return g_ascii_isalpha (c) || g_ascii_isdigit (c) || c == '-' || c == '.';
+}
+
+static guint
+count_name (gchar *line)
+{
+  gchar *p;
+
+  for (p = line; is_valid (*p); p++);
+
+  return p - line;
+}
+
+static guint
+parse_whitespaces (gchar *start, gchar **end)
+{
+  guint length = count_whitespaces (start);
+
+  if (end)
+    *end = start + length;
+
+  return length;
+}
+
+static gchar *
+parse_name (gchar   *start,
+            gchar  **end,
+            GError **error)
+{
+  guint length = count_name (start);
+
+  if (end)
+    *end = start + length;
+
+  if (length == 0) {
+    g_set_error (error,
+                 RETRO_TYPE_BML,
+                 RETRO_BML_ERROR_NOT_NAME,
+                 "Expected a name, got %s.",
+                 start);
+
+    return NULL;
+  }
+
+  return g_strndup (start, length);
+}
+
+static gchar *
+parse_value (gchar   *start,
+             gchar  **end,
+             GError **error)
+{
+  guint length = 0;
+
+  if(start[0] == '=' && start[1] == '\"') {
+    start += 2;
+    /* Parse quoted values. */
+    for (length = 0; start[length] && start[length] != '\"'; length++);
+    if(start[length] != '\"') {
+      g_set_error (error,
+                   RETRO_TYPE_BML,
+                   RETRO_BML_ERROR_NOT_QUOTED_VALUE,
+                   "Expected a quoted value, got %s: closing quote not found.",
+                   start);
+
+      return NULL;
+    }
+
+    if (end)
+      *end = start + length + 1;
+  }
+  else if(start[0] == '=') {
+    start++;
+    /* Parse unquoted values */
+    for (length = 0; start[length] && start[length] != '\"' && start[length] != ' '; length++);
+    if(start[length] == '\"') {
+      g_set_error (error,
+                   RETRO_TYPE_BML,
+                   RETRO_BML_ERROR_NOT_VALUE,
+                   "Expected a value, got %s: illegal character '%c'.",
+                   start, start[length]);
+
+      return NULL;
+    }
+
+    if (end)
+      *end = start + length;
+  }
+  else if(start[0] == ':') {
+    start++;
+    for (length = 0; start[length]; length++);
+
+    if (end)
+      *end = start + length;
+  }
+
+  return g_strndup (start, length);
+}
+
+/* Attributes are name-value pairs following the node's name on the same line.
+ * They can take the following forms:
+ * - name=value
+ * - name="long value"
+ * - name:value to the line end
+ */
+static GHashTable *
+parse_attributes (gchar   *start,
+                  gchar  **end,
+                  GError **error)
+{
+  g_autoptr (GHashTable) attributes =
+    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+  g_autoptr (GError) tmp_error = NULL;
+
+  while(start[0]) {
+    g_autofree gchar *name = NULL;
+    g_autofree gchar *value = NULL;
+
+    if(start[0] != ' ') {
+      /* Cheating a bit as what we expect isn't a name per se, but the spaces
+       * after the names didn't get parsed so if there are no spaces, it means
+       * there was an illegal character in the name.
+       */
+      g_set_error (error,
+                   RETRO_TYPE_BML,
+                   RETRO_BML_ERROR_NOT_NAME,
+                   "Expected a name, got the illegal character '%c'.",
+                   *start);
+
+      return NULL;
+    }
+
+    while (start[0] == ' ')
+      start++;
+
+    if(start[0] == '/' && start[1] == '/')
+      break;
+
+    name = parse_name (start, &start, &tmp_error);
+    if (tmp_error != NULL) {
+      g_propagate_error (error, g_steal_pointer (&tmp_error));
+
+      return NULL;
+    }
+
+    value = parse_value (start, &start, &tmp_error);
+    if (tmp_error != NULL) {
+      g_propagate_error (error, g_steal_pointer (&tmp_error));
+
+      return NULL;
+    }
+
+    g_strchomp (value);
+
+    g_hash_table_insert (attributes,
+                         g_steal_pointer (&name),
+                         g_steal_pointer (&value));
+  }
+
+  return g_steal_pointer (&attributes);
+}
+
+static void
+parse_stream (RetroBml      *self,
+              GInputStream  *stream,
+              GError       **error)
+{
+  g_autoptr (GDataInputStream) data_stream = g_data_input_stream_new (stream);
+  gsize length;
+  GNode *parent_node;
+  g_autoptr (GError) tmp_error = NULL;
+
+  parent_node = self->root;
+
+  while (TRUE) {
+    g_autofree gchar *line = NULL;
+    gchar *start;
+    g_autoptr (Data) data = g_new0 (Data, 1);
+    GNode *current_node;
+
+    line = g_data_input_stream_read_line (data_stream, &length, NULL, &tmp_error);
+    if (tmp_error != NULL) {
+      g_propagate_error (error, g_steal_pointer (&tmp_error));
+
+      return;
+    }
+
+    if (line == NULL)
+      break;
+
+    g_strchomp (line);
+    if (line[0] == '\0')
+      continue;
+
+    start = line;
+
+    data->depth = parse_whitespaces (start, &start);
+    if (line[data->depth] == '/' && line[data->depth + 1] == '/')
+      continue;
+
+    while (data->depth + 1 <= ((Data *) parent_node->data)->depth)
+      parent_node = parent_node->parent;
+
+    /* Parse multi-line values starting with ':'. */
+    if (start[0] == ':') {
+      data->value = g_strdup_printf ("%s%s\n",
+                                     ((Data *) parent_node->data)->value,
+                                     start + 1);
+      ((Data *) parent_node->data)->value = g_steal_pointer (&data->value);
+
+      continue;
+    }
+
+    data->name = parse_name (start, &start, &tmp_error);
+    if (tmp_error != NULL) {
+      g_propagate_error (error, g_steal_pointer (&tmp_error));
+
+      return;
+    }
+
+    data->value = parse_value (start, &start, &tmp_error);
+    if (tmp_error != NULL) {
+      g_propagate_error (error, g_steal_pointer (&tmp_error));
+
+      return;
+    }
+
+    data->attributes = parse_attributes (start, &start, &tmp_error);
+    if (tmp_error != NULL) {
+      g_propagate_error (error, g_steal_pointer (&tmp_error));
+
+      return;
+    }
+
+    /* Ensure all nodes are children of the RetroBml_private_offset. */
+    data->depth++;
+
+    current_node = g_node_new (g_steal_pointer (&data));
+    g_node_append (parent_node, current_node);
+    parent_node = current_node;
+  }
+}
+
+void
+retro_bml_parse_file (RetroBml  *self,
+                      GFile     *file,
+                      GError   **error)
+{
+  g_autoptr (GFileInputStream) stream = NULL;
+  g_autoptr (GError) tmp_error = NULL;
+  
+  stream = g_file_read (file, NULL, error);
+  if (G_UNLIKELY (tmp_error != NULL)) {
+    g_propagate_error (error, g_steal_pointer (&tmp_error));
+
+    return;
+  }
+
+  parse_stream (self, G_INPUT_STREAM (stream), error);
+}
+
+GNode *
+retro_bml_get_root (RetroBml  *self)
+{
+  return self->root;
+}
+
+gchar *
+retro_bml_node_get_name (GNode *node)
+{
+  Data *data = node->data;
+
+  return data->name;
+}
+
+gchar *
+retro_bml_node_get_value (GNode *node)
+{
+  Data *data = node->data;
+
+  return data->value;
+}
+
+GHashTable *
+retro_bml_node_get_attributes (GNode *node)
+{
+  Data *data = node->data;
+
+  return data->attributes;
+}
+
+G_DEFINE_QUARK (retro-bml-error, retro_bml_error)
diff --git a/tests/meson.build b/tests/meson.build
index 61d303c..1dc2d5a 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -47,8 +47,16 @@ build_conf.set('testlibretrodir', join_paths(meson.build_root(), 'tests'))
 build_conf.set('testexecdir', join_paths(meson.build_root(), 'tests'))
 build_conf.set('testdatadir', join_paths(meson.source_root(), 'tests'))
 
+test_bml_resources = gnome.compile_resources(
+  'test_bml_resources',
+  'test-bml.gresource.xml',
+  c_name: 'test_bml',
+  source_dir: '.',
+)
+
 tests = [
   ['RetroCore', 'test-core', [], [retro_dummy_lib]],
+  ['RetroBml', 'test-bml', [test_bml_resources[0]], []],
 ]
 
 foreach t : tests
diff --git a/tests/test-bml.c b/tests/test-bml.c
new file mode 100644
index 0000000..f61d675
--- /dev/null
+++ b/tests/test-bml.c
@@ -0,0 +1,179 @@
+/* test-bml.c
+ *
+ * Copyright (C) 2018 Adrien Plazas <kekun plazas laposte net>
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "../retro-gtk/retro-bml-private.h"
+
+static void
+test_parse (void)
+{
+  g_autoptr (RetroBml) bml = NULL;
+  g_autoptr (GFile) file = NULL;
+  GNode *root, *node;
+  gchar *name, *value;
+  GHashTable *attributes;
+  GError *error = NULL;
+
+  bml = retro_bml_new ();
+  file = g_file_new_for_uri ("resource:///org/gnome/Retro/Tests/RetroBml/test.bml");
+  retro_bml_parse_file (bml, file, &error);
+  g_assert_no_error (error);
+
+  root = retro_bml_get_root (bml);
+  g_assert_nonnull (root);
+  g_assert_cmpuint (g_node_n_children (root), ==, 4);
+
+  node = g_node_nth_child (root, 0);
+  g_assert_nonnull (node);
+  g_assert_cmpuint (g_node_n_children (node), ==, 2);
+  name = retro_bml_node_get_name (node);
+  g_assert_cmpstr (name, ==, "namespace");
+  attributes = retro_bml_node_get_attributes (node);
+  g_assert_nonnull (attributes);
+  g_assert_cmpuint (g_hash_table_size (attributes), ==, 1);
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "name"), ==, "Depth");
+  value = retro_bml_node_get_value (node);
+  g_assert_cmpstr (value, ==, "");
+
+  node = g_node_nth_child (root, 0);
+  g_assert_nonnull (node);
+  node = g_node_nth_child (node, 0);
+  g_assert_nonnull (node);
+  g_assert_cmpuint (g_node_n_children (node), ==, 1);
+  name = retro_bml_node_get_name (node);
+  g_assert_cmpstr (name, ==, "namespace");
+  attributes = retro_bml_node_get_attributes (node);
+  g_assert_nonnull (attributes);
+  g_assert_cmpuint (g_hash_table_size (attributes), ==, 1);
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "name"), ==, "Test1");
+  value = retro_bml_node_get_value (node);
+  g_assert_cmpstr (value, ==, "");
+
+  node = g_node_nth_child (root, 0);
+  g_assert_nonnull (node);
+  node = g_node_nth_child (node, 0);
+  g_assert_nonnull (node);
+  node = g_node_nth_child (node, 0);
+  g_assert_nonnull (node);
+  g_assert_cmpuint (g_node_n_children (node), ==, 0);
+  name = retro_bml_node_get_name (node);
+  g_assert_cmpstr (name, ==, "binary");
+  attributes = retro_bml_node_get_attributes (node);
+  g_assert_nonnull (attributes);
+  g_assert_cmpuint (g_hash_table_size (attributes), ==, 2);
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "name"), ==, "testfile1");
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "file"), ==, "test/testfile1.test");
+  value = retro_bml_node_get_value (node);
+  g_assert_cmpstr (value, ==, "");
+
+  node = g_node_nth_child (root, 0);
+  g_assert_nonnull (node);
+  node = g_node_nth_child (node, 1);
+  g_assert_nonnull (node);
+  g_assert_cmpuint (g_node_n_children (node), ==, 2);
+  name = retro_bml_node_get_name (node);
+  g_assert_cmpstr (name, ==, "namespace");
+  attributes = retro_bml_node_get_attributes (node);
+  g_assert_nonnull (attributes);
+  g_assert_cmpuint (g_hash_table_size (attributes), ==, 1);
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "name"), ==, "Test2");
+  value = retro_bml_node_get_value (node);
+  g_assert_cmpstr (value, ==, "");
+
+  node = g_node_nth_child (root, 0);
+  g_assert_nonnull (node);
+  node = g_node_nth_child (node, 1);
+  g_assert_nonnull (node);
+  node = g_node_nth_child (node, 0);
+  g_assert_nonnull (node);
+  g_assert_cmpuint (g_node_n_children (node), ==, 0);
+  name = retro_bml_node_get_name (node);
+  g_assert_cmpstr (name, ==, "binary");
+  attributes = retro_bml_node_get_attributes (node);
+  g_assert_nonnull (attributes);
+  g_assert_cmpuint (g_hash_table_size (attributes), ==, 2);
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "name"), ==, "testfile2a");
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "file"), ==, "test/testfile2a.test");
+  value = retro_bml_node_get_value (node);
+  g_assert_cmpstr (value, ==, "");
+
+  node = g_node_nth_child (root, 0);
+  g_assert_nonnull (node);
+  node = g_node_nth_child (node, 1);
+  g_assert_nonnull (node);
+  node = g_node_nth_child (node, 1);
+  g_assert_nonnull (node);
+  g_assert_cmpuint (g_node_n_children (node), ==, 0);
+  name = retro_bml_node_get_name (node);
+  g_assert_cmpstr (name, ==, "binary");
+  attributes = retro_bml_node_get_attributes (node);
+  g_assert_nonnull (attributes);
+  g_assert_cmpuint (g_hash_table_size (attributes), ==, 2);
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "name"), ==, "testfile2b");
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "file"), ==, "test/testfile2b.test");
+  value = retro_bml_node_get_value (node);
+  g_assert_cmpstr (value, ==, "");
+
+  node = g_node_nth_child (root, 1);
+  g_assert_nonnull (node);
+  g_assert_cmpuint (g_node_n_children (node), ==, 0);
+  name = retro_bml_node_get_name (node);
+  g_assert_cmpstr (name, ==, "attributes");
+  attributes = retro_bml_node_get_attributes (node);
+  g_assert_nonnull (attributes);
+  g_assert_cmpuint (g_hash_table_size (attributes), ==, 3);
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "simple"), ==, "simplevalue");
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "quoted"), ==, "I am quoted");
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "lineend"), ==, "This is a line end attribute");
+  value = retro_bml_node_get_value (node);
+  g_assert_cmpstr (value, ==, "");
+
+  node = g_node_nth_child (root, 2);
+  g_assert_nonnull (node);
+  g_assert_cmpuint (g_node_n_children (node), ==, 0);
+  name = retro_bml_node_get_name (node);
+  g_assert_cmpstr (name, ==, "cartridge");
+  attributes = retro_bml_node_get_attributes (node);
+  g_assert_nonnull (attributes);
+  g_assert_cmpuint (g_hash_table_size (attributes), ==, 1);
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "sha256"), ==, 
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
+  value = retro_bml_node_get_value (node);
+  g_assert_cmpstr (value, ==, "This is a multiline value.\n");
+
+  node = g_node_nth_child (root, 3);
+  g_assert_nonnull (node);
+  g_assert_cmpuint (g_node_n_children (node), ==, 0);
+  name = retro_bml_node_get_name (node);
+  g_assert_cmpstr (name, ==, "cartridge");
+  attributes = retro_bml_node_get_attributes (node);
+  g_assert_nonnull (attributes);
+  g_assert_cmpuint (g_hash_table_size (attributes), ==, 1);
+  g_assert_cmpstr (g_hash_table_lookup (attributes, "sha256"), ==, 
"fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210");
+  value = retro_bml_node_get_value (node);
+  g_assert_cmpstr (value, ==, "This multiline value\nactually is multiline.\n");
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func("/RetroBml/parse", test_parse);
+
+  return g_test_run();
+}
diff --git a/tests/test-bml.gresource.xml b/tests/test-bml.gresource.xml
new file mode 100644
index 0000000..9692ec1
--- /dev/null
+++ b/tests/test-bml.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/Retro/Tests/RetroBml">
+    <file>test.bml</file>
+  </gresource>
+</gresources>
diff --git a/tests/test.bml b/tests/test.bml
new file mode 100644
index 0000000..4a6e2d1
--- /dev/null
+++ b/tests/test.bml
@@ -0,0 +1,21 @@
+// This is comment and should not be parsed.
+
+// Test a hierachy with some depth and simple attributes.
+namespace name=Depth
+  namespace name=Test1
+    binary name=testfile1 file=test/testfile1.test
+  namespace name=Test2
+    binary name=testfile2a file=test/testfile2a.test
+    binary name=testfile2b file=test/testfile2b.test
+
+// Test quoted attrbiutes comments.
+attributes simple=simplevalue quoted="I am quoted" lineend:This is a line end attribute
+
+// Test multiline values.
+cartridge sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
+  :This is a multiline value.
+
+cartridge sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210
+  :This multiline value
+  :actually is multiline.
+


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