leptris 1.9.382.0-arm-linux → 1.9.383.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eeb080a56ba2f644ba7219077f866627006b2d5e8ea80405a86a17cff5853737
4
- data.tar.gz: 8498da423561b35ddfabe900ae828df3ee9f9963c1498ac90cbb543f73c4c58e
3
+ metadata.gz: 3efe4823a59efc5eb360bb82b72d0198c8e798cc03a7ded4afdb50392a4cb785
4
+ data.tar.gz: d360988779ab5b637a84e05e0d52a106a3e9667e0e8620ba3206dc9e4fd701b7
5
5
  SHA512:
6
- metadata.gz: 750842a780c631c64e41614fa9f6b1a812bec0cffe9965fa868967a0b0375b16d9b1466279a07b990a90bbb0f14ee307c3868e9f4dd1702c1436f1af54d3d3f7
7
- data.tar.gz: 16cfef2895a0d2b8ccfb5324492183111dacf30e4db0261f310418a3f2f97dbd6c68b24828858573ecd5062ba2ec9e3fa0f83c5904e8b769234dde45f9b7e86b
6
+ metadata.gz: e984c81a972bb5df3ef56a20689557b2f54d212e671df8be6dfc97d539038e2117ca25cb0c5eb4fa86d009905297e9a6b5649e000b02dfb7c64a5eafbf76185e
7
+ data.tar.gz: ed55e69b8ba40f631188c82e7947760bb16f9002668dcfe90cf17143d83915f79e203b3f538d0e07b4239247e60d43966ce3ac85daa6920b096d156fa6f6305d
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  # Pin for `rake compile` and the platform-gem builds. Keep in lockstep
9
9
  # with .github/workflows/build.yml (which calls `rake compile`) and the
10
10
  # CHANGELOG when libleptris releases.
11
- LIBLEPTRIS_VERSION = "1.9.382"
11
+ LIBLEPTRIS_VERSION = "1.9.383"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.382.0"
4
+ VERSION = "1.9.383.0"
5
5
  end
data/lib/libleptris.so CHANGED
Binary file
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.382
4
+ - libleptris v1.9.383
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,30 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.383] - 2026-10-26
4
+
5
+ ### Fixed
6
+
7
+ - XSD: an empty instance no longer bypasses the content model —
8
+ `<note/>` validated clean against a required-particle sequence
9
+ (#1860; found by the moxml#460 facade tests).
10
+ - XSD: substitution-group members on type-less globals are captured
11
+ (particlesDc001) and a complexContent RESTRICTION no longer
12
+ defers its base — the fold prepended the base's model and
13
+ demanded the base content next to the derived content (#1861;
14
+ moxml#466). modelGroups sweep 145/145 expected-valid.
15
+ - XSD: an `xs:all` with `minOccurs="0"` may be absent — an empty
16
+ instance validates even when the particles carry min=1 (#1863;
17
+ mgAa003/mgD013).
18
+ - XSD regex (moxml#464): `charClassSub` is codepoint-exact (ranges,
19
+ blocks and categories in the subtrahend now subtract); `\d` is
20
+ the Unicode Nd category; complement escapes (`\D \W \S \I \C`)
21
+ are exact over categories; `\P` works inside classes; an empty
22
+ pattern branch matches only the empty string; class members and
23
+ range endpoints accept escaped `]`/`}` and astral characters.
24
+ Regex_w3c expected-valid sweep: 98 -> 17 rejections.
25
+
26
+
27
+
3
28
  ## [1.9.382] - 2026-10-26
4
29
 
5
30
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.382
6
+ VERSION 1.9.383
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -55,6 +55,21 @@ typedef struct RreNode {
55
55
  * LEPTRIS_HAS_UTF8PROC (without it the escapes fail loudly). */
56
56
  unsigned cat_set;
57
57
  unsigned cat_mask;
58
+ /* cat_strict (moxml#464): a class-internal COMPLEMENT builtin
59
+ * ([\I], [\D]) needs bitmap AND cats to agree — the normal
60
+ * union can't express "not in the set" once the bitmap holds
61
+ * the complement's high bytes (U+00AA is in ~ASCII-bits but
62
+ * excluded by the category). */
63
+ unsigned cat_strict;
64
+ /* XSD charClassSub: the bracketed subtrahend (moxml#464). The
65
+ * node is consulted at match time so its ranges, blocks and
66
+ * categories all subtract — the old byte-bitmap merge ignored
67
+ * them. */
68
+ struct RreNode* subtract;
69
+ /* in-class \P member (moxml#464): the class matches the
70
+ * positive members UNIONED WITH the complement of this set —
71
+ * [\P{L}*] = not-L OR '*'. Class negation flips the union. */
72
+ struct RreNode* negm;
58
73
  int neg;
59
74
  int lo, hi; /* RRE_REP bounds; hi < 0 = unbounded */
60
75
  } RreNode;
@@ -77,6 +92,8 @@ static void node_free(RreNode* n) {
77
92
  if (!n) return;
78
93
  node_free(n->l);
79
94
  node_free(n->r);
95
+ node_free(n->subtract);
96
+ node_free(n->negm);
80
97
  free(n);
81
98
  }
82
99
 
@@ -88,6 +105,13 @@ static void cls_add_builtin(RreNode* n, char c) {
88
105
  switch (c) {
89
106
  case 'd':
90
107
  for (int i = '0'; i <= '9'; i++) cls_add(n, (unsigned char)i);
108
+ #if defined(LEPTRIS_HAS_UTF8PROC)
109
+ /* XSD \d = XML decimal = Nd: Arabic-Indic and the
110
+ * other unicode digit families are \\d too
111
+ * (moxml#464, the reZ/reC \d rows) */
112
+ n->cat_set = 1;
113
+ n->cat_mask |= 1u << UTF8PROC_CATEGORY_ND;
114
+ #endif
91
115
  break;
92
116
  case 'w':
93
117
  for (int i = '0'; i <= '9'; i++) cls_add(n, (unsigned char)i);
@@ -108,9 +132,21 @@ static void cls_add_builtin(RreNode* n, char c) {
108
132
  for (int i = 'A'; i <= 'Z'; i++) cls_add(n, (unsigned char)i);
109
133
  cls_add(n, '_');
110
134
  cls_add(n, ':');
135
+ #if defined(LEPTRIS_HAS_UTF8PROC)
136
+ /* letters above the bitmap: the old 0x80-0xFF blanket
137
+ * also accepted symbols like U+00D7 (moxml#464) */
138
+ n->cat_set = 1;
139
+ n->cat_mask = (1u << UTF8PROC_CATEGORY_LU) |
140
+ (1u << UTF8PROC_CATEGORY_LL) |
141
+ (1u << UTF8PROC_CATEGORY_LT) |
142
+ (1u << UTF8PROC_CATEGORY_LM) |
143
+ (1u << UTF8PROC_CATEGORY_LO) |
144
+ (1u << UTF8PROC_CATEGORY_NL);
145
+ #else
111
146
  for (int i = 0x80; i < 0x100; i++) cls_add(n, (unsigned char)i);
147
+ #endif
112
148
  break;
113
- case 'c': /* XML NameChar = \i + digits . - */
149
+ case 'c': /* XML NameChar = \i + digits . - + combining */
114
150
  for (int i = 'a'; i <= 'z'; i++) cls_add(n, (unsigned char)i);
115
151
  for (int i = 'A'; i <= 'Z'; i++) cls_add(n, (unsigned char)i);
116
152
  for (int i = '0'; i <= '9'; i++) cls_add(n, (unsigned char)i);
@@ -118,18 +154,76 @@ static void cls_add_builtin(RreNode* n, char c) {
118
154
  cls_add(n, ':');
119
155
  cls_add(n, '.');
120
156
  cls_add(n, '-');
157
+ #if defined(LEPTRIS_HAS_UTF8PROC)
158
+ n->cat_set = 1;
159
+ n->cat_mask = (1u << UTF8PROC_CATEGORY_LU) |
160
+ (1u << UTF8PROC_CATEGORY_LL) |
161
+ (1u << UTF8PROC_CATEGORY_LT) |
162
+ (1u << UTF8PROC_CATEGORY_LM) |
163
+ (1u << UTF8PROC_CATEGORY_LO) |
164
+ (1u << UTF8PROC_CATEGORY_NL) |
165
+ (1u << UTF8PROC_CATEGORY_ND) |
166
+ (1u << UTF8PROC_CATEGORY_MN) |
167
+ (1u << UTF8PROC_CATEGORY_MC);
168
+ #else
121
169
  for (int i = 0x80; i < 0x100; i++) cls_add(n, (unsigned char)i);
170
+ #endif
122
171
  break;
123
172
  default:
124
173
  break;
125
174
  }
126
175
  }
127
176
 
177
+ static unsigned rre_decode_cp(const char* s, size_t avail, int* len);
178
+
128
179
  static int cls_has(const RreNode* n, unsigned char c) {
129
180
  int hit = (n->cls[c >> 3] >> (c & 7)) & 1;
130
181
  return n->neg ? !hit : hit;
131
182
  }
132
183
 
184
+ /* One class MEMBER as a codepoint: singleCharEsc punctuation and
185
+ * control escapes, otherwise the raw (multi-byte) character
186
+ * (moxml#464: [\]} ranges and astral char-ref members). */
187
+ static int cls_read_cp(RreParser* ps, unsigned* out) {
188
+ if (*ps->p == '\\') {
189
+ char e = ps->p[1];
190
+ if (!e) return 0;
191
+ ps->p += 2;
192
+ switch (e) {
193
+ case 't': *out = '\t'; return 1;
194
+ case 'n': *out = '\n'; return 1;
195
+ case 'r': *out = '\r'; return 1;
196
+ case '\\': case '.': case '-': case '^':
197
+ case '?': case '*': case '+': case '(': case ')':
198
+ case '[': case ']': case '{': case '}': case '|':
199
+ *out = (unsigned char)e; return 1;
200
+ default: return 0; /* classes (\d...) are not members */
201
+ }
202
+ }
203
+ if (((unsigned char)*ps->p) < 0x80) {
204
+ *out = (unsigned char)*ps->p++;
205
+ return 1;
206
+ }
207
+ int len = 0;
208
+ unsigned cp = rre_decode_cp(ps->p, strlen(ps->p), &len);
209
+ if (!len) return 0;
210
+ ps->p += len;
211
+ *out = cp;
212
+ return 1;
213
+ }
214
+
215
+ static void cls_add_cp(RreNode* n, unsigned cp) {
216
+ if (cp < 0x100) {
217
+ cls_add(n, (unsigned char)cp);
218
+ return;
219
+ }
220
+ if (n->uni_n < 4) {
221
+ n->uni_lo[n->uni_n] = cp;
222
+ n->uni_hi[n->uni_n] = cp;
223
+ n->uni_n++;
224
+ }
225
+ }
226
+
133
227
  /* Codepoint form (moxml#464): bytes 0x00-0xFF check the bitmap,
134
228
  * higher codepoints check the unicode ranges; neg flips the
135
229
  * combined result. */
@@ -147,14 +241,19 @@ static int cls_has_cp(const RreNode* n, unsigned cp) {
147
241
  }
148
242
  }
149
243
  #if defined(LEPTRIS_HAS_UTF8PROC)
150
- if (!hit && n->cat_set) {
151
- hit = (n->cat_mask &
152
- (1u << utf8proc_get_property((utf8proc_int32_t)cp)
153
- ->category)) != 0;
244
+ if (n->cat_set) {
245
+ int c = (n->cat_mask &
246
+ (1u << utf8proc_get_property((utf8proc_int32_t)cp)
247
+ ->category)) != 0;
248
+ hit = n->cat_strict ? (hit && c) : (hit || c);
154
249
  }
155
250
  #else
156
251
  (void)0;
157
252
  #endif
253
+ if (n->negm)
254
+ hit = hit || !cls_has_cp(n->negm, cp);
255
+ if (hit && n->subtract)
256
+ hit = !cls_has_cp(n->subtract, cp);
158
257
  return n->neg ? !hit : hit;
159
258
  }
160
259
 
@@ -443,7 +542,8 @@ static RreNode* parse_class(RreParser* ps) {
443
542
  int first = 1;
444
543
  while (*ps->p && (*ps->p != ']' || first)) {
445
544
  first = 0;
446
- if (*ps->p == '\\' && ps->p[1] == 'p' && ps->p[2] == '{') {
545
+ if (*ps->p == '\\' && (ps->p[1] == 'p' || ps->p[1] == 'P') &&
546
+ ps->p[2] == '{') {
447
547
  /* XSD property classes for the Latin-1 byte world —
448
548
  * the byte-class matcher covers these exactly
449
549
  * (leptris-ruby#434, ST_ContentType). Other blocks
@@ -456,6 +556,12 @@ static RreNode* parse_class(RreParser* ps) {
456
556
  return NULL;
457
557
  }
458
558
  size_t len = (size_t)(close - ps->p);
559
+ char pc = ps->p[-2]; /* 'p' or 'P' */
560
+ RreNode* tmp = node_new(RRE_CLASS);
561
+ if (!tmp) {
562
+ node_free(n);
563
+ return NULL;
564
+ }
459
565
  unsigned lo = 0, hi = 0;
460
566
  int hit = rre_block_range(ps->p, len, &lo, &hi);
461
567
  if (!hit && len == 2 && ps->p[0] == 'C' && ps->p[1] == 'c') {
@@ -463,7 +569,8 @@ static RreNode* parse_class(RreParser* ps) {
463
569
  hit = 1;
464
570
  }
465
571
  if (hit) {
466
- if (!cls_add_block(n, lo, hi)) {
572
+ if (!cls_add_block(tmp, lo, hi)) {
573
+ node_free(tmp);
467
574
  node_free(n);
468
575
  ps->bad = 1;
469
576
  return NULL;
@@ -474,24 +581,47 @@ static RreNode* parse_class(RreParser* ps) {
474
581
  #if defined(LEPTRIS_HAS_UTF8PROC)
475
582
  unsigned mask = 0;
476
583
  if (!rre_category_mask(ps->p, len, &mask)) {
584
+ node_free(tmp);
477
585
  node_free(n);
478
586
  ps->bad = 1;
479
587
  return NULL;
480
588
  }
481
- n->cat_set = 1;
482
- n->cat_mask |= mask;
589
+ tmp->cat_set = 1;
590
+ tmp->cat_mask |= mask;
483
591
  #else
592
+ node_free(tmp);
484
593
  node_free(n);
485
594
  ps->bad = 1;
486
595
  return NULL;
487
596
  #endif
488
597
  }
489
598
  if (len == 2 && ps->p[0] == 'C' && ps->p[1] == 'c') {
490
- cls_add(n, 0x7F);
599
+ cls_add(tmp, 0x7F);
491
600
  for (int i = 0x80; i <= 0x9F; i++)
492
- cls_add(n, (unsigned char)i);
601
+ cls_add(tmp, (unsigned char)i);
493
602
  }
494
603
  ps->p = close + 1;
604
+ /* in-class \P (moxml#464, reF43/reK4): the member is
605
+ * the complement of the parsed set — stored as negm and
606
+ * unioned at match time. A second \P member degrades to
607
+ * the positive spelling (single negm slot). */
608
+ if (pc == 'P' && !n->negm) {
609
+ n->negm = tmp;
610
+ continue;
611
+ }
612
+ for (int i = 0; i < 256; i++)
613
+ if ((tmp->cls[i >> 3] >> (i & 7)) & 1)
614
+ cls_add(n, (unsigned char)i);
615
+ for (int i = 0; i < tmp->uni_n && n->uni_n < 4; i++) {
616
+ n->uni_lo[n->uni_n] = tmp->uni_lo[i];
617
+ n->uni_hi[n->uni_n] = tmp->uni_hi[i];
618
+ n->uni_n++;
619
+ }
620
+ if (tmp->cat_set) {
621
+ n->cat_set = 1;
622
+ n->cat_mask |= tmp->cat_mask;
623
+ }
624
+ node_free(tmp);
495
625
  continue;
496
626
  }
497
627
  if (*ps->p == '\\' && ps->p[1]) {
@@ -508,6 +638,16 @@ static RreNode* parse_class(RreParser* ps) {
508
638
  cls_add_builtin(tmp, e);
509
639
  int neg_b = e == 'D' || e == 'W' || e == 'S' ||
510
640
  e == 'I' || e == 'C';
641
+ #if defined(LEPTRIS_HAS_UTF8PROC)
642
+ if (!neg_b && tmp->cat_set) {
643
+ n->cat_set = 1;
644
+ n->cat_mask |= tmp->cat_mask;
645
+ } else if (neg_b && tmp->cat_set && !n->cat_set) {
646
+ n->cat_set = 1;
647
+ n->cat_strict = 1;
648
+ n->cat_mask = (~tmp->cat_mask) & 0x7FFFFFFFu;
649
+ }
650
+ #endif
511
651
  for (int i = 0; i < 256; i++)
512
652
  if (((tmp->cls[i >> 3] >> (i & 7)) & 1) ==
513
653
  (neg_b ? 0 : 1))
@@ -543,10 +683,9 @@ static RreNode* parse_class(RreParser* ps) {
543
683
  node_free(n);
544
684
  return NULL;
545
685
  }
546
- for (int i = 0; i < 256; i++)
547
- if ((sub->cls[i >> 3] >> (i & 7)) & 1)
548
- n->cls[i >> 3] &= (unsigned char)~(1u << (i & 7));
549
- node_free(sub);
686
+ /* owned: the matcher consults it codepoint-wide, so
687
+ * ranges/blocks/categories subtract exactly */
688
+ n->subtract = sub;
550
689
  if (*ps->p != ']') {
551
690
  node_free(n);
552
691
  ps->bad = 1;
@@ -554,13 +693,30 @@ static RreNode* parse_class(RreParser* ps) {
554
693
  }
555
694
  break;
556
695
  }
557
- unsigned char c = (unsigned char)*ps->p++;
696
+ unsigned lo = 0;
697
+ if (!cls_read_cp(ps, &lo)) {
698
+ node_free(n);
699
+ ps->bad = 1;
700
+ return NULL;
701
+ }
558
702
  if (*ps->p == '-' && ps->p[1] && ps->p[1] != ']') {
559
703
  ps->p++;
560
- unsigned char hi = (unsigned char)*ps->p++;
561
- for (unsigned int i = c; i <= hi; i++) cls_add(n, (unsigned char)i);
704
+ unsigned hi = 0;
705
+ if (!cls_read_cp(ps, &hi) || hi < lo) {
706
+ node_free(n);
707
+ ps->bad = 1;
708
+ return NULL;
709
+ }
710
+ if (lo < 0x100 && hi < 0x100) {
711
+ for (unsigned int i = lo; i <= hi; i++)
712
+ cls_add(n, (unsigned char)i);
713
+ } else if (n->uni_n < 4) {
714
+ n->uni_lo[n->uni_n] = lo;
715
+ n->uni_hi[n->uni_n] = hi;
716
+ n->uni_n++;
717
+ }
562
718
  } else {
563
- cls_add(n, c);
719
+ cls_add_cp(n, lo);
564
720
  }
565
721
  }
566
722
  if (*ps->p != ']') {
@@ -651,25 +807,18 @@ static RreNode* parse_atom(RreParser* ps) {
651
807
  ps->p += 2;
652
808
  RreNode* n = node_new(RRE_CLASS);
653
809
  if (!n) return NULL;
654
- if (e == 'd' || e == 'w' || e == 's' || e == 'i' ||
655
- e == 'c') {
656
- cls_add_builtin(n, e);
657
- } else {
658
- memset(n->cls, 0xff, sizeof(n->cls));
659
- RreNode* tmp = node_new(RRE_CLASS);
660
- if (!tmp) {
661
- node_free(n);
662
- return NULL;
663
- }
664
- cls_add_builtin(tmp, e == 'D' ? 'd'
665
- : e == 'W' ? 'w'
666
- : e == 'S' ? 's'
667
- : e == 'I' ? 'i' : 'c');
668
- for (int i = 0; i < 256; i++)
669
- if ((tmp->cls[i >> 3] >> (i & 7)) & 1)
670
- n->cls[i >> 3] &= (unsigned char)~(1u << (i & 7));
671
- node_free(tmp);
672
- }
810
+ /* complement atoms are the builtin with neg: cls_has_cp
811
+ * flips the combined bitmap/range/categorie result, so
812
+ * the class members stay exact (the old bitmap memset
813
+ * could not represent the category complement) */
814
+ cls_add_builtin(n, e == 'D' ? 'd'
815
+ : e == 'W' ? 'w'
816
+ : e == 'S' ? 's'
817
+ : e == 'I' ? 'i'
818
+ : e == 'C' ? 'c' : e);
819
+ if (e == 'D' || e == 'W' || e == 'S' || e == 'I' ||
820
+ e == 'C')
821
+ n->neg = 1;
673
822
  return n;
674
823
  }
675
824
  ps->p += 2;
@@ -830,7 +979,8 @@ static const char* rre_match(RreNode* n, const char* s,
830
979
  return NULL;
831
980
  case RRE_CLASS: {
832
981
  if (!*s) return NULL;
833
- if ((unsigned char)*s < 0x80 && !n->cat_set) {
982
+ if ((unsigned char)*s < 0x80 && !n->cat_set &&
983
+ !n->subtract && !n->negm) {
834
984
  if (cls_has(n, (unsigned char)*s))
835
985
  return rre_run(k, s + 1);
836
986
  return NULL;
@@ -900,6 +1050,10 @@ int rng_regex_supported(const char* pat) {
900
1050
 
901
1051
  int rng_regex_matches(const char* pat, const char* text) {
902
1052
  if (!pat || !text) return 0;
1053
+ /* XSD Appendix F.1: an EMPTY pattern branch matches only the
1054
+ * empty string (the reA1 corpus shape: pattern="" with
1055
+ * value="") */
1056
+ if (!*pat) return !*text;
903
1057
  RreParser ps;
904
1058
  ps.p = pat;
905
1059
  ps.depth = 0;
@@ -2294,16 +2294,29 @@ static void xsd_capture_root(struct leptris_xsd_schema* s,
2294
2294
  * synthesized slot and point the declaration at it. */
2295
2295
  char synth[512];
2296
2296
  snprintf(synth, sizeof(synth), "element:%s", nm);
2297
- if (xsd_capture_inline(s, (LeptrisElement)n, synth)) {
2298
- XsdElementDecl* d =
2299
- (XsdElementDecl*)calloc(1, sizeof(*d));
2300
- if (d) {
2301
- d->name = xsd_strdup(nm);
2302
- if (s->target_ns) d->ns = xsd_strdup(s->target_ns);
2297
+ /* moxml#466: the declaration is captured even when
2298
+ * the element carries no inline type (a bare
2299
+ * <xs:element name=... substitutionGroup=.../> member
2300
+ * has none) — the strict root walk and the head
2301
+ * chain both need the row to exist. */
2302
+ int has_inline =
2303
+ xsd_capture_inline(s, (LeptrisElement)n, synth);
2304
+ XsdElementDecl* d =
2305
+ (XsdElementDecl*)calloc(1, sizeof(*d));
2306
+ if (d) {
2307
+ d->name = xsd_strdup(nm);
2308
+ if (s->target_ns) d->ns = xsd_strdup(s->target_ns);
2309
+ if (has_inline)
2303
2310
  d->type = xsd_strdup(synth);
2304
- d->next = s->elements;
2305
- s->elements = d;
2311
+ const char* sg = leptris_element_attribute(
2312
+ (LeptrisElement)n, "substitutionGroup");
2313
+ if (sg) {
2314
+ const char* colon = strchr(sg, ':');
2315
+ d->sub_head = xsd_strdup(
2316
+ colon ? colon + 1 : sg);
2306
2317
  }
2318
+ d->next = s->elements;
2319
+ s->elements = d;
2307
2320
  }
2308
2321
  }
2309
2322
  } else if (strcmp(local, "complexType") == 0 && uri &&
@@ -2746,12 +2759,16 @@ static XsdCm* xsd_capture_complex_model(struct leptris_xsd_schema* s,
2746
2759
  * half-captured base silently truncates the model.
2747
2760
  * The visited memo means each file merges once, so
2748
2761
  * the double capture that used to mask this is
2749
- * gone. */
2762
+ * gone. EXTENSION ONLY (moxml#466): a restriction's
2763
+ * model REPLACES the base's — folding it in prepends
2764
+ * a duplicate particle sequence and demands the base
2765
+ * content next to the derived content. */
2750
2766
  int defer_base =
2751
2767
  base_root
2752
2768
  ? base_root->ext_base != NULL
2753
2769
  : (base && strncmp(base, "xs:", 3) != 0);
2754
- if (defer_base && base) {
2770
+ if (defer_base && base &&
2771
+ strcmp(dl, "extension") == 0) {
2755
2772
  free(root->ext_base);
2756
2773
  root->ext_base = xsd_strdup(base);
2757
2774
  }
@@ -255,6 +255,11 @@ static void ss_close(StateSet* ss, CmNfa* n) {
255
255
  static int all_root_valid(XsdCm* all, const char* target_ns,
256
256
  const char* const* names,
257
257
  const char* const* ns_uris, size_t count) {
258
+ /* the whole all is ABSENT (moxml#466, mgAa003/mgD013): with
259
+ * minOccurs="0" an empty instance validates even when the
260
+ * particles carry min=1 — their bounds apply only when the all
261
+ * is present */
262
+ if (count == 0 && all->min == 0) return 1;
258
263
  for (XsdCm* p = all->first_child; p; p = p->next) {
259
264
  size_t hits = 0;
260
265
  for (size_t i = 0; i < count; i++) {
@@ -325,15 +325,18 @@ static void validate_children(struct xsd_validator* v,
325
325
  leptris_node_first_child(leptris_element_as_node(elem));
326
326
  c; c = leptris_node_next_sibling(c))
327
327
  if (leptris_node_get_type(c) == LEPTRIS_NODE_TYPE_ELEMENT) n++;
328
- if (!n) return;
329
- const char** names = (const char**)malloc(n * sizeof(char*));
330
- const char** uris = (const char**)calloc(n, sizeof(char*));
331
- if (!names || !uris) {
332
- free(names);
333
- free(uris);
334
- return;
335
- }
328
+ const char** names = NULL;
329
+ const char** uris = NULL;
336
330
  size_t i = 0;
331
+ if (n > 0) {
332
+ names = (const char**)malloc(n * sizeof(char*));
333
+ uris = (const char**)calloc(n, sizeof(char*));
334
+ if (!names || !uris) {
335
+ free(names);
336
+ free(uris);
337
+ return;
338
+ }
339
+ }
337
340
  for (LeptrisNodeRef c =
338
341
  leptris_node_first_child(leptris_element_as_node(elem));
339
342
  c; c = leptris_node_next_sibling(c)) {
@@ -346,6 +349,11 @@ static void validate_children(struct xsd_validator* v,
346
349
  uris[i] = uri;
347
350
  i++;
348
351
  }
352
+ /* moxml#464-adjacent false-valid: the early return on !n let an
353
+ * empty instance pass ANY content model — required particles
354
+ * never failed. Zero children still walks: the NFA accepts the
355
+ * empty string only when every particle reaches accept with
356
+ * min == 0. names/uris stay NULL (never dereferenced at n==0). */
349
357
  int ok = xsd_content_valid(s, model, xsd_target_ns(s), names, uris,
350
358
  n);
351
359
  if (ok) {
@@ -143,6 +143,17 @@ target_compile_definitions(test_rng_corpus PRIVATE
143
143
  leptris_add_test(test_rng_corpus_msg rng/test_rng_corpus_msg.cpp)
144
144
  target_compile_definitions(test_rng_corpus_msg PRIVATE
145
145
  LEPTRIS_RNG_CASES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/rng/jing-cases")
146
+ if(LEPTRIS_ENABLE_UTF8PROC)
147
+ # the regex suites gate their unicode-escape assertions on the
148
+ # same macro the ENGINE was built with — a mismatch silently
149
+ # compiles out the assertions (the gtest then "passes" while
150
+ # the engine behavior goes unverified).
151
+ target_compile_definitions(test_rng PRIVATE LEPTRIS_HAS_UTF8PROC=1)
152
+ target_compile_definitions(test_rng_corpus
153
+ PRIVATE LEPTRIS_HAS_UTF8PROC=1)
154
+ target_compile_definitions(test_rng_corpus_msg
155
+ PRIVATE LEPTRIS_HAS_UTF8PROC=1)
156
+ endif()
146
157
  leptris_add_test(test_simd_text common/test_simd_text.cpp)
147
158
  leptris_add_test(test_simd_spans common/test_simd_spans.cpp)
148
159
  leptris_add_test(test_pool memory/test_pool.cpp)
@@ -1491,6 +1491,96 @@ TEST(RngRegex, BlockEscapesUnicode) {
1491
1491
  "\xE4\xB8\x80"), 1);
1492
1492
  }
1493
1493
 
1494
+ /* moxml#464 subtraction edges: the subtrahend carries RANGES and
1495
+ * categories, not just byte bits — the byte-bitmap merge ignored
1496
+ * them, so subtracting a range subtracted nothing. */
1497
+ TEST(RngRegex, SubtractionWithRangesAndCats) {
1498
+ /* byte-only subtraction still exact */
1499
+ EXPECT_EQ(rng_regex_matches("[a-z-[aeiou]]+", "bcd"), 1);
1500
+ EXPECT_EQ(rng_regex_matches("[a-z-[aeiou]]+", "axe"), 0);
1501
+ /* range in the subtrahend: subtract ALL lowercase from the
1502
+ * printable-ASCII range */
1503
+ EXPECT_EQ(rng_regex_matches("[!-~-[a-z]]+", "AB1!"), 1);
1504
+ EXPECT_EQ(rng_regex_matches("[!-~-[a-z]]+", "ab"), 0);
1505
+ EXPECT_EQ(rng_regex_matches("[!-~-[a-z]]+", "\xCE\xB1"), 0);
1506
+ #if defined(LEPTRIS_HAS_UTF8PROC)
1507
+ /* category in the subtrahend: letters minus uppercase */
1508
+ EXPECT_EQ(rng_regex_matches("[\\p{L}-[\\p{Lu}]]+", "abc"), 1);
1509
+ EXPECT_EQ(rng_regex_matches("[\\p{L}-[\\p{Lu}]]+", "ABC"), 0);
1510
+ EXPECT_EQ(rng_regex_matches("[\\p{L}-[\\p{Lu}]]+", "\xCE\xB1"), 1);
1511
+ EXPECT_EQ(rng_regex_matches("[\\p{L}-[\\p{Lu}]]+", "\xCE\x91"), 0);
1512
+ /* block in the subtrahend */
1513
+ EXPECT_EQ(rng_regex_matches("[\\p{L}-[\\p{IsGreek}]]+", "abc"), 1);
1514
+ EXPECT_EQ(rng_regex_matches("[\\p{L}-[\\p{IsGreek}]]+",
1515
+ "\xCE\xB1"), 0);
1516
+ #endif
1517
+ }
1518
+
1519
+ /* moxml#464 \d / \D: XML decimal = Nd category — Arabic-Indic
1520
+ * and the other unicode digit families are \\d too. */
1521
+ #if defined(LEPTRIS_HAS_UTF8PROC)
1522
+ TEST(RngRegex, DigitEscapesAreUnicode) {
1523
+ /* U+0660 Arabic-Indic zero = D9 A0 */
1524
+ EXPECT_EQ(rng_regex_matches("\\d", "\xD9\xA0"), 1);
1525
+ EXPECT_EQ(rng_regex_matches("\\d+", "\xD9\xA0\xD9\xA1"), 1);
1526
+ EXPECT_EQ(rng_regex_matches("\\d", "a"), 0);
1527
+ EXPECT_EQ(rng_regex_matches("\\D", "\xD9\xA0"), 0);
1528
+ EXPECT_EQ(rng_regex_matches("\\D", "a"), 1);
1529
+ }
1530
+
1531
+ TEST(RngRegex, EmptyPatternMatchesEmptyOnly) {
1532
+ EXPECT_EQ(rng_regex_matches("", ""), 1);
1533
+ EXPECT_EQ(rng_regex_matches("", "a"), 0);
1534
+ }
1535
+
1536
+ TEST(RngRegex, EscapedBracketInRange) {
1537
+ EXPECT_EQ(rng_regex_matches("[a-\\}]+", "abc}"), 1);
1538
+ EXPECT_EQ(rng_regex_matches("[a-\\}]+", "!"), 0);
1539
+ EXPECT_EQ(rng_regex_matches("[1-\\]]+", "12]"), 1);
1540
+ }
1541
+
1542
+ #if defined(LEPTRIS_HAS_UTF8PROC)
1543
+ /* moxml#464 in-class \P: complement member (reF43/reK4). Exact as
1544
+ * the class's only member; the class negation composes. */
1545
+ TEST(RngRegex, NegatedPropertyInsideClass) {
1546
+ EXPECT_EQ(rng_regex_matches("[^\\P{IsBasicLatin}]", "a"), 1);
1547
+ EXPECT_EQ(rng_regex_matches("[^\\P{IsBasicLatin}]", "\xC3"), 0);
1548
+ EXPECT_EQ(rng_regex_matches("[\\P{L}*]", "*"), 1);
1549
+ EXPECT_EQ(rng_regex_matches("[\\P{L}*]", "a"), 0);
1550
+ EXPECT_EQ(rng_regex_matches("[\\P{L}]", "\xCE\xB1"), 0);
1551
+ EXPECT_EQ(rng_regex_matches("[\\P{L}]{0,2}", "*!"), 1);
1552
+ }
1553
+ #endif
1554
+
1555
+ TEST(RngRegex, AstralCharClassMember) {
1556
+ /* U+10000 = F0 90 80 80 */
1557
+ EXPECT_EQ(rng_regex_matches("[\xF0\x90\x80\x80]",
1558
+ "\xF0\x90\x80\x80"), 1);
1559
+ EXPECT_EQ(rng_regex_matches("[\xF0\x90\x80\x80]", "a"), 0);
1560
+ }
1561
+ #endif
1562
+
1563
+ /* moxml#464 \i / \c: XML NameStartChar / NameChar escapes. */
1564
+ TEST(RngRegex, NameCharEscapes) {
1565
+ EXPECT_EQ(rng_regex_matches("\\i+", "abc_:def"), 1);
1566
+ EXPECT_EQ(rng_regex_matches("\\i", "9"), 0);
1567
+ EXPECT_EQ(rng_regex_matches("\\c+", "a-b.c9_d:e"), 1);
1568
+ EXPECT_EQ(rng_regex_matches("\\c", "*"), 0);
1569
+ #if defined(LEPTRIS_HAS_UTF8PROC)
1570
+ /* non-ASCII letters are NameStartChars */
1571
+ EXPECT_EQ(rng_regex_matches("\\i", "\xCE\xB1"), 1);
1572
+ EXPECT_EQ(rng_regex_matches("\\c", "\xCE\xB1"), 1);
1573
+ /* Latin-1 symbols are NOT NameStartChars (the old 0x80-0xFF
1574
+ * blanket accepted them): U+00D7 multiplication sign is Sm */
1575
+ EXPECT_EQ(rng_regex_matches("\\i", "\xC3\x97"), 0);
1576
+ EXPECT_EQ(rng_regex_matches("\\c", "\xC3\x97"), 0);
1577
+ /* combining marks are NameChars but not NameStartChars:
1578
+ * U+0300 combining grave is Mn */
1579
+ EXPECT_EQ(rng_regex_matches("\\i", "\xCC\x80"), 0);
1580
+ EXPECT_EQ(rng_regex_matches("\\c", "\xCC\x80"), 1);
1581
+ #endif
1582
+ }
1583
+
1494
1584
  /* moxml#464 category escapes: \p{L} family masks over the
1495
1585
  * utf8proc categories (LEPTRIS_HAS_UTF8PROC builds). */
1496
1586
  #if defined(LEPTRIS_HAS_UTF8PROC)
@@ -723,6 +723,47 @@ TEST(XsdValidate, ValidDocumentPasses) {
723
723
  EXPECT_EQ(validate_doc(k_v_schema, "<count>3</count>"), 1);
724
724
  }
725
725
 
726
+ // moxml#466: an xs:all with minOccurs="0" may be ABSENT — an empty
727
+ // instance validates even when its particles carry min=1 (the
728
+ // mgAa003/mgD013 corpus shapes).
729
+ TEST(XsdValidate, AllMinOccursZeroMayBeAbsent) {
730
+ const char* sch =
731
+ "<xsd:schema xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
732
+ "<xsd:element name=\"doc\" type=\"foo\"/>"
733
+ "<xsd:complexType name=\"foo\">"
734
+ "<xsd:all minOccurs=\"0\">"
735
+ "<xsd:element name=\"t1\"/>"
736
+ "</xsd:all></xsd:complexType>"
737
+ "</xsd:schema>";
738
+ EXPECT_EQ(validate_doc(sch, "<doc/>"), 1);
739
+ EXPECT_EQ(validate_doc(sch, "<doc><t1/></doc>"), 1);
740
+ EXPECT_EQ(validate_doc(sch, "<doc><t1/><t1/></doc>"), 0);
741
+ }
742
+
743
+ TEST(XsdValidate, AllMinOccursZeroAbsentWithRequiredParticles) {
744
+ const char* sch =
745
+ "<xsd:schema xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
746
+ "<xsd:element name=\"root\">"
747
+ "<xsd:complexType><xsd:choice minOccurs=\"1\""
748
+ " maxOccurs=\"unbounded\">"
749
+ "<xsd:element name=\"elt1\">"
750
+ "<xsd:complexType>"
751
+ "<xsd:all minOccurs=\"0\">"
752
+ "<xsd:element name=\"elt2\"/>"
753
+ "<xsd:element name=\"elt3\"/>"
754
+ "<xsd:element name=\"elt4\"/>"
755
+ "</xsd:all></xsd:complexType>"
756
+ "</xsd:element>"
757
+ "</xsd:choice></xsd:complexType>"
758
+ "</xsd:element></xsd:schema>";
759
+ EXPECT_EQ(validate_doc(sch, "<root><elt1/></root>"), 1);
760
+ EXPECT_EQ(validate_doc(
761
+ sch, "<root><elt1><elt2/><elt3/><elt4/></elt1>"
762
+ "</root>"), 1);
763
+ EXPECT_EQ(validate_doc(
764
+ sch, "<root><elt1><elt2/></elt1></root>"), 0);
765
+ }
766
+
726
767
  TEST(XsdValidate, ContentModelViolationReports) {
727
768
  EXPECT_EQ(validate_doc(k_v_schema,
728
769
  "<rows version=\"1\">"
@@ -1314,6 +1355,98 @@ TEST(XsdValidate, SubstitutionGroupMembersBind) {
1314
1355
  "<box><other>z</other></box>"), 0);
1315
1356
  }
1316
1357
 
1358
+ // moxml#466 class 1: a member of a substitution group binds a
1359
+ // ref-particle in a NAMESPACED schema (particlesDc001 family) —
1360
+ // elementFormDefault="qualified" + targetNamespace, head reached
1361
+ // through x:elem1.
1362
+ const char* k_subst_ns_schema =
1363
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
1364
+ " xmlns=\"http://xsdtesting\""
1365
+ " xmlns:x=\"http://xsdtesting\""
1366
+ " targetNamespace=\"http://xsdtesting\""
1367
+ " elementFormDefault=\"qualified\">"
1368
+ "<xs:element name=\"doc\">"
1369
+ "<xs:complexType><xs:sequence>"
1370
+ "<xs:element ref=\"x:elem1\"/>"
1371
+ "</xs:sequence></xs:complexType>"
1372
+ "</xs:element>"
1373
+ "<xs:element name=\"elem1\" abstract=\"false\"/>"
1374
+ "<xs:element name=\"elem2\" substitutionGroup=\"x:elem1\"/>"
1375
+ "</xs:schema>";
1376
+
1377
+ TEST(XsdValidate, SubstitutionGroupMemberBindsRefParticleQualified) {
1378
+ EXPECT_EQ(validate_doc(
1379
+ k_subst_ns_schema,
1380
+ "<x:doc xmlns:x=\"http://xsdtesting\">"
1381
+ "<x:elem1/></x:doc>"), 1);
1382
+ EXPECT_EQ(validate_doc(
1383
+ k_subst_ns_schema,
1384
+ "<x:doc xmlns:x=\"http://xsdtesting\">"
1385
+ "<x:elem2/></x:doc>"), 1);
1386
+ EXPECT_EQ(validate_doc(
1387
+ k_subst_ns_schema,
1388
+ "<x:doc xmlns:x=\"http://xsdtesting\">"
1389
+ "<x:other/></x:doc>"), 0);
1390
+ }
1391
+
1392
+ // moxml#466 class 2: a complexContent RESTRICTION whose base is a
1393
+ // forward reference must not defer-fold — the derived model
1394
+ // REPLACES the base's. The fold prepended the base's particles and
1395
+ // demanded the base content next to the derived content
1396
+ // (instance <e1/> rejected against SEQ(e1) restriction of
1397
+ // SEQ(e1) base because the fold produced SEQ(SEQ(e1), SEQ(e1))).
1398
+ TEST(XsdValidate, RestrictionReplacesForwardBaseModel) {
1399
+ const char* fwd =
1400
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
1401
+ "<xs:element name=\"doc\" type=\"foo\"/>"
1402
+ "<xs:complexType name=\"foo\"><xs:complexContent>"
1403
+ "<xs:restriction base=\"bar\">"
1404
+ "<xs:sequence><xs:element name=\"e1\"/></xs:sequence>"
1405
+ "</xs:restriction></xs:complexContent></xs:complexType>"
1406
+ "<xs:complexType name=\"bar\"><xs:sequence>"
1407
+ "<xs:element name=\"e1\"/><xs:element name=\"e2\"/>"
1408
+ "</xs:sequence></xs:complexType>"
1409
+ "</xs:schema>";
1410
+ EXPECT_EQ(validate_doc(fwd, "<doc><e1/></doc>"), 1);
1411
+ /* the base's model must NOT ride along */
1412
+ EXPECT_EQ(validate_doc(fwd, "<doc><e1/><e2/></doc>"), 0);
1413
+ /* resolved (non-forward) base: same replace semantics */
1414
+ const char* resolved =
1415
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
1416
+ "<xs:element name=\"doc\" type=\"foo\"/>"
1417
+ "<xs:complexType name=\"bar\"><xs:sequence>"
1418
+ "<xs:element name=\"e1\"/><xs:element name=\"e2\"/>"
1419
+ "</xs:sequence></xs:complexType>"
1420
+ "<xs:complexType name=\"foo\"><xs:complexContent>"
1421
+ "<xs:restriction base=\"bar\">"
1422
+ "<xs:sequence><xs:element name=\"e1\"/></xs:sequence>"
1423
+ "</xs:restriction></xs:complexContent></xs:complexType>"
1424
+ "</xs:schema>";
1425
+ EXPECT_EQ(validate_doc(resolved, "<doc><e1/></doc>"), 1);
1426
+ EXPECT_EQ(validate_doc(resolved, "<doc><e1/><e2/></doc>"), 0);
1427
+ }
1428
+
1429
+ /* mgO008 family: restriction through a group-ref particle — the
1430
+ * derived model (the group's all) is the whole content model. */
1431
+ TEST(XsdValidate, RestrictionGroupRefParticleReplaces) {
1432
+ const char* grp =
1433
+ "<xsd:schema xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
1434
+ "<xsd:element name=\"doc\" type=\"foo\"/>"
1435
+ "<xsd:complexType name=\"foo\"><xsd:complexContent>"
1436
+ "<xsd:restriction base=\"bar\">"
1437
+ "<xsd:group ref=\"group\"/>"
1438
+ "</xsd:restriction></xsd:complexContent></xsd:complexType>"
1439
+ "<xsd:complexType name=\"bar\">"
1440
+ "<xsd:all minOccurs=\"1\" maxOccurs=\"1\">"
1441
+ "<xsd:element name=\"e1\"/><xsd:element name=\"e2\"/>"
1442
+ "</xsd:all></xsd:complexType>"
1443
+ "<xsd:group name=\"group\"><xsd:all>"
1444
+ "<xsd:element name=\"e1\"/></xsd:all></xsd:group>"
1445
+ "</xsd:schema>";
1446
+ EXPECT_EQ(validate_doc(grp, "<doc><e1/></doc>"), 1);
1447
+ EXPECT_EQ(validate_doc(grp, "<doc><e1/><e2/></doc>"), 0);
1448
+ }
1449
+
1317
1450
  } // namespace
1318
1451
 
1319
1452
  // ---- tier-1 charter: include/import via compile_file ---------------
@@ -3166,3 +3299,36 @@ TEST(XsdNegativeWildcard, AnyAttributeNotQNameDefined) {
3166
3299
  leptris_document_free(b);
3167
3300
  leptris_xsd_free(s);
3168
3301
  }
3302
+
3303
+ // moxml#464-adjacent false-valid: an EMPTY instance against a
3304
+ // required-particle content model must fail — the walk's `!n`
3305
+ // early-return let it pass (the NFA never saw the instance).
3306
+ TEST(XsdContentModel, EmptyInstanceAgainstRequiredSequenceFails) {
3307
+ LeptrisStatus st = LEPTRIS_OK;
3308
+ const char xsd[] =
3309
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
3310
+ "<xs:element name=\"note\">"
3311
+ "<xs:complexType>"
3312
+ "<xs:sequence>"
3313
+ "<xs:element name=\"to\" type=\"xs:string\"/>"
3314
+ "</xs:sequence>"
3315
+ "</xs:complexType>"
3316
+ "</xs:element>"
3317
+ "</xs:schema>";
3318
+ LeptrisXsdSchema s = leptris_xsd_compile(xsd, strlen(xsd), &st);
3319
+ ASSERT_NE(s, (LeptrisXsdSchema)0) << "status " << (int)st;
3320
+
3321
+ const char empty[] = "<note/>";
3322
+ LeptrisDocument d = leptris_parse_string(empty, strlen(empty), &st);
3323
+ ASSERT_NE(d, nullptr);
3324
+ EXPECT_EQ(leptris_xsd_validate(s, d), 0);
3325
+ leptris_document_free(d);
3326
+
3327
+ // the satisfied model stays valid
3328
+ const char full[] = "<note><to>x</to></note>";
3329
+ LeptrisDocument f = leptris_parse_string(full, strlen(full), &st);
3330
+ ASSERT_NE(f, nullptr);
3331
+ EXPECT_EQ(leptris_xsd_validate(s, f), 1);
3332
+ leptris_document_free(f);
3333
+ leptris_xsd_free(s);
3334
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
3
3
  "name": "leptris",
4
- "version": "1.9.382",
4
+ "version": "1.9.383",
5
5
  "description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
6
6
  "homepage": "https://github.com/leptris/leptris",
7
7
  "license": "MIT",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.382.0
4
+ version: 1.9.383.0
5
5
  platform: arm-linux
6
6
  authors:
7
7
  - Ribose Inc.