/*
* 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
{
public errordomain JsonSyntaxError
{
BAD_TOKEN,
BAD_VALUE,
BAD_STRING,
BAD_COMMENT
}
public errordomain JsonWriteError
{
BAD_VALUE,
FILE_OPEN,
FILE_WRITE
}
///
/// Provides functions for encoding and decoding files in the JSON format.
///
[Compact]
public class JSON
{
///
/// Encodes the hashtable t in the JSON format. The hash table can
/// contain, numbers, bools, strings, ArrayLists and Hashtables.
///
public static string encode(Value? t) throws JsonWriteError
{
StringBuilder sb = new StringBuilder();
write(t, sb, 0);
sb.append_c('\n');
return sb.str;
}
///
/// Decodes a JSON bytestream into a hash table with numbers, bools, strings,
/// ArrayLists and Hashtables.
///
public static Value? decode(uint8[] sjson) throws JsonSyntaxError
{
int index = 0;
return parse(sjson, ref index);
}
///
/// Convenience function for loading a file.
///
public static Hashtable load(string path) throws JsonSyntaxError
{
FileStream fs = FileStream.open(path, "rb");
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 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_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(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) {
uint8 c = s[i];
if (c == '"' || c == '\\') {
builder.append_c('\\');
builder.append_c((char)c);
} else if (c == '\n') {
builder.append_c('\\');
builder.append_c('n');
} else {
builder.append_c((char)c);
}
}
builder.append_c('"');
}
static void write_array(Gee.ArrayList a, StringBuilder builder, int indentation) throws JsonWriteError
{
bool write_comma = false;
builder.append("[ ");
foreach (Value? item in a) {
if (write_comma)
builder.append(", ");
write(item, builder, indentation + 1);
write_comma = true;
}
builder.append("]");
}
static void write_object(Hashtable t, StringBuilder builder, int indentation) throws JsonWriteError
{
builder.append_c('{');
bool write_comma = false;
foreach (var de in t.entries) {
if (write_comma)
builder.append(", ");
write_new_line(builder, indentation);
write(de.key, builder, indentation);
builder.append(" : ");
write(de.value, builder, indentation);
write_comma = true;
}
write_new_line(builder, indentation);
builder.append_c('}');
}
static void skip_whitespace(uint8[] json, ref int index)
{
while (index < json.length) {
uint8 c = json[index];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',')
++index;
else
break;
}
}
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(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)
{
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_string(json, ref index);
consume(json, ref index, ":");
if (key.has_suffix("_binary"))
ht[key] = (Value?)parse_binary(json, ref index);
else
ht[key] = parse(json, ref index);
}
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(json, ref index);
a.add(value);
}
consume(json, ref index, "]");
return a;
}
static uint8[] parse_binary(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 s.to_array();
}
static string parse_string(uint8[] json, ref int index) throws JsonSyntaxError
{
return (string)parse_binary(json, ref index);
}
static double parse_number(uint8[] json, ref int index)
{
int end = index;
while ("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 */