/*
* Copyright (c) 2012-2026 Daniele Bartolini et al.
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/*
* Original C# code:
* Public Domain Niklas Frykholm
*/
namespace Crown
{
///
/// Provides functions for encoding and decoding files in the simplified JSON format.
///
[Compact]
public class SJSON
{
///
/// Encodes the Hashtable t in the simplified JSON format. The Hashtable can
/// contain, numbers, bools, strings, ArrayLists and Hashtables.
///
public static string encode(Hashtable t) throws JsonWriteError
{
StringBuilder sb = new StringBuilder();
write_root_object(t, sb);
sb.append_c('\n');
return sb.str;
}
///
/// Encodes the object o in the simplified JSON format (not as a root object).
///
public static string encode_object(Value? o) throws JsonWriteError
{
StringBuilder sb = new StringBuilder();
write(o, sb, 0);
return sb.str;
}
///
/// Decodes a SJSON bytestream into a Hashtable with numbers, bools, strings,
/// ArrayLists and Hashtables.
///
public static Hashtable decode(uint8[] sjson) throws JsonSyntaxError
{
int index = 0;
return parse_root_object(sjson, ref index);
}
///
/// Convenience function for loading a file.
///
public static Hashtable load_from_file(GLib.FileStream? fs) throws JsonSyntaxError
{
if (fs == null)
return new Hashtable();
// Get file size
fs.seek(0, FileSeek.END);
size_t size = fs.tell();
fs.rewind();
if (size == 0)
return new Hashtable();
// Read whole file
uint8[] bytes = new uint8[size];
size_t bytes_read = fs.read(bytes);
if (bytes_read != size)
return new Hashtable();
return decode(bytes) as Hashtable;
}
///
/// Convenience function for loading a file.
///
public static Hashtable load_from_path(string path) throws JsonSyntaxError
{
FileStream fs = FileStream.open(path, "rb");
return load_from_file(fs);
}
///
/// Convenience function for saving a file.
///
public static void save(Hashtable h, string path) throws JsonWriteError
{
FileStream fs = FileStream.open(path, "wb");
if (fs == null)
throw new JsonWriteError.FILE_OPEN("Unable to open '%s'".printf(path));
uint8[] data = encode(h).data;
size_t len = data.length;
if (fs.write(data) != len)
throw new JsonWriteError.FILE_WRITE("Error while writing '%s'".printf(path));
}
static void write_root_object(Hashtable t, StringBuilder builder) throws JsonWriteError
{
write_object_fields(t, builder, 0);
}
static void write_object_fields(Hashtable t, StringBuilder builder, int indentation) throws JsonWriteError
{
Gee.ArrayList keys = new Gee.ArrayList.wrap(t.keys.to_array());
keys.sort(Gee.Functions.get_compare_func_for(typeof(string)));
foreach (string key in keys) {
write_new_line(builder, indentation);
builder.append(key);
builder.append(" = ");
write(t[key], builder, indentation);
}
}
static void write_new_line(StringBuilder builder, int indentation)
{
if (builder.len > 0)
builder.append_c('\n');
for (int i = 0; i < indentation; ++i)
builder.append_c('\t');
}
static void write(Value? o, StringBuilder builder, int indentation) throws JsonWriteError
{
if (o == null)
builder.append("null");
else if (o.holds(typeof(bool)) && (bool)o == false)
builder.append("false");
else if (o.holds(typeof(bool)))
builder.append("true");
else if (o.holds(typeof(uint8)))
builder.append_printf("%u", (uint8)o);
else if (o.holds(typeof(int)))
builder.append_printf("%d", (int)o);
else if (o.holds(typeof(float)))
builder.append_printf("%.9g", (float)o);
else if (o.holds(typeof(double)))
builder.append_printf("%.17g", (double)o);
else if (o.holds(typeof(string)))
write_string((string)o, builder);
else if (o.holds(typeof(Gee.ArrayList)))
write_array((Gee.ArrayList)o, builder, indentation);
else if (o.holds(typeof(Hashtable)))
write_object((Hashtable)o, builder, indentation);
else
throw new JsonWriteError.BAD_VALUE("Unsupported value type '%s'".printf(o.type_name()));
}
static void write_string(string s, StringBuilder builder)
{
builder.append_c('"');
for (int i = 0; i < s.length; ++i) {
char c = s[i];
if (c == '"' || c == '\\')
builder.append_c('\\');
builder.append_c(c);
}
builder.append_c('"');
}
static void write_array(Gee.ArrayList a, StringBuilder builder, int indentation) throws JsonWriteError
{
Gee.ArrayList a_sorted = a;
a_sorted.sort((a, b) => {
if (!a.holds(typeof(Hashtable)) || !b.holds(typeof(Hashtable)))
return 0;
Hashtable obj_a = a as Hashtable;
Hashtable obj_b = b as Hashtable;
string guid_a_str;
string guid_b_str;
if (obj_a.has_key("id")) {
Value? val = obj_a["id"];
if (val.holds(typeof(string)))
guid_a_str = (string)val;
else
// The 'id' key has been used for something else than a Guid. Font files are
// an example of the 'id' key used to store the codepoint of a glyph.
return 0;
} else if (obj_a.has_key("_guid")) {
guid_a_str = (string)obj_a["_guid"];
} else {
return 0;
}
if (obj_b.has_key("id")) {
Value? val = obj_b["id"];
if (val.holds(typeof(string)))
guid_b_str = (string)val;
else
return 0; // See comment above.
} else if (obj_b.has_key("_guid")) {
guid_b_str = (string)obj_b["_guid"];
} else {
return 0;
}
Guid guid_a = Guid.parse(guid_a_str);
Guid guid_b = Guid.parse(guid_b_str);
return Guid.compare_func(guid_a, guid_b);
});
builder.append_c('[');
foreach (Value? item in a_sorted) {
write_new_line(builder, indentation + 1);
write(item, builder, indentation + 1);
}
write_new_line(builder, indentation);
builder.append_c(']');
}
static void write_object(Hashtable t, StringBuilder builder, int indentation) throws JsonWriteError
{
builder.append_c('{');
write_object_fields(t, builder, indentation + 1);
write_new_line(builder, indentation);
builder.append_c('}');
}
static Hashtable parse_root_object(uint8 [] json, ref int index) throws JsonSyntaxError
{
Hashtable ht = new Hashtable();
while (!at_end(json, ref index)) {
string key = parse_identifier(json, ref index);
consume(json, ref index, "=");
Value? value = parse_value(json, ref index);
ht[key] = value;
}
return ht;
}
static bool at_end(uint8 [] json, ref int index) throws JsonSyntaxError
{
skip_whitespace(json, ref index);
return index >= json.length;
}
static void skip_whitespace(uint8 [] json, ref int index) throws JsonSyntaxError
{
while (index < json.length) {
uint8 c = json[index];
if (c == '/')
skip_comment(json, ref index);
else if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',')
++index;
else
break;
}
}
static void skip_comment(uint8 [] json, ref int index) throws JsonSyntaxError
{
uint8 next = json[index + 1];
if (next == '/') {
while (index + 1 < json.length && json[index] != '\n')
++index;
++index;
} else if (next == '*') {
while (index + 2 < json.length && (json[index] != '*' || json[index + 1] != '/'))
++index;
index += 2;
} else {
throw new JsonSyntaxError.BAD_COMMENT("Bad comment");
}
}
static string parse_identifier(uint8 [] json, ref int index) throws JsonSyntaxError
{
skip_whitespace(json, ref index);
if (json[index] == '"')
return parse_string(json, ref index);
Gee.ArrayList s = new Gee.ArrayList();
while (true) {
uint8 c = json[index];
if (c == ' ' || c == '\t' || c == '\n' || c == '=')
break;
s.add(c);
++index;
}
s.add('\0');
return (string)s.to_array();
}
static void consume(uint8 [] json, ref int index, string consume) throws JsonSyntaxError
{
skip_whitespace(json, ref index);
for (int i = 0; i < consume.length; ++i) {
if (json[index] != consume[i])
throw new JsonSyntaxError.BAD_TOKEN("Expected '%c' got '%c'".printf(consume[i], json[index]));
++index;
}
}
static Value? parse_value(uint8 [] json, ref int index) throws JsonSyntaxError
{
uint8 c = next(json, ref index);
if (c == '{') {
return parse_object(json, ref index);
} else if (c == '[') {
return parse_array(json, ref index);
} else if (c == '"') {
return parse_string(json, ref index);
} else if (c == '-' || c >= '0' && c <= '9') {
return parse_number(json, ref index);
} else if (c == 't') {
consume(json, ref index, "true");
return true;
} else if (c == 'f') {
consume(json, ref index, "false");
return false;
} else if (c == 'n') {
consume(json, ref index, "null");
return null;
} else {
throw new JsonSyntaxError.BAD_VALUE("Bad value");
}
}
static uint8 next(uint8 [] json, ref int index) throws JsonSyntaxError
{
skip_whitespace(json, ref index);
return json[index];
}
static Hashtable parse_object(uint8 [] json, ref int index) throws JsonSyntaxError
{
Hashtable ht = new Hashtable();
consume(json, ref index, "{");
skip_whitespace(json, ref index);
while (next(json, ref index) != '}') {
string key = parse_identifier(json, ref index);
consume(json, ref index, "=");
Value? value = parse_value(json, ref index);
ht[key] = value;
}
consume(json, ref index, "}");
return ht;
}
static Gee.ArrayList parse_array(uint8 [] json, ref int index) throws JsonSyntaxError
{
Gee.ArrayList a = new Gee.ArrayList();
consume(json, ref index, "[");
while (next(json, ref index) != ']') {
Value? value = parse_value(json, ref index);
a.add(value);
}
consume(json, ref index, "]");
return a;
}
static string parse_string(uint8[] json, ref int index) throws JsonSyntaxError
{
Gee.ArrayList s = new Gee.ArrayList();
consume(json, ref index, "\"");
while (true) {
uint8 c = json[index];
++index;
if (c == '"') {
break;
} else if (c != '\\') {
s.add(c);
} else {
uint8 q = json[index];
++index;
if (q == '"' || q == '\\' || q == '/')
s.add(q);
else if (q == 'b')
s.add('\b');
else if (q == 'f')
s.add('\f');
else if (q == 'n')
s.add('\n');
else if (q == 'r')
s.add('\r');
else if (q == 't')
s.add('\t');
else if (q == 'u')
throw new JsonSyntaxError.BAD_STRING("Unsupported escape character 'u'");
else
throw new JsonSyntaxError.BAD_STRING("Bad string");
}
}
s.add('\0');
return (string)s.to_array();
}
static double parse_number(uint8[] json, ref int index)
{
int end = index;
while (end < json.length && "0123456789+-.eE".index_of_char((char)json[end]) != -1)
++end;
uint8[] num = json[index : end];
num += '\0';
index = end;
return double.parse((string)num);
}
}
} /* namespace Crown */