leptris 1.9.418.1-ppc64le-linux → 1.9.420.0-ppc64le-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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/ext/leptris/native/native.c +38 -0
  4. data/lib/leptris/version.rb +1 -1
  5. data/lib/leptris/xml/native.so +0 -0
  6. data/lib/leptris/xml/native_3_3.so +0 -0
  7. data/lib/libleptris.so +0 -0
  8. data/vendor-src/README.md +1 -1
  9. data/vendor-src/libleptris/CHANGELOG.md +26 -0
  10. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  11. data/vendor-src/libleptris/TODO.max-perf/15-c-face-vs-pugixml.md +106 -0
  12. data/vendor-src/libleptris/src/leptris/xsd/compile.c +81 -31
  13. data/vendor-src/libleptris/src/leptris/xsd/validate.c +193 -23
  14. data/vendor-src/libleptris/src/leptris/xsd/xsd_internal.h +3 -0
  15. data/vendor-src/libleptris/test/xsd/libxml2-cases/attZ014-set/attZ014a-11.xsd +16 -0
  16. data/vendor-src/libleptris/test/xsd/libxml2-cases/attZ014-set/attZ014a.xml +1 -0
  17. data/vendor-src/libleptris/test/xsd/libxml2-cases/attZ014-set/attZ014a.xsd +16 -0
  18. data/vendor-src/libleptris/test/xsd/libxml2-cases/attZ014-set/attZ014b.xml +1 -0
  19. data/vendor-src/libleptris/test/xsd/libxml2-cases/attZ014-set/attZ014b.xsd +16 -0
  20. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC035.imp +9 -0
  21. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC035.red +9 -0
  22. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC035.xml +1 -0
  23. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC035.xsd +21 -0
  24. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC036.inc +9 -0
  25. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC036.red +8 -0
  26. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC036.xml +1 -0
  27. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC036.xsd +14 -0
  28. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC037.xml +1 -0
  29. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC037.xsd +17 -0
  30. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC038.xml +1 -0
  31. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC038.xsd +19 -0
  32. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC038v1.red +7 -0
  33. data/vendor-src/libleptris/test/xsd/libxml2-cases/attgC-redefine/attgC038v2.red +8 -0
  34. data/vendor-src/libleptris/test/xsd/libxml2-cases/rs74-union-fixed/stE065.xml +1 -0
  35. data/vendor-src/libleptris/test/xsd/libxml2-cases/rs74-union-fixed/stE065.xsd +15 -0
  36. data/vendor-src/libleptris/test/xsd/libxml2-cases/rs74-union-fixed/stE066.xml +1 -0
  37. data/vendor-src/libleptris/test/xsd/libxml2-cases/rs74-union-fixed/stE066.xsd +7 -0
  38. data/vendor-src/libleptris/test/xsd/libxml2-cases/rs74-union-fixed/stE096.xml +1 -0
  39. data/vendor-src/libleptris/test/xsd/libxml2-cases/rs74-union-fixed/stE096.xsd +14 -0
  40. data/vendor-src/libleptris/test/xsd/libxml2-cases/wildZ013-set/test328873.xsd +122 -0
  41. data/vendor-src/libleptris/test/xsd/libxml2-cases/wildZ013-set/test328873_imp.xsd +15 -0
  42. data/vendor-src/libleptris/test/xsd/libxml2-cases/wildZ013-set/test328873b.xml +3 -0
  43. data/vendor-src/libleptris/test/xsd/libxml2-cases/wildZ013-set/test328873c.xml +3 -0
  44. data/vendor-src/libleptris/test/xsd/test_xsd_compile.cpp +169 -0
  45. data/vendor-src/libleptris/vcpkg.json +1 -1
  46. metadata +31 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c689812db7e1cb340db6b33aa9b1740d6aa74d0fd4b9f0a31093b4fd7a634b25
4
- data.tar.gz: 2ecb0d6953d0ea924e5f19560445c24d8eeaa9da9098f81818d65bd948a37f7b
3
+ metadata.gz: bfd921282d6b7f1c27ed608e90ecd9e827f597ff5ee17453139d71e59f197a94
4
+ data.tar.gz: db3496c29df65697e748d121535355399ca2b1732022ebd03493461c1704de11
5
5
  SHA512:
6
- metadata.gz: bf2be4df8c1fadeb4122f6bfdd47e91bf39f043e5a2b6145f9b597e8027ca4ece5cc0ae6501b0df9f386d8df29d671c68910497af8f436e403b5c0f4b8a2485a
7
- data.tar.gz: 3b527998d6aa9b59046d64d6a420ebd51eafade821b3fb454dfc539441038c84efa8fae76b1d52aa914162b82e7d87f2316b74b1445e5918b7002a3b2d372cff
6
+ metadata.gz: a70bb31f1cfbab7a36a96626e4b3ec578dbfd4e3b9a12d35d487a094356c445fdb2475c8e6d8122eb79bc2bf38f7b6b4856fc406465ea694a086b570a5bc61a9
7
+ data.tar.gz: 36acd6a66bcd6dc5d78115ef6461d53cddf627d06374f1631c5246affd2cd4b0e96e032d9bace7784a51d511202edb2574306fc24504219652689479c1da1cac
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.418"
11
+ LIBLEPTRIS_VERSION = "1.9.420"
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
@@ -1681,6 +1681,42 @@ static VALUE nn_set_root(VALUE klass, VALUE document, VALUE element)
1681
1681
  return element;
1682
1682
  }
1683
1683
 
1684
+ /* TODO.perf/39(a) (leptris-ruby#622 ask 1): hot readers
1685
+ * parameterized by subclass. Consumers whose wrapper classes SUBCLASS
1686
+ * NativeNode (klass injection, #340) shadow these C methods with
1687
+ * included Ruby modules — every shadowed read pays an
1688
+ * UnboundMethod#bind_call frame (~the whole difference between the
1689
+ * facade's ~331ns reads and the binding's ~160ns seam). This entry
1690
+ * defines the SAME C functions (same bodies, same walk-memos, same
1691
+ * TypedData type) DIRECTLY on the consumer's subclass, so lookup
1692
+ * answers in C with no module shadow in the chain.
1693
+ *
1694
+ * The consumer decides which classes get the hot surface: a
1695
+ * per-document klass map (#340) can route entity-bearing documents
1696
+ * to contract classes and clean documents to hot classes, keeping
1697
+ * any Ruby-side value post-processing exactly where it is still
1698
+ * needed.
1699
+ *
1700
+ * Re-defining a method that already exists on the class replaces it
1701
+ * (Ruby semantics); calling twice is harmless. The class must be the
1702
+ * consumer's NativeNode subclass or NativeNode itself — a non-
1703
+ * TypedData class would crash on first read, so the type is checked
1704
+ * here against the registry. */
1705
+ static VALUE nf_define_hot_readers(VALUE self, VALUE klass)
1706
+ {
1707
+ (void)self;
1708
+ if (!RB_TYPE_P(klass, T_CLASS))
1709
+ rb_raise(rb_eTypeError, "expected a class");
1710
+ if (klass != c_native_node &&
1711
+ rb_class_inherited_p(klass, c_native_node) != Qtrue)
1712
+ rb_raise(rb_eTypeError,
1713
+ "the class must subclass Leptris::XML::NativeNode");
1714
+ rb_define_method(klass, "name", nn_name, 0);
1715
+ rb_define_method(klass, "content", nn_content, 0);
1716
+ rb_define_method(klass, "attribute", nn_attribute, 1);
1717
+ return Qtrue;
1718
+ }
1719
+
1684
1720
  /* NativeNode surface completion (TODO.perf/05): bulk attribute
1685
1721
  * hash (name => value, one C walk, no per-attr Ruby frames) and
1686
1722
  * the position readers. */
@@ -4350,6 +4386,8 @@ static void leptris_native_setup(void)
4350
4386
  nf_append_binding_child, 3);
4351
4387
  rb_define_module_function(m_native, "set_binding_attribute",
4352
4388
  nf_set_binding_attribute, 4);
4389
+ rb_define_module_function(m_native, "define_hot_readers",
4390
+ nf_define_hot_readers, 1);
4353
4391
  rb_define_module_function(m_native, "insert_binding_child",
4354
4392
  nf_insert_binding_child, 4);
4355
4393
  rb_define_module_function(m_native, "at_xpath_first",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.418.1"
4
+ VERSION = "1.9.420.0"
5
5
  end
Binary file
Binary file
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.418
4
+ - libleptris v1.9.420
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,31 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.420] - 2026-11-13
4
+ ### Fixed
5
+
6
+ - **xsd: the element fixed constraint compares in the value space** (leptris-rs#74, W3C stE065/066/096). Union-typed elements' final equality ran strcmp-level lexicals — the accepting member's whitespace semantics never applied. The token family and normalizedString now equate collapse-equal spellings (`fixed="abcd edfgh "` admits `abcd edfgh`, `fixed="ENU "` admits `ENU`), and LIST-typed elements itemize both lexicals on whitespace runs before per-item union comparison (a list of gYear/gMonth/gDay equates single- and double-space item separation). The collapse normalization is factored and shared with the attribute fixed path; preserve-types (xs:string) still compare exactly.
7
+
8
+ - **xsd: the §4.2.2 redefine hook splices the original attributeGroup** (the engine root of lutaml/moxml#545, W3C attgC035-038). An attributeGroup-redefine body that refs its OWN name means the ORIGINAL group — the name-keyed slot held the redefining body itself, so expanding it looped through the depth guard and the original's members never joined the composition (strict admission false-reports "not declared (strict)"). The rows capture threads the def being expanded; a self-ref resolves to the shadowed def. attgC038's chained double-redefine composes through the same path.
9
+
10
+ ## Tests
11
+
12
+ - stE065/066/096 + attgC035/036/037/038 vendored from w3c/xsdtests as pins; the union fixed change is falsified by a no-op-collapse mutation (fails exactly the value-space pin, passes the teeth pin — a genuinely different value still fails). Full ctest 2269/2269.
13
+
14
+
15
+
16
+ ## [1.9.419] - 2026-11-12
17
+ ### Fixed
18
+
19
+ - **xsd: wildcards ride attributeGroup refs across units** (leptris-rs#72, W3C wildZ013b/c). The #1964 deep capture left two holes: the extension branch still scanned shallow, so a wildcard living inside a REFERENCED attributeGroup under the extension element was invisible (strict admission rejected attributes the composed union admits); and a wildcard found in a group from ANOTHER unit (a namespace-less import) resolved against the CONSUMING schema's targetNamespace instead of its declaring unit's — `##other` flipped meaning across the boundary and the union collapsed to admitting only half the instance's attribute namespaces. The capture now follows refs in the extension branch and carries the declaring group's own unit tns (absent for the import).
20
+
21
+ - **xsd: a bare `<xs:anyAttribute />` is a live ##any wildcard** (leptris-rs#73, W3C attZ014a/b). Absent @namespace means ##any (the moxml#460 posture) — the capture mapped it to NULL, an invisible wildcard, so wildcard-admitted attributes fell to strict rejection for the wrong reason. With admission live, XSD 1.0's one-ID-per-element rule fires: an element assessed with more than one ID-typed attribute (declared rows AND wildcard-admitted names whose global declaration is ID-derived) is invalid; XSD 1.1 lifted the constraint and reads the same shape valid. The rule is version-gated (`xsd_schema_version_11`).
22
+
23
+ ## Tests
24
+
25
+ - wildZ013b/c + attZ014a/b fixtures vendored from w3c/xsdtests (msData/wildcards, msData/attribute); attZ014a also pinned under `@version="1.1"` (valid). Falsified: an always-false ID test failed exactly the two 1.0 pins and passed the 1.1 reading.
26
+
27
+
28
+
3
29
  ## [1.9.418] - 2026-11-12
4
30
  ## Fixed
5
31
 
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.418
6
+ VERSION 1.9.420
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -405,3 +405,109 @@ the loop-body surgery slice — it inherits the region-wrap probe
405
405
 
406
406
  RANKED CUT #1 therefore moves to the loop-surgery slice with the
407
407
  scan-merge as its first step. Nothing ships from the kernel alone.
408
+
409
+ ## Track D addendum 9 — the inline text-scan cut REJECTED by CI (PR #2018, closed)
410
+
411
+ The ranked-#1 cut (inline 32-byte scalar prefix scan for short text
412
+ runs, kernel for the remainder) went through the full gate: round 1
413
+ caught the restart-at-run-start flaw (the kernel rescanned the
414
+ scalar prefix — every leg regressed), round 2 fixed that (kernel
415
+ from the prefix end) and STILL regressed on all three legs vs the
416
+ A3d baselines (macos -39-46%, ubuntu-arm -26-27%, ubuntu-x86 -37-45%
417
+ vs its AVX2 run). Cause: the bench-class run lengths (33-64B) sit
418
+ exactly where the cut loses — 32 branchy scalar bytes plus a kernel
419
+ pass over a 1-32B remainder exceeds the kernel alone, and runs
420
+ <=32B are too few in these shapes to carry it.
421
+
422
+ THE MISSING DATUM: the run-length distribution of the real fixtures
423
+ and the scoreboard corpus. The next session's opening instrument =
424
+ a run-length histogram (one counting pass per fixture, reusable),
425
+ which decides whether the scan-merge has a paying window at all —
426
+ and at what threshold. Until then the kernel stands unchanged; the
427
+ PR closed with the full data (three legs, two rounds, CI artifacts).
428
+
429
+ ## Track D addendum 10 — the scan-merge slice CLOSED (PR #2020, closed unmerged)
430
+
431
+ The 64-byte window (histogram-driven) went through the full gate and
432
+ regressed WORSE than the 32-byte attempt: macos -45/-39/-46%, arm
433
+ -32/-32/-29%, x86 -53/-53/-50% (vs the AVX2 run) — even for runs
434
+ fully inside the window. THE VERDICT: the branchy scalar loop
435
+ (~3-5 ops/byte) loses to the SIMD kernel (~1 op/byte) at every run
436
+ length on every platform; the kernel's per-run fixed cost is small
437
+ next to its per-byte advantage. The scan-merge slice is structurally
438
+ dead — two windows, two full CI runs, one consistent verdict.
439
+
440
+ THE TEXT-ROW LEVER REDIRECTS to the kernel's own per-byte
441
+ throughput: the text_scan_run NEON kernel processes ~1 byte/op
442
+ while pugixml's scanner sustains 2.1-2.7 GB/s — the Track A scanner
443
+ class. The next concrete candidate: process 32B chunks (NEON
444
+ registers ×2) per iteration instead of 16B, halving the reduction
445
+ frequency; and the parser's text-branch call overhead already
446
+ measures separably (the scalar wrapper at 2.2%).
447
+
448
+ The histogram instrument (probe/run-histogram) remains in the repo
449
+ history on probe/run-histogram; both rejected cut branches carry
450
+ their full data.
451
+
452
+ ## Track D addendum 11 — the 32B kernel window measured-NEGATIVE (PR #2022, closed unmerged)
453
+
454
+ The addendum-10 candidate (NEON text_scan_run at 32B windows — two
455
+ uint8x16 registers, term masks OR-combined before ONE vmaxvq, halving
456
+ the horizontal-reduction frequency) passed the full CI suite but the
457
+ CI split was poisoned by fleet variance (x86 +31% on code the diff
458
+ does not touch). The decisive instrument was a same-machine
459
+ interleaved A/B on Apple silicon (Release+LTO, 3 rounds ABBA, pugixml
460
+ side flat ~2370/1800 MB/s across rounds — machine verified stable):
461
+
462
+ | shape | 16B (main) | 32B (PR) | delta |
463
+ |-------|-----------|----------|-------|
464
+ | 1.5MB | ~1341 MB/s | ~1144 MB/s | −15% |
465
+ | 5MB | ~1332 MB/s | ~1135 MB/s | −15% |
466
+ | 13MB | ~1029 MB/s | ~913 MB/s | −11% |
467
+
468
+ THE INVARIANT: horizontal-reduction frequency is NOT the binding term
469
+ in text_scan_run on M-series. The accumulator chain is — two extra
470
+ cmpeq+orr per register per window lengthen the dependency chain and
471
+ raise live-register pressure (9+ Q registers) without removing any
472
+ real per-byte work. Widening the window cannot pay; the kernel's
473
+ per-byte op count is already minimal for its contract (first-term
474
+ scan + f1/f2 seen-flags).
475
+
476
+ TEXT-ROW STATUS after three measured closures (scan-merge 32B,
477
+ scan-merge 64B, kernel-width 32B): the kernel is at its floor. The
478
+ remaining text-row gap to pugixml is the parser's text-branch call
479
+ overhead (the scalar wrapper measures separably at 2.2% — the only
480
+ untried lever in this class), not the kernel.
481
+
482
+ ## Track D addendum 12 — the text-branch call-overhead class CLOSED (measured tie + the bound-vs-run datum)
483
+
484
+ The named remainder from addendum 11 ("the parser's text-branch call
485
+ overhead, the scalar wrapper at 2.2%") went through the full
486
+ instrumentation pass and closes the class:
487
+
488
+ 1. **The microbench decomposition**: the dispatch entry costs 2-3x the
489
+ kernel's own work per call (7.1 vs 3.0 ns at 5B, 17.9 vs 6.9 ns at
490
+ 31B) — the fn-pointer defeats thin-LTO and forces the full call ABI
491
+ plus per-call key re-materialization. A devirtualized cut (arch-
492
+ direct calls + a <16B scalar fast path) was implemented, pinned,
493
+ and mutation-falsified — and measured a DOC-LEVEL TIE in interleaved
494
+ A/B (−0.7/−2.3/−1.4%, inside the noise band). The microbench's
495
+ variant-B "win" was LTO INLINING the local kernel into the bench
496
+ loop — a zero-call comparison the real direct-call cut cannot
497
+ replicate across TUs.
498
+
499
+ 2. **The bound-vs-run instrument datum** (the decisive one): the
500
+ parser's scan calls pass the REMAINING BUFFER as `len`
501
+ (`p.end - p.pos`), not the run length — so a length-gated fast path
502
+ never engages in the parser context (bucket census on the bench
503
+ text fixture: <16B = 0 of 2.1M calls, 64+ = 99.999%). The
504
+ run-length histogram that motivated the fast path describes the
505
+ RUN distribution, which the kernel only knows after scanning.
506
+
507
+ Disposition after four closures in the call-structure class
508
+ (scan-merge 32B/64B, kernel-width 32B, devirtualization): **the
509
+ text-branch call structure is at its floor.** The remaining text-row
510
+ gap to pugixml is the kernel's per-byte throughput itself — and the
511
+ accumulator chain that binds it was measured at addendum 11. Next
512
+ text-row work, if any, is a kernel redesign (Track A scanner class),
513
+ not call-structure rearrangement.
@@ -95,6 +95,10 @@ static XsdCm* xsd_cm_parse(struct leptris_xsd_schema* s,
95
95
  static XsdAttrDecl* xsd_capture_attr_rows(struct leptris_xsd_schema* s,
96
96
  LeptrisElement container,
97
97
  int depth);
98
+ static XsdAttrDecl* xsd_capture_attr_rows_ex(struct leptris_xsd_schema* s,
99
+ LeptrisElement container,
100
+ int depth,
101
+ const XsdAttrGroupDef* owner);
98
102
  XsdCm* xsd_find_complex_ns(struct leptris_xsd_schema* s,
99
103
  const char* type_name, const char* uri);
100
104
  extern XsdCm* xsd_find_complex(struct leptris_xsd_schema* s,
@@ -128,9 +132,9 @@ static char* xsd_capture_unit_tns(LeptrisElement e);
128
132
  static void xsd_resolve_qname(LeptrisElement e, const char* qname,
129
133
  char* uri, size_t urisz, char* local,
130
134
  size_t localsz);
131
- static char* xsd_capture_any_attr_ns(LeptrisElement container);
132
135
  static char* xsd_capture_any_attr_ns_deep(
133
- struct leptris_xsd_schema* s, LeptrisElement container, int depth);
136
+ struct leptris_xsd_schema* s, LeptrisElement container, int depth,
137
+ char** out_unit);
134
138
  static char* xsd_capture_any_attr_not_qname(LeptrisElement container);
135
139
 
136
140
  /* #1626: prefixed QName refs (type="w:CT_Border") resolve by local
@@ -3257,13 +3261,26 @@ static XsdCm* xsd_capture_complex_model(struct leptris_xsd_schema* s,
3257
3261
  /* XSD 1.1: asserts nested under the derivation */
3258
3262
  if (s->version_11)
3259
3263
  xsd_capture_assert_children(s, dq, root);
3260
- if (!any_attr_ns)
3264
+ if (!any_attr_ns) {
3261
3265
  if (!any_attr_unit_ns)
3262
3266
  any_attr_unit_ns = xsd_capture_unit_tns(dq);
3263
- any_attr_ns = xsd_capture_any_attr_ns(dq);
3267
+ /* leptris-rs#72: the extension's wildcard rides
3268
+ * attributeGroup refs too — the shallow scan
3269
+ * misses attgD019/wildZ013 shapes. Cross-unit
3270
+ * found-groups carry their own tns. */
3271
+ char* deep_unit = NULL;
3272
+ any_attr_ns = xsd_capture_any_attr_ns_deep(
3273
+ s, dq, 0, &deep_unit);
3274
+ if (any_attr_ns && deep_unit) {
3275
+ free(any_attr_unit_ns);
3276
+ any_attr_unit_ns = deep_unit;
3277
+ } else {
3278
+ free(deep_unit);
3279
+ }
3264
3280
  if (!any_attr_not_qname)
3265
3281
  any_attr_not_qname =
3266
3282
  xsd_capture_any_attr_not_qname(dq);
3283
+ }
3267
3284
  }
3268
3285
  } else if (strcmp(local, "simpleContent") == 0) {
3269
3286
  for (LeptrisNodeRef q = leptris_node_first_child(
@@ -3344,8 +3361,19 @@ static XsdCm* xsd_capture_complex_model(struct leptris_xsd_schema* s,
3344
3361
  if (!attrs) attrs = xsd_capture_attr_rows(s, ct, 0);
3345
3362
  if (!any_attr_unit_ns)
3346
3363
  any_attr_unit_ns = xsd_capture_unit_tns(ct);
3347
- if (!any_attr_ns)
3348
- any_attr_ns = xsd_capture_any_attr_ns_deep(s, ct, 0);
3364
+ if (!any_attr_ns) {
3365
+ char* deep_unit = NULL;
3366
+ any_attr_ns = xsd_capture_any_attr_ns_deep(s, ct, 0,
3367
+ &deep_unit);
3368
+ if (any_attr_ns && deep_unit) {
3369
+ /* the wildcard lives in a REFERENCED group from
3370
+ * another unit — its declaring tns wins */
3371
+ free(any_attr_unit_ns);
3372
+ any_attr_unit_ns = deep_unit;
3373
+ } else {
3374
+ free(deep_unit);
3375
+ }
3376
+ }
3349
3377
  if (!any_attr_not_qname)
3350
3378
  any_attr_not_qname = xsd_capture_any_attr_not_qname(ct);
3351
3379
  }
@@ -3455,7 +3483,9 @@ XsdGroupDef* xsd_find_group(struct leptris_xsd_schema* s,
3455
3483
  return NULL;
3456
3484
  }
3457
3485
 
3458
- static const XsdAttrDecl* xsd_find_top_attr(
3486
+ /* leptris-rs#73: the global attribute declaration for a local name
3487
+ * (the ID-count pass resolves wildcard-admitted names against it). */
3488
+ const XsdAttrDecl* xsd_find_top_attr(
3459
3489
  struct leptris_xsd_schema* s, const char* name) {
3460
3490
  if (!s || !name) return NULL;
3461
3491
  for (XsdAttrDecl* a = s->top_attrs; a; a = a->next)
@@ -3563,6 +3593,13 @@ static const char* xsd_inline_attr_type(
3563
3593
  static XsdAttrDecl* xsd_capture_attr_rows(struct leptris_xsd_schema* s,
3564
3594
  LeptrisElement container,
3565
3595
  int depth) {
3596
+ return xsd_capture_attr_rows_ex(s, container, depth, NULL);
3597
+ }
3598
+
3599
+ static XsdAttrDecl* xsd_capture_attr_rows_ex(struct leptris_xsd_schema* s,
3600
+ LeptrisElement container,
3601
+ int depth,
3602
+ const XsdAttrGroupDef* owner) {
3566
3603
  XsdAttrDecl* head = NULL;
3567
3604
  XsdAttrDecl* tail = NULL;
3568
3605
  if (depth > 8) return NULL;
@@ -3635,6 +3672,18 @@ static XsdAttrDecl* xsd_capture_attr_rows(struct leptris_xsd_schema* s,
3635
3672
  XsdAttrGroupDef* g = s->attr_groups;
3636
3673
  for (; g; g = g->next)
3637
3674
  if (strcmp(g->name, ref) == 0) break;
3675
+ /* XSD 1.0 §4.2.2 redefine hook (moxml#545, attgC035-038):
3676
+ * a body redefining group N refs N to mean the ORIGINAL
3677
+ * group. The name-keyed slot may hold the redefining
3678
+ * body itself — when expanding THAT def and the ref
3679
+ * names it again, take the shadowed def instead, or the
3680
+ * original's members never splice into the composition. */
3681
+ if (g && g == owner) {
3682
+ XsdAttrGroupDef* alt = g->next;
3683
+ for (; alt; alt = alt->next)
3684
+ if (strcmp(alt->name, g->name) == 0) break;
3685
+ g = alt;
3686
+ }
3638
3687
  if (!g) continue;
3639
3688
  /* leptris#1957: expand lazily through the def's own
3640
3689
  * element when its rows are not captured yet (a group
@@ -3643,7 +3692,8 @@ static XsdAttrDecl* xsd_capture_attr_rows(struct leptris_xsd_schema* s,
3643
3692
  * the cycle guard. */
3644
3693
  XsdAttrDecl* copied =
3645
3694
  g->attrs ? xsd_copy_attr_rows(g->attrs)
3646
- : xsd_capture_attr_rows(s, g->elem, depth + 1);
3695
+ : xsd_capture_attr_rows_ex(s, g->elem, depth + 1,
3696
+ g);
3647
3697
  if (!copied) continue;
3648
3698
  if (!head)
3649
3699
  head = copied;
@@ -3686,7 +3736,7 @@ static char* xsd_capture_unit_tns(LeptrisElement e) {
3686
3736
  * (depth-bounded); the group's OWN element is the walk root. */
3687
3737
  static char* xsd_capture_any_attr_ns_deep(
3688
3738
  struct leptris_xsd_schema* s, LeptrisElement container,
3689
- int depth) {
3739
+ int depth, char** out_unit) {
3690
3740
  if (depth > 8) return NULL;
3691
3741
  for (LeptrisNodeRef ac = leptris_node_first_child(
3692
3742
  leptris_element_as_node(container));
@@ -3697,9 +3747,14 @@ static char* xsd_capture_any_attr_ns_deep(
3697
3747
  const char* al = NULL, *au = NULL;
3698
3748
  leptris_element_expanded_name(ae, &al, NULL, &au);
3699
3749
  if (!al || (au && strcmp(au, XSD_NS) != 0)) continue;
3700
- if (strcmp(al, "anyAttribute") == 0)
3701
- return xsd_strdup(
3702
- leptris_element_attribute(ae, "namespace"));
3750
+ if (strcmp(al, "anyAttribute") == 0) {
3751
+ /* leptris-rs#73: absent @namespace means ##any — a
3752
+ * bare <xs:anyAttribute /> is a live wildcard, not an
3753
+ * invisible one. */
3754
+ const char* ns =
3755
+ leptris_element_attribute(ae, "namespace");
3756
+ return xsd_strdup(ns ? ns : "##any");
3757
+ }
3703
3758
  if (strcmp(al, "attributeGroup") == 0) {
3704
3759
  const char* ref = xsd_local_name(
3705
3760
  leptris_element_attribute(ae, "ref"));
@@ -3708,31 +3763,26 @@ static char* xsd_capture_any_attr_ns_deep(
3708
3763
  for (; g; g = g->next)
3709
3764
  if (strcmp(g->name, ref) == 0) break;
3710
3765
  if (g && g->elem) {
3711
- char* r = xsd_capture_any_attr_ns_deep(s, g->elem,
3712
- depth + 1);
3713
- if (r) return r;
3766
+ char* inner_unit = NULL;
3767
+ char* r = xsd_capture_any_attr_ns_deep(
3768
+ s, g->elem, depth + 1, &inner_unit);
3769
+ if (r) {
3770
+ /* leptris-rs#72: the wildcard resolves against
3771
+ * its DECLARING unit's tns — the group's own
3772
+ * unit ("" for a namespace-less import), which
3773
+ * may differ from the consuming type's. */
3774
+ if (out_unit)
3775
+ *out_unit = inner_unit
3776
+ ? inner_unit
3777
+ : xsd_capture_unit_tns(g->elem);
3778
+ return r;
3779
+ }
3714
3780
  }
3715
3781
  }
3716
3782
  }
3717
3783
  return NULL;
3718
3784
  }
3719
3785
 
3720
- static char* xsd_capture_any_attr_ns(LeptrisElement container) {
3721
- for (LeptrisNodeRef ac = leptris_node_first_child(
3722
- leptris_element_as_node(container));
3723
- ac; ac = leptris_node_next_sibling(ac)) {
3724
- if (leptris_node_get_type(ac) != LEPTRIS_NODE_TYPE_ELEMENT)
3725
- continue;
3726
- LeptrisElement ae = (LeptrisElement)ac;
3727
- const char* al = NULL;
3728
- leptris_element_expanded_name(ae, &al, NULL, NULL);
3729
- if (al && strcmp(al, "anyAttribute") == 0)
3730
- return xsd_strdup(
3731
- leptris_element_attribute(ae, "namespace"));
3732
- }
3733
- return NULL;
3734
- }
3735
-
3736
3786
  /* moxml#460: the anyAttribute's XSD 1.1 @notQName (same walk as the
3737
3787
  * namespace capture). NULL when absent. */
3738
3788
  static char* xsd_capture_any_attr_not_qname(LeptrisElement container) {