DMS-XS-Parser
view release on metacpan or search on metacpan
vendor/dms-c/dms.c view on Meta::CPAN
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
return z ^ (z >> 31);
}
static void shuffle_table_items(dms_table *t) {
if (t->len < 2) return;
uint64_t state = (uint64_t)(uintptr_t)t ^ 0xD1B54A32D192ED03ULL;
/* Fisher-Yates over items[]. */
for (size_t i = t->len - 1; i > 0; i--) {
uint64_t r = splitmix64_step(&state);
size_t j = (size_t)(r % (uint64_t)(i + 1));
if (j != i) {
dms_kv tmp = t->items[i];
t->items[i] = t->items[j];
t->items[j] = tmp;
}
}
/* The open-addressed hash slots indexed the pre-shuffle layout;
drop them. dms_table_has / table_get fall back to linear scan
when hash_cap == 0, which is fine for the read paths we still
care about (encoder + lite emit walk items[] directly). */
if (t->hash_slots) {
free(t->hash_slots);
t->hash_slots = NULL;
t->hash_cap = 0;
}
}
static void mark_unordered_walk(dms_value *v) {
if (!v) return;
switch (v->type) {
case DMS_TABLE:
v->u.t.unordered = true;
shuffle_table_items(&v->u.t);
for (size_t i = 0; i < v->u.t.len; i++) {
mark_unordered_walk(v->u.t.items[i].value);
}
return;
case DMS_LIST:
for (size_t i = 0; i < v->u.l.len; i++) {
mark_unordered_walk(v->u.l.items[i]);
}
return;
default:
return;
}
}
static dms_document *parse_document_with_mode(const char *src, size_t srclen, dms_error *err, int lite, int unordered) {
parser p;
memset(&p, 0, sizeof p);
p.src = src;
p.len = srclen;
p.line = 1;
p.record_forms = 1;
p.lite = lite;
p.unordered = unordered;
/* SPEC §"UTF-8 only, NFC-normalized": DMS source is plain UTF-8 with
no byte-order mark. A leading U+FEFF is not silently consumed â
reject it explicitly so encoding mistakes surface loudly. (BOMs
*inside* string/heredoc bodies are fine; this only fires at offset 0.) */
if (srclen >= 3 && (unsigned char)src[0] == 0xEF && (unsigned char)src[1] == 0xBB && (unsigned char)src[2] == 0xBF) {
set_err_at(&p, 1, 0, 0,
"BOM (U+FEFF) at file start is not allowed; DMS source is plain UTF-8");
if (err) *err = p.err;
pending_free(&p.pending);
path_free(&p.path);
attached_free(&p.comments);
original_free(&p.original_forms);
return NULL;
}
/* U+0000 is not allowed anywhere in DMS source (see SPEC §Strings). */
for (size_t i = 0; i < srclen; i++) {
if (src[i] == '\0') {
size_t line = 1, line_start = 0;
for (size_t j = 0; j < i; j++) {
if (src[j] == '\n') { line++; line_start = j + 1; }
}
set_err_at(&p, line, line_start, line_start + (i - line_start),
"U+0000 (NUL) is not allowed in DMS source");
if (err) *err = p.err;
pending_free(&p.pending);
path_free(&p.path);
attached_free(&p.comments);
original_free(&p.original_forms);
return NULL;
}
}
/* SPEC §Unicode normalization: NFC the source before tokenization.
utf8proc allocates; we free at every exit path.
Fast path: if the source is pure ASCII (every byte < 0x80), NFC
is a no-op â the output is byte-identical. utf8proc_map still
walks every byte and allocates a fresh buffer (~420 µs on a
25 KB input vs ~9 µs for the ASCII scan). Skip it: borrow `src`
directly, leave `normalized` NULL, and let the existing
`free(normalized)` cleanup paths no-op (free(NULL) is a no-op).
Pure-ASCII is the common case for config / values files. */
utf8proc_uint8_t *normalized = NULL;
{
const char *scan_src = src + p.pos;
size_t scan_len = srclen - p.pos;
int all_ascii = 1;
for (size_t i = 0; i < scan_len; i++) {
if ((unsigned char)scan_src[i] >= 0x80) { all_ascii = 0; break; }
}
if (all_ascii) {
p.src = scan_src;
p.len = scan_len;
p.pos = 0;
p.line_start = 0;
} else {
utf8proc_ssize_t n = utf8proc_map(
(const utf8proc_uint8_t *)scan_src,
(utf8proc_ssize_t)scan_len,
&normalized,
UTF8PROC_STABLE | UTF8PROC_COMPOSE);
if (n < 0) {
set_err_at(&p, 1, 0, 0, "input is not valid UTF-8");
if (err) *err = p.err;
pending_free(&p.pending);
path_free(&p.path);
attached_free(&p.comments);
original_free(&p.original_forms);
vendor/dms-c/dms.c view on Meta::CPAN
}
/* SPEC v0.14 canonical names. */
dms_document *dms_decode_document(const char *src, size_t srclen, dms_error *err) {
return parse_document_with_mode(src, srclen, err, 0, 0);
}
dms_document *dms_decode_document_lite(const char *src, size_t srclen, dms_error *err) {
return parse_document_with_mode(src, srclen, err, 1, 0);
}
dms_document *dms_decode_document_unordered(const char *src, size_t srclen, dms_error *err) {
return parse_document_with_mode(src, srclen, err, 0, 1);
}
dms_document *dms_decode_document_lite_unordered(const char *src, size_t srclen, dms_error *err) {
return parse_document_with_mode(src, srclen, err, 1, 1);
}
dms_value *dms_decode(const char *src, size_t srclen, dms_error *err) {
dms_document *d = dms_decode_document(src, srclen, err);
if (!d) return NULL;
dms_value *v = d->body;
free_table_inplace(d->meta);
for (size_t i = 0; i < d->num_comments; i++) {
free(d->comments[i].content);
for (size_t j = 0; j < d->comments[i].path_len; j++) {
if (!d->comments[i].path[j].is_index) free(d->comments[i].path[j].key);
}
free(d->comments[i].path);
}
free(d->comments);
for (size_t i = 0; i < d->num_original_forms; i++) {
original_entry_free(&d->original_forms[i]);
}
free(d->original_forms);
free(d);
return v;
}
void dms_table_free(dms_table *t) {
free_table_inplace(t);
}
/* Fast byte-level pre-scan that bounds the front-matter region without
touching body bytes. Mirrors the line-by-line shape of
`skip_trivia` + `parse_front_matter_tbl`'s closer scan, but operates
on raw (pre-NFC) bytes and only resolves comment delimiters, blank
lines, and `+++` literals â never tokenizes keys/values.
Returns:
1 â `*fm_end_out` is the byte offset (in raw `src`) one past the
closing `+++`'s EOL. The caller can NUL/NFC-validate just
`src[0..*fm_end_out]` and feed that slice to
`parse_front_matter_tbl` â body bytes are skipped entirely.
0 â "no FM present": leading trivia walks cleanly, but the next
non-trivia byte is not `+++`. `*fm_end_out` is unset.
-1 â bail to the slow path. The fast scanner can't bound the FM
region without doing real validation work â either the input
starts with a BOM, contains an unterminated/malformed comment
in leading trivia, or never closes the FM. The slow path
surfaces the matching diagnostic.
The closing `+++` is detected by the same trimmed-line test
`parse_front_matter_tbl` uses (lines 2202-2206), so the bounded
region byte-matches what the slow path would have consumed. */
static int find_fm_end_fast(const char *src, size_t srclen, size_t *fm_end_out) {
/* BOM at offset 0 â slow path so the dedicated diagnostic fires. */
if (srclen >= 3
&& (unsigned char)src[0] == 0xEF
&& (unsigned char)src[1] == 0xBB
&& (unsigned char)src[2] == 0xBF) {
return -1;
}
size_t pos = 0;
/* ----- leading trivia: blank lines + line/block comments ----- */
while (pos < srclen) {
/* skip inline ws */
size_t ls = pos;
while (pos < srclen && (src[pos] == ' ' || src[pos] == '\t')) pos++;
if (pos >= srclen) break;
char c = src[pos];
if (c == '\n') {
pos++;
continue;
}
if (c == '\r') {
/* bare CR is a parse error per skip_trivia â slow path. */
if (pos + 1 >= srclen || src[pos + 1] != '\n') return -1;
pos += 2;
continue;
}
if (c == '#') {
if (pos + 2 < srclen && src[pos + 1] == '#' && src[pos + 2] == '#') {
/* ### [LABEL] block comment. Replicate the byte shape
of skip_hash_block_comment: read a label, expect EOL,
then scan for a line trimmed to the terminator. */
size_t op = pos;
pos += 3;
size_t label_start = pos;
while (pos < srclen) {
char lc = src[pos];
if (!(lc == '_' || (lc >= '0' && lc <= '9') ||
(lc >= 'a' && lc <= 'z') || (lc >= 'A' && lc <= 'Z'))) break;
pos++;
}
size_t label_len = pos - label_start;
/* Empty label OK (matches `###`); first-char rule
("must start with a letter or underscore") is a
diagnostic the slow path owns â bail if violated. */
if (label_len > 0) {
char fc = src[label_start];
if (!(fc == '_' || (fc >= 'a' && fc <= 'z') || (fc >= 'A' && fc <= 'Z'))) {
return -1;
}
}
/* trailing ws + EOL on opener line */
while (pos < srclen && (src[pos] == ' ' || src[pos] == '\t')) pos++;
if (pos < srclen) {
if (src[pos] == '\n') pos++;
else if (pos + 1 < srclen && src[pos] == '\r' && src[pos + 1] == '\n') pos += 2;
else return -1; /* opener-must-be-on-its-own-line diag */
}
/* scan for terminator */
int closed = 0;
while (pos < srclen) {
size_t lb = pos;
while (pos < srclen && src[pos] != '\n' && src[pos] != '\r') pos++;
vendor/dms-c/dms.c view on Meta::CPAN
} else {
pos++;
}
}
continue;
}
/* non-trivia byte: rewind to line start (we may have consumed
leading inline ws that turns out to be indentation before a
non-trivia char â `skip_trivia` rewinds to `start` in that
case at line 1513). */
pos = ls;
break;
}
/* ----- opener: expect "+++" ----- */
if (pos + 3 > srclen || src[pos] != '+' || src[pos + 1] != '+' || src[pos + 2] != '+') {
/* No FM present. */
return 0;
}
pos += 3;
/* opener line must end in EOL (with optional trailing ws). Trailing
junk is a parse error owned by the slow path â bail in that case
so the right diagnostic surfaces. */
while (pos < srclen && (src[pos] == ' ' || src[pos] == '\t')) pos++;
if (pos < srclen) {
if (src[pos] == '\n') pos++;
else if (pos + 1 < srclen && src[pos] == '\r' && src[pos + 1] == '\n') pos += 2;
else return -1; /* trailing junk â "opener must be on its own line" */
}
/* ----- inner lines until closing "+++" (trimmed-line test) ----- */
while (pos < srclen) {
size_t lb = pos;
while (pos < srclen && src[pos] != '\n' && src[pos] != '\r') pos++;
size_t le = pos;
size_t after_eol = pos;
if (pos < srclen) {
if (src[pos] == '\n') after_eol = pos + 1;
else if (pos + 1 < srclen && src[pos] == '\r' && src[pos + 1] == '\n') after_eol = pos + 2;
else return -1; /* bare CR */
}
/* trim ws */
size_t ts = lb, te = le;
while (ts < te && (src[ts] == ' ' || src[ts] == '\t')) ts++;
while (te > ts && (src[te - 1] == ' ' || src[te - 1] == '\t')) te--;
if (te - ts == 3 && src[ts] == '+' && src[ts + 1] == '+' && src[ts + 2] == '+') {
*fm_end_out = after_eol;
return 1;
}
if (after_eol == pos) {
/* EOF without closer â slow path emits "unterminated FM". */
return -1;
}
pos = after_eol;
}
/* Hit EOF without finding closer. */
return -1;
}
/* SPEC §Front-matter-only decode (tier-0, required).
Pre-scans raw bytes to bound the FM region (`find_fm_end_fast`),
then BOM/NUL/NFC-validates only that prefix and feeds it to
`parse_front_matter_tbl`. Body bytes are never visited â call cost
scales with FM-region size, not source size. On any pre-scan
ambiguity (BOM, malformed leading comment, missing closer, etc.)
we fall through to the slow path which validates the entire source
and emits the canonical diagnostic.
`parse_front_matter_tbl`'s contract:
- returns NULL with `p.has_err == 0` when the document has no
opening `+++` (no FM present),
- returns a calloc'd `dms_table *` (possibly with `len == 0`
for an empty `+++\n+++`) on success,
- returns NULL with `p.has_err == 1` on any in-FM error.
We translate that into `(has_front_matter, return value, err)`. */
dms_table *dms_decode_front_matter(const char *src, size_t srclen,
bool *has_front_matter,
dms_error *err) {
if (has_front_matter) *has_front_matter = false;
parser p;
memset(&p, 0, sizeof p);
p.src = src;
p.len = srclen;
p.line = 1;
p.record_forms = 1;
/* SPEC §Front-matter-only decode: this entry point runs in lite
mode (no comment AST, no original_forms inside the FM). */
p.lite = 1;
p.unordered = 0;
/* Fast pre-scan: bound the FM region so the BOM/NUL/NFC pass below
only touches FM bytes. NFC of a stable-boundary prefix equals
the prefix of NFC(full) (the closing `+++` is ASCII and therefore
a stable boundary), so byte offsets and line numbers inside the
FM region are identical to the full-source path â diagnostics
remain byte-identical per SPEC. */
size_t scan_len = srclen;
{
size_t fm_end = 0;
int rc = find_fm_end_fast(src, srclen, &fm_end);
if (rc == 0) {
/* No FM present â fast bail without touching body bytes. */
if (has_front_matter) *has_front_matter = false;
return NULL;
} else if (rc == 1) {
/* Restrict validation/parse to the FM region. */
scan_len = fm_end;
}
/* rc == -1: fall through to the slow full-source path. */
}
/* SPEC §"UTF-8 only, NFC-normalized": reject a leading BOM so
encoding mistakes surface loudly. */
if (scan_len >= 3
&& (unsigned char)src[0] == 0xEF
&& (unsigned char)src[1] == 0xBB
&& (unsigned char)src[2] == 0xBF) {
set_err_at(&p, 1, 0, 0,
"BOM (U+FEFF) at file start is not allowed; DMS source is plain UTF-8");
if (err) *err = p.err;
pending_free(&p.pending);
path_free(&p.path);
attached_free(&p.comments);
original_free(&p.original_forms);
return NULL;
}
/* U+0000 is not allowed anywhere in DMS source. We scan the FM
region only â body NULs are not surfaced by this entry point
per SPEC (body bytes aren't tokenized here). */
for (size_t i = 0; i < scan_len; i++) {
if (src[i] == '\0') {
size_t line = 1, line_start = 0;
for (size_t j = 0; j < i; j++) {
if (src[j] == '\n') { line++; line_start = j + 1; }
}
set_err_at(&p, line, line_start, line_start + (i - line_start),
"U+0000 (NUL) is not allowed in DMS source");
if (err) *err = p.err;
pending_free(&p.pending);
path_free(&p.path);
attached_free(&p.comments);
original_free(&p.original_forms);
return NULL;
}
}
/* NFC-normalize the FM region. ASCII fast path matches the body
decoder so the in-FM byte offsets reported in error messages
are identical. */
utf8proc_uint8_t *normalized = NULL;
{
const char *scan_src = src + p.pos;
size_t inner_len = scan_len - p.pos;
int all_ascii = 1;
for (size_t i = 0; i < inner_len; i++) {
if ((unsigned char)scan_src[i] >= 0x80) { all_ascii = 0; break; }
}
if (all_ascii) {
p.src = scan_src;
p.len = inner_len;
p.pos = 0;
p.line_start = 0;
} else {
utf8proc_ssize_t n = utf8proc_map(
(const utf8proc_uint8_t *)scan_src,
(utf8proc_ssize_t)inner_len,
&normalized,
UTF8PROC_STABLE | UTF8PROC_COMPOSE);
if (n < 0) {
set_err_at(&p, 1, 0, 0, "input is not valid UTF-8");
if (err) *err = p.err;
pending_free(&p.pending);
path_free(&p.path);
attached_free(&p.comments);
original_free(&p.original_forms);
return NULL;
}
p.src = (const char *)normalized;
p.len = (size_t)n;
p.pos = 0;
( run in 0.687 second using v1.01-cache-2.11-cpan-9581c071862 )