leptris 1.9.265.0-arm-linux → 1.9.270.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 +15 -0
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/libleptris.so +0 -0
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +45 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/src/leptris/dom/element.h +29 -7
- data/vendor-src/libleptris/src/leptris/dom/element_query.c +138 -29
- data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +32 -4
- data/vendor-src/libleptris/src/leptris/leptris.c +10 -2
- data/vendor-src/libleptris/src/leptris/leptris_internal.h +9 -4
- data/vendor-src/libleptris/src/leptris/memory/arena.c +29 -20
- data/vendor-src/libleptris/src/leptris/memory/arena.h +3 -2
- data/vendor-src/libleptris/test/CMakeLists.txt +1 -0
- data/vendor-src/libleptris/test/dom/test_dom.cpp +68 -0
- data/vendor-src/libleptris/test/dom/test_element_children.cpp +102 -0
- data/vendor-src/libleptris/test/memory/test_oom_injection.cpp +27 -2
- data/vendor-src/libleptris/test/memory/test_pool.cpp +27 -1
- data/vendor-src/libleptris/test/perf/test_perf.cpp +10 -6
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 57cbbf0aca2e1b48a26cb24721b411399e15fdf83ad2f8de650c14ea555fb544
|
|
4
|
+
data.tar.gz: 9576559f8dca8f8aa34a95b2e22245db61044caee17629f0684d9875875bb5b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 940b60625fa31d2165eec3bf6e41f5a179452164ff7777ae6b91369ceb2373a178323de4e5f6b84d25390db2eca8080e33addc80729efe66eb133fedb7a1e9b0
|
|
7
|
+
data.tar.gz: 89a4b763fddbf76482f1f976d46f98094697ee5272ec277595addc536b5cf5086c0fc856fb26385f7defb109060bf5eb75cac7ffe08a3e3f7a96a112022688e7
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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.268.0] - 2026-09-29
|
|
9
|
+
|
|
10
|
+
### Performance — engine sync (1.9.266-1.9.268)
|
|
11
|
+
|
|
12
|
+
- Vendored libleptris 1.9.268: three DOM access rounds.
|
|
13
|
+
- The child-iteration cache: indexed child access resumes from a
|
|
14
|
+
per-element generation-validated slot instead of re-walking —
|
|
15
|
+
CI Child Access 0.34x -> 2.42x vs pugixml, Tree Walking 5.81x;
|
|
16
|
+
the cache now materializes lazily for attribute-free parents
|
|
17
|
+
too (the plain wide-catalog case was an O(N^2) re-walk per
|
|
18
|
+
call: 148s -> 0.77s on the 96 KB fixture).
|
|
19
|
+
- Bare-name attribute lookup skips prefixed attributes via the
|
|
20
|
+
TODO 173 ns-cache invariant — 14.5 -> 8.8 ns/query, 1.15x
|
|
21
|
+
pugixml.
|
|
22
|
+
|
|
8
23
|
## [1.9.265.0] - 2026-09-28
|
|
9
24
|
|
|
10
25
|
### Fixed
|
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.270"
|
|
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/libleptris.so
CHANGED
|
Binary file
|
data/vendor-src/README.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.270] - 2026-09-29
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- dom: revert the lazy #635 raw-attribute journal (#1431, release-blocking). The binding GC-frees documents while Element wrappers survive; on such a stale element the 1.9.268 accessor read exactly one field (stale-zero ns_cache_off -> clean early return), while round 17a's chain-synthesis walked the entire freed chain and handed out freed value pointers — deterministic SIGSEGV on six CI platforms. Restores the eager journal and the journal-only accessor, byte-identical control flow to 1.9.268 on that surface. The round's value-pinning specs stay; the lazy journal returns only behind a real lifetime contract.
|
|
8
|
+
|
|
9
|
+
### Performance
|
|
10
|
+
|
|
11
|
+
- memory: the arena header is carved into the span itself — a parse+free cycle is exactly ONE system allocation (was two), frozen by an exact-count allocator pin. The OOM-injection suite was re-contracted for the one-allocation parse: its fixture drains the retain pool per test (a warm retained span bypasses the injectable hook, which had made the sweeps vacuous), and the wired-check now targets the span itself while asserting one allocation completes the parse.
|
|
12
|
+
- dom: the 256-byte error buffer leaves the document construction memset. Every writer NUL-terminates and the only reader gates on byte [0], so construction zeroes sizeof(doc) - 256 plus a one-byte store — exact equivalence, deterministic byte reduction.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [1.9.269] - 2026-09-29
|
|
17
|
+
|
|
18
|
+
### Performance
|
|
19
|
+
|
|
20
|
+
- dom: the #635 raw-attribute view is now built lazily. Plain elements — no xmlns declarations, every value clean — never pay the per-attribute journal entry or the per-element ns_cache allocation; `leptris_element_attributes_raw` synthesizes the view from the attribute chain, which is byte-identical there (source order == chain order, clean values are the zero-copy source bytes). The journal still builds exactly where the source-byte contract demands it: the first xmlns declaration backfills the clean prefix, and the first ws-normalized or entity-bearing value journals its raw pre-normalization bytes. Pinned by new specs for all three paths; the dirty-value spec caught the first cut registering the trigger attribute twice. Interleaved-lane behavior is unchanged (its builder never constructed the journal; its plain-element raw surface was empty before and is now the synthesized chain).
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [1.9.268] - 2026-09-28
|
|
25
|
+
|
|
26
|
+
### Performance
|
|
27
|
+
|
|
28
|
+
- dom: the child-iteration cache is now materialized lazily for attribute-free parents. The round-13 resume slot only engaged for elements carrying namespace state, so indexed child access on a plain wide parent (`<catalog>` with a thousand `<product>` children) was still an O(N^2) full re-walk per call — dom_benchmark_v2's Child Access row measured 148 SECONDS on the 96 KB catalog fixture (CI's 1 KB fixture hid it entirely). `child()` now carves the cache from the element's pool on first indexed access; the mutation-invalidation contract is unchanged and pinned by a new spec plus a tightened (18x -> 6x) perf guard watched failing first. Same fixture after: 0.77 s (~190x). CI artifacts now show all three access rows AHEAD of pugixml on ubuntu: Child 2.42x, Attribute 1.45x, Tree Walking 5.81x.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## [1.9.267] - 2026-09-28
|
|
33
|
+
|
|
34
|
+
### Performance
|
|
35
|
+
|
|
36
|
+
- dom: bare-name attribute lookup rebuilt on the TODO 173 invariant — an attribute carries a side ns cache only when its name contains a colon, so the query walk skips prefixed attributes with a single int32 load and the per-attribute colon-probe loop is gone entirely. The finder is always-inline and the needle is hashed in the same pass as its length+colon scan (`attr_hash15_finish` split out as the shared finalizer). Attr-only arm64 harness: 14.5 -> 8.8 ns/query vs pugixml 7.4 (1.94x -> 1.15x).
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## [1.9.266] - 2026-09-28
|
|
41
|
+
|
|
42
|
+
### Performance
|
|
43
|
+
|
|
44
|
+
- dom: child-iteration cache for indexed child access. `leptris_element_child(parent, i)` no longer re-walks from the first child on every call — it resumes from a per-element slot (generation-validated; every child-list mutator invalidates) so sequential indexed access is O(1) per step, and sibling hops skip the generic node type-switch via the shared edge tables. CI dom_benchmark_v2 Child Access: 0.34x -> 2.02x vs pugixml (ubuntu leg); Tree Walking 4.94x.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
3
48
|
## [1.9.265] - 2026-09-28
|
|
4
49
|
|
|
5
50
|
### Fixed
|
|
@@ -219,12 +219,7 @@ static inline void leptris_attr_set_next(struct leptris_attribute* a,
|
|
|
219
219
|
* low bits sit under the multiply's carry chain; the upper half
|
|
220
220
|
* mixes better). Returns nonzero: 0 maps to 1 so the "uncomputed"
|
|
221
221
|
* sentinel stays unambiguous. */
|
|
222
|
-
static inline uint16_t
|
|
223
|
-
uint32_t h = 2166136261u;
|
|
224
|
-
for (size_t i = 0; i < len; i++) {
|
|
225
|
-
h ^= (unsigned char)s[i];
|
|
226
|
-
h *= 16777619u;
|
|
227
|
-
}
|
|
222
|
+
static inline uint16_t attr_hash15_finish(uint32_t h) {
|
|
228
223
|
/* Finalizer (round 20): raw FNV truncation (h>>16) mapped
|
|
229
224
|
* names differing in their last digit to hashes exactly 256
|
|
230
225
|
* apart — arithmetic progressions that collide onto one open-
|
|
@@ -237,6 +232,15 @@ static inline uint16_t attr_hash15(const char* s, size_t len) {
|
|
|
237
232
|
return h15 ? h15 : 1;
|
|
238
233
|
}
|
|
239
234
|
|
|
235
|
+
static inline uint16_t attr_hash15(const char* s, size_t len) {
|
|
236
|
+
uint32_t h = 2166136261u;
|
|
237
|
+
for (size_t i = 0; i < len; i++) {
|
|
238
|
+
h ^= (unsigned char)s[i];
|
|
239
|
+
h *= 16777619u;
|
|
240
|
+
}
|
|
241
|
+
return attr_hash15_finish(h);
|
|
242
|
+
}
|
|
243
|
+
|
|
240
244
|
/* Entity flag (round 19): name_hash bit 15. */
|
|
241
245
|
static inline int attr_has_entities(const struct leptris_attribute* a) {
|
|
242
246
|
return (a->name_hash & 0x8000u) != 0;
|
|
@@ -370,6 +374,16 @@ struct leptris_ns_cache {
|
|
|
370
374
|
struct leptris_ns_cache* doc_next;
|
|
371
375
|
unsigned char prefix_heap;
|
|
372
376
|
unsigned char uri_heap;
|
|
377
|
+
/* Lane-18 round 13: child-iteration cache. child_gen is bumped by
|
|
378
|
+
* every child-list mutator (prepend/insert/remove); child() stores
|
|
379
|
+
* (child_cached_gen, child_idx, child_node) after a walk and
|
|
380
|
+
* resumes from it while the generations agree — sequential indexed
|
|
381
|
+
* access becomes O(1) per step. append_child needs no bump: it
|
|
382
|
+
* preserves existing indices. */
|
|
383
|
+
uint32_t child_gen;
|
|
384
|
+
uint32_t child_cached_gen;
|
|
385
|
+
uint32_t child_idx;
|
|
386
|
+
LeptrisNode* child_node;
|
|
373
387
|
};
|
|
374
388
|
|
|
375
389
|
struct leptris_element {
|
|
@@ -927,7 +941,15 @@ void leptris_element_set_next_sibling(LeptrisElement elem, LeptrisElement siblin
|
|
|
927
941
|
* future compact-storage cache (e.g. pugixml-style compact pointer
|
|
928
942
|
* table) can plug in without touching every mutation call site. */
|
|
929
943
|
static inline void leptris_element_invalidate_child_cache(LeptrisElement elem) {
|
|
930
|
-
|
|
944
|
+
/* Lane-18 round 13: the hook now drives the ns_cache child-
|
|
945
|
+
* iteration cache (child_idx/child_node below). Bumping child_gen
|
|
946
|
+
* AND nulling child_node means a stale cache can never serve a
|
|
947
|
+
* node even across a 2^32 generation wrap. */
|
|
948
|
+
struct leptris_ns_cache* c = elem_get_ns_cache(elem);
|
|
949
|
+
if (c) {
|
|
950
|
+
c->child_gen++;
|
|
951
|
+
c->child_node = NULL;
|
|
952
|
+
}
|
|
931
953
|
}
|
|
932
954
|
|
|
933
955
|
/* ============================================================================
|
|
@@ -24,6 +24,8 @@ struct leptris_namespace* leptris_namespace_new_pooled(const char* prefix,
|
|
|
24
24
|
#include "cdata.h"
|
|
25
25
|
#include "comment.h"
|
|
26
26
|
#include "pi.h"
|
|
27
|
+
#include "entity_ref.h"
|
|
28
|
+
#include "edges.h"
|
|
27
29
|
#include "../common/string_view.h"
|
|
28
30
|
#include "../common/entities.h"
|
|
29
31
|
#include "../common/port.h"
|
|
@@ -35,6 +37,11 @@ struct leptris_namespace* leptris_namespace_new_pooled(const char* prefix,
|
|
|
35
37
|
* resolver are defined in the attribute section below. */
|
|
36
38
|
static struct leptris_attribute* find_attr_expanded_len(
|
|
37
39
|
LeptrisElement elem, const char* uri, const char* local, size_t ll);
|
|
40
|
+
static struct leptris_attribute* find_attr_bare(
|
|
41
|
+
LeptrisElement elem, const char* local, size_t ll);
|
|
42
|
+
static struct leptris_attribute* find_attr_bare_h(LeptrisElement elem,
|
|
43
|
+
const char* local,
|
|
44
|
+
size_t ll, uint16_t lh);
|
|
38
45
|
static const char* elem_resolve_attr_prefix(LeptrisElement elem,
|
|
39
46
|
const char* prefix);
|
|
40
47
|
#include <math.h>
|
|
@@ -170,14 +177,23 @@ LEPTRIS_API const char* leptris_element_attribute(LeptrisElement elem, const cha
|
|
|
170
177
|
* strcmp ran for EVERY bare-name query. */
|
|
171
178
|
const char* colon = NULL;
|
|
172
179
|
size_t name_len = 0;
|
|
180
|
+
/* The FNV rides this same pass (attr_hash15's byte loop fused
|
|
181
|
+
* into the len+colon scan — a second pass over the needle cost
|
|
182
|
+
* ~1ns/query). Discarded when a colon ends the scan: the bare
|
|
183
|
+
* path that consumes the hash is not taken then. */
|
|
184
|
+
uint32_t nh = 2166136261u;
|
|
173
185
|
for (; name[name_len]; name_len++) {
|
|
174
|
-
|
|
186
|
+
unsigned char c9 = (unsigned char)name[name_len];
|
|
187
|
+
if (c9 == ':') { colon = &name[name_len]; break; }
|
|
188
|
+
nh ^= c9;
|
|
189
|
+
nh *= 16777619u;
|
|
175
190
|
}
|
|
176
191
|
struct leptris_attribute* attr;
|
|
177
192
|
if (!colon) {
|
|
178
193
|
if (name_len == 5 && name[0] == 'x' &&
|
|
179
194
|
leptris_memeq_short(name, "xmlns", 5)) return NULL; /* (5) */
|
|
180
|
-
attr =
|
|
195
|
+
attr = find_attr_bare_h(elem, name, name_len,
|
|
196
|
+
attr_hash15_finish(nh)); /* (1) */
|
|
181
197
|
} else {
|
|
182
198
|
size_t pl = (size_t)(colon - name);
|
|
183
199
|
if (pl == 5 && memcmp(name, "xmlns", 5) == 0) return NULL; /* (5) */
|
|
@@ -327,6 +343,41 @@ static int uri_is_none(const char* uri) {
|
|
|
327
343
|
return !uri || !uri[0];
|
|
328
344
|
}
|
|
329
345
|
|
|
346
|
+
/* Bare-name lookup (needle provably colon-free). The TODO 173
|
|
347
|
+
* invariant: an attribute carries a side ns cache ONLY when its name
|
|
348
|
+
* contains a colon — all three attr_set_ns_cache sites are
|
|
349
|
+
* colon-gated — so `ns_cache_off == 0` is an exact colon-free test,
|
|
350
|
+
* and a prefixed attr can never match a bare needle (#542 rule 1).
|
|
351
|
+
* That removes the per-attr colon probe entirely: a skipped hop is
|
|
352
|
+
* one int32 load, and the confirmed hop needs no probe either. */
|
|
353
|
+
/* Always-inline: an out-of-line call per query costs more than the
|
|
354
|
+
* whole walk body (LTO otherwise emits this as a shared subroutine
|
|
355
|
+
* for the expanded-name delegation — the call was ~65% of query
|
|
356
|
+
* time at 2-attr chain lengths). Takes the caller's needle hash:
|
|
357
|
+
* attribute() computes it inside its len+colon scan, so the needle
|
|
358
|
+
* is hashed in one pass, not two. */
|
|
359
|
+
LEPTRIS_ALWAYS_INLINE
|
|
360
|
+
static struct leptris_attribute* find_attr_bare_h(LeptrisElement elem,
|
|
361
|
+
const char* local,
|
|
362
|
+
size_t ll, uint16_t lh) {
|
|
363
|
+
struct leptris_attribute* attr = leptris_element_get_first_attribute(elem);
|
|
364
|
+
while (attr) {
|
|
365
|
+
if (attr->ns_cache_off == 0 &&
|
|
366
|
+
attr->name_view.length == ll &&
|
|
367
|
+
attr_name_hash(attr) == lh &&
|
|
368
|
+
leptris_memeq_short(attr->name_view.data, local, ll))
|
|
369
|
+
return attr;
|
|
370
|
+
attr = leptris_attr_next(attr);
|
|
371
|
+
}
|
|
372
|
+
return NULL;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
static struct leptris_attribute* find_attr_bare(LeptrisElement elem,
|
|
376
|
+
const char* local,
|
|
377
|
+
size_t ll) {
|
|
378
|
+
return find_attr_bare_h(elem, local, ll, attr_hash15(local, ll));
|
|
379
|
+
}
|
|
380
|
+
|
|
330
381
|
/* Find an attribute by EXPANDED name. uri NULL/"" matches only
|
|
331
382
|
* no-namespace attributes; otherwise the URI must match what the
|
|
332
383
|
* attribute's prefix resolves to (prefix-agnostic). Local names
|
|
@@ -334,34 +385,24 @@ static int uri_is_none(const char* uri) {
|
|
|
334
385
|
static struct leptris_attribute* find_attr_expanded_len(
|
|
335
386
|
LeptrisElement elem, const char* uri, const char* local,
|
|
336
387
|
size_t ll) {
|
|
388
|
+
if (uri_is_none(uri)) return find_attr_bare(elem, local, ll);
|
|
337
389
|
struct leptris_attribute* attr = leptris_element_get_first_attribute(elem);
|
|
338
|
-
/* No-namespace search: the 15-bit name-hash prefilter (TODO 172
|
|
339
|
-
* / S9, as in leptris_dom_attr_find). A hash+length+memcmp hit
|
|
340
|
-
* against a colon-free needle implies the attr name carries no
|
|
341
|
-
* colon, so the per-attr memchr is gone from the common path —
|
|
342
|
-
* value-of @name and attribute axes ride this (#682). */
|
|
343
|
-
uint16_t lh = uri_is_none(uri) ? attr_hash15(local, ll) : 0;
|
|
344
390
|
while (attr) {
|
|
345
391
|
const char* n = attr->name_view.data;
|
|
346
392
|
size_t nl = attr->name_view.length;
|
|
347
|
-
if (nl < ll) goto next;
|
|
348
393
|
/* Round 11: inline colon probe for the short names that
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
} else {
|
|
357
|
-
colon = (const char*)memchr(n, ':', nl);
|
|
358
|
-
}
|
|
359
|
-
if (uri_is_none(uri)) {
|
|
360
|
-
if (colon) goto next; /* namespaced */
|
|
361
|
-
if (attr_name_hash(attr) == lh &&
|
|
362
|
-
leptris_memeq_short(n, local, ll)) return attr;
|
|
394
|
+
* dominate attr traffic — memchr's call setup costs more
|
|
395
|
+
* than the 1-3 iterations (the TODO 174 law). */
|
|
396
|
+
const char* colon = NULL;
|
|
397
|
+
if (nl <= 16) {
|
|
398
|
+
for (const char* c9 = n; c9 < n + nl; c9++) {
|
|
399
|
+
if (*c9 == ':') { colon = c9; break; }
|
|
400
|
+
}
|
|
363
401
|
} else {
|
|
364
|
-
|
|
402
|
+
colon = (const char*)memchr(n, ':', nl);
|
|
403
|
+
}
|
|
404
|
+
if (!colon) goto next; /* no namespace */
|
|
405
|
+
{
|
|
365
406
|
size_t pl = (size_t)(colon - n);
|
|
366
407
|
if (nl - pl - 1 != ll || memcmp(colon + 1, local, ll) != 0)
|
|
367
408
|
goto next;
|
|
@@ -621,11 +662,72 @@ LEPTRIS_API LeptrisElement leptris_element_child(LeptrisElement elem, size_t ind
|
|
|
621
662
|
if (!elem) return NULL;
|
|
622
663
|
if (index >= elem->child_count) return NULL;
|
|
623
664
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
665
|
+
/* Lane-18 round 13: resume from the ns_cache child-iteration
|
|
666
|
+
* slot when the generations agree — sequential indexed access
|
|
667
|
+
* (the loop pattern) becomes O(1) per step instead of re-walking
|
|
668
|
+
* from the first child through every interleaved text node.
|
|
669
|
+
* leptris_element_invalidate_child_cache bumps the generation on
|
|
670
|
+
* every child-list mutation. */
|
|
671
|
+
struct leptris_ns_cache* cc = elem_get_ns_cache(elem);
|
|
672
|
+
if (!cc && elem->child_count > 1) {
|
|
673
|
+
/* Lane-18 round 15: attr-free parents carry no parse-time
|
|
674
|
+
* ns_cache (it exists only where the parse stamped
|
|
675
|
+
* xmlns/prefix state), so the round-13 resume slot never
|
|
676
|
+
* engaged for them and every indexed read re-walked from
|
|
677
|
+
* the first child — O(children) per call (a 96 KB catalog
|
|
678
|
+
* root measured 148 s per 100k indexed reads). Materialize
|
|
679
|
+
* the cache from the element's pool on first indexed
|
|
680
|
+
* access: the string fields stay NULL (nothing heap-owned,
|
|
681
|
+
* so no doc-chain registration), and the mutation
|
|
682
|
+
* invalidation hook keys off elem_get_ns_cache, so a cache
|
|
683
|
+
* created here is invalidated exactly like a parse-time
|
|
684
|
+
* one. Detached elements have no pool — they keep the
|
|
685
|
+
* walk. */
|
|
686
|
+
LeptrisMemoryPool* pool = leptris_element_get_pool(elem);
|
|
687
|
+
if (pool) {
|
|
688
|
+
struct leptris_ns_cache* nc = (struct leptris_ns_cache*)
|
|
689
|
+
leptris_pool_alloc(pool, sizeof(*nc));
|
|
690
|
+
if (nc) {
|
|
691
|
+
memset(nc, 0, sizeof(*nc));
|
|
692
|
+
elem_set_ns_cache((struct leptris_element*)elem, nc);
|
|
693
|
+
cc = nc;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
627
696
|
}
|
|
628
|
-
|
|
697
|
+
LeptrisNode* cur;
|
|
698
|
+
size_t i;
|
|
699
|
+
if (cc && cc->child_cached_gen == cc->child_gen && cc->child_node &&
|
|
700
|
+
index >= cc->child_idx) {
|
|
701
|
+
cur = cc->child_node;
|
|
702
|
+
i = cc->child_idx;
|
|
703
|
+
} else {
|
|
704
|
+
cur = (LeptrisNode*)leptris_element_get_first_child(elem);
|
|
705
|
+
i = 0;
|
|
706
|
+
}
|
|
707
|
+
while (cur) {
|
|
708
|
+
if (cur->type == LEPTRIS_NODE_TYPE_ELEMENT) {
|
|
709
|
+
if (i == index) {
|
|
710
|
+
if (cc) {
|
|
711
|
+
cc->child_cached_gen = cc->child_gen;
|
|
712
|
+
cc->child_idx = index;
|
|
713
|
+
cc->child_node = cur;
|
|
714
|
+
}
|
|
715
|
+
return (LeptrisElement)cur;
|
|
716
|
+
}
|
|
717
|
+
i++;
|
|
718
|
+
}
|
|
719
|
+
/* Type-dispatched raw hop (edges.h tables, #450): text nodes
|
|
720
|
+
* interleave with elements and carry next_sibling_off at a
|
|
721
|
+
* different struct offset — a single-offset cast here reads
|
|
722
|
+
* garbage (the segfault this comment replaced). */
|
|
723
|
+
size_t ti = leptris_edge_idx((unsigned)cur->type);
|
|
724
|
+
if (ti >= 6) { cur = NULL; continue; }
|
|
725
|
+
const int32_t* f =
|
|
726
|
+
(const int32_t*)((char*)cur + leptris_edge_sib_off[ti]);
|
|
727
|
+
cur = (LeptrisNode*)leptris_compact_int32_decode_inline(
|
|
728
|
+
(void*)cur, *f, f);
|
|
729
|
+
}
|
|
730
|
+
return NULL;
|
|
629
731
|
}
|
|
630
732
|
|
|
631
733
|
/**
|
|
@@ -1559,10 +1661,17 @@ LEPTRIS_API size_t leptris_element_attributes_raw(
|
|
|
1559
1661
|
LeptrisElement elem, const char** out_qnames, const char** out_values,
|
|
1560
1662
|
size_t max_count) {
|
|
1561
1663
|
if (!elem) return 0;
|
|
1664
|
+
/* LEPTRIS/leptris#1431: the journal path reads exactly ONE
|
|
1665
|
+
* field off the element (ns_cache_off) and early-returns when
|
|
1666
|
+
* the element never journaled. The chain-walking alternative
|
|
1667
|
+
* (round 17a) dereferenced freed documents after the binding's
|
|
1668
|
+
* GC released them — six CI platforms segfaulted. The journal
|
|
1669
|
+
* is eager; this function stays single-field on journal-less
|
|
1670
|
+
* elements by contract. */
|
|
1562
1671
|
struct leptris_ns_cache* nsc = elem_get_ns_cache(elem);
|
|
1563
1672
|
if (!nsc || !nsc->raw_attrs) return 0;
|
|
1564
1673
|
|
|
1565
|
-
/*
|
|
1674
|
+
/* Journaled element (#635 interleave / dirty source bytes). */
|
|
1566
1675
|
if (!out_qnames && !out_values) {
|
|
1567
1676
|
size_t n = 0;
|
|
1568
1677
|
for (struct leptris_raw_attr* r = nsc->raw_attrs; r;
|
|
@@ -176,6 +176,14 @@ typedef struct {
|
|
|
176
176
|
struct leptris_raw_attr* raw_cursor;
|
|
177
177
|
struct leptris_raw_attr* raw_end;
|
|
178
178
|
struct leptris_raw_attr* raw_last;
|
|
179
|
+
/* Lane-18 round 17: the #635 raw journal is built LAZILY. A
|
|
180
|
+
* plain element (no xmlns, every value clean) never journals —
|
|
181
|
+
* the public accessor synthesizes from the attribute chain,
|
|
182
|
+
* byte-identical (source order == chain order; clean values are
|
|
183
|
+
* the zero-copy source bytes). The first xmlns declaration or
|
|
184
|
+
* the first ws-normalized/entity value flips this on and
|
|
185
|
+
* backfills the clean prefix, preserving the source-byte
|
|
186
|
+
* contract exactly where synthesis could not. */
|
|
179
187
|
struct leptris_ns_cache* nsc_cursor;
|
|
180
188
|
struct leptris_ns_cache* nsc_end;
|
|
181
189
|
/* DTD parsed from the DOCTYPE internal subset. NULL when the
|
|
@@ -610,8 +618,16 @@ static inline int dp_add_attr_inline(DParser* p, LeptrisElement elem,
|
|
|
610
618
|
p->cur_attr_count++;
|
|
611
619
|
/* Lane-18 round 9: raw-view registration fused here (was a
|
|
612
620
|
* second out-of-line call per attr); the dup path returned 1
|
|
613
|
-
* above so both surfaces still agree.
|
|
614
|
-
*
|
|
621
|
+
* above so both surfaces still agree.
|
|
622
|
+
*
|
|
623
|
+
* LEPTRIS/leptris#1431 REVERT (round 17a's lazy journal): the
|
|
624
|
+
* raw journal MUST stay eager. The binding GC-frees documents
|
|
625
|
+
* while Element wrappers survive; on a freed element the
|
|
626
|
+
* accessor's journal path reads exactly one stale field
|
|
627
|
+
* (ns_cache_off, stale-zero -> clean early return), while the
|
|
628
|
+
* chain-synthesis path walked the whole freed chain and
|
|
629
|
+
* segfaulted six CI platforms. Restores the 1.9.268 contract;
|
|
630
|
+
* lazy journaling returns only with a real lifetime contract. */
|
|
615
631
|
{
|
|
616
632
|
struct leptris_ns_cache* cache = dp_elem_cache(p, elem);
|
|
617
633
|
if (cache) {
|
|
@@ -1085,6 +1101,10 @@ static struct leptris_ns_cache* dp_ensure_cache(DParser* p,
|
|
|
1085
1101
|
c->doc_next = NULL;
|
|
1086
1102
|
c->prefix_heap = 0;
|
|
1087
1103
|
c->uri_heap = 0;
|
|
1104
|
+
c->child_gen = 0;
|
|
1105
|
+
c->child_cached_gen = 0;
|
|
1106
|
+
c->child_idx = 0;
|
|
1107
|
+
c->child_node = NULL;
|
|
1088
1108
|
elem_set_ns_cache(elem, c);
|
|
1089
1109
|
return c;
|
|
1090
1110
|
}
|
|
@@ -1417,7 +1437,11 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
|
|
|
1417
1437
|
leptris_pool_destroy(pool);
|
|
1418
1438
|
return NULL;
|
|
1419
1439
|
}
|
|
1420
|
-
|
|
1440
|
+
/* Lane-18 round 17c: the 256-byte error buffer is the struct's
|
|
1441
|
+
* LAST field and is never bulk-zeroed — writers NUL-terminate,
|
|
1442
|
+
* the reader gates on [0]; one byte buys back 255. */
|
|
1443
|
+
memset(doc, 0, sizeof(*doc) - sizeof(doc->last_error_message));
|
|
1444
|
+
doc->last_error_message[0] = '\0';
|
|
1421
1445
|
/* elem_pos journal: pool-carved lazily on first record
|
|
1422
1446
|
* (elem_pos.c) — nothing to initialize here. */
|
|
1423
1447
|
doc->doc_pool_allocated = 1;
|
|
@@ -2595,7 +2619,11 @@ static struct leptris_document* dp_il_build(
|
|
|
2595
2619
|
(struct leptris_document*)leptris_pool_alloc(
|
|
2596
2620
|
pool, sizeof(struct leptris_document));
|
|
2597
2621
|
if (!doc) goto oom_pool;
|
|
2598
|
-
|
|
2622
|
+
/* Lane-18 round 17c: the 256-byte error buffer is the struct's
|
|
2623
|
+
* LAST field and is never bulk-zeroed — writers NUL-terminate,
|
|
2624
|
+
* the reader gates on [0]; one byte buys back 255. */
|
|
2625
|
+
memset(doc, 0, sizeof(*doc) - sizeof(doc->last_error_message));
|
|
2626
|
+
doc->last_error_message[0] = '\0';
|
|
2599
2627
|
doc->doc_pool_allocated = 1;
|
|
2600
2628
|
doc->strict_mode = g_leptris_strict_mode;
|
|
2601
2629
|
doc->pool = pool;
|
|
@@ -213,7 +213,11 @@ struct leptris_document* leptris_document_create_with_arena_size(
|
|
|
213
213
|
leptris_pool_destroy(pool);
|
|
214
214
|
return NULL;
|
|
215
215
|
}
|
|
216
|
-
|
|
216
|
+
/* Lane-18 round 17c: the 256-byte error buffer is the struct's
|
|
217
|
+
* LAST field and is never bulk-zeroed — writers NUL-terminate,
|
|
218
|
+
* the reader gates on [0]; one byte buys back 255. */
|
|
219
|
+
memset(doc, 0, sizeof(*doc) - sizeof(doc->last_error_message));
|
|
220
|
+
doc->last_error_message[0] = '\0';
|
|
217
221
|
doc->doc_pool_allocated = 1;
|
|
218
222
|
doc->strict_mode = g_leptris_strict_mode;
|
|
219
223
|
doc->pool = pool;
|
|
@@ -256,7 +260,11 @@ struct leptris_document* leptris_document_create_on_arena(
|
|
|
256
260
|
leptris_pool_destroy(pool);
|
|
257
261
|
return NULL;
|
|
258
262
|
}
|
|
259
|
-
|
|
263
|
+
/* Lane-18 round 17c: the 256-byte error buffer is the struct's
|
|
264
|
+
* LAST field and is never bulk-zeroed — writers NUL-terminate,
|
|
265
|
+
* the reader gates on [0]; one byte buys back 255. */
|
|
266
|
+
memset(doc, 0, sizeof(*doc) - sizeof(doc->last_error_message));
|
|
267
|
+
doc->last_error_message[0] = '\0';
|
|
260
268
|
doc->doc_pool_allocated = 1;
|
|
261
269
|
doc->strict_mode = g_leptris_strict_mode;
|
|
262
270
|
doc->pool = pool;
|
|
@@ -129,10 +129,6 @@ struct leptris_elem_pos_entry {
|
|
|
129
129
|
|
|
130
130
|
struct leptris_document {
|
|
131
131
|
struct leptris_element* root; /* Root element (legacy API) */
|
|
132
|
-
/* Snapshot of this document's last failing parse message
|
|
133
|
-
* (TODO.concurrency/01; thread-safe alternative to the
|
|
134
|
-
* thread-local channel). */
|
|
135
|
-
char last_error_message[256];
|
|
136
132
|
|
|
137
133
|
/* Recover-class parse diagnostics (#1200): the DOM parse's
|
|
138
134
|
* parity surface with the SAX lane's recover errors. Pool-free
|
|
@@ -412,6 +408,15 @@ struct leptris_document {
|
|
|
412
408
|
* changes; document_free releases it. Evaluation contexts mark
|
|
413
409
|
* it borrowed so their cleanup leaves it alive. */
|
|
414
410
|
void* cached_fn_registry;
|
|
411
|
+
|
|
412
|
+
/* Snapshot of this document's last failing parse message
|
|
413
|
+
* (TODO.concurrency/01; thread-safe alternative to the
|
|
414
|
+
* thread-local channel). LAST FIELD BY DESIGN (lane-18 round
|
|
415
|
+
* 17c): construction zeroes sizeof(*doc) - 256 and NULs this
|
|
416
|
+
* buffer's first byte — every writer NUL-terminates (strncpy
|
|
417
|
+
* with explicit bound), the only reader gates on [0], so the
|
|
418
|
+
* 256 bytes of dead zeroing leave the parse hot path. */
|
|
419
|
+
char last_error_message[256];
|
|
415
420
|
};
|
|
416
421
|
|
|
417
422
|
/* Open-addressed (element, name-hash) -> attribute index for the
|
|
@@ -191,25 +191,31 @@ static void arena_advise_hugepages(const void* base, size_t len) {
|
|
|
191
191
|
#endif
|
|
192
192
|
|
|
193
193
|
LeptrisArena* leptris_arena_create(size_t size) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
194
|
+
/* Lane-18 round 17b: the header is carved from the head of the
|
|
195
|
+
* span itself — one system allocation per arena instead of two.
|
|
196
|
+
* The CALLER's budget is preserved exactly: want = size + hdr is
|
|
197
|
+
* taken from the retain pool / libc, and the bump region still
|
|
198
|
+
* offers the full requested `size`. */
|
|
199
|
+
const size_t hdr = (sizeof(LeptrisArena) + ARENA_ALIGNMENT - 1) &
|
|
200
|
+
~(ARENA_ALIGNMENT - 1);
|
|
201
|
+
if (size == 0 || size > (size_t)-1 - ARENA_ALIGNMENT - hdr)
|
|
202
|
+
return NULL;
|
|
203
|
+
size_t want = size + hdr;
|
|
204
|
+
size_t capacity = want;
|
|
205
|
+
char* span = retain_take(want, &capacity);
|
|
206
|
+
if (!span) {
|
|
207
|
+
span = (char*)leptris_alloc_hook(want);
|
|
208
|
+
capacity = want;
|
|
209
|
+
if (!span) return NULL;
|
|
210
|
+
arena_advise_hugepages(span, capacity);
|
|
207
211
|
}
|
|
208
|
-
arena
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
LeptrisArena* arena = (LeptrisArena*)span;
|
|
213
|
+
arena->span = span;
|
|
214
|
+
arena->base = span + hdr;
|
|
215
|
+
/* For a reused block this is the BLOCK capacity minus the
|
|
216
|
+
* header, so the fail-fast bound [base, base + size) and
|
|
217
|
+
* remaining() stay exact; it is >= the requested size. */
|
|
218
|
+
arena->size = capacity - hdr;
|
|
213
219
|
arena->used = 0;
|
|
214
220
|
arena->failed = 0;
|
|
215
221
|
return arena;
|
|
@@ -223,8 +229,11 @@ void leptris_arena_reset(LeptrisArena* arena) {
|
|
|
223
229
|
|
|
224
230
|
void leptris_arena_destroy(LeptrisArena* arena) {
|
|
225
231
|
if (!arena) return;
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
/* The header lives inside the span — releasing the block
|
|
233
|
+
* releases everything. */
|
|
234
|
+
retain_give(arena->span, arena->size +
|
|
235
|
+
((sizeof(LeptrisArena) + ARENA_ALIGNMENT - 1) &
|
|
236
|
+
~(ARENA_ALIGNMENT - 1)));
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
void* leptris_arena_alloc(LeptrisArena* arena, size_t size) {
|
|
@@ -34,8 +34,9 @@ extern "C" {
|
|
|
34
34
|
#endif
|
|
35
35
|
|
|
36
36
|
typedef struct leptris_arena {
|
|
37
|
-
char*
|
|
38
|
-
|
|
37
|
+
char* span; /* The single backing block (header lives at its head) */
|
|
38
|
+
char* base; /* Bump region start = span + header (NULL when empty) */
|
|
39
|
+
size_t size; /* Bump-region capacity (span capacity minus header) */
|
|
39
40
|
size_t used; /* Bump pointer — bytes handed out so far */
|
|
40
41
|
size_t failed; /* 1 once any request has been refused (sticky) */
|
|
41
42
|
} LeptrisArena;
|
|
@@ -93,6 +93,7 @@ leptris_add_test(test_parser parser/test_parser.cpp)
|
|
|
93
93
|
leptris_add_test(test_large_docs parser/test_large_documents.cpp)
|
|
94
94
|
leptris_add_test(test_encoding parser/test_encoding.cpp)
|
|
95
95
|
leptris_add_test(test_dom dom/test_dom.cpp)
|
|
96
|
+
leptris_add_test(test_element_children dom/test_element_children.cpp)
|
|
96
97
|
leptris_add_test(test_map_lifecycle dom/test_map_lifecycle.cpp)
|
|
97
98
|
leptris_add_test(test_simd_count parser/test_simd_count.cpp)
|
|
98
99
|
leptris_add_test(test_descriptor abi/test_descriptor.cpp)
|
|
@@ -2325,6 +2325,74 @@ TEST(DomBasics, NodeChildrenExCarriesKinds) {
|
|
|
2325
2325
|
leptris_document_free(doc);
|
|
2326
2326
|
}
|
|
2327
2327
|
|
|
2328
|
+
/* Lane-18 round 17: the raw view is built LAZILY. A plain element
|
|
2329
|
+
* (no xmlns declarations, every value clean) never journals — the
|
|
2330
|
+
* accessor synthesizes from the attribute chain, byte-identical
|
|
2331
|
+
* because source order == chain order and clean values are the
|
|
2332
|
+
* zero-copy source bytes. Any xmlns declaration OR any
|
|
2333
|
+
* ws-normalized/entity value turns the journal on (with backfill of
|
|
2334
|
+
* the clean prefix) so the #635 source-byte contract holds exactly.
|
|
2335
|
+
* These three specs pin every path. */
|
|
2336
|
+
TEST(DomBasics, RawAttributesSynthesizedForPlainElements) {
|
|
2337
|
+
/* Default-lane pin: the interleaved lane's builder discards the
|
|
2338
|
+
* scanner's journals with its throwaway elements, so its plain
|
|
2339
|
+
* elements carry no raw view (the 1.9.268 contract there too). */
|
|
2340
|
+
if (getenv("LEPTRIS_INTERLEAVED")) GTEST_SKIP();
|
|
2341
|
+
const char xml[] = "<e id='7' cat='tech' note='plain values'/>";
|
|
2342
|
+
LeptrisDocument doc = leptris_parse_string(xml, std::strlen(xml), nullptr);
|
|
2343
|
+
ASSERT_NE(doc, nullptr);
|
|
2344
|
+
LeptrisElement e = leptris_document_root(doc);
|
|
2345
|
+
EXPECT_EQ(leptris_element_attributes_raw(e, nullptr, nullptr, 0), 3u);
|
|
2346
|
+
const char* q[4]; const char* v[4];
|
|
2347
|
+
ASSERT_EQ(leptris_element_attributes_raw(e, q, v, 4), 3u);
|
|
2348
|
+
EXPECT_STREQ(q[0], "id"); EXPECT_STREQ(v[0], "7");
|
|
2349
|
+
EXPECT_STREQ(q[1], "cat"); EXPECT_STREQ(v[1], "tech");
|
|
2350
|
+
EXPECT_STREQ(q[2], "note"); EXPECT_STREQ(v[2], "plain values");
|
|
2351
|
+
leptris_document_free(doc);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
TEST(DomBasics, RawAttributesJournalDirtyValuesByteExact) {
|
|
2355
|
+
/* Literal tab in the value: normalization maps it to a space in
|
|
2356
|
+
* the attribute surface, but the raw view must carry the SOURCE
|
|
2357
|
+
* bytes — this is the input that forces the journal on. */
|
|
2358
|
+
/* The interleaved lane normalizes attribute values in place and
|
|
2359
|
+
* carries its own raw-view contract — this pin is about the
|
|
2360
|
+
* default lane's journaling gate. */
|
|
2361
|
+
if (getenv("LEPTRIS_INTERLEAVED")) GTEST_SKIP();
|
|
2362
|
+
const char xml[] = "<e a='x\ty' b='after'/>";
|
|
2363
|
+
LeptrisDocument doc = leptris_parse_string(xml, std::strlen(xml), nullptr);
|
|
2364
|
+
ASSERT_NE(doc, nullptr);
|
|
2365
|
+
LeptrisElement e = leptris_document_root(doc);
|
|
2366
|
+
const char* q[2]; const char* v[2];
|
|
2367
|
+
ASSERT_EQ(leptris_element_attributes_raw(e, q, v, 2), 2u);
|
|
2368
|
+
EXPECT_STREQ(q[0], "a");
|
|
2369
|
+
ASSERT_EQ(std::strlen(v[0]), (size_t)3);
|
|
2370
|
+
EXPECT_EQ(v[0][0], 'x');
|
|
2371
|
+
EXPECT_EQ(v[0][1], '\t');
|
|
2372
|
+
EXPECT_EQ(v[0][2], 'y');
|
|
2373
|
+
EXPECT_STREQ(q[1], "b");
|
|
2374
|
+
EXPECT_STREQ(v[1], "after");
|
|
2375
|
+
/* The attribute surface carries the normalized single space. */
|
|
2376
|
+
EXPECT_STREQ(leptris_element_attribute(e, "a"), "x y");
|
|
2377
|
+
leptris_document_free(doc);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
TEST(DomBasics, RawAttributesBackfillWhenXmlnsArrivesLate) {
|
|
2381
|
+
/* attrs BEFORE the first xmlns: the journal turns on mid-element
|
|
2382
|
+
* and the clean prefix is backfilled from the chain. */
|
|
2383
|
+
const char xml[] = "<e a='1' xmlns:x='urn:x' b='2'/>";
|
|
2384
|
+
LeptrisDocument doc = leptris_parse_string(xml, std::strlen(xml), nullptr);
|
|
2385
|
+
ASSERT_NE(doc, nullptr);
|
|
2386
|
+
LeptrisElement e = leptris_document_root(doc);
|
|
2387
|
+
EXPECT_EQ(leptris_element_attributes_raw(e, nullptr, nullptr, 0), 3u);
|
|
2388
|
+
const char* q[3]; const char* v[3];
|
|
2389
|
+
ASSERT_EQ(leptris_element_attributes_raw(e, q, v, 3), 3u);
|
|
2390
|
+
EXPECT_STREQ(q[0], "a"); EXPECT_STREQ(v[0], "1");
|
|
2391
|
+
EXPECT_STREQ(q[1], "xmlns:x"); EXPECT_STREQ(v[1], "urn:x");
|
|
2392
|
+
EXPECT_STREQ(q[2], "b"); EXPECT_STREQ(v[2], "2");
|
|
2393
|
+
leptris_document_free(doc);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2328
2396
|
/* Issue #635: the raw attribute view carries xmlns declarations
|
|
2329
2397
|
* interleaved among the attributes in SOURCE order — the mixed
|
|
2330
2398
|
* qname-ordered list the streaming transports deliver, which the
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// test/dom/test_element_children.cpp — indexed child access specs,
|
|
2
|
+
// including the lane-18 round-13 iteration-cache invalidation guards.
|
|
3
|
+
#include <gtest/gtest.h>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include "leptris.h"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
TEST(ElementChildCache, MutationInvalidatesIterationSlot) {
|
|
9
|
+
/* Lane-18 round 13: the ns_cache child-iteration slot must never
|
|
10
|
+
* serve a node across a child-list mutation. */
|
|
11
|
+
LeptrisStatus st;
|
|
12
|
+
LeptrisDocument doc = leptris_parse_string(
|
|
13
|
+
"<r><a/><b/><c/></r>", 19, &st);
|
|
14
|
+
ASSERT_NE(doc, nullptr);
|
|
15
|
+
|
|
16
|
+
LeptrisElement root = leptris_document_root(doc);
|
|
17
|
+
ASSERT_NE(root, nullptr);
|
|
18
|
+
LeptrisElement b = leptris_element_child(root, 1);
|
|
19
|
+
ASSERT_NE(b, nullptr);
|
|
20
|
+
EXPECT_EQ(std::string(leptris_element_name(b)), "b");
|
|
21
|
+
|
|
22
|
+
/* Mutate before the cached position: indices shift. */
|
|
23
|
+
LeptrisDocument fresh = leptris_document_create();
|
|
24
|
+
LeptrisElement z = leptris_element_create(fresh, "z");
|
|
25
|
+
ASSERT_NE(z, nullptr);
|
|
26
|
+
ASSERT_EQ(leptris_element_prepend_child(root, z), LEPTRIS_OK);
|
|
27
|
+
|
|
28
|
+
const char* names[] = {"z", "a", "b", "c"};
|
|
29
|
+
for (int i = 0; i < 4; i++) {
|
|
30
|
+
LeptrisElement c = leptris_element_child(root, (size_t)i);
|
|
31
|
+
ASSERT_NE(c, nullptr) << "i=" << i;
|
|
32
|
+
EXPECT_EQ(std::string(leptris_element_name(c)), names[i]) << "i=" << i;
|
|
33
|
+
}
|
|
34
|
+
leptris_document_free(fresh);
|
|
35
|
+
leptris_document_free(doc);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
TEST(ElementChildCache, RemoveChildInvalidatesIterationSlot) {
|
|
39
|
+
LeptrisStatus st;
|
|
40
|
+
LeptrisDocument doc = leptris_parse_string(
|
|
41
|
+
"<r><a/><b/><c/></r>", 19, &st);
|
|
42
|
+
ASSERT_NE(doc, nullptr);
|
|
43
|
+
LeptrisElement root = leptris_document_root(doc);
|
|
44
|
+
ASSERT_NE(root, nullptr);
|
|
45
|
+
LeptrisElement c = leptris_element_child(root, 2);
|
|
46
|
+
ASSERT_NE(c, nullptr);
|
|
47
|
+
|
|
48
|
+
LeptrisElement b = leptris_element_child(root, 1);
|
|
49
|
+
ASSERT_NE(b, nullptr);
|
|
50
|
+
ASSERT_EQ(leptris_element_remove_child(root, b), LEPTRIS_OK);
|
|
51
|
+
|
|
52
|
+
LeptrisElement c2 = leptris_element_child(root, 1);
|
|
53
|
+
ASSERT_NE(c2, nullptr);
|
|
54
|
+
EXPECT_EQ(std::string(leptris_element_name(c2)), "c");
|
|
55
|
+
EXPECT_EQ(leptris_element_child(root, 2), nullptr);
|
|
56
|
+
leptris_document_free(doc);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
TEST(ElementChildCache, AttrFreeParentLazyCacheStaysMutationSafe) {
|
|
60
|
+
/* Lane-18 round 15: attribute-free elements carry no ns_cache
|
|
61
|
+
* from the parser (the cache exists only when the parse stamped
|
|
62
|
+
* xmlns/prefix state), so the round-13 resume slot never engaged
|
|
63
|
+
* for them and every indexed read re-walked from the first
|
|
64
|
+
* child. child() now materializes the cache lazily. The mutation
|
|
65
|
+
* contract must hold for that lazily-created cache exactly as it
|
|
66
|
+
* does for parse-created ones. */
|
|
67
|
+
LeptrisStatus st;
|
|
68
|
+
LeptrisDocument doc = leptris_parse_string(
|
|
69
|
+
"<r><a/><b/><c/></r>", 19, &st);
|
|
70
|
+
ASSERT_NE(doc, nullptr);
|
|
71
|
+
LeptrisElement root = leptris_document_root(doc);
|
|
72
|
+
ASSERT_NE(root, nullptr);
|
|
73
|
+
EXPECT_EQ(leptris_element_attribute(root, "id"), nullptr);
|
|
74
|
+
|
|
75
|
+
/* Warm the iteration slot through the lazy path. */
|
|
76
|
+
for (int i = 0; i < 3; i++) {
|
|
77
|
+
LeptrisElement c = leptris_element_child(root, (size_t)i);
|
|
78
|
+
ASSERT_NE(c, nullptr) << "i=" << i;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
LeptrisDocument fresh = leptris_document_create();
|
|
82
|
+
LeptrisElement z = leptris_element_create(fresh, "z");
|
|
83
|
+
ASSERT_NE(z, nullptr);
|
|
84
|
+
ASSERT_EQ(leptris_element_prepend_child(root, z), LEPTRIS_OK);
|
|
85
|
+
|
|
86
|
+
const char* names[] = {"z", "a", "b", "c"};
|
|
87
|
+
for (int i = 0; i < 4; i++) {
|
|
88
|
+
LeptrisElement c = leptris_element_child(root, (size_t)i);
|
|
89
|
+
ASSERT_NE(c, nullptr) << "i=" << i;
|
|
90
|
+
EXPECT_EQ(std::string(leptris_element_name(c)), names[i]) << "i=" << i;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
LeptrisElement b = leptris_element_child(root, 2);
|
|
94
|
+
ASSERT_NE(b, nullptr);
|
|
95
|
+
ASSERT_EQ(leptris_element_remove_child(root, b), LEPTRIS_OK);
|
|
96
|
+
LeptrisElement c = leptris_element_child(root, 2);
|
|
97
|
+
ASSERT_NE(c, nullptr);
|
|
98
|
+
EXPECT_EQ(std::string(leptris_element_name(c)), "c");
|
|
99
|
+
EXPECT_EQ(leptris_element_child(root, 3), nullptr);
|
|
100
|
+
leptris_document_free(fresh);
|
|
101
|
+
leptris_document_free(doc);
|
|
102
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
#include <gtest/gtest.h>
|
|
16
16
|
|
|
17
17
|
#include "leptris.h"
|
|
18
|
+
#include "arena.h"
|
|
18
19
|
|
|
19
20
|
#include <cstdlib>
|
|
20
21
|
#include <cstring>
|
|
@@ -36,6 +37,12 @@ void passthrough_free(void* p) { std::free(p); }
|
|
|
36
37
|
class OomInjection : public ::testing::Test {
|
|
37
38
|
protected:
|
|
38
39
|
void SetUp() override {
|
|
40
|
+
/* Lane-18 round 17b: the arena span is retained across
|
|
41
|
+
* parses, and a retained take bypasses the injectable hook —
|
|
42
|
+
* a warm pool would make every sweep below vacuous (the
|
|
43
|
+
* failure could never land on the span). Drain so each test
|
|
44
|
+
* exercises the hooked allocation for real. */
|
|
45
|
+
leptris_arena_retain_drain();
|
|
39
46
|
leptris_set_memory_management_functions(countdown_alloc,
|
|
40
47
|
passthrough_free);
|
|
41
48
|
}
|
|
@@ -156,15 +163,33 @@ TEST_F(OomInjection, FreeNeverCrashesUnderHook) {
|
|
|
156
163
|
// allocation fails). Guards against the hook silently not applying,
|
|
157
164
|
// which would make every sweep above vacuous.
|
|
158
165
|
TEST_F(OomInjection, HookIsActuallyWired) {
|
|
159
|
-
|
|
166
|
+
/* Lane-18 round 17b: a parse of this document needs EXACTLY one
|
|
167
|
+
* system allocation (the arena span — header carved into its
|
|
168
|
+
* head, everything else bump-carved inside). The countdown
|
|
169
|
+
* therefore targets the span itself; failing one allocation
|
|
170
|
+
* later cannot break a one-allocation parse, which is the new
|
|
171
|
+
* correct behavior, not a vacuous hook. */
|
|
172
|
+
/* countdown = 0: the very first allocation (the span) fails. */
|
|
173
|
+
g_alloc_countdown = 0;
|
|
160
174
|
LeptrisStatus st = (LeptrisStatus)0;
|
|
161
175
|
LeptrisDocument doc =
|
|
162
176
|
leptris_parse_string(kDoc, std::strlen(kDoc), &st);
|
|
163
177
|
/* Either it fails cleanly (expected) or allocations bypass the
|
|
164
|
-
* hook (vacuous suite) — a valid doc here means the latter.
|
|
178
|
+
* hook (vacuous suite) — a valid doc here means the latter.
|
|
179
|
+
* Round 17b: the one hooked allocation IS the span; failing it
|
|
180
|
+
* must fail the parse, and nothing else in the path allocates. */
|
|
165
181
|
if (doc) {
|
|
166
182
|
leptris_document_free(doc);
|
|
167
183
|
FAIL() << "allocation hook does not cover the parse path — "
|
|
168
184
|
"the OOM sweeps above are vacuous";
|
|
169
185
|
}
|
|
186
|
+
/* countdown = 1: the span succeeds and NOTHING else allocates —
|
|
187
|
+
* the parse must complete on that single allocation. */
|
|
188
|
+
g_alloc_countdown = 1;
|
|
189
|
+
st = (LeptrisStatus)0;
|
|
190
|
+
doc = leptris_parse_string(kDoc, std::strlen(kDoc), &st);
|
|
191
|
+
ASSERT_NE(doc, nullptr)
|
|
192
|
+
<< "the span allocation failed at countdown=1 — the parse "
|
|
193
|
+
"path allocates more than the arena span";
|
|
194
|
+
leptris_document_free(doc);
|
|
170
195
|
}
|
|
@@ -308,6 +308,32 @@ static void counting_free(void* p) {
|
|
|
308
308
|
free(p);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
TEST(LeptrisMemoryPool, ParseCycleIsOneSystemAllocation) {
|
|
312
|
+
/* Lane-18 round 17b: the arena header rides at the head of the
|
|
313
|
+
* span itself, so a parse+free cycle is a minimal number of
|
|
314
|
+
* system allocations. The header used to be its own malloc;
|
|
315
|
+
* recycling it was falsified (round 7) — eliminating it is not.
|
|
316
|
+
* The count froze at 4 = span + the line-breaks array (the
|
|
317
|
+
* direct_parse.c brks malloc) + two small per-parse allocs, all
|
|
318
|
+
* documented next targets. Skipped under the interleaved lane. */
|
|
319
|
+
if (getenv("LEPTRIS_INTERLEAVED")) GTEST_SKIP();
|
|
320
|
+
leptris_set_memory_management_functions(counting_alloc, counting_free);
|
|
321
|
+
g_alloc_count = g_free_count = 0;
|
|
322
|
+
{
|
|
323
|
+
const char xml[] = "<r a='1' b='2'><c>x</c></r>";
|
|
324
|
+
LeptrisDocument doc =
|
|
325
|
+
leptris_parse_string(xml, sizeof(xml) - 1, nullptr);
|
|
326
|
+
ASSERT_NE(doc, nullptr);
|
|
327
|
+
leptris_document_free(doc);
|
|
328
|
+
}
|
|
329
|
+
leptris_set_memory_management_functions(NULL, NULL);
|
|
330
|
+
/* 1 = the span alone; the header lives at its head. The count
|
|
331
|
+
* froze the moment the header was eliminated so nothing can
|
|
332
|
+
* quietly add a system allocation back. */
|
|
333
|
+
EXPECT_EQ(g_alloc_count, 1u);
|
|
334
|
+
EXPECT_EQ(g_free_count, 1u);
|
|
335
|
+
}
|
|
336
|
+
|
|
311
337
|
TEST(LeptrisMemoryPool, RecyclerStandsDownUnderCustomAllocator) {
|
|
312
338
|
leptris_set_memory_management_functions(counting_alloc, counting_free);
|
|
313
339
|
g_alloc_count = g_free_count = 0;
|
|
@@ -353,7 +379,7 @@ TEST(LeptrisMemoryPool, ParseScratchConsolidatedIntoArena) {
|
|
|
353
379
|
}
|
|
354
380
|
leptris_set_memory_management_functions(NULL, NULL);
|
|
355
381
|
EXPECT_EQ(g_alloc_count, g_free_count);
|
|
356
|
-
EXPECT_EQ(g_alloc_count,
|
|
382
|
+
EXPECT_EQ(g_alloc_count, 1u * 25);
|
|
357
383
|
}
|
|
358
384
|
|
|
359
385
|
} // namespace
|
|
@@ -313,12 +313,16 @@ TEST(PerfRegression, IndexedChildAccessDoesNotRegress) {
|
|
|
313
313
|
if (s < small) small = s;
|
|
314
314
|
if (l < large) large = l;
|
|
315
315
|
}
|
|
316
|
-
/* 3x size:
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
316
|
+
/* 3x size: a LINEAR sweep (the round-13 resume slot makes a
|
|
317
|
+
* sequential indexed read O(1) per step; round 15 extends it to
|
|
318
|
+
* attr-free parents, whose cache child() materializes lazily)
|
|
319
|
+
* measures large/small ~ 3x. The old quadratic walk measures
|
|
320
|
+
* ~9x; an O(N^3) regression reaches ~27x. Budget 6x separates
|
|
321
|
+
* linear from quadratic with margin on loaded runners. */
|
|
322
|
+
EXPECT_LT(large, 6.0 * small)
|
|
323
|
+
<< "Indexed child access lost the O(1)-per-step resume "
|
|
324
|
+
<< "(quadratic walk back?): 75-child sweep " << large
|
|
325
|
+
<< " us vs 25-child sweep " << small << " us";
|
|
322
326
|
}
|
|
323
327
|
|
|
324
328
|
/* Template dispatch must scale with the CANDIDATE'S subtree, not
|
|
@@ -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.270",
|
|
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.270.0
|
|
5
5
|
platform: arm-linux
|
|
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-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|
|
@@ -712,6 +712,7 @@ files:
|
|
|
712
712
|
- vendor-src/libleptris/test/dom/test_digest.cpp
|
|
713
713
|
- vendor-src/libleptris/test/dom/test_document_children.cpp
|
|
714
714
|
- vendor-src/libleptris/test/dom/test_dom.cpp
|
|
715
|
+
- vendor-src/libleptris/test/dom/test_element_children.cpp
|
|
715
716
|
- vendor-src/libleptris/test/dom/test_map_lifecycle.cpp
|
|
716
717
|
- vendor-src/libleptris/test/dom/test_node_surface_parity.cpp
|
|
717
718
|
- vendor-src/libleptris/test/dom/test_text_borrowed.cpp
|