leptris 1.9.207.0 → 1.9.209.0
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 +4 -4
- data/CHANGELOG.md +92 -21
- data/Rakefile +8 -7
- data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/s390x-linux-musl/libutf8proc.so.3 +0 -0
- data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/diff.rb +14 -0
- data/lib/leptris/xml/ffi.rb +0 -32
- data/vendor-src/libleptris/CHANGELOG.md +38 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/TODO.max-perf/00-INDEX.md +8 -4
- data/vendor-src/libleptris/TODO.max-perf/2-3-oneshot-tokenizer-design.md +7 -1
- data/vendor-src/libleptris/TODO.max-perf/node-layout-fork.md +70 -0
- data/vendor-src/libleptris/src/leptris/dom/element_query.c +43 -1
- data/vendor-src/libleptris/src/leptris/dom/root_doc_map.c +31 -9
- data/vendor-src/libleptris/src/leptris/dom/root_doc_map.h +2 -0
- data/vendor-src/libleptris/src/leptris/html/html_parse.c +107 -117
- data/vendor-src/libleptris/src/leptris/leptris.c +13 -3
- data/vendor-src/libleptris/test/dom/test_dom.cpp +96 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 700c8b243ae83bca888161a3db72b2e3c6304023c2d6876716d63cbc77046469
|
|
4
|
+
data.tar.gz: 3f5e149db82b4dd010e7a485650321bc752d74ee198e06b5711736781eca3281
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87f6bfc69bcabf29bef4721bddc2947311ba021146912be96f34d043123611f0bc5230fc027855d897d42b7979ff7bed97d0b6df749bcaaf9f7b62cc642c6d26
|
|
7
|
+
data.tar.gz: 022fa14f46227d54e5966be7f0b90da028a2ff4bed14e4c2f2f4e26343a4a684c43770637077ab31657efb6c7a63af357285558cc4d25550156c55742c39f83a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,87 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.209.0] - 2026-09-20
|
|
9
|
+
|
|
10
|
+
### Performance — engine sync (1.9.209)
|
|
11
|
+
|
|
12
|
+
- Vendored libleptris 1.9.209: WHATWG HTML parse round 2
|
|
13
|
+
(#1218) — the per-close-tag implied-end/open-side name-list
|
|
14
|
+
scans (40 + 33 names, ~7% of parse self-time on table-heavy
|
|
15
|
+
pages) became membership bits on the first-char-bucketed tag
|
|
16
|
+
classifier; bench_html_parse 24.1 → 25.1-26.7 MB/s. No
|
|
17
|
+
binding-facing surface changes; audit holds.
|
|
18
|
+
|
|
19
|
+
### Added — `Diff#summary` / `Diff#to_json` (the 1.9.200 output modes, #1184)
|
|
20
|
+
|
|
21
|
+
- The `leptris diff --summary` / `--json` CLI modes' library face:
|
|
22
|
+
per-kind op counts (`{update_attr: 1, insert: 1}`, `{}` when
|
|
23
|
+
identical) and the op list as JSON — pure Ruby over the existing
|
|
24
|
+
`#ops` face, no new engine symbols. Salvaged from #269's unique
|
|
25
|
+
half (its DTD half was superseded by the 1.9.204 wiring).
|
|
26
|
+
|
|
27
|
+
## [1.9.208.0] - 2026-09-20
|
|
28
|
+
|
|
29
|
+
### Fixed — engine sync: the #1242 TLS-memo corruption (1.9.208)
|
|
30
|
+
|
|
31
|
+
- Vendored libleptris 1.9.208: the TLS `(root, doc)` last-hit memo
|
|
32
|
+
in `leptris_element_get_document` trusted the root address and
|
|
33
|
+
was cleared only by document identity — an element-level
|
|
34
|
+
unregistration left `(freed_address, doc)` behind, and a
|
|
35
|
+
recycled address resolved the WRONG document, poisoning
|
|
36
|
+
attribute ownership (whole-run all-NULL
|
|
37
|
+
`leptris_element_attribute` reads; set_root's cross-document
|
|
38
|
+
check tripping EINVAL). The memo is now cleared whenever the
|
|
39
|
+
outgoing root address matches. `leptris_document_set_root` also
|
|
40
|
+
sets a fresh thread-local error on every rejection (no more
|
|
41
|
+
stale XPath messages), and `LEPTRIS_DEBUG_ATTR_MISS=1` dumps
|
|
42
|
+
attribute-chain state on lookup misses.
|
|
43
|
+
- **First IBM Z gem: `s390x-linux-musl`** ships from this release —
|
|
44
|
+
the 14th variant (qemu-built, in-container smoked). The release
|
|
45
|
+
build-musl containers needed `linux-headers` (the ffi gem's
|
|
46
|
+
bundled libffi includes `linux/limits.h` under musl; #283 fixed
|
|
47
|
+
the wrong apk line, #284 the one s390x runs). With #1197 fixed
|
|
48
|
+
(1.9.206) and strict s390x CI legs green since 1.9.207, the
|
|
49
|
+
big-endian platform is fully shippable.
|
|
50
|
+
- **CI un-quarantine where the evidence holds**: the s390x/ppc64le
|
|
51
|
+
qemu test legs are strict again, and so are the armv7/ppc64le/
|
|
52
|
+
s390x-musl release-build cells (#1174's ILP32 port shipped; arm
|
|
53
|
+
gems publish from every release since). The ubuntu-latest Ruby
|
|
54
|
+
3.4 leg stays non-blocking: strikes 7-10 on 1.9.208 show the
|
|
55
|
+
engine's memo clear was incomplete — in-spec forensics captured
|
|
56
|
+
a direct `leptris_element_attribute` call returning NULL with an
|
|
57
|
+
EMPTY chain on a live yielded element, and windows-4.0 produced
|
|
58
|
+
the fresh `set_root: element belongs to a different document`
|
|
59
|
+
for a same-document element. One mechanism: the TLS memo
|
|
60
|
+
resolving elements to a wrong document under address-recycling
|
|
61
|
+
interleavings. Tracked as leptris-ruby#279 / leptris/leptris#1242
|
|
62
|
+
with the dump env + forensics armed on the leg until the
|
|
63
|
+
residual engine fix lands.
|
|
64
|
+
- ffi.rb dedupe: the 1.9.206-surface block re-attached ten
|
|
65
|
+
symbols the 1.9.204 wiring had already attached (audit counted
|
|
66
|
+
345 attachments against 335 exports). The typed 1.9.206 block
|
|
67
|
+
is kept; audit 335/335.
|
|
68
|
+
|
|
69
|
+
## [1.9.207.0] - 2026-09-19
|
|
70
|
+
|
|
71
|
+
### Fixed — engine sync: big-endian close-tag parse (#1197 upstream)
|
|
72
|
+
|
|
73
|
+
- Vendored libleptris 1.9.207: the close-tag fast-path compare
|
|
74
|
+
masked the wrong end of its 8-byte name load on big-endian hosts,
|
|
75
|
+
rejecting ordinary paired-tag documents with "malformed input"
|
|
76
|
+
(434/727 binding-suite failures under qemu s390x). The mask now
|
|
77
|
+
follows memory order per byte dialect; the s390x-linux-musl
|
|
78
|
+
platform gem becomes viable with this release.
|
|
79
|
+
|
|
80
|
+
### Added — FFI mirror adopts the 1.9.202–1.9.207 engine surface
|
|
81
|
+
|
|
82
|
+
- audit:symbols lockstep attachments: recover diagnostics
|
|
83
|
+
(leptris_document_parse_diag[_count]), the standalone DTD family
|
|
84
|
+
(leptris_dtd_*), declaration/doctype removal, engine-heap buffer
|
|
85
|
+
allocation, and one-shot SAX. leptris_set_error stays internal —
|
|
86
|
+
the engine now hides it on ELF too (leptris#1243), so the export
|
|
87
|
+
surface agrees across Linux/macOS/Windows.
|
|
88
|
+
|
|
8
89
|
## [1.9.204.0] - 2026-09-19
|
|
9
90
|
|
|
10
91
|
### Added — document-level mutation completed: #1229 (the 1.9.204 surface)
|
|
@@ -51,29 +132,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
51
132
|
ppc64le qemu leg (ffi_spec:223) once the audit fix let the suite
|
|
52
133
|
run; the spec now asserts JVM ≡ host tier.
|
|
53
134
|
|
|
54
|
-
###
|
|
135
|
+
### Resolved upstream threads (final state)
|
|
55
136
|
|
|
137
|
+
- **#1220 (HTML mutations lost on serialization) — fixed in
|
|
138
|
+
1.9.204** (engine commit 737fe977 / #1228). Verified from the
|
|
139
|
+
binding on 1.9.208: the original reopen repro serializes the
|
|
140
|
+
mutation on both the whatwg and html4 lanes.
|
|
56
141
|
- **Iterparse attribute reads: all-nil runs on ubuntu-latest Ruby
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
rejecting ordinary paired-tag documents with "malformed input"
|
|
64
|
-
(434/727 binding-suite failures under qemu s390x). The mask now
|
|
65
|
-
follows memory order per byte dialect; the s390x-linux-musl
|
|
66
|
-
platform gem becomes viable with this release.
|
|
67
|
-
|
|
68
|
-
### Added — FFI mirror adopts the 1.9.202–1.9.207 engine surface
|
|
69
|
-
|
|
70
|
-
- audit:symbols lockstep attachments: recover diagnostics
|
|
71
|
-
(leptris_document_parse_diag[_count]), the standalone DTD family
|
|
72
|
-
(leptris_dtd_*), declaration/doctype removal, engine-heap buffer
|
|
73
|
-
allocation, and one-shot SAX. leptris_set_error stays internal —
|
|
74
|
-
the engine now hides it on ELF too (leptris#1243), so the export
|
|
75
|
-
surface agrees across Linux/macOS/Windows.
|
|
76
|
-
|
|
142
|
+
3.4 legs** — filed upstream as leptris/leptris#1242
|
|
143
|
+
(leptris-ruby#279 tracks the investigation). Originally
|
|
144
|
+
suspected binding-side, finally root-caused engine-side by
|
|
145
|
+
in-spec forensics (see the 1.9.208.0 entry); the engine's
|
|
146
|
+
1.9.208 fix is incomplete and the flake persists. The leg is
|
|
147
|
+
gated with diagnostics armed until the residual engine fix.
|
|
77
148
|
## [1.9.201.3] - 2026-09-19
|
|
78
149
|
|
|
79
150
|
### Added — the HTML facade (Nokogiri-shaped entry points)
|
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.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.209"
|
|
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
|
|
@@ -77,13 +77,14 @@ task :compile do
|
|
|
77
77
|
fetch_tarball.call(
|
|
78
78
|
"https://api.github.com/repos/leptris/leptris/tarball/v#{version}", build)
|
|
79
79
|
|
|
80
|
-
# NOTE: 32-bit ARM (armv7)
|
|
80
|
+
# NOTE: 32-bit ARM (armv7) WAS an engine port task — the round-19
|
|
81
81
|
# ABI asserts pin layouts to 8-byte-pointer sizes and correctly
|
|
82
|
-
#
|
|
83
|
-
# #1174;
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
82
|
+
# aborted the build on 32-bit. Upstream shipped the ILP32 port
|
|
83
|
+
# (leptris/leptris#1174, closed; per-wordsize layout pins +
|
|
84
|
+
# inline-value gate landed in 1.9.199) and arm-linux /
|
|
85
|
+
# arm-linux-musl gems publish from every release since — the
|
|
86
|
+
# emulated release legs are strict again. We patch nothing here;
|
|
87
|
+
# the emulated-leg smoke validates whatever the engine provides.
|
|
87
88
|
|
|
88
89
|
# utf8proc: shared build only, @rpath install name, local prefix.
|
|
89
90
|
u8_dir = File.expand_path("tmp/utf8proc-#{UTF8PROC_VERSION}", __dir__)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/diff.rb
CHANGED
|
@@ -71,4 +71,18 @@ class Leptris::XML::Diff
|
|
|
71
71
|
str_ptr = Leptris::XML::FFI.leptris_diff_serialize(@handle)
|
|
72
72
|
Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
73
73
|
end
|
|
74
|
+
|
|
75
|
+
# Per-kind op counts (#1184): {update_attr: 1, insert: 1, ...} —
|
|
76
|
+
# {} when identical.
|
|
77
|
+
def summary
|
|
78
|
+
ops.each_with_object(Hash.new(0)) { |op, h| h[op[:type]] += 1 }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The op list as JSON (#1184) — the same shape #ops yields, with
|
|
82
|
+
# stringified ops. Requires the json standard library lazily.
|
|
83
|
+
def to_json(*args)
|
|
84
|
+
require "json"
|
|
85
|
+
ops.map { |op| op.transform_values { |v| v.nil? ? nil : v.to_s } }
|
|
86
|
+
.to_json(*args)
|
|
87
|
+
end
|
|
74
88
|
end
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -386,12 +386,6 @@ attach_function :leptris_parse_string,
|
|
|
386
386
|
[:leptris_document, :int], :leptris_status
|
|
387
387
|
attach_function :leptris_document_set_doctype,
|
|
388
388
|
[:leptris_document, :string, :string, :string], :leptris_doctype
|
|
389
|
-
# The #1229 remove-half (libleptris 1.9.204): un-set the
|
|
390
|
-
# declaration; un-set the DOCTYPE (NOT_FOUND when none set).
|
|
391
|
-
attach_function :leptris_document_clear_declaration,
|
|
392
|
-
[:leptris_document], :leptris_status
|
|
393
|
-
attach_function :leptris_document_remove_doctype,
|
|
394
|
-
[:leptris_document], :leptris_status
|
|
395
389
|
attach_function :leptris_document_append_pi,
|
|
396
390
|
[:leptris_document, :string, :string], :leptris_node_ref
|
|
397
391
|
attach_function :leptris_document_remove_child,
|
|
@@ -860,14 +854,6 @@ attach_function :leptris_parse_string,
|
|
|
860
854
|
[:leptris_sax_parser], :void
|
|
861
855
|
attach_function :leptris_sax_parser_set_streaming,
|
|
862
856
|
[:leptris_sax_parser, :int], :int
|
|
863
|
-
# One-shot input hint (libleptris 1.9.204): whole document in
|
|
864
|
-
# a single feed(), alive for the parser's lifetime — enables
|
|
865
|
-
# in-place scans. The binding's one-shot entry
|
|
866
|
-
# (leptris_sax_parse) is one-shot by construction; the engine
|
|
867
|
-
# sets this itself for pull's memory source. Attach-only
|
|
868
|
-
# until a chunked-feed face wants to opt in.
|
|
869
|
-
attach_function :leptris_sax_parser_set_one_shot,
|
|
870
|
-
[:leptris_sax_parser, :int], :void
|
|
871
857
|
|
|
872
858
|
# Pull parsing (v1.6.0): StAX-style cursor over a document.
|
|
873
859
|
attach_function :leptris_pull_new,
|
|
@@ -1053,19 +1039,6 @@ attach_function :leptris_parse_string,
|
|
|
1053
1039
|
:line, :int,
|
|
1054
1040
|
:column, :int
|
|
1055
1041
|
end
|
|
1056
|
-
attach_function :leptris_dtd_parse,
|
|
1057
|
-
[:string, :size_t], :pointer
|
|
1058
|
-
# leptris_document_get_dtd attached above with the readers
|
|
1059
|
-
attach_function :leptris_dtd_parse_external_subset,
|
|
1060
|
-
[:pointer, :string, :size_t], :int
|
|
1061
|
-
attach_function :leptris_dtd_set_pe_loader,
|
|
1062
|
-
[:pointer, :pointer, :pointer], :void
|
|
1063
|
-
attach_function :leptris_dtd_validate,
|
|
1064
|
-
[:leptris_document, :pointer, :pointer], :int
|
|
1065
|
-
attach_function :leptris_dtd_free,
|
|
1066
|
-
[:pointer], :void
|
|
1067
|
-
attach_function :leptris_dtd_error_free,
|
|
1068
|
-
[:pointer], :void
|
|
1069
1042
|
|
|
1070
1043
|
attach_function :leptris_rng_parse,
|
|
1071
1044
|
[:string, :size_t, :pointer], :leptris_relaxng
|
|
@@ -1143,11 +1116,6 @@ attach_function :leptris_parse_string,
|
|
|
1143
1116
|
[:leptris_doctype], :string
|
|
1144
1117
|
|
|
1145
1118
|
attach_function :leptris_free_string, [:pointer], :void
|
|
1146
|
-
# Engine-heap allocation (libleptris 1.9.204): buffers handed
|
|
1147
|
-
# to engine callbacks (the DTD PE loader) so the engine frees
|
|
1148
|
-
# through its matching deallocation path. Release with
|
|
1149
|
-
# leptris_free_string when NOT consumed by the engine.
|
|
1150
|
-
attach_function :leptris_alloc_buffer, [:size_t], :pointer
|
|
1151
1119
|
attach_function :leptris_explicit_cleanup, [], :void
|
|
1152
1120
|
attach_function :leptris_set_memory_management_functions,
|
|
1153
1121
|
[:pointer, :pointer], :void
|
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.209] - 2026-09-20
|
|
4
|
+
|
|
5
|
+
### Performance
|
|
6
|
+
|
|
7
|
+
- WHATWG HTML parse, #1218 round 2: the per-close-tag name-list
|
|
8
|
+
scans (a 40-name implied-end set and a 33-name open-side set,
|
|
9
|
+
~7% of parse self-time on table-heavy pages) are now membership
|
|
10
|
+
bits on the first-char-bucketed tag classifier — both lookups
|
|
11
|
+
are one bucket walk, sets preserved bit-for-bit.
|
|
12
|
+
bench_html_parse plain fixture: 24.1 → 25.1-26.7 MB/s.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [1.9.208] - 2026-09-19
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Per-machine process-global corruption (#1242): the TLS (root, doc)
|
|
21
|
+
last-hit memo in `leptris_element_get_document` trusted the root
|
|
22
|
+
ADDRESS and was cleared only by document identity — an element-level
|
|
23
|
+
unregistration left `(freed_address, doc)` behind, and a
|
|
24
|
+
recycled address resolved the WRONG document (poisoning attribute
|
|
25
|
+
ownership: whole-run all-NULL `leptris_element_attribute` reads;
|
|
26
|
+
and tripping set_root's cross-document check). The memo is now
|
|
27
|
+
cleared whenever the outgoing root address matches, in both the
|
|
28
|
+
element-level unregister and the document sweep — closing the
|
|
29
|
+
same recycled-address class #1038 fixed for the root-doc map.
|
|
30
|
+
- `leptris_document_set_root` now sets a fresh thread-local error on
|
|
31
|
+
every rejection (NULL_ARG + both EINVAL paths) — previously the
|
|
32
|
+
previous operation's message leaked through (the reported "EINVAL
|
|
33
|
+
with a stale XPath parse error").
|
|
34
|
+
- New diagnostic: `LEPTRIS_DEBUG_ATTR_MISS=1` dumps attribute-chain
|
|
35
|
+
state on lookup misses (chain length, per-attr name bytes/hash15)
|
|
36
|
+
— the discriminator for any residual binding-side corruption
|
|
37
|
+
report.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
3
41
|
## [1.9.207] - 2026-09-19
|
|
4
42
|
|
|
5
43
|
### Fixed
|
|
@@ -5,12 +5,12 @@ RED bench first where perf claims are made.
|
|
|
5
5
|
|
|
6
6
|
| # | Lever | Type | Effort | Status |
|
|
7
7
|
|---|-------|------|--------|--------|
|
|
8
|
-
| 3 | iterparse fixed-cost (6.9µs) + error channel (#563) | perf | bench landed #
|
|
8
|
+
| 3 | iterparse fixed-cost (6.9µs) + error channel (#563) | perf | bench #1195; zero-copy emission landed #1236 (295->52us full-doc); remaining = iterparse per-event DOM construction (0.45->0.15us/event stretch) | mostly done |
|
|
9
9
|
| 1 | SIMD/memchr HTML tokenizer scans | perf | memchr ×2 (#1176/#1190) + LUT (#1199, +30%/+51%) | DONE v1.9.199 |
|
|
10
10
|
| 7 | leptris validate CLI + bindings | feature | engine half merged #1198 (--dtd + DTD engine fixes); bindings remain | half done |
|
|
11
|
-
| 8 | native XML diff consumer surface | feature |
|
|
12
|
-
| 4 | compact-node micro-opts (non-layout) | perf |
|
|
13
|
-
| 2 | compiled-dispatch tokenizer | perf |
|
|
11
|
+
| 8 | native XML diff consumer surface | feature | CLI diff text/JSON/--summary + engine API shipped | DONE (#1184 closed) |
|
|
12
|
+
| 4 | compact-node micro-opts (non-layout) | perf | residual = node-creation cost; SUPERSEDED by the node-layout fork (TODO.max-perf/node-layout-fork.md, owner green-lit 2026-09-20) | folded into fork |
|
|
13
|
+
| 2 | compiled-dispatch tokenizer | perf | slices 1+3 landed #1236; slice 2's class LUT + dense switch pre-existed; residual = iterparse consumer (row 3) | mostly done |
|
|
14
14
|
| 5 | XSLT 2x per-bench profile rounds (#682) | perf | medium, ongoing | in progress |
|
|
15
15
|
| 6 | language tails (QT3/dates/fn-items/lane 15) | correctness | medium-large | queued |
|
|
16
16
|
| 9 | prebuilt-gem 12-platform matrix | release | small, BLOCKED on owner go | blocked |
|
|
@@ -20,3 +20,7 @@ Portability wave 2026-09-18 (v1.9.199): ILP32 layout port #1193 —
|
|
|
20
20
|
per-wordsize pins, SSE2 gate, xsl:number 64-bit chain, ILP32 +
|
|
21
21
|
native-arm64 + Intel-macOS CI legs, concurrency groups everywhere.
|
|
22
22
|
Next portability gate: s390x big-endian (#1194, PR #1202).
|
|
23
|
+
|
|
24
|
+
The node-layout fork (row 4 successor, owner green-lit 2026-09-20):
|
|
25
|
+
TODO.max-perf/node-layout-fork.md — 5 slices, addresses #1222 +
|
|
26
|
+
#1179 residual + #1238 parse side.
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Levers 2+3 — compiled-dispatch + zero-copy one-shot emission (design)
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: LANDED (2026-09-19, PR #1236). Slices 1+3 implemented;
|
|
4
|
+
slice 2's class LUT (`sxs_chartype`) + dense state switch already
|
|
5
|
+
existed. Measured: bench_iterparse full-doc 295->52-63us wall,
|
|
6
|
+
15.9->74-91 MB/s, 2.54->0.45-0.55us/event. Residual to the 0.15
|
|
7
|
+
stretch = iterparse per-event DOM construction (element create +
|
|
8
|
+
namespace resolution through the public element API), a consumer
|
|
9
|
+
lever, not string copies.
|
|
4
10
|
Owners: engine repo. Bench: `benchmarks/sax/bench_iterparse.c` (#1195).
|
|
5
11
|
|
|
6
12
|
## Problem
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# The node-layout fork — pugixml-class allocation for the DOM
|
|
2
|
+
|
|
3
|
+
Status: DESIGN (2026-09-20). Owner green-light received. This is
|
|
4
|
+
the durable fix for #1222 (2-5.9x vs pugixml large docs), #1179's
|
|
5
|
+
residual node-creation cost, and the parse-side constant factor of
|
|
6
|
+
#1238. Measured basis: scaling is LINEAR on serialbench fixtures
|
|
7
|
+
(352-467 MB/s across 1.5-13MB) — the gap is a per-node constant
|
|
8
|
+
cost, not complexity.
|
|
9
|
+
|
|
10
|
+
## Why the gap exists (measured)
|
|
11
|
+
|
|
12
|
+
Every element today costs: pool_alloc (block chain + freelist
|
|
13
|
+
recycler bookkeeping) + compact-edge encoding (cp16 + overflow
|
|
14
|
+
table on out-of-range pairs) + root-doc registration for detached
|
|
15
|
+
nodes + name materialization through the namebp/inline paths.
|
|
16
|
+
pugixml pays: one bump-pointer increment inside ONE document-sized
|
|
17
|
+
block, with node "pointers" as intra-block offsets and names
|
|
18
|
+
interned into a shared char arena. Constant-factor difference per
|
|
19
|
+
node, multiplied by millions of nodes.
|
|
20
|
+
|
|
21
|
+
## Target layout (pugixml-class, adapted)
|
|
22
|
+
|
|
23
|
+
- ONE primary allocation per document (grown geometrically,
|
|
24
|
+
never per-node): node array with intra-block integer offsets for
|
|
25
|
+
parent/first-child/next-sibling/first-attr edges. No cp16
|
|
26
|
+
overflow table (offsets are block-relative and always
|
|
27
|
+
representable), no root-doc map for parse-created nodes (block
|
|
28
|
+
base implies the document).
|
|
29
|
+
- String arena beside the node array: element/attr names
|
|
30
|
+
interned by (ptr,len) hash into the block; parse buffers stay
|
|
31
|
+
immutable (#1125) and zero-copy views remain the primary
|
|
32
|
+
representation — interning only replaces the per-node copies.
|
|
33
|
+
- MUTATION-CREATED nodes keep today's paths initially (mut blocks
|
|
34
|
+
+ root-doc map) behind the same public API; the internal
|
|
35
|
+
get_document walk resolves by block base first. Migration of
|
|
36
|
+
mutation paths is a later slice, gated by the same benches.
|
|
37
|
+
- ABI unchanged: every public handle is already opaque; the
|
|
38
|
+
compact-pointer exports and element-size check (VALIDATION.md)
|
|
39
|
+
get new expected values.
|
|
40
|
+
|
|
41
|
+
## Slices (one PR each, RED bench first)
|
|
42
|
+
|
|
43
|
+
1. **Document block allocator**: the growable block + offset-edge
|
|
44
|
+
node struct, PARALLEL to the pool (parse path only, behind a
|
|
45
|
+
flag). Gate: no test changes; element-size check documents both.
|
|
46
|
+
2. **Parser wiring**: flat/direct_parse + html_parse build into
|
|
47
|
+
block storage. Gate: full ctest + html5lib corpus 1169 floor;
|
|
48
|
+
RED bench: serialbench shapes to >=350 MB/s at 13MB (close the
|
|
49
|
+
libxml2 gap, halve the pugixml gap).
|
|
50
|
+
3. **Name interning**: shared arena replaces per-node name copies.
|
|
51
|
+
Gate: attr/name read benches (many-attrs, first-access) no
|
|
52
|
+
regression; memory RSS down on name-heavy docs.
|
|
53
|
+
4. **Compact-edge retirement for parse trees**: drop overflow-table
|
|
54
|
+
traffic on the block path; mutation keeps the old encode until
|
|
55
|
+
slice 5.
|
|
56
|
+
5. **Mutation unification** (optional, last): programmatic
|
|
57
|
+
create/append onto the block. Gate: append <= pugixml (the
|
|
58
|
+
lane-18 P1 row).
|
|
59
|
+
|
|
60
|
+
## Non-goals / invariants
|
|
61
|
+
|
|
62
|
+
- No public API or ABI change (opaque handles).
|
|
63
|
+
- Digest (#869), diff, c14n correctness rides the same node
|
|
64
|
+
accessors — all read through the inline edge helpers, which
|
|
65
|
+
become offset arithmetic.
|
|
66
|
+
- The address-keyed TLS caches (root-doc map/memo) survive for
|
|
67
|
+
MUTATION nodes only; their invalidation contracts (#1242/#1038)
|
|
68
|
+
must be re-gated, not assumed.
|
|
69
|
+
|
|
70
|
+
Effort: medium-large. Every slice independently shippable.
|
|
@@ -178,7 +178,49 @@ LEPTRIS_API const char* leptris_element_attribute(LeptrisElement elem, const cha
|
|
|
178
178
|
if (!uri) return NULL; /* (4) */
|
|
179
179
|
attr = find_attr_expanded(elem, uri, colon + 1); /* (2) */
|
|
180
180
|
}
|
|
181
|
-
if (!attr)
|
|
181
|
+
if (!attr) {
|
|
182
|
+
/* #1242 diagnostic: LEPTRIS_DEBUG_ATTR_MISS=1 dumps the
|
|
183
|
+
* element's attribute-chain state for the first 5 misses in
|
|
184
|
+
* the process — distinguishes construction loss
|
|
185
|
+
* (chain_len=0) from storage corruption (garbage name
|
|
186
|
+
* bytes) from hash-discipline drift (correct bytes,
|
|
187
|
+
* mismatching hash15). Off by default; fprintf only on
|
|
188
|
+
* miss. */
|
|
189
|
+
static int dumped = 0;
|
|
190
|
+
if (dumped < 5 && getenv("LEPTRIS_DEBUG_ATTR_MISS")) {
|
|
191
|
+
dumped++;
|
|
192
|
+
size_t chain = 0;
|
|
193
|
+
for (struct leptris_attribute* a =
|
|
194
|
+
leptris_element_get_first_attribute(elem);
|
|
195
|
+
a; a = leptris_attr_next(a))
|
|
196
|
+
chain++;
|
|
197
|
+
fprintf(stderr,
|
|
198
|
+
"[leptris-attr-miss] elem=%p needle='%s' "
|
|
199
|
+
"chain_len=%zu\n",
|
|
200
|
+
(void*)elem, name, chain);
|
|
201
|
+
size_t i = 0;
|
|
202
|
+
for (struct leptris_attribute* a =
|
|
203
|
+
leptris_element_get_first_attribute(elem);
|
|
204
|
+
a && i < 8; a = leptris_attr_next(a), i++) {
|
|
205
|
+
char nb[17];
|
|
206
|
+
size_t nl = a->name_view.length < 16
|
|
207
|
+
? a->name_view.length : 16;
|
|
208
|
+
for (size_t k = 0; k < nl; k++) {
|
|
209
|
+
unsigned char c = (unsigned char)a->name_view.data[k];
|
|
210
|
+
nb[k] = (c >= 32 && c < 127) ? (char)c : '.';
|
|
211
|
+
}
|
|
212
|
+
nb[nl] = '\0';
|
|
213
|
+
fprintf(stderr,
|
|
214
|
+
" attr[%zu] addr=%p name_ptr=%p len=%zu "
|
|
215
|
+
"bytes='%s' hash15=%u entities=%d\n",
|
|
216
|
+
i, (void*)a, (void*)a->name_view.data,
|
|
217
|
+
a->name_view.length, nb,
|
|
218
|
+
(unsigned)(a->name_hash & 0x7FFFu),
|
|
219
|
+
(int)((a->name_hash & 0x8000u) != 0));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return NULL;
|
|
223
|
+
}
|
|
182
224
|
|
|
183
225
|
/* Single representation (round 4): entity values expand lazily
|
|
184
226
|
* INTO the view (owned copy); no-entity views are already
|
|
@@ -102,6 +102,15 @@ static size_t bucket_index(LeptrisElement root) {
|
|
|
102
102
|
return (size_t)(v & (ROOT_DOC_BUCKETS - 1));
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/* #682 2x lever (sound): last-hit (root, doc) memo. The climb to
|
|
106
|
+
* the root is unavoidable without a per-element field, but the TLS
|
|
107
|
+
* bucket-array walk is not — consecutive queries in a transform
|
|
108
|
+
* hit the same root, so one pointer compare replaces the hash
|
|
109
|
+
* bucket chain. One TLS access total (the memo pair), and the
|
|
110
|
+
* 1M-loop sentinel becomes a plain NULL check. */
|
|
111
|
+
static LEPTRIS_THREAD_LOCAL LeptrisElement g_memo_root;
|
|
112
|
+
static LEPTRIS_THREAD_LOCAL struct leptris_document* g_memo_doc;
|
|
113
|
+
|
|
105
114
|
void leptris_root_doc_register(LeptrisElement root, struct leptris_document* doc) {
|
|
106
115
|
if (!root || !doc) return;
|
|
107
116
|
size_t idx = bucket_index(root);
|
|
@@ -160,6 +169,17 @@ void leptris_root_doc_drain_thread_caches(void) {
|
|
|
160
169
|
void leptris_root_doc_unregister(LeptrisElement root) {
|
|
161
170
|
if (!root) return;
|
|
162
171
|
if (!rootmap_marked(root)) return; /* never registered: O(1) out */
|
|
172
|
+
/* #1242: the (root, doc) memo trusts the root ADDRESS. An
|
|
173
|
+
* element-level unregister frees that address while the doc
|
|
174
|
+
* lives — the doc-keyed invalidation never fires, and the
|
|
175
|
+
* allocator recycling the address into another element's climb
|
|
176
|
+
* target made get_document return the WRONG document. Same
|
|
177
|
+
* exposure class #1038 closed for the map; the memo needs it
|
|
178
|
+
* at element granularity. */
|
|
179
|
+
if (g_memo_root == root) {
|
|
180
|
+
g_memo_root = NULL;
|
|
181
|
+
g_memo_doc = NULL;
|
|
182
|
+
}
|
|
163
183
|
size_t idx = bucket_index(root);
|
|
164
184
|
RootDocEntry** pp = &g_root_doc_buckets[idx];
|
|
165
185
|
while (*pp) {
|
|
@@ -202,6 +222,13 @@ size_t leptris_root_doc_unregister_doc(struct leptris_document* doc) {
|
|
|
202
222
|
doc->map_entries = NULL;
|
|
203
223
|
while (e) {
|
|
204
224
|
RootDocEntry* next = e->doc_next;
|
|
225
|
+
/* #1242: a swept root address must not survive in the memo
|
|
226
|
+
* even when the memo's doc differs (a previously poisoned
|
|
227
|
+
* pair). */
|
|
228
|
+
if (g_memo_root == e->root) {
|
|
229
|
+
g_memo_root = NULL;
|
|
230
|
+
g_memo_doc = NULL;
|
|
231
|
+
}
|
|
205
232
|
size_t idx = bucket_index(e->root);
|
|
206
233
|
RootDocEntry** pp = &g_root_doc_buckets[idx];
|
|
207
234
|
while (*pp) {
|
|
@@ -234,15 +261,6 @@ struct leptris_document* leptris_root_doc_lookup(LeptrisElement root) {
|
|
|
234
261
|
* are skipped entirely (get_document was 3.5% of the light bench).
|
|
235
262
|
* Hint is thread-local-scoped by construction (one transform per
|
|
236
263
|
* thread), validated by the root-walk fallback when it misses. */
|
|
237
|
-
/* #682 2x lever (sound): last-hit (root, doc) memo. The climb to
|
|
238
|
-
* the root is unavoidable without a per-element field, but the TLS
|
|
239
|
-
* bucket-array walk is not — consecutive queries in a transform
|
|
240
|
-
* hit the same root, so one pointer compare replaces the hash
|
|
241
|
-
* bucket chain. One TLS access total (the memo pair), and the
|
|
242
|
-
* 1M-loop sentinel becomes a plain NULL check. */
|
|
243
|
-
static LEPTRIS_THREAD_LOCAL LeptrisElement g_memo_root;
|
|
244
|
-
static LEPTRIS_THREAD_LOCAL struct leptris_document* g_memo_doc;
|
|
245
|
-
|
|
246
264
|
/* #904: prime the TLS last-root memo from a driver that already
|
|
247
265
|
* knows the (root, doc) pair — the iterparse yield path hands out
|
|
248
266
|
* a subtree that is released before the next, so the memo misses
|
|
@@ -263,6 +281,10 @@ void leptris_root_doc_memo_invalidate(const struct leptris_document* doc) {
|
|
|
263
281
|
}
|
|
264
282
|
}
|
|
265
283
|
|
|
284
|
+
LeptrisElement leptris_root_doc_memo_root_for_tests(void) {
|
|
285
|
+
return g_memo_root;
|
|
286
|
+
}
|
|
287
|
+
|
|
266
288
|
struct leptris_document* leptris_element_get_document(LeptrisElement elem) {
|
|
267
289
|
if (!elem) return NULL;
|
|
268
290
|
LeptrisElement cur = elem;
|
|
@@ -15,6 +15,8 @@ void leptris_root_doc_register(LeptrisElement root, struct leptris_document* doc
|
|
|
15
15
|
void leptris_root_doc_unregister(LeptrisElement root);
|
|
16
16
|
struct leptris_document* leptris_root_doc_lookup(LeptrisElement root);
|
|
17
17
|
void leptris_root_doc_memo_invalidate(const struct leptris_document* doc);
|
|
18
|
+
/* Test hook (#1242): the memo's current root, or NULL. */
|
|
19
|
+
LeptrisElement leptris_root_doc_memo_root_for_tests(void);
|
|
18
20
|
/* #904: prime the TLS (root, doc) memo from a driver that knows
|
|
19
21
|
* the pair (iterparse yields). Root must be registered. */
|
|
20
22
|
void leptris_root_doc_memo_prime(LeptrisElement root,
|
|
@@ -2189,40 +2189,36 @@ static int h_closes_ww(const char* open, const char* start) {
|
|
|
2189
2189
|
/* WHATWG: starts that close an open p in button scope
|
|
2190
2190
|
* (13.2.6.4.7 "in body" block set + h/pre/form/li/dd/dt/plaintext/
|
|
2191
2191
|
* hr/xmp). */
|
|
2192
|
+
typedef struct {
|
|
2193
|
+
char name[12];
|
|
2194
|
+
uint8_t len;
|
|
2195
|
+
uint8_t special; /* WHATWG 13.2.4 special set */
|
|
2196
|
+
uint8_t no_fmt; /* never reconstructed (formatting) */
|
|
2197
|
+
uint8_t closes; /* bit0: h_p_closes implied-end set; bit1:
|
|
2198
|
+
* k_p_closers — replaced the per-close-tag
|
|
2199
|
+
* strcmp scans (#1218 profile). */
|
|
2200
|
+
} HTagInfo;
|
|
2201
|
+
|
|
2202
|
+
static const HTagInfo* h_tag_lookup(const char* n);
|
|
2203
|
+
|
|
2204
|
+
static const HTagInfo* h_tag_lookup(const char* n);
|
|
2205
|
+
|
|
2192
2206
|
static int h_p_closes(const char* start) {
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
"p", "search", "section", "summary", "ul",
|
|
2199
|
-
"h1", "h2", "h3", "h4", "h5",
|
|
2200
|
-
"h6", "pre", "listing", "form", "li",
|
|
2201
|
-
"dd", "dt", "plaintext", "hr", "xmp",
|
|
2202
|
-
NULL};
|
|
2203
|
-
for (int i = 0; k[i]; i++)
|
|
2204
|
-
if (strcmp(start, k[i]) == 0) return 1;
|
|
2205
|
-
return 0;
|
|
2207
|
+
/* Classifier hit (round: #1218 profile — the 40-name strcmp
|
|
2208
|
+
* scan was the top WHATWG self-time on table-heavy pages).
|
|
2209
|
+
* Sets preserved bit-for-bit from the old k[] list. */
|
|
2210
|
+
const HTagInfo* t = h_tag_lookup(start);
|
|
2211
|
+
return t && (t->closes & 1);
|
|
2206
2212
|
}
|
|
2207
2213
|
|
|
2208
2214
|
/* Implied-end sets: a start tag in `closes` closes any open
|
|
2209
2215
|
* element named `name` (HTML4 §7.5.4 / table model). */
|
|
2210
|
-
static const char* const k_p_closers[] = {
|
|
2211
|
-
"address", "article", "aside", "blockquote", "details", "dialog",
|
|
2212
|
-
"dir", "div", "dl", "fieldset", "figcaption", "figure", "footer",
|
|
2213
|
-
"form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup",
|
|
2214
|
-
"hr", "li", "main", "menu", "nav", "ol", "p", "pre", "section",
|
|
2215
|
-
"table", "ul", NULL,
|
|
2216
|
-
};
|
|
2217
|
-
static int h_in_list(const char* const* list, const char* name) {
|
|
2218
|
-
for (int i = 0; list[i]; i++)
|
|
2219
|
-
if (strcmp(list[i], name) == 0) return 1;
|
|
2220
|
-
return 0;
|
|
2221
|
-
}
|
|
2222
2216
|
/* Does starting `start` close an open element named `open`? */
|
|
2223
2217
|
static int h_closes(const char* open, const char* start) {
|
|
2224
|
-
if (strcmp(open, "p") == 0
|
|
2225
|
-
|
|
2218
|
+
if (strcmp(open, "p") == 0) {
|
|
2219
|
+
const HTagInfo* t = h_tag_lookup(start);
|
|
2220
|
+
if (t && (t->closes & 2)) return 1;
|
|
2221
|
+
}
|
|
2226
2222
|
if (strcmp(open, "li") == 0 && strcmp(start, "li") == 0)
|
|
2227
2223
|
return 1;
|
|
2228
2224
|
if ((strcmp(open, "dt") == 0 || strcmp(open, "dd") == 0) &&
|
|
@@ -3066,98 +3062,92 @@ static int h_is_formatting(const char* n) {
|
|
|
3066
3062
|
* sampler pinned at ~21% of WHATWG parse (h_is_special_ww +
|
|
3067
3063
|
* h_reconstructs). Keep in sync with the WHATWG special and
|
|
3068
3064
|
* no-formatting sets. */
|
|
3069
|
-
typedef struct {
|
|
3070
|
-
char name[12];
|
|
3071
|
-
uint8_t len;
|
|
3072
|
-
uint8_t special; /* WHATWG 13.2.4 special set */
|
|
3073
|
-
uint8_t no_fmt; /* never reconstructed (formatting) */
|
|
3074
|
-
} HTagInfo;
|
|
3075
3065
|
|
|
3076
3066
|
static const HTagInfo h_tag_infos[] = {
|
|
3077
|
-
{"address", 7, 1, 1},
|
|
3078
|
-
{"applet", 6, 1, 0},
|
|
3079
|
-
{"area", 4, 1, 0},
|
|
3080
|
-
{"article", 7, 1, 1},
|
|
3081
|
-
{"aside", 5, 1, 1},
|
|
3082
|
-
{"base", 4, 1, 1},
|
|
3083
|
-
{"basefont", 8, 1, 1},
|
|
3084
|
-
{"bgsound", 7, 1, 1},
|
|
3085
|
-
{"blockquote", 10, 1, 1},
|
|
3086
|
-
{"body", 4, 1, 1},
|
|
3087
|
-
{"br", 2, 1, 0},
|
|
3088
|
-
{"button", 6, 1, 0},
|
|
3089
|
-
{"caption", 7, 1, 1},
|
|
3090
|
-
{"center", 6, 1, 1},
|
|
3091
|
-
{"col", 3, 1, 1},
|
|
3092
|
-
{"colgroup", 8, 1, 1},
|
|
3093
|
-
{"dd", 2, 1, 1},
|
|
3094
|
-
{"details", 7, 1, 1},
|
|
3095
|
-
{"dialog", 6, 1, 1},
|
|
3096
|
-
{"dir", 3, 1, 1},
|
|
3097
|
-
{"div", 3, 1, 1},
|
|
3098
|
-
{"dl", 2, 1, 1},
|
|
3099
|
-
{"dt", 2, 1, 1},
|
|
3100
|
-
{"embed", 5, 1, 0},
|
|
3101
|
-
{"fieldset", 8, 1, 1},
|
|
3102
|
-
{"figcaption", 10, 1, 1},
|
|
3103
|
-
{"figure", 6, 1, 1},
|
|
3104
|
-
{"footer", 6, 1, 1},
|
|
3105
|
-
{"form", 4, 1, 1},
|
|
3106
|
-
{"frame", 5, 1, 1},
|
|
3107
|
-
{"frameset", 8, 1, 1},
|
|
3108
|
-
{"h1", 2, 1, 1},
|
|
3109
|
-
{"h2", 2, 1, 1},
|
|
3110
|
-
{"h3", 2, 1, 1},
|
|
3111
|
-
{"h4", 2, 1, 1},
|
|
3112
|
-
{"h5", 2, 1, 1},
|
|
3113
|
-
{"h6", 2, 1, 1},
|
|
3114
|
-
{"head", 4, 1, 1},
|
|
3115
|
-
{"header", 6, 1, 1},
|
|
3116
|
-
{"hgroup", 6, 1, 1},
|
|
3117
|
-
{"hr", 2, 1, 1},
|
|
3118
|
-
{"html", 4, 1, 1},
|
|
3119
|
-
{"iframe", 6, 1, 0},
|
|
3120
|
-
{"img", 3, 1, 0},
|
|
3121
|
-
{"input", 5, 1, 0},
|
|
3122
|
-
{"keygen", 6, 1, 0},
|
|
3123
|
-
{"li", 2, 1, 1},
|
|
3124
|
-
{"link", 4, 1, 1},
|
|
3125
|
-
{"listing", 7, 1, 1},
|
|
3126
|
-
{"main", 4, 1, 1},
|
|
3127
|
-
{"marquee", 7, 1, 0},
|
|
3128
|
-
{"menu", 4, 1, 1},
|
|
3129
|
-
{"meta", 4, 1, 1},
|
|
3130
|
-
{"nav", 3, 1, 1},
|
|
3131
|
-
{"noembed", 7, 1, 0},
|
|
3132
|
-
{"noframes", 8, 1, 1},
|
|
3133
|
-
{"noscript", 8, 1, 0},
|
|
3134
|
-
{"object", 6, 1, 0},
|
|
3135
|
-
{"ol", 2, 1, 1},
|
|
3136
|
-
{"p", 1, 1, 1},
|
|
3137
|
-
{"param", 5, 1, 0},
|
|
3138
|
-
{"plaintext", 9, 1, 1},
|
|
3139
|
-
{"pre", 3, 1, 1},
|
|
3140
|
-
{"script", 6, 1, 1},
|
|
3141
|
-
{"search", 6, 1, 1},
|
|
3142
|
-
{"section", 7, 1, 1},
|
|
3143
|
-
{"select", 6, 1, 0},
|
|
3144
|
-
{"source", 6, 1, 0},
|
|
3145
|
-
{"style", 5, 1, 1},
|
|
3146
|
-
{"summary", 7, 1, 1},
|
|
3147
|
-
{"table", 5, 1, 1},
|
|
3148
|
-
{"tbody", 5, 1, 1},
|
|
3149
|
-
{"td", 2, 1, 1},
|
|
3150
|
-
{"template", 8, 1, 1},
|
|
3151
|
-
{"textarea", 8, 1, 1},
|
|
3152
|
-
{"tfoot", 5, 1, 1},
|
|
3153
|
-
{"th", 2, 1, 1},
|
|
3154
|
-
{"thead", 5, 1, 1},
|
|
3155
|
-
{"title", 5, 1, 1},
|
|
3156
|
-
{"tr", 2, 1, 1},
|
|
3157
|
-
{"track", 5, 1, 0},
|
|
3158
|
-
{"ul", 2, 1, 1},
|
|
3159
|
-
{"wbr", 3, 1, 0},
|
|
3160
|
-
{"xmp", 3, 1, 0},
|
|
3067
|
+
{"address", 7, 1, 1, 3},
|
|
3068
|
+
{"applet", 6, 1, 0, 0},
|
|
3069
|
+
{"area", 4, 1, 0, 0},
|
|
3070
|
+
{"article", 7, 1, 1, 3},
|
|
3071
|
+
{"aside", 5, 1, 1, 3},
|
|
3072
|
+
{"base", 4, 1, 1, 0},
|
|
3073
|
+
{"basefont", 8, 1, 1, 0},
|
|
3074
|
+
{"bgsound", 7, 1, 1, 0},
|
|
3075
|
+
{"blockquote", 10, 1, 1, 3},
|
|
3076
|
+
{"body", 4, 1, 1, 0},
|
|
3077
|
+
{"br", 2, 1, 0, 0},
|
|
3078
|
+
{"button", 6, 1, 0, 0},
|
|
3079
|
+
{"caption", 7, 1, 1, 0},
|
|
3080
|
+
{"center", 6, 1, 1, 1},
|
|
3081
|
+
{"col", 3, 1, 1, 0},
|
|
3082
|
+
{"colgroup", 8, 1, 1, 0},
|
|
3083
|
+
{"dd", 2, 1, 1, 1},
|
|
3084
|
+
{"details", 7, 1, 1, 3},
|
|
3085
|
+
{"dialog", 6, 1, 1, 3},
|
|
3086
|
+
{"dir", 3, 1, 1, 3},
|
|
3087
|
+
{"div", 3, 1, 1, 3},
|
|
3088
|
+
{"dl", 2, 1, 1, 3},
|
|
3089
|
+
{"dt", 2, 1, 1, 1},
|
|
3090
|
+
{"embed", 5, 1, 0, 0},
|
|
3091
|
+
{"fieldset", 8, 1, 1, 3},
|
|
3092
|
+
{"figcaption", 10, 1, 1, 3},
|
|
3093
|
+
{"figure", 6, 1, 1, 3},
|
|
3094
|
+
{"footer", 6, 1, 1, 3},
|
|
3095
|
+
{"form", 4, 1, 1, 3},
|
|
3096
|
+
{"frame", 5, 1, 1, 0},
|
|
3097
|
+
{"frameset", 8, 1, 1, 0},
|
|
3098
|
+
{"h1", 2, 1, 1, 3},
|
|
3099
|
+
{"h2", 2, 1, 1, 3},
|
|
3100
|
+
{"h3", 2, 1, 1, 3},
|
|
3101
|
+
{"h4", 2, 1, 1, 3},
|
|
3102
|
+
{"h5", 2, 1, 1, 3},
|
|
3103
|
+
{"h6", 2, 1, 1, 3},
|
|
3104
|
+
{"head", 4, 1, 1, 0},
|
|
3105
|
+
{"header", 6, 1, 1, 3},
|
|
3106
|
+
{"hgroup", 6, 1, 1, 3},
|
|
3107
|
+
{"hr", 2, 1, 1, 3},
|
|
3108
|
+
{"html", 4, 1, 1, 0},
|
|
3109
|
+
{"iframe", 6, 1, 0, 0},
|
|
3110
|
+
{"img", 3, 1, 0, 0},
|
|
3111
|
+
{"input", 5, 1, 0, 0},
|
|
3112
|
+
{"keygen", 6, 1, 0, 0},
|
|
3113
|
+
{"li", 2, 1, 1, 3},
|
|
3114
|
+
{"link", 4, 1, 1, 0},
|
|
3115
|
+
{"listing", 7, 1, 1, 1},
|
|
3116
|
+
{"main", 4, 1, 1, 3},
|
|
3117
|
+
{"marquee", 7, 1, 0, 0},
|
|
3118
|
+
{"menu", 4, 1, 1, 3},
|
|
3119
|
+
{"meta", 4, 1, 1, 0},
|
|
3120
|
+
{"nav", 3, 1, 1, 3},
|
|
3121
|
+
{"noembed", 7, 1, 0, 0},
|
|
3122
|
+
{"noframes", 8, 1, 1, 0},
|
|
3123
|
+
{"noscript", 8, 1, 0, 0},
|
|
3124
|
+
{"object", 6, 1, 0, 0},
|
|
3125
|
+
{"ol", 2, 1, 1, 3},
|
|
3126
|
+
{"p", 1, 1, 1, 3},
|
|
3127
|
+
{"param", 5, 1, 0, 0},
|
|
3128
|
+
{"plaintext", 9, 1, 1, 1},
|
|
3129
|
+
{"pre", 3, 1, 1, 3},
|
|
3130
|
+
{"script", 6, 1, 1, 0},
|
|
3131
|
+
{"search", 6, 1, 1, 1},
|
|
3132
|
+
{"section", 7, 1, 1, 3},
|
|
3133
|
+
{"select", 6, 1, 0, 0},
|
|
3134
|
+
{"source", 6, 1, 0, 0},
|
|
3135
|
+
{"style", 5, 1, 1, 0},
|
|
3136
|
+
{"summary", 7, 1, 1, 1},
|
|
3137
|
+
{"table", 5, 1, 1, 2},
|
|
3138
|
+
{"tbody", 5, 1, 1, 0},
|
|
3139
|
+
{"td", 2, 1, 1, 0},
|
|
3140
|
+
{"template", 8, 1, 1, 0},
|
|
3141
|
+
{"textarea", 8, 1, 1, 0},
|
|
3142
|
+
{"tfoot", 5, 1, 1, 0},
|
|
3143
|
+
{"th", 2, 1, 1, 0},
|
|
3144
|
+
{"thead", 5, 1, 1, 0},
|
|
3145
|
+
{"title", 5, 1, 1, 0},
|
|
3146
|
+
{"tr", 2, 1, 1, 0},
|
|
3147
|
+
{"track", 5, 1, 0, 0},
|
|
3148
|
+
{"ul", 2, 1, 1, 3},
|
|
3149
|
+
{"wbr", 3, 1, 0, 0},
|
|
3150
|
+
{"xmp", 3, 1, 0, 1},
|
|
3161
3151
|
};
|
|
3162
3152
|
|
|
3163
3153
|
/* bucket[first_char] = start index; bucket[first_char+1] = end */
|
|
@@ -253,12 +253,19 @@ struct leptris_document* leptris_document_create_on_arena(
|
|
|
253
253
|
*/
|
|
254
254
|
LEPTRIS_API LeptrisStatus leptris_document_set_root(LeptrisDocument doc,
|
|
255
255
|
LeptrisElement root) {
|
|
256
|
-
if (!doc || !root)
|
|
256
|
+
if (!doc || !root) {
|
|
257
|
+
leptris_set_error(LEPTRIS_ERROR_NULL_ARG,
|
|
258
|
+
"set_root: NULL document or element");
|
|
259
|
+
return LEPTRIS_ERROR_NULL_ARG;
|
|
260
|
+
}
|
|
257
261
|
|
|
258
262
|
/* Already attached under a parent? (parent_off == 0 encodes NULL;
|
|
259
263
|
* leptris_elem_parent is the static inline accessor from dom/element.h) */
|
|
260
|
-
if (leptris_elem_parent(root))
|
|
264
|
+
if (leptris_elem_parent(root)) {
|
|
265
|
+
leptris_set_error(LEPTRIS_ERROR_INVALID_ARG,
|
|
266
|
+
"set_root: element is attached to a parent");
|
|
261
267
|
return LEPTRIS_ERROR_INVALID_ARG;
|
|
268
|
+
}
|
|
262
269
|
|
|
263
270
|
/* Cross-document attach would dangle the source pool on free.
|
|
264
271
|
* Lane 18 P1: resolve via get_document (map + namebp fallback)
|
|
@@ -266,8 +273,11 @@ LEPTRIS_API LeptrisStatus leptris_document_set_root(LeptrisDocument doc,
|
|
|
266
273
|
* lookup rejected every one of them. */
|
|
267
274
|
extern struct leptris_document* leptris_element_get_document(
|
|
268
275
|
LeptrisElement elem);
|
|
269
|
-
if (leptris_element_get_document(root) != doc)
|
|
276
|
+
if (leptris_element_get_document(root) != doc) {
|
|
277
|
+
leptris_set_error(LEPTRIS_ERROR_INVALID_ARG,
|
|
278
|
+
"set_root: element belongs to a different document");
|
|
270
279
|
return LEPTRIS_ERROR_INVALID_ARG;
|
|
280
|
+
}
|
|
271
281
|
|
|
272
282
|
doc->root = root;
|
|
273
283
|
doc->new_dom_root = root;
|
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
#include <gtest/gtest.h>
|
|
4
4
|
|
|
5
5
|
#include "leptris.h"
|
|
6
|
+
#include "leptris/error.h"
|
|
6
7
|
/* #1125 pristine-buffer gate: reach doc->xml_buffer directly. */
|
|
7
8
|
#include "../leptris/leptris_internal.h"
|
|
9
|
+
/* Internal root-doc-map surface (#1242 spec); C linkage. */
|
|
10
|
+
extern "C" {
|
|
11
|
+
LeptrisElement leptris_root_doc_memo_root_for_tests(void);
|
|
12
|
+
void leptris_root_doc_register(LeptrisElement root,
|
|
13
|
+
struct leptris_document* doc);
|
|
14
|
+
void leptris_root_doc_unregister(LeptrisElement root);
|
|
15
|
+
struct leptris_document* leptris_element_get_document(LeptrisElement elem);
|
|
16
|
+
}
|
|
8
17
|
#include "../leptris/memory/pool.h"
|
|
9
18
|
|
|
10
19
|
#include <cstring>
|
|
@@ -92,6 +101,30 @@ TEST(DomBasics, DocumentSetRootRejectsNullInputs) {
|
|
|
92
101
|
leptris_document_free(doc);
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
// #1242 symptom 2: a failed set_root must set a FRESH thread-local
|
|
105
|
+
// error — the binding's exception message was stitched from whatever
|
|
106
|
+
// the previous operation left behind (a stale XPath parse error).
|
|
107
|
+
TEST(DomBasics, DocumentSetRootFailureSetsFreshError) {
|
|
108
|
+
LeptrisDocument doc_a = leptris_document_create();
|
|
109
|
+
LeptrisDocument doc_b = leptris_document_create();
|
|
110
|
+
ASSERT_NE(doc_a, nullptr);
|
|
111
|
+
ASSERT_NE(doc_b, nullptr);
|
|
112
|
+
/* Poison the thread-local slot with an unrelated message. */
|
|
113
|
+
ASSERT_EQ(leptris_parse_string("<a><b></a>", 10, nullptr), nullptr);
|
|
114
|
+
|
|
115
|
+
LeptrisElement foreign = leptris_element_create(doc_b, "b");
|
|
116
|
+
ASSERT_NE(foreign, nullptr);
|
|
117
|
+
EXPECT_EQ(leptris_document_set_root(doc_a, foreign),
|
|
118
|
+
LEPTRIS_ERROR_INVALID_ARG);
|
|
119
|
+
const char* msg = leptris_last_error();
|
|
120
|
+
ASSERT_NE(msg, nullptr);
|
|
121
|
+
EXPECT_EQ(strstr(msg, "set_root"), msg)
|
|
122
|
+
<< "expected a fresh set_root message, got: " << msg;
|
|
123
|
+
|
|
124
|
+
leptris_document_free(doc_a);
|
|
125
|
+
leptris_document_free(doc_b);
|
|
126
|
+
}
|
|
127
|
+
|
|
95
128
|
TEST(DomBasics, DocumentSetRootRejectsForeignElement) {
|
|
96
129
|
LeptrisDocument doc_a = leptris_document_create();
|
|
97
130
|
LeptrisDocument doc_b = leptris_document_create();
|
|
@@ -117,6 +150,69 @@ TEST(DomBasics, DocumentSetRootRejectsParentedElement) {
|
|
|
117
150
|
leptris_document_free(doc);
|
|
118
151
|
}
|
|
119
152
|
|
|
153
|
+
/* #1242: the TLS (root, doc) memo trusts the root address. An
|
|
154
|
+
* element-level unregister frees that address while the document
|
|
155
|
+
* lives; a recycled address later made get_document resolve the
|
|
156
|
+
* WRONG document (per-machine corruption signature). Unregistering
|
|
157
|
+
* a root must drop the memo. */
|
|
158
|
+
TEST(DomBasics, RootDocMemoClearedWhenRootUnregisters) {
|
|
159
|
+
LeptrisDocument doc = leptris_document_create();
|
|
160
|
+
ASSERT_NE(doc, nullptr);
|
|
161
|
+
LeptrisElement e = leptris_element_create(doc, "r");
|
|
162
|
+
ASSERT_NE(e, nullptr);
|
|
163
|
+
leptris_root_doc_register(e, doc);
|
|
164
|
+
/* Prime the memo through a resolution. */
|
|
165
|
+
ASSERT_EQ(leptris_element_get_document(e), doc);
|
|
166
|
+
ASSERT_EQ(leptris_root_doc_memo_root_for_tests(), e);
|
|
167
|
+
/* Element-level lifecycle: the root goes away, the doc stays. */
|
|
168
|
+
leptris_root_doc_unregister(e);
|
|
169
|
+
EXPECT_EQ(leptris_root_doc_memo_root_for_tests(), nullptr)
|
|
170
|
+
<< "stale (root, doc) memo survived root unregistration";
|
|
171
|
+
leptris_document_free(doc);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* #1242: every status-returning rejection must refresh the
|
|
175
|
+
* thread-local error channel — a stale message from an earlier
|
|
176
|
+
* operation leaking through an EINVAL is the reported signature. */
|
|
177
|
+
TEST(DomBasics, DocumentSetRootRejectionSetsFreshError) {
|
|
178
|
+
const char xml[] = "<a><b/></a>";
|
|
179
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
180
|
+
LeptrisDocument doc = leptris_parse_string(xml, std::strlen(xml), &st);
|
|
181
|
+
ASSERT_NE(doc, nullptr);
|
|
182
|
+
/* Poison the channel with an XPath syntax error and snapshot it. */
|
|
183
|
+
LeptrisXPathResult bad = leptris_xpath_eval(doc, nullptr, "<<");
|
|
184
|
+
if (bad) leptris_xpath_result_free(bad);
|
|
185
|
+
std::string poisoned = leptris_last_error() ? leptris_last_error() : "";
|
|
186
|
+
ASSERT_FALSE(poisoned.empty());
|
|
187
|
+
|
|
188
|
+
LeptrisElement b = (LeptrisElement)leptris_node_first_child(
|
|
189
|
+
leptris_element_as_node(leptris_document_root(doc)));
|
|
190
|
+
ASSERT_NE(b, nullptr);
|
|
191
|
+
EXPECT_EQ(leptris_document_set_root(doc, b), LEPTRIS_ERROR_INVALID_ARG);
|
|
192
|
+
/* The channel must now describe THIS failure, not the XPath one. */
|
|
193
|
+
const char* err = leptris_last_error();
|
|
194
|
+
ASSERT_TRUE(err != nullptr && err[0] != '\0');
|
|
195
|
+
EXPECT_STRNE(err, poisoned.c_str())
|
|
196
|
+
<< "stale XPath error leaked through set_root EINVAL: " << err;
|
|
197
|
+
EXPECT_TRUE(strstr(err, "root") != nullptr)
|
|
198
|
+
<< "expected a set_root-specific message, got: " << err;
|
|
199
|
+
|
|
200
|
+
/* Cross-document rejection refreshes the channel too. */
|
|
201
|
+
LeptrisDocument other = leptris_document_create();
|
|
202
|
+
ASSERT_NE(other, nullptr);
|
|
203
|
+
LeptrisElement foreign = leptris_element_create(other, "f");
|
|
204
|
+
ASSERT_NE(foreign, nullptr);
|
|
205
|
+
EXPECT_EQ(leptris_document_set_root(doc, foreign),
|
|
206
|
+
LEPTRIS_ERROR_INVALID_ARG);
|
|
207
|
+
err = leptris_last_error();
|
|
208
|
+
ASSERT_TRUE(err != nullptr && err[0] != '\0');
|
|
209
|
+
EXPECT_STRNE(err, poisoned.c_str());
|
|
210
|
+
EXPECT_TRUE(strstr(err, "document") != nullptr)
|
|
211
|
+
<< "expected a cross-document message, got: " << err;
|
|
212
|
+
leptris_document_free(other);
|
|
213
|
+
leptris_document_free(doc);
|
|
214
|
+
}
|
|
215
|
+
|
|
120
216
|
TEST(DomBasics, AttributeLookupByName) {
|
|
121
217
|
const char xml[] = "<r a='1' b='2' c='3'/>";
|
|
122
218
|
LeptrisStatus st = LEPTRIS_OK;
|
|
@@ -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.
|
|
4
|
+
"version": "1.9.209",
|
|
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,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.209.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|
|
@@ -154,6 +154,8 @@ files:
|
|
|
154
154
|
- lib/leptris/vendor/arm64-darwin/libutf8proc.3.dylib
|
|
155
155
|
- lib/leptris/vendor/ppc64le-linux/libleptris.so
|
|
156
156
|
- lib/leptris/vendor/ppc64le-linux/libutf8proc.so.3
|
|
157
|
+
- lib/leptris/vendor/s390x-linux-musl/libleptris.so
|
|
158
|
+
- lib/leptris/vendor/s390x-linux-musl/libutf8proc.so.3
|
|
157
159
|
- lib/leptris/vendor/x86_64-darwin/libleptris.dylib
|
|
158
160
|
- lib/leptris/vendor/x86_64-darwin/libutf8proc.3.dylib
|
|
159
161
|
- lib/leptris/vendor/x86_64-linux-musl/libleptris.so
|
|
@@ -246,6 +248,7 @@ files:
|
|
|
246
248
|
- vendor-src/libleptris/TODO.max-perf/7-validate-bindings.md
|
|
247
249
|
- vendor-src/libleptris/TODO.max-perf/8-native-xml-diff-consumer.md
|
|
248
250
|
- vendor-src/libleptris/TODO.max-perf/9-prebuilt-gem-matrix.md
|
|
251
|
+
- vendor-src/libleptris/TODO.max-perf/node-layout-fork.md
|
|
249
252
|
- vendor-src/libleptris/TODO.md
|
|
250
253
|
- vendor-src/libleptris/TODO.transform/README.md
|
|
251
254
|
- vendor-src/libleptris/TODO.validate/README.md
|