From 5a2e2db527209b29b40731261102ab2cf35a7f02 Mon Sep 17 00:00:00 2001 From: phenomenon0 Date: Sat, 7 Mar 2026 16:38:48 -0600 Subject: [PATCH 1/2] Harden glyph codecs across runtimes --- c/glyph-codec/src/decimal128.c | 14 +- c/glyph-codec/src/glyph.c | 160 ++++++++--- c/glyph-codec/src/json.c | 189 ++++++++++-- c/glyph-codec/src/schema_evolution.c | 147 ++++++++-- c/glyph-codec/src/stream_validator.c | 332 +++++++++++++++++----- c/glyph-codec/test/test_glyph.c | 120 ++++++++ go/glyph/canon.go | 5 + go/glyph/incremental.go | 137 ++++++--- go/glyph/loose.go | 9 +- go/glyph/parse.go | 32 ++- go/glyph/parse_packed.go | 28 +- go/glyph/parse_patch.go | 9 +- go/glyph/parse_tabular.go | 3 + go/glyph/security_regression_test.go | 110 +++++++ js/src/glyph.test.ts | 62 ++++ js/src/json.ts | 67 +++-- js/src/loose.ts | 40 ++- js/src/parse.ts | 62 ++-- js/src/patch.ts | 197 +++++++++---- js/src/stream/gs1t.ts | 56 +++- js/src/stream/stream.test.ts | 13 + js/src/stream_validator.test.ts | 50 ++++ js/src/stream_validator.ts | 93 ++++-- py/glyph/loose.py | 21 +- py/glyph/parse.py | 292 +++++++++++-------- py/glyph/stream_validator.py | 347 ++++++++++++++++------- py/glyph/types.py | 17 +- py/tests/test_glyph.py | 64 +++++ py/tests/test_stream_validator.py | 90 ++++++ rust/glyph-codec/src/decimal128.rs | 238 ++++++++++++---- rust/glyph-codec/src/error.rs | 3 + rust/glyph-codec/src/json_bridge.rs | 102 +++++-- rust/glyph-codec/src/schema_evolution.rs | 36 +++ rust/glyph-codec/src/stream_validator.rs | 315 ++++++++++++++++++-- 34 files changed, 2758 insertions(+), 702 deletions(-) create mode 100644 go/glyph/security_regression_test.go diff --git a/c/glyph-codec/src/decimal128.c b/c/glyph-codec/src/decimal128.c index a9ce857..74354a0 100644 --- a/c/glyph-codec/src/decimal128.c +++ b/c/glyph-codec/src/decimal128.c @@ -103,9 +103,10 @@ decimal128_t decimal128_from_int(int64_t value) { decimal128_t d; d.scale = 0; if (value < 0) { + uint64_t magnitude = ((uint64_t)(-(value + 1))) + 1; d.negative = true; d.coef_high = 0; - d.coef_low = (uint64_t)(-value); + d.coef_low = magnitude; } else { d.negative = false; d.coef_high = 0; @@ -206,10 +207,15 @@ int64_t decimal128_to_int(const decimal128_t *d) { } /* Check if fits in int64 */ - if (h != 0 || l > INT64_MAX) { + if (h != 0 || (!d->negative && l > INT64_MAX) || + (d->negative && l > (uint64_t)INT64_MAX + 1ULL)) { return d->negative ? INT64_MIN : INT64_MAX; } + if (d->negative && l == (uint64_t)INT64_MAX + 1ULL) { + return INT64_MIN; + } + return d->negative ? -(int64_t)l : (int64_t)l; } @@ -241,6 +247,7 @@ char *decimal128_to_string(const decimal128_t *d) { } /* Build 0.000... */ char *result = malloc(d->scale + 3); + if (!result) return NULL; result[0] = '0'; result[1] = '.'; for (int i = 0; i < d->scale; i++) { @@ -262,6 +269,7 @@ char *decimal128_to_string(const decimal128_t *d) { if (d->scale == 0) { size_t result_len = digit_count + (d->negative ? 1 : 0) + 1; char *result = malloc(result_len); + if (!result) return NULL; if (d->negative) { result[0] = '-'; strcpy(result + 1, digit_str); @@ -277,6 +285,7 @@ char *decimal128_to_string(const decimal128_t *d) { /* 0.00...XXX */ size_t result_len = 2 + (-int_digits) + digit_count + (d->negative ? 1 : 0) + 1; char *result = malloc(result_len); + if (!result) return NULL; char *p = result; if (d->negative) *p++ = '-'; *p++ = '0'; @@ -291,6 +300,7 @@ char *decimal128_to_string(const decimal128_t *d) { /* Normal case: XXX.YYY */ size_t result_len = digit_count + 1 + (d->negative ? 1 : 0) + 1; char *result = malloc(result_len); + if (!result) return NULL; char *p = result; if (d->negative) *p++ = '-'; memcpy(p, digit_str, int_digits); diff --git a/c/glyph-codec/src/glyph.c b/c/glyph-codec/src/glyph.c index 8464ca0..c9352bf 100644 --- a/c/glyph-codec/src/glyph.c +++ b/c/glyph-codec/src/glyph.c @@ -29,41 +29,64 @@ typedef struct { char *data; size_t len; size_t cap; + bool oom; } strbuf_t; static void strbuf_init(strbuf_t *buf) { buf->data = malloc(256); buf->len = 0; buf->cap = 256; - if (buf->data) buf->data[0] = '\0'; + buf->oom = (buf->data == NULL); + if (buf->data) { + buf->data[0] = '\0'; + } else { + buf->cap = 0; + } } -static void strbuf_grow(strbuf_t *buf, size_t need) { - if (buf->len + need >= buf->cap) { - size_t new_cap = buf->cap * 2; - while (buf->len + need >= new_cap) new_cap *= 2; - char *new_data = realloc(buf->data, new_cap); - if (new_data) { - buf->data = new_data; - buf->cap = new_cap; +static bool strbuf_grow(strbuf_t *buf, size_t need) { + if (buf->oom) return false; + if (buf->len + need < buf->cap) return true; + + size_t new_cap = buf->cap ? buf->cap : 1; + while (buf->len + need >= new_cap) { + if (new_cap > SIZE_MAX / 2) { + buf->oom = true; + return false; } + new_cap *= 2; } + + char *new_data = realloc(buf->data, new_cap); + if (!new_data) { + buf->oom = true; + return false; + } + buf->data = new_data; + buf->cap = new_cap; + return true; } static void strbuf_append(strbuf_t *buf, const char *s) { + if (!s) return; size_t len = strlen(s); - strbuf_grow(buf, len + 1); + if (!strbuf_grow(buf, len + 1)) return; memcpy(buf->data + buf->len, s, len + 1); buf->len += len; } static void strbuf_append_char(strbuf_t *buf, char c) { - strbuf_grow(buf, 2); + if (!strbuf_grow(buf, 2)) return; buf->data[buf->len++] = c; buf->data[buf->len] = '\0'; } static char *strbuf_finish(strbuf_t *buf) { + if (buf->oom) { + free(buf->data); + buf->data = NULL; + return NULL; + } return buf->data; } @@ -109,16 +132,29 @@ glyph_value_t *glyph_str(const char *val) { if (v) { v->type = GLYPH_STR; v->str_val = strdup_safe(val); + if (val && !v->str_val) { + free(v); + return NULL; + } } return v; } glyph_value_t *glyph_bytes(const uint8_t *data, size_t len) { + if (len > 0 && !data) return NULL; + glyph_value_t *v = calloc(1, sizeof(glyph_value_t)); if (v) { v->type = GLYPH_BYTES; - v->bytes_val.data = malloc(len); - if (v->bytes_val.data) { + if (len == 0) { + v->bytes_val.data = NULL; + v->bytes_val.len = 0; + } else { + v->bytes_val.data = malloc(len); + if (!v->bytes_val.data) { + free(v); + return NULL; + } memcpy(v->bytes_val.data, data, len); v->bytes_val.len = len; } @@ -132,6 +168,12 @@ glyph_value_t *glyph_id(const char *prefix, const char *value) { v->type = GLYPH_ID; v->id_val.prefix = strdup_safe(prefix ? prefix : ""); v->id_val.value = strdup_safe(value); + if (!v->id_val.prefix || (value && !v->id_val.value)) { + free(v->id_val.prefix); + free(v->id_val.value); + free(v); + return NULL; + } } return v; } @@ -152,11 +194,13 @@ void glyph_list_append(glyph_value_t *list, glyph_value_t *item) { size_t new_count = list->list_val.count + 1; glyph_value_t **new_items = realloc(list->list_val.items, new_count * sizeof(glyph_value_t *)); - if (new_items) { - new_items[list->list_val.count] = item; - list->list_val.items = new_items; - list->list_val.count = new_count; + if (!new_items) { + glyph_value_free(item); + return; } + new_items[list->list_val.count] = item; + list->list_val.items = new_items; + list->list_val.count = new_count; } glyph_value_t *glyph_map_new(void) { @@ -172,15 +216,24 @@ glyph_value_t *glyph_map_new(void) { void glyph_map_set(glyph_value_t *map, const char *key, glyph_value_t *value) { if (!map || map->type != GLYPH_MAP || !key || !value) return; + char *key_copy = strdup_safe(key); + if (!key_copy) { + glyph_value_free(value); + return; + } + size_t new_count = map->map_val.count + 1; glyph_map_entry_t *new_entries = realloc(map->map_val.entries, new_count * sizeof(glyph_map_entry_t)); - if (new_entries) { - new_entries[map->map_val.count].key = strdup_safe(key); - new_entries[map->map_val.count].value = value; - map->map_val.entries = new_entries; - map->map_val.count = new_count; + if (!new_entries) { + free(key_copy); + glyph_value_free(value); + return; } + new_entries[map->map_val.count].key = key_copy; + new_entries[map->map_val.count].value = value; + map->map_val.entries = new_entries; + map->map_val.count = new_count; } glyph_value_t *glyph_struct_new(const char *type_name) { @@ -188,6 +241,10 @@ glyph_value_t *glyph_struct_new(const char *type_name) { if (v) { v->type = GLYPH_STRUCT; v->struct_val.type_name = strdup_safe(type_name); + if (type_name && !v->struct_val.type_name) { + free(v); + return NULL; + } v->struct_val.fields = NULL; v->struct_val.fields_count = 0; } @@ -197,15 +254,24 @@ glyph_value_t *glyph_struct_new(const char *type_name) { void glyph_struct_set(glyph_value_t *s, const char *key, glyph_value_t *value) { if (!s || s->type != GLYPH_STRUCT || !key || !value) return; + char *key_copy = strdup_safe(key); + if (!key_copy) { + glyph_value_free(value); + return; + } + size_t new_count = s->struct_val.fields_count + 1; glyph_map_entry_t *new_fields = realloc(s->struct_val.fields, new_count * sizeof(glyph_map_entry_t)); - if (new_fields) { - new_fields[s->struct_val.fields_count].key = strdup_safe(key); - new_fields[s->struct_val.fields_count].value = value; - s->struct_val.fields = new_fields; - s->struct_val.fields_count = new_count; + if (!new_fields) { + free(key_copy); + glyph_value_free(value); + return; } + new_fields[s->struct_val.fields_count].key = key_copy; + new_fields[s->struct_val.fields_count].value = value; + s->struct_val.fields = new_fields; + s->struct_val.fields_count = new_count; } glyph_value_t *glyph_sum(const char *tag, glyph_value_t *value) { @@ -214,6 +280,11 @@ glyph_value_t *glyph_sum(const char *tag, glyph_value_t *value) { v->type = GLYPH_SUM; v->sum_val.tag = strdup_safe(tag); v->sum_val.value = value; + if (tag && !v->sum_val.tag) { + glyph_value_free(value); + free(v); + return NULL; + } } return v; } @@ -382,11 +453,17 @@ static void write_canon_value(strbuf_t *buf, const glyph_value_t *v, /* Compare entries by canonical key for sorting */ static int compare_entries(const void *a, const void *b) { - const glyph_map_entry_t *ea = a; - const glyph_map_entry_t *eb = b; + const glyph_map_entry_t *ea = (const glyph_map_entry_t *)a; + const glyph_map_entry_t *eb = (const glyph_map_entry_t *)b; return strcmp(ea->key, eb->key); } +static int compare_key_strings(const void *a, const void *b) { + const char *const *ka = (const char *const *)a; + const char *const *kb = (const char *const *)b; + return strcmp(*ka, *kb); +} + static void write_canon_map(strbuf_t *buf, const glyph_map_entry_t *entries, size_t count, const glyph_canon_opts_t *opts) { strbuf_append_char(buf, '{'); @@ -422,6 +499,7 @@ static bool check_homogeneous(glyph_value_t **items, size_t count, /* Collect all keys */ size_t all_keys_cap = 64; char **all_keys = malloc(all_keys_cap * sizeof(char *)); + if (!all_keys) return false; size_t all_keys_count = 0; for (size_t i = 0; i < count; i++) { @@ -452,9 +530,21 @@ static bool check_homogeneous(glyph_value_t **items, size_t count, if (!found) { if (all_keys_count >= all_keys_cap) { all_keys_cap *= 2; - all_keys = realloc(all_keys, all_keys_cap * sizeof(char *)); + char **new_all_keys = realloc(all_keys, all_keys_cap * sizeof(char *)); + if (!new_all_keys) { + for (size_t m = 0; m < all_keys_count; m++) free(all_keys[m]); + free(all_keys); + return false; + } + all_keys = new_all_keys; + } + all_keys[all_keys_count] = strdup_safe(key); + if (!all_keys[all_keys_count]) { + for (size_t m = 0; m < all_keys_count; m++) free(all_keys[m]); + free(all_keys); + return false; } - all_keys[all_keys_count++] = strdup_safe(key); + all_keys_count++; } } } @@ -503,8 +593,7 @@ static bool check_homogeneous(glyph_value_t **items, size_t count, } /* Sort columns */ - qsort(all_keys, all_keys_count, sizeof(char *), - (int (*)(const void *, const void *))strcmp); + qsort(all_keys, all_keys_count, sizeof(char *), compare_key_strings); *out_cols = all_keys; *out_col_count = all_keys_count; @@ -717,7 +806,12 @@ char *glyph_canonicalize_loose_no_tabular(const glyph_value_t *v) { char *glyph_canonicalize_loose_with_opts(const glyph_value_t *v, const glyph_canon_opts_t *opts) { strbuf_t buf; + glyph_canon_opts_t default_opts; strbuf_init(&buf); + if (!opts) { + default_opts = glyph_canon_opts_default(); + opts = &default_opts; + } write_canon_value(&buf, v, opts); return strbuf_finish(&buf); } diff --git a/c/glyph-codec/src/json.c b/c/glyph-codec/src/json.c index 0d8d921..df19f90 100644 --- a/c/glyph-codec/src/json.c +++ b/c/glyph-codec/src/json.c @@ -10,6 +10,9 @@ #include #include +#define JSON_MAX_DEPTH 128u +#define JSON_MAX_STRING_LEN (1024u * 1024u) + /* ============================================================ * JSON Parser * ============================================================ */ @@ -18,8 +21,67 @@ typedef struct { const char *input; size_t pos; size_t len; + size_t depth; } json_parser_t; +typedef struct { + char *data; + size_t len; + size_t cap; +} parse_strbuf_t; + +static bool parse_strbuf_grow(parse_strbuf_t *buf, size_t needed) { + if (needed > JSON_MAX_STRING_LEN + 1) { + return false; + } + if (needed <= buf->cap) { + return true; + } + + size_t new_cap = buf->cap ? buf->cap : 1; + while (new_cap < needed) { + if (new_cap > (JSON_MAX_STRING_LEN + 1) / 2) { + new_cap = JSON_MAX_STRING_LEN + 1; + break; + } + new_cap *= 2; + } + if (new_cap < needed || new_cap > JSON_MAX_STRING_LEN + 1) { + return false; + } + + char *new_data = realloc(buf->data, new_cap); + if (!new_data) { + return false; + } + buf->data = new_data; + buf->cap = new_cap; + return true; +} + +static bool parse_strbuf_append_char(parse_strbuf_t *buf, char c) { + if (!parse_strbuf_grow(buf, buf->len + 2)) { + return false; + } + buf->data[buf->len++] = c; + buf->data[buf->len] = '\0'; + return true; +} + +static bool parser_enter_container(json_parser_t *p) { + if (p->depth >= JSON_MAX_DEPTH) { + return false; + } + p->depth++; + return true; +} + +static void parser_leave_container(json_parser_t *p) { + if (p->depth > 0) { + p->depth--; + } +} + static void skip_whitespace(json_parser_t *p) { while (p->pos < p->len && isspace((unsigned char)p->input[p->pos])) { p->pos++; @@ -51,15 +113,18 @@ static glyph_value_t *parse_value(json_parser_t *p); static char *parse_string(json_parser_t *p) { if (next(p) != '"') return NULL; - size_t cap = 64; - char *str = malloc(cap); - size_t len = 0; + parse_strbuf_t buf = { + .data = malloc(64), + .len = 0, + .cap = 64, + }; + if (!buf.data) return NULL; + buf.data[0] = '\0'; while (p->pos < p->len) { char c = p->input[p->pos++]; if (c == '"') { - str[len] = '\0'; - return str; + return buf.data; } if (c == '\\' && p->pos < p->len) { c = p->input[p->pos++]; @@ -80,35 +145,35 @@ static char *parse_string(json_parser_t *p) { if (code < 0x80) { c = (char)code; } else if (code < 0x800) { - if (len + 2 >= cap) { - cap *= 2; - str = realloc(str, cap); + if (!parse_strbuf_append_char(&buf, (char)(0xC0 | (code >> 6)))) { + free(buf.data); + return NULL; } - str[len++] = 0xC0 | (code >> 6); c = 0x80 | (code & 0x3F); } else { - if (len + 3 >= cap) { - cap *= 2; - str = realloc(str, cap); + if (!parse_strbuf_append_char(&buf, (char)(0xE0 | (code >> 12))) || + !parse_strbuf_append_char(&buf, (char)(0x80 | ((code >> 6) & 0x3F)))) { + free(buf.data); + return NULL; } - str[len++] = 0xE0 | (code >> 12); - str[len++] = 0x80 | ((code >> 6) & 0x3F); c = 0x80 | (code & 0x3F); } + } else { + free(buf.data); + return NULL; } break; } default: break; } } - if (len + 1 >= cap) { - cap *= 2; - str = realloc(str, cap); + if (!parse_strbuf_append_char(&buf, c)) { + free(buf.data); + return NULL; } - str[len++] = c; } - free(str); + free(buf.data); return NULL; } @@ -134,12 +199,18 @@ static glyph_value_t *parse_number(json_parser_t *p) { static glyph_value_t *parse_array(json_parser_t *p) { if (next(p) != '[') return NULL; + if (!parser_enter_container(p)) return NULL; glyph_value_t *list = glyph_list_new(); + if (!list) { + parser_leave_container(p); + return NULL; + } skip_whitespace(p); if (peek(p) == ']') { next(p); + parser_leave_container(p); return list; } @@ -147,18 +218,27 @@ static glyph_value_t *parse_array(json_parser_t *p) { glyph_value_t *item = parse_value(p); if (!item) { glyph_value_free(list); + parser_leave_container(p); return NULL; } + size_t prev_count = list->list_val.count; glyph_list_append(list, item); + if (list->list_val.count != prev_count + 1) { + glyph_value_free(list); + parser_leave_container(p); + return NULL; + } skip_whitespace(p); char c = peek(p); if (c == ']') { next(p); + parser_leave_container(p); break; } if (c != ',') { glyph_value_free(list); + parser_leave_container(p); return NULL; } next(p); /* consume comma */ @@ -169,12 +249,18 @@ static glyph_value_t *parse_array(json_parser_t *p) { static glyph_value_t *parse_object(json_parser_t *p) { if (next(p) != '{') return NULL; + if (!parser_enter_container(p)) return NULL; glyph_value_t *map = glyph_map_new(); + if (!map) { + parser_leave_container(p); + return NULL; + } skip_whitespace(p); if (peek(p) == '}') { next(p); + parser_leave_container(p); return map; } @@ -182,12 +268,14 @@ static glyph_value_t *parse_object(json_parser_t *p) { skip_whitespace(p); if (peek(p) != '"') { glyph_value_free(map); + parser_leave_container(p); return NULL; } char *key = parse_string(p); if (!key) { glyph_value_free(map); + parser_leave_container(p); return NULL; } @@ -195,6 +283,7 @@ static glyph_value_t *parse_object(json_parser_t *p) { if (next(p) != ':') { free(key); glyph_value_free(map); + parser_leave_container(p); return NULL; } @@ -202,20 +291,30 @@ static glyph_value_t *parse_object(json_parser_t *p) { if (!value) { free(key); glyph_value_free(map); + parser_leave_container(p); return NULL; } + size_t prev_count = map->map_val.count; glyph_map_set(map, key, value); + if (map->map_val.count != prev_count + 1) { + free(key); + glyph_value_free(map); + parser_leave_container(p); + return NULL; + } free(key); skip_whitespace(p); char c = peek(p); if (c == '}') { next(p); + parser_leave_container(p); break; } if (c != ',') { glyph_value_free(map); + parser_leave_container(p); return NULL; } next(p); /* consume comma */ @@ -264,9 +363,19 @@ glyph_value_t *glyph_from_json(const char *json) { .input = json, .pos = 0, .len = strlen(json), + .depth = 0, }; - return parse_value(&p); + glyph_value_t *value = parse_value(&p); + if (!value) return NULL; + + skip_whitespace(&p); + if (p.pos != p.len) { + glyph_value_free(value); + return NULL; + } + + return value; } /* ============================================================ @@ -277,31 +386,53 @@ typedef struct { char *data; size_t len; size_t cap; + bool oom; } json_buf_t; static void json_buf_init(json_buf_t *buf) { buf->cap = 256; buf->data = malloc(buf->cap); buf->len = 0; - if (buf->data) buf->data[0] = '\0'; + buf->oom = (buf->data == NULL); + if (buf->data) { + buf->data[0] = '\0'; + } else { + buf->cap = 0; + } } -static void json_buf_grow(json_buf_t *buf, size_t need) { - if (buf->len + need >= buf->cap) { - while (buf->len + need >= buf->cap) buf->cap *= 2; - buf->data = realloc(buf->data, buf->cap); +static bool json_buf_grow(json_buf_t *buf, size_t need) { + if (buf->oom) return false; + if (buf->len + need < buf->cap) return true; + + size_t new_cap = buf->cap ? buf->cap : 1; + while (buf->len + need >= new_cap) { + if (new_cap > SIZE_MAX / 2) { + buf->oom = true; + return false; + } + new_cap *= 2; + } + char *new_data = realloc(buf->data, new_cap); + if (!new_data) { + buf->oom = true; + return false; } + buf->data = new_data; + buf->cap = new_cap; + return true; } static void json_buf_append(json_buf_t *buf, const char *s) { + if (!s) return; size_t len = strlen(s); - json_buf_grow(buf, len + 1); + if (!json_buf_grow(buf, len + 1)) return; memcpy(buf->data + buf->len, s, len + 1); buf->len += len; } static void json_buf_append_char(json_buf_t *buf, char c) { - json_buf_grow(buf, 2); + if (!json_buf_grow(buf, 2)) return; buf->data[buf->len++] = c; buf->data[buf->len] = '\0'; } @@ -453,6 +584,10 @@ char *glyph_to_json(const glyph_value_t *v) { json_buf_t buf; json_buf_init(&buf); write_json_value(&buf, v); + if (buf.oom) { + free(buf.data); + return NULL; + } return buf.data; } diff --git a/c/glyph-codec/src/schema_evolution.c b/c/glyph-codec/src/schema_evolution.c index ae72392..5545a94 100644 --- a/c/glyph-codec/src/schema_evolution.c +++ b/c/glyph-codec/src/schema_evolution.c @@ -20,6 +20,21 @@ static char *strdup_safe(const char *s) { return copy; } +static void evolving_field_clear(evolving_field_t *f) { + if (!f) return; + free(f->name); + f->name = NULL; + field_value_free(&f->default_value); + free(f->added_in); + f->added_in = NULL; + free(f->deprecated_in); + f->deprecated_in = NULL; + free(f->renamed_from); + f->renamed_from = NULL; + free(f->validation); + f->validation = NULL; +} + /* ============================================================ * Field Value Functions * ============================================================ */ @@ -78,17 +93,21 @@ evolving_field_t *evolving_field_new(const char *name, const evolving_field_conf f->renamed_from = strdup_safe(config->renamed_from); f->validation = strdup_safe(config->validation); + if (!f->name || !f->added_in || + (config->default_value.type == FIELD_VALUE_STR && config->default_value.str_val && !f->default_value.str_val) || + (config->deprecated_in && !f->deprecated_in) || + (config->renamed_from && !f->renamed_from) || + (config->validation && !f->validation)) { + evolving_field_free(f); + return NULL; + } + return f; } void evolving_field_free(evolving_field_t *f) { if (!f) return; - free(f->name); - field_value_free(&f->default_value); - free(f->added_in); - free(f->deprecated_in); - free(f->renamed_from); - free(f->validation); + evolving_field_clear(f); free(f); } @@ -175,6 +194,11 @@ version_schema_t *version_schema_new(const char *name, const char *version) { s->fields_count = 0; s->fields_capacity = 0; + if ((name && !s->name) || (version && !s->version)) { + version_schema_free(s); + return NULL; + } + return s; } @@ -186,7 +210,7 @@ void version_schema_free(version_schema_t *s) { free(s->description); for (size_t i = 0; i < s->fields_count; i++) { - evolving_field_free(&s->fields[i]); + evolving_field_clear(&s->fields[i]); } free(s->fields); free(s); @@ -198,7 +222,10 @@ void version_schema_add_field(version_schema_t *s, evolving_field_t *field) { if (s->fields_count >= s->fields_capacity) { size_t new_cap = s->fields_capacity == 0 ? 8 : s->fields_capacity * 2; evolving_field_t *new_fields = realloc(s->fields, new_cap * sizeof(evolving_field_t)); - if (!new_fields) return; + if (!new_fields) { + evolving_field_free(field); + return; + } s->fields = new_fields; s->fields_capacity = new_cap; } @@ -269,6 +296,11 @@ versioned_schema_t *versioned_schema_new(const char *name) { s->latest_version = strdup_safe("1.0"); s->mode = EVOLUTION_MODE_TOLERANT; + if ((name && !s->name) || !s->latest_version) { + versioned_schema_free(s); + return NULL; + } + return s; } @@ -283,12 +315,7 @@ void versioned_schema_free(versioned_schema_t *s) { free(s->versions[i].version); free(s->versions[i].description); for (size_t j = 0; j < s->versions[i].fields_count; j++) { - free(s->versions[i].fields[j].name); - field_value_free(&s->versions[i].fields[j].default_value); - free(s->versions[i].fields[j].added_in); - free(s->versions[i].fields[j].deprecated_in); - free(s->versions[i].fields[j].renamed_from); - free(s->versions[i].fields[j].validation); + evolving_field_clear(&s->versions[i].fields[j]); } free(s->versions[i].fields); } @@ -330,7 +357,7 @@ void versioned_schema_add_version(versioned_schema_t *s, s->versions_capacity = new_cap; } - version_schema_t *vs = &s->versions[s->versions_count++]; + version_schema_t *vs = &s->versions[s->versions_count]; memset(vs, 0, sizeof(version_schema_t)); vs->name = strdup_safe(s->name); vs->version = strdup_safe(version); @@ -339,6 +366,12 @@ void versioned_schema_add_version(versioned_schema_t *s, vs->fields_count = 0; vs->fields_capacity = 0; + if ((s->name && !vs->name) || !vs->version) { + free(vs->name); + free(vs->version); + return; + } + for (size_t i = 0; i < field_count; i++) { evolving_field_config_t config = fields[i]; if (!config.added_in) { @@ -351,6 +384,7 @@ void versioned_schema_add_version(versioned_schema_t *s, } } + s->versions_count++; update_latest_version(s); } @@ -384,6 +418,11 @@ static evolution_parse_result_t migrate_step(const versioned_schema_t *s, result.data = calloc(max_count, sizeof(field_value_t)); result.keys = calloc(max_count, sizeof(char *)); result.data_count = 0; + if ((max_count > 0 && !result.data) || (max_count > 0 && !result.keys)) { + evolution_parse_result_free(&result); + result.error = strdup_safe("out of memory"); + return result; + } /* Copy existing data, handling renames */ for (size_t i = 0; i < count; i++) { @@ -393,9 +432,11 @@ static evolution_parse_result_t migrate_step(const versioned_schema_t *s, strcmp(to_schema->fields[j].renamed_from, keys[i]) == 0) { /* Field was renamed */ result.keys[result.data_count] = strdup_safe(to_schema->fields[j].name); + if (to_schema->fields[j].name && !result.keys[result.data_count]) goto oom; result.data[result.data_count] = data[i]; if (data[i].type == FIELD_VALUE_STR) { result.data[result.data_count].str_val = strdup_safe(data[i].str_val); + if (data[i].str_val && !result.data[result.data_count].str_val) goto oom; } result.data_count++; renamed = true; @@ -406,9 +447,11 @@ static evolution_parse_result_t migrate_step(const versioned_schema_t *s, /* Copy as-is if field exists in target schema or in tolerant mode */ if (s->mode == EVOLUTION_MODE_TOLERANT || version_schema_get_field(to_schema, keys[i])) { result.keys[result.data_count] = strdup_safe(keys[i]); + if (keys[i] && !result.keys[result.data_count]) goto oom; result.data[result.data_count] = data[i]; if (data[i].type == FIELD_VALUE_STR) { result.data[result.data_count].str_val = strdup_safe(data[i].str_val); + if (data[i].str_val && !result.data[result.data_count].str_val) goto oom; } result.data_count++; } @@ -427,10 +470,13 @@ static evolution_parse_result_t migrate_step(const versioned_schema_t *s, } if (!exists) { result.keys[result.data_count] = strdup_safe(field->name); + if (field->name && !result.keys[result.data_count]) goto oom; if (field->default_value.type != FIELD_VALUE_NULL) { result.data[result.data_count] = field->default_value; if (field->default_value.type == FIELD_VALUE_STR) { result.data[result.data_count].str_val = strdup_safe(field->default_value.str_val); + if (field->default_value.str_val && + !result.data[result.data_count].str_val) goto oom; } } else if (!field->required) { result.data[result.data_count] = field_value_null(); @@ -458,6 +504,11 @@ static evolution_parse_result_t migrate_step(const versioned_schema_t *s, } return result; + +oom: + evolution_parse_result_free(&result); + result.error = strdup_safe("out of memory"); + return result; } evolution_parse_result_t versioned_schema_parse(const versioned_schema_t *s, @@ -469,8 +520,15 @@ evolution_parse_result_t versioned_schema_parse(const versioned_schema_t *s, const version_schema_t *schema = versioned_schema_get_version(s, from_version); if (!schema) { - result.error = malloc(64); - snprintf(result.error, 64, "unknown version: %s", from_version); + result.error = strdup_safe("unknown version"); + if (from_version) { + char *error = malloc(strlen(from_version) + 18); + if (error) { + snprintf(error, strlen(from_version) + 18, "unknown version: %s", from_version); + free(result.error); + result.error = error; + } + } return result; } @@ -487,13 +545,28 @@ evolution_parse_result_t versioned_schema_parse(const versioned_schema_t *s, if (strcmp(from_version, s->latest_version) == 0) { result.data = calloc(count, sizeof(field_value_t)); result.keys = calloc(count, sizeof(char *)); + if ((count > 0 && !result.data) || (count > 0 && !result.keys)) { + evolution_parse_result_free(&result); + result.error = strdup_safe("out of memory"); + return result; + } result.data_count = count; for (size_t i = 0; i < count; i++) { result.data[i] = data[i]; if (data[i].type == FIELD_VALUE_STR) { result.data[i].str_val = strdup_safe(data[i].str_val); + if (data[i].str_val && !result.data[i].str_val) { + evolution_parse_result_free(&result); + result.error = strdup_safe("out of memory"); + return result; + } } result.keys[i] = strdup_safe(keys[i]); + if (keys[i] && !result.keys[i]) { + evolution_parse_result_free(&result); + result.error = strdup_safe("out of memory"); + return result; + } } return result; } @@ -512,8 +585,15 @@ evolution_emit_result_t versioned_schema_emit(const versioned_schema_t *s, const char *target_version = version ? version : s->latest_version; const version_schema_t *schema = versioned_schema_get_version(s, target_version); if (!schema) { - result.error = malloc(64); - snprintf(result.error, 64, "unknown version: %s", target_version); + result.error = strdup_safe("unknown version"); + if (target_version) { + char *error = malloc(strlen(target_version) + 18); + if (error) { + snprintf(error, strlen(target_version) + 18, "unknown version: %s", target_version); + free(result.error); + result.error = error; + } + } return result; } @@ -540,6 +620,12 @@ changelog_entry_t *versioned_schema_get_changelog(const versioned_schema_t *s, s const version_schema_t *vs = &s->versions[i]; entries[i].version = strdup_safe(vs->version); entries[i].description = strdup_safe(vs->description); + if ((vs->version && !entries[i].version) || + (vs->description && !entries[i].description)) { + changelog_free(entries, i + 1); + *count = 0; + return NULL; + } /* Count fields for each category */ size_t added = 0, deprecated = 0, renamed = 0; @@ -559,18 +645,41 @@ changelog_entry_t *versioned_schema_get_changelog(const versioned_schema_t *s, s entries[i].deprecated_fields = deprecated ? calloc(deprecated + 1, sizeof(char *)) : NULL; entries[i].renamed_from = renamed ? calloc(renamed + 1, sizeof(char *)) : NULL; entries[i].renamed_to = renamed ? calloc(renamed + 1, sizeof(char *)) : NULL; + if ((added && !entries[i].added_fields) || + (deprecated && !entries[i].deprecated_fields) || + (renamed && (!entries[i].renamed_from || !entries[i].renamed_to))) { + changelog_free(entries, i + 1); + *count = 0; + return NULL; + } size_t ai = 0, di = 0, ri = 0; for (size_t j = 0; j < vs->fields_count; j++) { if (vs->fields[j].added_in && strcmp(vs->fields[j].added_in, vs->version) == 0) { entries[i].added_fields[ai++] = strdup_safe(vs->fields[j].name); + if (vs->fields[j].name && !entries[i].added_fields[ai - 1]) { + changelog_free(entries, i + 1); + *count = 0; + return NULL; + } } if (vs->fields[j].deprecated_in && strcmp(vs->fields[j].deprecated_in, vs->version) == 0) { entries[i].deprecated_fields[di++] = strdup_safe(vs->fields[j].name); + if (vs->fields[j].name && !entries[i].deprecated_fields[di - 1]) { + changelog_free(entries, i + 1); + *count = 0; + return NULL; + } } if (vs->fields[j].renamed_from) { entries[i].renamed_from[ri] = strdup_safe(vs->fields[j].renamed_from); entries[i].renamed_to[ri++] = strdup_safe(vs->fields[j].name); + if ((vs->fields[j].renamed_from && !entries[i].renamed_from[ri - 1]) || + (vs->fields[j].name && !entries[i].renamed_to[ri - 1])) { + changelog_free(entries, i + 1); + *count = 0; + return NULL; + } } } diff --git a/c/glyph-codec/src/stream_validator.c b/c/glyph-codec/src/stream_validator.c index 9556c79..f865016 100644 --- a/c/glyph-codec/src/stream_validator.c +++ b/c/glyph-codec/src/stream_validator.c @@ -12,6 +12,12 @@ #include #include +#define DEFAULT_MAX_BUFFER (1024u * 1024u) +#define DEFAULT_MAX_FIELDS 1000u +#define DEFAULT_MAX_ERRORS 100u +#define DEFAULT_MAX_TIMELINE 1024u +#define DEFAULT_MAX_DEPTH 128 + /* ============================================================ * Internal Helpers * ============================================================ */ @@ -30,6 +36,46 @@ static uint64_t current_time_ms(void) { return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } +static void free_enum_values(char **values, size_t count) { + if (!values) return; + for (size_t i = 0; i < count; i++) { + free(values[i]); + } + free(values); +} + +static bool grow_char_buffer(char **buf, size_t *cap, size_t needed, size_t max_cap) { + if (!buf || !cap) return false; + if (needed <= *cap) return true; + if (needed > max_cap) return false; + + size_t new_cap = *cap ? *cap : 1; + while (new_cap < needed) { + if (new_cap > max_cap / 2) { + new_cap = max_cap; + break; + } + new_cap *= 2; + } + if (new_cap < needed || new_cap > max_cap) return false; + + char *new_buf = realloc(*buf, new_cap); + if (!new_buf) return false; + + *buf = new_buf; + *cap = new_cap; + return true; +} + +static bool append_buffer_char(char **buf, size_t *len, size_t *cap, size_t max_cap, char c) { + if (!grow_char_buffer(buf, cap, *len + 2, max_cap)) { + return false; + } + (*buf)[(*len)++] = c; + (*buf)[*len] = '\0'; + return true; +} + /* ============================================================ * Argument Schema Functions * ============================================================ */ @@ -49,6 +95,11 @@ arg_schema_t *arg_schema_new(const char *name, const char *type) { a->enum_values = NULL; a->enum_count = 0; + if ((name && !a->name) || (type && !a->type)) { + arg_schema_free(a); + return NULL; + } + return a; } @@ -80,20 +131,25 @@ void arg_schema_set_pattern(arg_schema_t *a, const char *pattern) { void arg_schema_set_enum(arg_schema_t *a, const char **values, size_t count) { if (!a) return; - /* Free existing */ - if (a->enum_values) { - for (size_t i = 0; i < a->enum_count; i++) { - free(a->enum_values[i]); + char **new_values = NULL; + if (count > 0) { + new_values = calloc(count, sizeof(char *)); + if (!new_values) { + return; } - free(a->enum_values); } - a->enum_values = calloc(count, sizeof(char *)); - a->enum_count = count; - for (size_t i = 0; i < count; i++) { - a->enum_values[i] = strdup_safe(values[i]); + new_values[i] = strdup_safe(values[i]); + if (values[i] && !new_values[i]) { + free_enum_values(new_values, count); + return; + } } + + free_enum_values(a->enum_values, a->enum_count); + a->enum_values = new_values; + a->enum_count = count; } void arg_schema_free(arg_schema_t *a) { @@ -103,12 +159,7 @@ void arg_schema_free(arg_schema_t *a) { free(a->type); free(a->pattern); - if (a->enum_values) { - for (size_t i = 0; i < a->enum_count; i++) { - free(a->enum_values[i]); - } - free(a->enum_values); - } + free_enum_values(a->enum_values, a->enum_count); free(a); } @@ -127,6 +178,11 @@ tool_schema_t *tool_schema_new(const char *name, const char *description) { t->args_count = 0; t->args_capacity = 0; + if ((name && !t->name) || (description && !t->description)) { + tool_schema_free(t); + return NULL; + } + return t; } @@ -136,7 +192,10 @@ void tool_schema_add_arg(tool_schema_t *t, arg_schema_t *arg) { if (t->args_count >= t->args_capacity) { size_t new_cap = t->args_capacity == 0 ? 8 : t->args_capacity * 2; arg_schema_t *new_args = realloc(t->args, new_cap * sizeof(arg_schema_t)); - if (!new_args) return; + if (!new_args) { + arg_schema_free(arg); + return; + } t->args = new_args; t->args_capacity = new_cap; } @@ -155,12 +214,7 @@ void tool_schema_free(tool_schema_t *t) { free(t->args[i].name); free(t->args[i].type); free(t->args[i].pattern); - if (t->args[i].enum_values) { - for (size_t j = 0; j < t->args[i].enum_count; j++) { - free(t->args[i].enum_values[j]); - } - free(t->args[i].enum_values); - } + free_enum_values(t->args[i].enum_values, t->args[i].enum_count); } free(t->args); @@ -193,12 +247,7 @@ void tool_registry_free(tool_registry_t *r) { free(r->tools[i].args[j].name); free(r->tools[i].args[j].type); free(r->tools[i].args[j].pattern); - if (r->tools[i].args[j].enum_values) { - for (size_t k = 0; k < r->tools[i].args[j].enum_count; k++) { - free(r->tools[i].args[j].enum_values[k]); - } - free(r->tools[i].args[j].enum_values); - } + free_enum_values(r->tools[i].args[j].enum_values, r->tools[i].args[j].enum_count); } free(r->tools[i].args); } @@ -213,7 +262,10 @@ void tool_registry_register(tool_registry_t *r, tool_schema_t *tool) { if (r->tools_count >= r->tools_capacity) { size_t new_cap = r->tools_capacity == 0 ? 8 : r->tools_capacity * 2; tool_schema_t *new_tools = realloc(r->tools, new_cap * sizeof(tool_schema_t)); - if (!new_tools) return; + if (!new_tools) { + tool_schema_free(tool); + return; + } r->tools = new_tools; r->tools_capacity = new_cap; } @@ -244,11 +296,20 @@ tool_registry_t *tool_registry_default(void) { /* Register 'search' tool */ tool_schema_t *search = tool_schema_new("search", "Search for information"); arg_schema_t *query = arg_schema_new("query", "string"); + if (!search || !query) { + tool_schema_free(search); + arg_schema_free(query); + goto fail; + } arg_schema_set_required(query, true); arg_schema_set_length(query, 1, SIZE_MAX); tool_schema_add_arg(search, query); arg_schema_t *max_results = arg_schema_new("max_results", "int"); + if (!max_results) { + tool_schema_free(search); + goto fail; + } arg_schema_set_range(max_results, 1, 100); tool_schema_add_arg(search, max_results); tool_registry_register(r, search); @@ -256,10 +317,19 @@ tool_registry_t *tool_registry_default(void) { /* Register 'calculate' tool */ tool_schema_t *calculate = tool_schema_new("calculate", "Evaluate a mathematical expression"); arg_schema_t *expression = arg_schema_new("expression", "string"); + if (!calculate || !expression) { + tool_schema_free(calculate); + arg_schema_free(expression); + goto fail; + } arg_schema_set_required(expression, true); tool_schema_add_arg(calculate, expression); arg_schema_t *precision = arg_schema_new("precision", "int"); + if (!precision) { + tool_schema_free(calculate); + goto fail; + } arg_schema_set_range(precision, 0, 15); tool_schema_add_arg(calculate, precision); tool_registry_register(r, calculate); @@ -267,6 +337,11 @@ tool_registry_t *tool_registry_default(void) { /* Register 'browse' tool */ tool_schema_t *browse = tool_schema_new("browse", "Fetch a web page"); arg_schema_t *url = arg_schema_new("url", "string"); + if (!browse || !url) { + tool_schema_free(browse); + arg_schema_free(url); + goto fail; + } arg_schema_set_required(url, true); arg_schema_set_pattern(url, "^https?://"); tool_schema_add_arg(browse, url); @@ -275,11 +350,20 @@ tool_registry_t *tool_registry_default(void) { /* Register 'execute' tool */ tool_schema_t *execute = tool_schema_new("execute", "Execute a shell command"); arg_schema_t *command = arg_schema_new("command", "string"); + if (!execute || !command) { + tool_schema_free(execute); + arg_schema_free(command); + goto fail; + } arg_schema_set_required(command, true); tool_schema_add_arg(execute, command); tool_registry_register(r, execute); return r; + +fail: + tool_registry_free(r); + return NULL; } /* ============================================================ @@ -294,6 +378,11 @@ streaming_validator_t *streaming_validator_new(tool_registry_t *registry) { /* Initialize parser state */ v->buffer = malloc(256); + if (!v->buffer) { + free(v); + return NULL; + } + v->buffer[0] = '\0'; v->buffer_len = 0; v->buffer_cap = 256; v->state = VALIDATOR_WAITING; @@ -302,6 +391,12 @@ streaming_validator_t *streaming_validator_new(tool_registry_t *registry) { v->escape_next = false; v->current_key = NULL; v->current_val = malloc(256); + if (!v->current_val) { + free(v->buffer); + free(v); + return NULL; + } + v->current_val[0] = '\0'; v->current_val_len = 0; v->current_val_cap = 256; v->has_key = false; @@ -370,6 +465,7 @@ void streaming_validator_reset(streaming_validator_t *v) { /* Reset buffer */ v->buffer_len = 0; + if (v->buffer) v->buffer[0] = '\0'; v->state = VALIDATOR_WAITING; v->depth = 0; v->in_string = false; @@ -377,6 +473,7 @@ void streaming_validator_reset(streaming_validator_t *v) { free(v->current_key); v->current_key = NULL; v->current_val_len = 0; + if (v->current_val) v->current_val[0] = '\0'; v->has_key = false; /* Reset parsed data */ @@ -423,8 +520,14 @@ void streaming_validator_start(streaming_validator_t *v) { /* Add error to validator */ static void add_error(streaming_validator_t *v, validation_error_code_t code, const char *message, const char *field) { + if (v->errors_count >= DEFAULT_MAX_ERRORS) { + return; + } if (v->errors_count >= v->errors_capacity) { size_t new_cap = v->errors_capacity == 0 ? 8 : v->errors_capacity * 2; + if (new_cap > DEFAULT_MAX_ERRORS) { + new_cap = DEFAULT_MAX_ERRORS; + } validation_error_t *new_errors = realloc(v->errors, new_cap * sizeof(validation_error_t)); if (!new_errors) return; v->errors = new_errors; @@ -441,8 +544,14 @@ static void add_error(streaming_validator_t *v, validation_error_code_t code, static void add_timeline_event(streaming_validator_t *v, const char *event, size_t token, size_t char_pos, uint64_t elapsed, const char *detail) { + if (v->timeline_count >= DEFAULT_MAX_TIMELINE) { + return; + } if (v->timeline_count >= v->timeline_capacity) { size_t new_cap = v->timeline_capacity == 0 ? 8 : v->timeline_capacity * 2; + if (new_cap > DEFAULT_MAX_TIMELINE) { + new_cap = DEFAULT_MAX_TIMELINE; + } timeline_event_t *new_timeline = realloc(v->timeline, new_cap * sizeof(timeline_event_t)); if (!new_timeline) return; v->timeline = new_timeline; @@ -517,15 +626,41 @@ static validator_field_value_t parse_value(const char *s, size_t len) { /* Add parsed field */ static void add_field(streaming_validator_t *v, const char *key, validator_field_value_t value) { + if (v->fields_count >= DEFAULT_MAX_FIELDS) { + if (value.type == VFIELD_STR) { + free(value.str_val); + } + add_error(v, VERR_CONSTRAINT_LEN, "too many fields", key); + v->state = VALIDATOR_ERROR; + return; + } if (v->fields_count >= v->fields_capacity) { size_t new_cap = v->fields_capacity == 0 ? 16 : v->fields_capacity * 2; + if (new_cap > DEFAULT_MAX_FIELDS) { + new_cap = DEFAULT_MAX_FIELDS; + } parsed_field_t *new_fields = realloc(v->fields, new_cap * sizeof(parsed_field_t)); - if (!new_fields) return; + if (!new_fields) { + if (value.type == VFIELD_STR) { + free(value.str_val); + } + add_error(v, VERR_CONSTRAINT_LEN, "out of memory", key); + v->state = VALIDATOR_ERROR; + return; + } v->fields = new_fields; v->fields_capacity = new_cap; } v->fields[v->fields_count].key = strdup_safe(key); + if (key && !v->fields[v->fields_count].key) { + if (value.type == VFIELD_STR) { + free(value.str_val); + } + add_error(v, VERR_CONSTRAINT_LEN, "out of memory", key); + v->state = VALIDATOR_ERROR; + return; + } v->fields[v->fields_count].value = value; v->fields_count++; } @@ -608,10 +743,15 @@ static void finish_field(streaming_validator_t *v) { /* Check for tool/action field */ if (strcmp(v->current_key, "action") == 0 || strcmp(v->current_key, "tool") == 0) { if (value.type == VFIELD_STR && value.str_val) { + free(v->tool_name); v->tool_name = strdup_safe(value.str_val); + if (!v->tool_name) { + add_error(v, VERR_CONSTRAINT_LEN, "out of memory", v->current_key); + v->state = VALIDATOR_ERROR; + } /* Validate against allow list */ - if (!tool_registry_is_allowed(v->registry, value.str_val)) { + if (v->tool_name && !tool_registry_is_allowed(v->registry, value.str_val)) { char buf[128]; snprintf(buf, sizeof(buf), "Unknown tool: %s", value.str_val); add_error(v, VERR_UNKNOWN_TOOL, buf, v->current_key); @@ -663,31 +803,33 @@ static void validate_complete(streaming_validator_t *v) { /* Process a single character */ static void process_char(streaming_validator_t *v, char c) { - /* Grow buffer if needed */ - if (v->buffer_len >= v->buffer_cap - 1) { - size_t new_cap = v->buffer_cap * 2; - char *new_buf = realloc(v->buffer, new_cap); - if (new_buf) { - v->buffer = new_buf; - v->buffer_cap = new_cap; - } + if (v->state == VALIDATOR_ERROR || v->state == VALIDATOR_COMPLETE) { + return; + } + + if (!append_buffer_char(&v->buffer, &v->buffer_len, &v->buffer_cap, DEFAULT_MAX_BUFFER, c)) { + add_error(v, VERR_CONSTRAINT_LEN, "validator buffer limit exceeded", NULL); + v->state = VALIDATOR_ERROR; + return; } - v->buffer[v->buffer_len++] = c; - v->buffer[v->buffer_len] = '\0'; /* Handle escape sequences */ if (v->escape_next) { v->escape_next = false; - if (v->current_val_len < v->current_val_cap - 1) { - v->current_val[v->current_val_len++] = c; + if (!append_buffer_char(&v->current_val, &v->current_val_len, &v->current_val_cap, + DEFAULT_MAX_BUFFER, c)) { + add_error(v, VERR_CONSTRAINT_LEN, "value buffer limit exceeded", v->current_key); + v->state = VALIDATOR_ERROR; } return; } if (c == '\\' && v->in_string) { v->escape_next = true; - if (v->current_val_len < v->current_val_cap - 1) { - v->current_val[v->current_val_len++] = c; + if (!append_buffer_char(&v->current_val, &v->current_val_len, &v->current_val_cap, + DEFAULT_MAX_BUFFER, c)) { + add_error(v, VERR_CONSTRAINT_LEN, "value buffer limit exceeded", v->current_key); + v->state = VALIDATOR_ERROR; } return; } @@ -705,8 +847,10 @@ static void process_char(streaming_validator_t *v, char c) { /* Inside string - accumulate */ if (v->in_string) { - if (v->current_val_len < v->current_val_cap - 1) { - v->current_val[v->current_val_len++] = c; + if (!append_buffer_char(&v->current_val, &v->current_val_len, &v->current_val_cap, + DEFAULT_MAX_BUFFER, c)) { + add_error(v, VERR_CONSTRAINT_LEN, "value buffer limit exceeded", v->current_key); + v->state = VALIDATOR_ERROR; } return; } @@ -717,10 +861,20 @@ static void process_char(streaming_validator_t *v, char c) { if (v->state == VALIDATOR_WAITING) { v->state = VALIDATOR_IN_OBJECT; } + if (v->depth >= DEFAULT_MAX_DEPTH) { + add_error(v, VERR_CONSTRAINT_LEN, "nesting depth exceeded", NULL); + v->state = VALIDATOR_ERROR; + return; + } v->depth++; break; case '}': + if (v->depth <= 0) { + add_error(v, VERR_CONSTRAINT_LEN, "unbalanced closing brace", NULL); + v->state = VALIDATOR_ERROR; + return; + } v->depth--; if (v->depth == 0) { finish_field(v); @@ -730,16 +884,30 @@ static void process_char(streaming_validator_t *v, char c) { break; case '[': + if (v->depth >= DEFAULT_MAX_DEPTH) { + add_error(v, VERR_CONSTRAINT_LEN, "nesting depth exceeded", v->current_key); + v->state = VALIDATOR_ERROR; + return; + } v->depth++; - if (v->current_val_len < v->current_val_cap - 1) { - v->current_val[v->current_val_len++] = c; + if (!append_buffer_char(&v->current_val, &v->current_val_len, &v->current_val_cap, + DEFAULT_MAX_BUFFER, c)) { + add_error(v, VERR_CONSTRAINT_LEN, "value buffer limit exceeded", v->current_key); + v->state = VALIDATOR_ERROR; } break; case ']': + if (v->depth <= 0) { + add_error(v, VERR_CONSTRAINT_LEN, "unbalanced closing bracket", v->current_key); + v->state = VALIDATOR_ERROR; + return; + } v->depth--; - if (v->current_val_len < v->current_val_cap - 1) { - v->current_val[v->current_val_len++] = c; + if (!append_buffer_char(&v->current_val, &v->current_val_len, &v->current_val_cap, + DEFAULT_MAX_BUFFER, c)) { + add_error(v, VERR_CONSTRAINT_LEN, "value buffer limit exceeded", v->current_key); + v->state = VALIDATOR_ERROR; } break; @@ -754,11 +922,19 @@ static void process_char(streaming_validator_t *v, char c) { char *start = v->current_val; while (*start && isspace(*start)) start++; v->current_key = strdup_safe(start); + if (!v->current_key) { + add_error(v, VERR_CONSTRAINT_LEN, "out of memory", NULL); + v->state = VALIDATOR_ERROR; + return; + } v->current_val_len = 0; + v->current_val[0] = '\0'; v->has_key = true; } else { - if (v->current_val_len < v->current_val_cap - 1) { - v->current_val[v->current_val_len++] = c; + if (!append_buffer_char(&v->current_val, &v->current_val_len, &v->current_val_cap, + DEFAULT_MAX_BUFFER, c)) { + add_error(v, VERR_CONSTRAINT_LEN, "value buffer limit exceeded", v->current_key); + v->state = VALIDATOR_ERROR; } } break; @@ -773,8 +949,10 @@ static void process_char(streaming_validator_t *v, char c) { break; default: - if (v->current_val_len < v->current_val_cap - 1) { - v->current_val[v->current_val_len++] = c; + if (!append_buffer_char(&v->current_val, &v->current_val_len, &v->current_val_cap, + DEFAULT_MAX_BUFFER, c)) { + add_error(v, VERR_CONSTRAINT_LEN, "value buffer limit exceeded", v->current_key); + v->state = VALIDATOR_ERROR; } } } @@ -836,6 +1014,7 @@ validation_result_t *streaming_validator_get_result(const streaming_validator_t r->complete = (v->state == VALIDATOR_COMPLETE); r->valid = (v->errors_count == 0); r->tool_name = strdup_safe(v->tool_name); + if (v->tool_name && !r->tool_name) goto fail; r->tool_allowed = v->tool_name ? tool_registry_is_allowed(v->registry, v->tool_name) : false; r->tool_allowed_set = (v->tool_name != NULL); @@ -843,10 +1022,15 @@ validation_result_t *streaming_validator_get_result(const streaming_validator_t r->errors_count = v->errors_count; if (v->errors_count > 0) { r->errors = calloc(v->errors_count, sizeof(validation_error_t)); + if (!r->errors) goto fail; for (size_t i = 0; i < v->errors_count; i++) { r->errors[i].code = v->errors[i].code; r->errors[i].message = strdup_safe(v->errors[i].message); r->errors[i].field = strdup_safe(v->errors[i].field); + if ((v->errors[i].message && !r->errors[i].message) || + (v->errors[i].field && !r->errors[i].field)) { + goto fail; + } } } @@ -854,11 +1038,14 @@ validation_result_t *streaming_validator_get_result(const streaming_validator_t r->fields_count = v->fields_count; if (v->fields_count > 0) { r->fields = calloc(v->fields_count, sizeof(parsed_field_t)); + if (!r->fields) goto fail; for (size_t i = 0; i < v->fields_count; i++) { r->fields[i].key = strdup_safe(v->fields[i].key); + if (v->fields[i].key && !r->fields[i].key) goto fail; r->fields[i].value = v->fields[i].value; if (v->fields[i].value.type == VFIELD_STR) { r->fields[i].value.str_val = strdup_safe(v->fields[i].value.str_val); + if (v->fields[i].value.str_val && !r->fields[i].value.str_val) goto fail; } } } @@ -877,16 +1064,25 @@ validation_result_t *streaming_validator_get_result(const streaming_validator_t r->timeline_count = v->timeline_count; if (v->timeline_count > 0) { r->timeline = calloc(v->timeline_count, sizeof(timeline_event_t)); + if (!r->timeline) goto fail; for (size_t i = 0; i < v->timeline_count; i++) { r->timeline[i].event = strdup_safe(v->timeline[i].event); r->timeline[i].token = v->timeline[i].token; r->timeline[i].char_pos = v->timeline[i].char_pos; r->timeline[i].elapsed = v->timeline[i].elapsed; r->timeline[i].detail = strdup_safe(v->timeline[i].detail); + if ((v->timeline[i].event && !r->timeline[i].event) || + (v->timeline[i].detail && !r->timeline[i].detail)) { + goto fail; + } } } return r; + +fail: + validation_result_free(r); + return NULL; } bool streaming_validator_should_stop(const streaming_validator_t *v) { @@ -905,23 +1101,29 @@ void validation_result_free(validation_result_t *r) { free(r->tool_name); - for (size_t i = 0; i < r->errors_count; i++) { - free(r->errors[i].message); - free(r->errors[i].field); + if (r->errors) { + for (size_t i = 0; i < r->errors_count; i++) { + free(r->errors[i].message); + free(r->errors[i].field); + } } free(r->errors); - for (size_t i = 0; i < r->fields_count; i++) { - free(r->fields[i].key); - if (r->fields[i].value.type == VFIELD_STR) { - free(r->fields[i].value.str_val); + if (r->fields) { + for (size_t i = 0; i < r->fields_count; i++) { + free(r->fields[i].key); + if (r->fields[i].value.type == VFIELD_STR) { + free(r->fields[i].value.str_val); + } } } free(r->fields); - for (size_t i = 0; i < r->timeline_count; i++) { - free(r->timeline[i].event); - free(r->timeline[i].detail); + if (r->timeline) { + for (size_t i = 0; i < r->timeline_count; i++) { + free(r->timeline[i].event); + free(r->timeline[i].detail); + } } free(r->timeline); diff --git a/c/glyph-codec/test/test_glyph.c b/c/glyph-codec/test/test_glyph.c index 0599f2c..703556f 100644 --- a/c/glyph-codec/test/test_glyph.c +++ b/c/glyph-codec/test/test_glyph.c @@ -3,10 +3,14 @@ */ #include "glyph.h" +#include "decimal128.h" +#include "schema_evolution.h" +#include "stream_validator.h" #include #include #include #include +#include static int tests_passed = 0; static int tests_failed = 0; @@ -283,6 +287,20 @@ TEST(tabular_homogeneous) { glyph_value_free(v); } +TEST(tabular_columns_sorted) { + glyph_value_t *v = glyph_list_new(); + for (int i = 0; i < 3; i++) { + glyph_value_t *m = glyph_map_new(); + glyph_map_set(m, "b", glyph_int(i + 10)); + glyph_map_set(m, "a", glyph_int(i)); + glyph_list_append(v, m); + } + char *canon = glyph_canonicalize_loose(v); + ASSERT_TRUE(strstr(canon, "rows=3 cols=2 [a b]") != NULL); + glyph_free(canon); + glyph_value_free(v); +} + TEST(tabular_sparse_keys_no_tabular) { /* [{a:1}, {b:2}, {c:3}] - less than 50% common keys */ glyph_value_t *v = glyph_list_new(); @@ -382,6 +400,100 @@ TEST(json_roundtrip) { glyph_value_free(v2); } +TEST(json_rejects_trailing_data) { + glyph_value_t *v = glyph_from_json("{\"name\":\"test\"} trailing"); + ASSERT_TRUE(v == NULL); +} + +TEST(json_rejects_excessive_nesting) { + const size_t depth = 129; + char *json = malloc(depth * 2 + 2); + ASSERT_TRUE(json != NULL); + for (size_t i = 0; i < depth; i++) { + json[i] = '['; + } + json[depth] = '0'; + for (size_t i = 0; i < depth; i++) { + json[depth + 1 + i] = ']'; + } + json[depth * 2 + 1] = '\0'; + + glyph_value_t *v = glyph_from_json(json); + ASSERT_TRUE(v == NULL); + free(json); +} + +/* ============================================================ + * Decimal / Schema / Validator Regression Tests + * ============================================================ */ + +TEST(decimal_int64_min_roundtrip) { + decimal128_t d = decimal128_from_int(INT64_MIN); + ASSERT_TRUE(decimal128_to_int(&d) == INT64_MIN); + + char *s = decimal128_to_string(&d); + ASSERT_TRUE(s != NULL); + ASSERT_STR_EQ("-9223372036854775808", s); + free(s); +} + +TEST(schema_version_schema_free_embedded_fields) { + version_schema_t *schema = version_schema_new("test", "1.0"); + ASSERT_TRUE(schema != NULL); + + evolving_field_config_t config = { + .type = FIELD_TYPE_STR, + .required = true, + .default_value = field_value_str("fallback"), + .added_in = "1.0", + .deprecated_in = NULL, + .renamed_from = NULL, + .validation = NULL, + }; + evolving_field_t *field = evolving_field_new("name", &config); + ASSERT_TRUE(field != NULL); + + version_schema_add_field(schema, field); + version_schema_free(schema); + field_value_free(&config.default_value); + + ASSERT_TRUE(true); +} + +TEST(stream_validator_rejects_excessive_depth) { + tool_registry_t *registry = tool_registry_default(); + ASSERT_TRUE(registry != NULL); + + streaming_validator_t *validator = streaming_validator_new(registry); + ASSERT_TRUE(validator != NULL); + + const size_t depth = 129; + char *payload = malloc(64 + depth * 2 + 1); + ASSERT_TRUE(payload != NULL); + + size_t pos = 0; + pos += (size_t)snprintf(payload + pos, 64, "{action=\"search\" query="); + for (size_t i = 0; i < depth; i++) { + payload[pos++] = '['; + } + payload[pos++] = '_'; + for (size_t i = 0; i < depth; i++) { + payload[pos++] = ']'; + } + payload[pos++] = '}'; + payload[pos] = '\0'; + + validation_result_t *result = streaming_validator_push_token(validator, payload); + ASSERT_TRUE(result != NULL); + ASSERT_TRUE(result->valid == false); + ASSERT_TRUE(result->errors_count > 0); + + validation_result_free(result); + free(payload); + streaming_validator_free(validator); + tool_registry_free(registry); +} + /* ============================================================ * Main * ============================================================ */ @@ -427,6 +539,7 @@ int main(void) { printf("\nTabular Mode Tests:\n"); RUN_TEST(tabular_homogeneous); + RUN_TEST(tabular_columns_sorted); RUN_TEST(tabular_sparse_keys_no_tabular); RUN_TEST(tabular_empty_objects_no_tabular); @@ -438,6 +551,13 @@ int main(void) { RUN_TEST(json_parse_array); RUN_TEST(json_parse_object); RUN_TEST(json_roundtrip); + RUN_TEST(json_rejects_trailing_data); + RUN_TEST(json_rejects_excessive_nesting); + + printf("\nRegression Tests:\n"); + RUN_TEST(decimal_int64_min_roundtrip); + RUN_TEST(schema_version_schema_free_embedded_fields); + RUN_TEST(stream_validator_rejects_excessive_depth); printf("\n===================\n"); printf("Results: %d passed, %d failed\n", tests_passed, tests_failed); diff --git a/go/glyph/canon.go b/go/glyph/canon.go index b625b8d..8d4e726 100644 --- a/go/glyph/canon.go +++ b/go/glyph/canon.go @@ -261,6 +261,8 @@ func maskToBinary(mask []bool) string { return b.String() } +const maxBitmapBits = 1 << 20 + // binaryToMask parses a "0bXXX" string back to a boolean mask. func binaryToMask(s string) ([]bool, error) { if !strings.HasPrefix(s, "0b") { @@ -271,6 +273,9 @@ func binaryToMask(s string) ([]bool, error) { if len(bits) == 0 { return nil, &ParseError{Message: "empty bitmap"} } + if len(bits) > maxBitmapBits { + return nil, &ParseError{Message: "bitmap too large"} + } // Bits are written MSB first, so reverse to get LSB-first mask mask := make([]bool, len(bits)) diff --git a/go/glyph/incremental.go b/go/glyph/incremental.go index e9a75b7..3a0fa3c 100644 --- a/go/glyph/incremental.go +++ b/go/glyph/incremental.go @@ -13,6 +13,9 @@ package glyph import ( "errors" + "fmt" + "math" + "strconv" "sync" ) @@ -20,17 +23,17 @@ import ( type ParseEventType uint8 const ( - EventNone ParseEventType = iota - EventStartObject // Beginning of { or Type{ - EventEndObject // End of } - EventStartList // Beginning of [ - EventEndList // End of ] - EventKey // Field key parsed - EventValue // Scalar value parsed - EventStartSum // Beginning of Tag( or Tag{ - EventEndSum // End of ) or } - EventError // Parse error - EventNeedMore // Need more input + EventNone ParseEventType = iota + EventStartObject // Beginning of { or Type{ + EventEndObject // End of } + EventStartList // Beginning of [ + EventEndList // End of ] + EventKey // Field key parsed + EventValue // Scalar value parsed + EventStartSum // Beginning of Tag( or Tag{ + EventEndSum // End of ) or } + EventError // Parse error + EventNeedMore // Need more input ) // String returns the event type name. @@ -100,6 +103,8 @@ type IncrementalParser struct { maxDepth int maxKeyLen int maxValueLen int + + handlingErrorEvent bool } type parseState int @@ -234,6 +239,7 @@ func (p *IncrementalParser) Reset() { p.state = stateStart p.stack = p.stack[:0] p.err = nil + p.handlingErrorEvent = false } // Path returns the current parse path. @@ -391,7 +397,9 @@ func (p *IncrementalParser) parseValue() int { // List if ch == '[' { p.pos++ - p.pushStack(stateInList, "", "") + if !p.pushStack(stateInList, "", "") { + return 0 + } p.emitEvent(ParseEvent{Type: EventStartList, Path: p.copyPath()}) p.state = stateInList return 1 @@ -400,7 +408,9 @@ func (p *IncrementalParser) parseValue() int { // Object or struct if ch == '{' { p.pos++ - p.pushStack(stateInObject, "", "") + if !p.pushStack(stateInObject, "", "") { + return 0 + } p.emitEvent(ParseEvent{Type: EventStartObject, Path: p.copyPath()}) p.state = stateExpectKey return 1 @@ -434,6 +444,10 @@ func (p *IncrementalParser) parseKey() int { if consumed == 0 { return 0 // Need more data } + if len(key) > p.maxKeyLen { + p.setError(fmt.Errorf("key too long: %d > %d", len(key), p.maxKeyLen)) + return 0 + } p.path = append(p.path, PathElement{Key: key}) p.emitEvent(ParseEvent{Type: EventKey, Key: key, Path: p.copyPath()}) p.state = stateExpectColon @@ -447,6 +461,10 @@ func (p *IncrementalParser) parseKey() int { p.pos++ } key := string(p.buffer[start:p.pos]) + if len(key) > p.maxKeyLen { + p.setError(fmt.Errorf("key too long: %d > %d", len(key), p.maxKeyLen)) + return 0 + } p.path = append(p.path, PathElement{Key: key}) p.emitEvent(ParseEvent{Type: EventKey, Key: key, Path: p.copyPath()}) p.state = stateExpectColon @@ -497,13 +515,25 @@ func (p *IncrementalParser) parseNumber() int { } numStr := string(p.buffer[start:p.pos]) + if len(numStr) > p.maxValueLen { + p.setError(fmt.Errorf("value too long: %d > %d", len(numStr), p.maxValueLen)) + return 0 + } var value *GValue if isFloat { - f := parseFloat(numStr) + f, err := strconv.ParseFloat(numStr, 64) + if err != nil || math.IsNaN(f) || math.IsInf(f, 0) { + p.setError(fmt.Errorf("invalid number: %s", numStr)) + return 0 + } value = Float(f) } else { - i := parseInt(numStr) + i, err := strconv.ParseInt(numStr, 10, 64) + if err != nil { + p.setError(fmt.Errorf("invalid number: %s", numStr)) + return 0 + } value = Int(i) } @@ -563,6 +593,10 @@ func (p *IncrementalParser) scanString() (string, int) { sb = append(sb, ch) p.pos++ } + if len(sb) > p.maxValueLen { + p.setError(fmt.Errorf("value too long: %d > %d", len(sb), p.maxValueLen)) + return "", 0 + } } // Unterminated string - need more data @@ -578,6 +612,10 @@ func (p *IncrementalParser) parseRef() int { for p.pos < len(p.buffer) && isRefChar(p.buffer[p.pos]) { refStr = append(refStr, p.buffer[p.pos]) p.pos++ + if len(refStr) > p.maxValueLen { + p.setError(fmt.Errorf("value too long: %d > %d", len(refStr), p.maxValueLen)) + return 0 + } } // Parse prefix:value @@ -602,6 +640,10 @@ func (p *IncrementalParser) parseIdentifier() int { } ident := string(p.buffer[start:p.pos]) + if len(ident) > p.maxValueLen { + p.setError(fmt.Errorf("value too long: %d > %d", len(ident), p.maxValueLen)) + return 0 + } // Check what follows if p.pos < len(p.buffer) { @@ -610,7 +652,9 @@ func (p *IncrementalParser) parseIdentifier() int { // Struct: Type{...} if ch == '{' { p.pos++ - p.pushStack(stateInObject, ident, "") + if !p.pushStack(stateInObject, ident, "") { + return 0 + } p.emitEvent(ParseEvent{Type: EventStartObject, TypeName: ident, Path: p.copyPath()}) p.state = stateExpectKey return p.pos - start @@ -619,7 +663,9 @@ func (p *IncrementalParser) parseIdentifier() int { // Sum: Tag(...) or Tag{...} if ch == '(' { p.pos++ - p.pushStack(stateExpectValue, "", ident) + if !p.pushStack(stateExpectValue, "", ident) { + return 0 + } p.emitEvent(ParseEvent{Type: EventStartSum, Tag: ident, Path: p.copyPath()}) p.state = stateExpectValue return p.pos - start @@ -649,13 +695,18 @@ func (p *IncrementalParser) matchKeyword(keyword string) bool { return true } -func (p *IncrementalParser) pushStack(state parseState, typeName, tag string) { +func (p *IncrementalParser) pushStack(state parseState, typeName, tag string) bool { + if len(p.stack)+1 > p.maxDepth { + p.setError(fmt.Errorf("max depth exceeded: %d", p.maxDepth)) + return false + } p.stack = append(p.stack, parseStackFrame{ state: state, typeName: typeName, tag: tag, count: 0, }) + return true } func (p *IncrementalParser) popStack() { @@ -688,13 +739,35 @@ func (p *IncrementalParser) copyPath() []PathElement { func (p *IncrementalParser) emitEvent(event ParseEvent) { if p.handler != nil { + if event.Type == EventError { + if p.handlingErrorEvent { + return + } + p.handlingErrorEvent = true + defer func() { + p.handlingErrorEvent = false + }() + } if err := p.handler(event); err != nil { + if event.Type == EventError || p.handlingErrorEvent { + if p.err == nil { + p.err = err + } + p.state = stateError + return + } p.setError(err) } } } func (p *IncrementalParser) setError(err error) { + if err == nil { + return + } + if p.state == stateError && p.err != nil { + return + } p.err = err p.state = stateError p.emitEvent(ParseEvent{Type: EventError, Error: err}) @@ -703,31 +776,3 @@ func (p *IncrementalParser) setError(err error) { func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' } - -func parseFloat(s string) float64 { - var f float64 - for i := 0; i < len(s); i++ { - if s[i] == '.' || s[i] == 'e' || s[i] == 'E' || s[i] == '-' || s[i] == '+' { - continue - } - f = f*10 + float64(s[i]-'0') - } - return f // Simplified - real impl would handle decimals/exponents -} - -func parseInt(s string) int64 { - var n int64 - neg := false - i := 0 - if len(s) > 0 && s[0] == '-' { - neg = true - i = 1 - } - for ; i < len(s); i++ { - n = n*10 + int64(s[i]-'0') - } - if neg { - return -n - } - return n -} diff --git a/go/glyph/loose.go b/go/glyph/loose.go index ef9ff5c..dfab5c8 100644 --- a/go/glyph/loose.go +++ b/go/glyph/loose.go @@ -1288,24 +1288,21 @@ func unquoteString(s string) (string, error) { func tryParseNumber(s string) (*GValue, bool) { // Check for integer if isIntString(s) { - var n int64 - _, err := fmt.Sscanf(s, "%d", &n) + n, err := strconv.ParseInt(s, 10, 64) if err == nil { return Int(n), true } } // Check for float - var f float64 - _, err := fmt.Sscanf(s, "%f", &f) + f, err := strconv.ParseFloat(s, 64) if err == nil { // Only treat as float if it has decimal point or exponent if strings.Contains(s, ".") || strings.ContainsAny(s, "eE") { return Float(f), true } // Otherwise try as int first - var n int64 - _, err := fmt.Sscanf(s, "%d", &n) + n, err := strconv.ParseInt(s, 10, 64) if err == nil { return Int(n), true } diff --git a/go/glyph/parse.go b/go/glyph/parse.go index aecad11..bba5bed 100644 --- a/go/glyph/parse.go +++ b/go/glyph/parse.go @@ -70,13 +70,27 @@ func ParseWithOptions(input string, opts ParseOptions) (*ParseResult, error) { } value := p.parseValue() - - return &ParseResult{ + result := &ParseResult{ Value: value, Errors: p.errors, Warnings: p.warnings, Schema: p.schema, - }, nil + } + + if next := p.stream.Peek(); next.Type != TokenEOF { + msg := fmt.Sprintf("unexpected trailing token %s", next.Type) + if p.tolerant { + p.addWarning(next.Pos, "%s", msg) + } else { + p.addError(next.Pos, "%s", msg) + } + result.Value = nil + result.Errors = p.errors + result.Warnings = p.warnings + return result, &ParseError{Message: msg, Pos: next.Pos} + } + + return result, nil } // parseValue parses any value. @@ -144,6 +158,7 @@ func (p *Parser) parseValue() *GValue { return Null() } p.addError(tok.Pos, "unexpected token %s", tok.Type) + p.advanceAfterError() return nil } } @@ -277,6 +292,7 @@ func (p *Parser) parseMapEntry() *MapEntry { return nil } p.addError(keyTok.Pos, "expected key, got %s", keyTok.Type) + p.advanceAfterError() return nil } @@ -287,6 +303,7 @@ func (p *Parser) parseMapEntry() *MapEntry { p.addWarning(p.stream.Peek().Pos, "expected = or :, continuing") } else { p.addError(p.stream.Peek().Pos, "expected = or :") + p.advanceAfterError() return nil } } @@ -379,6 +396,7 @@ func (p *Parser) parseStructField(typeName string) *MapEntry { return nil } p.addError(keyTok.Pos, "expected field name, got %s", keyTok.Type) + p.advanceAfterError() return nil } @@ -396,6 +414,7 @@ func (p *Parser) parseStructField(typeName string) *MapEntry { p.addWarning(p.stream.Peek().Pos, "expected = or :, continuing") } else { p.addError(p.stream.Peek().Pos, "expected = or :") + p.advanceAfterError() return nil } } @@ -424,6 +443,7 @@ func (p *Parser) parseSum(tag string) *GValue { p.addWarning(p.stream.Peek().Pos, "expected ), auto-closing sum") } else { p.addError(p.stream.Peek().Pos, "expected )") + p.advanceAfterError() } } } @@ -494,6 +514,12 @@ func (p *Parser) addWarning(pos Position, format string, args ...interface{}) { }) } +func (p *Parser) advanceAfterError() { + if !p.stream.AtEnd() { + p.stream.Advance() + } +} + // ============================================================ // Schema Parsing // ============================================================ diff --git a/go/glyph/parse_packed.go b/go/glyph/parse_packed.go index 7a20fb4..19e157d 100644 --- a/go/glyph/parse_packed.go +++ b/go/glyph/parse_packed.go @@ -84,6 +84,11 @@ func (p *packedParser) parse() (*GValue, error) { return nil, fmt.Errorf("expected ')' at pos %d, got %q", p.pos, string(p.peek())) } + p.skipWhitespace() + if p.pos != len(p.input) { + return nil, fmt.Errorf("unexpected trailing data at pos %d", p.pos) + } + return value, nil } @@ -141,6 +146,9 @@ func (p *packedParser) parseBitmapHeader(td *TypeDef) ([]bool, error) { if p.pos == start { return nil, fmt.Errorf("empty bitmap") } + if p.pos-start > maxBitmapBits { + return nil, fmt.Errorf("bitmap too large") + } bits := p.input[start:p.pos] mask, err := binaryToMask("0b" + bits) @@ -247,24 +255,22 @@ func (p *packedParser) parseValue(fd *FieldDef) (*GValue, error) { switch c { case 't': // true or bare string starting with t - if p.tryLiteral("true") || (p.pos < len(p.input) && p.input[p.pos] == 't' && (p.pos+1 >= len(p.input) || !isTypeNameCont(p.input[p.pos+1]))) { - if p.input[p.pos:p.pos+1] == "t" && (p.pos+1 >= len(p.input) || !isTypeNameCont(p.input[p.pos+1])) { - p.pos++ - return Bool(true), nil - } - if p.tryLiteral("true") { - return Bool(true), nil - } + if p.tryLiteral("true") { + return Bool(true), nil + } + if p.pos < len(p.input) && p.input[p.pos] == 't' && (p.pos+1 >= len(p.input) || !isTypeNameCont(p.input[p.pos+1])) { + p.pos++ + return Bool(true), nil } return p.parseBareOrQuotedString() case 'f': // false or bare string starting with f - if p.pos+1 < len(p.input) && !isTypeNameCont(p.input[p.pos+1]) { - p.pos++ + if p.tryLiteral("false") { return Bool(false), nil } - if p.tryLiteral("false") { + if p.pos < len(p.input) && p.input[p.pos] == 'f' && (p.pos+1 >= len(p.input) || !isTypeNameCont(p.input[p.pos+1])) { + p.pos++ return Bool(false), nil } return p.parseBareOrQuotedString() diff --git a/go/glyph/parse_patch.go b/go/glyph/parse_patch.go index 7f122f5..96b8164 100644 --- a/go/glyph/parse_patch.go +++ b/go/glyph/parse_patch.go @@ -192,7 +192,14 @@ func parsePatchOp(line string, keyMode KeyMode, schema *Schema) (*PatchOp, error // Check for @idx= suffix (insert at index) if idx := strings.Index(valueStr, " @idx="); idx >= 0 { idxStr := valueStr[idx+6:] - op.Index, _ = strconv.Atoi(idxStr) + parsedIdx, err := strconv.Atoi(idxStr) + if err != nil { + return nil, &ParseError{Message: fmt.Sprintf("invalid @idx value: %s", idxStr)} + } + if parsedIdx < -1 { + return nil, &ParseError{Message: fmt.Sprintf("invalid @idx value: %s", idxStr)} + } + op.Index = parsedIdx valueStr = valueStr[:idx] } diff --git a/go/glyph/parse_tabular.go b/go/glyph/parse_tabular.go index 27dca67..46d9f7b 100644 --- a/go/glyph/parse_tabular.go +++ b/go/glyph/parse_tabular.go @@ -472,6 +472,9 @@ func (p *tabularRowParser) parseBitmapHeader(td *TypeDef) ([]bool, error) { if p.pos == start { return nil, fmt.Errorf("empty bitmap") } + if p.pos-start > maxBitmapBits { + return nil, fmt.Errorf("bitmap too large") + } bits := p.input[start:p.pos] mask, err := binaryToMask("0b" + bits) diff --git a/go/glyph/security_regression_test.go b/go/glyph/security_regression_test.go new file mode 100644 index 0000000..b031fd1 --- /dev/null +++ b/go/glyph/security_regression_test.go @@ -0,0 +1,110 @@ +package glyph + +import ( + "errors" + "strings" + "testing" + "time" +) + +func TestParseRejectsTrailingTokens(t *testing.T) { + result, err := ParseWithOptions("{a:1} garbage", ParseOptions{Tolerant: false}) + if err == nil { + t.Fatal("expected trailing-token error") + } + if result == nil { + t.Fatal("expected parse result") + } + if result.Value != nil { + t.Fatal("expected nil value when trailing tokens are present") + } +} + +func TestStrictParseDoesNotHangOnMalformedMap(t *testing.T) { + done := make(chan struct{}) + go func() { + defer close(done) + _, _ = ParseWithOptions("{a b}", ParseOptions{Tolerant: false}) + }() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("strict parser did not make forward progress") + } +} + +func TestParsePackedParsesTrue(t *testing.T) { + schema, err := ParseSchema(`@schema{ + Flag:v1 struct{ ok: bool } + }`) + if err != nil { + t.Fatalf("ParseSchema error: %v", err) + } + + got, err := ParsePacked("Flag@(true)", schema) + if err != nil { + t.Fatalf("ParsePacked error: %v", err) + } + if !mustAsBool(t, got.Get("ok")) { + t.Fatal("expected ok=true") + } +} + +func TestParsePackedRejectsTrailingTokens(t *testing.T) { + schema, err := ParseSchema(`@schema{ + Flag:v1 struct{ ok: bool } + }`) + if err != nil { + t.Fatalf("ParseSchema error: %v", err) + } + + if _, err := ParsePacked("Flag@(t) trailing", schema); err == nil { + t.Fatal("expected trailing-token error") + } +} + +func TestBinaryToMaskRejectsOversizedBitmap(t *testing.T) { + _, err := binaryToMask("0b" + strings.Repeat("1", maxBitmapBits+1)) + if err == nil { + t.Fatal("expected oversized bitmap error") + } +} + +func TestParsePatchRejectsInvalidIndex(t *testing.T) { + input := `@patch ++ items "x" @idx=abc +@end` + + if _, err := ParsePatch(input, nil); err == nil { + t.Fatal("expected invalid @idx error") + } +} + +func TestTryParseNumberRejectsSuffixGarbage(t *testing.T) { + if v, ok := tryParseNumber("1abc"); ok || v != nil { + t.Fatal("expected suffixed number to be rejected") + } +} + +func TestIncrementalParserDoesNotRecurseOnErrorHandlerFailure(t *testing.T) { + handlerCalls := 0 + handler := func(event ParseEvent) error { + handlerCalls++ + if event.Type == EventError { + return errors.New("handler failure") + } + return nil + } + + p := NewIncrementalParser(handler, DefaultIncrementalParserOptions()) + if _, err := p.Feed([]byte("{incomplete")); err != nil { + t.Fatalf("Feed error: %v", err) + } + if err := p.End(); err == nil { + t.Fatal("expected end error") + } + if handlerCalls == 0 { + t.Fatal("expected handler to be called") + } +} diff --git a/js/src/glyph.test.ts b/js/src/glyph.test.ts index 20a9114..08fc626 100644 --- a/js/src/glyph.test.ts +++ b/js/src/glyph.test.ts @@ -163,6 +163,15 @@ describe('JSON conversion', () => { expect(v.get('name')?.asStr()).toBe('Arsenal'); }); + test('fromJson ignores inherited special markers', () => { + const json = Object.create({ $type: 'Team' }) as Record; + json.name = 'Arsenal'; + + const v = fromJson(json); + expect(v.type).toBe('map'); + expect(v.get('name')?.asStr()).toBe('Arsenal'); + }); + test('toJson primitives', () => { expect(toJson(g.null())).toBe(null); expect(toJson(g.bool(true))).toBe(true); @@ -182,6 +191,13 @@ describe('JSON conversion', () => { expect(json.name).toBe('Arsenal'); }); + test('toJson preserves __proto__ as data on a null-prototype object', () => { + const json = toJson(g.map(field('__proto__', g.str('safe')))) as Record; + + expect(Object.getPrototypeOf(json)).toBeNull(); + expect(json['__proto__']).toBe('safe'); + }); + test('roundtrip', () => { const original = { id: '^t:ARS', @@ -326,6 +342,20 @@ describe('Parse', () => { expect(parsed.get('name')?.asStr()).toBe('Arsenal'); expect(parsed.get('league')?.asStr()).toBe('EPL'); }); + + test('reject trailing garbage after packed value', () => { + expect(() => parsePacked('Team@(^t:ARS Arsenal EPL) trailing', schema)).toThrow('trailing garbage'); + }); + + test('reject malformed numeric tokens in packed values', () => { + const numericSchema = new SchemaBuilder() + .addPackedStruct('Score', 'v1') + .field('home', t.int(), { fid: 1 }) + .field('away', t.str(), { fid: 2 }) + .build(); + + expect(() => parsePacked('Score@(1e Arsenal)', numericSchema)).toThrow('invalid numeric token'); + }); }); // ============================================================ @@ -450,6 +480,22 @@ describe('Patch', () => { expect(result.get('home')?.get('score')?.asInt()).toBe(3); }); + + test('reject malformed patch paths', () => { + const input = `@patch @keys=wire @target=m:ARS-LIV += events[abc] 1 +@end`; + + expect(() => parsePatch(input)).toThrow('invalid list index'); + }); + + test('reject NaN patch indexes', () => { + const input = `@patch @keys=wire @target=m:ARS-LIV ++ events "Goal!" @idx=NaN +@end`; + + expect(() => parsePatch(input)).toThrow('invalid patch index'); + }); }); // ============================================================ @@ -550,6 +596,15 @@ describe('Loose Mode', () => { expect(obj.get('a')?.asInt()).toBe(1); }); + test('ignores inherited extended markers', () => { + const json = Object.create({ $glyph: 'time', value: '2025-12-19T10:30:00Z' }) as Record; + json.safe = 1; + + const obj = fromJsonLoose(json, { extended: true }); + expect(obj.type).toBe('map'); + expect(obj.get('safe')?.asInt()).toBe(1); + }); + test('rejects NaN', () => { expect(() => fromJsonLoose(NaN)).toThrow('NaN/Infinity'); }); @@ -587,6 +642,13 @@ describe('Loose Mode', () => { expect(idJson.$glyph).toBe('id'); expect(idJson.value).toBe('^user:123'); }); + + test('preserves __proto__ as data on null-prototype objects', () => { + const json = toJsonLoose(g.map(field('__proto__', g.str('safe')))) as Record; + + expect(Object.getPrototypeOf(json)).toBeNull(); + expect(json['__proto__']).toBe('safe'); + }); }); describe('JSON roundtrip', () => { diff --git a/js/src/json.ts b/js/src/json.ts index d6d56b7..25f136a 100644 --- a/js/src/json.ts +++ b/js/src/json.ts @@ -7,6 +7,16 @@ import { GValue, RefID, MapEntry } from './types'; import { Schema, TypeDef } from './schema'; +const hasOwnProperty = Object.prototype.hasOwnProperty; + +function hasOwn(obj: object, key: string): boolean { + return hasOwnProperty.call(obj, key); +} + +function createJsonObject(): Record { + return Object.create(null) as Record; +} + // ============================================================ // JSON to GValue Conversion // ============================================================ @@ -88,11 +98,16 @@ function convertValue( // Object if (typeof v === 'object') { const obj = v as Record; + const typeMarker = hasOwn(obj, '$type') ? obj.$type : undefined; + const refMarker = hasOwn(obj, '$ref') ? obj.$ref : undefined; + const timeMarker = hasOwn(obj, '$time') ? obj.$time : undefined; + const bytesMarker = hasOwn(obj, '$bytes') ? obj.$bytes : undefined; + const tagMarker = hasOwn(obj, '$tag') ? obj.$tag : undefined; // Check for special type markers - if ('$type' in obj && typeof obj.$type === 'string') { + if (typeof typeMarker === 'string') { // Typed struct: { $type: "TypeName", field1: ..., field2: ... } - const structTypeName = obj.$type; + const structTypeName = typeMarker; const td = schema?.getType(structTypeName); const fields: MapEntry[] = []; @@ -113,8 +128,8 @@ function convertValue( } // Check for ref marker - if ('$ref' in obj && typeof obj.$ref === 'string') { - const ref = obj.$ref; + if (typeof refMarker === 'string') { + const ref = refMarker; const colonIdx = ref.indexOf(':'); if (colonIdx > 0) { return GValue.id(ref.slice(0, colonIdx), ref.slice(colonIdx + 1)); @@ -123,21 +138,21 @@ function convertValue( } // Check for time marker - if ('$time' in obj && typeof obj.$time === 'string') { - return GValue.time(new Date(obj.$time)); + if (typeof timeMarker === 'string') { + return GValue.time(new Date(timeMarker)); } // Check for bytes marker - if ('$bytes' in obj && typeof obj.$bytes === 'string') { - return GValue.bytes(base64ToBytes(obj.$bytes)); + if (typeof bytesMarker === 'string') { + return GValue.bytes(base64ToBytes(bytesMarker)); } // Check for sum type marker - if ('$tag' in obj && typeof obj.$tag === 'string') { - const value = '$value' in obj + if (typeof tagMarker === 'string') { + const value = hasOwn(obj, '$value') ? convertValue(obj.$value, schema, undefined, parseDates, parseRefs) : null; - return GValue.sum(obj.$tag, value); + return GValue.sum(tagMarker, value); } // Regular object -> struct with typeName or map @@ -249,7 +264,9 @@ function convertToJson( case 'bytes': { const bytes = gv.asBytes(); const b64 = bytesToBase64(bytes); - return { $bytes: b64 }; + const result = createJsonObject(); + result.$bytes = b64; + return result; } case 'time': { @@ -257,7 +274,9 @@ function convertToJson( if (formatDates) { return date.toISOString(); } - return { $time: date.toISOString() }; + const result = createJsonObject(); + result.$time = date.toISOString(); + return result; } case 'id': { @@ -266,7 +285,9 @@ function convertToJson( if (compactRefs) { return `^${refStr}`; } - return { $ref: refStr }; + const result = createJsonObject(); + result.$ref = refStr; + return result; } case 'list': { @@ -276,7 +297,7 @@ function convertToJson( } case 'map': { - const result: Record = {}; + const result = createJsonObject(); for (const entry of gv.asMap()) { result[entry.key] = convertToJson( entry.value, includeTypeMarkers, compactRefs, formatDates, useWireKeys, schema @@ -287,7 +308,7 @@ function convertToJson( case 'struct': { const sv = gv.asStruct(); - const result: Record = {}; + const result = createJsonObject(); if (includeTypeMarkers) { result.$type = sv.typeName; @@ -315,15 +336,15 @@ function convertToJson( case 'sum': { const sum = gv.asSum(); + const result = createJsonObject(); + result.$tag = sum.tag; if (sum.value === null) { - return { $tag: sum.tag }; + return result; } - return { - $tag: sum.tag, - $value: convertToJson( - sum.value, includeTypeMarkers, compactRefs, formatDates, useWireKeys, schema - ), - }; + result.$value = convertToJson( + sum.value, includeTypeMarkers, compactRefs, formatDates, useWireKeys, schema + ); + return result; } } } diff --git a/js/src/loose.ts b/js/src/loose.ts index ba01e20..9b3b64c 100644 --- a/js/src/loose.ts +++ b/js/src/loose.ts @@ -24,6 +24,16 @@ import { GValue, MapEntry } from './types'; +const hasOwnProperty = Object.prototype.hasOwnProperty; + +function hasOwn(obj: object, key: string): boolean { + return hasOwnProperty.call(obj, key); +} + +function createJsonObject(): Record { + return Object.create(null) as Record; +} + // ============================================================ // Loose Canonicalization Options // ============================================================ @@ -1255,8 +1265,9 @@ export function fromJsonLoose(json: unknown, opts: BridgeOpts = {}): GValue { const obj = json as Record; // Check for extended markers - if (opts.extended && typeof obj.$glyph === 'string') { - return fromGlyphMarker(obj.$glyph, obj); + const glyphMarker = hasOwn(obj, '$glyph') ? obj.$glyph : undefined; + if (opts.extended && typeof glyphMarker === 'string') { + return fromGlyphMarker(glyphMarker, obj); } // Regular object/map @@ -1343,14 +1354,20 @@ export function toJsonLoose(gv: GValue, opts: BridgeOpts = {}): unknown { case 'bytes': { const b64 = bytesToBase64(gv.asBytes()); if (opts.extended) { - return { $glyph: 'bytes', base64: b64 }; + const result = createJsonObject(); + result.$glyph = 'bytes'; + result.base64 = b64; + return result; } return b64; } case 'time': { const iso = gv.asTime().toISOString(); if (opts.extended) { - return { $glyph: 'time', value: iso }; + const result = createJsonObject(); + result.$glyph = 'time'; + result.value = iso; + return result; } return iso; } @@ -1358,14 +1375,17 @@ export function toJsonLoose(gv: GValue, opts: BridgeOpts = {}): unknown { const ref = gv.asId(); const refStr = `^${ref.prefix ? ref.prefix + ':' : ''}${ref.value}`; if (opts.extended) { - return { $glyph: 'id', value: refStr }; + const result = createJsonObject(); + result.$glyph = 'id'; + result.value = refStr; + return result; } return refStr; } case 'list': return gv.asList().map(v => toJsonLoose(v, opts)); case 'map': { - const result: Record = {}; + const result = createJsonObject(); for (const entry of gv.asMap()) { result[entry.key] = toJsonLoose(entry.value, opts); } @@ -1374,7 +1394,7 @@ export function toJsonLoose(gv: GValue, opts: BridgeOpts = {}): unknown { case 'struct': { // Structs become objects const sv = gv.asStruct(); - const result: Record = {}; + const result = createJsonObject(); for (const field of sv.fields) { result[field.key] = toJsonLoose(field.value, opts); } @@ -1383,7 +1403,9 @@ export function toJsonLoose(gv: GValue, opts: BridgeOpts = {}): unknown { case 'sum': { // Sums become { tag: value } const sum = gv.asSum(); - return { [sum.tag]: sum.value ? toJsonLoose(sum.value, opts) : null }; + const result = createJsonObject(); + result[sum.tag] = sum.value ? toJsonLoose(sum.value, opts) : null; + return result; } } } @@ -1433,7 +1455,7 @@ function jsonValueEqual(a: unknown, b: unknown): boolean { const keysB = Object.keys(objB); if (keysA.length !== keysB.length) return false; for (const key of keysA) { - if (!(key in objB)) return false; + if (!hasOwn(objB, key)) return false; if (!jsonValueEqual(objA[key], objB[key])) return false; } return true; diff --git a/js/src/parse.ts b/js/src/parse.ts index 8dbeb68..fc0ce12 100644 --- a/js/src/parse.ts +++ b/js/src/parse.ts @@ -59,6 +59,10 @@ class PackedParser { } this.expect(')'); + this.skipWhitespace(); + if (this.pos !== this.input.length) { + throw new Error(`trailing garbage at pos ${this.pos}`); + } return value; } @@ -274,50 +278,40 @@ class PackedParser { const start = this.pos; while (this.pos < this.input.length) { const c = this.input[this.pos]; - if (c === ' ' || c === ')' || c === ']' || c === '}' || c === '\n') { + if (this.isTokenBoundary(c)) { break; } this.pos++; } const timeStr = this.input.slice(start, this.pos); - return GValue.time(new Date(timeStr)); + const date = new Date(timeStr); + if (Number.isNaN(date.getTime())) { + throw new Error(`invalid time at pos ${start}`); + } + return GValue.time(date); } private parseNumber(): GValue { const start = this.pos; - - // Optional minus - if (this.input[this.pos] === '-') this.pos++; - - // Integer part - while (this.pos < this.input.length && this.input[this.pos] >= '0' && this.input[this.pos] <= '9') { - this.pos++; + const match = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(this.input.slice(this.pos)); + if (!match) { + throw new Error(`invalid number at pos ${start}`); } - - let isFloat = false; - - // Decimal part - if (this.pos < this.input.length && this.input[this.pos] === '.') { - isFloat = true; - this.pos++; - while (this.pos < this.input.length && this.input[this.pos] >= '0' && this.input[this.pos] <= '9') { - this.pos++; - } + + const numStr = match[0]; + const next = this.input[this.pos + numStr.length] ?? ''; + if (next !== '' && !this.isTokenBoundary(next)) { + throw new Error(`invalid numeric token at pos ${start}`); } - - // Exponent - if (this.pos < this.input.length && (this.input[this.pos] === 'e' || this.input[this.pos] === 'E')) { - isFloat = true; - this.pos++; - if (this.input[this.pos] === '+' || this.input[this.pos] === '-') this.pos++; - while (this.pos < this.input.length && this.input[this.pos] >= '0' && this.input[this.pos] <= '9') { - this.pos++; - } + + this.pos += numStr.length; + const num = Number(numStr); + if (!Number.isFinite(num)) { + throw new Error(`invalid number at pos ${start}`); } - - const numStr = this.input.slice(start, this.pos); - if (isFloat) { - return GValue.float(parseFloat(numStr)); + + if (numStr.includes('.') || numStr.includes('e') || numStr.includes('E')) { + return GValue.float(num); } return GValue.int(parseInt(numStr, 10)); } @@ -446,6 +440,10 @@ class PackedParser { return this.pos < this.input.length ? this.input[this.pos] : ''; } + private isTokenBoundary(c: string): boolean { + return c === '' || c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === ')' || c === ']' || c === '}'; + } + private expect(c: string): void { this.skipWhitespace(); if (this.pos >= this.input.length || this.input[this.pos] !== c) { diff --git a/js/src/patch.ts b/js/src/patch.ts index 5b98168..9350ec4 100644 --- a/js/src/patch.ts +++ b/js/src/patch.ts @@ -49,13 +49,67 @@ export function fieldSeg(name: string, fid?: number): PathSeg { } export function listIdxSeg(idx: number): PathSeg { - return { kind: 'listIdx', listIdx: idx }; + return { kind: 'listIdx', listIdx: parseNonNegativeSafeInt(String(idx), 'list index') }; } export function mapKeySeg(key: string): PathSeg { return { kind: 'mapKey', mapKey: key }; } +function parseNonNegativeSafeInt(raw: string, field: string): number { + if (!/^\d+$/.test(raw)) { + throw new Error(`invalid ${field}: ${raw}`); + } + const value = Number(raw); + if (!Number.isSafeInteger(value)) { + throw new Error(`${field} out of range: ${raw}`); + } + return value; +} + +function parseFiniteNumber(raw: string, field: string): number { + if (!/^[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(raw)) { + throw new Error(`invalid ${field}: ${raw}`); + } + const value = Number(raw); + if (!Number.isFinite(value)) { + throw new Error(`invalid ${field}: ${raw}`); + } + return value; +} + +function parseQuotedPathString(path: string, start: number): { value: string; next: number } { + if (path[start] !== '"') { + throw new Error(`expected quoted string at pos ${start}`); + } + + let value = ''; + let escaped = false; + let i = start + 1; + + while (i < path.length) { + const c = path[i]; + if (escaped) { + value += c; + escaped = false; + i++; + continue; + } + if (c === '\\') { + escaped = true; + i++; + continue; + } + if (c === '"') { + return { value, next: i + 1 }; + } + value += c; + i++; + } + + throw new Error(`unterminated quoted path segment at pos ${start}`); +} + // ============================================================ // Path Parsing // ============================================================ @@ -65,77 +119,91 @@ export function mapKeySeg(key: string): PathSeg { * Supports: .fieldName, .#fid, [N], ["key"] */ export function parsePathToSegs(path: string): PathSeg[] { + path = path.trim(); if (!path) return []; - + const segs: PathSeg[] = []; - let i = 0; - + let i = path.startsWith('.') ? 1 : 0; + if (i >= path.length) { + throw new Error('path cannot end with dot'); + } + while (i < path.length) { - // Skip leading dots - if (path[i] === '.') { + const c = path[i]; + + if (c === '[') { i++; - continue; - } - - // List index: [N] or map key: ["key"] - if (path[i] === '[') { - const end = path.indexOf(']', i); - if (end === -1) { - // Malformed, treat rest as field - segs.push(fieldSeg(path.slice(i))); - break; + if (i >= path.length) { + throw new Error(`unterminated path segment at pos ${i - 1}`); } - - const inner = path.slice(i + 1, end); - if (inner.startsWith('"')) { - // Map key - segs.push(mapKeySeg(inner.slice(1, -1))); + + if (path[i] === '"') { + const parsed = parseQuotedPathString(path, i); + i = parsed.next; + if (path[i] !== ']') { + throw new Error(`unterminated map key segment at pos ${i}`); + } + segs.push(mapKeySeg(parsed.value)); + i++; } else { - // List index - segs.push(listIdxSeg(parseInt(inner, 10))); + const end = path.indexOf(']', i); + if (end < 0) { + throw new Error(`unterminated list index at pos ${i - 1}`); + } + const inner = path.slice(i, end); + segs.push(listIdxSeg(parseNonNegativeSafeInt(inner, 'list index'))); + i = end + 1; } - i = end + 1; - continue; - } - - // FID reference: #N - if (path[i] === '#') { - let j = i + 1; + } else if (c === '#') { + const start = i + 1; + let j = start; while (j < path.length && path[j] >= '0' && path[j] <= '9') { j++; } - if (j > i + 1) { - const fid = parseInt(path.slice(i + 1, j), 10); - segs.push({ kind: 'field', fid }); + if (j === start) { + throw new Error(`missing field id at pos ${i}`); } + segs.push({ kind: 'field', fid: parseNonNegativeSafeInt(path.slice(start, j), 'field id') }); i = j; - continue; - } - - // Field name: until . or [ or end - let j = i; - let inQuote = false; - while (j < path.length) { - const c = path[j]; + } else { + let field: string; if (c === '"') { - inQuote = !inQuote; - } else if (!inQuote && (c === '.' || c === '[')) { - break; + const parsed = parseQuotedPathString(path, i); + field = parsed.value; + i = parsed.next; + } else { + let j = i; + while (j < path.length && path[j] !== '.' && path[j] !== '[' && path[j] !== ']') { + j++; + } + if (j === i) { + throw new Error(`empty path segment at pos ${i}`); + } + field = path.slice(i, j); + i = j; } - j++; - } - - if (j > i) { - let field = path.slice(i, j); - // Remove quotes if present - if (field.startsWith('"') && field.endsWith('"')) { - field = field.slice(1, -1); + if (!field) { + throw new Error(`empty field name at pos ${i}`); } segs.push(fieldSeg(field)); } - i = j; + + if (i >= path.length) { + break; + } + if (path[i] === '.') { + i++; + if (i >= path.length) { + throw new Error('path cannot end with dot'); + } + continue; + } + if (path[i] === '[') { + continue; + } + throw new Error(`unexpected character '${path[i]}' in path`); } - + return segs; } @@ -241,7 +309,7 @@ export class PatchBuilder { op: '+', path: parsePathToSegs(path), value, - index, + index: parseNonNegativeSafeInt(String(index), 'patch index'), }); return this; } @@ -518,11 +586,12 @@ function parsePatchOp(line: string, schema?: Schema): PatchOp { case '=': case '+': { if (valueStr) { - // Check for @idx= suffix - const idxMatch = valueStr.match(/ @idx=(\d+)$/); - if (idxMatch) { - op.index = parseInt(idxMatch[1], 10); - valueStr = valueStr.slice(0, -idxMatch[0].length); + const tokens = tokenizeValues(valueStr); + const lastToken = tokens[tokens.length - 1]; + if (opChar === '+' && tokens.length > 1 && lastToken?.startsWith('@idx=')) { + op.index = parseNonNegativeSafeInt(lastToken.slice(5), 'patch index'); + tokens.pop(); + valueStr = tokens.join(' ').trim(); } op.value = parseInlineValue(valueStr, schema); } @@ -532,7 +601,7 @@ function parsePatchOp(line: string, schema?: Schema): PatchOp { if (!valueStr) { throw new Error('delta operation requires a value'); } - const num = parseFloat(valueStr); + const num = parseFiniteNumber(valueStr, 'delta'); op.value = GValue.float(num); break; } @@ -592,8 +661,9 @@ function parseInlineValue(s: string, schema?: Schema): GValue { // Number if (/^-?\d/.test(s)) { + const num = parseFiniteNumber(s, 'number'); if (s.includes('.') || s.includes('e') || s.includes('E')) { - return GValue.float(parseFloat(s)); + return GValue.float(num); } return GValue.int(parseInt(s, 10)); } @@ -613,6 +683,9 @@ function parseInlineValue(s: string, schema?: Schema): GValue { } function parseQuotedString(s: string): GValue { + if (s.length < 2 || !s.endsWith('"')) { + throw new Error('unterminated string literal'); + } let result = ''; for (let i = 1; i < s.length - 1; i++) { if (s[i] === '\\' && i + 1 < s.length - 1) { diff --git a/js/src/stream/gs1t.ts b/js/src/stream/gs1t.ts index d9d8049..e333a24 100644 --- a/js/src/stream/gs1t.ts +++ b/js/src/stream/gs1t.ts @@ -12,6 +12,7 @@ import { hashToHex, hexToHash } from './hash'; const encoder = new TextEncoder(); const decoder = new TextDecoder(); +const DEFAULT_MAX_HEADER_BYTES = 8 * 1024; // ============================================================ // Writer @@ -92,6 +93,8 @@ export function encodeFrames(frames: Frame[], options: WriterOptions = {}): Uint export interface ReaderOptions { /** Maximum payload size (default: 64 MiB) */ maxPayload?: number; + /** Maximum header size in bytes before newline (default: 8 KiB) */ + maxHeaderBytes?: number; /** Whether to verify CRC (default: true) */ verifyCRC?: boolean; } @@ -104,10 +107,12 @@ export class Reader { private buffer: Uint8Array = new Uint8Array(0); private offset = 0; private readonly maxPayload: number; + private readonly maxHeaderBytes: number; private readonly verifyCRC: boolean; constructor(options: ReaderOptions = {}) { this.maxPayload = options.maxPayload ?? MAX_PAYLOAD_SIZE; + this.maxHeaderBytes = options.maxHeaderBytes ?? DEFAULT_MAX_HEADER_BYTES; this.verifyCRC = options.verifyCRC ?? true; } @@ -135,8 +140,15 @@ export class Reader { // Find header line ending const headerEnd = this.findNewline(this.offset); if (headerEnd < 0) { + if (this.buffer.length - this.offset > this.maxHeaderBytes) { + throw new ParseError(`header too large: > ${this.maxHeaderBytes}`); + } return null; // Need more data } + + if (headerEnd - this.offset > this.maxHeaderBytes) { + throw new ParseError(`header too large: > ${this.maxHeaderBytes}`); + } const headerLine = decoder.decode(this.buffer.slice(this.offset, headerEnd)); @@ -148,7 +160,6 @@ export class Reader { // Check if we have enough data for payload + trailing newline const payloadStart = headerEnd + 1; - const frameEnd = payloadStart + header.payloadLen + 1; if (this.buffer.length < payloadStart + header.payloadLen) { return null; // Need more data @@ -218,6 +229,9 @@ export class Reader { if (endIdx < 0) { throw new ParseError('missing closing }'); } + if (endIdx !== line.length - 1) { + throw new ParseError('trailing data after header'); + } const content = line.slice(7, endIdx); const pairs = this.tokenize(content); @@ -239,19 +253,19 @@ export class Reader { switch (key) { case 'v': - header.version = parseInt(val, 10); + header.version = this.parseUnsignedInt(val, 'v'); break; case 'sid': - header.sid = BigInt(val); + header.sid = this.parseUnsignedBigInt(val, 'sid'); break; case 'seq': - header.seq = BigInt(val); + header.seq = this.parseUnsignedBigInt(val, 'seq'); break; case 'kind': header.kind = parseKind(val); break; case 'len': - header.payloadLen = parseInt(val, 10); + header.payloadLen = this.parseUnsignedInt(val, 'len'); break; case 'crc': header.crc = parseCRC(val) ?? undefined; @@ -263,7 +277,7 @@ export class Reader { header.final = val === 'true' || val === '1'; break; case 'flags': - header.flags = parseInt(val, 16); + header.flags = this.parseHexInt(val, 'flags'); break; } } @@ -295,6 +309,36 @@ export class Reader { } return tokens; } + + private parseUnsignedInt(raw: string, field: string): number { + if (!/^\d+$/.test(raw)) { + throw new ParseError(`invalid ${field}`); + } + const value = Number(raw); + if (!Number.isSafeInteger(value)) { + throw new ParseError(`${field} out of range`); + } + return value; + } + + private parseUnsignedBigInt(raw: string, field: string): bigint { + if (!/^\d+$/.test(raw)) { + throw new ParseError(`invalid ${field}`); + } + return BigInt(raw); + } + + private parseHexInt(raw: string, field: string): number { + const normalized = raw.replace(/^0x/i, ''); + if (!/^[0-9a-fA-F]+$/.test(normalized)) { + throw new ParseError(`invalid ${field}`); + } + const value = parseInt(normalized, 16); + if (!Number.isSafeInteger(value)) { + throw new ParseError(`${field} out of range`); + } + return value; + } } interface ParsedHeader { diff --git a/js/src/stream/stream.test.ts b/js/src/stream/stream.test.ts index 8d96606..5e97282 100644 --- a/js/src/stream/stream.test.ts +++ b/js/src/stream/stream.test.ts @@ -228,6 +228,19 @@ update expect(() => reader.next()).toThrow(ParseError); }); + + test('header too large throws before newline', () => { + const reader = new Reader({ maxHeaderBytes: 32 }); + reader.push(encoder.encode('@frame{' + 'x'.repeat(40))); + + expect(() => reader.next()).toThrow(ParseError); + }); + + test('invalid len token throws', () => { + const input = '@frame{v=1 sid=0 seq=0 kind=doc len=1x}\nA\n'; + + expect(() => decodeFrame(encoder.encode(input))).toThrow(ParseError); + }); }); // ============================================================ diff --git a/js/src/stream_validator.test.ts b/js/src/stream_validator.test.ts index 5700f60..12c2af6 100644 --- a/js/src/stream_validator.test.ts +++ b/js/src/stream_validator.test.ts @@ -306,6 +306,22 @@ describe('StreamingValidator - Constraint Validation', () => { expect(result.errors.some(e => e.code === ErrorCode.ConstraintPattern)).toBe(true); }); + test('reject invalid int argument types', () => { + const validator = new StreamingValidator(registry); + validator.pushToken('{action=test count=oops}'); + + const result = validator.getResult(); + expect(result.errors.some(e => e.code === ErrorCode.InvalidType)).toBe(true); + }); + + test('reject invalid string argument types', () => { + const validator = new StreamingValidator(registry); + validator.pushToken('{action=test name=42}'); + + const result = validator.getResult(); + expect(result.errors.some(e => e.code === ErrorCode.InvalidType)).toBe(true); + }); + test('valid constraints pass', () => { const validator = new StreamingValidator(registry); validator.pushToken('{action=test count=50 name=valid status=active url="https://example.com"}'); @@ -364,6 +380,22 @@ describe('StreamingValidator - Required Fields', () => { const result = validator.getResult(); expect(result.errors.some(e => e.code === ErrorCode.MissingTool)).toBe(true); }); + + test('required inherited prototype keys do not count as present', () => { + const registry = new ToolRegistry(); + registry.register({ + name: 'test', + args: { + ['toString']: { type: 'string', required: true }, + }, + }); + + const validator = new StreamingValidator(registry); + validator.pushToken('{action=test}'); + + const result = validator.getResult(); + expect(result.errors.some(e => e.code === ErrorCode.MissingRequired && e.field === 'toString')).toBe(true); + }); }); // ============================================================ @@ -647,6 +679,24 @@ describe('StreamingValidator - Edge Cases', () => { expect(result.toolName).toBe('search'); expect(result.fields.query).toBe('test'); }); + + test('stores __proto__ fields without mutating the result prototype', () => { + const registry = new ToolRegistry(); + registry.register({ + name: 'danger', + args: { + ['__proto__']: { type: 'string' }, + }, + }); + + const validator = new StreamingValidator(registry); + validator.pushToken('{action=danger __proto__=safe}'); + + const result = validator.getResult(); + expect(result.valid).toBe(true); + expect(Object.getPrototypeOf(result.fields)).toBeNull(); + expect((result.fields as Record)['__proto__']).toBe('safe'); + }); }); // ============================================================ diff --git a/js/src/stream_validator.ts b/js/src/stream_validator.ts index 87714ab..95e7c66 100644 --- a/js/src/stream_validator.ts +++ b/js/src/stream_validator.ts @@ -31,6 +31,24 @@ export interface ToolSchema { args: Record; } +const hasOwnProperty = Object.prototype.hasOwnProperty; + +function hasOwn(obj: object, key: string): boolean { + return hasOwnProperty.call(obj, key); +} + +function createArgRecord(): Record { + return Object.create(null) as Record; +} + +function createFieldRecord(): Record { + return Object.create(null) as Record; +} + +function cloneFieldRecord(fields: Record): Record { + return Object.assign(createFieldRecord(), fields); +} + export class ToolRegistry { private tools: Map = new Map(); @@ -38,7 +56,11 @@ export class ToolRegistry { * Register a tool. */ register(tool: ToolSchema): void { - this.tools.set(tool.name, tool); + const args = createArgRecord(); + for (const [name, schema] of Object.entries(tool.args)) { + args[name] = schema; + } + this.tools.set(tool.name, { ...tool, args }); } /** @@ -150,7 +172,8 @@ export class StreamingValidator { // Parsed data private toolName: string | null = null; - private fields: Record = {}; + private fields: Record = createFieldRecord(); + private fieldCount: number = 0; private errors: ValidationError[] = []; // Timing @@ -219,7 +242,8 @@ export class StreamingValidator { this.currentVal = ''; this.hasKey = false; this.toolName = null; - this.fields = {}; + this.fields = createFieldRecord(); + this.fieldCount = 0; this.errors = []; this.tokenCount = 0; this.charCount = 0; @@ -306,19 +330,14 @@ export class StreamingValidator { } private processChar(c: string): void { - // Check hard limits before processing - if (this.buffer.length >= this.maxBufferSize) { - if (this.state !== ValidatorState.Error) { - this.state = ValidatorState.Error; - this.addError(ErrorCode.LimitExceeded, 'Buffer size limit exceeded'); - } + if (this.state === ValidatorState.Error) { return; } - if (Object.keys(this.fields).length >= this.maxFieldCount) { - if (this.state !== ValidatorState.Error) { - this.state = ValidatorState.Error; - this.addError(ErrorCode.LimitExceeded, 'Field count limit exceeded'); - } + + // Check hard limits before processing + if (this.buffer.length >= this.maxBufferSize) { + this.state = ValidatorState.Error; + this.addError(ErrorCode.LimitExceeded, 'Buffer size limit exceeded'); return; } @@ -451,6 +470,14 @@ export class StreamingValidator { this.validateField(key, value); } + if (!hasOwn(this.fields, key)) { + if (this.fieldCount >= this.maxFieldCount) { + this.state = ValidatorState.Error; + this.addError(ErrorCode.LimitExceeded, 'Field count limit exceeded'); + return; + } + this.fieldCount++; + } this.fields[key] = value; } @@ -495,11 +522,16 @@ export class StreamingValidator { return; } - const argSchema = schema.args[key]; + const argSchema = hasOwn(schema.args, key) ? schema.args[key] : undefined; if (!argSchema) { return; } + if (!this.isValidType(argSchema.type, value)) { + this.addError(ErrorCode.InvalidType, `${key} expected ${argSchema.type}`, key); + return; + } + // Numeric constraints if (typeof value === 'number') { if (argSchema.min !== undefined && value < argSchema.min) { @@ -527,6 +559,31 @@ export class StreamingValidator { } } + private isValidType(type: string, value: FieldValue): boolean { + if (value === null) { + return true; + } + + switch (type) { + case 'string': + return typeof value === 'string'; + case 'int': + return typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value); + case 'float': + case 'number': + return typeof value === 'number' && Number.isFinite(value); + case 'bool': + case 'boolean': + return typeof value === 'boolean'; + case 'null': + return value === null; + case 'any': + return true; + default: + return true; + } + } + private validateComplete(): void { if (!this.toolName) { this.addError(ErrorCode.MissingTool, 'No action field found'); @@ -540,7 +597,7 @@ export class StreamingValidator { // Check required fields for (const [argName, argSchema] of Object.entries(schema.args)) { - if (argSchema.required && !(argName in this.fields)) { + if (argSchema.required && !hasOwn(this.fields, argName)) { this.addError(ErrorCode.MissingRequired, `Missing required field: ${argName}`, argName); } } @@ -559,7 +616,7 @@ export class StreamingValidator { toolName: this.toolName, toolAllowed, errors: [...this.errors], - fields: { ...this.fields }, + fields: cloneFieldRecord(this.fields), tokenCount: this.tokenCount, charCount: this.charCount, timeline: [...this.timeline], @@ -597,7 +654,7 @@ export class StreamingValidator { */ getParsed(): Record | null { if (this.state === ValidatorState.Complete && this.errors.length === 0) { - return { ...this.fields }; + return cloneFieldRecord(this.fields); } return null; } diff --git a/py/glyph/loose.py b/py/glyph/loose.py index 56ddc86..dd50ee0 100644 --- a/py/glyph/loose.py +++ b/py/glyph/loose.py @@ -80,7 +80,7 @@ def no_tabular_loose_canon_opts() -> LooseCanonOpts: NULL_UNDERSCORE = "_" # Reserved words that must be quoted -RESERVED_WORDS = {"t", "f", "true", "false", "null", "none", "nil", "_"} +RESERVED_WORDS = {"t", "f", "true", "false", "null", "none", "nil", "_", "NaN", "Inf"} # ============================================================ @@ -113,11 +113,8 @@ def canon_float(f: float) -> str: if math.copysign(1.0, f) < 0 and f == 0: # Negative zero return "0" - # Check for special values - if math.isnan(f): - return "NaN" - if math.isinf(f): - return "Inf" if f > 0 else "-Inf" + if not math.isfinite(f): + raise ValueError("non-finite floats are not supported") abs_f = abs(f) @@ -477,6 +474,8 @@ def from_json_loose(data: Any) -> GValue: elif isinstance(data, int): return GValue.int_(data) elif isinstance(data, float): + if not math.isfinite(data): + raise ValueError("non-finite floats are not supported") return GValue.float_(data) elif isinstance(data, str): return GValue.str_(data) @@ -504,7 +503,10 @@ def to_json_loose(v: GValue) -> Any: elif t == GType.INT: return v.as_int() elif t == GType.FLOAT: - return v.as_float() + value = v.as_float() + if not math.isfinite(value): + raise ValueError("non-finite floats are not JSON-safe") + return value elif t == GType.STR: return v.as_str() elif t == GType.BYTES: @@ -519,7 +521,10 @@ def to_json_loose(v: GValue) -> Any: elif t == GType.LIST: return [to_json_loose(item) for item in v.as_list()] elif t == GType.MAP: - return {e.key: to_json_loose(e.value) for e in v.as_map()} + result = {} + for e in v.as_map(): + result[e.key] = to_json_loose(e.value) + return result elif t == GType.STRUCT: sv = v.as_struct() result = {"$type": sv.type_name} diff --git a/py/glyph/parse.py b/py/glyph/parse.py index 9df3c50..f06c54c 100644 --- a/py/glyph/parse.py +++ b/py/glyph/parse.py @@ -5,13 +5,13 @@ """ from __future__ import annotations -import re import base64 +import math +from contextlib import contextmanager from dataclasses import dataclass -from datetime import datetime, timezone -from typing import List, Optional, Tuple, Any +from typing import List, Optional, Any -from .types import GValue, GType, MapEntry, RefID +from .types import GValue, MapEntry # ============================================================ @@ -49,6 +49,9 @@ class Token: pos: int +DEFAULT_MAX_DEPTH = 100 + + class Lexer: """Tokenizer for GLYPH text.""" @@ -201,7 +204,7 @@ def _read_bytes(self) -> Token: self.pos += 1 b64_str = "".join(result) try: - data = base64.b64decode(b64_str) + data = base64.b64decode(b64_str, validate=True) except Exception as e: raise ValueError(f"invalid base64: {e}") return Token(TokenType.BYTES, data, start) @@ -210,6 +213,19 @@ def _read_bytes(self) -> Token: raise ValueError("unterminated bytes literal") + def _parse_float_token(self, literal: str, start: int) -> Token: + try: + value = float(literal) + except OverflowError as e: + raise ValueError(f"float literal overflow at position {start}: {e}") from e + except ValueError as e: + raise ValueError(f"invalid float literal {literal!r} at position {start}") from e + + if not math.isfinite(value): + raise ValueError(f"non-finite float literal {literal!r} at position {start}") + + return Token(TokenType.FLOAT, value, start) + def _read_number_or_ident(self) -> Token: start = self.pos result = [] @@ -220,8 +236,7 @@ def _read_number_or_ident(self) -> Token: # Check for -Inf (with word boundary: next char must not be alphanumeric/underscore) if (self.pos + 2 < self.length and self.text[self.pos:self.pos+3] == "Inf" and (self.pos + 3 >= self.length or (not self.text[self.pos+3].isalnum() and self.text[self.pos+3] != '_'))): - self.pos += 3 - return Token(TokenType.FLOAT, float("-inf"), start) + raise ValueError(f"non-finite float literal '-Inf' at position {start}") # Read digits and decimal point has_dot = False @@ -251,7 +266,7 @@ def _read_number_or_ident(self) -> Token: # Determine if it's int or float if has_dot or has_exp: - return Token(TokenType.FLOAT, float(s), start) + return self._parse_float_token(s, start) else: try: return Token(TokenType.INT, int(s), start) @@ -279,9 +294,9 @@ def _read_ident(self) -> Token: if s == "null" or s == "nil": return Token(TokenType.NULL, None, start) if s == "NaN": - return Token(TokenType.FLOAT, float("nan"), start) + raise ValueError(f"non-finite float literal 'NaN' at position {start}") if s == "Inf": - return Token(TokenType.FLOAT, float("inf"), start) + raise ValueError(f"non-finite float literal 'Inf' at position {start}") return Token(TokenType.IDENT, s, start) @@ -293,11 +308,23 @@ def _read_ident(self) -> Token: class Parser: """Recursive descent parser for GLYPH text.""" - def __init__(self, text: str): + def __init__(self, text: str, max_depth: int = DEFAULT_MAX_DEPTH, nesting_depth: int = 0): self.lexer = Lexer(text) self.peeked: Optional[Token] = None + self.max_depth = max_depth + self.depth = nesting_depth # Don't read initial token here - let parse() do it + @contextmanager + def _container(self, kind: str): + if self.depth >= self.max_depth: + raise ValueError(f"maximum nesting depth exceeded while parsing {kind}") + self.depth += 1 + try: + yield + finally: + self.depth -= 1 + def peek(self) -> Token: if self.peeked is None: self.peeked = self.lexer.next_token() @@ -323,6 +350,10 @@ def parse(self) -> GValue: self.lexer.skip_whitespace_and_newlines() self.current = self.lexer.next_token() v = self._parse_value() + while self.current.type == TokenType.NEWLINE: + self.advance() + if self.current.type != TokenType.EOF: + raise ValueError(f"trailing garbage at position {self.current.pos}") return v def _parse_value(self) -> GValue: @@ -421,73 +452,34 @@ def _parse_ref(self) -> GValue: def _parse_list(self) -> GValue: """Parse a list [...] """ - self.expect(TokenType.LBRACKET) - items = [] + with self._container("list"): + self.expect(TokenType.LBRACKET) + items = [] - while self.current.type != TokenType.RBRACKET: - if self.current.type == TokenType.EOF: - raise ValueError("unterminated list") - if self.current.type == TokenType.COMMA: - self.advance() - continue - if self.current.type == TokenType.NEWLINE: - self.advance() - continue + while self.current.type != TokenType.RBRACKET: + if self.current.type == TokenType.EOF: + raise ValueError("unterminated list") + if self.current.type == TokenType.COMMA: + self.advance() + continue + if self.current.type == TokenType.NEWLINE: + self.advance() + continue - items.append(self._parse_value()) + items.append(self._parse_value()) - self.expect(TokenType.RBRACKET) - return GValue.list_(*items) + self.expect(TokenType.RBRACKET) + return GValue.list_(*items) def _parse_map(self) -> GValue: """Parse a map {...}""" - self.expect(TokenType.LBRACE) - entries = [] - - while self.current.type != TokenType.RBRACE: - if self.current.type == TokenType.EOF: - raise ValueError("unterminated map") - if self.current.type == TokenType.COMMA: - self.advance() - continue - if self.current.type == TokenType.NEWLINE: - self.advance() - continue - - # Parse key - if self.current.type == TokenType.IDENT: - key = self.current.value - self.advance() - elif self.current.type == TokenType.STRING: - key = self.current.value - self.advance() - else: - raise ValueError(f"expected key, got {self.current.type}") - - # Expect = or : - if self.current.type in (TokenType.EQUALS, TokenType.COLON): - self.advance() - - # Parse value - value = self._parse_value() - entries.append(MapEntry(key, value)) - - self.expect(TokenType.RBRACE) - return GValue.map_(*entries) - - def _parse_ident_value(self) -> GValue: - """Parse an identifier which could be a bare string, struct, or sum.""" - name = self.current.value - self.advance() - - if self.current.type == TokenType.LBRACE: - # Struct: Name{...} - self.advance() - fields = [] + with self._container("map"): + self.expect(TokenType.LBRACE) + entries = [] while self.current.type != TokenType.RBRACE: if self.current.type == TokenType.EOF: - raise ValueError("unterminated struct") + raise ValueError("unterminated map") if self.current.type == TokenType.COMMA: self.advance() continue @@ -495,7 +487,7 @@ def _parse_ident_value(self) -> GValue: self.advance() continue - # Parse field + # Parse key if self.current.type == TokenType.IDENT: key = self.current.value self.advance() @@ -503,28 +495,71 @@ def _parse_ident_value(self) -> GValue: key = self.current.value self.advance() else: - raise ValueError(f"expected field name, got {self.current.type}") + raise ValueError(f"expected key, got {self.current.type}") - if self.current.type in (TokenType.EQUALS, TokenType.COLON): - self.advance() + if self.current.type not in (TokenType.EQUALS, TokenType.COLON): + raise ValueError(f"expected '=' or ':' after key {key!r}") + self.advance() value = self._parse_value() - fields.append(MapEntry(key, value)) + entries.append(MapEntry(key, value)) self.expect(TokenType.RBRACE) - return GValue.struct(name, *fields) + return GValue.map_(*entries) + + def _parse_ident_value(self) -> GValue: + """Parse an identifier which could be a bare string, struct, or sum.""" + name = self.current.value + self.advance() + + if self.current.type == TokenType.LBRACE: + # Struct: Name{...} + with self._container("struct"): + self.advance() + fields = [] + + while self.current.type != TokenType.RBRACE: + if self.current.type == TokenType.EOF: + raise ValueError("unterminated struct") + if self.current.type == TokenType.COMMA: + self.advance() + continue + if self.current.type == TokenType.NEWLINE: + self.advance() + continue + + # Parse field + if self.current.type == TokenType.IDENT: + key = self.current.value + self.advance() + elif self.current.type == TokenType.STRING: + key = self.current.value + self.advance() + else: + raise ValueError(f"expected field name, got {self.current.type}") + + if self.current.type not in (TokenType.EQUALS, TokenType.COLON): + raise ValueError(f"expected '=' or ':' after field {key!r}") + self.advance() + + value = self._parse_value() + fields.append(MapEntry(key, value)) + + self.expect(TokenType.RBRACE) + return GValue.struct(name, *fields) if self.current.type == TokenType.LPAREN: # Sum: Tag(value) or Tag() - self.advance() - - if self.current.type == TokenType.RPAREN: + with self._container("sum"): self.advance() - return GValue.sum(name, None) - value = self._parse_value() - self.expect(TokenType.RPAREN) - return GValue.sum(name, value) + if self.current.type == TokenType.RPAREN: + self.advance() + return GValue.sum(name, None) + + value = self._parse_value() + self.expect(TokenType.RPAREN) + return GValue.sum(name, value) # Bare string return GValue.str_(name) @@ -546,57 +581,58 @@ def _parse_directive(self) -> GValue: def _parse_tabular(self) -> GValue: """Parse tabular format: @tab _ [cols] |row|... @end""" - # Skip the _ placeholder - if self.current.type == TokenType.IDENT and self.current.value == "_": - self.advance() - elif self.current.type == TokenType.NULL: - self.advance() - - # Parse column headers - if self.current.type != TokenType.LBRACKET: - raise ValueError("expected [ for column headers") - - self.advance() - cols = [] - while self.current.type != TokenType.RBRACKET: - if self.current.type == TokenType.IDENT: - cols.append(self.current.value) - self.advance() - elif self.current.type == TokenType.STRING: - cols.append(self.current.value) + with self._container("tabular directive"): + # Skip the _ placeholder + if self.current.type == TokenType.IDENT and self.current.value == "_": self.advance() - elif self.current.type == TokenType.COMMA: + elif self.current.type == TokenType.NULL: self.advance() - elif self.current.type == TokenType.NEWLINE: - self.advance() - else: - raise ValueError(f"expected column name, got {self.current.type}") - self.expect(TokenType.RBRACKET) + # Parse column headers + if self.current.type != TokenType.LBRACKET: + raise ValueError("expected [ for column headers") - # Parse rows - rows = [] - while True: - # Skip newlines - while self.current.type == TokenType.NEWLINE: - self.advance() + self.advance() + cols = [] + while self.current.type != TokenType.RBRACKET: + if self.current.type == TokenType.IDENT: + cols.append(self.current.value) + self.advance() + elif self.current.type == TokenType.STRING: + cols.append(self.current.value) + self.advance() + elif self.current.type == TokenType.COMMA: + self.advance() + elif self.current.type == TokenType.NEWLINE: + self.advance() + else: + raise ValueError(f"expected column name, got {self.current.type}") - if self.current.type == TokenType.AT: - self.advance() - if self.current.type == TokenType.IDENT and self.current.value == "end": + self.expect(TokenType.RBRACKET) + + # Parse rows + rows = [] + while True: + # Skip newlines + while self.current.type == TokenType.NEWLINE: self.advance() - break - raise ValueError("expected @end") - if self.current.type == TokenType.PIPE: - row = self._parse_tabular_row(cols) - rows.append(row) - elif self.current.type == TokenType.EOF: - break - else: - raise ValueError(f"expected row or @end, got {self.current.type}") + if self.current.type == TokenType.AT: + self.advance() + if self.current.type == TokenType.IDENT and self.current.value == "end": + self.advance() + break + raise ValueError("expected @end") + + if self.current.type == TokenType.PIPE: + row = self._parse_tabular_row(cols) + rows.append(row) + elif self.current.type == TokenType.EOF: + break + else: + raise ValueError(f"expected row or @end, got {self.current.type}") - return GValue.list_(*rows) + return GValue.list_(*rows) def _parse_tabular_row(self, cols: List[str]) -> GValue: """Parse a single tabular row: |val|val|val|""" @@ -644,7 +680,11 @@ def _parse_tabular_row(self, cols: List[str]) -> GValue: if cell_text == "" or cell_text == "∅" or cell_text == "_": value = GValue.null() else: - cell_parser = Parser(cell_text) + cell_parser = Parser( + cell_text, + max_depth=self.max_depth, + nesting_depth=self.depth, + ) value = cell_parser.parse() entries.append(MapEntry(col, value)) diff --git a/py/glyph/stream_validator.py b/py/glyph/stream_validator.py index 72cc2c9..bf9fe8f 100644 --- a/py/glyph/stream_validator.py +++ b/py/glyph/stream_validator.py @@ -28,6 +28,7 @@ ... return result.parsed() """ +import math import re import time from typing import Dict, List, Optional, Any, Pattern @@ -105,6 +106,8 @@ def validate(self, value: Any) -> Optional[str]: elif self.type == ArgType.FLOAT: if not isinstance(value, (int, float)) or isinstance(value, bool): return f"Argument {self.name} must be float" + if isinstance(value, float) and not math.isfinite(value): + return f"Argument {self.name} must be finite" if self.min is not None and value < self.min: return f"Argument {self.name} < min {self.min}" @@ -211,6 +214,14 @@ class ValidatorState(Enum): ERROR = "error" # Unrecoverable error +DEFAULT_MAX_BUFFER = 1024 * 1024 +DEFAULT_MAX_FIELDS = 1000 +DEFAULT_MAX_ERRORS = 100 + +OPEN_CONTAINERS = {"{", "[", "("} +CLOSE_CONTAINERS = {"}": "{", "]": "[", ")": "("} + + @dataclass class TimelineEvent: """A significant event during validation.""" @@ -246,6 +257,8 @@ class StreamValidationResult: # Timeline timeline: List[TimelineEvent] = dataclass_field(default_factory=list) + _tool_allowed: bool = True + _tool_finalized: bool = False @property def should_cancel(self) -> bool: @@ -255,10 +268,9 @@ def should_cancel(self) -> bool: @property def tool_allowed(self) -> bool: """True if detected tool is registered.""" - # Check if first error is UNKNOWN_TOOL - if self.errors: - return "unknown tool" not in self.errors[0].lower() - return True + if not self._tool_finalized: + return True + return self._tool_allowed class StreamingValidator: @@ -276,21 +288,34 @@ class StreamingValidator: return result.fields """ - def __init__(self, registry: ToolRegistry): + def __init__( + self, + registry: ToolRegistry, + max_buffer: int = DEFAULT_MAX_BUFFER, + max_fields: int = DEFAULT_MAX_FIELDS, + max_errors: int = DEFAULT_MAX_ERRORS, + ): self.registry = registry + self.max_buffer = max_buffer + self.max_fields = max_fields + self.max_errors = max_errors self.reset() def reset(self): """Reset validator state for reuse.""" self.buffer = "" + self.buffer_size = 0 self.state = ValidatorState.WAITING self.depth = 0 + self.container_stack: List[str] = [] self.in_string = False self.escape_next = False self.current_key = "" self.current_val = "" self.has_key = False + self.pending_key_separator = False self.tool_name = "" + self.tool_finalized = False self.fields: Dict[str, Any] = {} self.errors: List[str] = [] @@ -334,149 +359,265 @@ def push_token(self, token: str) -> StreamValidationResult: def _process_char(self, char: str): """Process a single character.""" + if self.state == ValidatorState.ERROR: + return + + char_size = len(char.encode("utf-8")) + if self.buffer_size + char_size > self.max_buffer: + self._add_error( + f"Buffer exceeds max size of {self.max_buffer} bytes", + fatal=True, + ) + return + self.buffer += char + self.buffer_size += char_size + + if self.state == ValidatorState.COMPLETE: + if char not in (" ", "\n", "\t", "\r"): + self._add_error("Trailing characters after complete tool call", fatal=True) + return # Handle escape sequences if self.escape_next: self.escape_next = False - self.current_val += char + self._append_current(char) return if char == '\\' and self.in_string: self.escape_next = True - self.current_val += char + self._append_current(char) return # Handle quotes if char == '"': - if self.in_string: - self.in_string = False - else: - self.in_string = True - self.current_val = "" + self._append_current(char) + self.in_string = not self.in_string return # Inside string - accumulate if self.in_string: - self.current_val += char + self._append_current(char) return - - # Handle structural characters - if char == '{': - if self.state == ValidatorState.WAITING: - # Capture tool name before entering object - if self.current_val.strip(): - self.tool_name = self.current_val.strip() - self.current_val = "" - # Check if tool is allowed - if not self.registry.is_allowed(self.tool_name): - self.errors.append(f"UNKNOWN_TOOL: {self.tool_name}") + + if self.state == ValidatorState.WAITING: + if char == "{": + self._finalize_tool_name() + if self.state == ValidatorState.ERROR: + return self.state = ValidatorState.IN_OBJECT - self.depth += 1 - - elif char == '}': - self.depth -= 1 - if self.depth == 0: - self._finish_field() - self.state = ValidatorState.COMPLETE - self._validate_complete() - - elif char == '[': - self.depth += 1 - self.current_val += char - - elif char == ']': - self.depth -= 1 - self.current_val += char - - elif char == '=': - if self.depth == 1 and not self.has_key: - self.current_key = self.current_val.strip() + self.container_stack.append("{") + self.depth = len(self.container_stack) + return + self._append_current(char) + return + + if self.state != ValidatorState.IN_OBJECT: + return + + root_level = len(self.container_stack) == 1 + + if root_level and not self.has_key: + if char in (" ", "\n", "\t", "\r"): + if self.current_val.strip(): + self.pending_key_separator = True + return + if char == ",": + if self.current_val.strip(): + self._add_error("Expected '=' or ':' after field name", fatal=True) + return + if char in ("=", ":"): + key_text = self.current_val.strip() + if not key_text: + self._add_error("Missing field name before separator", fatal=True) + return + try: + self.current_key = self._parse_key(key_text) + except ValueError as exc: + self._add_error(f"Invalid field name: {exc}", fatal=True) + return self.current_val = "" self.has_key = True - else: - self.current_val += char - - elif char in (' ', '\n', '\t', '\r'): - if self.depth == 1 and self.has_key and self.current_val: + self.pending_key_separator = False + return + if char == "}" and not self.current_val.strip(): + self.container_stack.pop() + self.depth = len(self.container_stack) + self.state = ValidatorState.COMPLETE + self._validate_complete() + return + if self.pending_key_separator: + self._add_error("Expected '=' or ':' after field name", fatal=True) + return + self._append_current(char) + return + + if root_level and self.has_key: + if char in (" ", "\n", "\t", "\r"): + if self.current_val.strip(): + self._finish_field() + return + if char == ",": + if not self.current_val.strip(): + self._add_error(f"Missing value for field: {self.current_key}", fatal=True) + return self._finish_field() - + return + + if char in OPEN_CONTAINERS: + self.container_stack.append(char) + self.depth = len(self.container_stack) + if self.has_key: + self._append_current(char) + return + + if char in CLOSE_CONTAINERS: + expected_open = CLOSE_CONTAINERS[char] + if not self.container_stack: + self._add_error(f"Unexpected closing delimiter: {char}", fatal=True) + self.depth = 0 + return + if self.container_stack[-1] != expected_open: + self._add_error( + f"Mismatched closing delimiter: expected {expected_open!r} before {char!r}", + fatal=True, + ) + return + + self.container_stack.pop() + self.depth = len(self.container_stack) + + if self.depth == 0: + if self.has_key: + if self.current_val.strip(): + self._finish_field() + else: + self._add_error(f"Missing value for field: {self.current_key}", fatal=True) + elif self.current_val.strip(): + self._add_error("Expected '=' or ':' after field name", fatal=True) + + if self.state != ValidatorState.ERROR: + self.state = ValidatorState.COMPLETE + self._validate_complete() + return + + if self.has_key: + self._append_current(char) + return + + if self.has_key: + self._append_current(char) else: - if self.state == ValidatorState.WAITING and self.depth == 0: - # Accumulating tool name - self.current_val += char - elif self.depth == 1: - self.current_val += char - + self._append_current(char) + + def _append_current(self, char: str): + """Append raw input to the current token/value buffer.""" + self.current_val += char + + def _add_error(self, message: str, fatal: bool = False): + """Record an error without allowing unbounded growth.""" + if message in self.errors: + if fatal: + self.state = ValidatorState.ERROR + return + + if len(self.errors) >= self.max_errors: + self.state = ValidatorState.ERROR + return + + self.errors.append(message) + if fatal or len(self.errors) >= self.max_errors: + self.state = ValidatorState.ERROR + + def _finalize_tool_name(self): + """Freeze the tool name once the opening brace arrives.""" + tool_name = self.current_val.strip() + self.current_val = "" + if not tool_name: + self._add_error("No tool name found", fatal=True) + return + + self.tool_name = tool_name + self.tool_finalized = True + if not self.registry.is_allowed(self.tool_name): + self._add_error(f"UNKNOWN_TOOL: {self.tool_name}") + + def _parse_key(self, key_str: str) -> str: + """Parse a field name using the GLYPH parser.""" + from .parse import parse_loose + from .types import GType + + key = parse_loose(key_str) + if key.type != GType.STR: + raise ValueError("field names must parse to strings") + return key.as_str() + def _finish_field(self): """Finish parsing a field and add to fields dict.""" if not self.has_key: return - # Parse field value - value = self._parse_value(self.current_val.strip()) - if self.current_key: - self.fields[self.current_key] = value + try: + value_text = self.current_val.strip() + if not value_text: + self._add_error(f"Missing value for field: {self.current_key}", fatal=True) + return - # Validate against schema if available - if self.tool_name: - tool_schema = self.registry.get_tool(self.tool_name) - if tool_schema and self.current_key in tool_schema.args: - arg_schema = tool_schema.args[self.current_key] - error = arg_schema.validate(value) - if error and error not in self.errors: - self.errors.append(error) + value = self._parse_value(value_text) - self.current_key = "" - self.current_val = "" - self.has_key = False + if self.current_key: + if self.current_key not in self.fields and len(self.fields) >= self.max_fields: + self._add_error( + f"Field count exceeds max of {self.max_fields}", + fatal=True, + ) + return + + self.fields[self.current_key] = value + + # Validate against schema if available + if self.tool_name: + tool_schema = self.registry.get_tool(self.tool_name) + if tool_schema and self.current_key in tool_schema.args: + arg_schema = tool_schema.args[self.current_key] + error = arg_schema.validate(value) + if error: + self._add_error(error) + except ValueError as exc: + self._add_error(f"Invalid value for field {self.current_key}: {exc}", fatal=True) + finally: + self.current_key = "" + self.current_val = "" + self.has_key = False + self.pending_key_separator = False def _parse_value(self, val_str: str) -> Any: - """Parse a simple value string.""" + """Parse a GLYPH value and project it to Python/JSON-friendly types.""" + from .loose import to_json_loose + from .parse import parse_loose + if not val_str: return None - - val_str = val_str.strip() - - # Null - if val_str in ('_', '∅', 'null'): - return None - - # Bool - if val_str in ('t', 'true'): - return True - if val_str in ('f', 'false'): - return False - - # Numbers - try: - if '.' in val_str or 'e' in val_str.lower(): - return float(val_str) - return int(val_str) - except ValueError: - pass - - # String (quoted or bare) - return val_str + + return to_json_loose(parse_loose(val_str)) def _validate_complete(self): """Validate the complete tool call.""" if not self.tool_name: - self.errors.append("No tool name found") + self._add_error("No tool name found") return # Check if tool is allowed if not self.registry.is_allowed(self.tool_name): - if "UNKNOWN_TOOL" not in self.errors: - self.errors.append(f"UNKNOWN_TOOL: {self.tool_name}") + self._add_error(f"UNKNOWN_TOOL: {self.tool_name}") return # Validate against schema tool_schema = self.registry.get_tool(self.tool_name) if tool_schema: error = tool_schema.validate(self.fields) - if error and error not in self.errors: - self.errors.append(error) + if error: + self._add_error(error) def _get_result(self) -> StreamValidationResult: """Get the current validation result.""" @@ -494,7 +635,9 @@ def _get_result(self) -> StreamValidationResult: self.tool_detected_at_token = self.token_count self.tool_detected_at_char = self.char_count self.tool_detected_at_time = elapsed - allowed = self.registry.is_allowed(effective_tool_name) + allowed = True + if self.tool_finalized: + allowed = self.registry.is_allowed(effective_tool_name) self.timeline.append(TimelineEvent( event="TOOL_DETECTED", token_count=self.token_count, @@ -545,6 +688,8 @@ def _get_result(self) -> StreamValidationResult: complete_at_token=self.complete_at_token, complete_at_time=self.complete_at_time, timeline=self.timeline.copy(), + _tool_allowed=self.registry.is_allowed(effective_tool_name) if (self.tool_finalized and effective_tool_name) else True, + _tool_finalized=self.tool_finalized, ) def get_result(self) -> StreamValidationResult: diff --git a/py/glyph/types.py b/py/glyph/types.py index fc11426..0031b2b 100644 --- a/py/glyph/types.py +++ b/py/glyph/types.py @@ -9,7 +9,6 @@ from datetime import datetime from enum import Enum from typing import List, Optional, Union -import copy class GType(Enum): @@ -242,14 +241,14 @@ def as_number(self) -> Union[int, float]: raise TypeError("not a number") def get(self, key: str) -> Optional["GValue"]: - """Get field from struct or map by key.""" + """Get field from struct or map by key using last-write-wins semantics.""" if self._type == GType.STRUCT: - for f in self._struct.fields: # type: ignore + for f in reversed(self._struct.fields): # type: ignore if f.key == key: return f.value return None if self._type == GType.MAP: - for e in self._map: # type: ignore + for e in reversed(self._map): # type: ignore if e.key == key: return e.value return None @@ -280,16 +279,10 @@ def __len__(self) -> int: def set(self, key: str, value: "GValue") -> None: """Set field on struct or map.""" if self._type == GType.STRUCT: - for i, f in enumerate(self._struct.fields): # type: ignore - if f.key == key: - self._struct.fields[i].value = value # type: ignore - return + self._struct.fields = [f for f in self._struct.fields if f.key != key] # type: ignore self._struct.fields.append(MapEntry(key, value)) # type: ignore elif self._type == GType.MAP: - for i, e in enumerate(self._map): # type: ignore - if e.key == key: - self._map[i].value = value # type: ignore - return + self._map = [e for e in self._map if e.key != key] # type: ignore self._map.append(MapEntry(key, value)) # type: ignore else: raise TypeError("cannot set on non-struct/map") diff --git a/py/tests/test_glyph.py b/py/tests/test_glyph.py index 5654160..b891da0 100644 --- a/py/tests/test_glyph.py +++ b/py/tests/test_glyph.py @@ -253,6 +253,65 @@ def test_json_to_glyph(self): v = parse(text) assert v.get("action").as_str() == "search" + def test_rejects_non_finite_json_numbers(self): + with pytest.raises(ValueError, match="non-finite"): + from_json(float("inf")) + + with pytest.raises(ValueError, match="non-finite"): + to_json(GValue.float_(float("nan"))) + + +class TestParserHardening: + """Regression tests for malformed input handling.""" + + def test_rejects_trailing_garbage(self): + with pytest.raises(ValueError, match="trailing garbage"): + parse("1 2") + + with pytest.raises(ValueError, match="trailing garbage"): + parse("{a=1} junk") + + def test_requires_map_value_separator(self): + with pytest.raises(ValueError, match="expected '=' or ':' after key"): + parse("{a b}") + + def test_requires_struct_field_separator(self): + with pytest.raises(ValueError, match="expected '=' or ':' after field"): + parse("Team{name Arsenal}") + + def test_rejects_invalid_base64(self): + with pytest.raises(ValueError, match="invalid base64"): + parse('b64"@@@"') + + @pytest.mark.parametrize("text", ["NaN", "Inf", "-Inf", "1e309"]) + def test_rejects_non_finite_and_overflow_float_literals(self, text): + with pytest.raises(ValueError, match="float"): + parse(text) + + def test_limits_nesting_depth(self): + deeply_nested = "[" * 101 + "0" + "]" * 101 + with pytest.raises(ValueError, match="maximum nesting depth"): + parse(deeply_nested) + + +class TestDuplicateKeySemantics: + """Duplicate-key access should match JSON conversion semantics.""" + + def test_map_get_and_json_use_last_duplicate(self): + v = parse("{a=1 a=2}") + assert v.get("a").as_int() == 2 + assert to_json(v) == {"a": 2} + + def test_set_collapses_duplicate_keys(self): + v = GValue.map_( + MapEntry("a", GValue.int_(1)), + MapEntry("a", GValue.int_(2)), + ) + v.set("a", GValue.int_(3)) + + assert len(v.as_map()) == 1 + assert v.get("a").as_int() == 3 + class TestFingerprint: """Tests for fingerprinting.""" @@ -302,6 +361,11 @@ def test_tabular_roundtrip(self): v = parse(text) assert len(v) == 3 + def test_quotes_numeric_keyword_strings(self): + assert emit(GValue.str_("Inf")) == '"Inf"' + assert emit(GValue.str_("NaN")) == '"NaN"' + assert parse(emit(GValue.str_("Inf"))).as_str() == "Inf" + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/py/tests/test_stream_validator.py b/py/tests/test_stream_validator.py index 38612a6..6f683ec 100644 --- a/py/tests/test_stream_validator.py +++ b/py/tests/test_stream_validator.py @@ -239,5 +239,95 @@ def test_integration_with_streaming(): assert result.fields["max_results"] == 5 +def test_tool_allowed_only_after_tool_name_is_final(): + """Partial tool-name detection should not mark the tool as disallowed.""" + registry = ToolRegistry() + registry.add_tool("search", {}) + + validator = StreamingValidator(registry) + + result = validator.push_token("unknown_tool") + assert result.tool_name == "unknown_tool" + assert result.tool_allowed + + result = validator.push_token("{") + assert not result.tool_allowed + assert "UNKNOWN_TOOL" in str(result.errors) + + +def test_nested_value_parsing(): + """Nested lists and maps should survive incremental parsing.""" + registry = ToolRegistry() + registry.add_tool("batch", { + "items": {"type": "list", "required": True}, + }) + + validator = StreamingValidator(registry) + response = 'batch{items=[{id=1 tags=[alpha beta]} {id=2 tags=[]}]}' + + for char in response: + result = validator.push_token(char) + + assert result.complete + assert result.valid + assert result.fields["items"][0]["id"] == 1 + assert result.fields["items"][0]["tags"] == ["alpha", "beta"] + assert result.fields["items"][1]["tags"] == [] + + +def test_buffer_limit_trips_error(): + """The validator should stop accepting oversized payloads.""" + registry = ToolRegistry() + registry.add_tool("search", {"query": {"type": "str"}}) + + validator = StreamingValidator(registry, max_buffer=16) + result = validator.push_token('search{query="0123456789"}') + + assert result.state == ValidatorState.ERROR + assert any("Buffer exceeds max size" in error for error in result.errors) + + +def test_field_limit_trips_error(): + """The validator should cap the number of parsed fields.""" + registry = ToolRegistry() + registry.add_tool("bulk", {}) + + validator = StreamingValidator(registry, max_fields=1) + for char in "bulk{a=1 b=2}": + result = validator.push_token(char) + + assert result.state == ValidatorState.ERROR + assert any("Field count exceeds max" in error for error in result.errors) + + +def test_error_limit_is_capped(): + """Error accumulation should remain bounded.""" + registry = ToolRegistry() + registry.add_tool("search", { + "query": {"type": "str", "required": True}, + "max_results": {"type": "int", "max": 1}, + }) + + validator = StreamingValidator(registry, max_errors=1) + for char in 'search{max_results=2 extra=foo}': + result = validator.push_token(char) + + assert len(result.errors) == 1 + assert not result.valid + + +def test_mismatched_nested_delimiters_fail_fast(): + """Unexpected nested closers should not drive depth negative.""" + registry = ToolRegistry() + registry.add_tool("search", {"query": {"type": "list"}}) + + validator = StreamingValidator(registry) + for char in "search{query=[1}}": + result = validator.push_token(char) + + assert result.state == ValidatorState.ERROR + assert any("Mismatched closing delimiter" in error for error in result.errors) + + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/rust/glyph-codec/src/decimal128.rs b/rust/glyph-codec/src/decimal128.rs index 083425d..99a4a62 100644 --- a/rust/glyph-codec/src/decimal128.rs +++ b/rust/glyph-codec/src/decimal128.rs @@ -14,6 +14,8 @@ use std::fmt; use std::ops::{Add, Sub, Mul, Div, Neg}; use std::str::FromStr; +const MAX_I128_POW10_EXP: u32 = 38; + /// Decimal128 represents a 128-bit decimal number. /// Value = coefficient * 10^(-scale) #[derive(Clone, Copy, Eq)] @@ -99,8 +101,25 @@ impl Decimal128 { /// Convert to i64 (truncates fractional part). pub fn to_i64(&self) -> i64 { let coef = coef_to_int(&self.coef); - let divisor = 10i128.pow(self.scale as u32); - (coef / divisor) as i64 + let value = if self.scale >= 0 { + match checked_pow10(self.scale as u32) { + Some(divisor) => coef / divisor, + None => 0, + } + } else { + match checked_scale_coef(coef, self.scale.unsigned_abs() as u32) { + Ok(value) => value, + Err(_) => { + return if coef.is_negative() { + i64::MIN + } else { + i64::MAX + }; + } + } + }; + + saturating_i128_to_i64(value) } /// Convert to f64 (with potential precision loss). @@ -130,7 +149,7 @@ impl Decimal128 { let coef = coef_to_int(&self.coef); Self { scale: self.scale, - coef: int_to_coef(coef.abs()), + coef: int_to_coef(coef.wrapping_abs()), } } @@ -139,7 +158,7 @@ impl Decimal128 { let coef = coef_to_int(&self.coef); Self { scale: self.scale, - coef: int_to_coef(-coef), + coef: int_to_coef(coef.wrapping_neg()), } } } @@ -147,26 +166,31 @@ impl Decimal128 { impl fmt::Display for Decimal128 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let coef = coef_to_int(&self.coef); - - if self.scale == 0 { - return write!(f, "{}", coef); - } - - let negative = coef < 0; - let mut coef_str = coef.abs().to_string(); - - // Pad with zeros if needed - while coef_str.len() <= self.scale as usize { - coef_str.insert(0, '0'); + let mut digits = coef.to_string(); + let negative = digits.starts_with('-'); + if negative { + digits.remove(0); } - let insert_pos = coef_str.len() - self.scale as usize; - coef_str.insert(insert_pos, '.'); + let rendered = if self.scale > 0 { + let mut digits = digits; + while digits.len() <= self.scale as usize { + digits.insert(0, '0'); + } + let insert_pos = digits.len() - self.scale as usize; + digits.insert(insert_pos, '.'); + digits + } else if self.scale < 0 { + let zeros = "0".repeat(self.scale.unsigned_abs() as usize); + format!("{}{}", digits, zeros) + } else { + digits + }; if negative { - write!(f, "-{}", coef_str) + write!(f, "-{}", rendered) } else { - write!(f, "{}", coef_str) + write!(f, "{}", rendered) } } } @@ -191,19 +215,23 @@ impl PartialOrd for Decimal128 { impl Ord for Decimal128 { fn cmp(&self, other: &Self) -> Ordering { - let mut c1 = coef_to_int(&self.coef); - let mut c2 = coef_to_int(&other.coef); - - // Align scales - if self.scale < other.scale { - let diff = (other.scale - self.scale) as u32; - c1 *= 10i128.pow(diff); - } else if self.scale > other.scale { - let diff = (self.scale - other.scale) as u32; - c2 *= 10i128.pow(diff); + let c1 = coef_to_int(&self.coef); + let c2 = coef_to_int(&other.coef); + + if c1 == c2 && self.scale == other.scale { + return Ordering::Equal; } - c1.cmp(&c2) + if c1.is_negative() != c2.is_negative() { + return c1.cmp(&c2); + } + + let ordering = compare_abs_decimal(c1, self.scale, c2, other.scale); + if c1.is_negative() { + ordering.reverse() + } else { + ordering + } } } @@ -211,26 +239,10 @@ impl Add for Decimal128 { type Output = Result; fn add(self, other: Self) -> Self::Output { - let mut c1 = coef_to_int(&self.coef); - let mut c2 = coef_to_int(&other.coef); - let target_scale; - - if self.scale < other.scale { - let diff = (other.scale - self.scale) as u32; - c1 *= 10i128.pow(diff); - target_scale = other.scale; - } else { - let diff = (self.scale - other.scale) as u32; - c2 *= 10i128.pow(diff); - target_scale = self.scale; - } - - let result = c1 + c2; - - // Check overflow (128-bit signed max) - if result.leading_zeros() < 1 && result.leading_ones() < 1 { - return Err(DecimalError::Overflow); - } + let target_scale = self.scale.max(other.scale); + let c1 = align_coef_to_scale(coef_to_int(&self.coef), self.scale, target_scale)?; + let c2 = align_coef_to_scale(coef_to_int(&other.coef), other.scale, target_scale)?; + let result = c1.checked_add(c2).ok_or(DecimalError::Overflow)?; Ok(Self { scale: target_scale, @@ -243,7 +255,15 @@ impl Sub for Decimal128 { type Output = Result; fn sub(self, other: Self) -> Self::Output { - self + other.negate() + let target_scale = self.scale.max(other.scale); + let c1 = align_coef_to_scale(coef_to_int(&self.coef), self.scale, target_scale)?; + let c2 = align_coef_to_scale(coef_to_int(&other.coef), other.scale, target_scale)?; + let result = c1.checked_sub(c2).ok_or(DecimalError::Overflow)?; + + Ok(Self { + scale: target_scale, + coef: int_to_coef(result), + }) } } @@ -253,7 +273,7 @@ impl Mul for Decimal128 { fn mul(self, other: Self) -> Self::Output { let c1 = coef_to_int(&self.coef); let c2 = coef_to_int(&other.coef); - let result = c1 * c2; + let result = c1.checked_mul(c2).ok_or(DecimalError::Overflow)?; let new_scale = self.scale as i16 + other.scale as i16; if new_scale > 127 || new_scale < -127 { @@ -277,7 +297,7 @@ impl Div for Decimal128 { } let c1 = coef_to_int(&self.coef); - let result = c1 / c2; + let result = c1.checked_div(c2).ok_or(DecimalError::Overflow)?; let new_scale = self.scale as i16 - other.scale as i16; if new_scale > 127 || new_scale < -127 { @@ -347,6 +367,83 @@ fn coef_to_int(coef: &[u8; 16]) -> i128 { i128::from_be_bytes(*coef) } +fn checked_pow10(exp: u32) -> Option { + if exp > MAX_I128_POW10_EXP { + return None; + } + + let mut value = 1i128; + for _ in 0..exp { + value = value.checked_mul(10)?; + } + Some(value) +} + +fn checked_scale_coef(coef: i128, exp: u32) -> Result { + if exp == 0 || coef == 0 { + return Ok(coef); + } + + let factor = checked_pow10(exp).ok_or(DecimalError::Overflow)?; + coef.checked_mul(factor).ok_or(DecimalError::Overflow) +} + +fn align_coef_to_scale(coef: i128, from_scale: i8, to_scale: i8) -> Result { + let diff = i16::from(to_scale) - i16::from(from_scale); + if diff < 0 { + return Err(DecimalError::ScaleOverflow); + } + + checked_scale_coef(coef, diff as u32) +} + +fn normalize_decimal_parts(coef: i128, scale: i8) -> (String, i32) { + if coef == 0 { + return ("0".to_string(), 0); + } + + let mut digits = coef.to_string(); + if digits.starts_with('-') { + digits.remove(0); + } + let mut scale = i32::from(scale); + + while digits.len() > 1 && digits.ends_with('0') { + digits.pop(); + scale -= 1; + } + + (digits, scale) +} + +fn compare_abs_decimal(left_coef: i128, left_scale: i8, right_coef: i128, right_scale: i8) -> Ordering { + let (mut left_digits, left_scale) = normalize_decimal_parts(left_coef, left_scale); + let (mut right_digits, right_scale) = normalize_decimal_parts(right_coef, right_scale); + + let target_scale = left_scale.max(right_scale); + if target_scale > left_scale { + left_digits.push_str(&"0".repeat((target_scale - left_scale) as usize)); + } + if target_scale > right_scale { + right_digits.push_str(&"0".repeat((target_scale - right_scale) as usize)); + } + + left_digits + .len() + .cmp(&right_digits.len()) + .then_with(|| left_digits.cmp(&right_digits)) +} + +fn saturating_i128_to_i64(value: i128) -> i64 { + if value > i64::MAX as i128 { + i64::MAX + } else if value < i64::MIN as i128 { + i64::MIN + } else { + value as i64 + } +} + /// Check if a string is a decimal literal (ends with 'm'). pub fn is_decimal_literal(s: &str) -> bool { let s = s.trim(); @@ -398,6 +495,17 @@ mod tests { assert_eq!(d.to_string(), "0.0001"); } + #[test] + fn test_negative_scale_display_and_conversion() { + let d = Decimal128::new(-2, int_to_coef(123)); + assert_eq!(d.to_string(), "12300"); + assert_eq!(d.to_i64(), 12300); + + let neg = Decimal128::new(-3, int_to_coef(-45)); + assert_eq!(neg.to_string(), "-45000"); + assert_eq!(neg.to_i64(), -45000); + } + #[test] fn test_arithmetic() { let d1 = Decimal128::from_string("100.50").unwrap(); @@ -442,4 +550,28 @@ mod tests { let d = parse_decimal_literal("99.99m").unwrap(); assert_eq!(d.to_string(), "99.99"); } + + #[test] + fn test_add_overflow_returns_error() { + let max = Decimal128::new(0, int_to_coef(i128::MAX)); + let one = Decimal128::from_i64(1); + + assert_eq!(max + one, Err(DecimalError::Overflow)); + } + + #[test] + fn test_mul_overflow_returns_error() { + let huge = Decimal128::new(0, int_to_coef(i128::MAX)); + let two = Decimal128::from_i64(2); + + assert_eq!(huge * two, Err(DecimalError::Overflow)); + } + + #[test] + fn test_compare_negative_scale() { + let lhs = Decimal128::new(-2, int_to_coef(123)); + let rhs = Decimal128::from_i64(12299); + + assert!(lhs > rhs); + } } diff --git a/rust/glyph-codec/src/error.rs b/rust/glyph-codec/src/error.rs index 9b493eb..c939026 100644 --- a/rust/glyph-codec/src/error.rs +++ b/rust/glyph-codec/src/error.rs @@ -22,6 +22,9 @@ pub enum GlyphError { #[error("Missing required field: {0}")] MissingField(String), + + #[error("Recursion limit exceeded: {limit}")] + RecursionLimitExceeded { limit: usize }, } pub type Result = std::result::Result; diff --git a/rust/glyph-codec/src/json_bridge.rs b/rust/glyph-codec/src/json_bridge.rs index 9396821..ed2baa4 100644 --- a/rust/glyph-codec/src/json_bridge.rs +++ b/rust/glyph-codec/src/json_bridge.rs @@ -4,9 +4,25 @@ use crate::types::*; use crate::error::*; use serde_json::{Value as JsonValue, Number, Map}; +pub const MAX_JSON_DEPTH: usize = 128; + /// Convert JSON value to GValue pub fn from_json(json: &JsonValue) -> GValue { - match json { + try_from_json(json).unwrap_or(GValue::Null) +} + +pub fn try_from_json(json: &JsonValue) -> Result { + from_json_with_depth(json, 0) +} + +fn from_json_with_depth(json: &JsonValue, depth: usize) -> Result { + if depth > MAX_JSON_DEPTH { + return Err(GlyphError::RecursionLimitExceeded { + limit: MAX_JSON_DEPTH, + }); + } + + Ok(match json { JsonValue::Null => GValue::Null, JsonValue::Bool(b) => GValue::Bool(*b), JsonValue::Number(n) => { @@ -20,21 +36,39 @@ pub fn from_json(json: &JsonValue) -> GValue { } JsonValue::String(s) => GValue::Str(s.clone()), JsonValue::Array(arr) => { - GValue::List(arr.iter().map(from_json).collect()) + let mut items = Vec::with_capacity(arr.len()); + for item in arr { + items.push(from_json_with_depth(item, depth + 1)?); + } + GValue::List(items) } JsonValue::Object(obj) => { - let entries: Vec = obj - .iter() - .map(|(k, v)| MapEntry::new(k.clone(), from_json(v))) - .collect(); + let mut entries = Vec::with_capacity(obj.len()); + for (k, v) in obj { + entries.push(MapEntry::new(k.clone(), from_json_with_depth(v, depth + 1)?)); + } GValue::Map(entries) } - } + }) } /// Convert GValue to JSON value pub fn to_json(gv: &GValue) -> JsonValue { - match gv { + try_to_json(gv).unwrap_or(JsonValue::Null) +} + +pub fn try_to_json(gv: &GValue) -> Result { + to_json_with_depth(gv, 0) +} + +fn to_json_with_depth(gv: &GValue, depth: usize) -> Result { + if depth > MAX_JSON_DEPTH { + return Err(GlyphError::RecursionLimitExceeded { + limit: MAX_JSON_DEPTH, + }); + } + + Ok(match gv { GValue::Null => JsonValue::Null, GValue::Bool(b) => JsonValue::Bool(*b), GValue::Int(n) => JsonValue::Number(Number::from(*n)), @@ -57,19 +91,23 @@ pub fn to_json(gv: &GValue) -> JsonValue { } } GValue::List(items) => { - JsonValue::Array(items.iter().map(to_json).collect()) + let mut json = Vec::with_capacity(items.len()); + for item in items { + json.push(to_json_with_depth(item, depth + 1)?); + } + JsonValue::Array(json) } GValue::Map(entries) => { let mut map = Map::new(); for entry in entries { - map.insert(entry.key.clone(), to_json(&entry.value)); + map.insert(entry.key.clone(), to_json_with_depth(&entry.value, depth + 1)?); } JsonValue::Object(map) } GValue::Struct(s) => { let mut map = Map::new(); for field in &s.fields { - map.insert(field.key.clone(), to_json(&field.value)); + map.insert(field.key.clone(), to_json_with_depth(&field.value, depth + 1)?); } // Include type name as special field map.insert("_type".to_string(), JsonValue::String(s.type_name.clone())); @@ -79,27 +117,33 @@ pub fn to_json(gv: &GValue) -> JsonValue { let mut map = Map::new(); map.insert("_tag".to_string(), JsonValue::String(s.tag.clone())); if let Some(ref value) = s.value { - map.insert("_value".to_string(), to_json(value)); + map.insert("_value".to_string(), to_json_with_depth(value, depth + 1)?); } JsonValue::Object(map) } - } + }) } /// Parse JSON string to GValue pub fn parse_json(json_str: &str) -> Result { let json: JsonValue = serde_json::from_str(json_str)?; - Ok(from_json(&json)) + try_from_json(&json) } /// Stringify GValue to JSON string pub fn stringify_json(gv: &GValue) -> String { - serde_json::to_string(&to_json(gv)).unwrap_or_default() + try_to_json(gv) + .ok() + .and_then(|json| serde_json::to_string(&json).ok()) + .unwrap_or_default() } /// Stringify GValue to pretty JSON string pub fn stringify_json_pretty(gv: &GValue) -> String { - serde_json::to_string_pretty(&to_json(gv)).unwrap_or_default() + try_to_json(gv) + .ok() + .and_then(|json| serde_json::to_string_pretty(&json).ok()) + .unwrap_or_default() } #[cfg(test)] @@ -166,4 +210,30 @@ mod tests { assert_eq!(original, restored); } + + #[test] + fn test_try_from_json_recursion_guard() { + let mut json = json!(null); + for _ in 0..=MAX_JSON_DEPTH { + json = JsonValue::Array(vec![json]); + } + + assert!(matches!( + try_from_json(&json), + Err(GlyphError::RecursionLimitExceeded { limit: MAX_JSON_DEPTH }) + )); + } + + #[test] + fn test_try_to_json_recursion_guard() { + let mut value = GValue::Null; + for _ in 0..=MAX_JSON_DEPTH { + value = GValue::List(vec![value]); + } + + assert!(matches!( + try_to_json(&value), + Err(GlyphError::RecursionLimitExceeded { limit: MAX_JSON_DEPTH }) + )); + } } diff --git a/rust/glyph-codec/src/schema_evolution.rs b/rust/glyph-codec/src/schema_evolution.rs index 0bf51cc..995e247 100644 --- a/rust/glyph-codec/src/schema_evolution.rs +++ b/rust/glyph-codec/src/schema_evolution.rs @@ -260,6 +260,16 @@ impl VersionSchema { Ok(()) } + + fn unknown_fields(&self, data: &HashMap) -> Vec { + let mut unknown: Vec = data + .keys() + .filter(|name| !self.fields.contains_key(*name)) + .cloned() + .collect(); + unknown.sort(); + unknown + } } // ============================================================ @@ -322,6 +332,15 @@ impl VersionedSchema { // Validate in strict mode if self.mode == EvolutionMode::Strict { schema.validate(&data)?; + let unknown_fields = schema.unknown_fields(&data); + if !unknown_fields.is_empty() { + let noun = if unknown_fields.len() == 1 { + "unknown field" + } else { + "unknown fields" + }; + return Err(format!("{}: {}", noun, unknown_fields.join(", "))); + } } let mut result = data.clone(); @@ -617,4 +636,21 @@ mod tests { fn test_format_version_header() { assert_eq!(format_version_header("2.0"), "@version 2.0"); } + + #[test] + fn test_strict_mode_rejects_unknown_fields() { + let mut schema = VersionedSchema::new("Match").with_mode(EvolutionMode::Strict); + schema.add_version("1.0", vec![ + EvolvingField::new("home", FieldType::Str).required(), + EvolvingField::new("away", FieldType::Str).required(), + ]); + + let mut data = HashMap::new(); + data.insert("home".to_string(), FieldValue::Str("Arsenal".to_string())); + data.insert("away".to_string(), FieldValue::Str("Liverpool".to_string())); + data.insert("venue".to_string(), FieldValue::Str("Emirates".to_string())); + + let err = schema.parse(data, "1.0").unwrap_err(); + assert_eq!(err, "unknown field: venue"); + } } diff --git a/rust/glyph-codec/src/stream_validator.rs b/rust/glyph-codec/src/stream_validator.rs index a711b05..da6f3df 100644 --- a/rust/glyph-codec/src/stream_validator.rs +++ b/rust/glyph-codec/src/stream_validator.rs @@ -147,8 +147,14 @@ pub enum ErrorCode { ConstraintPattern, ConstraintEnum, InvalidType, + LimitExceeded, + InvalidStructure, } +pub const DEFAULT_MAX_BUFFER: usize = 1 << 20; // 1 MB +pub const DEFAULT_MAX_FIELDS: usize = 1000; +pub const DEFAULT_MAX_ERRORS: usize = 100; + /// Validation error. #[derive(Debug, Clone)] pub struct ValidationError { @@ -231,10 +237,15 @@ pub struct StreamingValidator { // Timeline timeline: Vec, + + // Hard limits + max_buffer_size: usize, + max_field_count: usize, + max_error_count: usize, } /// Field value during parsing. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum FieldValue { Null, Bool(bool), @@ -269,9 +280,37 @@ impl StreamingValidator { complete_at_token: 0, complete_at_time: Duration::ZERO, timeline: Vec::new(), + max_buffer_size: DEFAULT_MAX_BUFFER, + max_field_count: DEFAULT_MAX_FIELDS, + max_error_count: DEFAULT_MAX_ERRORS, } } + pub fn with_limits( + mut self, + max_buffer_size: usize, + max_field_count: usize, + max_error_count: usize, + ) -> Self { + if max_buffer_size > 0 { + self.max_buffer_size = max_buffer_size; + } + if max_field_count > 0 { + self.max_field_count = max_field_count; + } + if max_error_count > 0 { + self.max_error_count = max_error_count; + } + self + } + + fn add_error(&mut self, error: ValidationError) { + if self.errors.len() >= self.max_error_count { + return; + } + self.errors.push(error); + } + /// Reset the validator for reuse. pub fn reset(&mut self) { self.buffer.clear(); @@ -313,6 +352,9 @@ impl StreamingValidator { for c in token.chars() { self.char_count += 1; self.process_char(c); + if self.state == ValidatorState::Error { + break; + } } let elapsed = self.start_time.map(|t| t.elapsed()).unwrap_or(Duration::ZERO); @@ -365,6 +407,19 @@ impl StreamingValidator { } fn process_char(&mut self, c: char) { + if matches!(self.state, ValidatorState::Complete | ValidatorState::Error) { + return; + } + + if self.buffer.len() >= self.max_buffer_size { + self.state = ValidatorState::Error; + self.add_error(ValidationError::new( + ErrorCode::LimitExceeded, + "Buffer size limit exceeded", + )); + return; + } + self.buffer.push(c); // Handle escape sequences @@ -402,15 +457,30 @@ impl StreamingValidator { '{' => { if self.state == ValidatorState::Waiting { self.state = ValidatorState::InObject; + } else if self.depth >= 1 { + self.current_val.push(c); } self.depth += 1; } '}' => { + if self.depth == 0 { + self.state = ValidatorState::Error; + self.add_error(ValidationError::new( + ErrorCode::InvalidStructure, + "Unexpected closing brace", + )); + return; + } + if self.depth > 1 { + self.current_val.push(c); + } self.depth -= 1; if self.depth == 0 { self.finish_field(); - self.state = ValidatorState::Complete; - self.validate_complete(); + if self.state != ValidatorState::Error { + self.state = ValidatorState::Complete; + self.validate_complete(); + } } } '[' => { @@ -418,6 +488,14 @@ impl StreamingValidator { self.current_val.push(c); } ']' => { + if self.depth == 0 { + self.state = ValidatorState::Error; + self.add_error(ValidationError::new( + ErrorCode::InvalidStructure, + "Unexpected closing bracket", + )); + return; + } self.depth -= 1; self.current_val.push(c); } @@ -451,26 +529,40 @@ impl StreamingValidator { self.current_val.clear(); self.has_key = false; + if !self.fields.contains_key(&key) && self.fields.len() >= self.max_field_count { + self.state = ValidatorState::Error; + self.add_error(ValidationError::new( + ErrorCode::LimitExceeded, + "Field count limit exceeded", + )); + return; + } + let value = self.parse_value(&val_str); // Check for tool/action field if key == "action" || key == "tool" { if let FieldValue::Str(ref s) = value { + let discovered_now = self.tool_name.is_none(); self.tool_name = Some(s.clone()); // Validate against allow list if !self.registry.is_allowed(s) { - self.errors.push( + self.add_error( ValidationError::new(ErrorCode::UnknownTool, &format!("Unknown tool: {}", s)) .with_field(&key) ); } + + if discovered_now { + self.validate_existing_fields(s); + } } } // Validate field constraints - if let Some(ref tool_name) = self.tool_name.clone() { - self.validate_field(&key, &value, tool_name); + if let Some(tool_name) = self.tool_name.clone() { + self.validate_field(&key, &value, &tool_name); } self.fields.insert(key, value); @@ -514,11 +606,27 @@ impl StreamingValidator { None => return, }; - let arg_schema = match schema.args.get(key) { + let arg_schema = match schema.args.get(key).cloned() { Some(s) => s, None => return, }; + if !Self::type_matches(&arg_schema, value) { + self.add_error( + ValidationError::new( + ErrorCode::InvalidType, + &format!( + "{} expected {} but got {}", + key, + arg_schema.arg_type, + Self::field_type_name(value), + ), + ) + .with_field(key), + ); + return; + } + // Numeric constraints if let Some(num) = match value { FieldValue::Int(i) => Some(*i as f64), @@ -527,7 +635,7 @@ impl StreamingValidator { } { if let Some(min) = arg_schema.min { if num < min { - self.errors.push( + self.add_error( ValidationError::new(ErrorCode::ConstraintMin, &format!("{} < {}", key, min)) .with_field(key) ); @@ -535,7 +643,7 @@ impl StreamingValidator { } if let Some(max) = arg_schema.max { if num > max { - self.errors.push( + self.add_error( ValidationError::new(ErrorCode::ConstraintMax, &format!("{} > {}", key, max)) .with_field(key) ); @@ -547,7 +655,7 @@ impl StreamingValidator { if let FieldValue::Str(s) = value { if let Some(min_len) = arg_schema.min_len { if s.len() < min_len { - self.errors.push( + self.add_error( ValidationError::new(ErrorCode::ConstraintLen, &format!("{} length < {}", key, min_len)) .with_field(key) ); @@ -555,7 +663,7 @@ impl StreamingValidator { } if let Some(max_len) = arg_schema.max_len { if s.len() > max_len { - self.errors.push( + self.add_error( ValidationError::new(ErrorCode::ConstraintLen, &format!("{} length > {}", key, max_len)) .with_field(key) ); @@ -563,7 +671,7 @@ impl StreamingValidator { } if let Some(ref pattern) = arg_schema.pattern { if !pattern.is_match(s) { - self.errors.push( + self.add_error( ValidationError::new(ErrorCode::ConstraintPattern, &format!("{} pattern mismatch", key)) .with_field(key) ); @@ -571,7 +679,7 @@ impl StreamingValidator { } if let Some(ref enum_values) = arg_schema.enum_values { if !enum_values.contains(s) { - self.errors.push( + self.add_error( ValidationError::new(ErrorCode::ConstraintEnum, &format!("{} not in allowed values", key)) .with_field(key) ); @@ -580,9 +688,43 @@ impl StreamingValidator { } } + fn validate_existing_fields(&mut self, tool_name: &str) { + let fields: Vec<(String, FieldValue)> = self + .fields + .iter() + .map(|(key, value)| (key.clone(), value.clone())) + .collect(); + + for (key, value) in fields { + self.validate_field(&key, &value, tool_name); + } + } + + fn type_matches(arg_schema: &ArgSchema, value: &FieldValue) -> bool { + let expected = arg_schema.arg_type.to_ascii_lowercase(); + + match value { + FieldValue::Null => !arg_schema.required, + FieldValue::Bool(_) => matches!(expected.as_str(), "bool" | "boolean"), + FieldValue::Int(_) => matches!(expected.as_str(), "int" | "integer" | "float" | "number"), + FieldValue::Float(_) => matches!(expected.as_str(), "float" | "number"), + FieldValue::Str(_) => matches!(expected.as_str(), "string" | "str"), + } + } + + fn field_type_name(value: &FieldValue) -> &'static str { + match value { + FieldValue::Null => "null", + FieldValue::Bool(_) => "bool", + FieldValue::Int(_) => "int", + FieldValue::Float(_) => "float", + FieldValue::Str(_) => "string", + } + } + fn validate_complete(&mut self) { if self.tool_name.is_none() { - self.errors.push(ValidationError::new(ErrorCode::MissingTool, "No action field found")); + self.add_error(ValidationError::new(ErrorCode::MissingTool, "No action field found")); return; } @@ -593,13 +735,23 @@ impl StreamingValidator { }; // Check required fields - for (arg_name, arg_schema) in &schema.args { - if arg_schema.required && !self.fields.contains_key(arg_name) { - self.errors.push( - ValidationError::new(ErrorCode::MissingRequired, &format!("Missing required field: {}", arg_name)) - .with_field(arg_name) - ); - } + let missing_required: Vec = schema + .args + .iter() + .filter_map(|(arg_name, arg_schema)| { + if arg_schema.required && !self.fields.contains_key(arg_name) { + Some(arg_name.clone()) + } else { + None + } + }) + .collect(); + + for arg_name in missing_required { + self.add_error( + ValidationError::new(ErrorCode::MissingRequired, &format!("Missing required field: {}", arg_name)) + .with_field(&arg_name) + ); } } @@ -609,7 +761,7 @@ impl StreamingValidator { ValidationResult { complete: self.state == ValidatorState::Complete, - valid: self.errors.is_empty(), + valid: self.state != ValidatorState::Error && self.errors.is_empty(), tool_name: self.tool_name.clone(), tool_allowed, errors: self.errors.clone(), @@ -628,7 +780,13 @@ impl StreamingValidator { /// Check if the stream should be cancelled. pub fn should_stop(&self) -> bool { - self.errors.iter().any(|e| e.code == ErrorCode::UnknownTool) + self.state == ValidatorState::Error + || self.errors.iter().any(|e| { + matches!( + e.code, + ErrorCode::UnknownTool | ErrorCode::LimitExceeded | ErrorCode::InvalidStructure + ) + }) } } @@ -757,4 +915,115 @@ mod tests { assert!(registry.is_allowed("browse")); assert!(!registry.is_allowed("unknown")); } + + #[test] + fn test_streaming_validator_type_enforcement() { + let mut registry = ToolRegistry::new(); + registry.register( + ToolSchema::new("search") + .arg("query", ArgSchema::new("string").required()) + .arg("max_results", ArgSchema::new("int")), + ); + + let mut v = StreamingValidator::new(registry); + v.start(); + + let result = v.push_token("{action=\"search\" query=123 max_results=\"ten\"}"); + + assert!(!result.valid); + assert_eq!( + result + .errors + .iter() + .filter(|e| e.code == ErrorCode::InvalidType) + .count(), + 2 + ); + } + + #[test] + fn test_streaming_validator_type_enforcement_when_action_arrives_last() { + let mut registry = ToolRegistry::new(); + registry.register( + ToolSchema::new("search") + .arg("query", ArgSchema::new("string").required()) + .arg("max_results", ArgSchema::new("int")), + ); + + let mut v = StreamingValidator::new(registry); + v.start(); + + let result = v.push_token("{query=123 max_results=\"ten\" action=\"search\"}"); + + assert!(!result.valid); + assert_eq!( + result + .errors + .iter() + .filter(|e| e.code == ErrorCode::InvalidType) + .count(), + 2 + ); + } + + #[test] + fn test_streaming_validator_buffer_limit() { + let registry = ToolRegistry::new(); + let mut v = + StreamingValidator::new(registry).with_limits(16, DEFAULT_MAX_FIELDS, DEFAULT_MAX_ERRORS); + v.start(); + + let result = v.push_token("{action=\"search\" query=\"this is too long\"}"); + + assert!(v.should_stop()); + assert!(matches!(v.state, ValidatorState::Error)); + assert!(result.errors.iter().any(|e| e.code == ErrorCode::LimitExceeded)); + } + + #[test] + fn test_streaming_validator_field_limit() { + let mut registry = ToolRegistry::new(); + registry.register(ToolSchema::new("test")); + + let mut v = + StreamingValidator::new(registry).with_limits(DEFAULT_MAX_BUFFER, 2, DEFAULT_MAX_ERRORS); + v.start(); + + let result = v.push_token("{action=\"test\" a=1 b=2 c=3}"); + + assert!(!result.valid); + assert!(result.errors.iter().any(|e| e.code == ErrorCode::LimitExceeded)); + } + + #[test] + fn test_streaming_validator_error_limit() { + let mut registry = ToolRegistry::new(); + registry.register( + ToolSchema::new("search") + .arg("a", ArgSchema::new("int").min(10.0)) + .arg("b", ArgSchema::new("int").min(10.0)) + .arg("c", ArgSchema::new("int").min(10.0)), + ); + + let mut v = + StreamingValidator::new(registry).with_limits(DEFAULT_MAX_BUFFER, DEFAULT_MAX_FIELDS, 2); + v.start(); + + let result = v.push_token("{action=\"search\" a=1 b=2 c=3}"); + + assert!(result.errors.len() <= 2); + } + + #[test] + fn test_streaming_validator_depth_underflow() { + let registry = ToolRegistry::new(); + let mut v = StreamingValidator::new(registry); + v.start(); + + let result = v.push_token("}"); + + assert!(v.should_stop()); + assert!(!result.complete); + assert!(result.errors.iter().any(|e| e.code == ErrorCode::InvalidStructure)); + } } From 0c6e97a0a06d207e11b782a7bca652df607f16f7 Mon Sep 17 00:00:00 2001 From: phenomenon0 Date: Sat, 7 Mar 2026 16:50:57 -0600 Subject: [PATCH 2/2] fix(js): add missing dist files for decimal128, schema_evolution, stream_validator These compiled outputs were not tracked, causing Go cross-impl tests to fail in CI with "Cannot find module './decimal128'". Co-Authored-By: Claude Opus 4.6 --- js/dist/decimal128.d.ts | 125 +++++++ js/dist/decimal128.d.ts.map | 1 + js/dist/decimal128.js | 290 ++++++++++++++++ js/dist/decimal128.js.map | 1 + js/dist/schema_evolution.d.ts | 151 +++++++++ js/dist/schema_evolution.d.ts.map | 1 + js/dist/schema_evolution.js | 405 ++++++++++++++++++++++ js/dist/schema_evolution.js.map | 1 + js/dist/stream_validator.d.ts | 176 ++++++++++ js/dist/stream_validator.d.ts.map | 1 + js/dist/stream_validator.js | 537 ++++++++++++++++++++++++++++++ js/dist/stream_validator.js.map | 1 + 12 files changed, 1690 insertions(+) create mode 100644 js/dist/decimal128.d.ts create mode 100644 js/dist/decimal128.d.ts.map create mode 100644 js/dist/decimal128.js create mode 100644 js/dist/decimal128.js.map create mode 100644 js/dist/schema_evolution.d.ts create mode 100644 js/dist/schema_evolution.d.ts.map create mode 100644 js/dist/schema_evolution.js create mode 100644 js/dist/schema_evolution.js.map create mode 100644 js/dist/stream_validator.d.ts create mode 100644 js/dist/stream_validator.d.ts.map create mode 100644 js/dist/stream_validator.js create mode 100644 js/dist/stream_validator.js.map diff --git a/js/dist/decimal128.d.ts b/js/dist/decimal128.d.ts new file mode 100644 index 0000000..fcb47b9 --- /dev/null +++ b/js/dist/decimal128.d.ts @@ -0,0 +1,125 @@ +/** + * Decimal128 - High-precision decimal type for GLYPH + * + * A 128-bit decimal for financial, scientific, and precise mathematical calculations. + * Value = coefficient * 10^(-scale) where scale is -127 to 127. + * + * Unlike JavaScript numbers (float64), Decimal128: + * - Preserves exact decimal representation + * - No precision loss for large numbers (>2^53) + * - Safe for financial calculations + * - Compatible with blockchain/crypto systems + */ +export declare class DecimalError extends Error { + constructor(message: string); +} +/** + * Decimal128 represents a 128-bit decimal number. + * Value = coefficient * 10^(-scale) + */ +export declare class Decimal128 { + /** Exponent: -127 to 127 */ + readonly scale: number; + /** 128-bit coefficient as BigInt */ + readonly coef: bigint; + constructor(scale: number, coef: bigint); + /** + * Create a Decimal128 from an integer. + */ + static fromInt(value: number | bigint): Decimal128; + /** + * Create a Decimal128 from a string. + * Examples: "123.45", "99.99", "-0.001" + */ + static fromString(s: string): Decimal128; + /** + * Create a Decimal128 from a number (with potential precision loss). + */ + static fromNumber(n: number): Decimal128; + /** + * Convert to integer (truncates fractional part). + */ + toInt(): bigint; + /** + * Convert to number (with potential precision loss). + */ + toNumber(): number; + /** + * Convert to string. + */ + toString(): string; + /** + * Check if value is zero. + */ + isZero(): boolean; + /** + * Check if value is negative. + */ + isNegative(): boolean; + /** + * Check if value is positive. + */ + isPositive(): boolean; + /** + * Return the absolute value. + */ + abs(): Decimal128; + /** + * Negate the value. + */ + negate(): Decimal128; + /** + * Add two decimals. + */ + add(other: Decimal128): Decimal128; + /** + * Subtract two decimals. + */ + sub(other: Decimal128): Decimal128; + /** + * Multiply two decimals. + */ + mul(other: Decimal128): Decimal128; + /** + * Divide two decimals. + */ + div(other: Decimal128): Decimal128; + /** + * Compare two decimals. + * Returns -1 if this < other, 0 if equal, 1 if this > other. + */ + cmp(other: Decimal128): number; + /** + * Check equality. + */ + equals(other: Decimal128): boolean; + /** + * Less than comparison. + */ + lt(other: Decimal128): boolean; + /** + * Greater than comparison. + */ + gt(other: Decimal128): boolean; + /** + * Less than or equal comparison. + */ + lte(other: Decimal128): boolean; + /** + * Greater than or equal comparison. + */ + gte(other: Decimal128): boolean; +} +/** + * Check if a string is a decimal literal (ends with 'm'). + */ +export declare function isDecimalLiteral(s: string): boolean; +/** + * Parse a decimal literal (with 'm' suffix). + */ +export declare function parseDecimalLiteral(s: string): Decimal128; +/** + * Convenience function to create a Decimal128. + */ +export declare function decimal(value: string | number | bigint): Decimal128; +//# sourceMappingURL=decimal128.d.ts.map \ No newline at end of file diff --git a/js/dist/decimal128.d.ts.map b/js/dist/decimal128.d.ts.map new file mode 100644 index 0000000..f1eff0f --- /dev/null +++ b/js/dist/decimal128.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"decimal128.d.ts","sourceRoot":"","sources":["../src/decimal128.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED;;;GAGG;AACH,qBAAa,UAAU;IACrB,4BAA4B;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAQvC;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU;IAIlD;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU;IA0CxC;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU;IAIxC;;OAEG;IACH,KAAK,IAAI,MAAM;IAKf;;OAEG;IACH,QAAQ,IAAI,MAAM;IAKlB;;OAEG;IACH,QAAQ,IAAI,MAAM;IAmBlB;;OAEG;IACH,MAAM,IAAI,OAAO;IAIjB;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,GAAG,IAAI,UAAU;IAIjB;;OAEG;IACH,MAAM,IAAI,UAAU;IAIpB;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU;IAkBlC;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU;IAIlC;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU;IAWlC;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU;IAelC;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM;IAiB9B;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;IAIlC;;OAEG;IACH,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;IAI9B;;OAEG;IACH,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;IAI9B;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;IAI/B;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;CAGhC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAWnD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAMzD;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAQnE"} \ No newline at end of file diff --git a/js/dist/decimal128.js b/js/dist/decimal128.js new file mode 100644 index 0000000..ad2a919 --- /dev/null +++ b/js/dist/decimal128.js @@ -0,0 +1,290 @@ +"use strict"; +/** + * Decimal128 - High-precision decimal type for GLYPH + * + * A 128-bit decimal for financial, scientific, and precise mathematical calculations. + * Value = coefficient * 10^(-scale) where scale is -127 to 127. + * + * Unlike JavaScript numbers (float64), Decimal128: + * - Preserves exact decimal representation + * - No precision loss for large numbers (>2^53) + * - Safe for financial calculations + * - Compatible with blockchain/crypto systems + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Decimal128 = exports.DecimalError = void 0; +exports.isDecimalLiteral = isDecimalLiteral; +exports.parseDecimalLiteral = parseDecimalLiteral; +exports.decimal = decimal; +class DecimalError extends Error { + constructor(message) { + super(message); + this.name = 'DecimalError'; + } +} +exports.DecimalError = DecimalError; +/** + * Decimal128 represents a 128-bit decimal number. + * Value = coefficient * 10^(-scale) + */ +class Decimal128 { + constructor(scale, coef) { + if (scale < -127 || scale > 127) { + throw new DecimalError(`scale must be -127 to 127, got ${scale}`); + } + this.scale = scale; + this.coef = coef; + } + /** + * Create a Decimal128 from an integer. + */ + static fromInt(value) { + return new Decimal128(0, BigInt(value)); + } + /** + * Create a Decimal128 from a string. + * Examples: "123.45", "99.99", "-0.001" + */ + static fromString(s) { + s = s.trim(); + // Remove 'm' suffix if present + if (s.endsWith('m')) { + s = s.slice(0, -1); + } + const negative = s.startsWith('-'); + if (negative) { + s = s.slice(1); + } + const parts = s.split('.'); + if (parts.length > 2) { + throw new DecimalError(`invalid decimal format: ${s}`); + } + let scale = 0; + let coefStr; + if (parts.length === 2) { + const intPart = parts[0] || '0'; + const fracPart = parts[1]; + scale = fracPart.length; + coefStr = intPart + fracPart; + } + else { + coefStr = parts[0]; + } + if (scale > 127) { + throw new DecimalError(`scale too large: ${scale}`); + } + let coef = BigInt(coefStr); + if (negative) { + coef = -coef; + } + return new Decimal128(scale, coef); + } + /** + * Create a Decimal128 from a number (with potential precision loss). + */ + static fromNumber(n) { + return Decimal128.fromString(n.toString()); + } + /** + * Convert to integer (truncates fractional part). + */ + toInt() { + const divisor = 10n ** BigInt(this.scale); + return this.coef / divisor; + } + /** + * Convert to number (with potential precision loss). + */ + toNumber() { + const divisor = 10 ** this.scale; + return Number(this.coef) / divisor; + } + /** + * Convert to string. + */ + toString() { + if (this.scale === 0) { + return this.coef.toString(); + } + const negative = this.coef < 0n; + let coefStr = (negative ? -this.coef : this.coef).toString(); + // Pad with zeros if needed + while (coefStr.length <= this.scale) { + coefStr = '0' + coefStr; + } + const insertPos = coefStr.length - this.scale; + const result = coefStr.slice(0, insertPos) + '.' + coefStr.slice(insertPos); + return negative ? '-' + result : result; + } + /** + * Check if value is zero. + */ + isZero() { + return this.coef === 0n; + } + /** + * Check if value is negative. + */ + isNegative() { + return this.coef < 0n; + } + /** + * Check if value is positive. + */ + isPositive() { + return this.coef > 0n; + } + /** + * Return the absolute value. + */ + abs() { + return new Decimal128(this.scale, this.coef < 0n ? -this.coef : this.coef); + } + /** + * Negate the value. + */ + negate() { + return new Decimal128(this.scale, -this.coef); + } + /** + * Add two decimals. + */ + add(other) { + let c1 = this.coef; + let c2 = other.coef; + let targetScale; + if (this.scale < other.scale) { + const diff = other.scale - this.scale; + c1 = c1 * (10n ** BigInt(diff)); + targetScale = other.scale; + } + else { + const diff = this.scale - other.scale; + c2 = c2 * (10n ** BigInt(diff)); + targetScale = this.scale; + } + return new Decimal128(targetScale, c1 + c2); + } + /** + * Subtract two decimals. + */ + sub(other) { + return this.add(other.negate()); + } + /** + * Multiply two decimals. + */ + mul(other) { + const result = this.coef * other.coef; + const newScale = this.scale + other.scale; + if (newScale > 127 || newScale < -127) { + throw new DecimalError('scale overflow'); + } + return new Decimal128(newScale, result); + } + /** + * Divide two decimals. + */ + div(other) { + if (other.coef === 0n) { + throw new DecimalError('division by zero'); + } + const result = this.coef / other.coef; + const newScale = this.scale - other.scale; + if (newScale > 127 || newScale < -127) { + throw new DecimalError('scale overflow'); + } + return new Decimal128(newScale, result); + } + /** + * Compare two decimals. + * Returns -1 if this < other, 0 if equal, 1 if this > other. + */ + cmp(other) { + let c1 = this.coef; + let c2 = other.coef; + if (this.scale < other.scale) { + const diff = other.scale - this.scale; + c1 = c1 * (10n ** BigInt(diff)); + } + else if (this.scale > other.scale) { + const diff = this.scale - other.scale; + c2 = c2 * (10n ** BigInt(diff)); + } + if (c1 < c2) + return -1; + if (c1 > c2) + return 1; + return 0; + } + /** + * Check equality. + */ + equals(other) { + return this.cmp(other) === 0; + } + /** + * Less than comparison. + */ + lt(other) { + return this.cmp(other) < 0; + } + /** + * Greater than comparison. + */ + gt(other) { + return this.cmp(other) > 0; + } + /** + * Less than or equal comparison. + */ + lte(other) { + return this.cmp(other) <= 0; + } + /** + * Greater than or equal comparison. + */ + gte(other) { + return this.cmp(other) >= 0; + } +} +exports.Decimal128 = Decimal128; +/** + * Check if a string is a decimal literal (ends with 'm'). + */ +function isDecimalLiteral(s) { + s = s.trim(); + if (!s.endsWith('m')) { + return false; + } + try { + Decimal128.fromString(s.slice(0, -1)); + return true; + } + catch { + return false; + } +} +/** + * Parse a decimal literal (with 'm' suffix). + */ +function parseDecimalLiteral(s) { + s = s.trim(); + if (!s.endsWith('m')) { + throw new DecimalError('not a decimal literal'); + } + return Decimal128.fromString(s.slice(0, -1)); +} +/** + * Convenience function to create a Decimal128. + */ +function decimal(value) { + if (typeof value === 'string') { + return Decimal128.fromString(value); + } + if (typeof value === 'bigint') { + return Decimal128.fromInt(value); + } + return Decimal128.fromNumber(value); +} +//# sourceMappingURL=decimal128.js.map \ No newline at end of file diff --git a/js/dist/decimal128.js.map b/js/dist/decimal128.js.map new file mode 100644 index 0000000..8419151 --- /dev/null +++ b/js/dist/decimal128.js.map @@ -0,0 +1 @@ +{"version":3,"file":"decimal128.js","sourceRoot":"","sources":["../src/decimal128.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/dist/schema_evolution.d.ts b/js/dist/schema_evolution.d.ts new file mode 100644 index 0000000..b328735 --- /dev/null +++ b/js/dist/schema_evolution.d.ts @@ -0,0 +1,151 @@ +/** + * Schema Evolution - Safe API versioning for GLYPH + * + * Enables schemas to evolve safely without breaking clients. Supports: + * - Adding new optional fields + * - Renaming fields (with compatibility mapping) + * - Deprecating fields + * - Changing defaults + * - Strict vs tolerant parsing modes + */ +export declare enum EvolutionMode { + /** Fail on unknown fields */ + Strict = "strict", + /** Ignore unknown fields (default) */ + Tolerant = "tolerant", + /** Auto-migrate between versions */ + Migrate = "migrate" +} +export type FieldType = 'str' | 'int' | 'float' | 'bool' | 'list' | 'decimal'; +export type FieldValue = null | boolean | number | string | FieldValue[]; +export interface EvolvingFieldConfig { + type: FieldType; + required?: boolean; + default?: FieldValue; + addedIn?: string; + deprecatedIn?: string; + renamedFrom?: string; + validation?: string | RegExp; +} +export declare class EvolvingField { + readonly name: string; + readonly type: FieldType; + readonly required: boolean; + readonly default: FieldValue | undefined; + readonly addedIn: string; + readonly deprecatedIn: string | undefined; + readonly renamedFrom: string | undefined; + readonly validation: RegExp | undefined; + constructor(name: string, config: EvolvingFieldConfig); + /** + * Check if field is available in a given version. + */ + isAvailableIn(version: string): boolean; + /** + * Check if field is deprecated in a given version. + */ + isDeprecatedIn(version: string): boolean; + /** + * Validate a value against this field. + */ + validate(value: FieldValue): string | null; +} +export declare class VersionSchema { + readonly name: string; + readonly version: string; + readonly fields: Map; + description: string; + constructor(name: string, version: string); + /** + * Add a field. + */ + addField(field: EvolvingField): void; + /** + * Get a field by name. + */ + getField(name: string): EvolvingField | undefined; + /** + * Validate data against this schema. + */ + validate(data: Record): string | null; +} +export interface ParseResult { + error?: string; + data?: Record; +} +export interface EmitResult { + error?: string; + header?: string; +} +export interface ChangelogEntry { + version: string; + description: string; + addedFields: string[]; + deprecatedFields: string[]; + renamedFields: [string, string][]; +} +export declare class VersionedSchema { + readonly name: string; + readonly versions: Map; + latestVersion: string; + mode: EvolutionMode; + constructor(name: string); + /** + * Set evolution mode. + */ + withMode(mode: EvolutionMode): this; + /** + * Add a version with fields. + */ + addVersion(version: string, fields: Record): void; + /** + * Get schema for a specific version. + */ + getVersion(version: string): VersionSchema | undefined; + /** + * Parse data from a specific version. + */ + parse(data: Record, fromVersion: string): ParseResult; + /** + * Emit version header for data. + */ + emit(data: Record, version?: string): EmitResult; + /** + * Migrate data between versions. + */ + private migrate; + /** + * Migrate one step. + */ + private migrateStep; + /** + * Get migration path between versions. + */ + private getMigrationPath; + /** + * Get the latest version string. + */ + private getLatestVersion; + /** + * Get changelog of schema evolution. + */ + getChangelog(): ChangelogEntry[]; +} +/** + * Compare two version strings. + * Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2. + */ +export declare function compareVersions(v1: string, v2: string): number; +/** + * Parse a version header (e.g., "@version 2.0"). + */ +export declare function parseVersionHeader(text: string): string | null; +/** + * Format a version header. + */ +export declare function formatVersionHeader(version: string): string; +/** + * Create a versioned schema. + */ +export declare function versionedSchema(name: string): VersionedSchema; +//# sourceMappingURL=schema_evolution.d.ts.map \ No newline at end of file diff --git a/js/dist/schema_evolution.d.ts.map b/js/dist/schema_evolution.d.ts.map new file mode 100644 index 0000000..650aa20 --- /dev/null +++ b/js/dist/schema_evolution.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"schema_evolution.d.ts","sourceRoot":"","sources":["../src/schema_evolution.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/dist/schema_evolution.js b/js/dist/schema_evolution.js new file mode 100644 index 0000000..16be4b8 --- /dev/null +++ b/js/dist/schema_evolution.js @@ -0,0 +1,405 @@ +"use strict"; +/** + * Schema Evolution - Safe API versioning for GLYPH + * + * Enables schemas to evolve safely without breaking clients. Supports: + * - Adding new optional fields + * - Renaming fields (with compatibility mapping) + * - Deprecating fields + * - Changing defaults + * - Strict vs tolerant parsing modes + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VersionedSchema = exports.VersionSchema = exports.EvolvingField = exports.EvolutionMode = void 0; +exports.compareVersions = compareVersions; +exports.parseVersionHeader = parseVersionHeader; +exports.formatVersionHeader = formatVersionHeader; +exports.versionedSchema = versionedSchema; +// ============================================================ +// Evolution Mode +// ============================================================ +var EvolutionMode; +(function (EvolutionMode) { + /** Fail on unknown fields */ + EvolutionMode["Strict"] = "strict"; + /** Ignore unknown fields (default) */ + EvolutionMode["Tolerant"] = "tolerant"; + /** Auto-migrate between versions */ + EvolutionMode["Migrate"] = "migrate"; +})(EvolutionMode || (exports.EvolutionMode = EvolutionMode = {})); +class EvolvingField { + constructor(name, config) { + this.name = name; + this.type = config.type; + this.required = config.required ?? false; + this.default = config.default; + this.addedIn = config.addedIn ?? '1.0'; + this.deprecatedIn = config.deprecatedIn; + this.renamedFrom = config.renamedFrom; + this.validation = config.validation + ? (typeof config.validation === 'string' ? new RegExp(config.validation) : config.validation) + : undefined; + } + /** + * Check if field is available in a given version. + */ + isAvailableIn(version) { + if (compareVersions(version, this.addedIn) < 0) { + return false; + } + if (this.deprecatedIn && compareVersions(version, this.deprecatedIn) >= 0) { + return false; + } + return true; + } + /** + * Check if field is deprecated in a given version. + */ + isDeprecatedIn(version) { + if (!this.deprecatedIn) { + return false; + } + return compareVersions(version, this.deprecatedIn) >= 0; + } + /** + * Validate a value against this field. + */ + validate(value) { + if (value === null || value === undefined) { + if (this.required) { + return `field ${this.name} is required`; + } + return null; + } + // Type checking + switch (this.type) { + case 'str': + if (typeof value !== 'string') { + return `field ${this.name} must be string`; + } + if (this.validation && !this.validation.test(value)) { + return `field ${this.name} does not match pattern`; + } + break; + case 'int': + if (typeof value !== 'number' || !Number.isInteger(value)) { + return `field ${this.name} must be int`; + } + break; + case 'float': + if (typeof value !== 'number') { + return `field ${this.name} must be float`; + } + break; + case 'bool': + if (typeof value !== 'boolean') { + return `field ${this.name} must be bool`; + } + break; + case 'list': + if (!Array.isArray(value)) { + return `field ${this.name} must be list`; + } + break; + } + return null; + } +} +exports.EvolvingField = EvolvingField; +// ============================================================ +// Version Schema +// ============================================================ +class VersionSchema { + constructor(name, version) { + this.name = name; + this.version = version; + this.fields = new Map(); + this.description = ''; + } + /** + * Add a field. + */ + addField(field) { + this.fields.set(field.name, field); + } + /** + * Get a field by name. + */ + getField(name) { + return this.fields.get(name); + } + /** + * Validate data against this schema. + */ + validate(data) { + // Check required fields + for (const [name, field] of this.fields) { + if (field.required && !(name in data)) { + return `missing required field: ${name}`; + } + } + // Validate field values + for (const [name, value] of Object.entries(data)) { + const field = this.fields.get(name); + if (field) { + const error = field.validate(value); + if (error) { + return error; + } + } + } + return null; + } +} +exports.VersionSchema = VersionSchema; +class VersionedSchema { + constructor(name) { + this.name = name; + this.versions = new Map(); + this.latestVersion = '1.0'; + this.mode = EvolutionMode.Tolerant; + } + /** + * Set evolution mode. + */ + withMode(mode) { + this.mode = mode; + return this; + } + /** + * Add a version with fields. + */ + addVersion(version, fields) { + const schema = new VersionSchema(this.name, version); + for (const [name, config] of Object.entries(fields)) { + const fieldConfig = { ...config }; + if (!fieldConfig.addedIn) { + fieldConfig.addedIn = version; + } + schema.addField(new EvolvingField(name, fieldConfig)); + } + this.versions.set(version, schema); + this.latestVersion = this.getLatestVersion(); + } + /** + * Get schema for a specific version. + */ + getVersion(version) { + return this.versions.get(version); + } + /** + * Parse data from a specific version. + */ + parse(data, fromVersion) { + const schema = this.getVersion(fromVersion); + if (!schema) { + return { error: `unknown version: ${fromVersion}` }; + } + // Validate in strict mode + if (this.mode === EvolutionMode.Strict) { + const error = schema.validate(data); + if (error) { + return { error }; + } + } + let result = { ...data }; + // Migrate to latest if needed + if (fromVersion !== this.latestVersion) { + const migrated = this.migrate(data, fromVersion, this.latestVersion); + if (migrated.error) { + return migrated; + } + result = migrated.data; + } + // Filter unknown fields in tolerant mode + if (this.mode === EvolutionMode.Tolerant) { + const targetSchema = this.getVersion(this.latestVersion); + if (targetSchema) { + const filtered = {}; + for (const [k, v] of Object.entries(result)) { + if (targetSchema.fields.has(k)) { + filtered[k] = v; + } + } + result = filtered; + } + } + return { data: result }; + } + /** + * Emit version header for data. + */ + emit(data, version) { + const targetVersion = version ?? this.latestVersion; + const schema = this.getVersion(targetVersion); + if (!schema) { + return { error: `unknown version: ${targetVersion}` }; + } + const error = schema.validate(data); + if (error) { + return { error }; + } + return { header: `@version ${targetVersion}` }; + } + /** + * Migrate data between versions. + */ + migrate(data, fromVersion, toVersion) { + const path = this.getMigrationPath(fromVersion, toVersion); + if (!path) { + return { error: `cannot migrate from ${fromVersion} to ${toVersion}` }; + } + let currentData = { ...data }; + let currentVersion = fromVersion; + for (const nextVersion of path) { + const result = this.migrateStep(currentData, currentVersion, nextVersion); + if (result.error) { + return result; + } + currentData = result.data; + currentVersion = nextVersion; + } + return { data: currentData }; + } + /** + * Migrate one step. + */ + migrateStep(data, _fromVersion, toVersion) { + const toSchema = this.getVersion(toVersion); + if (!toSchema) { + return { error: 'invalid version' }; + } + const result = { ...data }; + // Handle field renames + for (const [name, field] of toSchema.fields) { + if (field.renamedFrom && field.renamedFrom in result && !(name in result)) { + result[name] = result[field.renamedFrom]; + delete result[field.renamedFrom]; + } + } + // Handle new fields with defaults + for (const [name, field] of toSchema.fields) { + if (!(name in result)) { + if (field.default !== undefined) { + result[name] = field.default; + } + else if (!field.required) { + result[name] = null; + } + } + } + // Remove unknown fields in tolerant mode + if (this.mode === EvolutionMode.Tolerant) { + for (const key of Object.keys(result)) { + if (!toSchema.fields.has(key)) { + delete result[key]; + } + } + } + return { data: result }; + } + /** + * Get migration path between versions. + */ + getMigrationPath(fromVersion, toVersion) { + const versions = Array.from(this.versions.keys()).sort((a, b) => compareVersions(a, b)); + const fromIdx = versions.indexOf(fromVersion); + const toIdx = versions.indexOf(toVersion); + if (fromIdx === -1 || toIdx === -1) { + return null; + } + if (fromIdx < toIdx) { + return versions.slice(fromIdx + 1, toIdx + 1); + } + else if (fromIdx > toIdx) { + return null; // Downgrade not supported + } + return []; + } + /** + * Get the latest version string. + */ + getLatestVersion() { + const versions = Array.from(this.versions.keys()).sort((a, b) => compareVersions(a, b)); + return versions[versions.length - 1] ?? '1.0'; + } + /** + * Get changelog of schema evolution. + */ + getChangelog() { + const versions = Array.from(this.versions.keys()).sort((a, b) => compareVersions(a, b)); + return versions.map(version => { + const schema = this.versions.get(version); + const addedFields = []; + const deprecatedFields = []; + const renamedFields = []; + for (const [name, field] of schema.fields) { + if (field.addedIn === version) { + addedFields.push(name); + } + if (field.deprecatedIn === version) { + deprecatedFields.push(name); + } + if (field.renamedFrom) { + renamedFields.push([field.renamedFrom, name]); + } + } + return { + version, + description: schema.description, + addedFields, + deprecatedFields, + renamedFields, + }; + }); + } +} +exports.VersionedSchema = VersionedSchema; +// ============================================================ +// Helper Functions +// ============================================================ +/** + * Compare two version strings. + * Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2. + */ +function compareVersions(v1, v2) { + const parts1 = v1.split('.').map(s => parseInt(s, 10) || 0); + const parts2 = v2.split('.').map(s => parseInt(s, 10) || 0); + const maxLen = Math.max(parts1.length, parts2.length); + for (let i = 0; i < maxLen; i++) { + const p1 = parts1[i] ?? 0; + const p2 = parts2[i] ?? 0; + if (p1 < p2) + return -1; + if (p1 > p2) + return 1; + } + return 0; +} +/** + * Parse a version header (e.g., "@version 2.0"). + */ +function parseVersionHeader(text) { + text = text.trim(); + if (!text.startsWith('@version ')) { + return null; + } + const version = text.slice(9).trim(); + if (!version) { + return null; + } + return version; +} +/** + * Format a version header. + */ +function formatVersionHeader(version) { + return `@version ${version}`; +} +/** + * Create a versioned schema. + */ +function versionedSchema(name) { + return new VersionedSchema(name); +} +//# sourceMappingURL=schema_evolution.js.map \ No newline at end of file diff --git a/js/dist/schema_evolution.js.map b/js/dist/schema_evolution.js.map new file mode 100644 index 0000000..b62e5c6 --- /dev/null +++ b/js/dist/schema_evolution.js.map @@ -0,0 +1 @@ +{"version":3,"file":"schema_evolution.js","sourceRoot":"","sources":["../src/schema_evolution.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/dist/stream_validator.d.ts b/js/dist/stream_validator.d.ts new file mode 100644 index 0000000..154da4f --- /dev/null +++ b/js/dist/stream_validator.d.ts @@ -0,0 +1,176 @@ +/** + * GLYPH Streaming Validator + * + * Validates GLYPH tool calls incrementally as tokens arrive from an LLM. + * + * This enables: + * - Early tool detection: Know the tool name before full response + * - Early rejection: Stop on unknown tools mid-stream + * - Incremental validation: Check constraints as tokens arrive + * - Latency savings: Reject bad payloads without waiting for completion + */ +export interface ArgSchema { + type: string; + required?: boolean; + min?: number; + max?: number; + minLen?: number; + maxLen?: number; + pattern?: RegExp; + enumValues?: string[]; +} +export interface ToolSchema { + name: string; + description?: string; + args: Record; +} +export declare class ToolRegistry { + private tools; + /** + * Register a tool. + */ + register(tool: ToolSchema): void; + /** + * Check if a tool is allowed. + */ + isAllowed(name: string): boolean; + /** + * Get a tool schema. + */ + get(name: string): ToolSchema | undefined; +} +export declare enum ErrorCode { + UnknownTool = "UNKNOWN_TOOL", + MissingRequired = "MISSING_REQUIRED", + MissingTool = "MISSING_TOOL", + ConstraintMin = "CONSTRAINT_MIN", + ConstraintMax = "CONSTRAINT_MAX", + ConstraintLen = "CONSTRAINT_LEN", + ConstraintPattern = "CONSTRAINT_PATTERN", + ConstraintEnum = "CONSTRAINT_ENUM", + InvalidType = "INVALID_TYPE", + LimitExceeded = "LIMIT_EXCEEDED" +} +export declare const DEFAULT_MAX_BUFFER: number; +export declare const DEFAULT_MAX_FIELDS = 1000; +export declare const DEFAULT_MAX_ERRORS = 100; +export interface ValidatorLimits { + maxBufferSize?: number; + maxFieldCount?: number; + maxErrorCount?: number; +} +export interface ValidationError { + code: ErrorCode; + message: string; + field?: string; +} +export declare enum ValidatorState { + Waiting = "waiting", + InObject = "in_object", + Complete = "complete", + Error = "error" +} +export interface TimelineEvent { + event: string; + token: number; + charPos: number; + elapsed: number; + detail: string; +} +export type FieldValue = null | boolean | number | string; +export interface ValidationResult { + complete: boolean; + valid: boolean; + state: ValidatorState; + toolName: string | null; + toolAllowed: boolean | null; + errors: ValidationError[]; + fields: Record; + tokenCount: number; + charCount: number; + timeline: TimelineEvent[]; + toolDetectedAtToken: number; + toolDetectedAtChar: number; + toolDetectedAtTime: number; + firstErrorAtToken: number; + firstErrorAtTime: number; + completeAtToken: number; + completeAtTime: number; +} +export declare class StreamingValidator { + private registry; + private buffer; + private state; + private depth; + private inString; + private escapeNext; + private currentKey; + private currentVal; + private hasKey; + private toolName; + private fields; + private errors; + private tokenCount; + private charCount; + private startTime; + private toolDetectedAtToken; + private toolDetectedAtChar; + private toolDetectedAtTime; + private firstErrorAtToken; + private firstErrorAtTime; + private completeAtToken; + private completeAtTime; + private timeline; + private maxBufferSize; + private maxFieldCount; + private maxErrorCount; + constructor(registry: ToolRegistry, limits?: ValidatorLimits); + /** + * Set custom limits. Returns self for chaining. + */ + withLimits(limits: ValidatorLimits): this; + /** + * Add an error, respecting maxErrorCount limit. + */ + private addError; + /** + * Reset the validator for reuse. + */ + reset(): void; + /** + * Start timing. + */ + start(): void; + /** + * Process a token from the LLM. + */ + pushToken(token: string): ValidationResult; + private processChar; + private finishField; + private parseValue; + private validateField; + private validateComplete; + /** + * Get the current validation result. + */ + getResult(): ValidationResult; + /** + * Check if the stream should be cancelled. + */ + shouldStop(): boolean; + /** + * Check if the detected tool is allowed. + * Returns false if no tool detected or registry not configured. + */ + isToolAllowed(): boolean; + /** + * Get the parsed fields if validation is complete and valid. + * Returns null if not complete or if there are errors. + */ + getParsed(): Record | null; +} +/** + * Create a default tool registry with common tools. + */ +export declare function defaultToolRegistry(): ToolRegistry; +//# sourceMappingURL=stream_validator.d.ts.map \ No newline at end of file diff --git a/js/dist/stream_validator.d.ts.map b/js/dist/stream_validator.d.ts.map new file mode 100644 index 0000000..dc26b7f --- /dev/null +++ b/js/dist/stream_validator.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"stream_validator.d.ts","sourceRoot":"","sources":["../src/stream_validator.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/dist/stream_validator.js b/js/dist/stream_validator.js new file mode 100644 index 0000000..0824d10 --- /dev/null +++ b/js/dist/stream_validator.js @@ -0,0 +1,537 @@ +"use strict"; +/** + * GLYPH Streaming Validator + * + * Validates GLYPH tool calls incrementally as tokens arrive from an LLM. + * + * This enables: + * - Early tool detection: Know the tool name before full response + * - Early rejection: Stop on unknown tools mid-stream + * - Incremental validation: Check constraints as tokens arrive + * - Latency savings: Reject bad payloads without waiting for completion + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.StreamingValidator = exports.ValidatorState = exports.DEFAULT_MAX_ERRORS = exports.DEFAULT_MAX_FIELDS = exports.DEFAULT_MAX_BUFFER = exports.ErrorCode = exports.ToolRegistry = void 0; +exports.defaultToolRegistry = defaultToolRegistry; +class ToolRegistry { + constructor() { + this.tools = new Map(); + } + /** + * Register a tool. + */ + register(tool) { + this.tools.set(tool.name, tool); + } + /** + * Check if a tool is allowed. + */ + isAllowed(name) { + return this.tools.has(name); + } + /** + * Get a tool schema. + */ + get(name) { + return this.tools.get(name); + } +} +exports.ToolRegistry = ToolRegistry; +// ============================================================ +// Validation Errors +// ============================================================ +var ErrorCode; +(function (ErrorCode) { + ErrorCode["UnknownTool"] = "UNKNOWN_TOOL"; + ErrorCode["MissingRequired"] = "MISSING_REQUIRED"; + ErrorCode["MissingTool"] = "MISSING_TOOL"; + ErrorCode["ConstraintMin"] = "CONSTRAINT_MIN"; + ErrorCode["ConstraintMax"] = "CONSTRAINT_MAX"; + ErrorCode["ConstraintLen"] = "CONSTRAINT_LEN"; + ErrorCode["ConstraintPattern"] = "CONSTRAINT_PATTERN"; + ErrorCode["ConstraintEnum"] = "CONSTRAINT_ENUM"; + ErrorCode["InvalidType"] = "INVALID_TYPE"; + ErrorCode["LimitExceeded"] = "LIMIT_EXCEEDED"; +})(ErrorCode || (exports.ErrorCode = ErrorCode = {})); +// Default limits to prevent DoS +exports.DEFAULT_MAX_BUFFER = 1 << 20; // 1MB +exports.DEFAULT_MAX_FIELDS = 1000; +exports.DEFAULT_MAX_ERRORS = 100; +// ============================================================ +// Validator State +// ============================================================ +var ValidatorState; +(function (ValidatorState) { + ValidatorState["Waiting"] = "waiting"; + ValidatorState["InObject"] = "in_object"; + ValidatorState["Complete"] = "complete"; + ValidatorState["Error"] = "error"; +})(ValidatorState || (exports.ValidatorState = ValidatorState = {})); +class StreamingValidator { + constructor(registry, limits) { + // Parser state + this.buffer = ''; + this.state = ValidatorState.Waiting; + this.depth = 0; + this.inString = false; + this.escapeNext = false; + this.currentKey = ''; + this.currentVal = ''; + this.hasKey = false; + // Parsed data + this.toolName = null; + this.fields = {}; + this.errors = []; + // Timing + this.tokenCount = 0; + this.charCount = 0; + this.startTime = 0; + this.toolDetectedAtToken = 0; + this.toolDetectedAtChar = 0; + this.toolDetectedAtTime = 0; + this.firstErrorAtToken = 0; + this.firstErrorAtTime = 0; + this.completeAtToken = 0; + this.completeAtTime = 0; + // Timeline + this.timeline = []; + // Hard limits to prevent OOM/DoS + this.maxBufferSize = exports.DEFAULT_MAX_BUFFER; + this.maxFieldCount = exports.DEFAULT_MAX_FIELDS; + this.maxErrorCount = exports.DEFAULT_MAX_ERRORS; + this.registry = registry; + if (limits) { + this.withLimits(limits); + } + } + /** + * Set custom limits. Returns self for chaining. + */ + withLimits(limits) { + if (limits.maxBufferSize !== undefined && limits.maxBufferSize > 0) { + this.maxBufferSize = limits.maxBufferSize; + } + if (limits.maxFieldCount !== undefined && limits.maxFieldCount > 0) { + this.maxFieldCount = limits.maxFieldCount; + } + if (limits.maxErrorCount !== undefined && limits.maxErrorCount > 0) { + this.maxErrorCount = limits.maxErrorCount; + } + return this; + } + /** + * Add an error, respecting maxErrorCount limit. + */ + addError(code, message, field) { + if (this.errors.length >= this.maxErrorCount) { + return; + } + this.errors.push({ code, message, field }); + } + /** + * Reset the validator for reuse. + */ + reset() { + this.buffer = ''; + this.state = ValidatorState.Waiting; + this.depth = 0; + this.inString = false; + this.escapeNext = false; + this.currentKey = ''; + this.currentVal = ''; + this.hasKey = false; + this.toolName = null; + this.fields = {}; + this.errors = []; + this.tokenCount = 0; + this.charCount = 0; + this.startTime = 0; + this.toolDetectedAtToken = 0; + this.toolDetectedAtChar = 0; + this.toolDetectedAtTime = 0; + this.firstErrorAtToken = 0; + this.firstErrorAtTime = 0; + this.completeAtToken = 0; + this.completeAtTime = 0; + this.timeline = []; + } + /** + * Start timing. + */ + start() { + this.startTime = Date.now(); + } + /** + * Process a token from the LLM. + */ + pushToken(token) { + if (this.startTime === 0) { + this.start(); + } + this.tokenCount++; + for (const c of token) { + this.charCount++; + this.processChar(c); + } + const elapsed = Date.now() - this.startTime; + // Record tool detection + if (this.toolName && this.toolDetectedAtToken === 0) { + this.toolDetectedAtToken = this.tokenCount; + this.toolDetectedAtChar = this.charCount; + this.toolDetectedAtTime = elapsed; + const allowed = this.registry.isAllowed(this.toolName); + this.timeline.push({ + event: 'TOOL_DETECTED', + token: this.tokenCount, + charPos: this.charCount, + elapsed, + detail: `tool=${this.toolName} allowed=${allowed}`, + }); + } + // Record first error + if (this.errors.length > 0 && this.firstErrorAtToken === 0) { + this.firstErrorAtToken = this.tokenCount; + this.firstErrorAtTime = elapsed; + this.timeline.push({ + event: 'ERROR', + token: this.tokenCount, + charPos: this.charCount, + elapsed, + detail: this.errors[0].message, + }); + } + // Record completion + if (this.state === ValidatorState.Complete && this.completeAtToken === 0) { + this.completeAtToken = this.tokenCount; + this.completeAtTime = elapsed; + this.timeline.push({ + event: 'COMPLETE', + token: this.tokenCount, + charPos: this.charCount, + elapsed, + detail: `valid=${this.errors.length === 0}`, + }); + } + return this.getResult(); + } + processChar(c) { + // Check hard limits before processing + if (this.buffer.length >= this.maxBufferSize) { + if (this.state !== ValidatorState.Error) { + this.state = ValidatorState.Error; + this.addError(ErrorCode.LimitExceeded, 'Buffer size limit exceeded'); + } + return; + } + if (Object.keys(this.fields).length >= this.maxFieldCount) { + if (this.state !== ValidatorState.Error) { + this.state = ValidatorState.Error; + this.addError(ErrorCode.LimitExceeded, 'Field count limit exceeded'); + } + return; + } + this.buffer += c; + // Handle escape sequences + if (this.escapeNext) { + this.escapeNext = false; + this.currentVal += c; + return; + } + if (c === '\\' && this.inString) { + this.escapeNext = true; + this.currentVal += c; + return; + } + // Handle quotes + if (c === '"') { + if (this.inString) { + this.inString = false; + } + else { + this.inString = true; + this.currentVal = ''; + } + return; + } + // Inside string - accumulate + if (this.inString) { + this.currentVal += c; + return; + } + // Handle structural characters + switch (c) { + case '{': + if (this.state === ValidatorState.Waiting) { + // Check for tool name before brace (e.g., "search{query=test}") + const preBraceText = this.currentVal.trim(); + if (preBraceText) { + this.toolName = preBraceText; + this.currentVal = ''; + // Validate against allow list + if (!this.registry.isAllowed(preBraceText)) { + this.addError(ErrorCode.UnknownTool, `Unknown tool: ${preBraceText}`); + } + } + this.state = ValidatorState.InObject; + } + this.depth++; + break; + case '}': + this.depth--; + if (this.depth === 0) { + this.finishField(); + this.state = ValidatorState.Complete; + this.validateComplete(); + } + break; + case '[': + this.depth++; + this.currentVal += c; + break; + case ']': + this.depth--; + this.currentVal += c; + break; + case '=': + if (this.depth === 1 && !this.hasKey) { + this.currentKey = this.currentVal.trim(); + this.currentVal = ''; + this.hasKey = true; + } + else { + this.currentVal += c; + } + break; + case ' ': + case '\n': + case '\t': + case '\r': + if (this.depth === 1 && this.hasKey && this.currentVal.length > 0) { + this.finishField(); + } + break; + default: + // Accumulate tool name before brace when waiting, or field value when in object + if (this.state === ValidatorState.Waiting && this.depth === 0) { + this.currentVal += c; + } + else if (this.depth >= 1) { + this.currentVal += c; + } + } + } + finishField() { + if (!this.hasKey) { + return; + } + const key = this.currentKey; + const valStr = this.currentVal.trim(); + this.currentKey = ''; + this.currentVal = ''; + this.hasKey = false; + const value = this.parseValue(valStr); + // Check for tool/action field + if (key === 'action' || key === 'tool') { + if (typeof value === 'string') { + this.toolName = value; + // Validate against allow list + if (!this.registry.isAllowed(value)) { + this.addError(ErrorCode.UnknownTool, `Unknown tool: ${value}`, key); + } + } + } + // Validate field constraints + if (this.toolName) { + this.validateField(key, value); + } + this.fields[key] = value; + } + parseValue(s) { + // Boolean + if (s === 't' || s === 'true') { + return true; + } + if (s === 'f' || s === 'false') { + return false; + } + // Null (including Unicode ∅) + if (s === '_' || s === 'null' || s === '' || s === '∅') { + return null; + } + // Integer (no decimal point or exponent) + if (/^-?\d+$/.test(s)) { + return parseInt(s, 10); + } + // Float (including scientific notation like 1e5, 1.5e-3) + if (/^-?\d+\.?\d*(?:[eE][+-]?\d+)?$/.test(s) || /^-?\d*\.?\d+(?:[eE][+-]?\d+)?$/.test(s)) { + const f = parseFloat(s); + if (!isNaN(f)) { + return f; + } + } + // String + return s; + } + validateField(key, value) { + if (key === 'action' || key === 'tool') { + return; + } + const schema = this.registry.get(this.toolName); + if (!schema) { + return; + } + const argSchema = schema.args[key]; + if (!argSchema) { + return; + } + // Numeric constraints + if (typeof value === 'number') { + if (argSchema.min !== undefined && value < argSchema.min) { + this.addError(ErrorCode.ConstraintMin, `${key} < ${argSchema.min}`, key); + } + if (argSchema.max !== undefined && value > argSchema.max) { + this.addError(ErrorCode.ConstraintMax, `${key} > ${argSchema.max}`, key); + } + } + // String constraints + if (typeof value === 'string') { + if (argSchema.minLen !== undefined && value.length < argSchema.minLen) { + this.addError(ErrorCode.ConstraintLen, `${key} length < ${argSchema.minLen}`, key); + } + if (argSchema.maxLen !== undefined && value.length > argSchema.maxLen) { + this.addError(ErrorCode.ConstraintLen, `${key} length > ${argSchema.maxLen}`, key); + } + if (argSchema.pattern && !argSchema.pattern.test(value)) { + this.addError(ErrorCode.ConstraintPattern, `${key} pattern mismatch`, key); + } + if (argSchema.enumValues && !argSchema.enumValues.includes(value)) { + this.addError(ErrorCode.ConstraintEnum, `${key} not in allowed values`, key); + } + } + } + validateComplete() { + if (!this.toolName) { + this.addError(ErrorCode.MissingTool, 'No action field found'); + return; + } + const schema = this.registry.get(this.toolName); + if (!schema) { + return; + } + // Check required fields + for (const [argName, argSchema] of Object.entries(schema.args)) { + if (argSchema.required && !(argName in this.fields)) { + this.addError(ErrorCode.MissingRequired, `Missing required field: ${argName}`, argName); + } + } + } + /** + * Get the current validation result. + */ + getResult() { + const toolAllowed = this.toolName ? this.registry.isAllowed(this.toolName) : null; + return { + complete: this.state === ValidatorState.Complete, + valid: this.errors.length === 0, + state: this.state, + toolName: this.toolName, + toolAllowed, + errors: [...this.errors], + fields: { ...this.fields }, + tokenCount: this.tokenCount, + charCount: this.charCount, + timeline: [...this.timeline], + toolDetectedAtToken: this.toolDetectedAtToken, + toolDetectedAtChar: this.toolDetectedAtChar, + toolDetectedAtTime: this.toolDetectedAtTime, + firstErrorAtToken: this.firstErrorAtToken, + firstErrorAtTime: this.firstErrorAtTime, + completeAtToken: this.completeAtToken, + completeAtTime: this.completeAtTime, + }; + } + /** + * Check if the stream should be cancelled. + */ + shouldStop() { + return this.errors.some(e => e.code === ErrorCode.UnknownTool || e.code === ErrorCode.LimitExceeded); + } + /** + * Check if the detected tool is allowed. + * Returns false if no tool detected or registry not configured. + */ + isToolAllowed() { + if (!this.toolName) { + return false; + } + return this.registry.isAllowed(this.toolName); + } + /** + * Get the parsed fields if validation is complete and valid. + * Returns null if not complete or if there are errors. + */ + getParsed() { + if (this.state === ValidatorState.Complete && this.errors.length === 0) { + return { ...this.fields }; + } + return null; + } +} +exports.StreamingValidator = StreamingValidator; +// ============================================================ +// Default Registry +// ============================================================ +/** + * Create a default tool registry with common tools. + */ +function defaultToolRegistry() { + const registry = new ToolRegistry(); + registry.register({ + name: 'search', + description: 'Search for information', + args: { + query: { type: 'string', required: true, minLen: 1 }, + max_results: { type: 'int', min: 1, max: 100 }, + }, + }); + registry.register({ + name: 'calculate', + description: 'Evaluate a mathematical expression', + args: { + expression: { type: 'string', required: true }, + precision: { type: 'int', min: 0, max: 15 }, + }, + }); + registry.register({ + name: 'browse', + description: 'Fetch a web page', + args: { + url: { type: 'string', required: true, pattern: /^https?:\/\// }, + }, + }); + registry.register({ + name: 'execute', + description: 'Execute a shell command', + args: { + command: { type: 'string', required: true }, + }, + }); + registry.register({ + name: 'read_file', + description: 'Read a file from disk', + args: { + path: { type: 'string', required: true }, + limit: { type: 'int', min: 1 }, + }, + }); + registry.register({ + name: 'write_file', + description: 'Write content to a file', + args: { + path: { type: 'string', required: true }, + content: { type: 'string', required: true }, + }, + }); + return registry; +} +//# sourceMappingURL=stream_validator.js.map \ No newline at end of file diff --git a/js/dist/stream_validator.js.map b/js/dist/stream_validator.js.map new file mode 100644 index 0000000..647ae1a --- /dev/null +++ b/js/dist/stream_validator.js.map @@ -0,0 +1 @@ +{"version":3,"file":"stream_validator.js","sourceRoot":"","sources":["../src/stream_validator.ts"],"names":[],"mappings":"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"} \ No newline at end of file