leptris 1.9.385.0-aarch64-linux → 1.9.386.0-aarch64-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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/lib/leptris/version.rb +1 -1
  4. data/lib/leptris/xml/document.rb +11 -1
  5. data/lib/leptris/xml/element.rb +15 -10
  6. data/lib/leptris/xml/ffi.rb +42 -0
  7. data/lib/leptris/xml/xsd.rb +35 -0
  8. data/lib/libleptris.so +0 -0
  9. data/vendor-src/README.md +1 -1
  10. data/vendor-src/libleptris/CHANGELOG.md +135 -0
  11. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  12. data/vendor-src/libleptris/src/include/leptris/descriptor.h +16 -0
  13. data/vendor-src/libleptris/src/include/leptris.h +77 -0
  14. data/vendor-src/libleptris/src/leptris/descriptor/plan.c +82 -0
  15. data/vendor-src/libleptris/src/leptris/dom/element_modify.c +23 -2
  16. data/vendor-src/libleptris/src/leptris/dom/element_query.c +201 -0
  17. data/vendor-src/libleptris/src/leptris/memory/pool.c +27 -0
  18. data/vendor-src/libleptris/src/leptris/memory/pool.h +5 -0
  19. data/vendor-src/libleptris/src/leptris/xpath/evaluator_axes.c +15 -0
  20. data/vendor-src/libleptris/src/leptris/xsd/compile.c +232 -15
  21. data/vendor-src/libleptris/src/leptris/xsd/datatypes.c +180 -46
  22. data/vendor-src/libleptris/src/leptris/xsd/validate.c +69 -22
  23. data/vendor-src/libleptris/src/leptris/xsd/xsd_internal.h +10 -0
  24. data/vendor-src/libleptris/test/abi/test_descriptor.cpp +201 -0
  25. data/vendor-src/libleptris/test/dom/test_document_children.cpp +256 -0
  26. data/vendor-src/libleptris/test/dom/test_dom.cpp +263 -0
  27. data/vendor-src/libleptris/test/xsd/libxml2-cases/schema_set/f.xsd +13 -0
  28. data/vendor-src/libleptris/test/xsd/libxml2-cases/schema_set/g.xsd +9 -0
  29. data/vendor-src/libleptris/test/xsd/test_xsd_compile.cpp +293 -1
  30. data/vendor-src/libleptris/vcpkg.json +1 -1
  31. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec5fdf2a05f5ac4a57c8eb9416858280544604297ffacb31eea1a0a747f3defd
4
- data.tar.gz: bbce628918ad5decb69b2627783d8a44420c2efc77b1cc23839227a20a7d2d9b
3
+ metadata.gz: f41b94e6e3cb66c05df53aba90d6787e978959bb3edd0cc84aae80b1260596c1
4
+ data.tar.gz: fabc7f7993104597b43a076afa2ef82ec35f687acb4bd1ed9aebb7056a1012e7
5
5
  SHA512:
6
- metadata.gz: 1ccdea9054f384ae5b76a92da03d0be7410fa8562fd21b9fc09e49afac715813d8a144d9e0f8cbc8c7ab980bb460956c4adf2bdcf96e160ffe0ced7ee56dfb3c
7
- data.tar.gz: 2deeeef79807f24d6e02d8ef146c2b91ed2391c59a0f798df1a9f6b40c9fab4bdb12d109c6ac519ed15b2f8d19743b31676994c9a9c49f2208eeff02ab52444f
6
+ metadata.gz: 6df54d9775a9e091e01f07f96eafcd4bba5f3ab65409130f800d382da4b15751f0c81d6ce4c6f757f5ac4dbdf2708c48bcd50902b75a389b67c1e2037fce3bac
7
+ data.tar.gz: a452adaba63a6e1a2dd5a5bafbb608713ed58fec945b44739d5e2516db96ba9738a884d3c831d2835473548f768caabeadb53a90edb91545fe7541a727d51072
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.384"
11
+ LIBLEPTRIS_VERSION = "1.9.387"
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.385.0"
4
+ VERSION = "1.9.386.0"
5
5
  end
@@ -692,7 +692,17 @@ class Leptris::XML::Document
692
692
  def absorbed?
693
693
  !@absorbed_into.nil?
694
694
  end
695
- private :absorbed?
695
+
696
+ # leptris#1866: cross-document adoption lifetime anchor — the
697
+ # target document keeps the source alive so no wrapper/cache can
698
+ # outlive the source arena (standoc svg cleanup crash).
699
+ def retain_source_document!(source)
700
+ return unless source.is_a?(Leptris::XML::Document)
701
+ return if source.equal?(self)
702
+ (@retained_sources ||= []) << source
703
+ source
704
+ end
705
+
696
706
  def free
697
707
  return if @freed.state == :freed
698
708
  @freed.state = :freed
@@ -494,16 +494,19 @@ class Leptris::XML::Element < Leptris::XML::Node
494
494
  prefix = key.start_with?("xmlns:") ? key.delete_prefix("xmlns:") : nil
495
495
  node.add_namespace_definition(prefix, uri)
496
496
  end
497
- # libxml2 reparent parity (moxml #208 lineage): an OWN
498
- # declaration the attach target already resolves identically
499
- # is redundant — prune it so serialized output does not
500
- # re-declare (libxml2 drops these on reparent). Shadowing
501
- # declarations (same prefix, different URI) are preserved.
502
- node.namespace_definitions.each do |ns|
503
- key = ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns"
504
- next unless target_scope[key] == ns.href
505
- node.remove_namespace_definition(ns.prefix)
506
- end
497
+ # leptris#1866: the own-declaration PRUNE that used to live here
498
+ # (libxml2 reparent parity, moxml #208 lineage) is REMOVED — it
499
+ # is unsound on this engine. The engine's cross-document deep
500
+ # copy resolves each copied element's namespace through the
501
+ # SOURCE's in-scope declarations at copy time: pruning an own
502
+ # declaration made every descendant copy lose its namespace URI
503
+ # (adopted svgs fell out of the SVG namespace; classupdate's
504
+ # namespaced queries then matched nothing and the attribute
505
+ # write path corrupted a sibling attr name — <use =""> and the
506
+ # standoc svg-cleanup segfaults). libxml2 gets away with the
507
+ # prune because its nodes keep per-node URIs on reparent; the
508
+ # redundant xmlns in the serialized copy is the correct output
509
+ # here.
507
510
  nil
508
511
  end
509
512
 
@@ -559,6 +562,7 @@ class Leptris::XML::Element < Leptris::XML::Node
559
562
  Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces_for_sibling)
560
563
  end
561
564
  @document&.advance_version
565
+ @document&.retain_source_document!(node.document)
562
566
  Leptris::XML::FFI.check_status(
563
567
  Leptris::XML::FFI.leptris_element_insert_after(c_ptr, node.c_ptr))
564
568
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
@@ -595,6 +599,7 @@ class Leptris::XML::Element < Leptris::XML::Node
595
599
  Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces_for_sibling)
596
600
  end
597
601
  @document&.advance_version
602
+ @document&.retain_source_document!(node.document)
598
603
  Leptris::XML::FFI.check_status(
599
604
  Leptris::XML::FFI.leptris_element_insert_before(c_ptr, node.c_ptr))
600
605
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
@@ -442,6 +442,16 @@ module Leptris
442
442
  PLAN_OP_END = 3
443
443
  attach_function :leptris_plan_result_build,
444
444
  [:leptris_plan, :pointer, :size_t, :pointer], :leptris_plan_result
445
+ # libleptris 1.9.387: the packed-blob lane of the same face
446
+ # (five uint32 per op plus name/value bytes). SOFT attach —
447
+ # engines before 1.9.387 lack the symbol.
448
+ begin
449
+ attach_function :leptris_plan_result_build_packed,
450
+ [:leptris_plan, :pointer, :size_t, :pointer],
451
+ :leptris_plan_result
452
+ rescue ::FFI::NotFoundError
453
+ nil
454
+ end
445
455
 
446
456
  # libleptris 1.9.312 (#1551): serialize a walk result back
447
457
  # to XML guided by its plan — the plan supplies element
@@ -750,6 +760,26 @@ attach_function :leptris_parse_string,
750
760
  [:leptris_element, :pointer, :pointer, :size_t], :size_t
751
761
  attach_function :leptris_element_children,
752
762
  [:leptris_element, :pointer, :size_t], :size_t
763
+ # libleptris 1.9.387 engine faces not yet wrapped in Ruby —
764
+ # attached so the export audit stays in lockstep; adoption
765
+ # wraps them (leptris#1855 batch children: (addr, kind) pairs,
766
+ # document order only when cap >= count; #1874 namespace
767
+ # snapshot: one crossing per subtree, rows are seven uint32
768
+ # (depth, name_count, names_off, prefix_off, prefix_len,
769
+ # href_off, href_len) into a caller blob, 0xFFFFFFFF prefix_off
770
+ # = default-ns declaration; #1855 ask-2 name compare: strcmp
771
+ # semantics, non-elements compare as the empty string).
772
+ begin
773
+ attach_function :leptris_element_children_batch,
774
+ [:pointer, :pointer, :pointer, :size_t], :size_t
775
+ attach_function :leptris_node_name_compare,
776
+ [:pointer, :string, :size_t], :int
777
+ attach_function :leptris_element_namespace_declarations_snapshot,
778
+ [:leptris_element, :pointer, :size_t, :pointer,
779
+ :pointer, :size_t, :pointer], :size_t
780
+ rescue ::FFI::NotFoundError
781
+ nil
782
+ end
753
783
  attach_function :leptris_node_as_element,
754
784
  [:leptris_node_ref], :leptris_element
755
785
  attach_function :leptris_element_as_node,
@@ -1391,6 +1421,18 @@ attach_function :leptris_parse_string,
1391
1421
  # handles, exactly like the string face.
1392
1422
  attach_function :leptris_xsd_compile_file,
1393
1423
  [:string, :pointer], :leptris_xsd_schema
1424
+ # libleptris 1.9.387 (leptris#1881): compile a SET of schema
1425
+ # documents (NULL-terminated char* array) into one schema —
1426
+ # imports resolve across the whole set, so substitution-group
1427
+ # members declared in importing documents are visible to the
1428
+ # head's content model. SOFT attach: engines before 1.9.387
1429
+ # lack the symbol.
1430
+ begin
1431
+ attach_function :leptris_xsd_compile_set,
1432
+ [:pointer, :size_t, :pointer], :leptris_xsd_schema
1433
+ rescue ::FFI::NotFoundError
1434
+ nil
1435
+ end
1394
1436
  attach_function :leptris_xsd_free,
1395
1437
  [:leptris_xsd_schema], :void
1396
1438
  attach_function :leptris_xsd_declaration_count,
@@ -16,6 +16,12 @@ module Leptris::XML::XSD
16
16
  Schema.compile_file(path)
17
17
  end
18
18
 
19
+ # Compile a set of schema documents (paths) into one schema
20
+ # (libleptris >= 1.9.387): imports resolve across the whole set.
21
+ def compile_set(paths)
22
+ Schema.compile_set(paths)
23
+ end
24
+
19
25
  # Slice 2 (libleptris 1.9.319, #1075): built-in lexical table.
20
26
  # +builtin+ takes the "xs:NAME" reference spelling. Returns
21
27
  # true/false; an unknown table name raises — a silent false
@@ -101,6 +107,35 @@ module Leptris::XML::XSD
101
107
  new(Handle.new(raw))
102
108
  end
103
109
 
110
+ # Compile a SET of schema documents (paths) into one schema:
111
+ # imports resolve across the whole set, so components declared
112
+ # in importing documents (substitution-group members, derived
113
+ # types) are visible to the head's content models — the
114
+ # one-document compile cannot see them (libleptris 1.9.387,
115
+ # leptris#1881). Requires the leptris_xsd_compile_set face.
116
+ def self.compile_set(paths)
117
+ paths = paths.map { |p| p.to_s }
118
+ raise ArgumentError, "schema set needs at least one path" if paths.empty?
119
+ unless Leptris::XML::FFI.respond_to?(:leptris_xsd_compile_set)
120
+ raise Leptris::XML::Error,
121
+ "Schema.compile_set requires libleptris 1.9.387+ " \
122
+ "(the leptris_xsd_compile_set face)"
123
+ end
124
+
125
+ strings = paths.map { |p| ::FFI::MemoryPointer.from_string(p) }
126
+ array = ::FFI::MemoryPointer.new(:pointer, strings.size)
127
+ array.write_array_of_pointer(strings)
128
+ status = ::FFI::MemoryPointer.new(:int)
129
+ raw = Leptris::XML::FFI.leptris_xsd_compile_set(array, strings.size,
130
+ status)
131
+ if raw.null?
132
+ raise Leptris::XML::Error,
133
+ "schema set compile failed (status #{status.read_int}): " \
134
+ "#{paths.join(', ')}"
135
+ end
136
+ new(Handle.new(raw))
137
+ end
138
+
104
139
  # Number of top-level schema declarations the compiler
105
140
  # recognized (elements, types, groups, ...).
106
141
  def declaration_count
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.384
4
+ - libleptris v1.9.387
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,140 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.387] - 2026-10-28
4
+
5
+ ### Added
6
+
7
+ - **XSD: schema-set compilation — `leptris_xsd_compile_set(paths, count,
8
+ status)` (leptris#1881).** A head document cannot see
9
+ substitution-group members declared in schemas that IMPORT it —
10
+ nothing loads those from the head alone, so the head's content model
11
+ could never match the member. The set face merges every listed
12
+ document into ONE schema: per-document checks and capture with
13
+ per-unit target namespaces, a shared visited-set so each unit
14
+ captures exactly once in either member order, and extension bases
15
+ resolving once after all units are in. The deferred extension-base
16
+ fold now records the base QName's RESOLVED namespace
17
+ (`ext_base_ns`, prefix resolved against the extension element's
18
+ in-scope declarations) — set compilation extends ACROSS units
19
+ (g:foo1 extends f:foo), which the old same-ns fallback silently
20
+ dropped into an empty model.
21
+
22
+ ### Fixed
23
+
24
+ - **XSD: `processContents="skip"` wildcards impose no constraint on
25
+ matched children (leptris#1879).** XSD 1.0 §3.9.4.2 — the engine
26
+ found the child's global declaration and validated its lexical type
27
+ anyway (`<a>1</a>` under a skip wildcard rejected `1` as a bad
28
+ xs:ID; the W3C suite addB130/addB140 expect it valid).
29
+ `XsdCm` records the processContents level (strict/lax/skip) at both
30
+ wildcard capture sites; validate_children computes the strongest
31
+ level among the wildcards admitting each child (the attribute
32
+ wildcard's namespace-grammar matcher, unit-tns aware) and skips
33
+ skip-admitted children entirely. Lax keeps validating declared
34
+ children; wildcard-admitted undeclared names pass under every
35
+ level (the pinned moxml#460 notQName semantics).
36
+
37
+ - **XSD: attributeGroup refs resolve regardless of declaration order
38
+ (leptris#1880).** Top-level declarations form one symbol space, but
39
+ the ref expansion looked up `s->attr_groups` at capture time — a
40
+ group declared AFTER the referencing type was silently dropped
41
+ (W3C suite addB096). Top-level `xs:attributeGroup` definitions are
42
+ now captured in a pre-pass at the head of `xsd_capture_root`;
43
+ the main walk skips already-captured names instead of shadowing.
44
+
45
+ - **XSD: length facets measure the VALUE SPACE (leptris#1882).** On
46
+ list types the length family counts whitespace-separated ITEMS, not
47
+ characters (`xs:IDREFS` + `length=2` accepts `a b`, was rejected as
48
+ 3 characters; the minLength/maxLength NMTOKENS rows flip likewise).
49
+ The list detection walks the base chain — a restriction OF a list
50
+ keeps `deriv=RESTRICT`, so `xsd_chain_is_list` resolves through
51
+ local bases and threads a flag into the facet gate. And
52
+ `xs:QName`/`xs:NOTATION` have no length family at all (Part 2
53
+ §4.3.1–4.3.3 applicability): the gate skips those comparisons
54
+ entirely.
55
+
56
+ - **XSD: the builtin lexical table converges on Datatypes Part 2
57
+ (leptris#1883)** — thirteen rows disagreed in both directions.
58
+ Over-strict, now valid: `xs:token` accepts any whitespace shape
59
+ (collapse is total); `xs:ID` collapses before the NCName check; the
60
+ unbounded integer family accepts arbitrary precision with the sign
61
+ class intact; `xs:float`/`xs:double` accept `+INF` (float018).
62
+ Too lenient, now invalid: `xs:long` rejects 2^63 (strtoll ERANGE)
63
+ and `xs:unsignedLong` string-compares against 2^64−1; `xs:date`
64
+ applies calendar-aware day bounds with leap years; `xs:time` bounds
65
+ hour 0–23 (`24:00:00` exact), minute 0–59, second 0–60 with 60
66
+ reserved for `23:59:60`; `xs:duration` rejects a bare `T` (P1Y2MT)
67
+ and fractions on anything but seconds (P200.5Y); `xs:base64Binary`
68
+ rejects a one-byte remainder (`abcde`) and data after padding;
69
+ `xs:QName` rejects the reserved `xmlns` prefix.
70
+
71
+ - **DOM: bulk namespace-declaration snapshot — one crossing per
72
+ subtree (leptris#1874).**
73
+ `leptris_element_namespace_declarations_snapshot(elem, ...)` walks
74
+ the subtree in pre-order once and returns one row per (element
75
+ carrying declarations, declaration) with path depth, ancestor name
76
+ chain, and NUL-terminated prefix/URI lanes — the same rows the
77
+ binding's collect_native_namespaces produces today with one
78
+ `namespace_definitions` FFI round trip per node plus a
79
+ whole-subtree Ruby recursion. attributes_snapshot retry discipline;
80
+ default-ns and xmlns="" undeclaration markers included.
81
+
82
+ - **DOM: address-Integer batch faces (leptris#1855).**
83
+ `leptris_element_children_batch(elem_addr, ...)` returns every
84
+ child's address + node kind in document order from ONE crossing per
85
+ parent (the binding's plan-path hydration measured ~35% FFI
86
+ materialization; `bulk_children` was 12.4% of self-time) — NODE-level
87
+ walk, all five child kinds ride the batch.
88
+ `leptris_node_name_compare(node_addr, name, len)` compares names
89
+ inside the engine with strcmp semantics — the `fast_name` band (4.5%)
90
+ was Ruby constructing Strings that mostly feed `==`.
91
+
92
+ - **XSD: test pin — a type-less element declaration defaults to
93
+ xs:anyType (leptris#1878).** XSD 1.0 §3.3.2: any attributes, lax
94
+ content, never an undeclared-element error. The moxml 0.5.153 repro
95
+ fails on the bundled engine 1.9.381.0 and passes since e5762cdc;
96
+ the moxml Gemfile already allows `~> 1.9.351` (a `bundle update
97
+ leptris` picks the fixed engine).
98
+
99
+
100
+
101
+ ## [1.9.386] - 2026-10-28
102
+
103
+ ### Fixed
104
+
105
+ - **DOM: cross-document subtree adoption no longer corrupts attribute
106
+ chains when the destination arena spills into extension blocks
107
+ (leptris#1866).** `copy_subtree_detached` wired the first-attribute
108
+ edge with a truncating `int32_t` cast; once the sized arena was
109
+ exhausted, copy allocations landed in tracked malloc blocks that on
110
+ 64-bit hosts sit farther from the arena span than int32 can express
111
+ (~105 TB on macOS). The truncated delta decoded to a wild pointer —
112
+ blank attribute names (`<use ="">`) and a serializer SEGV in
113
+ mutation-heavy SVG-rewrite loops. The edge now goes through
114
+ `leptris_elem_set_first_attribute`, which spills out-of-range deltas
115
+ to the compact overflow table, and `leptris_pool_contains`
116
+ recognizes extension blocks as pool-owned.
117
+
118
+ - **XSD: a local element particle with its own type shadows a
119
+ same-named global element declaration (leptris#1871).** Per Element
120
+ Locally Valid (Complex Type), XSD 1.0 §3.4.4, the matched
121
+ particle's declaration governs its instance element — an inline
122
+ anonymous (or named `type=`) local type was previously assessed as
123
+ the global declaration's type, rejecting valid instances and
124
+ accepting invalid ones. Resolution order is particle-type-first
125
+ (with `xsi:type` substitution applied there); `ref` particles,
126
+ which carry no type, keep the global lookup unchanged.
127
+
128
+
129
+
130
+ ## [1.9.385] - 2026-10-27
131
+
132
+ ### Fixed
133
+
134
+ - the cross-doc adopt keeps its attribute chain; @id/.. names the owner
135
+
136
+
137
+
3
138
  ## [1.9.384] - 2026-10-27
4
139
 
5
140
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.384
6
+ VERSION 1.9.387
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -293,6 +293,22 @@ LEPTRIS_API LeptrisPlanResult leptris_plan_result_build(
293
293
  LeptrisPlan plan, const leptris_plan_row_op* ops, size_t count,
294
294
  LeptrisStatus* status);
295
295
 
296
+ /* leptris#1854: build a result from a PACKED op blob — the whole
297
+ * emission crosses in one call and no per-op host struct writes
298
+ * (the #408 address-Integer law applied to the build side; the
299
+ * binding's emission loop was 37% FFI::Pointer#+). Same program
300
+ * semantics as leptris_plan_result_build. Blob layout, per op:
301
+ * five native-byte-order uint32_t (kind, plan_index, row_index,
302
+ * name_len, value_len), then name_len name bytes PLUS a NUL
303
+ * terminator (the ATTR path dup's names NUL-terminated), then
304
+ * value_len raw value bytes; a zero length omits the bytes and
305
+ * reads as NULL. Bounds-checked: a truncated blob fails
306
+ * INVALID_ARG. Strings are copied; the blob is only read during
307
+ * the call. Memory: free with leptris_plan_result_free. */
308
+ LEPTRIS_API LeptrisPlanResult leptris_plan_result_build_packed(
309
+ LeptrisPlan plan, const void* blob, size_t blob_len,
310
+ LeptrisStatus* status);
311
+
296
312
  /* #1551: serialize a walk result back to XML, guided by its plan.
297
313
  * The plan supplies element wrappers (row wire_name + ns_prefix)
298
314
  * and the result supplies content; children emit in document order
@@ -913,6 +913,21 @@ LEPTRIS_API void leptris_rng_free(LeptrisRelaxNG rng);
913
913
  LEPTRIS_API LeptrisXsdSchema leptris_xsd_compile_file(const char* path,
914
914
  LeptrisStatus* status);
915
915
 
916
+ /* leptris#1881: compile a schema SET — every listed document's
917
+ * declarations merge into ONE schema, in order, with the shared
918
+ * visited-set closing include/import cycles across members. A head
919
+ * document cannot see substitution-group members declared in
920
+ * schemas that import it (nothing loads those from the head alone);
921
+ * a set compilation does. Per-unit target namespaces are honored
922
+ * during each document's capture; the FIRST document's
923
+ * targetNamespace remains the schema's. Relative schemaLocations
924
+ * resolve against each document's own directory. NULL paths or
925
+ * count 0 return NULL; an unreadable or malformed member yields an
926
+ * error-carrying handle like leptris_xsd_compile_file.
927
+ * (Memory: free with leptris_xsd_free.) */
928
+ LEPTRIS_API LeptrisXsdSchema leptris_xsd_compile_set(
929
+ const char* const* paths, size_t count, LeptrisStatus* status);
930
+
916
931
  LEPTRIS_API LeptrisXsdSchema leptris_xsd_compile(const char* xsd_text,
917
932
  size_t len,
918
933
  LeptrisStatus* status);
@@ -2019,6 +2034,68 @@ LEPTRIS_API size_t leptris_element_attributes_snapshot(
2019
2034
  char* ns_prefixes, size_t prefixes_cap, size_t* prefix_offsets,
2020
2035
  char* ns_uris, size_t uris_cap, size_t* uri_offsets);
2021
2036
 
2037
+ /* leptris#1855 (ask 1): batched child materialization under the
2038
+ * #408 address-Integer law — one crossing per PARENT returns every
2039
+ * child's address and node kind, replacing the binding's per-child
2040
+ * FFI walk. Returns the element's child count; the (addr, kind)
2041
+ * pairs are written in document order only when cap >= count —
2042
+ * otherwise nothing is written and the count comes back so the
2043
+ * caller sizes and retries. A 0 address reads as "no children". */
2044
+ LEPTRIS_API size_t leptris_element_children_batch(
2045
+ uintptr_t elem_addr,
2046
+ uintptr_t* child_addrs, uint8_t* child_kinds, size_t cap);
2047
+
2048
+ /* leptris#1855 (ask 2): engine-side name comparison — strcmp
2049
+ * semantics (-1/0/1) between the node's name and the caller's
2050
+ * (name, len) lens, with no String constructed on either side.
2051
+ * Element nodes compare their name; non-element nodes compare as
2052
+ * the empty string. A 0 address is an error lane: -2. */
2053
+ LEPTRIS_API int leptris_node_name_compare(uintptr_t node_addr,
2054
+ const char* name, size_t len);
2055
+
2056
+ /* leptris#1874: bulk namespace-declaration snapshot — the whole
2057
+ * subtree's declared namespaces in ONE crossing (the binding's
2058
+ * collect_native_namespaces walk paid one namespace_definitions FFI
2059
+ * round trip per node). Pre-order DFS under `elem`; one row per
2060
+ * (element carrying declarations, declaration) in document order,
2061
+ * exactly the rows the binding's per-node namespace_definitions
2062
+ * union produces.
2063
+ *
2064
+ * Each row carries the element's path depth (0 = `elem` itself, 1 =
2065
+ * a child, ...) and blob offsets: the path names (depth NUL-
2066
+ * terminated components in ancestor-first order, `names_off`), the
2067
+ * prefix (`prefix_off`/`prefix_len`, NUL-terminated in the blob;
2068
+ * 0xFFFFFFFFu off = a default-namespace declaration, no prefix),
2069
+ * and the URI (`href_off`/`href_len`, NUL-terminated in the blob;
2070
+ * len 0 = an xmlns="" undeclaration). Legacy xmlns:* attributes
2071
+ * added through the attribute API are included (same union as
2072
+ * leptris_element_namespace_count).
2073
+ *
2074
+ * Contract (the attributes_snapshot discipline): with `rows == NULL`
2075
+ * returns 0 and writes the required row count to *out_row_count and
2076
+ * the required blob bytes to *blob_used (either pointer may be
2077
+ * NULL); with non-NULL buffers smaller than required returns
2078
+ * (size_t)-1 and writes nothing; on success returns 0 and writes
2079
+ * the used blob bytes to *blob_used when non-NULL.
2080
+ *
2081
+ * Memory: the snapshot copies strings into the caller's blob;
2082
+ * nothing borrows the document.
2083
+ */
2084
+ typedef struct {
2085
+ uint32_t depth; /* 0 = elem itself, 1 = child, ... */
2086
+ uint32_t name_count; /* == depth */
2087
+ uint32_t names_off; /* blob offset of the first path name */
2088
+ uint32_t prefix_off; /* 0xFFFFFFFFu = default-ns declaration */
2089
+ uint32_t prefix_len;
2090
+ uint32_t href_off;
2091
+ uint32_t href_len;
2092
+ } leptris_ns_decl_row;
2093
+
2094
+ LEPTRIS_API size_t leptris_element_namespace_declarations_snapshot(
2095
+ LeptrisElement elem,
2096
+ leptris_ns_decl_row* rows, size_t rows_cap, size_t* out_row_count,
2097
+ char* blob, size_t blob_cap, size_t* blob_used);
2098
+
2022
2099
  /**
2023
2100
  * Get name of attribute by index
2024
2101
  *
@@ -2126,6 +2126,88 @@ LEPTRIS_API LeptrisPlanResult leptris_plan_result_build(
2126
2126
  return (LeptrisPlanResult)r;
2127
2127
  }
2128
2128
 
2129
+ /* leptris#1854: decode the packed blob into borrowed op rows
2130
+ * (name/value point INTO the blob — the windowed builder dup's
2131
+ * every string) and hand the IDENTICAL op stream to the IDENTICAL
2132
+ * builder, so byte parity with leptris_plan_result_build holds by
2133
+ * construction. */
2134
+ LEPTRIS_API LeptrisPlanResult leptris_plan_result_build_packed(
2135
+ LeptrisPlan plan, const void* blob, size_t blob_len,
2136
+ LeptrisStatus* status) {
2137
+ if (status) *status = LEPTRIS_OK;
2138
+ if (!plan || !blob || blob_len < 5 * sizeof(uint32_t)) {
2139
+ if (status) *status = LEPTRIS_ERROR_INVALID_ARG;
2140
+ return NULL;
2141
+ }
2142
+ /* Two passes: count ops to size the row array, then decode with
2143
+ * a bounds check on every field. */
2144
+ const uint8_t* p = (const uint8_t*)blob;
2145
+ const uint8_t* const end = p + blob_len;
2146
+ size_t op_count = 0;
2147
+ for (const uint8_t* q = p; q < end;) {
2148
+ if ((size_t)(end - q) < 5 * sizeof(uint32_t)) {
2149
+ if (status) *status = LEPTRIS_ERROR_INVALID_ARG;
2150
+ return NULL;
2151
+ }
2152
+ uint32_t nl, vl;
2153
+ memcpy(&nl, q + 12, sizeof(nl));
2154
+ memcpy(&vl, q + 16, sizeof(vl));
2155
+ q += 5 * sizeof(uint32_t);
2156
+ /* name bytes + their NUL (none when nl == 0) + value bytes
2157
+ * <= remaining: size_t math, no overflow (lens < 4G) */
2158
+ size_t name_span = nl ? (size_t)nl + 1 : 0;
2159
+ if ((size_t)(end - q) < name_span + (size_t)vl) {
2160
+ if (status) *status = LEPTRIS_ERROR_INVALID_ARG;
2161
+ return NULL;
2162
+ }
2163
+ q += name_span + (size_t)vl;
2164
+ op_count++;
2165
+ }
2166
+ leptris_plan_row_op* ops =
2167
+ (leptris_plan_row_op*)malloc(op_count * sizeof(*ops));
2168
+ if (!ops) {
2169
+ if (status) *status = LEPTRIS_ERROR_MEMORY;
2170
+ return NULL;
2171
+ }
2172
+ const uint8_t* q = p;
2173
+ for (size_t i = 0; i < op_count; i++) {
2174
+ uint32_t kind, pi, ri, nl, vl;
2175
+ memcpy(&kind, q, sizeof(kind));
2176
+ memcpy(&pi, q + 4, sizeof(pi));
2177
+ memcpy(&ri, q + 8, sizeof(ri));
2178
+ memcpy(&nl, q + 12, sizeof(nl));
2179
+ memcpy(&vl, q + 16, sizeof(vl));
2180
+ q += 5 * sizeof(uint32_t);
2181
+ ops[i].kind = (LeptrisPlanRowOpKind)kind;
2182
+ ops[i].plan_index = pi;
2183
+ ops[i].row_index = ri;
2184
+ ops[i].name = nl ? (const char*)q : NULL; /* NUL at q[nl] */
2185
+ q += nl ? (size_t)nl + 1 : 0;
2186
+ ops[i].value = vl ? (const char*)q : NULL;
2187
+ ops[i].value_len = vl;
2188
+ q += vl;
2189
+ }
2190
+ /* Same arena/window discipline as the struct face (#1677). */
2191
+ struct dp_arena* arena = dp_arena_new(DP_ARENA_CHUNK);
2192
+ if (!arena) {
2193
+ free(ops);
2194
+ if (status) *status = LEPTRIS_ERROR_MEMORY;
2195
+ return NULL;
2196
+ }
2197
+ struct dp_arena* saved_arena = dp_cur_arena;
2198
+ dp_cur_arena = arena;
2199
+ struct leptris_plan_result* r =
2200
+ plan_result_build_windowed(plan, ops, op_count, status, arena);
2201
+ dp_cur_arena = saved_arena;
2202
+ free(ops);
2203
+ if (!r) {
2204
+ dp_arena_free_all(arena);
2205
+ return NULL;
2206
+ }
2207
+ if (r->arena == arena) r->arena_owner = 1;
2208
+ return (LeptrisPlanResult)r;
2209
+ }
2210
+
2129
2211
  LEPTRIS_API void leptris_plan_result_free(LeptrisPlanResult result) {
2130
2212
  dp_result_free_rec((struct leptris_plan_result*)result);
2131
2213
  }
@@ -2475,6 +2475,16 @@ static LeptrisElement copy_subtree_detached(LeptrisElement source,
2475
2475
  v ? leptris_sv_from_ptr(v, sav_.length)
2476
2476
  : leptris_sv_from_cstr("");
2477
2477
  leptris_element_add_attribute(copy, nv, vv, pool);
2478
+ /* leptris#1866: add_attribute appends via the element's
2479
+ * cached last_attribute — the LOCAL tail below is now
2480
+ * stale. A following direct placement linked onto the
2481
+ * stale tail and orphaned every attribute between the
2482
+ * two wirings (a `<use xlink:href id>` copy lost the id:
2483
+ * invisible to the @-axis XPath, dropped at serialize,
2484
+ * and the attr_count-vs-chain mismatch fed the O(1)
2485
+ * index an empty name — `<use ="">`). Resync from the
2486
+ * element's cached tail, which add_attribute maintains. */
2487
+ last_attr = leptris_elem_last_attribute(copy);
2478
2488
  continue;
2479
2489
  }
2480
2490
  /* Direct placement: the source attr is already decoded and
@@ -2505,10 +2515,21 @@ static LeptrisElement copy_subtree_detached(LeptrisElement source,
2505
2515
  if (copy->first_attribute_off != 0) {
2506
2516
  leptris_attr_set_next(last_attr, attr);
2507
2517
  } else {
2508
- copy->first_attribute_off =
2509
- (int32_t)((char*)attr - (char*)copy);
2518
+ /* The destination pool may hand this allocation from an
2519
+ * arena EXTENSION block — malloc'd far from the arena
2520
+ * span (leptris#1866: macOS puts it ~105 TB away, beyond
2521
+ * int32). The raw cast truncated that delta and decode
2522
+ * produced a wild pointer (blank names, serializer SEGV);
2523
+ * the setter spills out-of-range deltas to the overflow
2524
+ * table. */
2525
+ leptris_elem_set_first_attribute(copy, attr);
2510
2526
  }
2511
2527
  last_attr = attr;
2528
+ /* leptris#1866: keep the element's cached tail in step with
2529
+ * the local chain tail — a following add_attribute appends
2530
+ * via the cached tail, and a stale one orphans everything
2531
+ * wired since. */
2532
+ leptris_elem_set_last_attribute(copy, attr);
2512
2533
  /* add_attribute's increment — attr_count is what the count
2513
2534
  * accessor and the O(1) index fast path read (uint8 wrap
2514
2535
  * at 255 latches the walk, same convention). */