ata-validator 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1574 +0,0 @@
1
- #include <napi.h>
2
- #include <node_api.h>
3
-
4
- #include <cmath>
5
- #include <thread>
6
- #include <future>
7
- #include <mutex>
8
- #include <condition_variable>
9
- #include <functional>
10
- #include <queue>
11
- #include <atomic>
12
- #ifndef ATA_NO_RE2
13
- #include <re2/re2.h>
14
- #endif
15
- #include <set>
16
- #include <string>
17
- #include <vector>
18
-
19
- #include "ata.h"
20
-
21
- // ============================================================================
22
- // V8 Direct Object Traversal Engine
23
- // Validates Napi::Value directly without JSON.stringify + simdjson parse
24
- // ============================================================================
25
-
26
- // Forward declare - we need access to compiled schema internals
27
- // We include the schema_node definition here to avoid modifying ata.h
28
- struct schema_node;
29
- using schema_node_ptr = std::shared_ptr<schema_node>;
30
-
31
- // MUST match layout in src/ata.cpp exactly (reinterpret_cast)
32
- struct schema_node {
33
- uint8_t type_mask = 0;
34
-
35
- std::optional<double> minimum;
36
- std::optional<double> maximum;
37
- std::optional<double> exclusive_minimum;
38
- std::optional<double> exclusive_maximum;
39
- std::optional<double> multiple_of;
40
-
41
- std::optional<uint64_t> min_length;
42
- std::optional<uint64_t> max_length;
43
- std::optional<std::string> pattern;
44
- #ifndef ATA_NO_RE2
45
- std::shared_ptr<re2::RE2> compiled_pattern;
46
- #endif
47
-
48
- std::optional<uint64_t> min_items;
49
- std::optional<uint64_t> max_items;
50
- bool unique_items = false;
51
- schema_node_ptr items_schema;
52
- std::vector<schema_node_ptr> prefix_items;
53
- schema_node_ptr contains_schema;
54
- std::optional<uint64_t> min_contains;
55
- std::optional<uint64_t> max_contains;
56
-
57
- std::unordered_map<std::string, schema_node_ptr> properties;
58
- std::vector<std::string> required;
59
- std::optional<bool> additional_properties_bool;
60
- schema_node_ptr additional_properties_schema;
61
- std::optional<uint64_t> min_properties;
62
- std::optional<uint64_t> max_properties;
63
- schema_node_ptr property_names_schema;
64
- std::unordered_map<std::string, std::vector<std::string>> dependent_required;
65
- std::unordered_map<std::string, schema_node_ptr> dependent_schemas;
66
-
67
- struct pattern_prop {
68
- std::string pattern;
69
- schema_node_ptr schema;
70
- #ifndef ATA_NO_RE2
71
- std::shared_ptr<re2::RE2> compiled;
72
- #endif
73
- };
74
- std::vector<pattern_prop> pattern_properties;
75
-
76
- std::vector<std::string> enum_values_minified;
77
- std::optional<std::string> const_value_raw;
78
-
79
- std::optional<std::string> format;
80
- uint8_t format_id = 255;
81
-
82
- std::vector<schema_node_ptr> all_of;
83
- std::vector<schema_node_ptr> any_of;
84
- std::vector<schema_node_ptr> one_of;
85
- schema_node_ptr not_schema;
86
-
87
- schema_node_ptr if_schema;
88
- schema_node_ptr then_schema;
89
- schema_node_ptr else_schema;
90
-
91
- std::string ref;
92
- std::string dynamic_ref;
93
- std::string id;
94
-
95
- std::unordered_map<std::string, schema_node_ptr> defs;
96
-
97
- std::optional<bool> boolean_schema;
98
- };
99
-
100
- struct compiled_schema_internal {
101
- schema_node_ptr root;
102
- std::unordered_map<std::string, schema_node_ptr> defs;
103
- };
104
-
105
- // --- Fast format validators (no regex) ---
106
-
107
- static bool nb_is_digit(char c) { return c >= '0' && c <= '9'; }
108
- static bool nb_is_alpha(char c) {
109
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
110
- }
111
- static bool nb_is_alnum(char c) { return nb_is_alpha(c) || nb_is_digit(c); }
112
- static bool nb_is_hex(char c) {
113
- return nb_is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
114
- }
115
-
116
- static bool napi_check_format(const std::string& sv, const std::string& fmt) {
117
- if (fmt == "email") {
118
- auto at = sv.find('@');
119
- if (at == std::string::npos || at == 0 || at == sv.size() - 1) return false;
120
- auto dot = sv.find('.', at + 1);
121
- return dot != std::string::npos && dot != at + 1 && dot != sv.size() - 1 &&
122
- (sv.size() - dot - 1) >= 2;
123
- }
124
- if (fmt == "date") {
125
- if (sv.size() != 10 || !nb_is_digit(sv[0]) || !nb_is_digit(sv[1]) ||
126
- !nb_is_digit(sv[2]) || !nb_is_digit(sv[3]) || sv[4] != '-' ||
127
- !nb_is_digit(sv[5]) || !nb_is_digit(sv[6]) || sv[7] != '-' ||
128
- !nb_is_digit(sv[8]) || !nb_is_digit(sv[9]))
129
- return false;
130
- int month = (sv[5] - '0') * 10 + (sv[6] - '0');
131
- int day = (sv[8] - '0') * 10 + (sv[9] - '0');
132
- return month >= 1 && month <= 12 && day >= 1 && day <= 31;
133
- }
134
- if (fmt == "time") {
135
- if (sv.size() < 8) return false;
136
- return nb_is_digit(sv[0]) && nb_is_digit(sv[1]) && sv[2] == ':' &&
137
- nb_is_digit(sv[3]) && nb_is_digit(sv[4]) && sv[5] == ':' &&
138
- nb_is_digit(sv[6]) && nb_is_digit(sv[7]);
139
- }
140
- if (fmt == "date-time") {
141
- if (sv.size() < 19) return false;
142
- if (!napi_check_format(sv.substr(0, 10), "date")) return false;
143
- if (sv[10] != 'T' && sv[10] != 't' && sv[10] != ' ') return false;
144
- return napi_check_format(sv.substr(11), "time");
145
- }
146
- if (fmt == "ipv4") {
147
- int parts = 0, val = 0, digits = 0;
148
- for (size_t i = 0; i <= sv.size(); ++i) {
149
- if (i == sv.size() || sv[i] == '.') {
150
- if (digits == 0 || val > 255) return false;
151
- ++parts; val = 0; digits = 0;
152
- } else if (nb_is_digit(sv[i])) {
153
- val = val * 10 + (sv[i] - '0'); ++digits;
154
- if (digits > 3) return false;
155
- } else {
156
- return false;
157
- }
158
- }
159
- return parts == 4;
160
- }
161
- if (fmt == "ipv6") return sv.find(':') != std::string::npos;
162
- if (fmt == "uri" || fmt == "uri-reference") {
163
- if (sv.size() < 3 || !nb_is_alpha(sv[0])) return false;
164
- size_t i = 1;
165
- while (i < sv.size() && (nb_is_alnum(sv[i]) || sv[i] == '+' || sv[i] == '-' || sv[i] == '.')) ++i;
166
- return i < sv.size() && sv[i] == ':' && i + 1 < sv.size();
167
- }
168
- if (fmt == "uuid") {
169
- if (sv.size() != 36) return false;
170
- for (size_t i = 0; i < 36; ++i) {
171
- if (i == 8 || i == 13 || i == 18 || i == 23) {
172
- if (sv[i] != '-') return false;
173
- } else {
174
- if (!nb_is_hex(sv[i])) return false;
175
- }
176
- }
177
- return true;
178
- }
179
- if (fmt == "hostname") {
180
- if (sv.empty() || sv.size() > 253) return false;
181
- size_t label_len = 0;
182
- for (size_t i = 0; i < sv.size(); ++i) {
183
- if (sv[i] == '.') { if (label_len == 0) return false; label_len = 0; }
184
- else if (nb_is_alnum(sv[i]) || sv[i] == '-') { ++label_len; if (label_len > 63) return false; }
185
- else return false;
186
- }
187
- return label_len > 0;
188
- }
189
- return true;
190
- }
191
-
192
- // --- V8 Direct Validator ---
193
-
194
- static std::string napi_type_of(Napi::Value val) {
195
- if (val.IsNull()) return "null";
196
- if (val.IsBoolean()) return "boolean";
197
- if (val.IsNumber()) {
198
- double d = val.As<Napi::Number>().DoubleValue();
199
- if (std::isfinite(d) && d == std::floor(d) &&
200
- std::abs(d) <= 9007199254740991.0) {
201
- return "integer";
202
- }
203
- return "number";
204
- }
205
- if (val.IsString()) return "string";
206
- if (val.IsArray()) return "array";
207
- if (val.IsObject()) return "object";
208
- return "unknown";
209
- }
210
-
211
- static bool napi_type_matches(Napi::Value val, const std::string& type) {
212
- auto actual = napi_type_of(val);
213
- if (actual == type) return true;
214
- if (type == "number" && (actual == "integer" || actual == "number"))
215
- return true;
216
- return false;
217
- }
218
-
219
- static uint64_t utf8_codepoint_length(const std::string& s) {
220
- uint64_t len = 0;
221
- for (size_t i = 0; i < s.size();) {
222
- unsigned char c = static_cast<unsigned char>(s[i]);
223
- if (c < 0x80) i += 1;
224
- else if ((c >> 5) == 0x06) i += 2;
225
- else if ((c >> 4) == 0x0E) i += 3;
226
- else if ((c >> 3) == 0x1E) i += 4;
227
- else i += 1;
228
- ++len;
229
- }
230
- return len;
231
- }
232
-
233
- // Serialize a Napi::Value to a minified JSON string (for enum/const comparison)
234
- // Canonical JSON: sort object keys for semantic equality comparison
235
- static std::string napi_canonical_json(Napi::Env env, Napi::Value val) {
236
- if (val.IsNull() || val.IsUndefined()) return "null";
237
- if (val.IsBoolean()) return val.As<Napi::Boolean>().Value() ? "true" : "false";
238
- if (val.IsNumber()) {
239
- double d = val.As<Napi::Number>().DoubleValue();
240
- if (d == static_cast<int64_t>(d) && std::abs(d) <= 9007199254740991.0) {
241
- return std::to_string(static_cast<int64_t>(d));
242
- }
243
- auto json = env.Global().Get("JSON").As<Napi::Object>();
244
- auto stringify = json.Get("stringify").As<Napi::Function>();
245
- auto r = stringify.Call(json, {val});
246
- return r.IsString() ? r.As<Napi::String>().Utf8Value() : "null";
247
- }
248
- if (val.IsString()) {
249
- // JSON-encode the string
250
- auto json = env.Global().Get("JSON").As<Napi::Object>();
251
- auto stringify = json.Get("stringify").As<Napi::Function>();
252
- auto r = stringify.Call(json, {val});
253
- return r.IsString() ? r.As<Napi::String>().Utf8Value() : "null";
254
- }
255
- if (val.IsArray()) {
256
- auto arr = val.As<Napi::Array>();
257
- std::string r = "[";
258
- for (uint32_t i = 0; i < arr.Length(); ++i) {
259
- if (i) r += ',';
260
- r += napi_canonical_json(env, arr.Get(i));
261
- }
262
- r += ']';
263
- return r;
264
- }
265
- if (val.IsObject()) {
266
- auto obj = val.As<Napi::Object>();
267
- auto keys = obj.GetPropertyNames();
268
- std::vector<std::string> sorted_keys;
269
- for (uint32_t i = 0; i < keys.Length(); ++i) {
270
- sorted_keys.push_back(keys.Get(i).As<Napi::String>().Utf8Value());
271
- }
272
- std::sort(sorted_keys.begin(), sorted_keys.end());
273
- std::string r = "{";
274
- for (size_t i = 0; i < sorted_keys.size(); ++i) {
275
- if (i) r += ',';
276
- // JSON-encode the key
277
- auto json = env.Global().Get("JSON").As<Napi::Object>();
278
- auto stringify = json.Get("stringify").As<Napi::Function>();
279
- auto k = stringify.Call(json, {Napi::String::New(env, sorted_keys[i])});
280
- r += k.As<Napi::String>().Utf8Value();
281
- r += ':';
282
- r += napi_canonical_json(env, obj.Get(sorted_keys[i]));
283
- }
284
- r += '}';
285
- return r;
286
- }
287
- return "null";
288
- }
289
-
290
- static std::string napi_to_json(Napi::Env env, Napi::Value val) {
291
- return napi_canonical_json(env, val);
292
- }
293
-
294
- static void validate_napi(const schema_node_ptr& node,
295
- Napi::Value value,
296
- Napi::Env env,
297
- const std::string& path,
298
- const compiled_schema_internal& ctx,
299
- std::vector<ata::validation_error>& errors);
300
-
301
- // Recursion depth guard — prevents stack overflow on self-referencing schemas
302
- struct NapiDepthGuard {
303
- static thread_local int depth;
304
- bool overflow;
305
- NapiDepthGuard() : overflow(++depth > 100) {}
306
- ~NapiDepthGuard() { --depth; }
307
- };
308
- thread_local int NapiDepthGuard::depth = 0;
309
-
310
- static void validate_napi(const schema_node_ptr& node,
311
- Napi::Value value,
312
- Napi::Env env,
313
- const std::string& path,
314
- const compiled_schema_internal& ctx,
315
- std::vector<ata::validation_error>& errors) {
316
- if (!node) return;
317
-
318
- NapiDepthGuard guard;
319
- if (guard.overflow) return;
320
-
321
- // Boolean schema
322
- if (node->boolean_schema.has_value()) {
323
- if (!node->boolean_schema.value()) {
324
- errors.push_back({ata::error_code::type_mismatch, path,
325
- "schema is false, no value is valid"});
326
- }
327
- return;
328
- }
329
-
330
- // $ref — Draft 2020-12: $ref is not a short-circuit, sibling keywords still apply
331
- bool ref_resolved = false;
332
- if (!node->ref.empty()) {
333
- auto it = ctx.defs.find(node->ref);
334
- if (it != ctx.defs.end()) {
335
- validate_napi(it->second, value, env, path, ctx, errors);
336
- ref_resolved = true;
337
- }
338
- if (!ref_resolved && node->ref.size() > 1 && node->ref[0] == '#' &&
339
- node->ref[1] == '/') {
340
- // Decode JSON Pointer segments
341
- auto decode_seg = [](const std::string& seg) -> std::string {
342
- std::string pct;
343
- for (size_t i = 0; i < seg.size(); ++i) {
344
- if (seg[i] == '%' && i + 2 < seg.size()) {
345
- auto hex = [](char c) -> int {
346
- if (c >= '0' && c <= '9') return c - '0';
347
- if (c >= 'a' && c <= 'f') return 10 + c - 'a';
348
- if (c >= 'A' && c <= 'F') return 10 + c - 'A';
349
- return -1;
350
- };
351
- int hv = hex(seg[i+1]), lv = hex(seg[i+2]);
352
- if (hv >= 0 && lv >= 0) { pct += static_cast<char>(hv * 16 + lv); i += 2; }
353
- else pct += seg[i];
354
- } else pct += seg[i];
355
- }
356
- std::string out;
357
- for (size_t i = 0; i < pct.size(); ++i) {
358
- if (pct[i] == '~' && i + 1 < pct.size()) {
359
- if (pct[i+1] == '1') { out += '/'; ++i; }
360
- else if (pct[i+1] == '0') { out += '~'; ++i; }
361
- else out += pct[i];
362
- } else out += pct[i];
363
- }
364
- return out;
365
- };
366
- std::string pointer = node->ref.substr(2);
367
- std::vector<std::string> segments;
368
- size_t spos = 0;
369
- while (spos < pointer.size()) {
370
- size_t snext = pointer.find('/', spos);
371
- segments.push_back(decode_seg(
372
- pointer.substr(spos, snext == std::string::npos ? snext : snext - spos)));
373
- spos = (snext == std::string::npos) ? pointer.size() : snext + 1;
374
- }
375
- schema_node_ptr current = ctx.root;
376
- bool resolved = true;
377
- for (size_t si = 0; si < segments.size() && current; ++si) {
378
- const auto& key = segments[si];
379
- if (key == "properties" && si + 1 < segments.size()) {
380
- auto pit = current->properties.find(segments[++si]);
381
- if (pit != current->properties.end()) current = pit->second;
382
- else { resolved = false; break; }
383
- } else if (key == "items" && current->items_schema) {
384
- current = current->items_schema;
385
- } else if (key == "$defs" || key == "definitions") {
386
- if (si + 1 < segments.size()) {
387
- const auto& def_name = segments[++si];
388
- auto dit = current->defs.find(def_name);
389
- if (dit != current->defs.end()) current = dit->second;
390
- else {
391
- auto cit = ctx.defs.find("#/" + key + "/" + def_name);
392
- if (cit != ctx.defs.end()) current = cit->second;
393
- else { resolved = false; break; }
394
- }
395
- } else { resolved = false; break; }
396
- } else if (key == "allOf" || key == "anyOf" || key == "oneOf") {
397
- if (si + 1 < segments.size()) {
398
- size_t idx = std::stoul(segments[++si]);
399
- auto& vec = (key == "allOf") ? current->all_of
400
- : (key == "anyOf") ? current->any_of : current->one_of;
401
- if (idx < vec.size()) current = vec[idx];
402
- else { resolved = false; break; }
403
- } else { resolved = false; break; }
404
- } else if (key == "not" && current->not_schema) {
405
- current = current->not_schema;
406
- } else if (key == "if" && current->if_schema) {
407
- current = current->if_schema;
408
- } else if (key == "then" && current->then_schema) {
409
- current = current->then_schema;
410
- } else if (key == "else" && current->else_schema) {
411
- current = current->else_schema;
412
- } else if (key == "additionalProperties" &&
413
- current->additional_properties_schema) {
414
- current = current->additional_properties_schema;
415
- } else if (key == "prefixItems") {
416
- if (si + 1 < segments.size()) {
417
- size_t idx = std::stoul(segments[++si]);
418
- if (idx < current->prefix_items.size()) current = current->prefix_items[idx];
419
- else { resolved = false; break; }
420
- } else { resolved = false; break; }
421
- } else { resolved = false; break; }
422
- }
423
- if (resolved && current) {
424
- validate_napi(current, value, env, path, ctx, errors);
425
- ref_resolved = true;
426
- }
427
- }
428
- if (!ref_resolved && node->ref == "#" && ctx.root) {
429
- validate_napi(ctx.root, value, env, path, ctx, errors);
430
- ref_resolved = true;
431
- }
432
- if (!ref_resolved) {
433
- errors.push_back({ata::error_code::ref_not_found, path,
434
- "cannot resolve $ref: " + node->ref});
435
- }
436
- }
437
-
438
- auto actual_type = napi_type_of(value);
439
-
440
- // type — uses bitmask matching ata.cpp json_type enum order:
441
- // 0=string, 1=number, 2=integer, 3=boolean, 4=null_value, 5=object, 6=array
442
- if (node->type_mask) {
443
- uint8_t val_bits = 0;
444
- if (actual_type == "string") val_bits = 1u << 0;
445
- else if (actual_type == "number") val_bits = 1u << 1;
446
- else if (actual_type == "integer") val_bits = (1u << 2) | (1u << 1); // integer matches number
447
- else if (actual_type == "boolean") val_bits = 1u << 3;
448
- else if (actual_type == "null") val_bits = 1u << 4;
449
- else if (actual_type == "object") val_bits = 1u << 5;
450
- else if (actual_type == "array") val_bits = 1u << 6;
451
- if (!(val_bits & node->type_mask)) {
452
- static const char* type_names[] = {"string","number","integer","boolean","null","object","array"};
453
- std::string expected;
454
- for (int b = 0; b < 7; ++b) {
455
- if (node->type_mask & (1u << b)) {
456
- if (!expected.empty()) expected += ", ";
457
- expected += type_names[b];
458
- }
459
- }
460
- errors.push_back({ata::error_code::type_mismatch, path,
461
- "expected type " + expected + ", got " + actual_type});
462
- }
463
- }
464
-
465
- // enum — compare against pre-minified canonical values
466
- if (!node->enum_values_minified.empty()) {
467
- std::string val_json = napi_to_json(env, value);
468
- bool found = false;
469
- for (const auto& ev : node->enum_values_minified) {
470
- if (ev == val_json) {
471
- found = true;
472
- break;
473
- }
474
- }
475
- if (!found) {
476
- errors.push_back({ata::error_code::enum_mismatch, path,
477
- "value not in enum"});
478
- }
479
- }
480
-
481
- // const
482
- if (node->const_value_raw.has_value()) {
483
- std::string val_json = napi_to_json(env, value);
484
- if (val_json != node->const_value_raw.value()) {
485
- errors.push_back({ata::error_code::const_mismatch, path,
486
- "value does not match const"});
487
- }
488
- }
489
-
490
- // Numeric validations
491
- if (actual_type == "integer" || actual_type == "number") {
492
- double v = value.As<Napi::Number>().DoubleValue();
493
- if (node->minimum.has_value() && v < node->minimum.value()) {
494
- errors.push_back({ata::error_code::minimum_violation, path,
495
- "value " + std::to_string(v) + " < minimum " +
496
- std::to_string(node->minimum.value())});
497
- }
498
- if (node->maximum.has_value() && v > node->maximum.value()) {
499
- errors.push_back({ata::error_code::maximum_violation, path,
500
- "value " + std::to_string(v) + " > maximum " +
501
- std::to_string(node->maximum.value())});
502
- }
503
- if (node->exclusive_minimum.has_value() &&
504
- v <= node->exclusive_minimum.value()) {
505
- errors.push_back({ata::error_code::exclusive_minimum_violation, path,
506
- "value must be > " +
507
- std::to_string(node->exclusive_minimum.value())});
508
- }
509
- if (node->exclusive_maximum.has_value() &&
510
- v >= node->exclusive_maximum.value()) {
511
- errors.push_back({ata::error_code::exclusive_maximum_violation, path,
512
- "value must be < " +
513
- std::to_string(node->exclusive_maximum.value())});
514
- }
515
- if (node->multiple_of.has_value()) {
516
- double divisor = node->multiple_of.value();
517
- double rem = std::fmod(v, divisor);
518
- if (std::abs(rem) > 1e-8 && std::abs(rem - divisor) > 1e-8) {
519
- errors.push_back({ata::error_code::multiple_of_violation, path,
520
- "value not a multiple of " +
521
- std::to_string(node->multiple_of.value())});
522
- }
523
- }
524
- }
525
-
526
- // String validations
527
- if (actual_type == "string") {
528
- std::string sv = value.As<Napi::String>().Utf8Value();
529
- uint64_t len = utf8_codepoint_length(sv);
530
-
531
- if (node->min_length.has_value() && len < node->min_length.value()) {
532
- errors.push_back({ata::error_code::min_length_violation, path,
533
- "string length " + std::to_string(len) +
534
- " < minLength " +
535
- std::to_string(node->min_length.value())});
536
- }
537
- if (node->max_length.has_value() && len > node->max_length.value()) {
538
- errors.push_back({ata::error_code::max_length_violation, path,
539
- "string length " + std::to_string(len) +
540
- " > maxLength " +
541
- std::to_string(node->max_length.value())});
542
- }
543
- #ifndef ATA_NO_RE2
544
- if (node->compiled_pattern) {
545
- if (!re2::RE2::PartialMatch(sv, *node->compiled_pattern)) {
546
- errors.push_back({ata::error_code::pattern_mismatch, path,
547
- "string does not match pattern: " +
548
- node->pattern.value()});
549
- }
550
- }
551
- #endif
552
- if (node->format.has_value()) {
553
- const auto& fmt = node->format.value();
554
- bool format_ok = napi_check_format(sv, fmt);
555
- if (!format_ok) {
556
- errors.push_back({ata::error_code::format_mismatch, path,
557
- "string does not match format: " + fmt});
558
- }
559
- }
560
- }
561
-
562
- // Array validations
563
- if (actual_type == "array" && value.IsArray()) {
564
- auto arr = value.As<Napi::Array>();
565
- uint32_t arr_size = arr.Length();
566
-
567
- if (node->min_items.has_value() && arr_size < node->min_items.value()) {
568
- errors.push_back({ata::error_code::min_items_violation, path,
569
- "array has " + std::to_string(arr_size) +
570
- " items, minimum " +
571
- std::to_string(node->min_items.value())});
572
- }
573
- if (node->max_items.has_value() && arr_size > node->max_items.value()) {
574
- errors.push_back({ata::error_code::max_items_violation, path,
575
- "array has " + std::to_string(arr_size) +
576
- " items, maximum " +
577
- std::to_string(node->max_items.value())});
578
- }
579
-
580
- if (node->unique_items) {
581
- std::set<std::string> seen;
582
- bool has_dup = false;
583
- for (uint32_t i = 0; i < arr_size; ++i) {
584
- auto s = napi_to_json(env, arr.Get(i));
585
- if (!seen.insert(s).second) {
586
- has_dup = true;
587
- break;
588
- }
589
- }
590
- if (has_dup) {
591
- errors.push_back({ata::error_code::unique_items_violation, path,
592
- "array contains duplicate items"});
593
- }
594
- }
595
-
596
- // prefixItems + items (Draft 2020-12 semantics)
597
- for (uint32_t i = 0; i < arr_size; ++i) {
598
- if (i < node->prefix_items.size()) {
599
- validate_napi(node->prefix_items[i], arr.Get(i), env,
600
- path + "/" + std::to_string(i), ctx, errors);
601
- } else if (node->items_schema) {
602
- validate_napi(node->items_schema, arr.Get(i), env,
603
- path + "/" + std::to_string(i), ctx, errors);
604
- }
605
- }
606
-
607
- // contains / minContains / maxContains
608
- if (node->contains_schema) {
609
- uint64_t match_count = 0;
610
- for (uint32_t i = 0; i < arr_size; ++i) {
611
- std::vector<ata::validation_error> tmp;
612
- validate_napi(node->contains_schema, arr.Get(i), env, path, ctx, tmp);
613
- if (tmp.empty()) ++match_count;
614
- }
615
- uint64_t min_c = node->min_contains.value_or(1);
616
- uint64_t max_c = node->max_contains.value_or(arr_size);
617
- if (match_count < min_c) {
618
- errors.push_back({ata::error_code::min_items_violation, path,
619
- "contains: " + std::to_string(match_count) +
620
- " matches, minimum " + std::to_string(min_c)});
621
- }
622
- if (match_count > max_c) {
623
- errors.push_back({ata::error_code::max_items_violation, path,
624
- "contains: " + std::to_string(match_count) +
625
- " matches, maximum " + std::to_string(max_c)});
626
- }
627
- }
628
- }
629
-
630
- // Object validations
631
- if (actual_type == "object" && value.IsObject() && !value.IsArray()) {
632
- auto obj = value.As<Napi::Object>();
633
- auto keys = obj.GetPropertyNames();
634
- uint32_t prop_count = keys.Length();
635
-
636
- if (node->min_properties.has_value() &&
637
- prop_count < node->min_properties.value()) {
638
- errors.push_back({ata::error_code::min_properties_violation, path,
639
- "object has " + std::to_string(prop_count) +
640
- " properties, minimum " +
641
- std::to_string(node->min_properties.value())});
642
- }
643
- if (node->max_properties.has_value() &&
644
- prop_count > node->max_properties.value()) {
645
- errors.push_back({ata::error_code::max_properties_violation, path,
646
- "object has " + std::to_string(prop_count) +
647
- " properties, maximum " +
648
- std::to_string(node->max_properties.value())});
649
- }
650
-
651
- // required — use HasOwnProperty to avoid prototype pollution
652
- for (const auto& req : node->required) {
653
- bool has = obj.HasOwnProperty(req);
654
- if (!has) {
655
- errors.push_back({ata::error_code::required_property_missing, path,
656
- "missing required property: " + req});
657
- }
658
- }
659
-
660
- // properties + patternProperties + additionalProperties
661
- for (uint32_t i = 0; i < prop_count; ++i) {
662
- std::string key_str = keys.Get(i).As<Napi::String>().Utf8Value();
663
- Napi::Value val = obj.Get(key_str);
664
- bool matched = false;
665
-
666
- auto it = node->properties.find(key_str);
667
- if (it != node->properties.end()) {
668
- validate_napi(it->second, val, env, path + "/" + key_str, ctx, errors);
669
- matched = true;
670
- }
671
-
672
- for (const auto& pp : node->pattern_properties) {
673
- #ifndef ATA_NO_RE2
674
- if (pp.compiled && re2::RE2::PartialMatch(key_str, *pp.compiled)) {
675
- validate_napi(pp.schema, val, env, path + "/" + key_str, ctx,
676
- errors);
677
- matched = true;
678
- }
679
- #endif
680
- }
681
-
682
- if (!matched) {
683
- if (node->additional_properties_bool.has_value() &&
684
- !node->additional_properties_bool.value()) {
685
- errors.push_back(
686
- {ata::error_code::additional_property_not_allowed, path,
687
- "additional property not allowed: " + key_str});
688
- } else if (node->additional_properties_schema) {
689
- validate_napi(node->additional_properties_schema, val, env,
690
- path + "/" + key_str, ctx, errors);
691
- }
692
- }
693
- }
694
-
695
- // propertyNames
696
- if (node->property_names_schema) {
697
- for (uint32_t i = 0; i < prop_count; ++i) {
698
- Napi::Value key_val = keys.Get(i);
699
- validate_napi(node->property_names_schema, key_val, env, path, ctx,
700
- errors);
701
- }
702
- }
703
-
704
- // dependentRequired
705
- for (const auto& [prop, deps] : node->dependent_required) {
706
- if (obj.HasOwnProperty(prop)) {
707
- for (const auto& dep : deps) {
708
- if (!obj.HasOwnProperty(dep)) {
709
- errors.push_back({ata::error_code::required_property_missing, path,
710
- "property '" + prop + "' requires '" + dep +
711
- "' to be present"});
712
- }
713
- }
714
- }
715
- }
716
-
717
- // dependentSchemas
718
- for (const auto& [prop, schema] : node->dependent_schemas) {
719
- if (obj.HasOwnProperty(prop)) {
720
- validate_napi(schema, value, env, path, ctx, errors);
721
- }
722
- }
723
- }
724
-
725
- // allOf
726
- if (!node->all_of.empty()) {
727
- for (const auto& sub : node->all_of) {
728
- std::vector<ata::validation_error> sub_errors;
729
- validate_napi(sub, value, env, path, ctx, sub_errors);
730
- if (!sub_errors.empty()) {
731
- errors.push_back({ata::error_code::all_of_failed, path,
732
- "allOf subschema failed"});
733
- errors.insert(errors.end(), sub_errors.begin(), sub_errors.end());
734
- }
735
- }
736
- }
737
-
738
- // anyOf
739
- if (!node->any_of.empty()) {
740
- bool any_valid = false;
741
- for (const auto& sub : node->any_of) {
742
- std::vector<ata::validation_error> sub_errors;
743
- validate_napi(sub, value, env, path, ctx, sub_errors);
744
- if (sub_errors.empty()) {
745
- any_valid = true;
746
- break;
747
- }
748
- }
749
- if (!any_valid) {
750
- errors.push_back({ata::error_code::any_of_failed, path,
751
- "no anyOf subschema matched"});
752
- }
753
- }
754
-
755
- // oneOf
756
- if (!node->one_of.empty()) {
757
- int match_count = 0;
758
- for (const auto& sub : node->one_of) {
759
- std::vector<ata::validation_error> sub_errors;
760
- validate_napi(sub, value, env, path, ctx, sub_errors);
761
- if (sub_errors.empty()) ++match_count;
762
- }
763
- if (match_count != 1) {
764
- errors.push_back({ata::error_code::one_of_failed, path,
765
- "expected exactly one oneOf match, got " +
766
- std::to_string(match_count)});
767
- }
768
- }
769
-
770
- // not
771
- if (node->not_schema) {
772
- std::vector<ata::validation_error> sub_errors;
773
- validate_napi(node->not_schema, value, env, path, ctx, sub_errors);
774
- if (sub_errors.empty()) {
775
- errors.push_back({ata::error_code::not_failed, path,
776
- "value should not match 'not' schema"});
777
- }
778
- }
779
-
780
- // if/then/else
781
- if (node->if_schema) {
782
- std::vector<ata::validation_error> if_errors;
783
- validate_napi(node->if_schema, value, env, path, ctx, if_errors);
784
- if (if_errors.empty()) {
785
- if (node->then_schema) {
786
- validate_napi(node->then_schema, value, env, path, ctx, errors);
787
- }
788
- } else {
789
- if (node->else_schema) {
790
- validate_napi(node->else_schema, value, env, path, ctx, errors);
791
- }
792
- }
793
- }
794
- }
795
-
796
- // ============================================================================
797
- // N-API Binding
798
- // ============================================================================
799
-
800
- static Napi::Object make_result(Napi::Env env,
801
- const ata::validation_result& result) {
802
- Napi::Object obj = Napi::Object::New(env);
803
- obj.Set("valid", Napi::Boolean::New(env, result.valid));
804
- Napi::Array errors = Napi::Array::New(env, result.errors.size());
805
- for (size_t i = 0; i < result.errors.size(); ++i) {
806
- Napi::Object err = Napi::Object::New(env);
807
- err.Set("code",
808
- Napi::Number::New(env, static_cast<int>(result.errors[i].code)));
809
- err.Set("path", Napi::String::New(env, result.errors[i].path));
810
- err.Set("message", Napi::String::New(env, result.errors[i].message));
811
- errors[i] = err;
812
- }
813
- obj.Set("errors", errors);
814
- return obj;
815
- }
816
-
817
- class CompiledSchema : public Napi::ObjectWrap<CompiledSchema> {
818
- public:
819
- static Napi::Object Init(Napi::Env env, Napi::Object exports) {
820
- Napi::Function func = DefineClass(
821
- env, "CompiledSchema",
822
- {InstanceMethod("validate", &CompiledSchema::Validate),
823
- InstanceMethod("validateJSON", &CompiledSchema::ValidateJSON),
824
- InstanceMethod("validateDirect", &CompiledSchema::ValidateDirect),
825
- InstanceMethod("isValidJSON", &CompiledSchema::IsValidJSON)});
826
- auto* constructor = new Napi::FunctionReference();
827
- *constructor = Napi::Persistent(func);
828
- env.SetInstanceData(constructor);
829
- exports.Set("CompiledSchema", func);
830
- return exports;
831
- }
832
-
833
- CompiledSchema(const Napi::CallbackInfo& info)
834
- : Napi::ObjectWrap<CompiledSchema>(info) {
835
- Napi::Env env = info.Env();
836
- if (info.Length() < 1 || !info[0].IsString()) {
837
- Napi::TypeError::New(env, "Schema JSON string expected")
838
- .ThrowAsJavaScriptException();
839
- return;
840
- }
841
- std::string schema_json = info[0].As<Napi::String>().Utf8Value();
842
- schema_ = ata::compile(schema_json);
843
- if (!schema_) {
844
- Napi::Error::New(env, "Failed to compile schema")
845
- .ThrowAsJavaScriptException();
846
- return;
847
- }
848
- // Store internal pointers for direct validation
849
- auto* impl = reinterpret_cast<compiled_schema_internal*>(schema_.impl.get());
850
- internal_root_ = impl->root;
851
- internal_defs_ = &impl->defs;
852
- }
853
-
854
- // Validate any JS value directly via V8 traversal (no stringify needed)
855
- Napi::Value Validate(const Napi::CallbackInfo& info) {
856
- Napi::Env env = info.Env();
857
- if (info.Length() < 1) {
858
- Napi::TypeError::New(env, "Argument expected")
859
- .ThrowAsJavaScriptException();
860
- return env.Undefined();
861
- }
862
- return ValidateDirectImpl(env, info[0]);
863
- }
864
-
865
- // Thread-local reusable buffer for string extraction — avoids per-call allocation.
866
- // Sized with SIMDJSON_PADDING so simdjson can read safely beyond the JSON.
867
- static constexpr size_t TL_BUF_SHRINK_THRESHOLD = 64 * 1024; // 64KB
868
-
869
- static std::string& tl_json_buf() {
870
- thread_local std::string buf;
871
- return buf;
872
- }
873
-
874
- // Extract JS string into reusable thread-local buffer with simdjson padding.
875
- // Returns {data, length} — data is valid until next call on same thread.
876
- static std::pair<const char*, size_t> extract_string(napi_env env, napi_value val) {
877
- size_t len = 0;
878
- napi_get_value_string_utf8(env, val, nullptr, 0, &len);
879
- auto& buf = tl_json_buf();
880
- const size_t needed = len + 1 + ata::REQUIRED_PADDING;
881
- if (buf.size() < needed) buf.resize(needed);
882
- napi_get_value_string_utf8(env, val, buf.data(), len + 1, &len);
883
- // Shrink back if a one-off large string bloated the buffer
884
- if (buf.size() > TL_BUF_SHRINK_THRESHOLD && len < TL_BUF_SHRINK_THRESHOLD / 2) {
885
- buf.resize(TL_BUF_SHRINK_THRESHOLD);
886
- buf.shrink_to_fit();
887
- }
888
- return {buf.data(), len};
889
- }
890
-
891
- // Validate via JSON string (simdjson parse path)
892
- Napi::Value ValidateJSON(const Napi::CallbackInfo& info) {
893
- Napi::Env env = info.Env();
894
- if (info.Length() < 1) {
895
- Napi::TypeError::New(env, "JSON string expected")
896
- .ThrowAsJavaScriptException();
897
- return env.Undefined();
898
- }
899
- // Support Buffer for zero-copy
900
- if (info[0].IsBuffer()) {
901
- auto buf = info[0].As<Napi::Buffer<char>>();
902
- auto result = ata::validate(schema_, std::string_view(buf.Data(), buf.Length()));
903
- return make_result(env, result);
904
- }
905
- if (!info[0].IsString()) {
906
- Napi::TypeError::New(env, "JSON string or Buffer expected")
907
- .ThrowAsJavaScriptException();
908
- return env.Undefined();
909
- }
910
- auto [data, len] = extract_string(env, info[0]);
911
- auto result = ata::validate(schema_, std::string_view(data, len));
912
- return make_result(env, result);
913
- }
914
-
915
- // Fast boolean-only validation — no error object creation
916
- Napi::Value IsValidJSON(const Napi::CallbackInfo& info) {
917
- Napi::Env env = info.Env();
918
- if (info.Length() < 1) {
919
- return Napi::Boolean::New(env, false);
920
- }
921
- // Support both String and Buffer inputs
922
- if (info[0].IsBuffer()) {
923
- auto buf = info[0].As<Napi::Buffer<char>>();
924
- auto result = ata::validate(schema_, std::string_view(buf.Data(), buf.Length()));
925
- return Napi::Boolean::New(env, result.valid);
926
- }
927
- if (!info[0].IsString()) {
928
- return Napi::Boolean::New(env, false);
929
- }
930
- auto [data, len] = extract_string(env, info[0]);
931
- // Buffer already has REQUIRED_PADDING — use zero-copy prepadded path
932
- bool valid = ata::is_valid_prepadded(schema_, data, len);
933
- return Napi::Boolean::New(env, valid);
934
- }
935
-
936
- // Explicit direct validation (always V8 traversal, never stringify)
937
- Napi::Value ValidateDirect(const Napi::CallbackInfo& info) {
938
- Napi::Env env = info.Env();
939
- if (info.Length() < 1) {
940
- Napi::TypeError::New(env, "Argument expected")
941
- .ThrowAsJavaScriptException();
942
- return env.Undefined();
943
- }
944
- return ValidateDirectImpl(env, info[0]);
945
- }
946
-
947
- private:
948
- Napi::Value ValidateDirectImpl(Napi::Env env, Napi::Value value) {
949
- compiled_schema_internal ctx;
950
- ctx.root = internal_root_;
951
- ctx.defs = *internal_defs_;
952
-
953
- std::vector<ata::validation_error> errors;
954
- validate_napi(internal_root_, value, env, "", ctx, errors);
955
-
956
- ata::validation_result result{errors.empty(), std::move(errors)};
957
- return make_result(env, result);
958
- }
959
-
960
- ata::schema_ref schema_;
961
- schema_node_ptr internal_root_;
962
- const std::unordered_map<std::string, schema_node_ptr>* internal_defs_ =
963
- nullptr;
964
- };
965
-
966
- // One-shot validate function (always V8 direct path)
967
- Napi::Value ValidateOneShot(const Napi::CallbackInfo& info) {
968
- Napi::Env env = info.Env();
969
- if (info.Length() < 2 || !info[0].IsString()) {
970
- Napi::TypeError::New(env, "Expected (schemaJson, data)")
971
- .ThrowAsJavaScriptException();
972
- return env.Undefined();
973
- }
974
- std::string schema_json = info[0].As<Napi::String>().Utf8Value();
975
-
976
- auto schema = ata::compile(schema_json);
977
- if (!schema) {
978
- Napi::Error::New(env, "Failed to compile schema")
979
- .ThrowAsJavaScriptException();
980
- return env.Undefined();
981
- }
982
- auto* impl =
983
- reinterpret_cast<compiled_schema_internal*>(schema.impl.get());
984
- compiled_schema_internal ctx;
985
- ctx.root = impl->root;
986
- ctx.defs = impl->defs;
987
-
988
- std::vector<ata::validation_error> errors;
989
- validate_napi(impl->root, info[1], env, "", ctx, errors);
990
-
991
- ata::validation_result result{errors.empty(), std::move(errors)};
992
- return make_result(env, result);
993
- }
994
-
995
- Napi::Value GetVersion(const Napi::CallbackInfo& info) {
996
- return Napi::String::New(info.Env(), std::string(ata::version()));
997
- }
998
-
999
- // --- Thread Pool ---
1000
- class ThreadPool {
1001
- public:
1002
- ThreadPool() {
1003
- unsigned n = std::thread::hardware_concurrency();
1004
- if (n == 0) n = 4;
1005
- for (unsigned i = 0; i < n; i++) {
1006
- workers_.emplace_back([this] {
1007
- // Each thread gets its own schema cache
1008
- std::unordered_map<uint32_t, ata::schema_ref> cache;
1009
- while (true) {
1010
- std::function<void(std::unordered_map<uint32_t, ata::schema_ref>&)> task;
1011
- {
1012
- std::unique_lock<std::mutex> lock(mtx_);
1013
- cv_.wait(lock, [this] { return stop_ || !tasks_.empty(); });
1014
- if (stop_ && tasks_.empty()) return;
1015
- task = std::move(tasks_.front());
1016
- tasks_.pop();
1017
- }
1018
- task(cache);
1019
- {
1020
- std::unique_lock<std::mutex> lock(done_mtx_);
1021
- pending_--;
1022
- if (pending_ == 0) done_cv_.notify_all();
1023
- }
1024
- }
1025
- });
1026
- }
1027
- }
1028
-
1029
- void submit(std::function<void(std::unordered_map<uint32_t, ata::schema_ref>&)> task) {
1030
- {
1031
- std::unique_lock<std::mutex> lock(mtx_);
1032
- tasks_.push(std::move(task));
1033
- }
1034
- {
1035
- std::unique_lock<std::mutex> lock(done_mtx_);
1036
- pending_++;
1037
- }
1038
- cv_.notify_one();
1039
- }
1040
-
1041
- void wait() {
1042
- std::unique_lock<std::mutex> lock(done_mtx_);
1043
- done_cv_.wait(lock, [this] { return pending_ == 0; });
1044
- }
1045
-
1046
- unsigned size() const { return (unsigned)workers_.size(); }
1047
-
1048
- ~ThreadPool() {
1049
- { std::unique_lock<std::mutex> lock(mtx_); stop_ = true; }
1050
- cv_.notify_all();
1051
- for (auto& w : workers_) w.join();
1052
- }
1053
-
1054
- private:
1055
- std::vector<std::thread> workers_;
1056
- std::queue<std::function<void(std::unordered_map<uint32_t, ata::schema_ref>&)>> tasks_;
1057
- std::mutex mtx_;
1058
- std::condition_variable cv_;
1059
- std::mutex done_mtx_;
1060
- std::condition_variable done_cv_;
1061
- std::atomic<int> pending_{0};
1062
- bool stop_ = false;
1063
- };
1064
-
1065
- static ThreadPool& pool() {
1066
- static ThreadPool p;
1067
- return p;
1068
- }
1069
-
1070
- // --- Fast Validation Registry ---
1071
- // Global schema slots for pre-compiled validation (bypasses per-call compilation)
1072
- static constexpr size_t MAX_FAST_SLOTS = 4096;
1073
- static ata::schema_ref g_fast_schemas[MAX_FAST_SLOTS];
1074
- static std::string g_fast_schema_jsons[MAX_FAST_SLOTS];
1075
- static uint32_t g_fast_slot_count = 0;
1076
-
1077
- // Register a compiled schema in a fast slot, returns slot ID.
1078
- // Deduplicates: same schema JSON string returns existing slot.
1079
- Napi::Value FastRegister(const Napi::CallbackInfo& info) {
1080
- Napi::Env env = info.Env();
1081
- if (info.Length() < 1 || !info[0].IsString()) {
1082
- Napi::TypeError::New(env, "Schema JSON string expected").ThrowAsJavaScriptException();
1083
- return env.Undefined();
1084
- }
1085
- std::string schema_json = info[0].As<Napi::String>().Utf8Value();
1086
- // Deduplicate: return existing slot for identical schema JSON
1087
- for (uint32_t i = 0; i < g_fast_slot_count; i++) {
1088
- if (g_fast_schema_jsons[i] == schema_json) {
1089
- return Napi::Number::New(env, i);
1090
- }
1091
- }
1092
- if (g_fast_slot_count >= MAX_FAST_SLOTS) {
1093
- Napi::Error::New(env, "Max fast schema slots reached").ThrowAsJavaScriptException();
1094
- return env.Undefined();
1095
- }
1096
- auto schema = ata::compile(schema_json);
1097
- if (!schema) {
1098
- Napi::Error::New(env, "Failed to compile schema").ThrowAsJavaScriptException();
1099
- return env.Undefined();
1100
- }
1101
- uint32_t slot = g_fast_slot_count++;
1102
- g_fast_schemas[slot] = std::move(schema);
1103
- g_fast_schema_jsons[slot] = schema_json;
1104
- return Napi::Number::New(env, slot);
1105
- }
1106
-
1107
- // Fast validation: slot + raw buffer → bool
1108
- // Routes through is_valid_buf → is_valid_prepadded → On-Demand od_plan fast path
1109
- static bool FastValidateImpl(uint32_t slot, const uint8_t* data, size_t length) {
1110
- if (slot >= g_fast_slot_count) return false;
1111
- return ata::is_valid_buf(g_fast_schemas[slot], data, length);
1112
- }
1113
-
1114
- // Zero-copy validation with pre-padded buffer
1115
- static bool FastValidatePrepadded(uint32_t slot, const uint8_t* data, size_t length) {
1116
- if (slot >= g_fast_slot_count) return false;
1117
- return ata::is_valid_prepadded(g_fast_schemas[slot],
1118
- reinterpret_cast<const char*>(data), length);
1119
- }
1120
-
1121
- // Slow path (NAPI) — called when V8 can't use fast path
1122
- Napi::Value FastValidateSlow(const Napi::CallbackInfo& info) {
1123
- Napi::Env env = info.Env();
1124
- if (info.Length() < 2 || !info[0].IsNumber()) {
1125
- return Napi::Boolean::New(env, false);
1126
- }
1127
- uint32_t slot = info[0].As<Napi::Number>().Uint32Value();
1128
- if (info[1].IsTypedArray()) {
1129
- auto arr = info[1].As<Napi::TypedArray>();
1130
- if (arr.TypedArrayType() == napi_uint8_array) {
1131
- auto u8 = info[1].As<Napi::Uint8Array>();
1132
- bool ok = FastValidateImpl(slot, u8.Data(), u8.ByteLength());
1133
- return Napi::Boolean::New(env, ok);
1134
- }
1135
- }
1136
- if (info[1].IsBuffer()) {
1137
- auto buf = info[1].As<Napi::Buffer<uint8_t>>();
1138
- bool ok = FastValidateImpl(slot, buf.Data(), buf.Length());
1139
- return Napi::Boolean::New(env, ok);
1140
- }
1141
- if (info[1].IsString()) {
1142
- std::string json = info[1].As<Napi::String>().Utf8Value();
1143
- bool ok = FastValidateImpl(slot, reinterpret_cast<const uint8_t*>(json.data()), json.size());
1144
- return Napi::Boolean::New(env, ok);
1145
- }
1146
- return Napi::Boolean::New(env, false);
1147
- }
1148
-
1149
- // --- Raw NAPI fast path (minimal overhead) ---
1150
- static napi_value RawFastValidate(napi_env env, napi_callback_info info) {
1151
- size_t argc = 3;
1152
- napi_value args[3];
1153
- napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
1154
-
1155
- if (argc < 2) {
1156
- napi_value result;
1157
- napi_get_boolean(env, false, &result);
1158
- return result;
1159
- }
1160
-
1161
- uint32_t slot;
1162
- napi_get_value_uint32(env, args[0], &slot);
1163
-
1164
- // Check if pre-padded mode (3rd arg = json length, buffer has padding)
1165
- bool prepadded = (argc >= 3);
1166
- uint32_t json_length = 0;
1167
- if (prepadded) {
1168
- napi_get_value_uint32(env, args[2], &json_length);
1169
- }
1170
-
1171
- bool valid = false;
1172
-
1173
- // Try typed array first (zero-copy)
1174
- bool is_typedarray = false;
1175
- napi_is_typedarray(env, args[1], &is_typedarray);
1176
-
1177
- if (is_typedarray) {
1178
- napi_typedarray_type type;
1179
- size_t length;
1180
- void* data;
1181
- napi_get_typedarray_info(env, args[1], &type, &length, &data, nullptr, nullptr);
1182
- if (data) {
1183
- size_t actual_len = prepadded ? json_length : length;
1184
- if (prepadded) {
1185
- valid = FastValidatePrepadded(slot, static_cast<const uint8_t*>(data), actual_len);
1186
- } else {
1187
- valid = FastValidateImpl(slot, static_cast<const uint8_t*>(data), actual_len);
1188
- }
1189
- }
1190
- } else {
1191
- bool is_buffer = false;
1192
- napi_is_buffer(env, args[1], &is_buffer);
1193
- if (is_buffer) {
1194
- void* data;
1195
- size_t length;
1196
- napi_get_buffer_info(env, args[1], &data, &length);
1197
- if (data) {
1198
- size_t actual_len = prepadded ? json_length : length;
1199
- if (prepadded) {
1200
- valid = FastValidatePrepadded(slot, static_cast<const uint8_t*>(data), actual_len);
1201
- } else {
1202
- valid = FastValidateImpl(slot, static_cast<const uint8_t*>(data), actual_len);
1203
- }
1204
- }
1205
- } else {
1206
- napi_valuetype vtype;
1207
- napi_typeof(env, args[1], &vtype);
1208
- if (vtype != napi_string) {
1209
- napi_throw_type_error(env, nullptr,
1210
- "rawFastValidate() requires a Buffer, TypedArray, or string. For parsed objects, use validate() or isValidObject().");
1211
- return nullptr;
1212
- }
1213
- // String, must copy (can't pre-pad strings)
1214
- size_t len = 0;
1215
- napi_get_value_string_utf8(env, args[1], nullptr, 0, &len);
1216
- if (len <= 4096) {
1217
- char buf[4097];
1218
- napi_get_value_string_utf8(env, args[1], buf, len + 1, &len);
1219
- valid = FastValidateImpl(slot, reinterpret_cast<const uint8_t*>(buf), len);
1220
- } else {
1221
- std::string buf(len, '\0');
1222
- napi_get_value_string_utf8(env, args[1], buf.data(), len + 1, &len);
1223
- valid = FastValidateImpl(slot, reinterpret_cast<const uint8_t*>(buf.data()), len);
1224
- }
1225
- }
1226
- }
1227
-
1228
- napi_value result;
1229
- napi_get_boolean(env, valid, &result);
1230
- return result;
1231
- }
1232
-
1233
- // --- Batch validation: one NAPI call, N validations ---
1234
- static napi_value RawBatchValidate(napi_env env, napi_callback_info info) {
1235
- size_t argc = 2;
1236
- napi_value args[2];
1237
- napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
1238
-
1239
- uint32_t slot;
1240
- napi_get_value_uint32(env, args[0], &slot);
1241
- if (slot >= g_fast_slot_count) {
1242
- napi_value r;
1243
- napi_get_null(env, &r);
1244
- return r;
1245
- }
1246
-
1247
- uint32_t arr_len;
1248
- napi_get_array_length(env, args[1], &arr_len);
1249
-
1250
- napi_value result_arr;
1251
- napi_create_array_with_length(env, arr_len, &result_arr);
1252
-
1253
- for (uint32_t i = 0; i < arr_len; i++) {
1254
- napi_value item;
1255
- napi_get_element(env, args[1], i, &item);
1256
-
1257
- bool valid = false;
1258
- bool is_buffer = false;
1259
- napi_is_buffer(env, item, &is_buffer);
1260
-
1261
- if (is_buffer) {
1262
- void* data; size_t length;
1263
- napi_get_buffer_info(env, item, &data, &length);
1264
- if (data && length > 0)
1265
- valid = ata::validate(g_fast_schemas[slot],
1266
- std::string_view(static_cast<const char*>(data), length)).valid;
1267
- } else {
1268
- bool is_ta = false;
1269
- napi_is_typedarray(env, item, &is_ta);
1270
- if (is_ta) {
1271
- napi_typedarray_type type; size_t length; void* data;
1272
- napi_get_typedarray_info(env, item, &type, &length, &data, nullptr, nullptr);
1273
- if (data && length > 0)
1274
- valid = ata::validate(g_fast_schemas[slot],
1275
- std::string_view(static_cast<const char*>(data), length)).valid;
1276
- } else {
1277
- napi_valuetype vtype;
1278
- napi_typeof(env, item, &vtype);
1279
- if (vtype != napi_string) {
1280
- napi_throw_type_error(env, nullptr,
1281
- "rawNDJSONValidate() batch elements must be Buffer, TypedArray, or string");
1282
- return nullptr;
1283
- }
1284
- size_t len = 0;
1285
- napi_get_value_string_utf8(env, item, nullptr, 0, &len);
1286
- std::string buf(len, '\0');
1287
- napi_get_value_string_utf8(env, item, buf.data(), len + 1, &len);
1288
- valid = ata::validate(g_fast_schemas[slot], buf).valid;
1289
- }
1290
- }
1291
-
1292
- napi_value bval;
1293
- napi_get_boolean(env, valid, &bval);
1294
- napi_set_element(env, result_arr, i, bval);
1295
- }
1296
- return result_arr;
1297
- }
1298
-
1299
- // --- Parallel NDJSON: multi-core validation, ajv can't do this ---
1300
- static napi_value RawParallelValidate(napi_env env, napi_callback_info info) {
1301
- size_t argc = 2;
1302
- napi_value args[2];
1303
- napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
1304
-
1305
- uint32_t slot;
1306
- napi_get_value_uint32(env, args[0], &slot);
1307
- if (slot >= g_fast_slot_count) {
1308
- napi_value r; napi_get_null(env, &r); return r;
1309
- }
1310
-
1311
- const char* data = nullptr;
1312
- size_t total_len = 0;
1313
- bool is_buffer = false;
1314
- napi_is_buffer(env, args[1], &is_buffer);
1315
- if (is_buffer) {
1316
- void* d; napi_get_buffer_info(env, args[1], &d, &total_len);
1317
- data = static_cast<const char*>(d);
1318
- } else {
1319
- bool is_ta = false;
1320
- napi_is_typedarray(env, args[1], &is_ta);
1321
- if (is_ta) {
1322
- napi_typedarray_type type; void* d;
1323
- napi_get_typedarray_info(env, args[1], &type, &total_len, &d, nullptr, nullptr);
1324
- data = static_cast<const char*>(d);
1325
- }
1326
- }
1327
- if (!data || total_len == 0) {
1328
- napi_value r; napi_create_array_with_length(env, 0, &r); return r;
1329
- }
1330
-
1331
- // Split lines
1332
- struct line { const char* ptr; size_t len; };
1333
- std::vector<line> lines;
1334
- const char* start = data;
1335
- const char* end = data + total_len;
1336
- while (start < end) {
1337
- const char* nl = static_cast<const char*>(memchr(start, '\n', end - start));
1338
- size_t line_len = nl ? (size_t)(nl - start) : (size_t)(end - start);
1339
- if (line_len > 0) lines.push_back({start, line_len});
1340
- start += line_len + 1;
1341
- }
1342
-
1343
- size_t n = lines.size();
1344
- std::vector<bool> results(n, false);
1345
-
1346
- // Parallel validation across CPU cores
1347
- unsigned num_threads = std::thread::hardware_concurrency();
1348
- if (num_threads == 0) num_threads = 4;
1349
- if (num_threads > n) num_threads = (unsigned)n;
1350
-
1351
- // Each thread gets its own schema_ref (thread-safe: compile is one-time, validate is read-only)
1352
- // But ata::validate uses internal parser that's NOT thread-safe
1353
- // So each thread needs its own compiled schema
1354
- const auto& schema_json = g_fast_schema_jsons[slot];
1355
-
1356
- if (schema_json.empty() || n < num_threads * 2) {
1357
- // Fallback: single-threaded for small batches
1358
- for (size_t i = 0; i < n; i++) {
1359
- auto r = ata::validate(g_fast_schemas[slot], std::string_view(lines[i].ptr, lines[i].len));
1360
- results[i] = r.valid;
1361
- }
1362
- } else {
1363
- auto& tp = pool();
1364
- unsigned nworkers = tp.size();
1365
- size_t chunk = (n + nworkers - 1) / nworkers;
1366
-
1367
- for (unsigned t = 0; t < nworkers; t++) {
1368
- size_t from = t * chunk;
1369
- size_t to = std::min(from + chunk, n);
1370
- if (from >= n) break;
1371
-
1372
- tp.submit([&results, &lines, from, to, slot](
1373
- std::unordered_map<uint32_t, ata::schema_ref>& cache) {
1374
- auto it = cache.find(slot);
1375
- if (it == cache.end()) {
1376
- it = cache.emplace(slot, ata::compile(g_fast_schema_jsons[slot])).first;
1377
- }
1378
- auto& s = it->second;
1379
- // Free padding: lines in NDJSON buffer almost always have free padding
1380
- // (next line's data serves as padding). Only last line might need copy.
1381
- for (size_t i = from; i < to; i++) {
1382
- results[i] = ata::is_valid_prepadded(s, lines[i].ptr, lines[i].len);
1383
- }
1384
- });
1385
- }
1386
- tp.wait();
1387
- }
1388
-
1389
- napi_value result_arr;
1390
- napi_create_array_with_length(env, n, &result_arr);
1391
- for (size_t i = 0; i < n; i++) {
1392
- napi_value bval;
1393
- napi_get_boolean(env, results[i], &bval);
1394
- napi_set_element(env, result_arr, (uint32_t)i, bval);
1395
- }
1396
- return result_arr;
1397
- }
1398
-
1399
- // --- Parallel count: returns just the number of valid items (no array overhead) ---
1400
- static napi_value RawParallelCount(napi_env env, napi_callback_info info) {
1401
- size_t argc = 2;
1402
- napi_value args[2];
1403
- napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
1404
-
1405
- uint32_t slot;
1406
- napi_get_value_uint32(env, args[0], &slot);
1407
- if (slot >= g_fast_slot_count) {
1408
- napi_value r; napi_create_uint32(env, 0, &r); return r;
1409
- }
1410
-
1411
- const char* data = nullptr;
1412
- size_t total_len = 0;
1413
- bool is_buffer = false;
1414
- napi_is_buffer(env, args[1], &is_buffer);
1415
- if (is_buffer) {
1416
- void* d; napi_get_buffer_info(env, args[1], &d, &total_len);
1417
- data = static_cast<const char*>(d);
1418
- } else {
1419
- bool is_ta = false;
1420
- napi_is_typedarray(env, args[1], &is_ta);
1421
- if (is_ta) {
1422
- napi_typedarray_type type; void* d;
1423
- napi_get_typedarray_info(env, args[1], &type, &total_len, &d, nullptr, nullptr);
1424
- data = static_cast<const char*>(d);
1425
- }
1426
- }
1427
- if (!data || total_len == 0) {
1428
- napi_value r; napi_create_uint32(env, 0, &r); return r;
1429
- }
1430
-
1431
- struct line { const char* ptr; size_t len; };
1432
- std::vector<line> lines;
1433
- const char* start = data;
1434
- const char* end = data + total_len;
1435
- while (start < end) {
1436
- const char* nl = static_cast<const char*>(memchr(start, '\n', end - start));
1437
- size_t line_len = nl ? (size_t)(nl - start) : (size_t)(end - start);
1438
- if (line_len > 0) lines.push_back({start, line_len});
1439
- start += line_len + 1;
1440
- }
1441
-
1442
- size_t n = lines.size();
1443
- std::atomic<uint32_t> valid_count{0};
1444
-
1445
- auto& tp = pool();
1446
- unsigned nworkers = tp.size();
1447
- size_t chunk = (n + nworkers - 1) / nworkers;
1448
-
1449
- if (n < nworkers * 2) {
1450
- // Small batch — single thread
1451
- uint32_t cnt = 0;
1452
- for (size_t i = 0; i < n; i++) {
1453
- if (ata::validate(g_fast_schemas[slot], std::string_view(lines[i].ptr, lines[i].len)).valid)
1454
- cnt++;
1455
- }
1456
- napi_value r; napi_create_uint32(env, cnt, &r); return r;
1457
- }
1458
-
1459
- for (unsigned t = 0; t < nworkers; t++) {
1460
- size_t from = t * chunk;
1461
- size_t to = std::min(from + chunk, n);
1462
- if (from >= n) break;
1463
-
1464
- tp.submit([&valid_count, &lines, from, to, slot](
1465
- std::unordered_map<uint32_t, ata::schema_ref>& cache) {
1466
- auto it = cache.find(slot);
1467
- if (it == cache.end()) {
1468
- it = cache.emplace(slot, ata::compile(g_fast_schema_jsons[slot])).first;
1469
- }
1470
- auto& s = it->second;
1471
- uint32_t local_cnt = 0;
1472
- for (size_t i = from; i < to; i++) {
1473
- if (ata::is_valid_prepadded(s, lines[i].ptr, lines[i].len))
1474
- local_cnt++;
1475
- }
1476
- valid_count.fetch_add(local_cnt, std::memory_order_relaxed);
1477
- });
1478
- }
1479
- tp.wait();
1480
-
1481
- napi_value r;
1482
- napi_create_uint32(env, valid_count.load(), &r);
1483
- return r;
1484
- }
1485
-
1486
- // --- NDJSON: single buffer, newline-delimited ---
1487
- static napi_value RawNDJSONValidate(napi_env env, napi_callback_info info) {
1488
- size_t argc = 2;
1489
- napi_value args[2];
1490
- napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
1491
-
1492
- uint32_t slot;
1493
- napi_get_value_uint32(env, args[0], &slot);
1494
- if (slot >= g_fast_slot_count) {
1495
- napi_value r; napi_get_null(env, &r); return r;
1496
- }
1497
-
1498
- const char* data = nullptr;
1499
- size_t total_len = 0;
1500
- bool is_buffer = false;
1501
- napi_is_buffer(env, args[1], &is_buffer);
1502
- if (is_buffer) {
1503
- void* d; napi_get_buffer_info(env, args[1], &d, &total_len);
1504
- data = static_cast<const char*>(d);
1505
- } else {
1506
- bool is_ta = false;
1507
- napi_is_typedarray(env, args[1], &is_ta);
1508
- if (is_ta) {
1509
- napi_typedarray_type type; void* d;
1510
- napi_get_typedarray_info(env, args[1], &type, &total_len, &d, nullptr, nullptr);
1511
- data = static_cast<const char*>(d);
1512
- }
1513
- }
1514
- if (!data || total_len == 0) {
1515
- napi_value r; napi_create_array_with_length(env, 0, &r); return r;
1516
- }
1517
-
1518
- // Count lines first for array allocation
1519
- uint32_t count = 0;
1520
- for (size_t i = 0; i < total_len; i++) if (data[i] == '\n') count++;
1521
- if (total_len > 0 && data[total_len-1] != '\n') count++;
1522
-
1523
- napi_value result_arr;
1524
- napi_create_array_with_length(env, count, &result_arr);
1525
-
1526
- const char* start = data;
1527
- const char* end = data + total_len;
1528
- uint32_t idx = 0;
1529
-
1530
- while (start < end) {
1531
- const char* nl = static_cast<const char*>(memchr(start, '\n', end - start));
1532
- size_t line_len = nl ? (size_t)(nl - start) : (size_t)(end - start);
1533
- if (line_len > 0) {
1534
- auto r = ata::validate(g_fast_schemas[slot], std::string_view(start, line_len));
1535
- napi_value bval;
1536
- napi_get_boolean(env, r.valid, &bval);
1537
- napi_set_element(env, result_arr, idx++, bval);
1538
- }
1539
- start += line_len + 1;
1540
- }
1541
- return result_arr;
1542
- }
1543
-
1544
- Napi::Object Init(Napi::Env env, Napi::Object exports) {
1545
- CompiledSchema::Init(env, exports);
1546
- exports.Set("validate", Napi::Function::New(env, ValidateOneShot));
1547
- exports.Set("version", Napi::Function::New(env, GetVersion));
1548
- exports.Set("fastRegister", Napi::Function::New(env, FastRegister));
1549
- exports.Set("fastValidate", Napi::Function::New(env, FastValidateSlow));
1550
-
1551
- napi_value raw_fn;
1552
- napi_create_function(env, "rawFastValidate", NAPI_AUTO_LENGTH, RawFastValidate, nullptr, &raw_fn);
1553
- exports.Set("rawFastValidate", Napi::Value(env, raw_fn));
1554
-
1555
- napi_value batch_fn;
1556
- napi_create_function(env, "rawBatchValidate", NAPI_AUTO_LENGTH, RawBatchValidate, nullptr, &batch_fn);
1557
- exports.Set("rawBatchValidate", Napi::Value(env, batch_fn));
1558
-
1559
- napi_value ndjson_fn;
1560
- napi_create_function(env, "rawNDJSONValidate", NAPI_AUTO_LENGTH, RawNDJSONValidate, nullptr, &ndjson_fn);
1561
- exports.Set("rawNDJSONValidate", Napi::Value(env, ndjson_fn));
1562
-
1563
- napi_value parallel_fn;
1564
- napi_create_function(env, "rawParallelValidate", NAPI_AUTO_LENGTH, RawParallelValidate, nullptr, &parallel_fn);
1565
- exports.Set("rawParallelValidate", Napi::Value(env, parallel_fn));
1566
-
1567
- napi_value pcount_fn;
1568
- napi_create_function(env, "rawParallelCount", NAPI_AUTO_LENGTH, RawParallelCount, nullptr, &pcount_fn);
1569
- exports.Set("rawParallelCount", Napi::Value(env, pcount_fn));
1570
-
1571
- return exports;
1572
- }
1573
-
1574
- NODE_API_MODULE(ata, Init)