add config support

This commit is contained in:
2026-07-30 13:18:41 +02:00
parent 215ed9705e
commit dc1aa5e5b6
124 changed files with 7402 additions and 8 deletions
+35
View File
@@ -0,0 +1,35 @@
.NOTPARALLEL:
# disable merge tests for now
DIRS := scankey scanvalue parser merge cpp stdtest source
BUILDDIRS = $(DIRS:%=build-%)
CLEANDIRS = $(DIRS:%=clean-%)
FORMATDIRS = $(DIRS:%=format-%)
TESTDIRS = $(DIRS:%=test-%)
all: $(BUILDDIRS)
$(BUILDDIRS):
$(MAKE) -C $(@:build-%=%)
test: $(TESTDIRS)
format: $(FORMATDIRS)
clean: $(CLEANDIRS)
$(CLEANDIRS):
$(MAKE) -C $(@:clean-%=%) clean
$(FORMATDIRS):
$(MAKE) -C $(@:format-%=%) format
$(TESTDIRS):
$(MAKE) -C $(@:test-%=%) test
.PHONY: $(DIRS) $(BUILDDIRS) $(CLEANDIRS) $(FORMATDIRS)
.PHONY: all install format test
+5
View File
@@ -0,0 +1,5 @@
scankey : test scanner on keys
scanvalue : test scanner on values
parser : test parser
merge : test the toml_merge function
stdtest : the official regression tests
@@ -0,0 +1 @@
/test1
@@ -0,0 +1,49 @@
CFLAGS = -std=c17 -fsanitize=address,undefined -Wmissing-declarations -Wall -Wextra -MMD
EXEC = test1
ifdef DEBUG
CFLAGS += -O0 -g
else
CFLAGS += -O3 -DNDEBUG
endif
CXXFLAGS = $(subst -std=c17,-std=c++20,$(CFLAGS))
# Test for C++20 support
CXX20_SUPPORT := $(shell echo "int main(){}" | $(CXX) -std=c++20 -x c++ -c - -o /dev/null 2>/dev/null && echo yes || echo no)
ifeq ($(CXX20_SUPPORT),yes)
all: $(EXEC)
test1: test1.cpp
$(CXX) $(CXXFLAGS) -o $@ $@.cpp -L../../src -ltomlc17
test: all
@echo
@echo =========================
@echo == cpp test
@echo =========================
./test1
else
all:
@echo "Skipped because C++20 not available"
test:
@echo "Skipped because C++20 not available"
endif
-include test1.d
clean:
rm -f *.o *.d $(EXEC)
distclean: clean
format:
clang-format -i *.cpp
.PHONY: all clean distclean format test
+121
View File
@@ -0,0 +1,121 @@
#include "../../src/tomlcpp.hpp"
#include <cstring>
#include <iostream>
static void failed() {
printf("FAILED\n");
exit(1);
}
#define CHECK(x) \
if (x) \
; \
else \
failed()
using namespace std::chrono;
using std::cout;
using std::endl;
static void test_string() {
printf("test string ...\n");
const char *doc = "title = \"Moby Dick\"";
auto result = toml::parse(doc);
CHECK(result.ok());
auto value = *(*result.toptab().get("title")).as_str();
CHECK(value == "Moby Dick");
}
static void test_int() {
printf("test int ...\n");
const char *doc = "count = 20";
auto result = toml::parse(doc);
CHECK(result.ok());
auto value = *(*result.toptab().get("count")).as_int();
CHECK(value == 20);
}
static void test_float() {
printf("test float ...\n");
const char *doc = "speed = 20.5";
auto result = toml::parse(doc);
CHECK(result.ok());
auto value = *(*result.toptab().get("speed")).as_real();
CHECK(value == 20.5);
}
static void test_boolean() {
printf("test boolean ...\n");
const char *doc = "always = true";
auto result = toml::parse(doc);
CHECK(result.ok());
auto value = *(*result.toptab().get("always")).as_bool();
CHECK(value == true);
}
static void test_date() {
printf("test date ...\n");
const char *doc = "christmas = 2025-12-25";
auto result = toml::parse(doc);
CHECK(result.ok());
auto value = *(*result.toptab().get("christmas")).as_date();
CHECK(value == December / 25 / 2025);
}
static void test_time() {
printf("test time ...\n");
const char *doc = "noon = 12:00:00";
auto result = toml::parse(doc);
CHECK(result.ok());
auto value = *(*result.toptab().get("noon")).as_time();
CHECK(value.hours() == 12h && value.minutes() == 0min &&
value.seconds() == 0s);
}
static void test_datetime() {
printf("test datetime ...\n");
const char *doc = "now = 2025-06-01 11:15:00.123";
auto result = toml::parse(doc);
CHECK(result.ok());
auto value = *(*result.toptab().get("now")).as_datetime();
auto ymd = year_month_day{floor<days>(value)};
CHECK(ymd.year() == year{2025} && ymd.month() == month{6} &&
ymd.day() == day{1});
auto tod = hh_mm_ss<microseconds>{value - sys_days{ymd}};
CHECK(tod.hours() == hours(11) && tod.minutes() == minutes(15) &&
tod.seconds() == seconds(0) &&
tod.subseconds() == microseconds(123000));
}
static void test_datetimetz() {
printf("test datetimetz ...\n");
const char *doc = "now = 2025-06-01 11:15:00.123-05:00";
auto result = toml::parse(doc);
CHECK(result.ok());
auto valuetz = *(*result.toptab().get("now")).as_datetimetz();
auto [value, tzoff] = valuetz;
auto ymd = year_month_day{floor<days>(value)};
CHECK(ymd.year() == year{2025} && ymd.month() == month{6} &&
ymd.day() == day{1});
auto tod = hh_mm_ss<microseconds>{value - sys_days{ymd}};
CHECK(tod.hours() == hours(11) && tod.minutes() == minutes(15) &&
tod.seconds() == seconds(0) &&
tod.subseconds() == microseconds(123000));
CHECK(tzoff == -5 * 60);
}
static void test_array() {
printf("test array ...\n");
const char *doc = "array = [1,2,3]";
auto result = toml::parse(doc);
CHECK(result.ok());
auto value = *(*result.toptab().get("array")).as_intvec();
CHECK(value == (std::vector<int64_t>{1, 2, 3}));
}
int main() {
test_string();
test_int();
test_float();
test_boolean();
test_date();
test_time();
test_datetime();
test_datetimetz();
test_array();
return 0;
}
@@ -0,0 +1 @@
/test1
@@ -0,0 +1,27 @@
CFLAGS := -O0 -g -fsanitize=address,undefined -std=c17 -Wmissing-declarations -Wall -Wextra -MMD
EXEC = test1
all: $(EXEC)
test1: test1.c
$(CC) $(CFLAGS) -o $@ $@.c
test: all
@echo
@echo =========================
@echo == merge test
@echo =========================
./test1
-include test1.d
clean:
rm -f *.o *.d $(EXEC)
distclean: clean
format:
clang-format -i *.[ch]
.PHONY: all clean distclean format test
+200
View File
@@ -0,0 +1,200 @@
#include "../../src/tomlc17.c"
#include <inttypes.h>
static void failed() {
printf("FAILED\n");
exit(1);
}
#define CHECK(x) \
if (x) \
; \
else \
failed()
static void check(const char *doc1, const char *doc2, const char *expected) {
toml_result_t r1 = toml_parse(doc1, strlen(doc1));
toml_result_t r2 = toml_parse(doc2, strlen(doc2));
toml_result_t merged = toml_merge(&r1, &r2);
toml_result_t exp = toml_parse(expected, strlen(expected));
CHECK(toml_equiv(&merged, &exp));
toml_free(r1);
toml_free(r2);
toml_free(merged);
toml_free(exp);
}
// All test cases as separate functions
static void test_simple_merge() {
printf("Running test_simple_merge...\n");
const char *doc1 = "title = \"First\"";
const char *doc2 = "version = \"1.0\"";
const char *expected = "title = \"First\"\n"
"version = \"1.0\"";
check(doc1, doc2, expected);
}
static void test_overwrite_values() {
printf("Running test_overwrite_values...\n");
const char *doc1 = "title = \"First\"\n"
"version = \"0.9\"";
const char *doc2 = "version = \"1.0\"";
const char *expected = "title = \"First\"\n"
"version = \"1.0\"";
check(doc1, doc2, expected);
}
static void test_nested_tables() {
printf("Running test_nested_tables...\n");
const char *doc1 = "[owner]\n"
"name = \"Alice\"\n"
"dob = \"1979-05-27\"";
const char *doc2 = "[owner]\n"
"organization = \"ACME\"";
const char *expected = "[owner]\n"
"name = \"Alice\"\n"
"dob = \"1979-05-27\"\n"
"organization = \"ACME\"";
check(doc1, doc2, expected);
}
static void test_array_merging() {
printf("Running test_array_merging...\n");
const char *doc1 = "dependencies = [\"lib1\", \"lib2\"]";
const char *doc2 = "dependencies = [\"lib3\"]";
const char *expected = "dependencies = [\"lib3\"]";
check(doc1, doc2, expected);
}
static void test_deep_merge() {
printf("Running test_deep_merge...\n");
const char *doc1 = "[database]\n"
"server = \"localhost\"\n"
"ports = [8000, 8001]\n"
"connection_max = 5000";
const char *doc2 = "[database]\n"
"ports = [8001, 8002]\n"
"enabled = true";
const char *expected = "[database]\n"
"server = \"localhost\"\n"
"ports = [8001, 8002]\n"
"connection_max = 5000\n"
"enabled = true";
check(doc1, doc2, expected);
}
static void test_complex_merge() {
printf("Running test_complex_merge...\n");
const char *doc1 = "title = \"TOML Example\"\n"
"\n"
"[owner]\n"
"name = \"Tom Preston-Werner\"\n"
"dob = 1979-05-27T07:32:00Z\n"
"\n"
"[database]\n"
"server = \"192.168.1.1\"\n"
"ports = [8001, 8001, 8002]\n"
"connection_max = 5000\n"
"enabled = true";
const char *doc2 = "title = \"Updated TOML Example\"\n"
"\n"
"[owner]\n"
"organization = \"GitHub\"\n"
"\n"
"[database]\n"
"ports = [9000]\n"
"enabled = false\n"
"\n"
"[clients]\n"
"data = [[\"gamma\", \"delta\"], [1, 2]]";
const char *expected = "title = \"Updated TOML Example\"\n"
"\n"
"[owner]\n"
"name = \"Tom Preston-Werner\"\n"
"dob = 1979-05-27T07:32:00Z\n"
"organization = \"GitHub\"\n"
"\n"
"[database]\n"
"server = \"192.168.1.1\"\n"
"ports = [9000]\n"
"connection_max = 5000\n"
"enabled = false\n"
"\n"
"[clients]\n"
"data = [[\"gamma\", \"delta\"], [1, 2]]";
check(doc1, doc2, expected);
}
static void test_array_of_tables() {
printf("Running test_array_of_tables...\n");
const char *doc1 = "[[products]]\n"
"name = \"Hammer\"\n"
"sku = 738594937";
const char *doc2 = "[[products]]\n"
"color = \"red\"";
const char *expected = "[[products]]\n"
"name = \"Hammer\"\n"
"sku = 738594937\n"
"[[products]]\n"
"color = \"red\"";
check(doc1, doc2, expected);
}
static void test_type_conflicts() {
printf("Running test_type_conflicts...\n");
const char *doc1 = "value = 42";
const char *doc2 = "value = \"forty-two\"";
const char *expected = "value = \"forty-two\"";
check(doc1, doc2, expected);
}
static void test_empty_documents() {
printf("Running test_empty_documents...\n");
check("", "a = 1", "a = 1");
check("a = 1", "", "a = 1");
check("", "", "");
}
// Merged result must own its keys: tomlc17 requires each result be freed
// independently, so reading the merged tree after freeing the inputs must
// not touch the freed input pools.
static void test_keys_outlive_inputs() {
printf("Running test_keys_outlive_inputs...\n");
const char *doc1 = "[owner]\n"
"name = \"Alice\"\n";
const char *doc2 = "[owner]\n"
"organization = \"ACME\"\n";
toml_result_t r1 = toml_parse(doc1, strlen(doc1));
toml_result_t r2 = toml_parse(doc2, strlen(doc2));
toml_result_t merged = toml_merge(&r1, &r2);
CHECK(merged.ok);
toml_free(r1);
toml_free(r2);
toml_datum_t owner = toml_get(merged.toptab, "owner");
toml_datum_t name = toml_get(owner, "name");
toml_datum_t org = toml_get(owner, "organization");
CHECK(name.type == TOML_STRING && 0 == strcmp(name.u.str.ptr, "Alice"));
CHECK(org.type == TOML_STRING && 0 == strcmp(org.u.str.ptr, "ACME"));
toml_free(merged);
}
int main() {
test_simple_merge();
test_overwrite_values();
test_nested_tables();
test_array_merging();
test_deep_merge();
test_complex_merge();
test_array_of_tables();
test_type_conflicts();
test_empty_documents();
test_keys_outlive_inputs();
printf("All tests completed.\n");
return 0;
}
@@ -0,0 +1,2 @@
/driver
/out
@@ -0,0 +1,21 @@
CFLAGS := -O0 -g -fsanitize=address,undefined -std=c17 -Wmissing-declarations -Wall -Wextra -MMD
all: driver
driver: parser.c
$(CC) $(CFLAGS) -o $@ parser.c
test: all
bash run.sh
clean:
rm -f *.o *.d driver
distclean: clean
format:
clang-format -i *.[ch]
-include driver.d
.PHONY: all clean distclean format test
@@ -0,0 +1,7 @@
# This is a TOML document
"title" = "TOML Example"
name = "Aero"
weight = 40
species = "Beagle"
@@ -0,0 +1,10 @@
str1 = """
Roses are red
Violets are blue"""
# On a Unix system, the above multi-line string will most likely be the same as:
str2 = "Roses are red\nViolets are blue"
# On a Windows system, it will most likely be equivalent to:
str3 = "Roses are red\r\nViolets are blue"
@@ -0,0 +1,34 @@
# Configuration file
[main]
wayland_displays = [ "$WAYLAND_DISPLAY" ]
clipboards = [ "Default" ]
[default]
connection_timeout = 500
data_timeout = 500
max_entries = 100
max_entries_memory = 10
[wayland_displays."$WAYLAND_DISPLAY"]
connection_timeout = 500
data_timeout = 500
seats = [ "$XDG_SEAT" ]
[wayland_displays."$WAYLAND_DISPLAY"."$XDG_SEAT"]
clipboard = "Default"
regular = true
primary = false
[clipboards.Default]
max_entries = 10
max_entries_memory = 5
allowed_mime_types = [ "text/*", "image/*" ]
[[clipboards.Default.mime_type_groups]]
mime_type = "text/plain;charset=utf-8"
group = [ "TEXT", "STRING", "UTF8_STRING", "text/plain" ]
@@ -0,0 +1,7 @@
str4 = """Here are two quotation marks: "". Simple enough."""
# str5 = """Here are three quotation marks: """.""" # INVALID
str5 = """Here are three quotation marks: ""\"."""
str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\"."""
# "This," she said, "is just a pointless statement."
str7 = """"This," she said, "is just a pointless statement.""""
@@ -0,0 +1,5 @@
# What you see is what you get.
winpath = 'C:\Users\nodejs\templates'
winpath2 = '\\ServerX\admin$\system32\'
quoted = 'Tom "Dubs" Preston-Werner'
regex = '<\i\c*\s*>'
@@ -0,0 +1,7 @@
regex2 = '''I [dw]on't need \d{2} apples'''
lines = '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''
@@ -0,0 +1,8 @@
quot15 = '''Here are fifteen quotation marks: """""""""""""""'''
# apos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID
apos15 = "Here are fifteen apostrophes: '''''''''''''''"
# 'That,' she said, 'is still pointless.'
str = ''''That,' she said, 'is still pointless.''''
@@ -0,0 +1,15 @@
# The following strings are byte-for-byte equivalent:
str1 = "abc"
str2 = """
a\
b\
c"""
str3 = """\
a\
b\
c\
"""
@@ -0,0 +1,17 @@
# all results are the same
str1 = "abc"
str2 = """
a\
b\
c"""
str3 = """\
a\
b\
c\
"""
@@ -0,0 +1,9 @@
# line-ending backslash with whitespace "geeee\ "
# result should be "heeee\ngeeee"
str = """
heeee
geeee\
"""
@@ -0,0 +1,16 @@
# The following strings are byte-for-byte equivalent:
str1 = "The quick brown fox jumps over the lazy dog."
str2 = """
The quick brown \
\
fox jumps over \
the lazy dog."""
str3 = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""
@@ -0,0 +1,6 @@
arr = [
{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},
{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},
{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},{x=1},
{x=1}
]
@@ -0,0 +1,4 @@
key = "value"
bare_key = "value"
bare-key = "value"
1234 = "value"
@@ -0,0 +1,2 @@
# seconds are optional in v1.1
dt = 2025-12-25 15:30
@@ -0,0 +1 @@
str = "I'm a string. \"You can quote me\". Name\tJos\xE9\nLocation\tSF."
@@ -0,0 +1,10 @@
# RECOMMENDED
apple.type = "fruit"
apple.skin = "thin"
apple.color = "red"
orange.type = "fruit"
orange.skin = "thick"
orange.color = "orange"
@@ -0,0 +1 @@
3.14159 = "pi"
@@ -0,0 +1,20 @@
int1 = +99
int2 = 42
int3 = 0
int4 = -17
int5 = 1_000
int6 = 5_349_221
int7 = 53_49_221 # Indian number system grouping
int8 = 1_2_3_4_5 # VALID but discouraged
# hexadecimal with prefix `0x`
hex1 = 0xDEADBEEF
hex2 = 0xdeadbeef
hex3 = 0xdead_beef
# octal with prefix `0o`
oct1 = 0o01234567
oct2 = 0o755 # useful for Unix file permissions
# binary with prefix `0b`
bin1 = 0b11010110
@@ -0,0 +1,24 @@
# fractional
flt1 = +1.0
flt2 = 3.1415
flt3 = -0.01
# exponent
flt4 = 5e+22
flt5 = 1e06
flt6 = -2E-2
# both
flt7 = 6.626e-34
flt8 = 224_617.445_991_228
# infinity
sf1 = inf # positive infinity
sf2 = +inf # positive infinity
sf3 = -inf # negative infinity
# not a number
sf4 = nan # actual sNaN/qNaN encoding is implementation-specific
sf5 = +nan # same as `nan`
sf6 = -nan # valid, actual encoding is implementation-specific
@@ -0,0 +1,2 @@
bool1 = true
bool2 = false
@@ -0,0 +1,11 @@
odt1 = 1979-05-27T07:32:00Z
odt2 = 1979-05-27T00:32:00-07:00
odt3 = 1979-05-27T00:32:00.999999-07:00
odt4 = 1979-05-27 07:32:00Z
ldt1 = 1979-05-27T07:32:00
ldt2 = 1979-05-27T00:32:00.999999
ld1 = 1979-05-27
lt1 = 07:32:00
lt2 = 00:32:00.999999
lower = 1987-07-05t17:45:00z
@@ -0,0 +1,13 @@
integers = [ 1, 2, 3 ]
colors = [ "red", "yellow", "green" ]
nested_arrays_of_ints = [ [ 1, 2 ], [3, 4, 5] ]
nested_mixed_array = [ [ 1, 2 ], ["a", "b", "c"] ]
string_array = [ "all", 'strings', """are the same""", '''type''' ]
# Mixed-type arrays are allowed
numbers = [ 0.1, 0.2, 0.5, 1.0, 2, 5 ]
contributors = [
"Foo Bar <foo@example.com>",
{ name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }
]
@@ -0,0 +1,8 @@
integers2 = [
1, 2, 3
]
integers3 = [
1,
2, # this is ok
]
@@ -0,0 +1,102 @@
a = [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100]
@@ -0,0 +1,9 @@
integers2 = [
1, 2, 3
]
integers3 = [
1,
2, # this is ok
]
@@ -0,0 +1,3 @@
[fruit]
[fruit]
@@ -0,0 +1,5 @@
# 30-left-brackets is okay
key1 = [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1,2]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
# 31-left-brackets causes stackoverflow
key2 = [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1,2]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
@@ -0,0 +1,2 @@
# large month integer overflows
date = 2026-10000000000-01
@@ -0,0 +1,11 @@
[table-1]
key1 = "some string"
key2 = 123
[table-2]
key1 = "another string"
key2 = 456
[dog."tater.man"]
type.name = "pug"
@@ -0,0 +1,6 @@
[a.b.c] # this is best practice
[ d.e.f ] # same as [d.e.f]
[ g . h . i ] # same as [g.h.i]
[ j . "ʞ" . 'l' ] # same as [j."ʞ".'l']
@@ -0,0 +1,11 @@
# [x] you
# [x.y] don't
# [x.y.z] need these
[x.y.z.w] # for this to work
fruit = "apple"
[x] # defining a super-table afterward is ok
country = "usa"
@@ -0,0 +1,9 @@
# Top-level table begins.
name = "Fido"
breed = "pug"
# Top-level table ends.
[owner]
name = "Regina Dogman"
member_since = 1999-08-04
@@ -0,0 +1,4 @@
name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }
animal = { type.name = "pug" }
@@ -0,0 +1,11 @@
[[products]]
name = "Hammer"
sku = 738594937
[[products]] # empty table within the array
[[products]]
name = "Nail"
sku = 284758393
color = "gray"
@@ -0,0 +1,100 @@
a0 = 0
a1 = 1
a2 = 2
a3 = 3
a4 = 4
a5 = 5
a6 = 6
a7 = 7
a8 = 8
a9 = 9
a10 = 10
a11 = 11
a12 = 12
a13 = 13
a14 = 14
a15 = 15
a16 = 16
a17 = 17
a18 = 18
a19 = 19
a20 = 20
a21 = 21
a22 = 22
a23 = 23
a24 = 24
a25 = 25
a26 = 26
a27 = 27
a28 = 28
a29 = 29
a30 = 30
a31 = 31
a32 = 32
a33 = 33
a34 = 34
a35 = 35
a36 = 36
a37 = 37
a38 = 38
a39 = 39
a40 = 40
a41 = 41
a42 = 42
a43 = 43
a44 = 44
a45 = 45
a46 = 46
a47 = 47
a48 = 48
a49 = 49
a50 = 50
a51 = 51
a52 = 52
a53 = 53
a54 = 54
a55 = 55
a56 = 56
a57 = 57
a58 = 58
a59 = 59
a60 = 60
a61 = 61
a62 = 62
a63 = 63
a64 = 64
a65 = 65
a66 = 66
a67 = 67
a68 = 68
a69 = 69
a70 = 70
a71 = 71
a72 = 72
a73 = 73
a74 = 74
a75 = 75
a76 = 76
a77 = 77
a78 = 78
a79 = 79
a80 = 80
a81 = 81
a82 = 82
a83 = 83
a84 = 84
a85 = 85
a86 = 86
a87 = 87
a88 = 88
a89 = 89
a90 = 90
a91 = 91
a92 = 92
a93 = 93
a94 = 94
a95 = 95
a96 = 96
a97 = 97
a98 = 98
a99 = 99
@@ -0,0 +1,14 @@
name = { first = "Tom", last = "Preston-Werner" }
point = {x=1, y=2}
animal = { type.name = "pug" }
contact = {
personal = {
name = "Donald Duck",
email = "donald@duckburg.com",
},
work = {
name = "Coin cleaner",
email = "donald@ScroogeCorp.com",
},
}
@@ -0,0 +1,7 @@
[[arr]]
[arr.subtab]
val=1
[[arr]]
[arr.subtab]
val=2
@@ -0,0 +1,9 @@
[a.b.c] # OK
answer = 42 # OK
[a] # OK - a was non-explicit and was created by std-table-expr
better = 43 # OK - a is now explicit
[t1] # OK
t2.t3.v = 0 # OK
[t1.t2] # should FAIL - t2 was non-explicit but was not created by std-table-expr
@@ -0,0 +1,3 @@
[[tab.arr]]
[tab]
arr.val1=1 # ERROR: arr was previously defined
@@ -0,0 +1,278 @@
#include "../../src/tomlc17.c"
#include <inttypes.h>
const char **g_argv = 0;
int g_argc = 0;
static void usage() {
fprintf(stderr, "Usage: %s [fname]\n", g_argv[0]);
exit(1);
}
// Print a string in json. Properly escape special chars.
static void print_string(const char *s, int len) {
printf("{\"type\": \"string\", \"value\": \"");
for (int i = 0; i < len; i++) {
int ch = s[i];
if (isprint(ch) && ch != '\"' && ch != '\\') {
putchar(ch);
continue;
}
switch (ch) {
case '\b':
putchar('\\');
putchar('b');
break; // Escape backspace
case '\t':
putchar('\\');
putchar('t');
break; // Escape tab
case '\n':
putchar('\\');
putchar('n');
break; // Escape newline
case '\f':
putchar('\\');
putchar('f');
break; // Escape formfeed
case '\r':
putchar('\\');
putchar('r');
break; // Escape carriage return
case '"':
putchar('\\');
putchar('"');
break; // Escape double quotes
case '\\':
putchar('\\');
putchar('\\');
break; // Escape backslash
default:
if (0 <= ch && ch < ' ') {
printf("\\u%04x", ch);
} else {
putchar(ch);
}
break;
}
}
printf("\"}");
}
// Print a key
static void print_key(const char *s, int len) {
putchar('"');
for (int i = 0; i < len; i++) {
int ch = s[i];
if (isprint(ch) && ch != '\"' && ch != '\\') {
putchar(ch);
continue;
}
switch (ch) {
case '"':
putchar('\\');
putchar('"');
break; // Escape double quotes
case '\\':
putchar('\\');
putchar('\\');
break; // Escape backslash
case '\b':
putchar('\\');
putchar('b');
break; // Escape backspace
case '\f':
putchar('\\');
putchar('f');
break; // Escape formfeed
case '\n':
putchar('\\');
putchar('n');
break; // Escape newline
case '\r':
putchar('\\');
putchar('r');
break; // Escape carriage return
case '\t':
putchar('\\');
putchar('t');
break; // Escape tab
default:
if (0 <= ch && ch < ' ') {
printf("\\u%04x", ch);
} else {
putchar(ch);
}
break;
}
}
putchar('"');
}
// Print a DATE datum
static void print_date(toml_datum_t datum) {
printf("{\"type\": \"date-local\", \"value\": \"%04d-%02d-%02d\"}",
datum.u.ts.year, datum.u.ts.month, datum.u.ts.day);
}
// Print a TIME datum
static void print_time(toml_datum_t datum) {
char fracstr[20];
fracstr[0] = fracstr[1] = '\0';
if (datum.u.ts.usec) {
double f = datum.u.ts.usec / 1000000.0;
snprintf(fracstr, sizeof(fracstr), "%.6f", f);
fracstr[5] = '\0'; // millisec precision
}
printf("{\"type\": \"time-local\", \"value\": \"%02d:%02d:%02d%s\"}",
datum.u.ts.hour, datum.u.ts.minute, datum.u.ts.second, fracstr + 1);
}
// Print a DATETIME datum
static void print_datetime(toml_datum_t datum) {
char fracstr[20];
fracstr[0] = fracstr[1] = '\0';
if (datum.u.ts.usec) {
double f = datum.u.ts.usec / 1000000.0;
snprintf(fracstr, sizeof(fracstr), "%.6f", f);
fracstr[5] = '\0'; // millisec precision
}
printf("{\"type\": \"datetime-local\", \"value\": \"%04d-%02d-%02d "
"%02d:%02d:%02d%s\"}",
datum.u.ts.year, datum.u.ts.month, datum.u.ts.day, datum.u.ts.hour,
datum.u.ts.minute, datum.u.ts.second, fracstr + 1);
}
// Print a DATETIMETZ datum
static void print_datetimetz(toml_datum_t datum) {
char fracstr[20];
char tzstr[20];
fracstr[0] = fracstr[1] = tzstr[0] = '\0';
if (datum.u.ts.usec) {
double f = datum.u.ts.usec / 1000000.0;
snprintf(fracstr, sizeof(fracstr), "%.6f", f);
fracstr[5] = '\0'; // millisec precision
}
int tz = datum.u.ts.tz;
char sign = tz < 0 ? '-' : '+';
tz = tz < 0 ? -tz : tz;
snprintf(tzstr, sizeof(tzstr), "%c%02d:%02d", sign, tz / 60, tz % 60);
printf("{\"type\": \"datetime\", \"value\": \"%04d-%02d-%02d "
"%02d:%02d:%02d%s%s\"}",
datum.u.ts.year, datum.u.ts.month, datum.u.ts.day, datum.u.ts.hour,
datum.u.ts.minute, datum.u.ts.second, fracstr + 1, tzstr);
}
// Print indent spaces
int indent_level = 0;
static int indent() {
for (int i = 0; i < indent_level * 2; i++) {
putchar(' ');
}
return 0;
}
static const char *fmt_double(double f, char *buf, int buflen) {
snprintf(buf, buflen, "%.16g", f);
if (!strchr(buf, 'e') && !strchr(buf, '.') && !strchr(buf, 'n')) {
// add a .0 if the number is an int, and not inf or nan.
snprintf(buf, buflen, "%.16g%s", f, ".0");
}
if (isnan(f) && signbit(f)) {
snprintf(buf, buflen, "%s", "-nan");
}
return buf;
}
// Print datum tree recursively
static void print_datum(toml_datum_t datum) {
char buf[50];
switch (datum.type) {
case TOML_STRING:
print_string(datum.u.str.ptr, datum.u.str.len);
break;
case TOML_INT64:
printf("{\"type\": \"integer\", \"value\": \"%" PRId64 "\"}",
datum.u.int64);
break;
case TOML_FP64:
printf("{\"type\": \"float\", \"value\": \"%s\"}",
fmt_double(datum.u.fp64, buf, sizeof(buf)));
break;
case TOML_BOOLEAN:
printf("{\"type\": \"bool\", \"value\": \"%s\"}",
datum.u.boolean ? "true" : "false");
break;
case TOML_DATE:
print_date(datum);
break;
case TOML_TIME:
print_time(datum);
break;
case TOML_DATETIME:
print_datetime(datum);
break;
case TOML_DATETIMETZ:
print_datetimetz(datum);
break;
case TOML_ARRAY:
printf("[");
for (int i = 0; i < datum.u.arr.size; i++) {
printf("%s", i ? ", " : "");
print_datum(datum.u.arr.elem[i]);
}
printf("]");
break;
case TOML_TABLE:
printf("{\n");
indent_level++;
for (int i = 0; i < datum.u.tab.size; i++) {
printf("%s", i ? ",\n" : "");
indent();
print_key(datum.u.tab.key[i], datum.u.tab.len[i]);
putchar(':');
putchar(' ');
print_datum(datum.u.tab.value[i]);
}
printf("\n");
indent_level--;
indent(), printf("}");
break;
default:
fprintf(stderr, "ERROR: unimplemented datum type %d\n", datum.type);
abort();
}
}
int main(int argc, const char *argv[]) {
g_argc = argc;
g_argv = argv;
if (argc > 2) {
usage();
}
toml_option_t opt = toml_default_option();
opt.check_utf8 = 1;
toml_set_option(opt);
toml_result_t result;
if (argc == 2) {
result = toml_parse_file_ex(argv[1]);
} else {
result = toml_parse_file(stdin);
}
if (!result.ok) {
printf("%s\n", result.errmsg);
toml_free(result);
exit(1);
}
print_datum(result.toptab);
printf("\n");
toml_free(result);
return 0;
}
@@ -0,0 +1,20 @@
#!/bin/bash
mkdir -p out
echo
echo =========================
echo == parser test
echo =========================
for fname in {1..200} array{1..10} tab{1..10} x{1..10} e{1..10}; do
IN="in/$fname.toml"
if [ -f $IN ]; then
echo test $fname.toml
OUT="out/$fname.out"
GOOD="good/$fname.out"
./driver $IN &> $OUT || true # ignore failure
diff $GOOD $OUT || { echo '--- FAILED ---'; exit 1; }
fi
done
echo DONE
@@ -0,0 +1,2 @@
/driver
/out
@@ -0,0 +1,21 @@
CFLAGS := -O0 -g -fsanitize=address,undefined -std=c17 -Wmissing-declarations -Wall -Wextra -MMD
all: driver
driver: scankey.c
$(CC) $(CFLAGS) -o $@ scankey.c
test: all
bash run.sh
-include driver.d
clean:
rm -f *.o *.d driver
distclean: clean
format:
clang-format -i *.[ch]
.PHONY: all clean distclean format test
@@ -0,0 +1,5 @@
str1 =
# comment
str2 #comment
@@ -0,0 +1,6 @@
"title" =
'title' =
bare_key =
bare-key =
1234 =
@@ -0,0 +1,4 @@
apple.type =
3.14159 =
1.2 .3 . 4 =
@@ -0,0 +1,7 @@
[table-1]
[dog."tater.man"]
[a.b.c] # this is best practice
[ d.e.f ] # same as [d.e.f]
[ g . h . i ] # same as [g.h.i]
[ j . "ʞ" . 'l' ] # same as [j."ʞ".'l']
@@ -0,0 +1,7 @@
# [x] you
# [x.y] don't
# [x.y.z] need these
[x.y.z.w] # for this to work
[x] # defining a super-table afterward is ok
@@ -0,0 +1,5 @@
name { # error
name } # error
name [ # error
name ] # error
@@ -0,0 +1,3 @@
[[products.1]]
[[products.2]]
@@ -0,0 +1,3 @@
café =
ñoño =
日本語 =
@@ -0,0 +1,20 @@
#!/bin/bash
mkdir -p out
echo
echo =========================
echo == scankey test
echo =========================
for fname in {1..100}; do
IN="in/$fname"
if [ -f $IN ]; then
echo test $fname
OUT="out/$fname.out"
GOOD="good/$fname.out"
./driver $IN &> $OUT
diff $GOOD $OUT || { echo '--- FAILED ---'; exit 1; }
fi
done
echo DONE
@@ -0,0 +1,127 @@
#include "../../src/tomlc17.c"
#include <stdlib.h>
const char **g_argv = 0;
int g_argc = 0;
static void usage() {
fprintf(stderr, "Usage: %s fname\n", g_argv[0]);
exit(1);
}
static void printspecial(const char *p, int n) {
for (int i = 0; i < n; i++, p++) {
int ch = (*p == '\n') ? '_' : *p;
putchar(ch);
}
}
static void printtok(char *content, const token_t tok) {
// clang-format off
#define CASESTR(x) case TOK_ ## x: s = #x; break
// clang-format on
char *s = 0;
switch (tok.toktyp) {
CASESTR(DOT);
CASESTR(EQUAL);
CASESTR(COMMA);
CASESTR(LBRACK);
CASESTR(LLBRACK);
CASESTR(RBRACK);
CASESTR(RRBRACK);
CASESTR(LBRACE);
CASESTR(RBRACE);
CASESTR(STRING);
CASESTR(MLSTRING);
CASESTR(LITSTRING);
CASESTR(MLLITSTRING);
CASESTR(TIME);
CASESTR(DATE);
CASESTR(DATETIME);
CASESTR(DATETIMETZ);
CASESTR(INTEGER);
CASESTR(FLOAT);
CASESTR(BOOL);
CASESTR(LIT);
CASESTR(ENDL);
CASESTR(FIN);
}
printf("%s %ld %d ", s, tok.str.ptr - content, tok.str.len);
printspecial(tok.str.ptr, tok.str.len);
printf("\n");
}
static char *readfile(const char *fname, int *ret_len) {
FILE *fp = fopen(fname, "r");
if (!fp) {
perror("fopen");
exit(1);
}
// Seek to the end of the file to determine its size
if (fseek(fp, 0, SEEK_END) != 0) {
perror("fseek");
exit(1);
}
long file_size = ftell(fp);
if (file_size == -1) {
perror("ftell");
exit(1);
}
rewind(fp); // Go back to the beginning of the file
// Allocate memory for the file content, plus one for the null terminator
char *content = malloc(file_size + 1);
if (!content) {
perror("out of memory");
exit(1);
}
// Read the file into the buffer
size_t read_size = fread(content, 1, file_size, fp);
if (read_size != (size_t)file_size) {
perror("fread");
exit(1);
}
content[file_size] = '\0'; // Null-terminate the string
fclose(fp);
*ret_len = file_size;
return content;
}
int main(int argc, const char *argv[]) {
g_argc = argc;
g_argv = argv;
if (argc != 2) {
usage();
}
int len;
char *content = readfile(argv[1], &len);
char errbuf[200];
scanner_t scanner;
scanner_t *sp = &scanner;
scan_init(sp, content, len, errbuf, sizeof(errbuf));
for (;;) {
(void)scan_value; // silent compiler
token_t tok;
if (scan_key(sp, &tok)) {
break;
}
if (tok.toktyp == TOK_FIN) {
break;
}
printtok(content, tok);
}
if (sp->errmsg) {
printf("ERROR: %s (line %d)\n", sp->errmsg, sp->lineno);
exit(1);
}
free(content);
return 0;
}
@@ -0,0 +1,2 @@
/driver
/out
@@ -0,0 +1,21 @@
CFLAGS := -O0 -g -fsanitize=address,undefined -std=c17 -Wmissing-declarations -Wall -Wextra -MMD
all: driver
driver: scanvalue.c
$(CC) $(CFLAGS) -o $@ scanvalue.c -lm
test: all
bash run.sh
-include driver.d
clean:
rm -f *.o *.d driver
distclean: clean
format:
clang-format -i *.[ch]
.PHONY: all clean distclean format test
@@ -0,0 +1,16 @@
= """
Roses are red
Violets are blue"""
# On a Unix system, the above multi-line string will most likely be the same as:
= "Roses are red\nViolets are blue"
# On a Windows system, it will most likely be equivalent to:
= "Roses are red\r\nViolets are blue"
= """Here are two quotation marks: "". Simple enough."""
= """Here are three quotation marks: ""\"."""
= """Here are fifteen quotation marks: ""\"""\"""\"""\"""\"."""
# "This," she said, "is just a pointless statement."
= """"This," she said, "is just a pointless statement.""""# What you see is what you get.
@@ -0,0 +1,12 @@
# integers
= [ 1, 2, 3 ]
# colors
= [ "red", "yellow", "green" ]
# nested
= [ [ 1, 2 ], [3, 4, 5] ]
= [ [ 1, 2 ], ["a", "b", "c"] ]
= [ "all", 'strings', """are the same""", '''type''' ]
@@ -0,0 +1,6 @@
# Mixed-type arrays are allowed
= [ 0.1, 0.2, 0.5, 1.0, 2, 5 ]
= [
"Foo Bar <foo@example.com>",
{ }
]
@@ -0,0 +1,11 @@
# multiline
= [
1, 2, 3
]
# extra comma at end
= [
1,
2, # this is ok
]
@@ -0,0 +1,2 @@
= { }
@@ -0,0 +1,17 @@
# all results are the same
= "abc"
= """
a\
b\
c"""
= """\
a\
b\
c\
"""
@@ -0,0 +1,9 @@
# line-ending backslash with whitespace "geeee\ "
# result should be "heeee\ngeeee"
= """
heeee
geeee\
"""
@@ -0,0 +1,2 @@
= "\t tab tab tab \t"
= "\e There is no escape! \e"
@@ -0,0 +1 @@
= inf
@@ -0,0 +1,4 @@
= 2023-01-01T12:34:56.123456789Z
= 2023-01-01T12:34:56.123456789
= 07:32:00.123456789
@@ -0,0 +1,11 @@
= 'C:\Users\nodejs\templates'
= '\\ServerX\admin$\system32\'
= 'Tom "Dubs" Preston-Werner'
= '<\i\c*\s*>'
= '''I [dw]on't need \d{2} apples'''
= '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''
@@ -0,0 +1,7 @@
= '''Here are fifteen quotation marks: """""""""""""""'''
# apos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID
= "Here are fifteen apostrophes: '''''''''''''''"
# 'That,' she said, 'is still pointless.'
= ''''That,' she said, 'is still pointless.''''
@@ -0,0 +1,10 @@
= "pi"
= +99
= 42
= 0
= -17
= 1_000
= 5_349_221
= 53_49_221 # Indian number system grouping
= 1_2_3_4_5 # VALID but discouraged
@@ -0,0 +1,11 @@
# hexadecimal with prefix `0x`
= 0xDEADBEEF
= 0xdeadbeef
= 0xdead_beef
# octal with prefix `0o`
= 0o01234567
= 0o755 # useful for Unix file permissions
# binary with prefix `0b`
= 0b11010110
@@ -0,0 +1,15 @@
# fractional
= +1.0
= 3.1415
= -0.01
# exponent
= 5e+22
= 1e06
= -2E-2
# both
= 6.626e-34
= 224_617.445_991_228
@@ -0,0 +1,10 @@
# infinity
= inf # positive infinity
= +inf # positive infinity
= -inf # negative infinity
# not a number
= nan # actual sNaN/qNaN encoding is implementation-specific
= +nan # same as `nan`
= -nan # valid, actual encoding is implementation-specific
@@ -0,0 +1,3 @@
= true
= false
@@ -0,0 +1,13 @@
= 1979-05-27T07:32:00Z
= 1979-05-27T00:32:00-07:00
= 1979-05-27T00:32:00.999999-07:00
= 1979-05-27 07:32:00Z
= 1979-05-27T07:32:00
= 1979-05-27T00:32:00.999999
= 1979-05-27
= 07:32:00
= 00:32:00.999999
# lower
= 1987-07-05t17:45:00z
@@ -0,0 +1,2 @@
# trailing .
= 1997-09-09T09:09:09.
@@ -0,0 +1,2 @@
# timezone offset overflow minute
= 1985-06-18 17:04:07+12:60
@@ -0,0 +1,20 @@
#!/bin/bash
mkdir -p out
echo
echo =========================
echo == scanvalue test
echo =========================
for fname in {1..100} e{1..100}; do
IN="in/$fname"
if [ -f $IN ]; then
echo test $fname
OUT="out/$fname.out"
GOOD="good/$fname.out"
./driver $IN &> $OUT
diff $GOOD $OUT || { echo '--- FAILED ---'; exit 1; }
fi
done
echo DONE
@@ -0,0 +1,161 @@
#include "../../src/tomlc17.c"
#include <inttypes.h>
#include <stdlib.h>
const char **g_argv = 0;
int g_argc = 0;
static void usage() {
fprintf(stderr, "Usage: %s fname\n", g_argv[0]);
exit(1);
}
static void printspecial(const char *p, int n) {
for (int i = 0; i < n; i++, p++) {
int ch = (*p == '\n') ? '_' : *p;
putchar(ch);
}
}
static const char *fmt_double(double f, char *buf, int buflen) {
snprintf(buf, buflen, "%.16g", f);
if (!strchr(buf, 'e') && !strchr(buf, '.') && !strchr(buf, 'n')) {
// add a .0 if the number is an int, and not inf or nan.
snprintf(buf, buflen, "%.16g%s", f, ".0");
}
if (isnan(f) && signbit(f)) {
snprintf(buf, buflen, "%s", "-nan");
}
return buf;
}
static void printtok(char *content, const token_t tok) {
// clang-format off
#define CASESTR(x) case TOK_ ## x: s = #x; break
// clang-format on
char *s = 0;
switch (tok.toktyp) {
CASESTR(DOT);
CASESTR(EQUAL);
CASESTR(COMMA);
CASESTR(LBRACK);
CASESTR(RBRACK);
CASESTR(LBRACE);
CASESTR(RBRACE);
CASESTR(STRING);
CASESTR(MLSTRING);
CASESTR(LITSTRING);
CASESTR(MLLITSTRING);
CASESTR(TIME);
CASESTR(DATE);
CASESTR(DATETIME);
CASESTR(DATETIMETZ);
CASESTR(INTEGER);
CASESTR(FLOAT);
CASESTR(BOOL);
CASESTR(LIT);
CASESTR(ENDL);
CASESTR(FIN);
default:
s = "UNKNOWN";
break;
}
printf("%s %ld %d ", s, tok.str.ptr - content, tok.str.len);
printspecial(tok.str.ptr, tok.str.len);
char buf[50];
switch (tok.toktyp) {
case TOK_INTEGER:
printf(" %" PRId64, tok.u.int64);
break;
case TOK_FLOAT:
printf(" %s", fmt_double(tok.u.fp64, buf, sizeof(buf)));
break;
case TOK_BOOL:
printf(" %s", tok.u.b1 ? "true" : "false");
break;
default:
break;
}
printf("\n");
}
static char *readfile(const char *fname, int *ret_len) {
FILE *fp = fopen(fname, "r");
if (!fp) {
perror("fopen");
exit(1);
}
// Seek to the end of the file to determine its size
if (fseek(fp, 0, SEEK_END) != 0) {
perror("fseek");
exit(1);
}
long file_size = ftell(fp);
if (file_size == -1) {
perror("ftell");
exit(1);
}
rewind(fp); // Go back to the beginning of the file
// Allocate memory for the file content, plus one for the null terminator
char *content = malloc(file_size + 1);
if (!content) {
perror("out of memory");
exit(1);
}
// Read the file into the buffer
size_t read_size = fread(content, 1, file_size, fp);
if (read_size != (size_t)file_size) {
perror("fread");
exit(1);
}
content[file_size] = '\0'; // Null-terminate the string
fclose(fp);
*ret_len = file_size;
return content;
}
int main(int argc, const char *argv[]) {
g_argc = argc;
g_argv = argv;
if (argc != 2) {
usage();
}
int len;
char *content = readfile(argv[1], &len);
char errbuf[200];
scanner_t scanner;
scanner_t *sp = &scanner;
scan_init(sp, content, len, errbuf, sizeof(errbuf));
for (;;) {
(void)scan_key; // silent compiler
token_t tok;
if (scan_value(sp, &tok)) {
printf("%s\n", errbuf);
goto bail;
}
if (tok.toktyp == TOK_FIN) {
break;
}
printtok(content, tok);
}
if (sp->errmsg) {
printf("ERROR: %s (line %d)\n", sp->errmsg, sp->lineno);
goto bail;
}
free(content);
return 0;
bail:
free(content);
return 1;
}
@@ -0,0 +1 @@
test1
@@ -0,0 +1,23 @@
CFLAGS := -O0 -g -fsanitize=address,undefined -std=c17 -Wmissing-declarations -Wall -Wextra -MMD
EXEC = test1
all: $(EXEC)
test1: test1.c
$(CC) $(CFLAGS) -o $@ $@.c
test: all
./test1
-include test1.d
clean:
rm -f *.o *.d $(EXEC)
distclean: clean
format:
clang-format -i *.[ch]
.PHONY: all clean distclean format test
@@ -0,0 +1,158 @@
#include "../../src/tomlc17.c"
static void failed(const char *msg) {
printf("FAILED: %s\n", msg);
exit(1);
}
#define CHECK(x) \
if (x) \
; \
else \
failed(#x)
static void test_named_scalars(void) {
printf("Running test_named_scalars...\n");
const char *doc = "title = \"hi\"\n"
"[owner]\n"
"name = \"Alice\"\n"
"[[pkg]]\n"
"n = 1\n"
"nums = [1, 2, 3]\n"
"inl = { a = 1, b = 2 }\n";
toml_result_t r = toml_parse_named(doc, (int)strlen(doc), "a.toml");
CHECK(r.ok);
CHECK(r.toptab.source && 0 == strcmp(r.toptab.source, "a.toml"));
toml_datum_t title = toml_get(r.toptab, "title");
CHECK(title.type == TOML_STRING);
CHECK(title.source && 0 == strcmp(title.source, "a.toml"));
toml_datum_t owner = toml_get(r.toptab, "owner");
CHECK(owner.type == TOML_TABLE);
CHECK(owner.source && 0 == strcmp(owner.source, "a.toml"));
toml_datum_t oname = toml_get(owner, "name");
CHECK(oname.source && 0 == strcmp(oname.source, "a.toml"));
toml_datum_t pkg = toml_get(r.toptab, "pkg");
CHECK(pkg.type == TOML_ARRAY && pkg.u.arr.size == 1);
CHECK(pkg.u.arr.elem[0].source &&
0 == strcmp(pkg.u.arr.elem[0].source, "a.toml"));
// inline array: datum and elements carry source
toml_datum_t nums = toml_get(pkg.u.arr.elem[0], "nums");
CHECK(nums.type == TOML_ARRAY && nums.u.arr.size == 3);
CHECK(nums.source && 0 == strcmp(nums.source, "a.toml"));
CHECK(nums.u.arr.elem[1].source &&
0 == strcmp(nums.u.arr.elem[1].source, "a.toml"));
// inline table: datum and values carry source
toml_datum_t inl = toml_get(pkg.u.arr.elem[0], "inl");
CHECK(inl.type == TOML_TABLE && inl.u.tab.size == 2);
CHECK(inl.source && 0 == strcmp(inl.source, "a.toml"));
toml_datum_t inl_a = toml_get(inl, "a");
CHECK(inl_a.type == TOML_INT64);
CHECK(inl_a.source && 0 == strcmp(inl_a.source, "a.toml"));
// interned: same pointer across datums of one document
CHECK(title.source == oname.source);
toml_free(r);
}
static void test_null_name(void) {
printf("Running test_null_name...\n");
const char *doc = "x = 1\n";
toml_result_t r1 = toml_parse_named(doc, (int)strlen(doc), NULL);
CHECK(r1.ok);
CHECK(r1.toptab.source == NULL);
CHECK(toml_get(r1.toptab, "x").source == NULL);
toml_free(r1);
toml_result_t r2 = toml_parse(doc, (int)strlen(doc));
CHECK(r2.ok);
CHECK(toml_get(r2.toptab, "x").source == NULL);
toml_free(r2);
}
static void test_file_ex(void) {
printf("Running test_file_ex...\n");
const char *path = "src_test_tmp.toml";
FILE *fp = fopen(path, "w");
CHECK(fp != NULL);
fputs("k = 42\n", fp);
fclose(fp);
toml_result_t r = toml_parse_file_ex(path);
CHECK(r.ok);
CHECK(r.toptab.source && 0 == strcmp(r.toptab.source, path));
CHECK(toml_get(r.toptab, "k").source &&
0 == strcmp(toml_get(r.toptab, "k").source, path));
toml_free(r);
remove(path);
}
static void test_merge_sources(void) {
printf("Running test_merge_sources...\n");
const char *a = "title = \"A\"\n"
"[owner]\n"
"name = \"Alice\"\n";
const char *b = "version = \"1\"\n"
"[owner]\n"
"org = \"ACME\"\n";
toml_result_t rA = toml_parse_named(a, (int)strlen(a), "a.toml");
toml_result_t rB = toml_parse_named(b, (int)strlen(b), "b.toml");
toml_result_t m = toml_merge(&rA, &rB);
CHECK(m.ok);
// free inputs first: merged result must be self-contained
toml_free(rA);
toml_free(rB);
CHECK(0 == strcmp(toml_get(m.toptab, "title").source, "a.toml"));
CHECK(0 == strcmp(toml_get(m.toptab, "version").source, "b.toml"));
toml_datum_t owner = toml_get(m.toptab, "owner");
toml_datum_t name = toml_get(owner, "name"); // from A
toml_datum_t org = toml_get(owner, "org"); // from B
CHECK(0 == strcmp(name.source, "a.toml"));
CHECK(0 == strcmp(org.source, "b.toml"));
// dedup: same-origin datums share a pointer
CHECK(toml_get(m.toptab, "title").source == name.source);
toml_free(m);
}
static void test_merge_array_of_tables_source(void) {
printf("Running test_merge_array_of_tables_source...\n");
const char *a = "[[pkg]]\n"
"name = \"left\"\n";
const char *b = "[[pkg]]\n"
"name = \"right\"\n";
toml_result_t rA = toml_parse_named(a, (int)strlen(a), "a.toml");
toml_result_t rB = toml_parse_named(b, (int)strlen(b), "b.toml");
toml_result_t m = toml_merge(&rA, &rB);
CHECK(m.ok);
// free inputs first: merged result must be self-contained
toml_free(rA);
toml_free(rB);
toml_datum_t pkg = toml_get(m.toptab, "pkg");
CHECK(pkg.type == TOML_ARRAY && pkg.u.arr.size == 2);
// append semantics: A's element first, B's element appended
CHECK(0 == strcmp(pkg.u.arr.elem[0].source, "a.toml"));
CHECK(0 == strcmp(pkg.u.arr.elem[1].source, "b.toml"));
CHECK(0 == strcmp(toml_get(pkg.u.arr.elem[0], "name").source, "a.toml"));
CHECK(0 == strcmp(toml_get(pkg.u.arr.elem[1], "name").source, "b.toml"));
toml_free(m);
}
int main(void) {
test_named_scalars();
test_null_name();
test_file_ex();
test_merge_sources();
test_merge_array_of_tables_source();
printf("OK\n");
return 0;
}
@@ -0,0 +1 @@
/driver
@@ -0,0 +1,21 @@
CFLAGS := -O0 -g -fsanitize=address,undefined -std=c17 -Wmissing-declarations -Wall -Wextra -MMD
all: driver
driver: parser.c
$(CC) $(CFLAGS) -o $@ parser.c
test: all
bash run.sh
-include driver.d
clean:
rm -f *.o *.d driver
distclean: clean
format:
clang-format -i *.[ch]
.PHONY: all clean distclean format test
@@ -0,0 +1,278 @@
#include "../../src/tomlc17.c"
#include <inttypes.h>
const char **g_argv = 0;
int g_argc = 0;
static void usage() {
fprintf(stderr, "Usage: %s [fname]\n", g_argv[0]);
exit(1);
}
// Print a string in json. Properly escape special chars.
static void print_string(const char *s, int len) {
printf("{\"type\": \"string\", \"value\": \"");
for (int i = 0; i < len; i++) {
int ch = s[i];
if (isprint(ch) && ch != '\"' && ch != '\\') {
putchar(ch);
continue;
}
switch (ch) {
case '\b':
putchar('\\');
putchar('b');
break; // Escape backspace
case '\t':
putchar('\\');
putchar('t');
break; // Escape tab
case '\n':
putchar('\\');
putchar('n');
break; // Escape newline
case '\f':
putchar('\\');
putchar('f');
break; // Escape formfeed
case '\r':
putchar('\\');
putchar('r');
break; // Escape carriage return
case '"':
putchar('\\');
putchar('"');
break; // Escape double quotes
case '\\':
putchar('\\');
putchar('\\');
break; // Escape backslash
default:
if (0 <= ch && ch < ' ') {
printf("\\u%04x", ch);
} else {
putchar(ch);
}
break;
}
}
printf("\"}");
}
// Print a key
static void print_key(const char *s, int len) {
putchar('"');
for (int i = 0; i < len; i++) {
int ch = s[i];
if (isprint(ch) && ch != '\"' && ch != '\\') {
putchar(ch);
continue;
}
switch (ch) {
case '"':
putchar('\\');
putchar('"');
break; // Escape double quotes
case '\\':
putchar('\\');
putchar('\\');
break; // Escape backslash
case '\b':
putchar('\\');
putchar('b');
break; // Escape backspace
case '\f':
putchar('\\');
putchar('f');
break; // Escape formfeed
case '\n':
putchar('\\');
putchar('n');
break; // Escape newline
case '\r':
putchar('\\');
putchar('r');
break; // Escape carriage return
case '\t':
putchar('\\');
putchar('t');
break; // Escape tab
default:
if (0 <= ch && ch < ' ') {
printf("\\u%04x", ch);
} else {
putchar(ch);
}
break;
}
}
putchar('"');
}
// Print a DATE datum
static void print_date(toml_datum_t datum) {
printf("{\"type\": \"date-local\", \"value\": \"%04d-%02d-%02d\"}",
datum.u.ts.year, datum.u.ts.month, datum.u.ts.day);
}
// Print a TIME datum
static void print_time(toml_datum_t datum) {
char fracstr[20];
fracstr[0] = fracstr[1] = '\0';
if (datum.u.ts.usec) {
double f = datum.u.ts.usec / 1000000.0;
snprintf(fracstr, sizeof(fracstr), "%.6f", f);
fracstr[5] = '\0'; // millisec precision
}
printf("{\"type\": \"time-local\", \"value\": \"%02d:%02d:%02d%s\"}",
datum.u.ts.hour, datum.u.ts.minute, datum.u.ts.second, fracstr + 1);
}
// Print a DATETIME datum
static void print_datetime(toml_datum_t datum) {
char fracstr[20];
fracstr[0] = fracstr[1] = '\0';
if (datum.u.ts.usec) {
double f = datum.u.ts.usec / 1000000.0;
snprintf(fracstr, sizeof(fracstr), "%.6f", f);
fracstr[5] = '\0'; // millisec precision
}
printf("{\"type\": \"datetime-local\", \"value\": \"%04d-%02d-%02d "
"%02d:%02d:%02d%s\"}",
datum.u.ts.year, datum.u.ts.month, datum.u.ts.day, datum.u.ts.hour,
datum.u.ts.minute, datum.u.ts.second, fracstr + 1);
}
// Print a DATETIMETZ datum
static void print_datetimetz(toml_datum_t datum) {
char fracstr[20];
char tzstr[20];
fracstr[0] = fracstr[1] = tzstr[0] = '\0';
if (datum.u.ts.usec) {
double f = datum.u.ts.usec / 1000000.0;
snprintf(fracstr, sizeof(fracstr), "%.6f", f);
fracstr[5] = '\0'; // millisec precision
}
int tz = datum.u.ts.tz;
char sign = tz < 0 ? '-' : '+';
tz = tz < 0 ? -tz : tz;
snprintf(tzstr, sizeof(tzstr), "%c%02d:%02d", sign, tz / 60, tz % 60);
printf("{\"type\": \"datetime\", \"value\": \"%04d-%02d-%02d "
"%02d:%02d:%02d%s%s\"}",
datum.u.ts.year, datum.u.ts.month, datum.u.ts.day, datum.u.ts.hour,
datum.u.ts.minute, datum.u.ts.second, fracstr + 1, tzstr);
}
// Print indent spaces
int indent_level = 0;
static int indent() {
for (int i = 0; i < indent_level * 2; i++) {
putchar(' ');
}
return 0;
}
static const char *fmt_double(double f, char *buf, int buflen) {
snprintf(buf, buflen, "%.16g", f);
if (!strchr(buf, 'e') && !strchr(buf, '.') && !strchr(buf, 'n')) {
// add a .0 if the number is an int, and not inf or nan.
snprintf(buf, buflen, "%.16g%s", f, ".0");
}
if (isnan(f) && signbit(f)) {
snprintf(buf, buflen, "%s", "-nan");
}
return buf;
}
// Print datum tree recursively
static void print_datum(toml_datum_t datum) {
char buf[50];
switch (datum.type) {
case TOML_STRING:
print_string(datum.u.str.ptr, datum.u.str.len);
break;
case TOML_INT64:
printf("{\"type\": \"integer\", \"value\": \"%" PRId64 "\"}",
datum.u.int64);
break;
case TOML_FP64:
printf("{\"type\": \"float\", \"value\": \"%s\"}",
fmt_double(datum.u.fp64, buf, sizeof(buf)));
break;
case TOML_BOOLEAN:
printf("{\"type\": \"bool\", \"value\": \"%s\"}",
datum.u.boolean ? "true" : "false");
break;
case TOML_DATE:
print_date(datum);
break;
case TOML_TIME:
print_time(datum);
break;
case TOML_DATETIME:
print_datetime(datum);
break;
case TOML_DATETIMETZ:
print_datetimetz(datum);
break;
case TOML_ARRAY:
printf("[");
for (int i = 0; i < datum.u.arr.size; i++) {
printf("%s", i ? ", " : "");
print_datum(datum.u.arr.elem[i]);
}
printf("]");
break;
case TOML_TABLE:
printf("{\n");
indent_level++;
for (int i = 0; i < datum.u.tab.size; i++) {
printf("%s", i ? ",\n" : "");
indent();
print_key(datum.u.tab.key[i], datum.u.tab.len[i]);
putchar(':');
putchar(' ');
print_datum(datum.u.tab.value[i]);
}
printf("\n");
indent_level--;
indent(), printf("}");
break;
default:
fprintf(stderr, "ERROR: unimplemented datum type %d\n", datum.type);
abort();
}
}
int main(int argc, const char *argv[]) {
g_argc = argc;
g_argv = argv;
if (argc > 2) {
usage();
}
toml_option_t opt = toml_default_option();
opt.check_utf8 = 1;
toml_set_option(opt);
toml_result_t result;
if (argc == 2) {
result = toml_parse_file_ex(argv[1]);
} else {
result = toml_parse_file(stdin);
}
if (!result.ok) {
printf("%s\n", result.errmsg);
toml_free(result);
exit(1);
}
print_datum(result.toptab);
printf("\n");
toml_free(result);
return 0;
}
@@ -0,0 +1,10 @@
echo
echo =========================
echo == stdtest
echo =========================
#v1.0 ==> go install github.com/toml-lang/toml-test/cmd/toml-test@latest
go install github.com/toml-lang/toml-test/v2/cmd/toml-test@latest
toml-test test -toml 1.1 -decoder $PWD/driver \
-skip invalid/key/special-character # μ (U+03BC) is valid per TOML 1.1 ABNF; toml-test v2 doesn't exclude this 1.0-era test for 1.1