leptris 1.9.204.0-arm-linux → 1.9.208.0-arm-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -19
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/ffi.rb +28 -0
- data/lib/libleptris.so +0 -0
- data/vendor-src/libleptris/CHANGELOG.md +76 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/TODO.max-perf/00-INDEX.md +3 -3
- data/vendor-src/libleptris/TODO.max-perf/2-3-oneshot-tokenizer-design.md +7 -1
- data/vendor-src/libleptris/src/include/leptris/error.h +56 -0
- data/vendor-src/libleptris/src/include/leptris/types.h +29 -1
- data/vendor-src/libleptris/src/leptris/dom/diag.h +2 -13
- data/vendor-src/libleptris/src/leptris/dom/digest.c +5 -1
- 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/error.c +22 -0
- data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +61 -6
- data/vendor-src/libleptris/src/leptris/html/html_parse.c +125 -39
- data/vendor-src/libleptris/src/leptris/leptris.c +20 -3
- data/vendor-src/libleptris/src/leptris/leptris_internal.h +23 -1
- data/vendor-src/libleptris/src/leptris/sax/pull.c +52 -17
- data/vendor-src/libleptris/src/leptris/sax/streaming.c +90 -5
- data/vendor-src/libleptris/test/dom/test_digest.cpp +36 -0
- data/vendor-src/libleptris/test/dom/test_dom.cpp +120 -0
- data/vendor-src/libleptris/test/parser/test_parser.cpp +43 -0
- data/vendor-src/libleptris/test/sax/test_pull.cpp +32 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12d572113446e18e8af819d5856f43251e0f4680f082fc9f099b1b96b80db06a
|
|
4
|
+
data.tar.gz: 16624fb18f02d16ef61a96482b2261607639a8c69fe9c3cf14b5e965577d572b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e06f82afefb6eef0699a0dd8f791fb4c66b348a9c5bafe1ae76243e86222e45c72dbce74ffc968d63b9d8eb5a4832c4605a994f7e3566f5a484793f6c3938c8
|
|
7
|
+
data.tar.gz: e8a3d151f5a01df2c13904aff5dd29cd25a6d37e5107057f114423f431ce8c851569ffe65cf9445d6d90837d99d7f1af382f2ed3868805b9eeadb1c2824f686d
|
data/CHANGELOG.md
CHANGED
|
@@ -54,25 +54,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
54
54
|
### Not fixed despite upstream closes
|
|
55
55
|
|
|
56
56
|
- **Iterparse attribute reads: all-nil runs on ubuntu-latest Ruby
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
57
|
+
## [1.9.207.0] - 2026-09-19
|
|
58
|
+
|
|
59
|
+
### Fixed — engine sync: big-endian close-tag parse (#1197 upstream)
|
|
60
|
+
|
|
61
|
+
- Vendored libleptris 1.9.207: the close-tag fast-path compare
|
|
62
|
+
masked the wrong end of its 8-byte name load on big-endian hosts,
|
|
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
76
|
|
|
77
77
|
## [1.9.201.3] - 2026-09-19
|
|
78
78
|
|
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.208"
|
|
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
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -137,6 +137,7 @@ module Leptris
|
|
|
137
137
|
typedef :pointer, :leptris_iterparse
|
|
138
138
|
typedef :pointer, :leptris_xpath_var_set
|
|
139
139
|
typedef :pointer, :leptris_sax_parser
|
|
140
|
+
typedef :pointer, :leptris_dtd
|
|
140
141
|
typedef :int, :leptris_status
|
|
141
142
|
|
|
142
143
|
class SAXHandler < ::FFI::Struct
|
|
@@ -1156,6 +1157,33 @@ attach_function :leptris_parse_string,
|
|
|
1156
1157
|
[:leptris_document, :pointer, :pointer], :leptris_status
|
|
1157
1158
|
attach_function :leptris_status_string, [:leptris_status], :string
|
|
1158
1159
|
attach_function :leptris_error_message, [:leptris_status], :string
|
|
1160
|
+
# Engine surface adopted with libleptris 1.9.206 (the FFI
|
|
1161
|
+
# mirror audit keeps attachments and exports in lockstep):
|
|
1162
|
+
# recover diagnostics (#1200), the standalone DTD family
|
|
1163
|
+
# (leptris_dtd_*), declaration/doctype removal (#1094), the
|
|
1164
|
+
# engine-heap buffer (#1219), one-shot SAX (#1178), and the
|
|
1165
|
+
# exported error-recorder entry point.
|
|
1166
|
+
attach_function :leptris_alloc_buffer, [:size_t], :pointer
|
|
1167
|
+
attach_function :leptris_document_clear_declaration,
|
|
1168
|
+
[:leptris_document], :leptris_status
|
|
1169
|
+
attach_function :leptris_document_remove_doctype,
|
|
1170
|
+
[:leptris_document], :leptris_status
|
|
1171
|
+
attach_function :leptris_document_parse_diag_count,
|
|
1172
|
+
[:leptris_document], :size_t
|
|
1173
|
+
attach_function :leptris_document_parse_diag,
|
|
1174
|
+
[:leptris_document, :size_t, :pointer, :pointer, :size_t], :int
|
|
1175
|
+
attach_function :leptris_dtd_parse,
|
|
1176
|
+
[:string, :size_t], :leptris_dtd
|
|
1177
|
+
attach_function :leptris_dtd_free, [:leptris_dtd], :void
|
|
1178
|
+
attach_function :leptris_dtd_parse_external_subset,
|
|
1179
|
+
[:leptris_dtd, :string, :size_t], :int
|
|
1180
|
+
attach_function :leptris_dtd_set_pe_loader,
|
|
1181
|
+
[:leptris_dtd, :pointer, :pointer], :void
|
|
1182
|
+
attach_function :leptris_dtd_validate,
|
|
1183
|
+
[:leptris_document, :leptris_dtd, :pointer], :int
|
|
1184
|
+
attach_function :leptris_dtd_error_free, [:pointer], :void
|
|
1185
|
+
attach_function :leptris_sax_parser_set_one_shot,
|
|
1186
|
+
[:leptris_sax_parser, :int], :void
|
|
1159
1187
|
# Thread-global last-failure position (error.h): companion to
|
|
1160
1188
|
# leptris_last_error; populated by recover parses (#547).
|
|
1161
1189
|
attach_function :leptris_last_error_position,
|
data/lib/libleptris.so
CHANGED
|
Binary file
|
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.208] - 2026-09-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Per-machine process-global corruption (#1242): the TLS (root, doc)
|
|
8
|
+
last-hit memo in `leptris_element_get_document` trusted the root
|
|
9
|
+
ADDRESS and was cleared only by document identity — an element-level
|
|
10
|
+
unregistration left `(freed_address, doc)` behind, and a
|
|
11
|
+
recycled address resolved the WRONG document (poisoning attribute
|
|
12
|
+
ownership: whole-run all-NULL `leptris_element_attribute` reads;
|
|
13
|
+
and tripping set_root's cross-document check). The memo is now
|
|
14
|
+
cleared whenever the outgoing root address matches, in both the
|
|
15
|
+
element-level unregister and the document sweep — closing the
|
|
16
|
+
same recycled-address class #1038 fixed for the root-doc map.
|
|
17
|
+
- `leptris_document_set_root` now sets a fresh thread-local error on
|
|
18
|
+
every rejection (NULL_ARG + both EINVAL paths) — previously the
|
|
19
|
+
previous operation's message leaked through (the reported "EINVAL
|
|
20
|
+
with a stale XPath parse error").
|
|
21
|
+
- New diagnostic: `LEPTRIS_DEBUG_ATTR_MISS=1` dumps attribute-chain
|
|
22
|
+
state on lookup misses (chain length, per-attr name bytes/hash15)
|
|
23
|
+
— the discriminator for any residual binding-side corruption
|
|
24
|
+
report.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## [1.9.207] - 2026-09-19
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- stop leptris_set_error leaking into the ELF dynamic table (build)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## [1.9.206] - 2026-09-19
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- endian-neutral close-tag name compare (#1197) (parse)
|
|
41
|
+
|
|
42
|
+
### Performance
|
|
43
|
+
|
|
44
|
+
- memchr finds the text span end (TODO.max-perf/2-3 slice 3) (sax)
|
|
45
|
+
- zero-copy slice emission for one-shot pulls (#1178 tail) (sax)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## [1.9.205] - 2026-09-19
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- Parse recover diagnostics (#1200): the DOM parse now records
|
|
54
|
+
duplicate-attribute events on the document — `LeptrisDiagKind`
|
|
55
|
+
(with the new `LEPTRIS_DIAG_RECOVER` kind) is public in
|
|
56
|
+
types.h, and the diag list reads through
|
|
57
|
+
`leptris_document_parse_diag_count` /
|
|
58
|
+
`leptris_document_parse_diag` (error.h). First definition wins,
|
|
59
|
+
matching the SAX lane's libxml2-recover surface; the digest
|
|
60
|
+
gate no longer needs a SAX probe pass.
|
|
61
|
+
- `LEPTRIS_DIGEST_ATTR_ORDER` (#1200 part 1): opt-in digest flag
|
|
62
|
+
that makes attribute order identity-bearing instead of sorted
|
|
63
|
+
away.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- MSVC C2027 in `leptris_internal.h`: the `LeptrisDiag` forward
|
|
68
|
+
declaration moved to file scope (broke the windows-dll-exports
|
|
69
|
+
leg).
|
|
70
|
+
|
|
71
|
+
### Performance
|
|
72
|
+
|
|
73
|
+
- First-char-bucketed tag classifier for the WHATWG HTML path:
|
|
74
|
+
the tokenizer's name-list strcmp scans become one-load bucket
|
|
75
|
+
lookups — parse +31-37% on the #1218 bench shapes.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
3
79
|
## [1.9.204] - 2026-09-19
|
|
4
80
|
|
|
5
81
|
### Added
|
|
@@ -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 |
|
|
11
|
+
| 8 | native XML diff consumer surface | feature | CLI diff text/JSON/--summary + engine API shipped | DONE (#1184 closed) |
|
|
12
12
|
| 4 | compact-node micro-opts (non-layout) | perf | audited: TODOs 172/195/Lane18-P3 already in; residual = node-creation cost (node-layout fork, owner call) | parked on fork |
|
|
13
|
-
| 2 | compiled-dispatch tokenizer | perf |
|
|
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 |
|
|
@@ -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
|
|
@@ -96,6 +96,62 @@ LEPTRIS_API void leptris_last_error_position(int* line, int* column);
|
|
|
96
96
|
*/
|
|
97
97
|
LEPTRIS_API const char* leptris_document_last_error(LeptrisDocument doc);
|
|
98
98
|
|
|
99
|
+
/* ============================================================================
|
|
100
|
+
* Parse diagnostics (#1200): recover-class events recorded on the
|
|
101
|
+
* DOM parse (duplicate attributes today) — the SAX lane's recover
|
|
102
|
+
* error list, unified engine-side so both lanes report the same
|
|
103
|
+
* surface. Documents stay usable; the diagnostics are advisory.
|
|
104
|
+
* ============================================================================ */
|
|
105
|
+
|
|
106
|
+
#ifndef LEPTRIS_API
|
|
107
|
+
# ifdef _WIN32
|
|
108
|
+
# ifdef LEPTRIS_BUILD_SHARED
|
|
109
|
+
# define LEPTRIS_API __declspec(dllexport)
|
|
110
|
+
# elif defined(LEPTRIS_BUILDING_DLL)
|
|
111
|
+
# define LEPTRIS_API __declspec(dllexport)
|
|
112
|
+
# elif defined(LEPTRIS_USE_SHARED)
|
|
113
|
+
# define LEPTRIS_API __declspec(dllimport)
|
|
114
|
+
# else
|
|
115
|
+
# define LEPTRIS_API
|
|
116
|
+
# endif
|
|
117
|
+
# else
|
|
118
|
+
# if defined(__GNUC__) && !defined(__clang__)
|
|
119
|
+
# define LEPTRIS_API __attribute__((visibility("default"), used, \
|
|
120
|
+
externally_visible))
|
|
121
|
+
# else
|
|
122
|
+
# define LEPTRIS_API __attribute__((visibility("default"), used))
|
|
123
|
+
# endif
|
|
124
|
+
# endif
|
|
125
|
+
#endif
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Number of recover-class diagnostics recorded during the
|
|
129
|
+
* document's parse.
|
|
130
|
+
*
|
|
131
|
+
* @param doc Document handle
|
|
132
|
+
* @return Diagnostic count (0 for NULL doc or a clean parse)
|
|
133
|
+
*/
|
|
134
|
+
LEPTRIS_API size_t leptris_document_parse_diag_count(LeptrisDocument doc);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Read one parse diagnostic by index.
|
|
138
|
+
*
|
|
139
|
+
* @param doc Document handle
|
|
140
|
+
* @param index Diagnostic index (< leptris_document_parse_diag_count)
|
|
141
|
+
* @param kind Out: diagnostic kind (may be NULL)
|
|
142
|
+
* @param message Out: NUL-terminated message buffer (may be NULL)
|
|
143
|
+
* @param message_cap Capacity of message (ignored when message NULL)
|
|
144
|
+
* @return 1 on success; 0 for NULL doc or out-of-range index
|
|
145
|
+
*
|
|
146
|
+
* Memory: Message is copied into the caller's buffer; diagnostics
|
|
147
|
+
* are owned by the document until leptris_document_free.
|
|
148
|
+
*/
|
|
149
|
+
LEPTRIS_API int leptris_document_parse_diag(LeptrisDocument doc,
|
|
150
|
+
size_t index,
|
|
151
|
+
LeptrisDiagKind* kind,
|
|
152
|
+
char* message,
|
|
153
|
+
size_t message_cap);
|
|
154
|
+
|
|
99
155
|
#ifdef __cplusplus
|
|
100
156
|
}
|
|
101
157
|
#endif
|
|
@@ -246,6 +246,28 @@ typedef enum {
|
|
|
246
246
|
LEPTRIS_DIFF_IGNORE_WS_TEXT = 1u
|
|
247
247
|
} LeptrisDiffFlags;
|
|
248
248
|
|
|
249
|
+
/* Unified narration record kinds (#1126). One structured record
|
|
250
|
+
* per validation/recover event, shared by every validator (RNG
|
|
251
|
+
* today; XSD/Schematron follow) and the DOM parse's recover
|
|
252
|
+
* channel (#1200). The kind selects the reporting convention;
|
|
253
|
+
* append-only. */
|
|
254
|
+
typedef enum {
|
|
255
|
+
LEPTRIS_DIAG_INVALID = 0,
|
|
256
|
+
LEPTRIS_DIAG_NOT_ALLOWED_ANYWHERE, /* name never valid in model */
|
|
257
|
+
LEPTRIS_DIAG_NOT_ALLOWED_HERE, /* valid name, wrong position */
|
|
258
|
+
LEPTRIS_DIAG_NOT_ALLOWED_YET, /* later; missing required first */
|
|
259
|
+
LEPTRIS_DIAG_INCOMPLETE, /* content ends before required */
|
|
260
|
+
LEPTRIS_DIAG_MISSING_REQUIRED_ATTR,
|
|
261
|
+
LEPTRIS_DIAG_ATTR_NOT_ALLOWED,
|
|
262
|
+
LEPTRIS_DIAG_ATTR_VALUE_INVALID,
|
|
263
|
+
LEPTRIS_DIAG_CHAR_CONTENT_INVALID, /* data/value/list mismatch */
|
|
264
|
+
/* Recover-class parse event: the document is usable, the input
|
|
265
|
+
* was not conformant (duplicate attribute, ...). Not a model
|
|
266
|
+
* error; SAX-lane "recover" parity (#1200). */
|
|
267
|
+
LEPTRIS_DIAG_RECOVER,
|
|
268
|
+
LEPTRIS_DIAG_COUNT_
|
|
269
|
+
} LeptrisDiagKind;
|
|
270
|
+
|
|
249
271
|
/* Subtree digest flags (issue #869). The digest is content-defined
|
|
250
272
|
* (no pointers/addresses — stable across processes); equality
|
|
251
273
|
* implies subtree equivalence under the flag semantics. */
|
|
@@ -253,7 +275,13 @@ typedef enum {
|
|
|
253
275
|
LEPTRIS_DIGEST_DEFAULT = 0,
|
|
254
276
|
/* Skip whitespace-only text nodes entirely (comparator's
|
|
255
277
|
* strip-profile default). */
|
|
256
|
-
LEPTRIS_DIGEST_DROP_WS_TEXT = 1u
|
|
278
|
+
LEPTRIS_DIGEST_DROP_WS_TEXT = 1u,
|
|
279
|
+
/* Hash attributes in DOCUMENT order instead of the sorted
|
|
280
|
+
* canonical order — documents differing only in attribute
|
|
281
|
+
* order digest DIFFERENT under this flag (canon's verbose
|
|
282
|
+
* diff gate, #1200). Equality under this flag additionally
|
|
283
|
+
* implies attribute-order identity. */
|
|
284
|
+
LEPTRIS_DIGEST_ATTR_ORDER = 2u
|
|
257
285
|
} LeptrisDigestFlags;
|
|
258
286
|
|
|
259
287
|
typedef struct {
|
|
@@ -17,20 +17,9 @@
|
|
|
17
17
|
extern "C" {
|
|
18
18
|
#endif
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
LEPTRIS_DIAG_INVALID = 0,
|
|
22
|
-
LEPTRIS_DIAG_NOT_ALLOWED_ANYWHERE, /* name never valid in model */
|
|
23
|
-
LEPTRIS_DIAG_NOT_ALLOWED_HERE, /* valid name, wrong position */
|
|
24
|
-
LEPTRIS_DIAG_NOT_ALLOWED_YET, /* later; missing required first */
|
|
25
|
-
LEPTRIS_DIAG_INCOMPLETE, /* content ends before required */
|
|
26
|
-
LEPTRIS_DIAG_MISSING_REQUIRED_ATTR,
|
|
27
|
-
LEPTRIS_DIAG_ATTR_NOT_ALLOWED,
|
|
28
|
-
LEPTRIS_DIAG_ATTR_VALUE_INVALID,
|
|
29
|
-
LEPTRIS_DIAG_CHAR_CONTENT_INVALID, /* data/value/list mismatch */
|
|
30
|
-
LEPTRIS_DIAG_COUNT_
|
|
31
|
-
} LeptrisDiagKind;
|
|
20
|
+
/* LeptrisDiagKind lives in leptris/types.h (public, #1200). */
|
|
32
21
|
|
|
33
|
-
typedef struct {
|
|
22
|
+
typedef struct LeptrisDiag {
|
|
34
23
|
LeptrisDiagKind kind;
|
|
35
24
|
char* offender_name; /* captured at emit time (strdup'd);
|
|
36
25
|
* NULL when the diag has no offender.
|
|
@@ -181,7 +181,11 @@ static uint64_t digest_element(LeptrisElement e, LeptrisDigestFlags flags) {
|
|
|
181
181
|
attrs[n].value_len = leptris_attr_value_sv(a).length;
|
|
182
182
|
n++;
|
|
183
183
|
}
|
|
184
|
-
|
|
184
|
+
/* ATTR_ORDER (#1200): document order is canonical — the
|
|
185
|
+
* walk above already collected in document order, so
|
|
186
|
+
* skip the sort and attribute order feeds the hash. */
|
|
187
|
+
if (!(flags & LEPTRIS_DIGEST_ATTR_ORDER))
|
|
188
|
+
qsort(attrs, n, sizeof(DigestAttr), digest_attr_cmp);
|
|
185
189
|
}
|
|
186
190
|
h = digest_mix_u64(h, n);
|
|
187
191
|
for (size_t i = 0; i < n; i++) {
|
|
@@ -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,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
#include "common/port.h" /* LEPTRIS_THREAD_LOCAL (TODO.concurrency/01) */
|
|
8
8
|
#include <stdio.h>
|
|
9
9
|
#include <string.h>
|
|
10
|
+
#include "dom/diag.h" /* LeptrisDiag layout (#1200) */
|
|
10
11
|
|
|
11
12
|
/* Error state — THREAD-LOCAL since TODO.concurrency/01: concurrent
|
|
12
13
|
* parses (one document per thread) no longer race the channel. The
|
|
@@ -131,4 +132,25 @@ LEPTRIS_API const char* leptris_status_string(LeptrisStatus status) {
|
|
|
131
132
|
case LEPTRIS_ERROR_IO: return "I/O error";
|
|
132
133
|
default: return "Unknown error";
|
|
133
134
|
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ---- #1200: recover-class parse diagnostics ------------------- */
|
|
138
|
+
|
|
139
|
+
LEPTRIS_API size_t leptris_document_parse_diag_count(LeptrisDocument doc) {
|
|
140
|
+
return doc ? (size_t)doc->parse_diag_count : 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
LEPTRIS_API int leptris_document_parse_diag(LeptrisDocument doc,
|
|
144
|
+
size_t index,
|
|
145
|
+
LeptrisDiagKind* kind,
|
|
146
|
+
char* message,
|
|
147
|
+
size_t message_cap) {
|
|
148
|
+
if (!doc || index >= (size_t)doc->parse_diag_count) return 0;
|
|
149
|
+
struct LeptrisDiag* d = (struct LeptrisDiag*)doc->parse_diags + index;
|
|
150
|
+
if (kind) *kind = d->kind;
|
|
151
|
+
if (message && message_cap) {
|
|
152
|
+
strncpy(message, d->message, message_cap - 1);
|
|
153
|
+
message[message_cap - 1] = '\0';
|
|
154
|
+
}
|
|
155
|
+
return 1;
|
|
134
156
|
}
|
|
@@ -437,7 +437,47 @@ static inline int dp_add_attr_inline(DParser* p, LeptrisElement elem,
|
|
|
437
437
|
* Careful: first_attribute_off == 0 means empty list. We can't decode
|
|
438
438
|
* the pointer and check for NULL — at offset 0 the decoded pointer
|
|
439
439
|
* is `elem` itself, which is non-NULL. Check the offset field. */
|
|
440
|
+
/* #1200: duplicate-attribute detection. Prefilter on (len,
|
|
441
|
+
* first byte) — a mismatching pair costs two loads; the memcmp
|
|
442
|
+
* only runs on genuine candidates. First definition wins
|
|
443
|
+
* (libxml2 recover semantics, and the digest's first-wins
|
|
444
|
+
* becomes official); the duplicate never enters the chain, it
|
|
445
|
+
* costs its already-carved slot. */
|
|
440
446
|
if (elem->first_attribute_off != 0) {
|
|
447
|
+
int dup = 0;
|
|
448
|
+
for (struct leptris_attribute* a =
|
|
449
|
+
(struct leptris_attribute*)((char*)elem +
|
|
450
|
+
elem->first_attribute_off);
|
|
451
|
+
a; a = leptris_attr_next(a)) {
|
|
452
|
+
if (a->name_view.length == attr->name_view.length &&
|
|
453
|
+
a->name_view.data[0] == attr->name_view.data[0] &&
|
|
454
|
+
memcmp(a->name_view.data, attr->name_view.data,
|
|
455
|
+
attr->name_view.length) == 0) {
|
|
456
|
+
dup = 1;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (dup) {
|
|
461
|
+
if (p->doc) {
|
|
462
|
+
extern struct LeptrisDiag* leptris_diag_emit(
|
|
463
|
+
struct LeptrisDiag**, int*, int*,
|
|
464
|
+
LeptrisDiagKind, LeptrisNodeRef,
|
|
465
|
+
const char*, ...);
|
|
466
|
+
char qname[130];
|
|
467
|
+
size_t ql = attr->name_view.length < 129
|
|
468
|
+
? attr->name_view.length : 129;
|
|
469
|
+
memcpy(qname, attr->name_view.data, ql);
|
|
470
|
+
qname[ql] = '\0';
|
|
471
|
+
leptris_diag_emit(&p->doc->parse_diags,
|
|
472
|
+
&p->doc->parse_diag_count,
|
|
473
|
+
&p->doc->parse_diag_cap,
|
|
474
|
+
LEPTRIS_DIAG_RECOVER, NULL,
|
|
475
|
+
"Attribute %s redefined", qname);
|
|
476
|
+
}
|
|
477
|
+
/* 1 = duplicate skipped (keep first): the caller skips
|
|
478
|
+
* the raw-attr registration so BOTH surfaces agree. */
|
|
479
|
+
return 1;
|
|
480
|
+
}
|
|
441
481
|
/* Cache should always be valid mid-parse. Fall back to walk
|
|
442
482
|
* only if cache is NULL (defensive — shouldn't happen). */
|
|
443
483
|
struct leptris_attribute* tail = p->current_elem_last_attr;
|
|
@@ -845,13 +885,16 @@ static int dp_parse_attrs(DParser* p, LeptrisElement elem) {
|
|
|
845
885
|
continue;
|
|
846
886
|
}
|
|
847
887
|
|
|
848
|
-
/* Regular attribute — zero-copy name/value, bulk-allocated
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
888
|
+
/* Regular attribute — zero-copy name/value, bulk-allocated
|
|
889
|
+
* struct. 1 = duplicate skipped: neither surface sees it. */
|
|
890
|
+
int arc = dp_add_attr_inline(p, elem, name_start, name_len,
|
|
891
|
+
val_start, val_len, has_amp,
|
|
892
|
+
has_ws, attr_colon);
|
|
893
|
+
if (arc < 0) return -1;
|
|
894
|
+
if (arc == 0 &&
|
|
895
|
+
dp_raw_attr(p, elem, name_start, name_len,
|
|
896
|
+
val_start, val_len) != 0)
|
|
852
897
|
return -1;
|
|
853
|
-
if (dp_raw_attr(p, elem, name_start, name_len,
|
|
854
|
-
val_start, val_len) != 0) return -1;
|
|
855
898
|
}
|
|
856
899
|
/* for (;;): every exit is a return inside the loop (0 = tag
|
|
857
900
|
* closed, 1 = self-closing, -1 = error) — there is no
|
|
@@ -1571,9 +1614,21 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
|
|
|
1571
1614
|
if (close_local_len <= 8 &&
|
|
1572
1615
|
(p.probe_slack ||
|
|
1573
1616
|
close_local + 8 <= p.end + 1)) {
|
|
1617
|
+
/* The mask follows MEMORY order: memcpy fills the low
|
|
1618
|
+
* bytes on little-endian and the high bytes on
|
|
1619
|
+
* big-endian, so each dialect must mask the end its
|
|
1620
|
+
* load populated. The unconditional low-bytes mask
|
|
1621
|
+
* compared bytes past the name on big-endian and
|
|
1622
|
+
* failed every close tag (#1197). */
|
|
1623
|
+
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
|
1624
|
+
uint64_t mask = (close_local_len == 8)
|
|
1625
|
+
? ~(uint64_t)0
|
|
1626
|
+
: (~(uint64_t)0 << (64 - close_local_len * 8));
|
|
1627
|
+
#else
|
|
1574
1628
|
uint64_t mask = (close_local_len == 8)
|
|
1575
1629
|
? ~(uint64_t)0
|
|
1576
1630
|
: ((1ull << (close_local_len * 8)) - 1);
|
|
1631
|
+
#endif
|
|
1577
1632
|
uint64_t a, b;
|
|
1578
1633
|
memcpy(&a, open_name, 8);
|
|
1579
1634
|
memcpy(&b, close_local, 8);
|