leptris 1.9.394.0-arm-linux → 1.9.396.0-arm-linux

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.
@@ -380,3 +380,116 @@ ptrdiff_t leptris_text_scan_run_neon(const char* s, size_t len, char term,
380
380
  return (p < end) ? (ptrdiff_t)(p - s) : -1;
381
381
  }
382
382
  }
383
+
384
+ /* ---- UTF-8 validation (leptris#1842 A5) -------------------------------
385
+ * Same derivation as the SSE core in simd_text_avx2.c; NEON bytes are
386
+ * unsigned, so ranges are direct vcge/vcle pairs. */
387
+ int leptris_utf8_valid_neon(const char* src, size_t len) {
388
+ const unsigned char* p = (const unsigned char*)src;
389
+ if (len < 16) return leptris_utf8_valid_scalar(src, len);
390
+
391
+ const uint8x16_t zero = vdupq_n_u8(0);
392
+ const uint8x16_t ff = vdupq_n_u8(0xFF);
393
+ const uint8x16_t b80 = vdupq_n_u8(0x80);
394
+ const uint8x16_t bBF = vdupq_n_u8(0xBF);
395
+ uint8x16_t prev = zero;
396
+
397
+ #define IN_RANGE(v, lo, hi) \
398
+ vandq_u8(vcgeq_u8((v), vdupq_n_u8(lo)), vcleq_u8((v), vdupq_n_u8(hi)))
399
+ size_t i = 0;
400
+ while (i + 16 <= len) {
401
+ uint8x16_t cur = vld1q_u8(p + i);
402
+ /* lane j = byte(j-1): vext(prev, cur, k) keeps the last k
403
+ * lanes of prev at the head -> k = 15/14/13. */
404
+ uint8x16_t b1 = vextq_u8(prev, cur, 15);
405
+ uint8x16_t b2 = vextq_u8(prev, cur, 14);
406
+ uint8x16_t b3 = vextq_u8(prev, cur, 13);
407
+
408
+ uint8x16_t st2c = IN_RANGE(cur, 0xC2, 0xDF);
409
+ uint8x16_t st3c = IN_RANGE(cur, 0xE0, 0xEF);
410
+ uint8x16_t st4c = IN_RANGE(cur, 0xF0, 0xF4);
411
+ uint8x16_t is_ascii = vcltq_u8(cur, b80);
412
+ uint8x16_t legal_start = vorrq_u8(st2c, vorrq_u8(st3c, st4c));
413
+
414
+ uint8x16_t st2b1 = IN_RANGE(b1, 0xC2, 0xDF);
415
+ uint8x16_t st3b1 = IN_RANGE(b1, 0xE0, 0xEF);
416
+ uint8x16_t st4b1 = IN_RANGE(b1, 0xF0, 0xF4);
417
+ uint8x16_t st3b2 = IN_RANGE(b2, 0xE0, 0xEF);
418
+ uint8x16_t st4b2 = IN_RANGE(b2, 0xF0, 0xF4);
419
+ uint8x16_t st4b3 = IN_RANGE(b3, 0xF0, 0xF4);
420
+
421
+ uint8x16_t must_cont =
422
+ vorrq_u8(vorrq_u8(st2b1, vorrq_u8(st3b1, st4b1)),
423
+ vorrq_u8(vorrq_u8(st3b2, st4b2), st4b3));
424
+
425
+ const uint8x16_t bA0 = vdupq_n_u8(0xA0);
426
+ const uint8x16_t b90 = vdupq_n_u8(0x90);
427
+ const uint8x16_t b9F = vdupq_n_u8(0x9F);
428
+ const uint8x16_t b8F = vdupq_n_u8(0x8F);
429
+ uint8x16_t e0 = vceqq_u8(b1, vdupq_n_u8(0xE0));
430
+ uint8x16_t ed = vceqq_u8(b1, vdupq_n_u8(0xED));
431
+ uint8x16_t f0 = vceqq_u8(b1, vdupq_n_u8(0xF0));
432
+ uint8x16_t f4 = vceqq_u8(b1, vdupq_n_u8(0xF4));
433
+ uint8x16_t lo = vbslq_u8(e0, bA0, b80);
434
+ lo = vbslq_u8(f0, b90, lo);
435
+ uint8x16_t hi = vbslq_u8(ed, b9F, bBF);
436
+ hi = vbslq_u8(f4, b8F, hi);
437
+
438
+ uint8x16_t cont_ok = vandq_u8(vcgeq_u8(cur, lo), vcleq_u8(cur, hi));
439
+
440
+ uint8x16_t block_error = vorrq_u8(
441
+ vandq_u8(must_cont, veorq_u8(cont_ok, ff)),
442
+ vandq_u8(veorq_u8(must_cont, ff),
443
+ vandq_u8(veorq_u8(is_ascii, ff),
444
+ veorq_u8(legal_start, ff))));
445
+ /* any lane set? */
446
+ /* any lane set? */
447
+ if (vmaxvq_u8(block_error) != 0) return 0;
448
+ prev = cur;
449
+ i += 16;
450
+ }
451
+ if (i >= len) {
452
+ /* The input ENDS at the block edge: the last sequence must
453
+ * be complete (the scalar DFA's "len - i <= n" rule). Walk
454
+ * back over at most 3 continuations; s2 lands ON the last
455
+ * continuation (or the final non-continuation byte). */
456
+ size_t s2 = i;
457
+ size_t conts = 0;
458
+ while (s2 > 0 && (p[s2 - 1] & 0xC0) == 0x80 && conts < 3) {
459
+ s2--;
460
+ conts++;
461
+ }
462
+ if (conts > 0) {
463
+ if (s2 == 0) return 0; /* continuations, no opener anywhere */
464
+ unsigned op = p[s2 - 1]; /* the opener BEFORE the run */
465
+ unsigned owed = (op >= 0xF0) ? 3
466
+ : (op >= 0xE0) ? 2
467
+ : (op >= 0xC2) ? 1
468
+ : 0;
469
+ if (owed > conts) return 0;
470
+ return 1;
471
+ }
472
+ /* the last byte is not a continuation: it must be ASCII */
473
+ unsigned op = p[i - 1];
474
+ unsigned owed = (op >= 0xF0) ? 3
475
+ : (op >= 0xE0) ? 2
476
+ : (op >= 0xC2) ? 1
477
+ : 0;
478
+ return owed == 0 ? 1 : 0;
479
+ }
480
+ /* Tail: scalar DFA anchored at a character boundary (at most 3
481
+ * bytes back from the SIMD end; a sequence is at most 4) — the
482
+ * anchored run also covers a sequence spanning the block edge.
483
+ * One more step back when the byte before the anchor is a start
484
+ * byte: an opener in the final block lane reaches into the tail
485
+ * (400k-case randomized cross-check vs the scalar oracle). */
486
+ size_t start = i;
487
+ while (start > 0 && (p[start] & 0xC0) == 0x80 && i - start < 3)
488
+ start--;
489
+ if (i - start < 3 && start > 0) {
490
+ unsigned char b = p[start - 1];
491
+ if (b >= 0xC2 && b <= 0xF4) start--;
492
+ }
493
+ return leptris_utf8_valid_scalar((const char*)(p + start), len - start);
494
+ }
495
+ #undef IN_RANGE
@@ -2314,6 +2314,12 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
2314
2314
  if (*p.pos != '?') {
2315
2315
  if (dp_nul(&p, p.pos)) goto fail;
2316
2316
  p.pos++;
2317
+ /* consuming the separator may exhaust the buffer
2318
+ * (" <?xm" — the fuzzer shape): p.pos one past
2319
+ * p.end made p.end - p.pos underflow to SIZE_MAX and
2320
+ * the data memchr below walked off the allocation
2321
+ * (nightly-fuzz crash-aac3c9bb, no-LTO build) */
2322
+ if (p.pos >= p.end) goto fail;
2317
2323
  data_start = p.pos;
2318
2324
  }
2319
2325
  /* memchr for '?' — fast-skip PI data bodies. */
@@ -3531,48 +3537,15 @@ static int dp_declares_non_utf8(const char* s, size_t len) {
3531
3537
  }
3532
3538
 
3533
3539
  static int dp_utf8_valid(const char* s, size_t len) {
3534
- const unsigned char* p = (const unsigned char*)s;
3535
- size_t i = 0;
3536
- /* ASCII fast path: 8 bytes per iteration. The DFA below runs
3537
- * only when a high byte is present; ordinary documents pay one
3538
- * memory-bandwidth pass. */
3539
- while (i + 8 <= len) {
3540
- uint64_t w;
3541
- memcpy(&w, p + i, 8);
3542
- if (w & UINT64_C(0x8080808080808080)) break;
3543
- i += 8;
3544
- }
3545
- while (i < len && p[i] < 0x80) i++;
3546
- while (i < len) {
3547
- unsigned c = p[i];
3548
- if (c < 0x80) {
3549
- i++;
3550
- continue;
3551
- }
3552
- size_t n;
3553
- unsigned lo = 0x80;
3554
- unsigned hi = 0xBF;
3555
- if (c >= 0xC2 && c <= 0xDF) {
3556
- n = 1;
3557
- } else if (c >= 0xE0 && c <= 0xEF) {
3558
- n = 2;
3559
- if (c == 0xE0) lo = 0xA0;
3560
- else if (c == 0xED) hi = 0x9F;
3561
- } else if (c >= 0xF0 && c <= 0xF4) {
3562
- n = 3;
3563
- if (c == 0xF0) lo = 0x90;
3564
- else if (c == 0xF4) hi = 0x8F;
3565
- } else {
3566
- return 0;
3567
- }
3568
- if (len - i <= n) return 0;
3569
- if (p[i + 1] < lo || p[i + 1] > hi) return 0;
3570
- for (size_t k = 2; k <= n; k++) {
3571
- if (p[i + k] < 0x80 || p[i + k] > 0xBF) return 0;
3572
- }
3573
- i += n + 1;
3574
- }
3575
- return 1;
3540
+ /* #1842 A5: the per-byte DFA moved to the SIMD layer —
3541
+ * leptris_utf8_valid dispatches to boundary-vector cores (AVX2/
3542
+ * NEON) with the DFA as the fallback+oracle. Pure accept/reject
3543
+ * on the caller's bytes: no buffer-identity interaction (the A3
3544
+ * revert settled that question for xml_buffer; this face only
3545
+ * READS). */
3546
+ (void)s;
3547
+ (void)len;
3548
+ return leptris_utf8_valid(s, len);
3576
3549
  }
3577
3550
 
3578
3551
  static int dp_input_encoding_ok(const char* s, size_t len) {
@@ -3658,7 +3631,10 @@ struct leptris_document* direct_parse_inplace(char* buf, size_t len) {
3658
3631
  if (!dp_declares_non_utf8(buf, len) && saw_high &&
3659
3632
  !dp_utf8_valid(buf, len))
3660
3633
  return NULL;
3661
- buf[len] = '\0'; /* Ensure NUL termination */
3634
+ /* No sentinel write past len: the caller owns exactly `len`
3635
+ * writable bytes (the header contract promises NULL terminators
3636
+ * INSIDE the buffer; an exactly-sized caller trips ASAN, and
3637
+ * bindings hand read-only views). The scanner takes `len`. */
3662
3638
  return direct_parse_internal(buf, len, 0, 0, 0, 0, 0, 0,
3663
3639
  lt_count, dq_count, sq_count);
3664
3640
  }
@@ -183,7 +183,9 @@ static void xsd_cm_free(XsdCm* cm) {
183
183
  free(cm->name);
184
184
  free(cm->type);
185
185
  free(cm->ns);
186
- free(cm->ext_base);
186
+ free(cm->res_base);
187
+ free(cm->res_base_ns);
188
+ free(cm->ext_base);
187
189
  free(cm->ext_base_ns);
188
190
  free(cm->any_ns);
189
191
  free(cm->not_qname);
@@ -1858,11 +1860,11 @@ static void xsd_collect_decls_doc(LeptrisElement root, const char* eff_tns,
1858
1860
  xsd_collect_decls_doc(subroot, eff, sub_base, decls, n, max,
1859
1861
  claimed, nclaimed, cmax, depth + 1);
1860
1862
  }
1861
- /* leptris#1913: an xs:override body's declarations belong
1862
- * to the symbol space too — walk the override element as a
1863
- * pseudo-root (it has no include/import children of its
1864
- * own). */
1865
- if (strcmp(l, "override") == 0)
1863
+ /* leptris#1913/#1926: override and redefine bodies'
1864
+ * declarations belong to the symbol space too — walk the
1865
+ * derivation element as a pseudo-root (it has no
1866
+ * include/import children of its own). */
1867
+ if (strcmp(l, "override") == 0 || strcmp(l, "redefine") == 0)
1866
1868
  xsd_collect_decls_doc(c, eff_tns, base_dir, decls, n, max,
1867
1869
  claimed, nclaimed, cmax, depth + 1);
1868
1870
  leptris_document_free(sub);
@@ -2343,14 +2345,20 @@ static void xsd_capture_root(struct leptris_xsd_schema* s,
2343
2345
  if (sub_ns) free(sub_ns);
2344
2346
  s->target_ns = saved_ns;
2345
2347
  }
2346
- /* leptris#1913 (XSD 1.1 §I.4): the override
2347
- * element's OWN declaration children
2348
- * replace same-named components of the base
2349
- * just captured — capture them AFTER the
2350
- * base so the lookup ladder (head-first)
2351
- * sees the replacements. */
2348
+ /* leptris#1913 (XSD 1.1 §I.4) + #1926 (XSD
2349
+ * 1.0 §4.2.2): the override/redefine
2350
+ * element's OWN declaration children replace
2351
+ * same-named components of the base just
2352
+ * captured — capture them AFTER the base so
2353
+ * the lookup ladder (head-first) sees the
2354
+ * replacements. A redefine body that EXTENDS
2355
+ * the redefined name folds its extension
2356
+ * against the base at this moment (base
2357
+ * captured, ladder sees the new head), which
2358
+ * is exactly the redefine hook. */
2352
2359
  if (subroot && same_ns &&
2353
- strcmp(local, "override") == 0) {
2360
+ (strcmp(local, "override") == 0 ||
2361
+ strcmp(local, "redefine") == 0)) {
2354
2362
  s->include_depth++;
2355
2363
  s->capture_force = 1;
2356
2364
  xsd_capture_root(s, (LeptrisElement)n);
@@ -2946,9 +2954,21 @@ static XsdCm* xsd_capture_complex_model(struct leptris_xsd_schema* s,
2946
2954
  attrs = own_attrs;
2947
2955
  }
2948
2956
  } else {
2949
- /* restriction: the derived model REPLACES */
2957
+ /* restriction: the derived model REPLACES.
2958
+ * leptris#1923: the base still rides the type —
2959
+ * libxml2 admits the restriction chain's declared
2960
+ * attributes at validation, so record the base
2961
+ * (local + resolved ns) for the strict-attribute
2962
+ * admission walk. */
2950
2963
  root->first_child = xsd_cm_parse(s, dq, NULL);
2951
2964
  attrs = xsd_capture_attr_rows(s, dq, 0);
2965
+ if (base && strncmp(base, "xs:", 3) != 0) {
2966
+ free(root->res_base);
2967
+ root->res_base = xsd_strdup(base);
2968
+ free(root->res_base_ns);
2969
+ root->res_base_ns =
2970
+ buri[0] ? xsd_strdup(buri) : NULL;
2971
+ }
2952
2972
  }
2953
2973
  /* XSD 1.1: asserts nested under the derivation */
2954
2974
  if (s->version_11)
@@ -3015,6 +3035,16 @@ static XsdCm* xsd_capture_complex_model(struct leptris_xsd_schema* s,
3015
3035
  } else {
3016
3036
  root->text_type = xsd_strdup(base);
3017
3037
  }
3038
+ /* leptris#1923: a simpleContent restriction
3039
+ * rides its base for strict-attribute
3040
+ * admission (libxml2 admits the chain's
3041
+ * declared attributes — ctE019). */
3042
+ if (strcmp(dl, "restriction") == 0) {
3043
+ free(root->res_base);
3044
+ root->res_base = xsd_strdup(base);
3045
+ free(root->res_base_ns);
3046
+ root->res_base_ns = NULL;
3047
+ }
3018
3048
  }
3019
3049
  }
3020
3050
  attrs = xsd_capture_attr_rows(s, dq, 0);
@@ -635,7 +635,8 @@ static int any_attr_matches(const char* grammar, const char* uri,
635
635
  static void validate_attributes(struct xsd_validator* v,
636
636
  struct leptris_xsd_schema* s,
637
637
  LeptrisElement elem,
638
- const XsdTypeAttrs* ta) {
638
+ const XsdTypeAttrs* ta,
639
+ const XsdCm* chain_ct) {
639
640
  /* ta == NULL (no captured rows — attribute-less complexTypes)
640
641
  * skips the walk entirely; simple/builtin element validation
641
642
  * passes k_no_attrs to run the strict walk with zero declared
@@ -688,6 +689,37 @@ static void validate_attributes(struct xsd_validator* v,
688
689
  if (strcmp(a->name, local) == 0 ||
689
690
  strcmp(a->name, qn) == 0)
690
691
  declared = 1;
692
+ /* leptris#1923 (oracle parity with libxml2): the restriction
693
+ * chain's declared attributes stay visible for strict
694
+ * admission — a RESTRICTION that narrows the base's rows
695
+ * still admits what the base declared (ctE019). */
696
+ if (!declared && chain_ct) {
697
+ const XsdCm* anc = chain_ct;
698
+ for (int hops = 0; anc && anc->res_base && hops < 8;
699
+ hops++) {
700
+ const XsdTypeAttrs* base_rows = xsd_find_type_attrs_ns(
701
+ s, anc->res_base,
702
+ anc->res_base_ns ? anc->res_base_ns
703
+ : xsd_target_ns(s));
704
+ if (!base_rows)
705
+ base_rows = xsd_find_type_attrs(s, anc->res_base);
706
+ for (XsdAttrDecl* a = base_rows ? base_rows->attrs
707
+ : NULL;
708
+ a && !declared; a = a->next)
709
+ if (strcmp(a->name, local) == 0 ||
710
+ strcmp(a->name, qn) == 0)
711
+ declared = 1;
712
+ if (declared) break;
713
+ const XsdCm* base_ct = anc->res_base
714
+ ? xsd_find_complex_ns(s, anc->res_base,
715
+ anc->res_base_ns ? anc->res_base_ns
716
+ : xsd_target_ns(s))
717
+ : NULL;
718
+ if (!base_ct)
719
+ base_ct = xsd_find_complex(s, anc->res_base);
720
+ anc = base_ct;
721
+ }
722
+ }
691
723
  if (declared) continue;
692
724
  /* namespace of the attribute: engine-side attributes
693
725
  * carry their uri through the expanded form */
@@ -824,7 +856,7 @@ static void validate_local_uri(struct xsd_validator* v,
824
856
  const XsdTypeAttrs* rows =
825
857
  xsd_find_type_attrs_ns(s, type, euri);
826
858
  if (!rows) rows = xsd_find_type_attrs(s, type);
827
- validate_attributes(v, s, elem, rows);
859
+ validate_attributes(v, s, elem, rows, ct);
828
860
  validate_children(v, s, elem, ct);
829
861
  /* XSD 1.1: the type's xs:assert list — context is the
830
862
  * element; evaluation errors are failures */
@@ -866,7 +898,7 @@ static void validate_local_uri(struct xsd_validator* v,
866
898
  if (strncmp(type, "xs:", 3) == 0 ||
867
899
  xsd_find_simple_pub(s, type)) {
868
900
  static const XsdTypeAttrs k_no_attrs;
869
- validate_attributes(v, s, elem, &k_no_attrs);
901
+ validate_attributes(v, s, elem, &k_no_attrs, NULL);
870
902
  const char* text = leptris_element_text(elem);
871
903
  if (text && *text)
872
904
  text_valid(v, s, type, text, euri, "element", local,
@@ -886,7 +918,7 @@ static void validate_local_uri(struct xsd_validator* v,
886
918
  (int)sizeof(builtin) &&
887
919
  xsd_builtin_known(builtin)) {
888
920
  static const XsdTypeAttrs k_no_attrs;
889
- validate_attributes(v, s, elem, &k_no_attrs);
921
+ validate_attributes(v, s, elem, &k_no_attrs, NULL);
890
922
  const char* text = leptris_element_text(elem);
891
923
  if (text && *text)
892
924
  text_valid(v, s, builtin, text, euri, "element",
@@ -120,6 +120,14 @@ typedef struct xsd_cm {
120
120
  char* ext_base; /* extension whose base was not yet captured at
121
121
  * capture time (forward ref) — folded by the
122
122
  * post-capture resolver (leptris-ruby#440 A) */
123
+ char* res_base; /* restriction whose base was not yet captured at
124
+ * capture time — the post-capture resolver only
125
+ * folds EXTENSION models; the restriction's model
126
+ * replaces the base's, so this rides along solely
127
+ * for strict-attribute ADMISSION (leptris#1923:
128
+ * libxml2 admits the restriction chain's declared
129
+ * attributes) */
130
+ char* res_base_ns;
123
131
  char* ext_base_ns; /* #1881: the base QName's RESOLVED namespace
124
132
  * (prefix resolved against the extension
125
133
  * element's in-scope declarations) — a set
@@ -118,6 +118,7 @@ target_compile_definitions(test_sch_corpus PRIVATE
118
118
  leptris_add_test(test_parser parser/test_parser.cpp)
119
119
  leptris_add_test(test_large_docs parser/test_large_documents.cpp)
120
120
  leptris_add_test(test_encoding parser/test_encoding.cpp)
121
+ leptris_add_test(test_utf8_simd parser/test_utf8_simd.cpp)
121
122
  leptris_add_test(test_dom dom/test_dom.cpp)
122
123
  leptris_add_test(test_element_children dom/test_element_children.cpp)
123
124
  leptris_add_test(test_entity_refs dom/test_entity_refs.cpp)
@@ -8,6 +8,8 @@
8
8
 
9
9
  #include "leptris.h"
10
10
 
11
+ #include "../../src/leptris/common/simd_text.h"
12
+
11
13
  #include <cstring>
12
14
  #include <vector>
13
15
 
@@ -367,3 +369,116 @@ TEST(EncodingLatin1, WithEncodingConvertsToUtf8) {
367
369
  EXPECT_EQ((unsigned char)text[4], 0xA9);
368
370
  leptris_document_free(doc);
369
371
  }
372
+
373
+ // leptris-py CI regression (2026-10-31): the binding's flow is
374
+ // fast-path parse (raw UTF-16 into the UTF-8 parser) then retry via
375
+ // the encoding-detecting entry. On x86 CI the retry of a BOM-only
376
+ // document whose text contains a multi-byte character failed with
377
+ // "malformed input". Mirror the flow stage by stage so the failing
378
+ // stage is visible on every architecture.
379
+ TEST(EncodingUtf16Le, PyBindingFlowBomWithMultibyteText) {
380
+ // "<?xml version='1.0'?><r><a>café</a></r>" as UTF-16LE + BOM —
381
+ // the exact bytes the py fixture produces.
382
+ static const unsigned char kDoc[] = {
383
+ 0xFF, 0xFE,
384
+ '<', 0x00, '?', 0x00, 'x', 0x00, 'm', 0x00, 'l', 0x00,
385
+ ' ', 0x00, 'v', 0x00, 'e', 0x00, 'r', 0x00, 's', 0x00,
386
+ 'i', 0x00, 'o', 0x00, 'n', 0x00, '=', 0x00, '\'', 0x00,
387
+ '1', 0x00, '.', 0x00, '0', 0x00, '\'', 0x00, '?', 0x00,
388
+ '>', 0x00,
389
+ '<', 0x00, 'r', 0x00, '>', 0x00,
390
+ '<', 0x00, 'a', 0x00, '>', 0x00,
391
+ 'c', 0x00, 'a', 0x00, 'f', 0x00, 0xE9, 0x00,
392
+ '<', 0x00, '/', 0x00, 'a', 0x00, '>', 0x00,
393
+ '<', 0x00, '/', 0x00, 'r', 0x00, '>', 0x00,
394
+ };
395
+ const size_t n = sizeof kDoc;
396
+ ASSERT_EQ(n, 80u);
397
+
398
+ // Stage 1 — the UTF-8 entry over raw UTF-16 (py fast path): the
399
+ // lenient contract is parse-or-fail, never a crash. The inplace
400
+ // entry owns the buffer it is given (NUL-masking is part of the
401
+ // inplace contract), so the scratch copy absorbs the mutation —
402
+ // the pristine kDoc stays for the retry.
403
+ std::vector<unsigned char> buf(kDoc, kDoc + n);
404
+ LeptrisStatus st = LEPTRIS_OK;
405
+ LeptrisDocument fast = leptris_parse_string_inplace(
406
+ reinterpret_cast<char*>(buf.data()), n, &st);
407
+ if (fast) leptris_document_free(fast);
408
+ ASSERT_EQ(buf.size(), n) << "inplace parse must not resize";
409
+
410
+ // Stage 2 — the encoding-detecting retry on the pristine bytes:
411
+ // BOM-only, multi-byte payload; must convert and parse.
412
+ LeptrisDocument doc = leptris_parse_string_with_encoding(
413
+ reinterpret_cast<const char*>(kDoc), n, &st);
414
+ ASSERT_NE(doc, nullptr) << "status: " << leptris_status_string(st);
415
+
416
+ LeptrisElement root = leptris_document_root(doc);
417
+ ASSERT_NE(root, nullptr);
418
+ EXPECT_STREQ(leptris_element_name(root), "r");
419
+ LeptrisElement a = leptris_element_first_child_any(root);
420
+ ASSERT_NE(a, nullptr);
421
+ const char* text = leptris_element_text(a);
422
+ ASSERT_STREQ(text, "caf\xC3\xA9");
423
+
424
+ leptris_document_free(doc);
425
+ }
426
+
427
+ // x86-only multibyte-parse bisection (follows the PyBindingFlow spec):
428
+ // the Utf8Simd matrix is green on every lane while multibyte PARSES
429
+ // fail on all four x86 lanes. Stage the flow so the failing surface
430
+ // names itself: validator verdict, per-sequence-length parses, the
431
+ // converted-buffer shape, and the interleaved vs default lane.
432
+ TEST(EncodingUtf8Strict, X86MultibyteBisection) {
433
+ const char two[] = "<r>caf\xC3\xA9</r>";
434
+ const char three[] = "<r>\xE4\xB8\xAD</r>";
435
+ const char four[] = "<r>\xF0\x9F\x98\x80</r>";
436
+ const char mix[] = "<r>caf\xC3\xA9 \xE4\xB8\xAD \xF0\x9F\x98\x80</r>";
437
+ EXPECT_EQ(leptris_utf8_valid(two, strlen(two)), 1) << "validator: 2-byte doc";
438
+ EXPECT_EQ(leptris_utf8_valid(three, strlen(three)), 1) << "validator: 3-byte doc";
439
+ EXPECT_EQ(leptris_utf8_valid(four, strlen(four)), 1) << "validator: 4-byte doc";
440
+ /* Localize the offending lane: every prefix of the mixed doc is
441
+ * either valid or ends mid-sequence (which the scalar oracle
442
+ * rejects too) — print the verdict map so the first SIMD/scalar
443
+ * divergence names its byte offset. */
444
+ {
445
+ std::string map;
446
+ for (size_t k = 0; k <= strlen(mix); k++) {
447
+ int sc = leptris_utf8_valid_scalar(mix, k);
448
+ int si = leptris_utf8_valid(mix, k);
449
+ if (sc != si || k == strlen(mix))
450
+ map += " [" + std::to_string(k) + "]:" + std::to_string(sc)
451
+ + "/" + std::to_string(si);
452
+ }
453
+ EXPECT_EQ(leptris_utf8_valid(mix, strlen(mix)), 1)
454
+ << "validator: mixed doc; divergences scalar/simd" << map;
455
+ }
456
+
457
+ LeptrisStatus st = LEPTRIS_OK;
458
+ LeptrisDocument d2 = leptris_parse_string(two, strlen(two), &st);
459
+ EXPECT_NE(d2, nullptr) << "parse 2-byte: " << leptris_status_string(st);
460
+ if (d2) leptris_document_free(d2);
461
+ LeptrisDocument d3 = leptris_parse_string(three, strlen(three), &st);
462
+ EXPECT_NE(d3, nullptr) << "parse 3-byte: " << leptris_status_string(st);
463
+ if (d3) leptris_document_free(d3);
464
+ LeptrisDocument d4 = leptris_parse_string(four, strlen(four), &st);
465
+ EXPECT_NE(d4, nullptr) << "parse 4-byte: " << leptris_status_string(st);
466
+ if (d4) leptris_document_free(d4);
467
+
468
+ /* The converted-buffer shape from the py flow (40 bytes). */
469
+ const char conv[] = "<?xml version='1.0'?><r><a>caf\xC3\xA9</a></r>";
470
+ EXPECT_EQ(leptris_utf8_valid(conv, strlen(conv)), 1) << "validator: converted buffer";
471
+ LeptrisDocument dc = leptris_parse_string(conv, strlen(conv), &st);
472
+ EXPECT_NE(dc, nullptr) << "parse converted: " << leptris_status_string(st);
473
+ if (dc) leptris_document_free(dc);
474
+
475
+ /* The default (non-interleaved) lane via RECOVER. */
476
+ LeptrisParseOptions opts;
477
+ memset(&opts, 0, sizeof opts);
478
+ opts.flags = LEPTRIS_PARSE_RECOVER;
479
+ opts.strict_mode = -1;
480
+ LeptrisDocument dr = leptris_parse_string_ex(mix, strlen(mix),
481
+ &opts, &st);
482
+ EXPECT_NE(dr, nullptr) << "parse mixed (default lane): " << leptris_status_string(st);
483
+ if (dr) leptris_document_free(dr);
484
+ }
@@ -0,0 +1,130 @@
1
+ // #1842 A5: the SIMD UTF-8 validator must agree with the scalar DFA
2
+ // on every input — adversarial boundary matrix plus a randomized
3
+ // cross-check over lengths straddling the 16-byte block edge.
4
+ #include "../../src/leptris/common/simd_text.h"
5
+
6
+ #include <gtest/gtest.h>
7
+ #include <cstring>
8
+ #include <cstdlib>
9
+ #include <string>
10
+ #include <utility>
11
+ #include <vector>
12
+
13
+ namespace {
14
+ }
15
+
16
+ TEST(Utf8Simd, SingleByteValues) {
17
+ for (int b = 0; b <= 255; b++) {
18
+ char buf[1] = {(char)b};
19
+ int scalar = leptris_utf8_valid_scalar(buf, 1);
20
+ int simd = leptris_utf8_valid(buf, 1);
21
+ EXPECT_EQ(simd, scalar) << "byte 0x" << std::hex << b << std::dec;
22
+ bool expect = (b < 0x80); /* a lone start byte is truncated */
23
+ EXPECT_EQ(scalar, expect ? 1 : 0)
24
+ << "byte 0x" << std::hex << b << std::dec;
25
+ }
26
+ }
27
+
28
+ TEST(Utf8Simd, SequenceMatrixAcrossBlockEdges) {
29
+ // (case, expected) — corrupt rows must be rejected too.
30
+ const std::vector<std::pair<std::string, int>> cases = {
31
+ // valid
32
+ {{"\xC2\xA0"}, 1}, {{"\xDF\xBF"}, 1}, {{"\xE0\xA0\x80"}, 1},
33
+ {{"\xE1\x80\x80"}, 1}, {{"\xED\x9F\xBF"}, 1}, {{"\xEE\x80\x80"}, 1},
34
+ {{"\xEF\xBF\xBD"}, 1}, {{"\xF0\x90\x80\x80"}, 1},
35
+ {{"\xF1\xBF\xBF\xBF"}, 1}, {{"\xF3\xA0\x80\x80"}, 1},
36
+ {{"\xF4\x8F\xBF\xBF"}, 1},
37
+ // overlong / surrogates / out-of-range
38
+ {{"\xC0\x80"}, 0}, {{"\xC1\xBF"}, 0}, {{"\xE0\x80\x80"}, 0},
39
+ {{"\xE0\x9F\xBF"}, 0}, {{"\xED\xA0\x80"}, 0}, {{"\xED\xBF\xBF"}, 0},
40
+ {{"\xF0\x80\x80\x80"}, 0}, {{"\xF0\x8F\xBF\xBF"}, 0},
41
+ {{"\xF4\x90\x80\x80"}, 0}, {{"\xF5\x80\x80\x80"}, 0},
42
+ // truncated (each prefix)
43
+ {{"\xC2"}, 0}, {{"\xE0\xA0"}, 0}, {{"\xF0\x90\x80"}, 0},
44
+ {{"\xF4\x8F"}, 0},
45
+ // stray continuation
46
+ {{"\x80"}, 0}, {{"a\x80" "b"}, 0}, {{"\xBF"}, 0},
47
+ };
48
+ for (std::vector<std::pair<std::string, int> >::const_iterator
49
+ it = cases.begin();
50
+ it != cases.end(); ++it) {
51
+ const std::string& s = it->first;
52
+ const int expect = it->second;
53
+ for (int pad = 0; pad < 20; pad++) {
54
+ std::string buf((size_t)pad, 'a');
55
+ buf += s;
56
+ int scalar =
57
+ leptris_utf8_valid_scalar(buf.data(), buf.size());
58
+ int simd = leptris_utf8_valid(buf.data(), buf.size());
59
+ EXPECT_EQ(simd, scalar)
60
+ << "case len=" << s.size() << " pad=" << pad;
61
+ EXPECT_EQ(scalar, expect)
62
+ << "verdict drifted: len=" << s.size() << " pad=" << pad;
63
+ }
64
+ }
65
+ }
66
+
67
+ TEST(Utf8Simd, BlockEdgeSizes) {
68
+ for (size_t n = 0; n <= 40; n++) {
69
+ std::string buf(n, 'x');
70
+ buf += "\xC3\xA9";
71
+ ASSERT_EQ(leptris_utf8_valid_scalar(buf.data(), buf.size()), 1);
72
+ ASSERT_EQ(leptris_utf8_valid(buf.data(), buf.size()), 1) << "n=" << n;
73
+ }
74
+ for (size_t n = 0; n <= 40; n++) {
75
+ std::string buf(n, 'x');
76
+ buf += "\xF0\x90\x80";
77
+ ASSERT_EQ(leptris_utf8_valid_scalar(buf.data(), buf.size()), 0);
78
+ ASSERT_EQ(leptris_utf8_valid(buf.data(), buf.size()), 0) << "n=" << n;
79
+ }
80
+ }
81
+
82
+ TEST(Utf8Simd, RandomizedCrossCheck) {
83
+ unsigned seed = 0x1842;
84
+ auto rnd = [&seed]() {
85
+ seed = seed * 1103515245u + 12345u;
86
+ return (seed >> 16) & 0xFF;
87
+ };
88
+ for (int trial = 0; trial < 4000; trial++) {
89
+ size_t len = (size_t)(rnd() % 97);
90
+ std::string buf;
91
+ for (size_t i = 0; i < len; i++) {
92
+ unsigned r = rnd() % 100;
93
+ char c;
94
+ if (r < 55) c = (char)('a' + (r % 26));
95
+ else if (r < 75) c = (char)(0xC0 + (r % 0x40));
96
+ else c = (char)(0x80 + (r % 0x80));
97
+ buf.push_back(c);
98
+ }
99
+ /* The plain corpus rarely produces the boundary shapes that
100
+ * broke the cores (x86-TSAN 2026-10-31): a start byte in the
101
+ * final block lane with a non-continuation after it, and
102
+ * sequences straddling 16/32-byte edges. Append an ASCII
103
+ * tail after a valid-or-not multibyte run so every trial
104
+ * also probes the tail-anchor at a shifted offset. */
105
+ if (trial % 3 == 0) {
106
+ static const char* mids[] = {
107
+ "\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80",
108
+ "\xC3", "\xE0\xA0", "\xF0\x90\x80", "\xED\xA0\x80",
109
+ "\xF4\x8F\xBF\xBF", "\xC2\xBF", "\xEF\xBF\xBD"
110
+ };
111
+ buf += mids[trial % 10];
112
+ buf += "zz";
113
+ }
114
+ int scalar = leptris_utf8_valid_scalar(buf.data(), buf.size());
115
+ int simd = leptris_utf8_valid(buf.data(), buf.size());
116
+ EXPECT_EQ(simd, scalar)
117
+ << "trial " << trial << " len " << buf.size();
118
+ }
119
+ }
120
+
121
+ TEST(Utf8Simd, DenseValidDocument) {
122
+ std::string buf;
123
+ buf.reserve(64 * 1024);
124
+ const char* unit = "héllo wörld — 日本語テキスト 🎉 emoji §naïve";
125
+ for (int i = 0; i < 1024; i++) buf += unit;
126
+ EXPECT_EQ(leptris_utf8_valid_scalar(buf.data(), buf.size()), 1);
127
+ EXPECT_EQ(leptris_utf8_valid(buf.data(), buf.size()), 1);
128
+ buf[buf.size() / 2] = (char)0xF5;
129
+ EXPECT_EQ(leptris_utf8_valid(buf.data(), buf.size()), 0);
130
+ }