diff --git a/Makefile b/Makefile index c54f21f..1ada537 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ # compiler flags CC = gcc -CFLAGS = -Wall -Wextra -Iincludes -Isrc +CFLAGS = -Wall -Wextra -Iincludes -Isrc -Iincludes/tomlc17-R260618/src # source & output -SRC = main.c src/driver.c src/data.c -OBJ = main.o src/driver.o src/data.o +SRC = main.c src/driver.c src/data.c src/config.c src/help.c includes/tomlc17-R260618/src/tomlc17.c +OBJ = main.o src/driver.o src/data.o src/config.o src/help.o includes/tomlc17-R260618/src/tomlc17.o TARGET = k1ng_driver LDFLAGS = -lusb-1.0 @@ -25,6 +25,15 @@ src/driver.o: src/driver.c src/data.o: src/data.c $(CC) $(CFLAGS) -c src/data.c -o src/data.o +src/config.o: src/config.c + $(CC) $(CFLAGS) -c src/config.c -o src/config.o + +src/help.o: src/help.c + $(CC) $(CFLAGS) -c src/help.c -o src/help.o + +includes/tomlc17-R260618/src/tomlc17.o: includes/tomlc17-R260618/src/tomlc17.c + $(CC) $(CFLAGS) -c includes/tomlc17-R260618/src/tomlc17.c -o includes/tomlc17-R260618/src/tomlc17.o + # cleanup build artifacts clean: rm -f $(OBJ) $(TARGET) diff --git a/README.md b/README.md index 6e0635f..4e4fba3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ > [!NOTE] > work in progress, but works! +> [!IMPORTANT] +> U may need something like this for the driver to work: + +``` +services.udev.extraRules = '' + SUBSYSTEM=="hidraw", ATTRS{idVendor}=="3554", ATTRS{idProduct}=="f54d", MODE="0666" + ''; +``` + ## K1NG-Driver a driver for K1NG PRO (4K) mouse from redragon diff --git a/includes/include.h b/includes/include.h index 08a408a..c9a5055 100644 --- a/includes/include.h +++ b/includes/include.h @@ -5,7 +5,9 @@ #include #include #include +#include #include "libusb-1.0.30/libusb/libusb.h" +#include "tomlc17-R260618/src/tomlc17.h" // mouse pids #define TARGET_VID 0x3554 diff --git a/includes/tomlc17-R260618/.github/workflows/c-cpp.yml b/includes/tomlc17-R260618/.github/workflows/c-cpp.yml new file mode 100644 index 0000000..aea11aa --- /dev/null +++ b/includes/tomlc17-R260618/.github/workflows/c-cpp.yml @@ -0,0 +1,49 @@ + +name: C/C++ CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + DEBUG: "1" + +jobs: + build-ubuntu: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + cache: false + - name: install toml-test + run: go install github.com/toml-lang/toml-test/v2/cmd/toml-test@latest + - name: make clean all + run: make clean all + - name: make test + run: make test + - name: make install + run: make install prefix=./tmp + + + build-macos: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.21' # or whatever version you need + cache: false + - name: install toml-test + run: go install github.com/toml-lang/toml-test/v2/cmd/toml-test@latest + - name: make clean all + run: make clean all + - name: make test + run: make test + - name: make install + run: make install prefix=./tmp + diff --git a/includes/tomlc17-R260618/.github/workflows/windows-build.yml.disabled b/includes/tomlc17-R260618/.github/workflows/windows-build.yml.disabled new file mode 100644 index 0000000..47d66eb --- /dev/null +++ b/includes/tomlc17-R260618/.github/workflows/windows-build.yml.disabled @@ -0,0 +1,58 @@ +name: Windows Build and Test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + DEBUG: "1" + +jobs: + build-windows: + runs-on: windows-latest + + strategy: + matrix: + include: + - { cc: gcc, cxx: g++ } + - { cc: clang, cxx: clang++ } + + steps: + - uses: actions/checkout@v4 + + - name: Install MinGW and tools + run: | + choco install mingw make -y + refreshenv + + - name: Install LLVM (for clang build) + if: matrix.cc == 'clang' + run: choco install llvm -y + + - name: Setup Go + uses: actions/setup-go@v5 + with: + cache: false + + - name: Install toml-test + run: go install github.com/toml-lang/toml-test/cmd/toml-test@latest + + - name: Build + shell: cmd + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + run: | + make clean all + + - name: Run tests + shell: cmd + run: | + make test + + - name: Install to temp directory + shell: cmd + run: | + make install prefix=.\tmp diff --git a/includes/tomlc17-R260618/.gitignore b/includes/tomlc17-R260618/.gitignore new file mode 100644 index 0000000..b1d8fab --- /dev/null +++ b/includes/tomlc17-R260618/.gitignore @@ -0,0 +1,7 @@ +*~ +*.o +*.d +*.a +*.swp +a.out +t diff --git a/includes/tomlc17-R260618/API.md b/includes/tomlc17-R260618/API.md new file mode 100644 index 0000000..8a67c17 --- /dev/null +++ b/includes/tomlc17-R260618/API.md @@ -0,0 +1,321 @@ +# tomlc17 API Reference + +## Overview + +```c +#include "tomlc17.h" +``` + +The typical usage pattern: + +1. Call `toml_parse()`, `toml_parse_file()`, or `toml_parse_file_ex()` to parse a document. +2. Check `result.ok`. On failure, `result.errmsg` contains a description. +3. Navigate the document tree using `toml_get()` or `toml_seek()`. +4. Call `toml_free()` when done. + +**Important:** `toml_free()` must be called on every result returned by a parse or merge +function, even if `result.ok` is false. + +All strings and data returned from the API are owned by the `toml_result_t` and remain +valid until `toml_free()` is called on that result. + +--- + +## Types + +### `toml_type_t` + +```c +typedef enum { + TOML_UNKNOWN = 0, + TOML_STRING, + TOML_INT64, + TOML_FP64, + TOML_BOOLEAN, + TOML_DATE, + TOML_TIME, + TOML_DATETIME, + TOML_DATETIMETZ, + TOML_ARRAY, + TOML_TABLE, +} toml_type_t; +``` + +The type tag carried by every `toml_datum_t`. A datum with `type == TOML_UNKNOWN` +means "not found" or "invalid". + +--- + +### `toml_datum_t` + +A node in the parsed document tree. + +```c +struct toml_datum_t { + toml_type_t type; + uint32_t flag; // internal + int lineno; // 1-based source line, 0 when synthesized + int colno; // 1-based source column, 0 when synthesized + const char *source; // source name (e.g. filename), NULL when not provided + union { + const char *s; // TOML_STRING: shorthand for str.ptr + struct { + const char *ptr; // NUL-terminated string + int len; // length excluding the NUL terminator + } str; // TOML_STRING + int64_t int64; // TOML_INT64 + double fp64; // TOML_FP64 + bool boolean; // TOML_BOOLEAN + struct { + int16_t year, month, day; + int16_t hour, minute, second; + int32_t usec; // microseconds + int16_t tz; // timezone offset in minutes + } ts; // TOML_DATE, TOML_TIME, TOML_DATETIME, TOML_DATETIMETZ + struct { + int32_t size; // number of elements + toml_datum_t *elem; // elem[0..size-1] + } arr; // TOML_ARRAY + struct { + int32_t size; // number of key/value pairs + const char **key; // key[0..size-1] (NUL-terminated strings) + int *len; // len[0..size-1] (key lengths) + toml_datum_t *value; // value[0..size-1] + } tab; // TOML_TABLE + } u; +}; +``` + +**Fields:** + +- `type`: The type of this datum (see `toml_type_t`). +- `flag`: Internal use only. Do not read or modify. +- `lineno`: 1-based line number in the source document where this value appeared. Set to 0 for synthesized values. +- `colno`: 1-based column number in the source document where this value appeared. Set to 0 for synthesized values. +- `source`: Name of the document this value came from (e.g. the filename), or + `NULL` if no name was supplied. Copied into the result and preserved across + `toml_merge`, so merged documents retain each entry's origin. +- `u`: Union containing the actual value based on the `type`. + +**Accessing values by type:** + +| `type` | field(s) to read | +|------------------|------------------------------------------| +| `TOML_STRING` | `u.s` or `u.str.ptr` / `u.str.len` | +| `TOML_INT64` | `u.int64` | +| `TOML_FP64` | `u.fp64` | +| `TOML_BOOLEAN` | `u.boolean` | +| `TOML_DATE` | `u.ts.year`, `.month`, `.day` | +| `TOML_TIME` | `u.ts.hour`, `.minute`, `.second`, `.usec` | +| `TOML_DATETIME` | all `u.ts` fields except `.tz` | +| `TOML_DATETIMETZ`| all `u.ts` fields | +| `TOML_ARRAY` | `u.arr.size`, `u.arr.elem[]` | +| `TOML_TABLE` | `u.tab.size`, `u.tab.key[]`, `u.tab.value[]` | + +--- + +### `toml_result_t` + +Returned by every parse function. + +```c +struct toml_result_t { + bool ok; // true on success + toml_datum_t toptab; // the top-level table; valid when ok == true + char errmsg[200]; // error description; valid when ok == false + void *__internal; // internal use only; do not access +}; +``` + +--- + +### `toml_option_t` + +Global options. See [`toml_set_option()`](#toml_set_option). + +```c +struct toml_option_t { + bool check_utf8; // validate UTF-8; default: false + void *(*mem_realloc)(void *ptr, size_t); // default: realloc() + void (*mem_free)(void *ptr); // default: free() +}; +``` + +--- + +## Parsing + +### `toml_parse` + +```c +toml_result_t toml_parse(const char *src, int len); +``` + +Parse a TOML document from a NUL-terminated string. `len` is the length of `src` +excluding the NUL terminator. The result must be released with `toml_free()`. + +--- + +### `toml_parse_file` + +```c +toml_result_t toml_parse_file(FILE *fp); +``` + +Parse a TOML document from an open file handle. The caller is responsible for +calling `fclose(fp)`. The result must be released with `toml_free()`. + +--- + +### `toml_parse_file_ex` + +```c +toml_result_t toml_parse_file_ex(const char *fname); +``` + +Parse a TOML document from a file path. Opens and closes the file internally. +Every parsed datum's `source` is set to `fname`. The result must be released +with `toml_free()`. + +--- + +### `toml_parse_named` + +```c +toml_result_t toml_parse_named(const char *src, int len, const char *name); +``` + +Like `toml_parse`, but tags every parsed datum's `source` with `name` (copied +into the result; pass `NULL` for no name). The name survives `toml_merge`. + +--- + +### `toml_parse_file_named` + +```c +toml_result_t toml_parse_file_named(FILE *fp, const char *name); +``` + +Like `toml_parse_file`, but tags datums with `name`. `toml_parse_file_ex` +automatically uses the file path as the name. + +--- + +## Releasing + +### `toml_free` + +```c +void toml_free(toml_result_t result); +``` + +Release all memory associated with a `toml_result_t`. Must be called on every +result returned by a parse or merge function, regardless of whether parsing +succeeded. All pointers obtained from this result become invalid after this call. + +--- + +## Querying + +### `toml_get` + +```c +toml_datum_t toml_get(toml_datum_t table, const char *key); +``` + +Look up a single key in a table. Returns a datum with `type == TOML_UNKNOWN` +if the key is not found or if `table` is not a `TOML_TABLE`. + +--- + +### `toml_seek` + +```c +toml_datum_t toml_seek(toml_datum_t table, const char *multipart_key); +``` + +Look up a dot-separated key path starting from `table`. For example, +`"server.host"` is equivalent to calling `toml_get` twice. + +Constraints: +- Keys must not contain escape characters. +- The total length of `multipart_key` must not exceed 255 bytes. + +Returns a datum with `type == TOML_UNKNOWN` if any component is not found. + +--- + +## Merging and Comparing + +### `toml_merge` + +```c +toml_result_t toml_merge(const toml_result_t *r1, const toml_result_t *r2); +``` + +Produce a new result that is `r1` overridden by `r2`. Merge rules: + +- Key in r2 not in r1 → added to result. +- Key in r2 with a different type than r1 → overrides r1. +- Key is an array of tables → r2's entries are appended to r1's. +- Key is a table → r2's sub-keys are recursively merged into r1's. +- Otherwise → r2 overrides r1. + +All three results (`r1`, `r2`, and the returned result) must each be freed +with `toml_free()` independently. + +--- + +### `toml_equiv` + +```c +bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2); +``` + +Return `true` if the two results represent identical documents. Table key order +and array element order are both significant. + +--- + +## Options + +### `toml_default_option` + +```c +toml_option_t toml_default_option(void); +``` + +Return the current default options. Use this to obtain a baseline before +modifying individual fields. + +--- + +### `toml_set_option` + +```c +void toml_set_option(toml_option_t opt); +``` + +Replace the global options. This affects all subsequent parse calls. +Not thread-safe; call once during program initialization. + +**Example — custom allocator:** + +```c +toml_option_t opt = toml_default_option(); +opt.mem_realloc = my_realloc; +opt.mem_free = my_free; +toml_set_option(opt); +``` + +--- + +## Deprecated + +### `toml_table_find` + +```c +toml_datum_t toml_table_find(toml_datum_t table, const char *key); +``` + +Alias for `toml_get()`. Use `toml_get()` instead. diff --git a/includes/tomlc17-R260618/BAREKEYCHAR.md b/includes/tomlc17-R260618/BAREKEYCHAR.md new file mode 100644 index 0000000..2a3df07 --- /dev/null +++ b/includes/tomlc17-R260618/BAREKEYCHAR.md @@ -0,0 +1,43 @@ +# Bare Key Characters in TOML v1.1 + +In TOML v1.1, the definition of a **bare key** (unquoted key) has been significantly expanded from the strict ASCII-only restriction of TOML v1.0 to support internationalization. + +## 1. The TOML v1.0 Definition (Legacy) +In TOML v1.0, a bare key could only contain: +* ASCII letters (`A-Z`, `a-z`) +* ASCII digits (`0-9`) +* Underscores (`_`) +* Dashes (`-`) + +## 2. The TOML v1.1 Expansion +TOML v1.1 allows **non-ASCII Unicode characters** in bare keys. The character ranges are largely aligned with the **XML 1.1 `NameChar`** specification, which covers most letters, digits, and combining marks from scripts worldwide. + +A `bare-key-char` in TOML v1.1 is defined as: +* Any character allowed in v1.0 (`[A-Za-z0-9_-]`). +* Any non-ASCII Unicode codepoint from the following specific ranges: + * `U+00B2`, `U+00B3`, `U+00B9` (Superscripts) + * `U+00BC` to `U+00BE` (Fractions) + * `U+00C0` to `U+00D6` + * `U+00D8` to `U+00F6` + * `U+00F8` to `U+037D` + * `U+037F` to `U+1FFF` + * `U+200C` to `U+200D` (Zero-width joiners) + * `U+203F` to `U+2040` + * `U+2070` to `U+218F` + * `U+2460` to `U+24FF` + * `U+2C00` to `U+2FEF` + * `U+3001` to `U+D7FF` + * `U+F900` to `U+FDCF` + * `U+FDF0` to `U+FFFD` + * `U+10000` to `U+EFFFF` (Supplementary Planes) + +## Summary Table + +| Feature | TOML v1.0 | TOML v1.1 | +| :--- | :--- | :--- | +| **Allowed Characters** | `[A-Za-z0-9_-]` | `[A-Za-z0-9_-]` + Wide Unicode ranges | +| **Examples** | `key = "val"` | `café = "coffee"`, `ñoño = "val"`, `日本語 = "val"` | +| **Prohibited** | `café = "val"` (must use `"café"`) | Whitespace, `.`, `[`, `]`, `#`, `=`, etc. | + +## Implementation Status in `tomlc17` +The `tomlc17` project recently updated its scanner in commit `df7ecd4` to support these TOML v1.1 ranges using a new helper function `is_unicode_bare_key_char()`. This allows the parser to correctly handle unquoted internationalized keys. diff --git a/includes/tomlc17-R260618/CLAUDE.md b/includes/tomlc17-R260618/CLAUDE.md new file mode 100644 index 0000000..21be9c5 --- /dev/null +++ b/includes/tomlc17-R260618/CLAUDE.md @@ -0,0 +1,70 @@ +# CLAUDE.md + +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. +- **One thing at a time**: Never combine multiple refactoring, optimization, or feature items in a single edit session. Work on them sequentially: implement one, test it, get verification, commit it, and only then proceed to the next. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +## 5. Sprite Environment + +- Do not automatically call `sprite-env checkpoints create` or create checkpoints during the session unless explicitly requested by the user. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. diff --git a/includes/tomlc17-R260618/LICENSE b/includes/tomlc17-R260618/LICENSE new file mode 100644 index 0000000..cd2995d --- /dev/null +++ b/includes/tomlc17-R260618/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024-2026, CK Tan +https://github.com/cktan/tomlc17 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/includes/tomlc17-R260618/Makefile b/includes/tomlc17-R260618/Makefile new file mode 100644 index 0000000..14cabe0 --- /dev/null +++ b/includes/tomlc17-R260618/Makefile @@ -0,0 +1,61 @@ +.NOTPARALLEL: + +prefix ?= /usr/local +# remove trailing / +override prefix := $(prefix:%/=%) +DIRS := src simple test + +BUILDDIRS := $(DIRS:%=build-%) +CLEANDIRS := $(DIRS:%=clean-%) +FORMATDIRS := $(DIRS:%=format-%) +TESTDIRS := $(DIRS:%=test-%) + +################################### + +# Define PCFILE content based on prefix +define PCFILE +prefix=${prefix} +Name: libtomlc17 +URL: https://github.com/cktan/tomlc17/ +Description: TOML C library in c17. +Version: v1.0 +Libs: -L$${prefix}/lib -ltomlc17 +Cflags: -I$${prefix}/include +endef + +# Make it available to subshells +export PCFILE + +################################# + +all: $(BUILDDIRS) + +$(BUILDDIRS): + $(MAKE) -C $(@:build-%=%) + +install: all + install -d $(DESTDIR)${prefix}/include + install -d $(DESTDIR)${prefix}/lib + install -d $(DESTDIR)${prefix}/lib/pkgconfig + install -m 0644 src/tomlc17.h $(DESTDIR)${prefix}/include/ + install -m 0644 src/tomlcpp.hpp $(DESTDIR)${prefix}/include/ + install -m 0644 src/libtomlc17.a $(DESTDIR)${prefix}/lib/ + @echo "$$PCFILE" >> $(DESTDIR)${prefix}/lib/pkgconfig/libtomlc17.pc + +test: $(TESTDIRS) + +format: $(FORMATDIRS) + +clean: $(CLEANDIRS) + +$(TESTDIRS): + $(MAKE) -C $(@:test-%=%) test + +$(CLEANDIRS): + $(MAKE) -C $(@:clean-%=%) clean + +$(FORMATDIRS): + $(MAKE) -C $(@:format-%=%) format + +.PHONY: $(DIRS) $(BUILDDIRS) $(TESTDIRS) $(CLEANDIRS) $(FORMATDIRS) +.PHONY: all install test format clean diff --git a/includes/tomlc17-R260618/OPTIONS.md b/includes/tomlc17-R260618/OPTIONS.md new file mode 100644 index 0000000..9ad1767 --- /dev/null +++ b/includes/tomlc17-R260618/OPTIONS.md @@ -0,0 +1,69 @@ +# tomlc17 Options + +## Setting Options: Custom Allocators + +By default, `tomlc17` uses the standard C library `realloc()` and `free()` +functions for memory management. You can override this behavior by setting +custom allocator functions using `toml_set_option()`. + +This is useful when you need to: +- Use a custom memory pool or arena allocator +- Track memory allocations for debugging +- Enforce memory constraints in embedded or resource-limited environments +- Integrate with a specialized memory management system + +### Example: Using Custom Allocators + +```c +#include "tomlc17.h" +#include + +// Custom realloc function +void* my_realloc(void *ptr, size_t size) { + // Your custom allocation logic here + if (size == 0) { + free(ptr); + return NULL; + } + return realloc(ptr, size); +} + +// Custom free function +void my_free(void *ptr) { + // Your custom deallocation logic here + free(ptr); +} + +int main() { + // Set custom allocators + toml_option_t opt = toml_default_option(); + opt.mem_realloc = my_realloc; + opt.mem_free = my_free; + toml_set_option(opt); + + // Now all subsequent parse calls will use your custom allocators + toml_result_t result = toml_parse_file_ex("config.toml"); + + if (result.ok) { + // Use the parsed data + toml_free(result); + } + + return 0; +} +``` + +### Important Notes + +- **Initialization:** Call `toml_set_option()` once during program startup, before + any parsing operations. +- **Thread Safety:** `toml_set_option()` is not thread-safe. Set options only during + initialization in a single-threaded context. +- **Custom Allocator Requirements:** Your custom `mem_realloc` function should behave + like standard `realloc()`: + - When `size > 0`: allocate or resize memory, return pointer or NULL on failure + - When `size == 0`: free memory and return NULL + - Your custom `mem_free` function should handle NULL pointers gracefully + +For more details, see the [`toml_set_option()` documentation](API.md#toml_set_option) +in API.md. diff --git a/includes/tomlc17-R260618/README.md b/includes/tomlc17-R260618/README.md new file mode 100644 index 0000000..7dc4092 --- /dev/null +++ b/includes/tomlc17-R260618/README.md @@ -0,0 +1,150 @@ +# tomlc17 + +A lightweight, strictly compliant TOML v1.1 parser for C and C++. + +## Overview + +`tomlc17` parses TOML documents into an in-memory tree structure for +straightforward navigation. It is optimized for clean integration and +efficient execution, utilizing a single-pass scanner, a dedicated +string memory pool, and safe recursive teardowns. + +* **Compliance:** Fully implements TOML v1.1 and passes the standard +`toml-test` validation suite. +* **Compatibility:** Written in C17. Fully compatible with C99 and C++. +* **Modern C++ Support:** Includes dedicated C++20 accessors (see +`README_CXX.md`). +* **Zero-Friction Integration:** Amalgamated design. Simply drop +`tomlc17.h` and `tomlc17.c` into your source tree, or build it as a +library. + +## Quick Start + +For complete API details, refer to [`API.md`](API.md). + +### Example: Parsing & Extraction + +Parsing a toml document creates a tree data structure in memory that +reflects the document. Information can be extracted by navigating this +data structure. + + +```c +/* + * Parse the config file simple.toml: + * + * [server] + * host = "www.example.com" + * port = [8080, 8181, 8282] + * + */ +#include "../src/tomlc17.h" +#include +#include +#include +#include + +static void error(const char *msg, const char *msg1) { + fprintf(stderr, "ERROR: %s%s\n", msg, msg1 ? msg1 : ""); + exit(1); +} + +int main() { + // Parse the toml file + toml_result_t result = toml_parse_file_ex("simple.toml"); + + // Check for parse error + if (!result.ok) { + error(result.errmsg, 0); + } + + // Extract values + toml_datum_t host = toml_seek(result.toptab, "server.host"); + toml_datum_t port = toml_seek(result.toptab, "server.port"); + + // Print server.host + if (host.type != TOML_STRING) { + error("missing or invalid 'server.host' property in config", 0); + } + printf("server.host = %s\n", host.u.s); + + // Print server.port + if (port.type != TOML_ARRAY) { + error("missing or invalid 'server.port' property in config", 0); + } + printf("server.port = ["); + for (int i = 0; i < port.u.arr.size; i++) { + toml_datum_t elem = port.u.arr.elem[i]; + if (elem.type != TOML_INT64) { + error("server.port element not an integer", 0); + } + printf("%s%" PRId64, i ? ", " : "", elem.u.int64); + } + printf("]\n"); + + // Done! + toml_free(result); + return 0; +} +``` + + +## Building + +For debug build: +```bash +export DEBUG=1 +make +``` + +For release build: +```bash +unset DEBUG +make +``` + +## Running tests + +We run the official `toml-test` as described +[here](https://github.com/toml-lang/toml-test). Refer to +[this +section](https://github.com/toml-lang/toml-test?tab=readme-ov-file#installation) +for prerequisites to run the tests. + +The following command invokes the tests: + +```bash +make test +``` + +As of May 7, 2025, all tests passed for TOML v1.0: + +``` +toml-test v0001-01-01 [/home/cktan/p/tomlc17/test/stdtest/driver]: using embedded tests + valid tests: 185 passed, 0 failed +invalid tests: 371 passed, 0 failed +``` + +As of Dec 25, 2025, all tests passed for TOML v1.1: + +``` +toml-test v0001-01-01 [/home/cktan/p/tomlc17/test/stdtest/driver] [no encoder] + valid tests: 214 passed, 0 failed +encoder tests: no encoder command given +invalid tests: 466 passed, 0 failed +``` + +## Installing + +The install command will copy `tomlc17.h`, `tomlcpp.hpp` and +`libtomlc17.a` to the `$prefix/include` and `$prefix/lib` directories. + +```bash +unset DEBUG +make clean install prefix=/usr/local +``` + +## Options + +For information on configuring library options, such as setting custom memory +allocators, see [`OPTIONS.md`](OPTIONS.md). diff --git a/includes/tomlc17-R260618/README_CXX.md b/includes/tomlc17-R260618/README_CXX.md new file mode 100644 index 0000000..f550a19 --- /dev/null +++ b/includes/tomlc17-R260618/README_CXX.md @@ -0,0 +1,73 @@ +# C++20 Accessors in tomlc17 + +The library includes C++ class and routines in the `toml` namespace. These constructs provide: + +- **Safe resource management** - Result class respects RAII rules. +- **Direct subtable access** – Retrieves values from nested subtables directly. +- **Safe value access** – Uses C++ `std::optional` for error handling, throwing exceptions on invalid access. +- **Time and date support** – Represents timestamps as `std::chrono` objects for precise time handling. +- **Array conversion** – Converts datum arrays into integer or string vectors for easy manipulation. + +## Usage + +Here is a simple example: + +```c++ +/* + * Parse the config file simple.toml: + * + * [server] + * host = "www.example.com" + * port = [8080, 8181, 8282] + * + */ +#include "../src/tomlcpp.hpp" +#include +#include + +using std::cout; + +static void error(const char *msg) { + fprintf(stderr, "ERROR: %s\n", msg); + exit(1); +} + +int main() { + // Parse the toml file + toml::Result result = toml::parse_file_ex("simple.toml"); + + // Check for parse error + if (!result.ok()) { + error(result.errmsg()); + } + + // Extract values + std::string host; + std::vector port; + + try { + // use the Datum::get() method + host = result.get({"server", "host"})->as_str().value(); + } catch (const std::bad_optional_access &ex) { + error("missing or invalid 'server.host' property in config"); + } + + try { + // use the Datum::seek() method + port = result.seek("server.port")->as_intvec().value(); + } catch (const std::bad_optional_access &ex) { + error("missing or invalid 'server.port' property in config"); + } + + // Print values + cout << "server.host = " << host << "\n"; + cout << "server.port = ["; + for (size_t i = 0; i < port.size(); i++) { + cout << (i ? ", " : "") << port[i]; + } + cout << "]\n"; + + // Done! No need to call toml_free(). Result::~Result() will handle destruction. + return 0; +} +``` diff --git a/includes/tomlc17-R260618/simple/.gitignore b/includes/tomlc17-R260618/simple/.gitignore new file mode 100644 index 0000000..50ebe46 --- /dev/null +++ b/includes/tomlc17-R260618/simple/.gitignore @@ -0,0 +1,3 @@ +/simple +/simplecpp +/repro \ No newline at end of file diff --git a/includes/tomlc17-R260618/simple/Makefile b/includes/tomlc17-R260618/simple/Makefile new file mode 100644 index 0000000..db08b4b --- /dev/null +++ b/includes/tomlc17-R260618/simple/Makefile @@ -0,0 +1,59 @@ +EXEC = simple simplecpp + +# set up CFLAGS / CXXFLAGS +CFLAGS = -std=c17 -Wmissing-declarations -Wall -Wextra -MMD + +ifdef DEBUG + CFLAGS += -O0 -g -fsanitize=address,undefined +else + CFLAGS += -O3 -DNDEBUG +endif +# Add -fpic only for non-Windows platforms +ifneq ($(OS), Windows_NT) + CFLAGS += -fpic +endif + +CXXFLAGS = $(subst -std=c17,-std=c++20,$(CFLAGS)) + +# Test if C++ is available +HAVE_CXX := $(shell command -v $(CXX) 2> /dev/null) + +# 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) + + + +all: $(EXEC) + +simple: simple.c ../src/libtomlc17.a + $(CC) $(CFLAGS) -o $@ $@.c -L../src -ltomlc17 + +repro: repro.c ../src/libtomlc17.a + $(CC) $(CFLAGS) -o $@ $@.c -L../src -ltomlc17 + + +simplecpp: simplecpp.cpp ../src/libtomlc17.a +ifndef HAVE_CXX + @echo "INFO: skipping $@ because no $(CXX) compiler found" +else ifeq ($(CXX20_SUPPORT),no) + @echo "INFO: skipping $@ because $(CXX) does not support C++20" +else + $(CXX) $(CXXFLAGS) -o $@ $@.cpp -L ../src -l tomlc17 +endif + +-include simple.d simplecpp.d + +test: all + ./simple > /dev/null + [ ! -x ./simplecpp ] || ./simplecpp > /dev/null + +clean: + rm -f *.o *.d *.a $(EXEC) + +distclean: clean + +format: + clang-format -i *.[ch] *.cpp + +.PHONY: all clean distclean format + diff --git a/includes/tomlc17-R260618/simple/repro.c b/includes/tomlc17-R260618/simple/repro.c new file mode 100644 index 0000000..a08e39b --- /dev/null +++ b/includes/tomlc17-R260618/simple/repro.c @@ -0,0 +1,46 @@ +#include "../src/tomlc17.h" +#include +#include +#include + +const char *PATH = "/tmp/t.toml"; + +static void setup() { + const char *text = + "[default]\n" + "\n" + "[wayland_displays.\"$WAYLAND_DISPLAY\"]\n" + "seats = [ \"$XDG_SEAT\" ] \n" + "[[clipboards.Default.mime_type_groups]]\n" + "group = [ \"TEXT\", \"STRING\", \"UTF8_STRING\", \"text/plain\" ]\n" + "xxxx xx xx\n"; + + (void)text; + FILE *fp = fopen(PATH, "w"); + fprintf(fp, "%s", text); + fclose(fp); +} + +static void run() { + + toml_result_t root = toml_parse_file_ex(PATH); + + if (!root.ok) { + fprintf(stderr, "toml_parse_file_ex: %s\n", root.errmsg); + toml_free(root); + exit(-1); + } + + toml_datum_t wayland_displays = + toml_seek(root.toptab, "main.wayland_displays"); + toml_datum_t clipboards = toml_seek(root.toptab, "main.clipboards"); + (void) + + toml_free(root); +} + +int main() { + setup(); + run(); + return 0; +} diff --git a/includes/tomlc17-R260618/simple/simple.c b/includes/tomlc17-R260618/simple/simple.c new file mode 100644 index 0000000..eca51ae --- /dev/null +++ b/includes/tomlc17-R260618/simple/simple.c @@ -0,0 +1,62 @@ +/* + * Parse the config file simple.toml: + * + * [server] + * host = "www.example.com" + * port = [8080, 8181, 8282] + * + */ +#include "../src/tomlc17.h" +#include +#include +#include + +static void error(const char *msg, const char *msg1) { + fprintf(stderr, "ERROR: %s%s\n", msg, msg1 ? msg1 : ""); + exit(1); +} + +int main() { + // Parse the toml file + toml_result_t result = toml_parse_file_ex("simple.toml"); + + // Check for parse error + if (!result.ok) { + error(result.errmsg, 0); + } + + // Extract values + toml_datum_t host = toml_seek(result.toptab, "server.host"); + toml_datum_t port = toml_seek(result.toptab, "server.port"); + + // Print server.host + if (host.type != TOML_STRING) { + error("missing or invalid 'server.host' property in config", 0); + } + printf("server.host = %s\n", host.u.s); + + // Print server.port + if (port.type != TOML_ARRAY) { + error("missing or invalid 'server.port' property in config", 0); + } + printf("server.port = ["); + for (int i = 0; i < port.u.arr.size; i++) { + toml_datum_t elem = port.u.arr.elem[i]; + if (elem.type != TOML_INT64) { + error("server.port element not an integer", 0); + } + printf("%s%d", i ? ", " : "", (int)elem.u.int64); + } + printf("]\n"); + + // Negative test + toml_datum_t dummy = toml_seek(result.toptab, "server.dummy"); + if (dummy.type != TOML_UNKNOWN) { + printf("Error: dummy element exists\n"); + exit(1); + } + + // Done! + toml_free(result); + return 0; +} diff --git a/includes/tomlc17-R260618/simple/simple.toml b/includes/tomlc17-R260618/simple/simple.toml new file mode 100644 index 0000000..c4c5f3a --- /dev/null +++ b/includes/tomlc17-R260618/simple/simple.toml @@ -0,0 +1,3 @@ +[server] +host = "www.example.com" +port = [8080, 8181, 8282] diff --git a/includes/tomlc17-R260618/simple/simplecpp.cpp b/includes/tomlc17-R260618/simple/simplecpp.cpp new file mode 100644 index 0000000..0753182 --- /dev/null +++ b/includes/tomlc17-R260618/simple/simplecpp.cpp @@ -0,0 +1,58 @@ +/* + * Parse the config file simple.toml: + * + * [server] + * host = "www.example.com" + * port = [8080, 8181, 8282] + * + */ +#include "../src/tomlcpp.hpp" +#include +#include + +using std::cout; + +static void error(const char *msg) { + fprintf(stderr, "ERROR: %s\n", msg); + exit(1); +} + +int main() { + // Parse the toml file + toml::Result result = toml::parse_file_ex("simple.toml"); + + // Check for parse error + if (!result.ok()) { + error(result.errmsg()); + } + + // Extract values + std::string host; + std::vector port; + + try { + // use the Datum::get() method + host = result.get({"server", "host"})->as_str().value(); + } catch (const std::bad_optional_access &ex) { + error("missing or invalid 'server.host' property in config"); + } + + try { + // use the Datum::seek() method + port = result.seek("server.port")->as_intvec().value(); + } catch (const std::bad_optional_access &ex) { + error("missing or invalid 'server.port' property in config"); + } + + // Print values + cout << "server.host = " << host << "\n"; + cout << "server.port = ["; + for (size_t i = 0; i < port.size(); i++) { + cout << (i ? ", " : "") << port[i]; + } + cout << "]\n"; + + // Done! No need to call toml_free(). Result::~Result() will handle + // destruction. + return 0; +} diff --git a/includes/tomlc17-R260618/src/.gitignore b/includes/tomlc17-R260618/src/.gitignore new file mode 100644 index 0000000..f3c8330 --- /dev/null +++ b/includes/tomlc17-R260618/src/.gitignore @@ -0,0 +1,4 @@ +/scanvalue +/scankey +/parser +/*.so.* diff --git a/includes/tomlc17-R260618/src/Makefile b/includes/tomlc17-R260618/src/Makefile new file mode 100644 index 0000000..4a0891c --- /dev/null +++ b/includes/tomlc17-R260618/src/Makefile @@ -0,0 +1,56 @@ +HFILES := tomlc17.h +CFILES := tomlc17.c +OBJ := $(CFILES:.c=.o) + +# set up LIB_SHARED +LIB_VERSION := 1.0 +LIB := libtomlc17.a +UNAME := $(shell uname) +ifeq ($(UNAME), Darwin) + LIB_SHARED = +else ifeq ($(OS), Windows_NT) + LIB_SHARED = +else + LIB_SHARED = libtomlc17.so.$(LIB_VERSION) +endif +EXEC = + +# set up CFLAGS / CXXFLAGS +CFLAGS := -std=c17 -Wmissing-declarations -Wall -Wextra -MMD + +ifdef DEBUG + CFLAGS += -O0 -g -fsanitize=address,undefined +else + CFLAGS += -O3 -DNDEBUG +endif +# Add -fpic only for non-Windows platforms +ifneq ($(OS), Windows_NT) + CFLAGS += -fpic +endif + + +all: $(EXEC) $(LIB) $(LIB_SHARED) + +*.o: $(HFILES) + +libtomlc17.a: tomlc17.o + ar -rcs $@ $^ + +$(LIB_SHARED): tomlc17.o + $(CC) -shared -o $@ $^ + +-include $(OBJ:%.o=%.d) $(EXEC:%=%.d) + +test: all + +clean: + rm -f *.o *.d *.a *.so.* $(EXEC) + +distclean: clean + +format: + clang-format -i *.[ch] *.hpp + + +.PHONY: all clean distclean format + diff --git a/includes/tomlc17-R260618/src/tomlc17.c b/includes/tomlc17-R260618/src/tomlc17.c new file mode 100644 index 0000000..9c8855f --- /dev/null +++ b/includes/tomlc17-R260618/src/tomlc17.c @@ -0,0 +1,3150 @@ +/* Copyright (c) 2024-2026, CK Tan. + * https://github.com/cktan/tomlc17/blob/main/LICENSE + */ +#include "tomlc17.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const toml_datum_t DATUM_ZERO = {0}; + +static toml_option_t toml_option = {0, realloc, free}; + +#define MALLOC(n) toml_option.mem_realloc(0, n) +#define REALLOC(p, n) toml_option.mem_realloc(p, n) +#define FREE(p) toml_option.mem_free(p) + +#define DO(x) \ + if (x) \ + return -1; \ + else \ + (void)0 + +// Copy string src to dst where dst is limited to dstsz that includes +// NUL. Return 0 on success, -1 otherwise (because src[] is longer than dst[]). +static inline int copystring(char *dst, int dstsz, const char *src) { + int srcsz = strlen(src) + 1; + if (srcsz > dstsz) { + return -1; + } + memcpy(dst, src, srcsz); + return 0; +} + +/* + * Error buffer + */ +typedef struct ebuf_t ebuf_t; +struct ebuf_t { + char *ptr; + int len; +}; + +/* + * Format an error into ebuf[]. Always return -1. + */ +static int SETERROR(ebuf_t ebuf, int lineno, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + char *p = ebuf.ptr; + char *q = p + ebuf.len; + if (lineno) { + snprintf(p, p < q ? q - p : 0, "(line %d) ", lineno); + p += strlen(p); + } + vsnprintf(p, p < q ? q - p : 0, fmt, args); + va_end(args); + return -1; +} + +/* + * Memory pool. Allocated a big block once and hand out piecemeal. + */ +typedef struct pool_t pool_t; +struct pool_t { + int max; // size of buf[] + int top; // offset of first free byte in buf[] + char buf[1]; // first byte starts here +}; + +/** + * Create a memory pool of N bytes. Return the memory pool on + * success, or NULL if out of memory. + */ +static pool_t *pool_create(int N) { + if (N <= 0) { + N = 100; // minimum + } + int totalsz = sizeof(pool_t) + N; + pool_t *pool = MALLOC(totalsz); + if (!pool) { + return NULL; + } + memset(pool, 0, totalsz); + pool->max = N; + return pool; +} + +/** + * Destroy a memory pool. + */ +static void pool_destroy(pool_t *pool) { FREE(pool); } + +/** + * Allocate n bytes from pool. Return the memory allocated on + * success, or NULL if out of memory. + */ +static char *pool_alloc(pool_t *pool, int n) { + if (pool->top + n > pool->max) { + return NULL; + } + char *ret = pool->buf + pool->top; + pool->top += n; + return ret; +} + +// Cell: expandable buffer that behaves like realloc +typedef struct cell_t cell_t; +struct cell_t { + uint32_t top, max; + // first byte of data starts here (i.e. &(*this)[1]) +}; + +static char *cell_realloc(char *p, int size) { + assert(size >= 0); + if (p == NULL) { + // first malloc + cell_t *tmp = REALLOC(NULL, size + sizeof(cell_t)); + if (!tmp) { + return 0; // out of memory + } + cell_t *cp = tmp; + cp->max = size; + cp->top = size; + return (char *)&cp[1]; + } + + // obtain a handle to cell info + cell_t *cp = (cell_t *)(p - sizeof(cell_t)); + if ((uint32_t)size <= cp->max) { + // cell is big enough for the new size. DONE. + cp->top = size; + return p; + } + + // need to expand. add 30% margin. + int newmax = size * 1.3 + 100; + cell_t *tmp = REALLOC(cp, newmax + sizeof(cell_t)); + if (!tmp) { + return 0; // out of memory + } + cp = tmp; + cp->max = newmax; + cp->top = size; + return (char *)&cp[1]; +} + +static void cell_free(char *p) { + if (p) { + cell_t *cp = (cell_t *)(p - sizeof(cell_t)); + FREE(cp); + } +} + +/* This is a string view. */ +typedef struct span_t span_t; +struct span_t { + const char *ptr; + int len; +}; + +/* Represents a multi-part key */ +#define KEYPARTMAX 10 +typedef struct keypart_t keypart_t; +struct keypart_t { + int nspan; + span_t span[KEYPARTMAX]; +}; + +static int utf8_to_ucs(const char *s, int len, uint32_t *ret); +static int ucs_to_utf8(uint32_t code, char buf[4]); + +// flags for toml_datum_t::flag. +#define FLAG_INLINED 1 +#define FLAG_STDEXPR 2 +#define FLAG_EXPLICIT 4 + +// Maximum levels of brackets and braces to prevent +// stack overflow during recursive descent of the parser. +#define BRACKET_LEVEL_MAX 30 +#define BRACE_LEVEL_MAX 30 +#define TABLE_MAX (1 << 14) // 16k +#define ARRAY_MAX (1 << 14) // 16k + +enum toktyp_t { + TOK_DOT = 1, + TOK_EQUAL, + TOK_COMMA, + TOK_LBRACK, // [ + TOK_LLBRACK, // [[ + TOK_RBRACK, // ] + TOK_RRBRACK, // ]] + TOK_LBRACE, // { + TOK_RBRACE, // } + TOK_LIT, + TOK_STRING, // "string" + TOK_MLSTRING, // """multi-line-string""" + TOK_LITSTRING, // 'lit-string' + TOK_MLLITSTRING, // '''multi-line-lit-string''' + TOK_TIME, + TOK_DATE, + TOK_DATETIME, + TOK_DATETIMETZ, + TOK_INTEGER, + TOK_FLOAT, + TOK_BOOL, + TOK_ENDL, + TOK_FIN = -5000, // EOF +}; +typedef enum toktyp_t toktyp_t; +typedef struct scanner_t scanner_t; + +/* Remember the current state of a scanner */ +typedef struct scanner_state_t scanner_state_t; +struct scanner_state_t { + scanner_t *sp; + const char *cur; // points into scanner_t::src[] + int lineno; // current line number + const char *line_start; +}; + +// A scan token +typedef struct token_t token_t; +struct token_t { + toktyp_t toktyp; + int lineno; + int colno; + span_t str; + + // values represented by str + union { + const char *escp; // point to an esc char in str + int64_t int64; + double fp64; + bool b1; + struct { + // validity depends on toktyp for TIME, DATE, DATETIME, DATETIMETZ + int year, month, day, hour, minute, sec, usec; + int tz; // +- minutes + } tsval; + } u; +}; + +// Scanner object +struct scanner_t { + const char *src; // src[] is a NUL-terminated string + const char *endp; // end of src[]. always pointing at a NUL char. + const char *cur; // current char in src[] + int lineno; // line number of current char + const char *line_start; + char *errmsg; // set to ebuf.ptr if there was an error + ebuf_t ebuf; // buffer to store error message + + int bracket_level; // count depth of [ ] + int brace_level; // count depth of { } +}; +static void scan_init(scanner_t *sp, const char *src, int len, char *errbuf, + int errbufsz); +static int scan_key(scanner_t *sp, token_t *tok); +static int scan_value(scanner_t *sp, token_t *tok); +// restore scanner to state before tok was returned +static scanner_state_t scan_mark(scanner_t *sp); +static void scan_restore(scanner_t *sp, scanner_state_t state); + +#ifndef min +static inline int min(int a, int b) { return a < b ? a : b; } +#endif + +// Copy up to dstsz - 1 chars from the current position of the scanner +// to dst, and always terminate dst[] with a NUL if dstsz > 0. +static void scan_copystr(scanner_t *sp, char *dst, int dstsz) { + assert(dstsz > 0); + int len = min(sp->endp - sp->cur, dstsz - 1); // account for NUL + if (len > 0) { + memcpy(dst, sp->cur, len); + dst[len] = '\0'; + } +} + +// Parser object +typedef struct parser_t parser_t; +struct parser_t { + scanner_t scanner; + toml_datum_t toptab; // top table + toml_datum_t *curtab; // current table + pool_t *pool; // memory pool for strings + const char *srcname; // source name copied into the pool, or NULL + ebuf_t ebuf; // buffer to store last error message +}; + +// Find key in tab and return its index. If not found, return -1. +static int tab_find(toml_datum_t *tab, span_t key) { + assert(tab->type == TOML_TABLE); + for (int i = 0, top = tab->u.tab.size; i < top; i++) { + if (tab->u.tab.len[i] == key.len && + 0 == memcmp(tab->u.tab.key[i], key.ptr, key.len)) { + return i; + } + } + return -1; +} + +// Put key into tab dictionary. Return a pointer to the datum for the key. +// If the key already exists, returns a pointer to its existing datum (the +// datum is NOT zeroed — callers must check datum->type to detect duplicates). +// If the key is new, appends it with a zero datum and returns a pointer to it. +// Returns NULL on allocation failure. +// +// Dual-use: tab_add() uses the non-zero type to detect and reject duplicates; +// datum_copy/datum_merge() use it to locate or create a slot for writing. +static toml_datum_t *tab_emplace(toml_datum_t *tab, span_t key, + const char **reason) { + assert(tab->type == TOML_TABLE); + int i = tab_find(tab, key); + if (i >= 0) { + return &tab->u.tab.value[i]; + } + + // Expand pkey[], plen[] and value[]. + int N = tab->u.tab.size; + if (N >= TABLE_MAX) { + *reason = "table too large"; + return NULL; + } + + { + char **pkey = (char **)cell_realloc((char *)(void *)tab->u.tab.key, + sizeof(*pkey) * (N + 1)); + int *plen = + (int *)cell_realloc((char *)tab->u.tab.len, sizeof(*plen) * (N + 1)); + toml_datum_t *value = (toml_datum_t *)cell_realloc( + (char *)tab->u.tab.value, sizeof(*value) * (N + 1)); + + // on success, must save new pointers in tab->u.tab because the + // old memory areas are gone. + if (pkey) { + tab->u.tab.key = (const char **)pkey; + } + if (plen) { + tab->u.tab.len = plen; + } + if (value) { + tab->u.tab.value = value; + } + + // if any fail, it is safe to bail out. + if (!pkey || !plen || !value) { + *reason = "out of memory"; + return NULL; + } + } + + // There is sufficient space in all the arrays for one more element. + // Append the new key. The value is set to DATUM_ZERO. Caller will + // overwrite with a valid datum. + tab->u.tab.size = N + 1; + tab->u.tab.key[N] = (char *)key.ptr; + tab->u.tab.len[N] = key.len; + tab->u.tab.value[N] = DATUM_ZERO; + return &tab->u.tab.value[N]; +} + +// Add a new key in tab. Return 0 on success, -1 otherwise. +// On error, *reason will point to an error message. +static int tab_add(toml_datum_t *tab, span_t newkey, toml_datum_t newvalue, + const char **reason) { + assert(tab->type == TOML_TABLE); + toml_datum_t *pvalue = tab_emplace(tab, newkey, reason); + if (!pvalue) { + return -1; + } + if (pvalue->type) { + *reason = "duplicate key"; + return -1; + } + *pvalue = newvalue; + return 0; +} + +// Add a new element into an array. Return 0 on success, -1 otherwise. +// On error, *reason will point to an error message. +static toml_datum_t *arr_emplace(toml_datum_t *arr, const char **reason) { + assert(arr->type == TOML_ARRAY); + int n = arr->u.arr.size; + if (n >= ARRAY_MAX) { + *reason = "array too large"; + return NULL; + } + toml_datum_t *elem = (toml_datum_t *)cell_realloc((char *)arr->u.arr.elem, + sizeof(*elem) * (n + 1)); + if (!elem) { + *reason = "out of memory"; + return NULL; + } + arr->u.arr.elem = elem; + arr->u.arr.size = n + 1; + elem[n] = DATUM_ZERO; + return &elem[n]; +} + +// ------------------- parser section +static int parse_norm(parser_t *pp, token_t tok, span_t *ret_span); +static int parse_val(parser_t *pp, token_t tok, toml_datum_t *ret); +static int parse_keyvalue_expr(parser_t *pp, token_t tok); +static int parse_std_table_expr(parser_t *pp, token_t tok); +static int parse_array_table_expr(parser_t *pp, token_t tok); + +static toml_datum_t mkdatum(toml_type_t ty) { + toml_datum_t ret = {0}; + ret.type = ty; + if (ty == TOML_DATE || ty == TOML_TIME || ty == TOML_DATETIME || + ty == TOML_DATETIMETZ) { + ret.u.ts.year = -1; + ret.u.ts.month = -1; + ret.u.ts.day = -1; + ret.u.ts.hour = -1; + ret.u.ts.minute = -1; + ret.u.ts.second = -1; + ret.u.ts.usec = -1; + ret.u.ts.tz = -1; + } + return ret; +} + +static toml_datum_t mkdatum_at(toml_type_t ty, int lineno, int colno) { + toml_datum_t ret = mkdatum(ty); + ret.lineno = lineno; + ret.colno = colno; + return ret; +} + +// Recursively free any dynamically allocated memory in the datum tree +static void datum_free(toml_datum_t *datum) { + if (datum->type == TOML_TABLE) { + for (int i = 0, top = datum->u.tab.size; i < top; i++) { + datum_free(&datum->u.tab.value[i]); + } + cell_free((char *)(void *)datum->u.tab.key); + cell_free((char *)datum->u.tab.len); + cell_free((char *)datum->u.tab.value); + } else if (datum->type == TOML_ARRAY) { + for (int i = 0, top = datum->u.arr.size; i < top; i++) { + datum_free(&datum->u.arr.elem[i]); + } + cell_free((char *)datum->u.arr.elem); + } + // other types do not allocate memory + *datum = DATUM_ZERO; +} + +// Maps each distinct source-name pointer to a single copy in the destination +// pool (deduplicated), so the merged pool holds at most one copy of each source +// name (keeping it within the r1pool->top + r2pool->top budget). +typedef struct srcmap_t srcmap_t; +struct srcmap_t { + pool_t *pool; + const char **olds; // cell buffer of old pointers + const char **news; // cell buffer of new (copied) pointers + int n, cap; +}; + +// Returns the deduplicated copy of src, or NULL for a NULL input or on +// out-of-memory. Callers distinguish the two: a non-NULL src that yields NULL +// is a hard failure. +static const char *dedup_source(srcmap_t *m, const char *src) { + if (!src) { + return NULL; + } + for (int i = 0; i < m->n; i++) { + if (m->olds[i] == src) { + return m->news[i]; + } + } + // Grow the memo first, so an allocation failure aborts cleanly. + if (m->n == m->cap) { + int newcap = m->cap ? m->cap * 2 : 4; + const char **no = (const char **)cell_realloc((char *)(void *)m->olds, + sizeof(*no) * newcap); + if (!no) { + return NULL; // out of memory + } + m->olds = no; + const char **nn = (const char **)cell_realloc((char *)(void *)m->news, + sizeof(*nn) * newcap); + if (!nn) { + return NULL; // out of memory + } + m->news = nn; + m->cap = newcap; + } + int len = (int)strlen(src) + 1; + char *p = pool_alloc(m->pool, len); + if (!p) { + return NULL; // out of memory + } + memcpy(p, src, len); + m->olds[m->n] = src; + m->news[m->n] = p; + m->n++; + return p; +} + +// Make a deep copy of src to dst. +// Return 0 on success, -1 otherwise. +static int datum_copy(toml_datum_t *dst, toml_datum_t src, srcmap_t *sm, + const char **reason) { + *dst = mkdatum_at(src.type, src.lineno, src.colno); + dst->flag = src.flag; + switch (src.type) { + case TOML_STRING: + dst->u.str.ptr = pool_alloc(sm->pool, src.u.str.len + 1); + if (!dst->u.str.ptr) { + *reason = "out of memory"; + goto bail; + } + dst->u.str.len = src.u.str.len; + memcpy((char *)dst->u.str.ptr, src.u.str.ptr, src.u.str.len + 1); + break; + case TOML_TABLE: + for (int i = 0; i < src.u.tab.size; i++) { + char *keycopy = pool_alloc(sm->pool, src.u.tab.len[i] + 1); + if (!keycopy) { + *reason = "out of memory"; + goto bail; + } + memcpy(keycopy, src.u.tab.key[i], src.u.tab.len[i] + 1); + span_t newkey = {keycopy, src.u.tab.len[i]}; + toml_datum_t *pvalue = tab_emplace(dst, newkey, reason); + if (!pvalue) { + goto bail; + } + if (datum_copy(pvalue, src.u.tab.value[i], sm, reason)) { + goto bail; + } + } + break; + case TOML_ARRAY: + for (int i = 0; i < src.u.arr.size; i++) { + toml_datum_t *pelem = arr_emplace(dst, reason); + if (!pelem) { + goto bail; + } + if (datum_copy(pelem, src.u.arr.elem[i], sm, reason)) { + goto bail; + } + } + break; + default: + *dst = src; + break; + } + + dst->source = dedup_source(sm, src.source); + if (src.source && !dst->source) { + *reason = "out of memory"; + goto bail; + } + return 0; + +bail: + datum_free(dst); + return -1; +} + +// Check if datum is an array of tables. +static inline bool is_array_of_tables(toml_datum_t datum) { + bool ret = (datum.type == TOML_ARRAY); + for (int i = 0; ret && i < datum.u.arr.size; i++) { + ret = (datum.u.arr.elem[i].type == TOML_TABLE); + } + return ret; +} + +// Merge src into dst. Return 0 on success, -1 otherwise. +static int datum_merge(toml_datum_t *dst, toml_datum_t src, srcmap_t *sm, + const char **reason) { + if (dst->type != src.type) { + datum_free(dst); + return datum_copy(dst, src, sm, reason); + } + switch (src.type) { + case TOML_TABLE: + // for key-value in src: + // override key-value in dst. + for (int i = 0; i < src.u.tab.size; i++) { + char *keycopy = pool_alloc(sm->pool, src.u.tab.len[i] + 1); + if (!keycopy) { + *reason = "out of memory"; + return -1; + } + memcpy(keycopy, src.u.tab.key[i], src.u.tab.len[i] + 1); + span_t key; + key.ptr = keycopy; + key.len = src.u.tab.len[i]; + toml_datum_t *pvalue = tab_emplace(dst, key, reason); + if (!pvalue) { + return -1; + } + if (pvalue->type) { + DO(datum_merge(pvalue, src.u.tab.value[i], sm, reason)); + } else { + datum_free(pvalue); + DO(datum_copy(pvalue, src.u.tab.value[i], sm, reason)); + } + } + return 0; + case TOML_ARRAY: + if (is_array_of_tables(src)) { + // append src array to dst + for (int i = 0; i < src.u.arr.size; i++) { + toml_datum_t *pelem = arr_emplace(dst, reason); + if (!pelem) { + return -1; + } + DO(datum_copy(pelem, src.u.arr.elem[i], sm, reason)); + } + return 0; + } + // fallthru + default: + break; + } + datum_free(dst); + return datum_copy(dst, src, sm, reason); +} + +// Compare the content of a and b. +static bool datum_equiv(toml_datum_t a, toml_datum_t b) { + if (a.type != b.type) { + return false; + } + int N; + switch (a.type) { + case TOML_STRING: + return a.u.str.len == b.u.str.len && + 0 == memcmp(a.u.str.ptr, b.u.str.ptr, a.u.str.len); + case TOML_INT64: + return a.u.int64 == b.u.int64; + case TOML_FP64: + return a.u.fp64 == b.u.fp64 || (isnan(a.u.fp64) && isnan(b.u.fp64)); + case TOML_BOOLEAN: + return !!a.u.boolean == !!b.u.boolean; + case TOML_DATE: + return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month && + a.u.ts.day == b.u.ts.day; + case TOML_TIME: + return a.u.ts.hour == b.u.ts.hour && a.u.ts.minute == b.u.ts.minute && + a.u.ts.second == b.u.ts.second && a.u.ts.usec == b.u.ts.usec; + case TOML_DATETIME: + return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month && + a.u.ts.day == b.u.ts.day && a.u.ts.hour == b.u.ts.hour && + a.u.ts.minute == b.u.ts.minute && a.u.ts.second == b.u.ts.second && + a.u.ts.usec == b.u.ts.usec; + case TOML_DATETIMETZ: + return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month && + a.u.ts.day == b.u.ts.day && a.u.ts.hour == b.u.ts.hour && + a.u.ts.minute == b.u.ts.minute && a.u.ts.second == b.u.ts.second && + a.u.ts.usec == b.u.ts.usec && a.u.ts.tz == b.u.ts.tz; + case TOML_ARRAY: + N = a.u.arr.size; + if (N != b.u.arr.size) { + return false; + } + for (int i = 0; i < N; i++) { + if (!datum_equiv(a.u.arr.elem[i], b.u.arr.elem[i])) { + return false; + } + } + return true; + case TOML_TABLE: + N = a.u.tab.size; + if (N != b.u.tab.size) { + return false; + } + for (int i = 0; i < N; i++) { + int len = a.u.tab.len[i]; + if (len != b.u.tab.len[i]) { + return false; + } + if (0 != memcmp(a.u.tab.key[i], b.u.tab.key[i], len)) { + return false; + } + if (!datum_equiv(a.u.tab.value[i], b.u.tab.value[i])) { + return false; + } + } + return true; + default: + break; + } + return false; +} + +/** + * Override values in r1 using r2. Return a new result. All results + * (i.e., r1, r2 and the returned result) must be freed using toml_free() + * after use. + * + * LOGIC: + * ret = copy of r1 + * for each item x in r2: + * if x is not in ret: + * override + * elif x in ret is NOT of the same type: + * override + * elif x is an array of tables: + * append r2.x to ret.x + * elif x is a table: + * merge r2.x to ret.x + * else: + * override + */ +toml_result_t toml_merge(const toml_result_t *r1, const toml_result_t *r2) { + const char *reason = ""; + toml_result_t ret = {0}; + pool_t *pool = 0; + srcmap_t sm = {0}; + if (!r1->ok) { + reason = "param error: r1 not ok"; + goto bail; + } + if (!r2->ok) { + reason = "param error: r2 not ok"; + goto bail; + } + { + pool_t *r1pool = (pool_t *)r1->__internal; + pool_t *r2pool = (pool_t *)r2->__internal; + pool = pool_create(r1pool->top + r2pool->top); + if (!pool) { + reason = "out of memory"; + goto bail; + } + } + sm.pool = pool; + + // Make a copy of r1 + if (datum_copy(&ret.toptab, r1->toptab, &sm, &reason)) { + goto bail; + } + + // Merge r2 into the result + if (datum_merge(&ret.toptab, r2->toptab, &sm, &reason)) { + goto bail; + } + + cell_free((char *)(void *)sm.olds); + cell_free((char *)(void *)sm.news); + ret.ok = 1; + ret.__internal = pool; + return ret; + +bail: + cell_free((char *)(void *)sm.olds); + cell_free((char *)(void *)sm.news); + pool_destroy(pool); + snprintf(ret.errmsg, sizeof(ret.errmsg), "%s", reason); + return ret; +} + +bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2) { + if (!(r1->ok && r2->ok)) { + return false; + } + return datum_equiv(r1->toptab, r2->toptab); +} + +/** + * Find a key in a toml_table. Return the value of the key if found, + * or a TOML_UNKNOWN otherwise. + */ +toml_datum_t toml_get(toml_datum_t datum, const char *key) { + if (datum.type == TOML_TABLE) { + int n = datum.u.tab.size; + if (n > 0) { + int key_len = strlen(key); + const char **pkey = datum.u.tab.key; + int *plen = datum.u.tab.len; + toml_datum_t *pvalue = datum.u.tab.value; + for (int i = 0; i < n; i++) { + if (plen[i] == key_len && 0 == memcmp(pkey[i], key, key_len)) { + return pvalue[i]; + } + } + } + } + return DATUM_ZERO; +} + +/** + * Locate a value starting from a toml_table. Return the value of the key if + * found, or a TOML_UNKNOWN otherwise. + * + * Note: the multipart-key is separated by DOT, and must not have any escape + * chars. + */ +toml_datum_t toml_seek(toml_datum_t table, const char *multipart_key) { + if (table.type != TOML_TABLE) { + return DATUM_ZERO; + } + + // Make a mutable copy of the multipart_key for splitting + char buf[256]; + if (copystring(buf, sizeof(buf), multipart_key)) { + // if the multipart_key is longer than buffer, just + // signal a not-found. + return DATUM_ZERO; + } + + // Go through the multipart name part by part. + char *p = buf; + toml_datum_t datum = table; + while (datum.type == TOML_TABLE) { + char *q = strchr(p, '.'); + if (q) { + // traverse to next key + *q = 0; + datum = toml_get(datum, p); + p = q + 1; + continue; + } + + // At end of last keypart. + // look up p in the final table + return toml_get(datum, p); + } + + return DATUM_ZERO; +} + +/** + * Return the default options. + */ +toml_option_t toml_default_option(void) { + toml_option_t opt = {0, realloc, free}; + return opt; +} + +/** + * Override the current options. + */ +void toml_set_option(toml_option_t opt) { toml_option = opt; } + +/** + * Free the result returned by toml_parse(). + */ +void toml_free(toml_result_t result) { + datum_free(&result.toptab); + pool_destroy((pool_t *)result.__internal); +} + +/** + * Parse a toml document. + */ +toml_result_t toml_parse_file_ex(const char *fname) { + toml_result_t result = {0}; + FILE *fp = fopen(fname, "r"); + if (!fp) { + snprintf(result.errmsg, sizeof(result.errmsg), "fopen %s: %s", fname, + strerror(errno)); + return result; + } + result = toml_parse_file_named(fp, fname); + fclose(fp); + return result; +} + +toml_result_t toml_parse_file(FILE *fp) { + return toml_parse_file_named(fp, NULL); +} + +/** + * Parse a toml document from a file, tagging datums with name. + */ +toml_result_t toml_parse_file_named(FILE *fp, const char *name) { + toml_result_t result = {0}; + if (!fp) { + snprintf(result.errmsg, sizeof(result.errmsg), "fp is NULL"); + return result; + } + char *buf = 0; + int top = 0; // number of bytes read into buf[] + enum { CHUNKSZ = 8 * 1024 }; // bytes to read per iteration + + // Read file into memory. cell_realloc handles capacity growth, so we only + // need to ask for room for one more chunk (plus a terminating NUL) each pass. + // Drive the loop off fread's return value rather than feof(): feof() only + // reports true after a read has already hit EOF, and this also guarantees buf + // is allocated at least once before the NUL terminator below. + for (;;) { + if (top > INT_MAX - CHUNKSZ - 1) { + snprintf(result.errmsg, sizeof(result.errmsg), "file is too big"); + break; + } + // add 1 to CHUNKSZ so we always have room for terminating NUL. + char *tmp = cell_realloc(buf, top + CHUNKSZ + 1); + if (!tmp) { + snprintf(result.errmsg, sizeof(result.errmsg), "out of memory"); + break; + } + buf = tmp; + + errno = 0; + size_t n = fread(buf + top, 1, CHUNKSZ, fp); + top += n; + if (n < CHUNKSZ) { // short read => EOF or error + if (ferror(fp)) { + snprintf(result.errmsg, sizeof(result.errmsg), "%s", + errno ? strerror(errno) : "Error reading file"); + break; + } + if (feof(fp)) { + break; + } + // small probability of short read due to signal, etc. + } + } + // error? + if (result.errmsg[0]) { + cell_free(buf); + return result; + } + buf[top] = 0; // NUL terminator + + result = toml_parse_named(buf, top, name); + cell_free(buf); + return result; +} + +static void set_source_recursive(toml_datum_t *datum, const char *source) { + datum->source = source; + switch (datum->type) { + case TOML_ARRAY: + for (int i = 0, top = datum->u.arr.size; i < top; i++) { + set_source_recursive(&datum->u.arr.elem[i], source); + } + break; + case TOML_TABLE: + for (int i = 0, top = datum->u.tab.size; i < top; i++) { + set_source_recursive(&datum->u.tab.value[i], source); + } + break; + default: + break; + } +} + +/** + * Parse a toml document. + */ +toml_result_t toml_parse(const char *src, int len) { + return toml_parse_named(src, len, NULL); +} + +toml_result_t toml_parse_named(const char *src, int len, const char *name) { + toml_result_t result = {0}; + parser_t parser = {0}; + parser_t *pp = &parser; + + // Check that src is not NULL. + if (!src) { + snprintf(result.errmsg, sizeof(result.errmsg), "src is NULL"); + goto bail; + } + + // Check that src is NUL terminated. + if (src[len]) { + snprintf(result.errmsg, sizeof(result.errmsg), + "src[] must be NUL terminated"); + goto bail; + } + + // If user insists, check that src[] is a valid utf8 string. + if (toml_option.check_utf8) { + int line = 1; // keeps track of line number + for (int i = 0; i < len;) { + uint32_t ch; + int n = utf8_to_ucs(src + i, len - i, &ch); + if (n < 0) { + snprintf(result.errmsg, sizeof(result.errmsg), + "invalid UTF8 char on line %d", line); + goto bail; + } + if (0xD800 <= ch && ch <= 0xDFFF) { + // explicitly prohibit surrogates (non-scalar unicode code point) + snprintf(result.errmsg, sizeof(result.errmsg), + "invalid UTF8 char \\u%04x on line %d", ch, line); + goto bail; + } + line += (ch == '\n' ? 1 : 0); + i += n; + } + } + + // Initialize parser + pp->toptab = mkdatum_at(TOML_TABLE, 1, 1); + pp->curtab = &pp->toptab; + pp->ebuf.ptr = result.errmsg; // parse error will be printed into pp->ebuf + pp->ebuf.len = sizeof(result.errmsg); + + // Alloc memory pool (extra bytes for NUL term, safety, and the source name) + int namelen = name ? (int)strlen(name) + 1 : 0; + pp->pool = pool_create(len + 10 + namelen); + if (!pp->pool) { + snprintf(result.errmsg, sizeof(result.errmsg), "out of memory"); + goto bail; + } + if (name) { + char *p = pool_alloc(pp->pool, namelen); + if (!p) { + snprintf(result.errmsg, sizeof(result.errmsg), "out of memory"); + goto bail; + } + memcpy(p, name, namelen); + pp->srcname = p; + } + + // Initialize scanner. Scan error will be printed into pp->ebuf. + scan_init(&pp->scanner, src, len, pp->ebuf.ptr, pp->ebuf.len); + + // Keep parsing until FIN + for (;;) { + token_t tok; + if (scan_key(&pp->scanner, &tok)) { + goto bail; + } + // break on FIN + if (tok.toktyp == TOK_FIN) { + break; + } + switch (tok.toktyp) { + case TOK_ENDL: // skip blank lines + continue; + case TOK_LBRACK: + if (parse_std_table_expr(pp, tok)) { + goto bail; + } + break; + case TOK_LLBRACK: + if (parse_array_table_expr(pp, tok)) { + goto bail; + } + break; + default: + // non-blank line: parse an expression + if (parse_keyvalue_expr(pp, tok)) { + goto bail; + } + break; + } + // each expression must be followed by newline + if (scan_key(&pp->scanner, &tok)) { + goto bail; + } + if (tok.toktyp == TOK_FIN || tok.toktyp == TOK_ENDL) { + continue; + } + SETERROR(pp->ebuf, tok.lineno, "ENDL expected"); + goto bail; + } + + // return result + result.ok = true; + set_source_recursive(&pp->toptab, pp->srcname); + result.toptab = pp->toptab; + result.__internal = (void *)pp->pool; + return result; + +bail: + // return error + datum_free(&pp->toptab); + pool_destroy(pp->pool); + result.ok = false; + if (result.errmsg[0] == '\0') { + assert(0); + snprintf(result.errmsg, sizeof(result.errmsg), "Error near line %d\n", + pp->scanner.lineno); + } + return result; +} + +// Convert a (LITSTRING, LIT, MLLITSTRING, MLSTRING, or STRING) token to a +// datum. +static int token_to_string(parser_t *pp, token_t tok, toml_datum_t *ret) { + *ret = mkdatum_at(TOML_STRING, tok.lineno, tok.colno); + span_t span; + DO(parse_norm(pp, tok, &span)); + ret->u.str.ptr = (char *)span.ptr; + ret->u.str.len = span.len; + return 0; +} + +// Convert a TIME/DATE/DATETIME/DATETIMETZ to a datum +static int token_to_timestamp(parser_t *pp, token_t tok, toml_datum_t *ret) { + (void)pp; + static const toml_type_t map[] = {[TOK_TIME] = TOML_TIME, + [TOK_DATE] = TOML_DATE, + [TOK_DATETIME] = TOML_DATETIME, + [TOK_DATETIMETZ] = TOML_DATETIMETZ}; + switch (tok.toktyp) { + case TOK_TIME: + case TOK_DATE: + case TOK_DATETIME: + case TOK_DATETIMETZ: + break; + default: + assert(0 && "unexpected token type"); + return -1; + } + + *ret = mkdatum_at(map[tok.toktyp], tok.lineno, tok.colno); + ret->u.ts.year = tok.u.tsval.year; + ret->u.ts.month = tok.u.tsval.month; + ret->u.ts.day = tok.u.tsval.day; + ret->u.ts.hour = tok.u.tsval.hour; + ret->u.ts.minute = tok.u.tsval.minute; + ret->u.ts.second = tok.u.tsval.sec; + ret->u.ts.usec = tok.u.tsval.usec; + ret->u.ts.tz = tok.u.tsval.tz; + return 0; +} + +// Convert an int64 token to a datum. +static int token_to_int64(parser_t *pp, token_t tok, toml_datum_t *ret) { + (void)pp; + assert(tok.toktyp == TOK_INTEGER); + *ret = mkdatum_at(TOML_INT64, tok.lineno, tok.colno); + ret->u.int64 = tok.u.int64; + return 0; +} + +// Convert a fp64 token to a datum. +static int token_to_fp64(parser_t *pp, token_t tok, toml_datum_t *ret) { + (void)pp; + assert(tok.toktyp == TOK_FLOAT); + *ret = mkdatum_at(TOML_FP64, tok.lineno, tok.colno); + ret->u.fp64 = tok.u.fp64; + return 0; +} + +// Convert a boolean token to a datum. +static int token_to_boolean(parser_t *pp, token_t tok, toml_datum_t *ret) { + (void)pp; + assert(tok.toktyp == TOK_BOOL); + *ret = mkdatum_at(TOML_BOOLEAN, tok.lineno, tok.colno); + ret->u.boolean = tok.u.b1; + return 0; +} + +// Parse a multipart key. Return 0 on success, -1 otherwise. +static int parse_key(parser_t *pp, token_t tok, keypart_t *ret_keypart) { + ret_keypart->nspan = 0; + // key = simple-key | dotted_key + // simple-key = STRING | LITSTRING | LIT + // dotted-key = simple-key (DOT simple-key)+ + if (tok.toktyp != TOK_STRING && tok.toktyp != TOK_LITSTRING && + tok.toktyp != TOK_LIT) { + return SETERROR(pp->ebuf, tok.lineno, "missing key"); + } + + int n = 0; + span_t *kpspan = ret_keypart->span; + + // Normalize the first keypart + if (parse_norm(pp, tok, &kpspan[n])) { + return SETERROR(pp->ebuf, tok.lineno, + "unable to normalize string; probably a unicode issue"); + } + n++; + + // Scan and normalize the second to last keypart + while (1) { + scanner_state_t mark = scan_mark(&pp->scanner); + + // Eat the dot if it is there + DO(scan_key(&pp->scanner, &tok)); + + // If not a dot, we are done with keyparts. + if (tok.toktyp != TOK_DOT) { + scan_restore(&pp->scanner, mark); + break; + } + + // Scan the n-th key + DO(scan_key(&pp->scanner, &tok)); + + if (tok.toktyp != TOK_STRING && tok.toktyp != TOK_LITSTRING && + tok.toktyp != TOK_LIT) { + return SETERROR(pp->ebuf, tok.lineno, "expects a string in dotted-key"); + } + + if (n >= KEYPARTMAX) { + return SETERROR(pp->ebuf, tok.lineno, "too many key parts"); + } + + // Normalize the n-th key. + DO(parse_norm(pp, tok, &kpspan[n])); + n++; + } + + // This key has n parts. + ret_keypart->nspan = n; + return 0; +} + +// Starting at toptab, descend following keypart[]. If a key does not +// exist in the current table, create a new table entry for the +// key. Returns the final table represented by the key. +static toml_datum_t *descend_keypart(parser_t *pp, int lineno, int colno, + toml_datum_t *toptab, keypart_t *keypart, + bool stdtabexpr) { + toml_datum_t *tab = toptab; // current tab + + for (int i = 0; i < keypart->nspan; i++) { + const char *reason; + // Find the i-th keypart + int j = tab_find(tab, keypart->span[i]); + // Not found: add a new (key, tab) pair. + if (j < 0) { + toml_datum_t newtab = mkdatum_at(TOML_TABLE, lineno, colno); + newtab.flag |= stdtabexpr ? FLAG_STDEXPR : 0; + if (tab_add(tab, keypart->span[i], newtab, &reason)) { + SETERROR(pp->ebuf, lineno, "%s", reason); + return NULL; + } + tab = &tab->u.tab.value[tab->u.tab.size - 1]; // descend + continue; + } + + // Found: extract the value of the key. + toml_datum_t *value = &tab->u.tab.value[j]; + + // If the value is a table, descend. + if (value->type == TOML_TABLE) { + tab = value; // descend + continue; + } + + // If the value is an array: locate the last entry and descend. + if (value->type == TOML_ARRAY) { + // If empty: error. + if (value->u.arr.size <= 0) { + SETERROR(pp->ebuf, lineno, "array %s has no elements", + keypart->span[i].ptr); + return NULL; + } + + // Extract the last element of the array. + value = &value->u.arr.elem[value->u.arr.size - 1]; + + // It must be a table! + if (value->type != TOML_TABLE) { + SETERROR(pp->ebuf, lineno, "array %s must be array of tables", + keypart->span[i].ptr); + return NULL; + } + tab = value; // descend + continue; + } + + // key not found + SETERROR(pp->ebuf, lineno, "cannot locate table at key %s", + keypart->span[i].ptr); + return NULL; + } + + // Return the table corresponding to the keypart[]. + return tab; +} + +// Recursively set flags on datum +static void set_flag_recursive(toml_datum_t *datum, uint32_t flag) { + datum->flag |= flag; + switch (datum->type) { + case TOML_ARRAY: + for (int i = 0, top = datum->u.arr.size; i < top; i++) { + set_flag_recursive(&datum->u.arr.elem[i], flag); + } + break; + case TOML_TABLE: + for (int i = 0, top = datum->u.tab.size; i < top; i++) { + set_flag_recursive(&datum->u.tab.value[i], flag); + } + break; + default: + break; + } +} + +// Parse an inline array. +static int parse_inline_array(parser_t *pp, token_t tok, + toml_datum_t *ret_datum) { + assert(tok.toktyp == TOK_LBRACK); + *ret_datum = mkdatum_at(TOML_ARRAY, tok.lineno, tok.colno); + int need_comma = 0; + + // loop until RBRACK + for (;;) { + // skip ENDL + do { + DO(scan_value(&pp->scanner, &tok)); + } while (tok.toktyp == TOK_ENDL); + + // If got an RBRACK: done! + if (tok.toktyp == TOK_RBRACK) { + break; + } + + // If got a COMMA: check if it is expected. + if (tok.toktyp == TOK_COMMA) { + if (need_comma) { + need_comma = 0; + continue; + } + return SETERROR(pp->ebuf, tok.lineno, + "syntax error while parsing array: unexpected comma"); + } + + // Not a comma, but need a comma: error! + if (need_comma) { + return SETERROR(pp->ebuf, tok.lineno, + "syntax error while parsing array: missing comma"); + } + + // This is a valid value! Obtain the value. + toml_datum_t value = DATUM_ZERO; + if (parse_val(pp, tok, &value)) { + datum_free(&value); + return -1; + } + + // Add the value to the array. + const char *reason; + toml_datum_t *pelem = arr_emplace(ret_datum, &reason); + if (!pelem) { + datum_free(&value); + return SETERROR(pp->ebuf, tok.lineno, "while parsing array: %s", reason); + } + *pelem = value; + + // Need comma before the next value. + need_comma = 1; + } + + // Set the INLINE flag for all things in this array. + set_flag_recursive(ret_datum, FLAG_INLINED); + return 0; +} + +// Parse an inline table. +static int parse_inline_table(parser_t *pp, token_t tok, + toml_datum_t *ret_datum) { + assert(tok.toktyp == TOK_LBRACE); + *ret_datum = mkdatum_at(TOML_TABLE, tok.lineno, tok.colno); + bool need_comma = 0; + bool was_comma = 0; + + // loop until RBRACE + for (;;) { + DO(scan_key(&pp->scanner, &tok)); + + // Got an RBRACE: done! + if (tok.toktyp == TOK_RBRACE) { + if (was_comma) { + /* + return SETERROR(pp->ebuf, tok.lineno, + "extra comma before closing brace"); + */ + // extra comma before RBRACE is allowed for v1.1 + (void)0; + } + break; + } + + // Got a comma: check if it is expected. + if (tok.toktyp == TOK_COMMA) { + if (need_comma) { + need_comma = 0, was_comma = 1; + continue; + } + return SETERROR(pp->ebuf, tok.lineno, "unexpected comma"); + } + + // Newline not allowed in inline table. + // newline is allowed in v1.1 + if (tok.toktyp == TOK_ENDL) { + // return SETERROR(pp->ebuf, tok.lineno, "unexpected newline"); + continue; + } + + // Not a comma, but need a comma: error! + if (need_comma) { + return SETERROR(pp->ebuf, tok.lineno, "missing comma"); + } + + // Get the keyparts + keypart_t keypart = {0}; + int keylineno = tok.lineno; + int keycolno = tok.colno; + DO(parse_key(pp, tok, &keypart)); + + // Descend to one keypart before last + assert(keypart.nspan > 0); + span_t lastkeypart = keypart.span[--keypart.nspan]; + toml_datum_t *tab = + descend_keypart(pp, keylineno, keycolno, ret_datum, &keypart, false); + if (!tab) { + return -1; + } + + // If tab is a previously declared inline table: error. + if (tab->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, tok.lineno, "inline table cannot be extended"); + } + + // We are explicitly defining it now. + tab->flag |= FLAG_EXPLICIT; + + // match EQUAL + DO(scan_value(&pp->scanner, &tok)); + + if (tok.toktyp != TOK_EQUAL) { + if (tok.toktyp == TOK_ENDL) { + return SETERROR(pp->ebuf, tok.lineno, "unexpected newline"); + } else { + return SETERROR(pp->ebuf, tok.lineno, "missing '='"); + } + } + + // obtain the value + DO(scan_value(&pp->scanner, &tok)); + toml_datum_t value = DATUM_ZERO; + if (parse_val(pp, tok, &value)) { + datum_free(&value); + return -1; + } + + // Add the value to tab. + const char *reason; + if (tab_add(tab, lastkeypart, value, &reason)) { + datum_free(&value); + return SETERROR(pp->ebuf, tok.lineno, "%s", reason); + } + need_comma = 1, was_comma = 0; + } + + set_flag_recursive(ret_datum, FLAG_INLINED); + return 0; +} + +// Parse a value. +static int parse_val(parser_t *pp, token_t tok, toml_datum_t *ret) { + *ret = DATUM_ZERO; // initialize + + // val = string / boolean / array / inline-table / date-time / float / integer + switch (tok.toktyp) { + case TOK_STRING: + case TOK_MLSTRING: + case TOK_LITSTRING: + case TOK_MLLITSTRING: + return token_to_string(pp, tok, ret); + case TOK_TIME: + case TOK_DATE: + case TOK_DATETIME: + case TOK_DATETIMETZ: + return token_to_timestamp(pp, tok, ret); + case TOK_INTEGER: + return token_to_int64(pp, tok, ret); + case TOK_FLOAT: + return token_to_fp64(pp, tok, ret); + case TOK_BOOL: + return token_to_boolean(pp, tok, ret); + case TOK_LBRACK: // inline-array + return parse_inline_array(pp, tok, ret); + case TOK_LBRACE: // inline-table + return parse_inline_table(pp, tok, ret); + default: + return SETERROR(pp->ebuf, tok.lineno, "missing value"); + } +} + +// Parse a standard table expression, and set the curtab of the parser +// to the table referenced. A standard table expression is a line +// like [a.b.c.d]. +static int parse_std_table_expr(parser_t *pp, token_t tok) { + // std-table = [ key ] + // Eat the [ + assert(tok.toktyp == TOK_LBRACK); // [ ate by caller + + // Read the first keypart + DO(scan_key(&pp->scanner, &tok)); + + // Extract the keypart[] + int keylineno = tok.lineno; + int keycolno = tok.colno; + keypart_t keypart; + DO(parse_key(pp, tok, &keypart)); + + // Eat the ] + DO(scan_key(&pp->scanner, &tok)); + if (tok.toktyp != TOK_RBRACK) { + return SETERROR(pp->ebuf, tok.lineno, "missing right-bracket"); + } + + // Descend to one keypart before last. + span_t lastkeypart = keypart.span[--keypart.nspan]; + + // Descend keypart from the toptab. + toml_datum_t *tab = + descend_keypart(pp, keylineno, keycolno, &pp->toptab, &keypart, true); + if (!tab) { + return -1; + } + + // Look for the last keypart in the final tab + int j = tab_find(tab, lastkeypart); + if (j < 0) { + // If not found: add it. + if (tab->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended"); + } + const char *reason; + toml_datum_t newtab = mkdatum_at(TOML_TABLE, keylineno, keycolno); + newtab.flag |= FLAG_STDEXPR; + if (tab_add(tab, lastkeypart, newtab, &reason)) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + // this is the new tab + tab = &tab->u.tab.value[tab->u.tab.size - 1]; + } else { + // Found: check for errors + tab = &tab->u.tab.value[j]; + if (tab->flag & FLAG_EXPLICIT) { + /* + This is not OK: + [x.y.z] + [x.y.z] + + but this is OK: + [x.y.z] + [x] + */ + return SETERROR(pp->ebuf, keylineno, "table defined more than once"); + } + if (!(tab->flag & FLAG_STDEXPR)) { + /* + [t1] # OK + t2.t3.v = 0 # OK + [t1.t2] # should FAIL - t2 was non-explicit but was not + created by std-table-expr + */ + return SETERROR(pp->ebuf, keylineno, "table defined before"); + } + } + + // Set explicit flag on tab + tab->flag |= FLAG_EXPLICIT; + tab->lineno = keylineno; + tab->colno = keycolno; + + // Set tab as curtab of the parser + pp->curtab = tab; + return 0; +} + +// Parse an array table expression, and set the curtab of the parser +// to the table referenced. A standard array table expression is a line +// like [[a.b.c.d]]. +static int parse_array_table_expr(parser_t *pp, token_t tok) { + // array-table = [[ key ]] + assert(tok.toktyp == TOK_LLBRACK); // [[ ate by caller + + // Read the first keypart + DO(scan_key(&pp->scanner, &tok)); + + int keylineno = tok.lineno; + int keycolno = tok.colno; + keypart_t keypart; + DO(parse_key(pp, tok, &keypart)); + + // eat the ]] + token_t rrb; + DO(scan_key(&pp->scanner, &rrb)); + if (rrb.toktyp != TOK_RRBRACK) { + return SETERROR(pp->ebuf, rrb.lineno, "missing ']]'"); + } + + // remove the last keypart from keypart[] + assert(keypart.nspan > 0); + span_t lastkeypart = keypart.span[--keypart.nspan]; + + // descend intermediate keys from toptab + toml_datum_t *tab = + descend_keypart(pp, keylineno, keycolno, &pp->toptab, &keypart, true); + if (!tab) + return -1; + + // For the final keypart, make sure entry at key is an array of tables + const char *reason; + int idx = tab_find(tab, lastkeypart); + if (idx == -1) { + // If not found, add an array of table. + if (tab->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended"); + } + toml_datum_t newarr = mkdatum_at(TOML_ARRAY, keylineno, keycolno); + if (tab_add(tab, lastkeypart, newarr, &reason)) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + idx = tab_find(tab, lastkeypart); + assert(idx >= 0); + } + // Check that this is an array. + if (tab->u.tab.value[idx].type != TOML_ARRAY) { + return SETERROR(pp->ebuf, keylineno, "entry must be an array"); + } + // Add an empty table to the array + toml_datum_t *arr = &tab->u.tab.value[idx]; + if (arr->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, keylineno, "cannot extend a static array"); + } + toml_datum_t *pelem = arr_emplace(arr, &reason); + if (!pelem) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + *pelem = mkdatum_at(TOML_TABLE, keylineno, keycolno); + + // Set the last element of this array as curtab of the parser + pp->curtab = &arr->u.arr.elem[arr->u.arr.size - 1]; + assert(pp->curtab->type == TOML_TABLE); + + return 0; +} + +// Parse an expression. A toml doc is just a list of expressions. +static int parse_keyvalue_expr(parser_t *pp, token_t tok) { + // Obtain the key + int keylineno = tok.lineno; + int keycolno = tok.colno; + keypart_t keypart; + DO(parse_key(pp, tok, &keypart)); + + // match the '=' + DO(scan_key(&pp->scanner, &tok)); + if (tok.toktyp != TOK_EQUAL) { + return SETERROR(pp->ebuf, tok.lineno, "expect '='"); + } + + // Locate the last table using keypart[] + const char *reason; + toml_datum_t *tab = pp->curtab; + for (int i = 0; i < keypart.nspan - 1; i++) { + int j = tab_find(tab, keypart.span[i]); + if (j < 0) { + if (i > 0 && (tab->flag & FLAG_EXPLICIT)) { + return SETERROR( + pp->ebuf, keylineno, + "cannot extend a previously defined table using dotted expression"); + } + toml_datum_t newtab = mkdatum_at(TOML_TABLE, keylineno, keycolno); + if (tab_add(tab, keypart.span[i], newtab, &reason)) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + tab = &tab->u.tab.value[tab->u.tab.size - 1]; + continue; + } + toml_datum_t *value = &tab->u.tab.value[j]; + if (value->type == TOML_TABLE) { + tab = value; + continue; + } + if (value->type == TOML_ARRAY) { + return SETERROR(pp->ebuf, keylineno, + "encountered previously declared array '%s'", + keypart.span[i].ptr); + } + return SETERROR(pp->ebuf, keylineno, "cannot locate table at '%s'", + keypart.span[i].ptr); + } + + // Check for disallowed situations. + if (tab->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended"); + } + if (keypart.nspan > 1 && (tab->flag & FLAG_EXPLICIT)) { + return SETERROR( + pp->ebuf, keylineno, + "cannot extend a previously defined table using dotted expression"); + } + + // Obtain the value + DO(scan_value(&pp->scanner, &tok)); + toml_datum_t newval = DATUM_ZERO; + if (parse_val(pp, tok, &newval)) { + datum_free(&newval); + return -1; + } + + // Add a new key/value for tab. + if (tab_add(tab, keypart.span[keypart.nspan - 1], newval, &reason)) { + datum_free(&newval); + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + + return 0; +} + +// Normalize a LIT/STRING/MLSTRING/LITSTRING/MLLITSTRING +// -> unescape all escaped chars +// The returned string is allocated out of pp->pool +static int parse_norm(parser_t *pp, token_t tok, span_t *ret_span) { + // Allocate a buffer to store the normalized string. Add one + // extra-byte for terminating NUL. + char *p = pool_alloc(pp->pool, tok.str.len + 1); + if (!p) { + return SETERROR(pp->ebuf, tok.lineno, "out of memory"); + } + + // Copy from token string into buffer + memcpy(p, tok.str.ptr, tok.str.len); + p[tok.str.len] = 0; // additional NUL term for safety + + ret_span->ptr = p; + ret_span->len = tok.str.len; + + switch (tok.toktyp) { + case TOK_LIT: + case TOK_LITSTRING: + case TOK_MLLITSTRING: + // no need to handle escape chars + return 0; + + case TOK_STRING: + case TOK_MLSTRING: + // need to handle escape chars + break; + + default: + return SETERROR(pp->ebuf, 0, "internal: arg must be a string"); + } + + // if there is no escape char, then done! + if (!tok.u.escp) { + return 0; // success + } + + // p points to the backslash + p += (tok.u.escp - tok.str.ptr); + assert(p - ret_span->ptr == tok.u.escp - tok.str.ptr); + assert(*p == '\\'); + + // Normalize the escaped chars + char *dst = p; + while (*p) { + if (*p != '\\') { + *dst++ = *p++; + continue; + } + switch (p[1]) { + case '"': + case '\\': + *dst++ = p[1]; + p += 2; + continue; + case 'b': + *dst++ = '\b'; + p += 2; + continue; + case 't': + *dst++ = '\t'; + p += 2; + continue; + case 'n': + *dst++ = '\n'; + p += 2; + continue; + case 'f': + *dst++ = '\f'; + p += 2; + continue; + case 'r': + *dst++ = '\r'; + p += 2; + continue; + case 'e': + *dst++ = '\033'; + p += 2; + continue; + case 'x': { + char buf[3]; + memcpy(buf, p + 2, 2); + buf[2] = 0; + // There is no need to check for two hex digits here because + // the scanner already checked it. + int32_t ucs = strtol(buf, 0, 16); + int n = ucs_to_utf8(ucs, dst); + if (n < 0) { + return SETERROR(pp->ebuf, tok.lineno, "error converting UCS %s to UTF8", + buf); + } + dst += n; + p += 2 + 2; // \xNN + continue; + } + case 'u': + case 'U': { + char buf[9]; + int sz = (p[1] == 'u' ? 4 : 8); + memcpy(buf, p + 2, sz); + buf[sz] = 0; + // There is no need to check for 4 or 8 hex digits here because + // the scanner already checked it. + int32_t ucs = strtol(buf, 0, 16); + if (0xD800 <= ucs && ucs <= 0xDFFF) { + // explicitly prohibit surrogates (non-scalar unicode code point) + return SETERROR(pp->ebuf, tok.lineno, "invalid UTF8 char \\u%04x", ucs); + } + int n = ucs_to_utf8(ucs, dst); + if (n < 0) { + return SETERROR(pp->ebuf, tok.lineno, "error converting UCS %s to UTF8", + buf); + } + dst += n; + p += 2 + sz; // \uNNNN or \UNNNNNNNN + continue; + } + + case ' ': + case '\t': + case '\r': + // line-ending backslash + // --- allow for extra whitespace chars after backslash + // --- skip until newline + p++; // skip the escape char + p += strspn(p, " \t\r"); // skip whitespaces + if (*p != '\n') { + return SETERROR(pp->ebuf, tok.lineno, + "unexpected char after line-ending backslash"); + } + // fallthru + case '\n': + // skip all whitespaces including newline + p++; + p += strspn(p, " \t\r\n"); + continue; + default: + return SETERROR(pp->ebuf, tok.lineno, + "internal: unknown escape char \\%c", p[1]); + } + } + *dst = 0; + ret_span->len = dst - ret_span->ptr; + return 0; +} + +// =================================================================== +// == SCANNER SECTION +// =================================================================== + +// Get the next char +static int scan_get(scanner_t *sp) { + int ret = TOK_FIN; + const char *p = sp->cur; + if (p < sp->endp) { + ret = *p++; + if (ret == '\r' && p < sp->endp && *p == '\n') { + ret = *p++; + } + } + sp->cur = p; + if (ret == '\n') { + sp->lineno += 1; + sp->line_start = p; + } + return ret; +} + +// Check if the next char matches ch. +static inline bool scan_match(scanner_t *sp, int ch) { + const char *p = sp->cur; + // exact match? done. + if (p < sp->endp && *p == ch) { + return true; + } + // \n also matches \r\n + if (ch == '\n' && p + 1 < sp->endp) { + return p[0] == '\r' && p[1] == '\n'; + } + // not a match + return false; +} + +// Check if the next char is in accept[]. +static bool scan_matchany(scanner_t *sp, const char *accept) { + for (; *accept; accept++) { + if (scan_match(sp, *accept)) { + return true; + } + } + return false; +} + +// Check if the next n chars match ch. +static inline bool scan_nmatch(scanner_t *sp, int ch, int n) { + assert(ch != '\n'); // not handled + if (sp->cur + n > sp->endp) { + return false; + } + const char *p = sp->cur; + int i; + for (i = 0; i < n && p[i] == ch; i++) + ; + return i == n; +} + +// Initialize a token. +static inline token_t mktoken(scanner_t *sp, toktyp_t typ) { + token_t tok = {0}; + tok.toktyp = typ; + tok.str.ptr = sp->cur; + tok.lineno = sp->lineno; + tok.colno = (int)(sp->cur - sp->line_start) + 1; + return tok; +} + +#define S_GET() scan_get(sp) +#define S_MATCH(ch) scan_match(sp, (ch)) +#define S_MATCH3(ch) scan_nmatch(sp, (ch), 3) +#define S_MATCH4(ch) scan_nmatch(sp, (ch), 4) +#define S_MATCH6(ch) scan_nmatch(sp, (ch), 6) + +static inline bool is_valid_char(int ch) { + // i.e. (0x20 <= ch && ch <= 0x7e) || (ch & 0x80); + return isprint((unsigned char)ch) || (ch & 0x80); +} + +static inline bool is_hex_char(int ch) { + ch = toupper((unsigned char)ch); + return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F'); +} + +// Initialize a scanner +static void scan_init(scanner_t *sp, const char *src, int len, char *errbuf, + int errbufsz) { + memset(sp, 0, sizeof(*sp)); + sp->src = src; + sp->endp = src + len; + assert(*sp->endp == '\0'); + sp->cur = src; + sp->lineno = 1; + sp->line_start = src; + sp->ebuf.ptr = errbuf; + sp->ebuf.len = errbufsz; +} + +// Scan escape sequence characters after a backslash. +// Return #char match on success, 0 if no match, -1 on error. +static int scan_escape_chars(scanner_t *sp) { + const char *p = sp->cur; + if (p >= sp->endp) { + return 0; + } + int ch = *p++; + if (ch && strchr("btnfre\"\\", ch)) { + return p - sp->cur; + } + int hex = (ch == 'x') ? 2 : (ch == 'u') ? 4 : (ch == 'U') ? 8 : 0; + if (hex) { + int i = 0; + for (; i < hex && p < sp->endp && is_hex_char(*p); i++, p++) + ; + if (i != hex) { + return SETERROR(sp->ebuf, sp->lineno, "expect %d hex digits after \\%c", + hex, ch); + } + return p - sp->cur; + } + return 0; +} + +// Scan """ ... """ +static int scan_multiline_string(scanner_t *sp, token_t *tok) { + assert(S_MATCH3('"')); + S_GET(), S_GET(), S_GET(); // skip opening """ + + // According to spec: trim first newline after """ + if (S_MATCH('\n')) { + S_GET(); + } + + *tok = mktoken(sp, TOK_MLSTRING); + // scan until terminating """ + const char *escp = NULL; + while (1) { + if (S_MATCH3('"')) { + if (S_MATCH4('"')) { + // special case... """abcd """" -> (abcd ") + // but sequences of 3 or more double quotes are not allowed + if (S_MATCH6('"')) { + return SETERROR(sp->ebuf, sp->lineno, + "detected sequences of 3 or more double quotes"); + } else { + ; // no problem + } + } else { + break; // found terminating """ + } + } + int ch = S_GET(); + if (ch == TOK_FIN) { + return SETERROR(sp->ebuf, sp->lineno, "unterminated \"\"\""); + } + // If non-escaped char ... + if (ch != '\\') { + if (!(is_valid_char(ch) || (ch && strchr(" \t\n", ch)))) { + return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); + } + continue; + } + // ch is backslash + if (!escp) { + escp = sp->cur - 1; /* mark the first esc position */ + assert(*escp == '\\'); + } + + // handle escape char + int cnt = scan_escape_chars(sp); + if (cnt < 0) { + return -1; + } + if (cnt > 0) { + sp->cur += cnt; // skip the escape sequence + continue; + } + assert(cnt == 0); + + // handle line-ending backslash + ch = S_GET(); + if (ch == ' ' || ch == '\t') { + // Although the spec does not allow for whitespace following a + // line-ending backslash, some standard tests expect it. + // Skip whitespace till EOL. + while (ch != TOK_FIN && ch && strchr(" \t", ch)) { + ch = S_GET(); + } + if (ch != '\n') { + // Got a backslash followed by whitespace, followed by some char + // before newline + return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string"); + } + // fallthru + } + if (ch == '\n') { + // got a line-ending backslash + // - skip all whitespaces + while (scan_matchany(sp, " \t\n")) { + S_GET(); + } + continue; + } + return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string"); + } + tok->str.len = sp->cur - tok->str.ptr; + tok->u.escp = escp; + + assert(S_MATCH3('"')); + S_GET(), S_GET(), S_GET(); + return 0; +} + +// Scan " ... " +static int scan_string(scanner_t *sp, token_t *tok) { + assert(S_MATCH('"')); + if (S_MATCH3('"')) { + return scan_multiline_string(sp, tok); + } + S_GET(); // skip opening " + + // scan until closing " + *tok = mktoken(sp, TOK_STRING); + const char *escp = NULL; + while (!S_MATCH('"')) { + int ch = S_GET(); + if (ch == TOK_FIN) { + return SETERROR(sp->ebuf, sp->lineno, "unterminated string"); + } + // If non-escaped char ... + if (ch != '\\') { + if (!(is_valid_char(ch) || ch == ' ' || ch == '\t')) { + return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); + } + continue; + } + // ch is backslash + if (!escp) { + escp = sp->cur - 1; + assert(*escp == '\\'); + } + + // handle escape char + int cnt = scan_escape_chars(sp); + if (cnt < 0) { + return -1; + } + if (cnt > 0) { + sp->cur += cnt; + continue; + } + return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string"); + } + tok->str.len = sp->cur - tok->str.ptr; + tok->u.escp = escp; + + assert(S_MATCH('"')); + S_GET(); // skip the terminating " + return 0; +} + +// Scan ''' ... ''' +static int scan_multiline_litstring(scanner_t *sp, token_t *tok) { + assert(S_MATCH3('\'')); + S_GET(), S_GET(), S_GET(); // skip opening ''' + + // According to spec: trim first newline after ''' + if (S_MATCH('\n')) { + S_GET(); + } + + // scan until terminating ''' + *tok = mktoken(sp, TOK_MLLITSTRING); + while (1) { + if (S_MATCH3('\'')) { + if (S_MATCH4('\'')) { + // special case... '''abcd '''' -> (abcd ') + // but sequences of 3 or more single quotes are not allowed + if (S_MATCH6('\'')) { + return SETERROR(sp->ebuf, sp->lineno, + "sequences of 3 or more single quotes"); + } else { + ; // no problem + } + } else { + break; // found terminating ''' + } + } + int ch = S_GET(); + if (ch == TOK_FIN) { + return SETERROR(sp->ebuf, sp->lineno, + "unterminated multiline lit string"); + } + if (!(is_valid_char(ch) || (ch && strchr(" \t\n", ch)))) { + return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); + } + } + tok->str.len = sp->cur - tok->str.ptr; + + assert(S_MATCH3('\'')); + S_GET(), S_GET(), S_GET(); + return 0; +} + +// Scan ' ... ' +static int scan_litstring(scanner_t *sp, token_t *tok) { + assert(S_MATCH('\'')); + if (S_MATCH3('\'')) { + return scan_multiline_litstring(sp, tok); + } + S_GET(); // skip opening ' + + // scan until closing ' + *tok = mktoken(sp, TOK_LITSTRING); + while (!S_MATCH('\'')) { + int ch = S_GET(); + if (ch == TOK_FIN) { + return SETERROR(sp->ebuf, sp->lineno, "unterminated string"); + } + if (!(is_valid_char(ch) || ch == '\t')) { + return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); + } + } + tok->str.len = sp->cur - tok->str.ptr; + assert(S_MATCH('\'')); + S_GET(); + return 0; +} + +static bool is_valid_date(int year, int month, int day) { + if (!(1 <= year)) { + return false; + } + if (!(1 <= month && month <= 12)) { + return false; + } + int is_leap_year = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + int days_in_month[] = { + 31, 28 + is_leap_year, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + return (1 <= day && day <= days_in_month[month - 1]); +} + +static bool is_valid_time(int hour, int minute, int sec, int usec) { + if (!(0 <= hour && hour <= 23)) { + return false; + } + if (!(0 <= minute && minute <= 59)) { + return false; + } + if (!(0 <= sec && sec <= 59)) { + return false; + } + if (!(0 <= usec)) { + return false; + } + return true; +} + +static bool is_valid_timezone(int minute) { + minute = (minute < 0 ? -minute : minute); + int hour = minute / 60; + minute = minute % 60; + if (!(0 <= hour && hour <= 23)) { + return false; + } + if (!(0 <= minute && minute < 60)) { + return false; + } + return true; +} + +// Read an int (without signs) from the string p. +static int read_int(const char *p, int *ret) { + const char *pp = p; + int64_t val = 0; + for (; isdigit((unsigned char)*p); p++) { + val = val * 10 + (*p - '0'); + if (val > INT_MAX) { + return 0; // overflowed + } + } + *ret = (int)val; + return p - pp; +} + +// Read a date as YYYY-MM-DD from p[]. Return #bytes consumed. +static int read_date(const char *p, int *year, int *month, int *day) { + const char *pp = p; + int n; + n = read_int(p, year); + if (n != 4 || p[4] != '-') { + return 0; + } + n = read_int(p += n + 1, month); + if (n != 2 || p[2] != '-') { + return 0; + } + n = read_int(p += n + 1, day); + if (n != 2) { + return 0; + } + p += 2; + assert(p - pp == 10); + return p - pp; +} + +// Read a time as HH:MM:SS.subsec from p[]. Return #bytes consumed. +static int read_time(const char *p, int *hour, int *minute, int *second, + int *usec) { + const char *pp = p; + int n; + *hour = *minute = *second = *usec = 0; + // scan hours + n = read_int(p, hour); + if (n != 2 || p[2] != ':') { + return 0; + } + p += 3; + + // scan minutes + n = read_int(p, minute); + if (n != 2) { + return 0; + } + if (p[2] != ':') { + // seconds are optional in v1.1 + p += 2; + return p - pp; + } + p += 3; + + // scan seconds + n = read_int(p, second); + if (n != 2) { + return 0; + } + p += 2; + + if (*p != '.') { + return p - pp; + } + p++; // skip the period + if (!isdigit((unsigned char)*p)) { + // trailing period + return 0; + } + int micro_factor = 100000; + while (isdigit((unsigned char)*p) && micro_factor) { + *usec += (*p - '0') * micro_factor; + micro_factor /= 10; + p++; + } + while (isdigit((unsigned char)*p)) + p++; // consume extra sub-microsecond digits + return p - pp; +} + +// Reads a timezone from p[]. Return #bytes consumed. +// tzhours and tzminutes restricted to 2-char integers only. +static int read_tzone(const char *p, char *tzsign, int *tzhour, int *tzminute) { + const char *pp = p; + + // Default values + *tzhour = *tzminute = 0; + *tzsign = '+'; + + // Look for Zulu + if (*p == 'Z' || *p == 'z') { + return 1; // done! tz is +00:00. + } + + // Look for +/- + *tzsign = *p++; + if (!(*tzsign == '+' || *tzsign == '-')) { + return 0; + } + + // Look for HH:MM + int n; + n = read_int(p, tzhour); + if (n != 2 || p[2] != ':') { + return 0; + } + n = read_int(p += 3, tzminute); + if (n != 2) { + return 0; + } + p += 2; + return p - pp; +} + +// Scan hh:mm:ss.xxxxx +static int scan_time(scanner_t *sp, token_t *tok) { + int lineno = sp->lineno; + char buffer[20]; + scan_copystr(sp, buffer, sizeof(buffer)); + + char *p = buffer; + int hour, minute, sec, usec; + int len = read_time(p, &hour, &minute, &sec, &usec); + if (len == 0) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + if (!is_valid_time(hour, minute, sec, usec)) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + + *tok = mktoken(sp, TOK_TIME); + tok->str.len = len; + sp->cur += len; + tok->u.tsval.year = -1; + tok->u.tsval.month = -1; + tok->u.tsval.day = -1; + tok->u.tsval.hour = hour; + tok->u.tsval.minute = minute; + tok->u.tsval.sec = sec; + tok->u.tsval.usec = usec; + tok->u.tsval.tz = -1; + return 0; +} + +// Scan a time, a date, a datetime, or a datatimetz +static int scan_timestamp(scanner_t *sp, token_t *tok) { + int year, month, day, hour, minute, sec, usec, tz; + year = month = day = hour = minute = sec = usec = tz = -1; + + int n; + // make a copy of sp->cur into buffer to ensure NUL terminated string + char buffer[80]; + scan_copystr(sp, buffer, sizeof(buffer)); + + toktyp_t toktyp = TOK_FIN; + int lineno = sp->lineno; + + // See if this a TIME only + const char *p = buffer; + if (isdigit((unsigned char)p[0]) && isdigit((unsigned char)p[1]) && + p[2] == ':') { + n = read_time(buffer, &hour, &minute, &sec, &usec); + if (!n) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + toktyp = TOK_TIME; + p += n; + goto done; + } + + // Try reading a DATE + n = read_date(p, &year, &month, &day); + if (!n) { + return SETERROR(sp->ebuf, lineno, "invalid date"); + } + toktyp = TOK_DATE; + p += n; + + // Check if there is no time component in addition + if (!((p[0] == 'T' || p[0] == ' ' || p[0] == 't') && + isdigit((unsigned char)p[1]) && isdigit((unsigned char)p[2]) && + p[3] == ':')) { + goto done; // no TIME component. we are done. + } + + // Read the TIME + n = read_time(p += 1, &hour, &minute, &sec, &usec); + if (!n) { + return SETERROR(sp->ebuf, lineno, "invalid timestamp"); + } + toktyp = TOK_DATETIME; + p += n; + + // Read the (optional) timezone + char tzsign; + int tzhour, tzminute; + n = read_tzone(p, &tzsign, &tzhour, &tzminute); + if (n == 0) { + goto done; // datetime only + } + toktyp = TOK_DATETIMETZ; + p += n; + + // Check tzminute range. This must be done here instead of is_valid_timezone() + // because we combine tzhour and tzminute into tz (by minutes only). + if (!(0 <= tzminute && tzminute < 60)) { + return SETERROR(sp->ebuf, lineno, "invalid timezone"); + } + tz = (tzhour * 60 + tzminute) * (tzsign == '-' ? -1 : 1); + goto done; // datetimetz + +done: + *tok = mktoken(sp, toktyp); + n = p - buffer; + tok->str.len = n; + sp->cur += n; + + tok->u.tsval.year = year; + tok->u.tsval.month = month; + tok->u.tsval.day = day; + tok->u.tsval.hour = hour; + tok->u.tsval.minute = minute; + tok->u.tsval.sec = sec; + tok->u.tsval.usec = usec; + tok->u.tsval.tz = tz; + + // Do some error checks based on type + switch (tok->toktyp) { + case TOK_TIME: + if (!is_valid_time(hour, minute, sec, usec)) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + break; + case TOK_DATE: + if (!is_valid_date(year, month, day)) { + return SETERROR(sp->ebuf, lineno, "invalid date"); + } + break; + case TOK_DATETIME: + case TOK_DATETIMETZ: + if (!is_valid_date(year, month, day)) { + return SETERROR(sp->ebuf, lineno, "invalid date"); + } + if (!is_valid_time(hour, minute, sec, usec)) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + if (tok->toktyp == TOK_DATETIMETZ && !is_valid_timezone(tz)) { + return SETERROR(sp->ebuf, lineno, "invalid timezone"); + } + break; + default: + assert(0); + return SETERROR(sp->ebuf, lineno, "internal error"); + } + + return 0; +} + +// Given a toml number (int and float) in buffer[]: +// 1. squeeze out '_' +// 2. check for syntax restrictions +static int process_numstr(char *buffer, int base, const char **reason) { + // squeeze out _ + char *q = strchr(buffer, '_'); + if (q) { + for (int i = q - buffer; buffer[i]; i++) { + if (buffer[i] != '_') { + *q++ = buffer[i]; + continue; + } + int left = (i == 0) ? 0 : (unsigned char)buffer[i - 1]; + int right = (unsigned char)buffer[i + 1]; + if (!isdigit(left) && !(base == 16 && is_hex_char(left))) { + *reason = "underscore only allowed between digits"; + return -1; + } + if (!isdigit(right) && !(base == 16 && is_hex_char(right))) { + *reason = "underscore only allowed between digits"; + return -1; + } + } + *q = 0; + } + + // decimal points must be surrounded by digits. Also, convert to lowercase. + for (int i = 0; buffer[i]; i++) { + if (buffer[i] == '.') { + if (i == 0 || !isdigit((unsigned char)buffer[i - 1]) || + !isdigit((unsigned char)buffer[i + 1])) { + *reason = "decimal point must be surrounded by digits"; + return -1; + } + } else if ('A' <= buffer[i] && buffer[i] <= 'Z') { + buffer[i] = tolower((unsigned char)buffer[i]); + } + } + + if (base == 10) { + // check for leading 0: '+01' is an error! + q = buffer; + q += (*q == '+' || *q == '-') ? 1 : 0; + if (q[0] == '0' && isdigit((unsigned char)q[1])) { + *reason = "leading 0 in numbers"; + return -1; + } + } + + return 0; +} + +static int scan_float(scanner_t *sp, token_t *tok) { + char buffer[50]; // need to accommodate "9_007_199_254_740_991.0" + scan_copystr(sp, buffer, sizeof(buffer)); + + int lineno = sp->lineno; + char *p = buffer; + p += (*p == '+' || *p == '-') ? 1 : 0; + if (0 == memcmp(p, "nan", 3) || (0 == memcmp(p, "inf", 3))) { + p += 3; + } else { + p += strspn(p, "_0123456789eE.+-"); + } + int len = p - buffer; + buffer[len] = 0; + + const char *reason; + if (process_numstr(buffer, 10, &reason)) { + return SETERROR(sp->ebuf, lineno, "%s", reason); + } + + errno = 0; + char *q; + double fp64 = strtod(buffer, &q); + if (errno || *q || q == buffer) { + return SETERROR(sp->ebuf, lineno, "error parsing float"); + } + + *tok = mktoken(sp, TOK_FLOAT); + tok->u.fp64 = fp64; + tok->str.len = len; + sp->cur += len; + return 0; +} + +static int scan_number(scanner_t *sp, token_t *tok) { + const char *reason; + char buffer[50]; // need to accommodate "9_007_199_254_740_991.0" + scan_copystr(sp, buffer, sizeof(buffer)); + + char *p = buffer; + int lineno = sp->lineno; + // process %0x, %0o or %0b integers + if (p[0] == '0') { + const char *span = 0; + int base = 0; + switch (p[1]) { + case 'x': + base = 16; + span = "_0123456789abcdefABCDEF"; + break; + case 'o': + base = 8; + span = "_01234567"; + break; + case 'b': + base = 2; + span = "_01"; + break; + } + if (base) { + p += 2; + p += strspn(p, span); + int len = p - buffer; + buffer[len] = 0; + + if (process_numstr(buffer + 2, base, &reason)) { + return SETERROR(sp->ebuf, lineno, "%s", reason); + } + + // use strtoll to obtain the value + *tok = mktoken(sp, TOK_INTEGER); + char *q; + errno = 0; + tok->u.int64 = strtoll(buffer + 2, &q, base); + if (errno || *q || q == buffer + 2) { + return SETERROR(sp->ebuf, lineno, "error parsing integer"); + } + tok->str.len = len; + sp->cur += len; + return 0; + } + } + + // handle inf/nan + if (*p == '+' || *p == '-') { + p++; + } + if (*p == 'i' || *p == 'n') { + return scan_float(sp, tok); + } + + // regular int or float + p = buffer; + p += strspn(p, "0123456789_+-.eE"); + int len = p - buffer; + buffer[len] = 0; + + if (process_numstr(buffer, 10, &reason)) { + return SETERROR(sp->ebuf, lineno, "%s", reason); + } + + *tok = mktoken(sp, TOK_INTEGER); + char *q; + errno = 0; + tok->u.int64 = strtoll(buffer, &q, 10); + if (errno || *q || q == buffer) { + if (*q && strchr(".eE", *q)) { + return scan_float(sp, tok); // try to fit a float + } + return SETERROR(sp->ebuf, lineno, "error parsing integer"); + } + + tok->str.len = len; + sp->cur += len; + return 0; +} + +static int scan_bool(scanner_t *sp, token_t *tok) { + char buffer[10]; + scan_copystr(sp, buffer, sizeof(buffer)); + + int lineno = sp->lineno; + bool val = false; + const char *p = buffer; + if (0 == strncmp(p, "true", 4)) { + val = true; + p += 4; + } else if (0 == strncmp(p, "false", 5)) { + val = false; + p += 5; + } else { + return SETERROR(sp->ebuf, lineno, "invalid boolean value"); + } + if (*p && !strchr("# \r\n\t,}]", *p)) { + return SETERROR(sp->ebuf, lineno, "invalid boolean value"); + } + + int len = p - buffer; + *tok = mktoken(sp, TOK_BOOL); + tok->u.b1 = val; + tok->str.len = len; + sp->cur += len; + return 0; +} + +// Check if the next token may be TIME +static inline bool test_time(const char *p, const char *endp) { + return &p[2] < endp && isdigit((unsigned char)p[0]) && + isdigit((unsigned char)p[1]) && p[2] == ':'; +} + +// Check if the next token may be DATE +static inline bool test_date(const char *p, const char *endp) { + return &p[4] < endp && isdigit((unsigned char)p[0]) && + isdigit((unsigned char)p[1]) && isdigit((unsigned char)p[2]) && + isdigit((unsigned char)p[3]) && p[4] == '-'; +} + +// Check if the next token may be BOOL +static inline bool test_bool(const char *p, const char *endp) { + return &p[0] < endp && (*p == 't' || *p == 'f'); +} + +// Check if the next token may be NUMBER +static bool test_number(const char *p, const char *endp) { + if (&p[0] < endp && *p && strchr("0123456789+-._", *p)) { + return true; + } + if (&p[2] < endp) { + if (0 == memcmp(p, "nan", 3) || 0 == memcmp(p, "inf", 3)) { + return true; + } + } + return false; +} + +// Scan a literal that is not a string +static int scan_nonstring_literal(scanner_t *sp, token_t *tok) { + int lineno = sp->lineno; + if (test_time(sp->cur, sp->endp)) { + return scan_time(sp, tok); + } + + if (test_date(sp->cur, sp->endp)) { + return scan_timestamp(sp, tok); + } + + if (test_bool(sp->cur, sp->endp)) { + return scan_bool(sp, tok); + } + + if (test_number(sp->cur, sp->endp)) { + return scan_number(sp, tok); + } + return SETERROR(sp->ebuf, lineno, "invalid value"); +} + +// Return true if Unicode codepoint is allowed in a TOML 1.1 bare key. +// Ranges taken verbatim from the TOML 1.1 spec grammar for bare-key-char. +static bool is_unicode_bare_key_char(uint32_t cp) { + if (cp == 0xB2 || cp == 0xB3 || cp == 0xB9) + return true; + if (0xBC <= cp && cp <= 0xBE) + return true; + if (0xC0 <= cp && cp <= 0xD6) + return true; + if (0xD8 <= cp && cp <= 0xF6) + return true; + if (0xF8 <= cp && cp <= 0x37D) + return true; + if (0x37F <= cp && cp <= 0x1FFF) + return true; + if (cp == 0x200C || cp == 0x200D) + return true; + if (0x203F <= cp && cp <= 0x2040) + return true; + if (0x2070 <= cp && cp <= 0x218F) + return true; + if (0x2460 <= cp && cp <= 0x24FF) + return true; + if (0x2C00 <= cp && cp <= 0x2FEF) + return true; + if (0x3001 <= cp && cp <= 0xD7FF) + return true; + if (0xF900 <= cp && cp <= 0xFDCF) + return true; + if (0xFDF0 <= cp && cp <= 0xFFFD) + return true; + if (0x10000 <= cp && cp <= 0xEFFFF) + return true; + return false; +} + +// Scan a literal +static int scan_literal(scanner_t *sp, token_t *tok) { + *tok = mktoken(sp, TOK_LIT); + const char *p = sp->cur; + while (p < sp->endp) { + if (isalnum((unsigned char)*p) || *p == '_' || *p == '-') { + p++; + continue; + } + if ((unsigned char)*p >= 0x80) { + uint32_t cp; + int n = utf8_to_ucs(p, sp->endp - p, &cp); + if (n > 0 && is_unicode_bare_key_char(cp)) { + p += n; + continue; + } + } + break; + } + tok->str.len = p - tok->str.ptr; + sp->cur = p; + return 0; +} + +// Save the current state of the scanner +static scanner_state_t scan_mark(scanner_t *sp) { + scanner_state_t mark; + mark.sp = sp; + mark.cur = sp->cur; + mark.lineno = sp->lineno; + mark.line_start = sp->line_start; + return mark; +} + +// Restore the scanner state to a previously saved state +static void scan_restore(scanner_t *sp, scanner_state_t mark) { + assert(mark.sp == sp); + sp->cur = mark.cur; + sp->lineno = mark.lineno; + sp->line_start = mark.line_start; +} + +// Return the next token +static int scan_next(scanner_t *sp, bool keymode, token_t *tok) { + static const toktyp_t map[128] = { + ['\n'] = TOK_ENDL, ['.'] = TOK_DOT, ['='] = TOK_EQUAL, + [','] = TOK_COMMA, ['{'] = TOK_LBRACE, ['}'] = TOK_RBRACE}; +again: + *tok = mktoken(sp, TOK_FIN); + + int ch = S_GET(); + if (ch == TOK_FIN) { + return 0; + } + + tok->str.len = 1; + if (0 <= ch && ch < 128 && map[ch]) { + // map simple char to token type and done + tok->toktyp = map[ch]; + return 0; + } + + // handle char that require logic + switch (ch) { + case ' ': + case '\t': + goto again; // skip whitespace + + case '#': + // comment: skip until newline + while (!S_MATCH('\n')) { + ch = S_GET(); + if (ch == TOK_FIN) + break; + if ((0 <= ch && ch <= 0x8) || (0x0a <= ch && ch <= 0x1f) || + (ch == 0x7f)) { + return SETERROR(sp->ebuf, sp->lineno, "bad control char in comment"); + } + } + goto again; // skip comment + + case '[': + tok->toktyp = TOK_LBRACK; + if (keymode && S_MATCH('[')) { + S_GET(); + tok->toktyp = TOK_LLBRACK; + tok->str.len = 2; + } + break; + + case ']': + tok->toktyp = TOK_RBRACK; + if (keymode && S_MATCH(']')) { + S_GET(); + tok->toktyp = TOK_RRBRACK; + tok->str.len = 2; + } + break; + + case '"': + sp->cur--; + DO(scan_string(sp, tok)); + break; + + case '\'': + sp->cur--; + DO(scan_litstring(sp, tok)); + break; + + default: + sp->cur--; + DO(keymode ? scan_literal(sp, tok) : scan_nonstring_literal(sp, tok)); + break; + } + + return 0; +} + +// Check for stack overflow due to excessive number of brackets or braces +static int check_overflow(scanner_t *sp, token_t *tok) { + switch (tok->toktyp) { + case TOK_LBRACK: + sp->bracket_level++; + if (sp->bracket_level > BRACKET_LEVEL_MAX) { + return SETERROR(sp->ebuf, sp->lineno, "stack overflow"); + } + break; + case TOK_RBRACK: + sp->bracket_level--; + break; + case TOK_LBRACE: + sp->brace_level++; + if (sp->brace_level > BRACE_LEVEL_MAX) { + return SETERROR(sp->ebuf, sp->lineno, "stack overflow"); + } + break; + case TOK_RBRACE: + sp->brace_level--; + break; + default: + break; + } + return 0; +} + +static int scan_key(scanner_t *sp, token_t *tok) { + if (sp->errmsg) { + return -1; + } + if (scan_next(sp, true, tok) || check_overflow(sp, tok)) { + sp->errmsg = sp->ebuf.ptr; + return -1; + } + return 0; +} + +static int scan_value(scanner_t *sp, token_t *tok) { + if (sp->errmsg) { + return -1; + } + if (scan_next(sp, false, tok) || check_overflow(sp, tok)) { + sp->errmsg = sp->ebuf.ptr; + return -1; + } + return 0; +} + +/** + * Convert a char in utf8 into UCS, and store it in *ret. + * Return #bytes consumed or -1 on failure. + */ +static int utf8_to_ucs(const char *orig, int len, uint32_t *ret) { + const unsigned char *buf = (const unsigned char *)orig; + unsigned i = *buf++; + uint32_t v; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (0 == (i >> 7)) { + if (len < 1) + return -1; + v = i; + return *ret = v, 1; + } + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (0x6 == (i >> 5)) { + if (len < 2) + return -1; + v = i & 0x1f; + for (int j = 0; j < 1; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (0xE == (i >> 4)) { + if (len < 3) + return -1; + v = i & 0x0F; + for (int j = 0; j < 2; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x1E == (i >> 3)) { + if (len < 4) + return -1; + v = i & 0x07; + for (int j = 0; j < 3; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + if (0) { + // NOTE: these code points taking more than 4 bytes are not supported + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x3E == (i >> 2)) { + if (len < 5) + return -1; + v = i & 0x03; + for (int j = 0; j < 4; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x7e == (i >> 1)) { + if (len < 6) + return -1; + v = i & 0x01; + for (int j = 0; j < 5; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + } + + return -1; +} + +/** + * Convert a UCS char to utf8 code, and return it in buf. + * Return #bytes used in buf to encode the char, or + * -1 on error. + */ +static int ucs_to_utf8(uint32_t code, char buf[4]) { + /* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16 + */ + /* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well + * as 0xfffe and 0xffff (UCS noncharacters) should not appear in + * conforming UTF-8 streams. + */ + /* + * https://github.com/toml-lang/toml-test/issues/165 + * [0xd800, 0xdfff] and [0xfffe, 0xffff] are implicitly allowed by TOML, so + * we disable the check. + */ + if (0) { + if (0xd800 <= code && code <= 0xdfff) + return -1; + if (0xfffe <= code && code <= 0xffff) + return -1; + } + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (code <= 0x7F) { + buf[0] = (unsigned char)code; + return 1; + } + + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (code <= 0x000007FF) { + buf[0] = (unsigned char)(0xc0 | (code >> 6)); + buf[1] = (unsigned char)(0x80 | (code & 0x3f)); + return 2; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x0000FFFF) { + buf[0] = (unsigned char)(0xe0 | (code >> 12)); + buf[1] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[2] = (unsigned char)(0x80 | (code & 0x3f)); + return 3; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x001FFFFF) { + buf[0] = (unsigned char)(0xf0 | (code >> 18)); + buf[1] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); + buf[2] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[3] = (unsigned char)(0x80 | (code & 0x3f)); + return 4; + } + +#ifdef UNDEF + if (0) { + // NOTE: these code points taking more than 4 bytes are not supported + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x03FFFFFF) { + buf[0] = (unsigned char)(0xf8 | (code >> 24)); + buf[1] = (unsigned char)(0x80 | ((code >> 18) & 0x3f)); + buf[2] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); + buf[3] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[4] = (unsigned char)(0x80 | (code & 0x3f)); + return 5; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x7FFFFFFF) { + buf[0] = (unsigned char)(0xfc | (code >> 30)); + buf[1] = (unsigned char)(0x80 | ((code >> 24) & 0x3f)); + buf[2] = (unsigned char)(0x80 | ((code >> 18) & 0x3f)); + buf[3] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); + buf[4] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[5] = (unsigned char)(0x80 | (code & 0x3f)); + return 6; + } + } +#endif + + return -1; +} diff --git a/includes/tomlc17-R260618/src/tomlc17.h b/includes/tomlc17-R260618/src/tomlc17.h new file mode 100644 index 0000000..1ba4c44 --- /dev/null +++ b/includes/tomlc17-R260618/src/tomlc17.h @@ -0,0 +1,277 @@ +/* Copyright (c) 2024-2026, CK Tan. + * https://github.com/cktan/tomlc17/blob/main/LICENSE + */ + +/** + * @file tomlc17.h + * @brief A TOML parser for C17. + * + * This library provides a simple and efficient way to parse TOML documents + * in C. It supports standard TOML features and provides an easy-to-use API + * for traversing the parsed data. + */ + +#ifndef TOMLC17_H +#define TOMLC17_H + +// A crude way to determine version. Manually changed. +#define TOMLC17_RELEASE_AFTER "260618" + +/* + * USAGE: + * + * 1. Call toml_parse(), toml_parse_file(), or toml_parse_file_ex() + * 2. Check result.ok + * 3. Use toml_get() or toml_seek() to query and traverse the + * result.toptab + * 4. Call toml_free() to release resources. + * + */ + +#include +#include +#include + +#ifdef __cplusplus +#define TOML_EXTERN extern "C" +#else +#define TOML_EXTERN extern +#endif + +/** + * @brief Enumeration of TOML data types. + */ +enum toml_type_t { + TOML_UNKNOWN = 0, /**< Unknown or invalid type */ + TOML_STRING, /**< String type */ + TOML_INT64, /**< 64-bit integer type */ + TOML_FP64, /**< 64-bit floating point type */ + TOML_BOOLEAN, /**< Boolean type */ + TOML_DATE, /**< Local date type */ + TOML_TIME, /**< Local time type */ + TOML_DATETIME, /**< Local datetime type */ + TOML_DATETIMETZ, /**< Offset datetime type */ + TOML_ARRAY, /**< Array type */ + TOML_TABLE, /**< Table type */ +}; +typedef enum toml_type_t toml_type_t; + +/** + * @brief Represents a single piece of TOML data. + * + * This structure is a node in a tree that represents a TOML document. + * The `u` union contains the actual value based on the `type` field. + */ +typedef struct toml_datum_t toml_datum_t; +struct toml_datum_t { + toml_type_t type; /**< Type of the datum */ + uint32_t flag; /**< Internal flag, do not use */ + int lineno; /**< 1-based source line number, 0 if synthesized */ + int colno; /**< 1-based source column number, 0 if synthesized */ + const char *source; /**< Source name (e.g. filename), NULL if not provided. + Owned by the result; valid until toml_free(). */ + union { + const char *s; /**< Shorthand for str.ptr */ + struct { + const char *ptr; /**< NUL terminated string pointer */ + int len; /**< Length of the string excluding the terminating NUL */ + } str; + int64_t int64; /**< 64-bit integer value */ + double fp64; /**< 64-bit floating point value */ + bool boolean; /**< Boolean value */ + struct { /**< Date and time components */ + int16_t year, month, day; + int16_t hour, minute, second; + int32_t usec; + int16_t tz; /**< Timezone offset in minutes */ + } ts; + struct { /**< Array data */ + int32_t size; /**< Number of elements in the array */ + toml_datum_t *elem; /**< Array of elements */ + } arr; + struct { /**< Table data */ + int32_t size; /**< Number of keys in the table */ + const char **key; /**< Array of keys */ + int *len; /**< Array of key lengths */ + toml_datum_t *value; /**< Array of values corresponding to keys */ + } tab; + } u; +}; + +/** + * @brief Result of a TOML parsing operation. + */ +typedef struct toml_result_t toml_result_t; +struct toml_result_t { + bool ok; /**< True if parsing was successful */ + toml_datum_t toptab; /**< The top-level table (valid if ok is true) */ + char errmsg[200]; /**< Error message (valid if ok is false) */ + void *__internal; /**< Internal state, do not use */ +}; + +/** + * @brief Parse a TOML document from a string. + * + * @param src A NUL-terminated string containing the TOML document. + * @param len The length of the string (excluding the NUL terminator). + * @return A toml_result_t structure. Must be freed with toml_free(). + * + * IMPORTANT: src[] must be a NUL terminated string! The len parameter + * does not include the NUL terminator. + */ +TOML_EXTERN toml_result_t toml_parse(const char *src, int len); + +/** + * @brief Parse a TOML document, tagging every datum with a source name. + * + * @param src A NUL-terminated string containing the TOML document. + * @param len The length of the string (excluding the NUL terminator). + * @param name A source name (e.g. filename) copied into the result, or NULL. + * Every parsed datum's `source` is set to this name (or NULL). + * @return A toml_result_t structure. Must be freed with toml_free(). + * + * IMPORTANT: src[] must be a NUL terminated string! The len parameter + * does not include the NUL terminator. + */ +TOML_EXTERN toml_result_t toml_parse_named(const char *src, int len, + const char *name); + +/** + * @brief Parse a TOML document from a file pointer. + * + * @param fp A pointer to the open file. The caller is responsible for closing + * it. + * @return A toml_result_t structure. Must be freed with toml_free(). + * + * IMPORTANT: you are still responsible to fclose(fp). + */ +TOML_EXTERN toml_result_t toml_parse_file(FILE *fp); + +/** + * @brief Parse a TOML document from a file pointer, tagging datums with a name. + * + * @param fp A pointer to the open file. The caller is responsible for closing + * it. + * @param name A source name copied into the result, or NULL. + * @return A toml_result_t structure. Must be freed with toml_free(). + * + * IMPORTANT: you are still responsible to fclose(fp). + */ +TOML_EXTERN toml_result_t toml_parse_file_named(FILE *fp, const char *name); + +/** + * @brief Parse a TOML document from a file path. + * + * @param fname The path to the TOML file. + * @return A toml_result_t structure. Must be freed with toml_free(). + */ +TOML_EXTERN toml_result_t toml_parse_file_ex(const char *fname); + +/** + * @brief Release resources allocated for a TOML result. + * + * @param result The TOML result to free. + */ +TOML_EXTERN void toml_free(toml_result_t result); + +/** + * @brief Find a value for a specific key in a TOML table. + * + * @param table The TOML table to search in. + * @param key The key to look for. + * @return The value associated with the key, or a datum with type TOML_UNKNOWN + * if not found. + */ +TOML_EXTERN toml_datum_t toml_get(toml_datum_t table, const char *key); + +/** + * @brief Locate a value using a multipart-key (e.g., "a.b.c"). + * + * @param table The TOML table to start the search from. + * @param multipart_key A dot-separated key string. No escape characters + * allowed. Maximum length is 255 bytes. + * @return The value found, or a datum with type TOML_UNKNOWN if not found. + */ +TOML_EXTERN toml_datum_t toml_seek(toml_datum_t table, + const char *multipart_key); + +/** + * @brief OBSOLETE: use toml_get() instead. + * Find a key in a toml_table. Return the value of the key if found, + * or a TOML_UNKNOWN otherwise. + */ +static inline toml_datum_t toml_table_find(toml_datum_t table, + const char *key) { + return toml_get(table, key); +} + +/** + * @brief Merge two TOML results. + * + * All results (r1, r2, and the returned result) must be freed independently. + * + * @param r1 The base TOML result. + * @param r2 The TOML result containing overrides. + * @return A new toml_result_t representing the merged document. + * + * LOGIC: + * ret = copy of r1 + * for each item x in r2: + * if x is not in ret: + * set x in ret + * elif x in ret is NOT of the same type: + * override + * elif x in ret is an array of tables: + * append r2.x to ret.x + * elif x in ret is a table: + * merge r2.x to ret.x + * else: + * override + */ +TOML_EXTERN toml_result_t toml_merge(const toml_result_t *r1, + const toml_result_t *r2); + +/** + * @brief Compare two TOML results for equality. + * + * Comparison is sensitive to the order of elements in arrays and tables. + * + * @param r1 The first TOML result. + * @param r2 The second TOML result. + * @return True if they are equivalent, false otherwise. + */ +TOML_EXTERN bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2); + +/** + * @brief Global options for the TOML parser. + */ +typedef struct toml_option_t toml_option_t; +struct toml_option_t { + bool check_utf8; /**< If true, check if all characters are valid UTF-8. + Default: false. */ + void *(*mem_realloc)( + void *ptr, + size_t size); /**< Custom realloc function. Default: realloc(). */ + void (*mem_free)(void *ptr); /**< Custom free function. Default: free(). */ +}; + +/** + * @brief Get the default parser options. + * + * Use this to obtain and initialize a toml_option_t structure before + * customizing it. + * + * @return A toml_option_t with default values. + */ +TOML_EXTERN toml_option_t toml_default_option(void); + +/** + * @brief Set the global parser options. + * + * Call this only if you need to override the default behavior. + * + * @param opt The options to set. + */ +TOML_EXTERN void toml_set_option(toml_option_t opt); + +#endif // TOMLC17_H diff --git a/includes/tomlc17-R260618/src/tomlcpp.hpp b/includes/tomlc17-R260618/src/tomlcpp.hpp new file mode 100644 index 0000000..bd58e32 --- /dev/null +++ b/includes/tomlc17-R260618/src/tomlcpp.hpp @@ -0,0 +1,239 @@ +#pragma once +#if !defined(__cplusplus) || __cplusplus < 202002L +#error "This code requires C++20 or later" +#endif + +#include "tomlc17.h" +#include +#include +#include +#include +#include + +namespace toml { + +class Datum : public toml_datum_t { +public: + using datetime = std::chrono::sys_time; + using time = std::chrono::hh_mm_ss; + using date = std::chrono::year_month_day; + + Datum(toml_datum_t dat = toml_datum_t{}) : toml_datum_t(dat) {} + bool is_table() const { return type == TOML_TABLE; } + bool is_array() const { return type == TOML_ARRAY; } + + // Retrieve a string. + std::optional as_str() const { + if (type != TOML_STRING) + return std::nullopt; + else + return std::string_view{u.str.ptr, (size_t)u.str.len}; + } + + // Retrieve an int. + std::optional as_int() const { + if (type != TOML_INT64) + return std::nullopt; + else + return u.int64; + } + + // Retrieve a float. + std::optional as_real() const { + if (type != TOML_FP64) + return std::nullopt; + else + return u.fp64; + } + + // Retrieve a boolean. + std::optional as_bool() const { + if (type != TOML_BOOLEAN) + return std::nullopt; + else + return !!u.boolean; + } + + // Retrieve a date. + std::optional as_date() const { + using namespace std::chrono; + if (type != TOML_DATE) + return std::nullopt; + else + return date{year(u.ts.year), month(u.ts.month), day(u.ts.day)}; + } + + // Retrieve a time. + std::optional