...
 
Commits (1)
  • Adrien Plazas's avatar
    Add RetroBml · cddcbdae
    Adrien Plazas authored
    This allows to parse the BML file format used by the Higan shader
    format.
    cddcbdae
......@@ -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',
......
// 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
// 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)
......@@ -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
......
/* 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();
}
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/Retro/Tests/RetroBml">
<file>test.bml</file>
</gresource>
</gresources>
// 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.