2024-08-15 00:54:31 +00:00
|
|
|
/**
|
|
|
|
* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET
|
|
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
|
|
*
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
*/
|
|
|
|
#include <assert.h>
|
|
|
|
#include <ctype.h>
|
|
|
|
#include <stdbool.h>
|
|
|
|
#include <stdint.h>
|
2024-10-02 17:40:38 +00:00
|
|
|
#include <stdlib.h>
|
2024-08-15 00:54:31 +00:00
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
#include "xml.h"
|
|
|
|
#include "util.h"
|
|
|
|
|
|
|
|
#define copy_out(out, v) memcpy((out), &(v), sizeof(v));
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse a string as an integer.
|
|
|
|
*
|
|
|
|
* The expected value matches case-insensitive regex `^\s*[-+]?(0x[0-9A-F]+|[0-9]+)\s*$`.
|
|
|
|
* The value may be base 10, or base 16 with a (case-insensitive) 0x prefix.
|
|
|
|
* Leading and trailing whitespace is ignored.
|
|
|
|
*/
|
|
|
|
static int
|
|
|
|
xml_str_to_int(const char *value)
|
|
|
|
{
|
|
|
|
if (value == NULL || value[0] == '\0')
|
|
|
|
goto err;
|
|
|
|
|
|
|
|
bool neg = false;
|
|
|
|
int res;
|
|
|
|
|
|
|
|
size_t value_len = strlen(value);
|
|
|
|
size_t start;
|
|
|
|
|
|
|
|
// consume initial whitespace
|
|
|
|
for (start = 0; start < value_len; start++) {
|
|
|
|
if (!isspace(value[start]))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// if we consumed the whole string, it was bad
|
|
|
|
if (start == value_len)
|
|
|
|
goto err;
|
|
|
|
|
|
|
|
// handle sign character if present
|
|
|
|
if (value[start] == '+' || value[start] == '-') {
|
|
|
|
neg = value[start] == '-';
|
|
|
|
start++;
|
|
|
|
}
|
|
|
|
|
|
|
|
int base;
|
|
|
|
|
|
|
|
// get absolute value in either base 10 or 16
|
|
|
|
if (start + 2 < value_len && value[start + 0] == '0' && tolower(value[start + 1]) == 'x') {
|
|
|
|
start += 2;
|
|
|
|
base = 16;
|
|
|
|
} else {
|
|
|
|
base = 10;
|
|
|
|
}
|
|
|
|
|
|
|
|
char *str_end;
|
|
|
|
res = strtol(&value[start], &str_end, base);
|
|
|
|
size_t end = str_end - value;
|
|
|
|
if (start == end)
|
|
|
|
goto err;
|
|
|
|
|
|
|
|
// consume trailing whitespace
|
|
|
|
while (value[end] != '\0') {
|
|
|
|
if (!isspace(value[end]))
|
|
|
|
goto err;
|
|
|
|
end++;
|
|
|
|
}
|
|
|
|
|
|
|
|
assert(end == value_len);
|
|
|
|
|
|
|
|
// apply sign
|
|
|
|
return neg ? -res : res;
|
|
|
|
err:
|
|
|
|
error("bad int value %s", value);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_int(const char *value, void *out)
|
|
|
|
{
|
|
|
|
int v = xml_str_to_int(value);
|
|
|
|
|
|
|
|
copy_out(out, v);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_uint(const char *value, void *out)
|
|
|
|
{
|
|
|
|
int v = xml_str_to_int(value);
|
|
|
|
if (v < 0)
|
|
|
|
error("Value should be unsigned");
|
|
|
|
|
|
|
|
copy_out(out, v);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_s16(const char *value, void *out)
|
|
|
|
{
|
|
|
|
int v = xml_str_to_int(value);
|
|
|
|
if (v < INT16_MIN || v > INT16_MAX)
|
|
|
|
error("Value %d out of range for s16", v);
|
|
|
|
int16_t vs16 = v;
|
|
|
|
|
|
|
|
copy_out(out, vs16);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_u8(const char *value, void *out)
|
|
|
|
{
|
|
|
|
int v = xml_str_to_int(value);
|
|
|
|
if (v < 0 || v > UINT8_MAX)
|
|
|
|
error("Value %d out of range for u8", v);
|
|
|
|
uint8_t vu8 = v;
|
|
|
|
|
|
|
|
copy_out(out, vu8);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_s8(const char *value, void *out)
|
|
|
|
{
|
|
|
|
int v = xml_str_to_int(value);
|
|
|
|
if (v < INT8_MIN || v > INT8_MAX)
|
|
|
|
error("Value %d out of range for s8", v);
|
|
|
|
int8_t vs8 = v;
|
|
|
|
|
|
|
|
copy_out(out, vs8);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse a note number name to its s8 [0;127] value.
|
|
|
|
* For example "PITCH_EF4" -> 42.
|
|
|
|
*/
|
|
|
|
void
|
|
|
|
xml_parse_note_number(const char *value, void *out)
|
|
|
|
{
|
|
|
|
size_t value_len = strlen(value);
|
|
|
|
int8_t vs8;
|
|
|
|
char c;
|
|
|
|
|
|
|
|
if (value_len == 0)
|
|
|
|
goto err;
|
|
|
|
|
|
|
|
// consume initial whitespace
|
|
|
|
size_t start;
|
|
|
|
for (start = 0; start < value_len; start++) {
|
|
|
|
if (!isspace(value[start]))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// if we consumed the whole string, it was bad
|
|
|
|
if (start == value_len)
|
|
|
|
goto err;
|
|
|
|
|
|
|
|
c = toupper(value[start]);
|
|
|
|
|
|
|
|
if (c >= 'A' && c <= 'G') {
|
|
|
|
start++;
|
|
|
|
|
|
|
|
// got a note number
|
|
|
|
static const int8_t notes_lut[] = {
|
|
|
|
/* A */ 12,
|
|
|
|
/* B */ 14,
|
|
|
|
/* C */ 3,
|
|
|
|
/* D */ 5,
|
|
|
|
/* E */ 7,
|
|
|
|
/* F */ 8,
|
|
|
|
/* G */ 10,
|
|
|
|
};
|
|
|
|
char cm = toupper(value[start]);
|
|
|
|
int mod = 0;
|
|
|
|
int v;
|
|
|
|
|
|
|
|
if (value_len > start && (cm == 'F' || cm == 'S')) {
|
|
|
|
// handle flat/sharp modifier
|
|
|
|
mod = (cm == 'S') ? 1 : -1;
|
|
|
|
start++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (start == value_len)
|
|
|
|
goto err;
|
|
|
|
|
|
|
|
bool neg = false;
|
|
|
|
int res;
|
|
|
|
|
|
|
|
// if value starts with NEG (ignoring case)
|
|
|
|
if (start + 3 <= value_len && toupper(value[start + 0]) == 'N' && toupper(value[start + 1]) == 'E' &&
|
|
|
|
toupper(value[start + 2]) == 'G') {
|
|
|
|
neg = true;
|
|
|
|
start += 3;
|
|
|
|
}
|
|
|
|
|
|
|
|
int base = 10;
|
|
|
|
|
|
|
|
char *str_end;
|
|
|
|
res = strtol(&value[start], &str_end, base);
|
|
|
|
size_t end = str_end - value;
|
|
|
|
if (start == end)
|
|
|
|
goto err;
|
|
|
|
|
|
|
|
// consume trailing whitespace
|
|
|
|
while (value[end] != '\0') {
|
|
|
|
if (!isspace(value[end]))
|
|
|
|
goto err;
|
|
|
|
end++;
|
|
|
|
}
|
|
|
|
|
|
|
|
assert(end == value_len);
|
|
|
|
|
|
|
|
// apply sign
|
|
|
|
v = neg ? -res : res;
|
|
|
|
|
|
|
|
if (v < -1 || v > 10)
|
|
|
|
error("Value %d out of range for note number", v);
|
|
|
|
|
|
|
|
vs8 = (v - 1) * 12 + notes_lut[c - 'A'] + mod;
|
|
|
|
if (vs8 < 0)
|
|
|
|
vs8 += 128;
|
|
|
|
} else { // got a raw value
|
|
|
|
vs8 = xml_str_to_int(&value[start]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (vs8 < 0)
|
|
|
|
goto err;
|
|
|
|
|
|
|
|
copy_out(out, vs8);
|
|
|
|
return;
|
|
|
|
err:
|
|
|
|
error("Invalid note %s", value);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_string(const char *value, void *out)
|
|
|
|
{
|
|
|
|
// copies only the pointer to the string
|
|
|
|
copy_out(out, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_c_identifier(const char *value, void *out)
|
|
|
|
{
|
|
|
|
if (!str_is_c_identifier(value))
|
|
|
|
error("Input %s is not a valid C Language identifier", value);
|
|
|
|
|
|
|
|
// copies only the pointer to the string
|
|
|
|
copy_out(out, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_bool(const char *value, void *out)
|
|
|
|
{
|
|
|
|
bool v;
|
|
|
|
|
|
|
|
// TODO make case-insensitive
|
|
|
|
if (strequ(value, "true"))
|
|
|
|
v = true;
|
|
|
|
else if (strequ(value, "false"))
|
|
|
|
v = false;
|
|
|
|
else
|
|
|
|
error("Invalid value %s for bool", value);
|
|
|
|
|
|
|
|
copy_out(out, v);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_float(const char *value, void *out)
|
|
|
|
{
|
|
|
|
char *end;
|
|
|
|
float v = strtof(value, &end);
|
|
|
|
|
|
|
|
if (value == end)
|
|
|
|
error("Invalid value %s for float", value);
|
|
|
|
if (v < 0.0f)
|
|
|
|
error("Only positive floats are allowed");
|
|
|
|
|
|
|
|
copy_out(out, v);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_double(const char *value, void *out)
|
|
|
|
{
|
|
|
|
char *end;
|
|
|
|
double v = strtod(value, &end);
|
|
|
|
|
|
|
|
if (value == end)
|
|
|
|
error("Invalid value %s for double", value);
|
|
|
|
if (v < 0.0)
|
|
|
|
error("Only positive doubles are allowed");
|
|
|
|
|
|
|
|
copy_out(out, v);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_get_single_property(void *out, const xmlNodePtr node, const char *name, xml_parser_func parser)
|
|
|
|
{
|
|
|
|
xmlAttrPtr attr = node->properties;
|
|
|
|
|
|
|
|
if (attr == NULL || attr->next != NULL)
|
|
|
|
error("Expected only property %s on line %d", name, node->line);
|
|
|
|
|
|
|
|
const char *prop_name = XMLSTR_TO_STR(attr->name);
|
|
|
|
|
|
|
|
if (!strequ(prop_name, name))
|
|
|
|
error("Unexpected attribute on line %d: got: \"%s\", expected: \"%s\"", node->line, prop_name, name);
|
|
|
|
|
|
|
|
xmlChar *xvalue = xmlNodeListGetString(node->doc, attr->children, 1);
|
|
|
|
const char *value = XMLSTR_TO_STR(xvalue);
|
|
|
|
|
|
|
|
parser(value, (uint8_t *)out);
|
|
|
|
|
|
|
|
if (parser != xml_parse_string && parser != xml_parse_c_identifier)
|
|
|
|
free(xvalue);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_parse_node_by_spec(void *out, const xmlNodePtr node, const xml_attr_spec spec, size_t spec_length)
|
|
|
|
{
|
|
|
|
bool *got = alloca(spec_length * sizeof(bool));
|
|
|
|
memset(got, false, spec_length * sizeof(bool));
|
|
|
|
|
|
|
|
LL_FOREACH(xmlAttrPtr, attr, node->properties) {
|
|
|
|
const char *name = XMLSTR_TO_STR(attr->name);
|
|
|
|
bool found = false;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < spec_length; i++) {
|
|
|
|
if (strequ(name, spec[i].name)) {
|
|
|
|
// strictly speaking this pointer needs to be freed but we may want to save the string itself
|
|
|
|
// so we just don't free it and let it clean up when the program terminates
|
|
|
|
xmlChar *xvalue = xmlNodeListGetString(node->doc, attr->children, 1);
|
|
|
|
const char *value = XMLSTR_TO_STR(xvalue);
|
|
|
|
|
|
|
|
spec[i].parser_func(value, (uint8_t *)out + spec[i].offset);
|
|
|
|
|
|
|
|
if (spec[i].parser_func != xml_parse_string && spec[i].parser_func != xml_parse_c_identifier)
|
|
|
|
free(xvalue); // free when we don't need the string in the future, TODO strdup when we do need it?
|
|
|
|
|
|
|
|
got[i] = true;
|
|
|
|
found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!found)
|
|
|
|
error("Unrecognized attribute %s on line %d", name, node->line);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (size_t i = 0; i < spec_length; i++) {
|
|
|
|
if (!spec[i].optional && !got[i])
|
|
|
|
error("Expected a %s attribute on line %d", spec[i].name, node->line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
xml_print_rec(xmlNodePtr base, int *pIndent)
|
|
|
|
{
|
|
|
|
LL_FOREACH(xmlNodePtr, node, base) {
|
|
|
|
if (node->type != XML_ELEMENT_NODE)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
fprintf(stdout, "%*cChild is <%s> (%i)\n", *pIndent, ' ', node->name, node->type);
|
|
|
|
*pIndent += 4;
|
|
|
|
|
|
|
|
LL_FOREACH(xmlAttrPtr, attr, node->properties) {
|
|
|
|
xmlChar *value = xmlNodeListGetString(node->doc, attr->children, 1);
|
|
|
|
fprintf(stdout, "%*c- Property <%s> \"%s\"\n", *pIndent, ' ', attr->name, XMLSTR_TO_STR(value));
|
|
|
|
free(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
xml_print_rec(node->children, pIndent);
|
|
|
|
*pIndent -= 4;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
xml_print_tree(xmlDocPtr document)
|
|
|
|
{
|
|
|
|
xmlNodePtr root = xmlDocGetRootElement(document);
|
|
|
|
int indent = 4;
|
|
|
|
fprintf(stdout, "Root is <%s> (%i)\n", root->name, root->type);
|
|
|
|
|
|
|
|
LL_FOREACH(xmlAttrPtr, attr, root->properties) {
|
|
|
|
xmlChar *value = xmlNodeListGetString(root->doc, attr->children, 1);
|
|
|
|
fprintf(stdout, "%*c- Property <%s> \"%s\"\n", indent, ' ', attr->name, XMLSTR_TO_STR(value));
|
|
|
|
free(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
xml_print_rec(root->children, &indent);
|
|
|
|
}
|