leptris 1.9.391.0-arm-linux → 1.9.396.0-arm-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +9 -3
  3. data/ext/leptris/native/native.c +30 -0
  4. data/lib/leptris/version.rb +1 -1
  5. data/lib/leptris/xml/ffi.rb +5 -0
  6. data/lib/leptris/xml/native.so +0 -0
  7. data/lib/leptris/xml/native_3_3.so +0 -0
  8. data/lib/leptris/xml.rb +25 -0
  9. data/lib/libleptris.so +0 -0
  10. data/vendor-src/README.md +1 -1
  11. data/vendor-src/libleptris/CHANGELOG.md +146 -0
  12. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  13. data/vendor-src/libleptris/README.adoc +37 -0
  14. data/vendor-src/libleptris/src/CMakeLists.txt +12 -5
  15. data/vendor-src/libleptris/src/include/leptris/types.h +52 -0
  16. data/vendor-src/libleptris/src/include/leptris.h +6 -0
  17. data/vendor-src/libleptris/src/leptris/common/simd_text.c +61 -0
  18. data/vendor-src/libleptris/src/leptris/common/simd_text.h +13 -0
  19. data/vendor-src/libleptris/src/leptris/common/simd_text_avx2.c +156 -2
  20. data/vendor-src/libleptris/src/leptris/common/simd_text_neon.c +113 -0
  21. data/vendor-src/libleptris/src/leptris/core.c +31 -0
  22. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +19 -43
  23. data/vendor-src/libleptris/src/leptris/xpath/parser.c +11 -29
  24. data/vendor-src/libleptris/src/leptris/xsd/compile.c +82 -8
  25. data/vendor-src/libleptris/src/leptris/xsd/validate.c +36 -4
  26. data/vendor-src/libleptris/src/leptris/xsd/xsd_internal.h +8 -0
  27. data/vendor-src/libleptris/test/CMakeLists.txt +18 -0
  28. data/vendor-src/libleptris/test/abi/test_capabilities.cpp +112 -0
  29. data/vendor-src/libleptris/test/dom/test_entity_refs.cpp +111 -0
  30. data/vendor-src/libleptris/test/parser/test_encoding.cpp +115 -0
  31. data/vendor-src/libleptris/test/parser/test_utf8_simd.cpp +130 -0
  32. data/vendor-src/libleptris/test/xsd/test_xsd_compile.cpp +299 -3
  33. data/vendor-src/libleptris/vcpkg.json +1 -1
  34. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ad8a3795b30020ae925fb64a19f9acadbcd26ab10da9283a145be14f6e2dede
4
- data.tar.gz: 3838a8f9937409fefcce230eed59d0c6e46c53e069159e00722538ebfb695193
3
+ metadata.gz: d9946e2b50f4e9284001bb923683803b7cca891c28154e28129c4a079a6ace83
4
+ data.tar.gz: 1da14aa052d16d328f2c49ac9a59f023b8ee192324b0d8e510402230e1e8866e
5
5
  SHA512:
6
- metadata.gz: d2272eec63c0c2ddd3dab229cc209255b9ac8b4056659985f1195d0e00b175abfa98ffb1ac59f66b2a286ea6293edf201816c7201eac74dfd0ae0d3fcebcee54
7
- data.tar.gz: 45f7f2a9af016191fb590dd2c110e150564567799a6e76c62513ffee715e2c78b6d119190e2c4bb36fc697d96b345d59977c33102b25f082dfad4ef21626316b
6
+ metadata.gz: 89551eed859940f012e3e49b276c0f4b998597a803bb2cfab1eb73d2c7787ccd753a3040946a410bd1fc2b8fe00b436fecaa5a37012defe57994a783861dba9e
7
+ data.tar.gz: a775d3e6462e2b1dea2eaabcc21239b7a6d0b6df02e9a10020033d5b03d0f34141ee68ef49bdb4355847ea65891cbd13bb93896d0ca6d948f85f677203ac8fac
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.391"
11
+ LIBLEPTRIS_VERSION = "1.9.396"
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
@@ -401,8 +401,14 @@ namespace :audit do
401
401
  .map { |n| n.sub(/\A_/, "") }
402
402
  .select { |n| n.start_with?("leptris_") }
403
403
  .map { |n| n.sub(/\Aleptris_/, "") }.sort
404
- attached = File.read("lib/leptris/xml/ffi.rb")
405
- .scan(/attach_function :leptris_([a-z_0-9]+)/).flatten.sort
404
+ attached = (
405
+ File.read("lib/leptris/xml/ffi.rb")
406
+ .scan(/attach_function :leptris_([a-z_0-9]+)/).flatten +
407
+ # The native shim's C->C dlsym dispatches (#572: struct-value
408
+ # returns libffi can't carry) are bound surface too.
409
+ File.read("ext/leptris/native/native.c")
410
+ .scan(/lib_sym\((?:h|handle), "leptris_([a-z_0-9]+)"\)/).flatten
411
+ ).sort
406
412
  # Version-gated faces: soft-attached because engines BEFORE the
407
413
  # named release lack the symbol (the load must survive them).
408
414
  # They join the audit only when the engine pin reaches that
@@ -278,6 +278,14 @@ static void *lib_open(const char *path) { return dlopen(path, RTLD_NOW); }
278
278
  static void *lib_sym(void *h, const char *name) { return dlsym(h, name); }
279
279
  #endif
280
280
 
281
+ /* leptris-ruby#572: the engine's capability descriptor (1.9.392+). */
282
+ typedef struct {
283
+ unsigned long long caps;
284
+ unsigned descriptor_version;
285
+ } rb_caps_t;
286
+ typedef rb_caps_t (*capabilities_fn)(void);
287
+ static capabilities_fn f_capabilities;
288
+
281
289
  static void resolve_symbols(const char *lib_path)
282
290
  {
283
291
  void *h = lib_open(lib_path);
@@ -314,6 +322,7 @@ static void resolve_symbols(const char *lib_path)
314
322
  /* Single-crossing element construction (libleptris 1.9.237,
315
323
  * #1344): NULL in older engine pins; the create path falls
316
324
  * back to create + per-pair sets. */
325
+ f_capabilities = (capabilities_fn)lib_sym(h, "leptris_capabilities");
317
326
  f_elem_new_with_attrs =
318
327
  (elem_new_attrs_fn)lib_sym(h, "leptris_element_new_with_attributes");
319
328
  f_text_create = (text_create_fn)lib_sym(h, "leptris_text_node_create");
@@ -3965,6 +3974,25 @@ static VALUE nf_sax_parser_free(VALUE self, VALUE obj)
3965
3974
  return Qnil;
3966
3975
  }
3967
3976
 
3977
+ /* leptris-ruby#572: the engine's capability descriptor as a plain
3978
+ * hash — libffi's small-struct value return proved unreliable across
3979
+ * ABIs, so the shim reads the C struct and returns the two fields
3980
+ * (raw bitset + descriptor version; bits append per #1905). */
3981
+ static VALUE nf_capabilities(VALUE module) {
3982
+ VALUE hash = rb_hash_new();
3983
+ if (!f_capabilities) {
3984
+ rb_hash_aset(hash, ID2SYM(rb_intern("caps")), Qnil);
3985
+ rb_hash_aset(hash, ID2SYM(rb_intern("descriptor_version")), Qnil);
3986
+ return hash;
3987
+ }
3988
+ rb_caps_t c = f_capabilities();
3989
+ rb_hash_aset(hash, ID2SYM(rb_intern("caps")),
3990
+ ULL2NUM(c.caps));
3991
+ rb_hash_aset(hash, ID2SYM(rb_intern("descriptor_version")),
3992
+ UINT2NUM(c.descriptor_version));
3993
+ return hash;
3994
+ }
3995
+
3968
3996
  LEPTRIS_INIT_EXPORT void Init_native(void)
3969
3997
  {
3970
3998
  VALUE m_leptris, m_xml, m_native;
@@ -4084,6 +4112,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
4084
4112
  c_sax_parser = rb_define_class_under(m_native, "SaxParser",
4085
4113
  c_object);
4086
4114
  rb_define_module_function(m_native, "sax_parse", nf_sax_parse, 3);
4115
+ rb_define_module_function(m_native, "capabilities",
4116
+ nf_capabilities, 0);
4087
4117
  rb_define_module_function(m_native, "sax_parser_create",
4088
4118
  nf_sax_parser_create, 2);
4089
4119
  rb_define_module_function(m_native, "sax_parser_feed",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.391.0"
4
+ VERSION = "1.9.396.0"
5
5
  end
@@ -566,6 +566,11 @@ module Leptris
566
566
  [:string, :size_t, :leptris_plan, :pointer],
567
567
  :leptris_plan_result
568
568
 
569
+ # leptris-ruby#572: the engine's capability descriptor is read
570
+ # through the native shim's Native.capabilities (libffi's
571
+ # small-struct value return proved ABI-unreliable); the shim
572
+ # exposes the raw bitset + descriptor version and re-encodes
573
+ # nothing (bits append per contract).
569
574
  attach_function :leptris_version, [], UTF8_STRING
570
575
  attach_function :leptris_version_components, [:pointer, :pointer, :pointer], :void
571
576
 
Binary file
Binary file
data/lib/leptris/xml.rb CHANGED
@@ -43,6 +43,31 @@ module Leptris
43
43
 
44
44
  # Nokogiri-style top-level entry points (Nokogiri::XML(...) /
45
45
  # Nokogiri::XML.parse). Delegates to Document.parse.
46
+ # leptris-ruby#572: the engine's capability descriptor — the
47
+ # raw bitset plus descriptor version (leptris/leptris#1905).
48
+ # Frozen and memoized: the engine's answers are fixed per binary.
49
+ # Consumers (moxml#504) OR this with wrapper knowledge; bits
50
+ # append per the engine's contract — never reorder.
51
+ def self.capabilities
52
+ @capabilities ||=
53
+ begin
54
+ raw =
55
+ begin
56
+ Native.capabilities
57
+ rescue NameError
58
+ # the native layer loads with `require "leptris"`;
59
+ # a direct `require "leptris/xml"` has not loaded it
60
+ # yet — the explicit entry is the parallel-API door.
61
+ require "leptris"
62
+ Native.capabilities
63
+ end
64
+ {
65
+ caps: raw[:caps],
66
+ descriptor_version: raw[:descriptor_version]
67
+ }.freeze
68
+ end
69
+ end
70
+
46
71
  def self.parse(xml_or_io, options: nil, readonly: false,
47
72
  recover: true, strict: false)
48
73
  recover = false if strict
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.391
4
+ - libleptris v1.9.396
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,151 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.396] - 2026-10-31
4
+
5
+ ### Fixed
6
+
7
+ - **The AVX2 UTF-8 validator rejected valid multi-byte input
8
+ (leptris#1842 A5 follow-up).** Three defects shipped with the
9
+ 1.9.395 SIMD gate, all invisible on arm64: the ascii lane's
10
+ polarity was inverted (`cmplt(cur, zero)` is the NOT-ascii half,
11
+ so every ASCII byte tripped the error term), both
12
+ continuation-range bounds compared in the wrong direction
13
+ (`cont_ok` was unsatisfiable — NEON's direct `vcge/vcle` pair
14
+ became a swapped saturating-subtraction idiom), and the tail
15
+ anchor walked back only over continuation runs, letting an
16
+ opener in the final block lane escape validation (both cores).
17
+ The leptris-py UTF-16 BOM lane caught it (`malformed input` on
18
+ `café`); x86 CI failed 25+ tests. Now verified by a 400k-case
19
+ lane-math simulation against the scalar oracle plus boundary
20
+ shapes seeded into the randomized cross-check.
21
+
22
+ - **`leptris_parse_string_inplace` no longer writes past the
23
+ buffer.** The NUL sentinel at `buf[len]` exceeded the documented
24
+ contract (terminators INSIDE the buffer); an exactly-sized
25
+ caller tripped ASAN. The scanner takes `len` and never read it.
26
+
27
+ - **`xs:redefine` bodies were dropped (leptris#1926).** A
28
+ `redefine`d complexType whose body declared new particles (or
29
+ attributes) lost them: the extension derived from the base type
30
+ captured its model against the BASE body, and the redefinition was
31
+ never folded in. Bodies are now captured after the base resolves —
32
+ the same mechanism `xs:override` already uses (PR #1913) — so the
33
+ ladder resolves head-first through the redefinition and the
34
+ extension folds against the redefined content. `xs:group`/attr
35
+ refs inside redefine bodies are collected like override bodies.
36
+
37
+
38
+
39
+ ## [1.9.395] - 2026-10-31
40
+
41
+ ### Added
42
+
43
+ - **Boundary-vector UTF-8 validation (leptris#1842 A5, PRs #1922/#1924).**
44
+ `leptris_utf8_valid` — AVX2/NEON cores over the existing dispatch
45
+ layer, the scalar DFA as the fallback and oracle — pinned by an
46
+ adversarial matrix plus a 4000-case randomized cross-check. The
47
+ parser's UTF-8 gate now routes through it: **parse of UTF-8-dense
48
+ documents is +32%** (3.70ms -> 2.81ms on a 2.2MB catalog; the
49
+ validator alone ~3.7 GB/s on NEON). Unblocked line-level profiling
50
+ first: the no-LTO `-g` build resolves file:line through
51
+ dsymutil/atos (ThinLTO bitcode was the blocker).
52
+
53
+ ### Fixed
54
+
55
+ - **PI data-scan bounds (fuzzer class).** Consuming the PI target's
56
+ separator could leave `p.pos` one past `p.end`; `p.end - p.pos`
57
+ underflowed to SIZE_MAX and the data `memchr` walked off the
58
+ allocation (the nightly-fuzz witness segfaulted under the no-LTO
59
+ build — allocator luck hid it on LTO).
60
+ - **ctE019 regression (leptris#1923, PR #1927).** A chameleon-included
61
+ attribute referenced through a restriction base was false-rejected
62
+ as "not declared (strict)" — the 1.9.391 pending-ref fix made the
63
+ forward-referenced rows visible, surfacing that only the derived
64
+ type's rows were consulted. The oracle (xmllint) validates ctE019:
65
+ both derivation spellings now record the restriction base and the
66
+ strict-attribute admission walks the chain (admission only;
67
+ required/fixed/type checks stay derived-only).
68
+ - drop the const-path pristine carve (leptris#1842 A3) (parse)
69
+
70
+
71
+
72
+ ## [1.9.394] - 2026-10-30
73
+
74
+ ### Fixed
75
+
76
+ - **xs:override loads its base and replaces the overridden components
77
+ (leptris#1913, PR #1917).** `override` was absent from the
78
+ declaration gate and the include-family chase, so the base document
79
+ never loaded and the override body's nested declarations were never
80
+ captured — the compiled schema had an empty symbol space (decls=0,
81
+ every instance element "not declared"). Override now chases its
82
+ schemaLocation like include (same targetNamespace), then captures
83
+ its own declaration children as a pseudo-root AFTER the base so the
84
+ head-first lookup ladder sees the replacements; the attributeGroup
85
+ pre-pass honors a capture-force flag in that body, and the ref-check
86
+ declaration collector walks the override body too. Witness: base
87
+ `Doc` replaced by the override's `Doc` — `<doc><b>1</b></doc>`
88
+ validates, `<doc><a>x</a></doc>` is rejected.
89
+
90
+
91
+
92
+ ## [1.9.393] - 2026-10-30
93
+
94
+ ### Added
95
+
96
+ - **Capability bit for entity-reference nodes (leptris#1904, PR #1915).**
97
+ `LEPTRIS_PARSE_KEEP_ENTITY_REFS` already parses references as
98
+ first-class `LEPTRIS_NODE_TYPE_ENTITY_REF` nodes — text segments
99
+ split around the five built-ins and custom named references
100
+ (numeric character references stay text, XML 1.0 §4.1) — but
101
+ nothing advertised it. `LEPTRIS_CAP_ENTITY_REF_NODES` lets a
102
+ binding probe once and parse with the flag instead of re-scanning
103
+ expanded text; a spec pins the DOM shape and the
104
+ string-value / serialization / XPath agreement.
105
+
106
+ ### Fixed
107
+
108
+ - **XSD 1.1 was inert for every `vc:minVersion`-marked schema
109
+ (leptris#1910, leptris#1911, PR #1914).** The version detector
110
+ read only the root's plain `version` attribute; the W3C suite and
111
+ the binding ecosystem mark 1.1 with `vc:minVersion` in the
112
+ versioning namespace. With the capture gates off,
113
+ `xs:alternative` and `xs:assert` compiled to nothing —
114
+ conditional type assignment applied the declared base type's
115
+ model in both directions (false rejects and false accepts), and
116
+ asserts never attached to their types. The detector now honors
117
+ `version`, `vc:minVersion`, and a `vc:maxVersion="1.0"` pin, and
118
+ `xs:openContent` interleave/suffix semantics engage with it
119
+ (leptris#1912's repro validates clean).
120
+ - **Value-comparison keywords accept name-test operands
121
+ (`w le h`).** The XPath `eq/ne/lt/le/gt/ge` operators only read
122
+ as operators when the right operand opened with a scalar, paren,
123
+ variable, or function call — an `xs:assert test="w le h"` failed
124
+ to compile. Adjacent name tokens are never a valid path
125
+ continuation, so a bare name-test follower is unambiguously an
126
+ operand; every path start is now admitted.
127
+
128
+
129
+
130
+ ## [1.9.392] - 2026-10-30
131
+
132
+ ### Added
133
+
134
+ - **Capability descriptor face (leptris#1905, PR #1908).**
135
+ `leptris_capabilities()` returns a versioned bitset of every face
136
+ this installed engine serves — the engine-side half of the
137
+ moxml#504 capability report. Bindings OR it with their own wrapper
138
+ knowledge instead of maintaining scattered version-gate
139
+ constants; build-conditional faces (DUCET collation, utf8proc,
140
+ iconv, AVX2) follow the same `LEPTRIS_HAS_*` macros that gate the
141
+ sources, so the descriptor cannot drift from the binary it ships
142
+ in. Contract: bits append, never reorder; the descriptor version
143
+ gates shape changes. The spec anchors every unconditional bit to
144
+ its real face symbol — a bit without a linked face fails the
145
+ build, not the assertion.
146
+
147
+
148
+
3
149
  ## [1.9.391] - 2026-10-30
4
150
 
5
151
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.391
6
+ VERSION 1.9.396
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -744,6 +744,43 @@ To pin enum values (bindings hard-code these):
744
744
  ctest --test-dir build -R HeaderHygiene
745
745
  ----
746
746
 
747
+ === Capability descriptor (one call: what does this engine serve?)
748
+
749
+ Bindings and embedders stop maintaining scattered version-gate
750
+ constants: one call returns a versioned bitset of every face this
751
+ installed engine serves. Build-conditional faces (Unicode
752
+ normalization, iconv, DUCET collation, AVX2) answer honestly for the
753
+ exact library you linked. New capabilities APPEND bits — the
754
+ descriptor version gates shape changes.
755
+
756
+ [source,c]
757
+ ----
758
+ LeptrisCapabilities c = leptris_capabilities();
759
+ if (c.descriptor_version >= 1 && (c.caps & LEPTRIS_CAP_XSD)) {
760
+ /* xsd compile/validate faces are present */
761
+ }
762
+ ----
763
+
764
+ Entity references: parsing with `LEPTRIS_PARSE_KEEP_ENTITY_REFS`
765
+ yields `LEPTRIS_NODE_TYPE_ENTITY_REF` nodes instead of pre-expanded
766
+ text — segments split around the five built-ins and custom named
767
+ references (numeric character references stay text, per XML 1.0
768
+ §4.1). String-value, serialization, and XPath all see the expanded
769
+ value. Probe `LEPTRIS_CAP_ENTITY_REF_NODES` before relying on the
770
+ mode.
771
+
772
+ The bits: `LEPTRIS_CAP_PARSE`, `LEPTRIS_CAP_DOM`, `LEPTRIS_CAP_XPATH`,
773
+ `LEPTRIS_CAP_XQUERY`, `LEPTRIS_CAP_DTD`, `LEPTRIS_CAP_SAX`,
774
+ `LEPTRIS_CAP_HTML`, `LEPTRIS_CAP_XINCLUDE`, `LEPTRIS_CAP_DIFF`,
775
+ `LEPTRIS_CAP_DIGEST`, `LEPTRIS_CAP_XSD`, `LEPTRIS_CAP_XSD_SET`,
776
+ `LEPTRIS_CAP_RELAXNG`, `LEPTRIS_CAP_SCHEMATRON`, `LEPTRIS_CAP_XSLT`,
777
+ `LEPTRIS_CAP_DESCRIPTOR`, `LEPTRIS_CAP_NS_SNAPSHOT_BULK`,
778
+ `LEPTRIS_CAP_CHILDREN_BATCH`, `LEPTRIS_CAP_NAME_COMPARE`,
779
+ `LEPTRIS_CAP_VERSION`, and the build-conditional
780
+ `LEPTRIS_CAP_COLLATION_DUCET`, `LEPTRIS_CAP_UNICODE_UTF8PROC`,
781
+ `LEPTRIS_CAP_ENCODING_ICONV`, `LEPTRIS_CAP_SIMD_AVX2`,
782
+ `LEPTRIS_CAP_ENTITY_REF_NODES`.
783
+
747
784
  [#FFI]
748
785
  == FFI design
749
786
 
@@ -300,16 +300,23 @@ target_compile_definitions(leptris_objects PUBLIC _POSIX_C_SOURCE=200809L)
300
300
  # them all into the same library, dispatch at runtime via cpu.c. The
301
301
  # scalar TU (simd_text.c) is always built; these add accelerated paths.
302
302
  if(CMAKE_SYSTEM_PROCESSOR MATCHES "(x86_64|AMD64|i[3-6]86)")
303
- check_c_compiler_flag("-mavx2" LEPTRIS_HAVE_AVX2_FLAG)
303
+ # MSVC has no -mavx2; /arch:AVX2 enables the whole AVX set for the
304
+ # TU (the windows lane shipped scalar-only and the capability bit
305
+ # disagreed with test_capabilities — 2026-10-31).
306
+ if(MSVC)
307
+ set(LEPTRIS_AVX2_FLAG "/arch:AVX2")
308
+ else()
309
+ set(LEPTRIS_AVX2_FLAG "-mavx2")
310
+ endif()
311
+ check_c_compiler_flag("${LEPTRIS_AVX2_FLAG}" LEPTRIS_HAVE_AVX2_FLAG)
304
312
  if(LEPTRIS_HAVE_AVX2_FLAG)
305
313
  target_sources(leptris_objects PRIVATE leptris/common/simd_text_avx2.c)
306
314
  set_source_files_properties(leptris/common/simd_text_avx2.c
307
- PROPERTIES COMPILE_FLAGS "-mavx2")
315
+ PROPERTIES COMPILE_FLAGS "${LEPTRIS_AVX2_FLAG}")
308
316
  # Consumers (simd_text.c dispatch) reference the AVX2 symbol only
309
- # when this define is present — MSVC rejects -mavx2 (uses /arch:),
310
- # so the TU is skipped there and the dispatch falls back to scalar.
317
+ # when this define is present.
311
318
  target_compile_definitions(leptris_objects PRIVATE LEPTRIS_HAS_AVX2_BUILD=1)
312
- message(STATUS "AOT SIMD: AVX2 text scan ENABLED")
319
+ message(STATUS "AOT SIMD: AVX2 text scan ENABLED (${LEPTRIS_AVX2_FLAG})")
313
320
  else()
314
321
  message(STATUS "AOT SIMD: AVX2 unavailable (flag rejected)")
315
322
  endif()
@@ -447,6 +447,58 @@ typedef struct leptris_xpath_compiled* LeptrisXPathCompiled;
447
447
  * compiled — apply it to any number of documents, from any threads. */
448
448
  typedef struct leptris_xslt* LeptrisXslt;
449
449
 
450
+ /* ============================================================================
451
+ * Capability Descriptor (leptris#1905)
452
+ * ============================================================================ */
453
+
454
+ /* What this INSTALLED engine serves — one call, one versioned
455
+ * bitset. Contract: new capabilities APPEND bits; a bit is never
456
+ * removed, repurposed, or reordered — that is a descriptor-version
457
+ * bump. Build-conditional faces (utf8proc/iconv/DUCET/AVX2) answer
458
+ * honestly for the exact library a consumer linked. Bindings OR
459
+ * this with their own wrapper knowledge; consumers stop maintaining
460
+ * scattered version-gate constants. */
461
+ enum {
462
+ LEPTRIS_CAP_PARSE = 1ULL << 0,
463
+ LEPTRIS_CAP_DOM = 1ULL << 1,
464
+ LEPTRIS_CAP_XPATH = 1ULL << 2,
465
+ LEPTRIS_CAP_XQUERY = 1ULL << 3,
466
+ LEPTRIS_CAP_DTD = 1ULL << 4,
467
+ LEPTRIS_CAP_SAX = 1ULL << 5,
468
+ LEPTRIS_CAP_HTML = 1ULL << 6,
469
+ LEPTRIS_CAP_XINCLUDE = 1ULL << 7,
470
+ LEPTRIS_CAP_DIFF = 1ULL << 8,
471
+ LEPTRIS_CAP_DIGEST = 1ULL << 9,
472
+ LEPTRIS_CAP_XSD = 1ULL << 10,
473
+ LEPTRIS_CAP_XSD_SET = 1ULL << 11,
474
+ LEPTRIS_CAP_RELAXNG = 1ULL << 12,
475
+ LEPTRIS_CAP_SCHEMATRON = 1ULL << 13,
476
+ LEPTRIS_CAP_XSLT = 1ULL << 14,
477
+ LEPTRIS_CAP_DESCRIPTOR = 1ULL << 15,
478
+ LEPTRIS_CAP_NS_SNAPSHOT_BULK = 1ULL << 16,
479
+ LEPTRIS_CAP_CHILDREN_BATCH = 1ULL << 17,
480
+ LEPTRIS_CAP_NAME_COMPARE = 1ULL << 18,
481
+ LEPTRIS_CAP_VERSION = 1ULL << 19,
482
+ /* build-conditional */
483
+ LEPTRIS_CAP_COLLATION_DUCET = 1ULL << 20,
484
+ LEPTRIS_CAP_UNICODE_UTF8PROC = 1ULL << 21,
485
+ LEPTRIS_CAP_ENCODING_ICONV = 1ULL << 22,
486
+ LEPTRIS_CAP_SIMD_AVX2 = 1ULL << 23,
487
+ /* LEPTRIS_PARSE_KEEP_ENTITY_REFS yields entity references as
488
+ * first-class ENTITY_REF nodes (segments split around the five
489
+ * built-ins and custom named references; leptris#1904). */
490
+ LEPTRIS_CAP_ENTITY_REF_NODES = 1ULL << 24
491
+ };
492
+
493
+ /* Descriptor shape version: 1. Bump when the STRUCT changes or a
494
+ * bit is retired (never expected — bits append). */
495
+ #define LEPTRIS_CAPS_DESCRIPTOR_VERSION 1u
496
+
497
+ typedef struct leptris_capabilities {
498
+ unsigned long long caps;
499
+ unsigned descriptor_version;
500
+ } LeptrisCapabilities;
501
+
450
502
  /* ============================================================================
451
503
  * Memory Allocation Function Types
452
504
  * ============================================================================ */
@@ -4230,6 +4230,12 @@ LEPTRIS_API const char* leptris_version(void);
4230
4230
  */
4231
4231
  LEPTRIS_API void leptris_version_components(int* major, int* minor, int* patch);
4232
4232
 
4233
+ /* leptris#1905: the capability descriptor — what this installed
4234
+ * engine serves, as a versioned bitset (LEPTRIS_CAP_*). Query once
4235
+ * at attach; new capabilities append bits (the descriptor version
4236
+ * gates shape changes). Thread-safe: pure, allocation-free. */
4237
+ LEPTRIS_API LeptrisCapabilities leptris_capabilities(void);
4238
+
4233
4239
  /**
4234
4240
  * Release this thread's libleptris caches
4235
4241
  *
@@ -49,6 +49,54 @@ static ptrdiff_t find3_scalar(const char* s, size_t len,
49
49
  return -1;
50
50
  }
51
51
 
52
+ /* Scalar UTF-8 DFA — the always-available fallback (and the oracle
53
+ * the SIMD cores are pinned against). Moved from the parser's
54
+ * dp_utf8_valid (#1842 A5). */
55
+ int leptris_utf8_valid_scalar(const char* s_, size_t len) {
56
+ const unsigned char* p = (const unsigned char*)s_;
57
+ size_t i = 0;
58
+ /* ASCII fast path: 8 bytes per iteration. The DFA below runs
59
+ * only when a high byte is present; ordinary documents pay one
60
+ * memory-bandwidth pass. */
61
+ while (i + 8 <= len) {
62
+ uint64_t w;
63
+ memcpy(&w, p + i, 8);
64
+ if (w & UINT64_C(0x8080808080808080)) break;
65
+ i += 8;
66
+ }
67
+ while (i < len && p[i] < 0x80) i++;
68
+ while (i < len) {
69
+ unsigned c = p[i];
70
+ if (c < 0x80) {
71
+ i++;
72
+ continue;
73
+ }
74
+ size_t n;
75
+ unsigned lo = 0x80;
76
+ unsigned hi = 0xBF;
77
+ if (c >= 0xC2 && c <= 0xDF) {
78
+ n = 1;
79
+ } else if (c >= 0xE0 && c <= 0xEF) {
80
+ n = 2;
81
+ if (c == 0xE0) lo = 0xA0;
82
+ else if (c == 0xED) hi = 0x9F;
83
+ } else if (c >= 0xF0 && c <= 0xF4) {
84
+ n = 3;
85
+ if (c == 0xF0) lo = 0x90;
86
+ else if (c == 0xF4) hi = 0x8F;
87
+ } else {
88
+ return 0;
89
+ }
90
+ if (len - i <= n) return 0;
91
+ if (p[i + 1] < lo || p[i + 1] > hi) return 0;
92
+ for (size_t k = 2; k <= n; k++) {
93
+ if (p[i + k] < 0x80 || p[i + k] > 0xBF) return 0;
94
+ }
95
+ i += n + 1;
96
+ }
97
+ return 1;
98
+ }
99
+
52
100
  /* ---- Dispatch ---------------------------------------------------------- */
53
101
 
54
102
  /* Resolved pointer — initialized on first use (thread-safe enough:
@@ -63,6 +111,7 @@ static void (*g_count3_high_fn)(const char*, size_t, char, char, char,
63
111
  int*) = NULL;
64
112
  static void (*g_copy3_fn)(char*, const char*, size_t, char, char, char,
65
113
  size_t*, size_t*, size_t*) = NULL;
114
+ static int (*g_utf8_fn)(const char*, size_t) = NULL;
66
115
  static ptrdiff_t (*g_scan_run_fn)(const char*, size_t, char, char, char,
67
116
  int*, int*) = NULL;
68
117
 
@@ -105,6 +154,9 @@ static size_t count_scalar(const char* s, size_t len, char c) {
105
154
  * dispatch_init()" pattern raced when two threads hit the same
106
155
  * entry point on first call. */
107
156
  static void dispatch_init(void);
157
+ int leptris_utf8_valid_scalar(const char* s, size_t len);
158
+ int leptris_utf8_valid_avx2(const char* s, size_t len);
159
+ int leptris_utf8_valid_neon(const char* s, size_t len);
108
160
  static void count3_high_scalar(const char* s, size_t len,
109
161
  char c0, char c1, char c2,
110
162
  size_t* n0, size_t* n1, size_t* n2,
@@ -132,6 +184,7 @@ static void dispatch_init(void) {
132
184
  g_count_fn = leptris_text_count_char_avx2;
133
185
  g_count3_high_fn = count3_high_scalar;
134
186
  g_scan_run_fn = leptris_text_scan_run_avx2;
187
+ g_utf8_fn = leptris_utf8_valid_avx2;
135
188
  return;
136
189
  }
137
190
  #endif
@@ -144,6 +197,7 @@ static void dispatch_init(void) {
144
197
  g_count3_high_fn = leptris_text_count3_high_neon;
145
198
  g_copy3_fn = leptris_copy_count3_neon;
146
199
  g_scan_run_fn = leptris_text_scan_run_neon;
200
+ g_utf8_fn = leptris_utf8_valid_neon;
147
201
  return;
148
202
  }
149
203
  #endif
@@ -153,6 +207,7 @@ static void dispatch_init(void) {
153
207
  g_count_fn = count_scalar;
154
208
  g_count3_high_fn = count3_high_scalar;
155
209
  g_scan_run_fn = scan_run_scalar;
210
+ g_utf8_fn = leptris_utf8_valid_scalar;
156
211
  }
157
212
 
158
213
  /* Scalar kernel: one byte loop counts all three chars and detects
@@ -239,6 +294,12 @@ void leptris_copy_count3(char* dst, const char* src, size_t len,
239
294
  leptris_text_count3(src, len, c0, c1, c2, n0, n1, n2);
240
295
  }
241
296
 
297
+ int leptris_utf8_valid(const char* s, size_t len) {
298
+ if (!g_utf8_fn) dispatch_init();
299
+ if (!g_utf8_fn) g_utf8_fn = leptris_utf8_valid_scalar;
300
+ return g_utf8_fn(s, len);
301
+ }
302
+
242
303
  ptrdiff_t leptris_text_scan_run(const char* s, size_t len, char term,
243
304
  char f1, char f2,
244
305
  int* saw_f1, int* saw_f2) {
@@ -57,6 +57,19 @@ void leptris_text_count3_high(const char* s, size_t len,
57
57
  size_t* n0, size_t* n1, size_t* n2,
58
58
  int* saw_high);
59
59
 
60
+ /* Returns 1 when [s, s+len) is valid UTF-8, 0 otherwise. SIMD
61
+ * boundary-vector validation (the Lemire/Steagall construction) with
62
+ * the scalar DFA as the fallback; identical verdicts on every input
63
+ * (pinned by the encoding suite's adversarial matrix + a randomized
64
+ * cross-check). #1842 A5: the parser's UTF-8 gate ran a per-byte DFA
65
+ * over the whole document — the hottest line on UTF-8-dense
66
+ * fixtures (line-level profile, 2026-10-30). */
67
+ int leptris_utf8_valid(const char* s, size_t len);
68
+
69
+ /* The scalar DFA fallback (and the SIMD verdicts' oracle) — also
70
+ * the shared tail anchor for the ISA cores. */
71
+ int leptris_utf8_valid_scalar(const char* s, size_t len);
72
+
60
73
  /* Copy [src, src+len) to dst while counting c0/c1/c2 occurrences —
61
74
  * one fused memory pass (falls back to memcpy + count3 where no
62
75
  * SIMD path is compiled). The parser's owns-copy path gets its