[retro-gtk/wip/aplazas/bml] Add RetroBml
- From: Adrien Plazas <aplazas src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [retro-gtk/wip/aplazas/bml] Add RetroBml
- Date: Fri, 16 Nov 2018 15:39:57 +0000 (UTC)
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]