leptris 1.9.225.0 → 1.9.228.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/Rakefile +1 -1
  4. data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
  5. data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
  6. data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
  7. data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
  8. data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
  9. data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
  10. data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
  11. data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
  12. data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
  13. data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
  14. data/lib/leptris/version.rb +1 -1
  15. data/vendor-src/README.md +1 -1
  16. data/vendor-src/libleptris/CHANGELOG.md +82 -0
  17. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  18. data/vendor-src/libleptris/src/leptris/dom/cdata.c +1 -2
  19. data/vendor-src/libleptris/src/leptris/dom/comment.c +0 -1
  20. data/vendor-src/libleptris/src/leptris/dom/compact.c +16 -0
  21. data/vendor-src/libleptris/src/leptris/dom/compact.h +9 -0
  22. data/vendor-src/libleptris/src/leptris/dom/element.h +6 -7
  23. data/vendor-src/libleptris/src/leptris/dom/entity_ref.c +0 -1
  24. data/vendor-src/libleptris/src/leptris/dom/node.h +3 -5
  25. data/vendor-src/libleptris/src/leptris/dom/node_public.c +74 -3
  26. data/vendor-src/libleptris/src/leptris/dom/pi.c +0 -1
  27. data/vendor-src/libleptris/src/leptris/dom/text.c +2 -5
  28. data/vendor-src/libleptris/src/leptris/dom/text.h +23 -1
  29. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +21 -1
  30. data/vendor-src/libleptris/src/leptris/html/html_parse.c +37 -2
  31. data/vendor-src/libleptris/src/leptris/leptris.c +5 -0
  32. data/vendor-src/libleptris/src/leptris/leptris_internal.h +9 -0
  33. data/vendor-src/libleptris/test/CMakeLists.txt +1 -0
  34. data/vendor-src/libleptris/test/abi/test_header_hygiene.cpp +7 -0
  35. data/vendor-src/libleptris/test/dom/test_dom.cpp +45 -0
  36. data/vendor-src/libleptris/test/dom/test_text_borrowed.cpp +13 -10
  37. data/vendor-src/libleptris/test/html/test_html_borrowed_names.cpp +143 -0
  38. data/vendor-src/libleptris/test/parser/test_interleaved.cpp +74 -8
  39. data/vendor-src/libleptris/vcpkg.json +1 -1
  40. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a65d81fa73df8297d35b8e095e69c656d4ff8819620a40bd2c173ae685727452
4
- data.tar.gz: de25ee0a34c202432ccbc870a5a40b689000827902851d4387c1a1f98aebfa80
3
+ metadata.gz: fa53c1b21f30aa89e5992390c40deb360eb8d94d0000ca5346e381b35b086f87
4
+ data.tar.gz: e667ca695bb8b10ecd251ad03d77521adacf6829cdd5bac7ec9c4f9bcb5b42a0
5
5
  SHA512:
6
- metadata.gz: 2a61094f9eff1615f87cccb47791ecb899b8d2ca1a23afd4579238bbc9cf3afea51220a82ad5a6556ae21462a7f928c008277da2d10045e41c52a8ed6422d76f
7
- data.tar.gz: 6833ca765302ade1e2874962bb86ae419b17671caa75e556b7324636b7930b5c568f1dc759abd7e6767388b36a3c1ddab89c539f263c1a139c303ef1683754f4
6
+ metadata.gz: 2927fc583597d9c607816ad16cb293a73c9ab7647a57b77429b8efbf8124b6e7ff52b871c25825b826d3a70501dbd51d4cc4b628c4992ea916825bd3f42845fa
7
+ data.tar.gz: a4a6371fa8e9e461db91e4705308b6ab9bc1920d08a76cee7d32f964f75bf9c5e9d16751359e6159b39fe8c0d3a57f0a6ffe5560806114f5d36b142385cd9204
data/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ 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.228.0] - 2026-09-23
9
+
10
+ ### Fixed — Text#content= on created-but-unattached text nodes (#318; engine 1.9.227–228)
11
+
12
+ - **The #318 regression is fixed at the engine root**: engine
13
+ 1.9.228 (upstream leptris#1320) gives detached text nodes the
14
+ same #519 owner treatment PI/comment/CDATA already had — an
15
+ owner backref stamped by `leptris_text_node_create` (compact
16
+ int32 offset, LP64 layout preserved) — so
17
+ `leptris_text_node_set_content` works before attach again.
18
+ The create → fill → attach builder sequence moxml's
19
+ `Text#content=` depends on is unblocked; verified against the
20
+ vendored 1.9.228 (the #318 repro passes; suite 778 → 781/0).
21
+ - 1.9.227 rides along (header-hygiene spec zeroing).
22
+
23
+ ## [1.9.226.0] - 2026-09-23
24
+
25
+ ### Performance — engine sync (1.9.226)
26
+
27
+ - Vendored libleptris 1.9.226: zero-re-copy HTML element opens
28
+ (#1285 slice 5, #1315) — element names drop from two pool
29
+ allocations + copies per open to one (the lowercased pooled
30
+ name or the static literal of a synthesized open, stored
31
+ directly; no QName split, no colon re-copy). Table-row parse
32
+ 33.1 → 35.3 MB/s on the contended host. No binding-facing
33
+ surface changes; audit 351/351, suite 781/0.
34
+
8
35
  ## [1.9.225.0] - 2026-09-22
9
36
 
10
37
  ### Added — native HTML serialization, construction, and the bulk SAX drain (engine 1.9.223–225)
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.225"
11
+ LIBLEPTRIS_VERSION = "1.9.228"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.225.0"
4
+ VERSION = "1.9.228.0"
5
5
  end
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.225
4
+ - libleptris v1.9.228
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,87 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.228] - 2026-09-23
4
+
5
+ ### Fixed
6
+
7
+ - **dom: `leptris_text_node_set_content` works again on
8
+ created-but-unattached text nodes (#1320).** The 1.9.223–1.9.226
9
+ window returned `LEPTRIS_ERROR_INVALID_ARG` for exactly the
10
+ create → fill → attach builder sequence bindings use (moxml's
11
+ `Text#content=` failed 5 shared examples). Root cause: issue #519
12
+ gave detached PI/comment/CDATA nodes an `owner_doc` field but
13
+ skipped text — at the time text nodes carried their own pool
14
+ backref, which #1285 slice 4 then deleted, leaving detached text
15
+ with no document to allocate from. Text now gets the same #519
16
+ treatment: an owner backref stamped by the public creator only,
17
+ stored as a compact int32 offset (the LP64 struct keeps its 40
18
+ bytes — the field lands in existing padding; ILP32 32), with
19
+ overflow-table fallback tagged with the EXPLICIT document (the
20
+ parse-context TLS slot holds the wrong doc or NULL outside parse).
21
+ Every parse-carve path zero-initializes the field; the
22
+ attached/parsed parent-walk fast path is untouched. TDD'd
23
+ red-first against the reported minimal repro;
24
+ `DetachedNodes.TextSetContentWorksBeforeAttach` pins the
25
+ unattached, attach-then-set, and parsed paths.
26
+
27
+
28
+
29
+ ## [1.9.227] - 2026-09-23
30
+
31
+ ### Fixed
32
+
33
+ - **test:** the header-hygiene compact-pointer edge specs zero their
34
+ fake stack-allocated nodes before use. The `last_child` and
35
+ `last_attribute` getters *walk* the sibling/attribute list, so the
36
+ uninitialized fake node's next-edge was read as stack garbage —
37
+ zero on arm64, nonzero on x86_64, where the slice-3b layout shift
38
+ surfaced the latent UB as a real CI failure. It was always UB.
39
+
40
+ ### Performance
41
+
42
+ - **dom: binding_wrapper out of the node base — element 48B, text
43
+ 40B, every node type −8B (#1285 slice 3b).** `LeptrisNode` itself
44
+ drops 24 → 12 bytes; the element struct reaches the lane's stated
45
+ goal of 48 bytes. The `binding_wrapper` field (#262) was never
46
+ adopted — zero callers in leptris-ruby and leptris-py (both cache
47
+ wrappers on their own side), making it pure dead weight at 8 bytes
48
+ per node per document, and it carried the #421 stale-pointer bug
49
+ class, which the removal structurally eliminates. The public
50
+ `leptris_node_get/set_binding_wrapper` API is unchanged and now
51
+ serves from a lazily-allocated open-addressed map owned by the
52
+ document: documents that never wrap (all of them today) allocate
53
+ nothing, and entries cannot outlive their nodes because the map
54
+ dies with the document.
55
+ - **parse: interleaved lane size floor — small docs defer to classic
56
+ (#1258).** The two-phase scan+replay lane pays a fixed setup cost
57
+ that small documents cannot amortize: canon's gate measured ~25%
58
+ slower on a 24KB readonly shape while large documents run 0.69–0.74×
59
+ (256KB–3.4MB). `dp_il_try` now defers to the classic parser below a
60
+ 64KB floor (the midpoint between the measured loss and the wins);
61
+ `LEPTRIS_IL_MIN` overrides it in bytes, with explicit `0` disabling
62
+ the floor. Coverage is preserved, not narrowed: the parity suite
63
+ pins `LEPTRIS_IL_MIN=1` in every lane toggle, the lane-forced CI leg
64
+ sets it to `0` so the full suite still exercises the lane, and a new
65
+ spec pins the deferral itself.
66
+
67
+
68
+
69
+ ## [1.9.226] - 2026-09-22
70
+
71
+ ### Performance
72
+
73
+ - **html:** zero-re-copy element opens (#1285 slice 5, #1315): HTML
74
+ element names dropped from two pool allocations + copies per open to
75
+ one — the builder stores the lowercased pooled name (or the static
76
+ literal of a synthesized open) directly, with no second copy, no QName
77
+ split, and no colon re-copy. Local table-row parse (the #1218
78
+ fixture): 33.1 → 35.3 MB/s best-of on a contended arm64 host; the CI
79
+ benchmark leg is the authoritative read. Semantics pinned by 8 new
80
+ specs (case folding, literal QNames, attributes across every
81
+ delimiter shape, hidden-input gating, image→img).
82
+
83
+
84
+
3
85
  ## [1.9.225] - 2026-09-22
4
86
 
5
87
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.225
6
+ VERSION 1.9.228
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -23,8 +23,7 @@ LeptrisCDATANode* leptris_cdata_create(const char* content,
23
23
  node->base.type = LEPTRIS_NODE_TYPE_CDATA;
24
24
  node->base.frozen = 0;
25
25
  node->base.version = 0;
26
- node->base.binding_wrapper = NULL;
27
- node->parent_off = 0;
26
+ node->parent_off = 0;
28
27
  node->next_sibling_off = 0;
29
28
 
30
29
  if (content && content_len > 0) {
@@ -24,7 +24,6 @@ LeptrisCommentNode* leptris_comment_create(const char* content,
24
24
  node->base.type = LEPTRIS_NODE_TYPE_COMMENT;
25
25
  node->base.frozen = 0;
26
26
  node->base.version = 0;
27
- node->base.binding_wrapper = NULL;
28
27
  node->parent_off = 0;
29
28
  node->next_sibling_off = 0;
30
29
 
@@ -298,6 +298,22 @@ int32_t leptris_compact_int32_encode(void* base, void* target,
298
298
  return (int32_t)d;
299
299
  }
300
300
 
301
+ int32_t leptris_compact_int32_encode_doc(void* base, void* target,
302
+ const int32_t* field_addr,
303
+ struct leptris_document* doc) {
304
+ if (!target) return 0;
305
+ ptrdiff_t d = (char*)target - (char*)base;
306
+ if (d < INT32_MIN || d > INT32_MAX) {
307
+ LeptrisCompactOverflowTable* table = get_overflow_table();
308
+ if (table && leptris_compact_overflow_set(table, field_addr,
309
+ target, doc) == 0) {
310
+ return LEPTRIS_INT32_OVERFLOW_SENTINEL;
311
+ }
312
+ return 0;
313
+ }
314
+ return (int32_t)d;
315
+ }
316
+
301
317
  void* leptris_compact_int32_decode(void* base, int32_t off,
302
318
  const int32_t* field_addr) {
303
319
  if (off == 0) return NULL;
@@ -109,6 +109,15 @@ int32_t leptris_compact_int32_encode(void* base, void* target,
109
109
  const int32_t* field_addr);
110
110
  void* leptris_compact_int32_decode(void* base, int32_t off,
111
111
  const int32_t* field_addr);
112
+ /* #1320: same encoding, but the overflow-table entry is tagged with
113
+ * the caller's explicit document instead of the thread-local
114
+ * "current document" — for stamps made OUTSIDE a parse context
115
+ * (public node creators), where the TLS slot holds the wrong doc or
116
+ * NULL and a mis-tagged entry would outlive its document. */
117
+ struct leptris_document;
118
+ int32_t leptris_compact_int32_encode_doc(void* base, void* target,
119
+ const int32_t* field_addr,
120
+ struct leptris_document* doc);
112
121
  #ifdef __cplusplus
113
122
  }
114
123
  #endif
@@ -683,17 +683,16 @@ static inline struct leptris_namespace** leptris_elem_namespaces_ptr(
683
683
  * pinned structs hold only pointers and int-sized scalars, so i686
684
684
  * and armv7 produce identical layouts. */
685
685
  #if SIZE_MAX == UINT64_MAX
686
- LEPTRIS_STATIC_ASSERT(sizeof(struct leptris_element) == 64,
687
- "#1285 slice 3a: ns_cache_off + offsets side table -> 64 bytes");
686
+ LEPTRIS_STATIC_ASSERT(sizeof(struct leptris_element) == 48,
687
+ "#1285 slice 3b: binding_wrapper out of the base -> 48 bytes");
688
688
 
689
689
  LEPTRIS_STATIC_ASSERT(sizeof(struct leptris_attribute) == 40,
690
690
  "round 19 attr layout: 16+16+4+2+2 = 40");
691
691
  #else
692
- /* ILP32: LeptrisNode 24->16 (binding_wrapper), element name +
693
- * ns_cache pointers 8->4 each -> 56; attribute name_view 16->8
694
- * (value union stays 16 via inline_value[16]) -> 32. */
695
- LEPTRIS_STATIC_ASSERT(sizeof(struct leptris_element) == 48,
696
- "#1285 slice 3a ILP32: 48 bytes");
692
+ /* ILP32: LeptrisNode 16->12 (#1285 slice 3b); attribute
693
+ * name_view 16->8 (value union stays 16 via inline_value[16]). */
694
+ LEPTRIS_STATIC_ASSERT(sizeof(struct leptris_element) == 44,
695
+ "#1285 slice 3b ILP32: base 12 + 8 + 6*4 + ptr 4 = 44 bytes");
697
696
 
698
697
  LEPTRIS_STATIC_ASSERT(sizeof(struct leptris_attribute) == 32,
699
698
  "leptris_attribute ILP32 layout: 8+16+4+2+2 = 32");
@@ -24,7 +24,6 @@ LeptrisEntityRefNode* leptris_entity_ref_create(const char* name,
24
24
  node->base.type = LEPTRIS_NODE_TYPE_ENTITY_REF;
25
25
  node->base.frozen = 0;
26
26
  node->base.version = 0;
27
- node->base.binding_wrapper = NULL;
28
27
  node->parent_off = 0;
29
28
  node->next_sibling_off = 0;
30
29
  node->owner_doc = NULL;
@@ -57,11 +57,9 @@ typedef struct leptris_node {
57
57
  * parse-time bulk strides hold. */
58
58
  unsigned int version : 30; /* COW 2.2: Node version for tracking modifications */
59
59
  uint32_t line; /* Source line (1-based, 0 = unknown). Issue #223 */
60
- void* binding_wrapper; /* FFI wrapper cache (#262). NULL when no binding
61
- * is attached. Set by the language binding on
62
- * first node wrap; subsequent traversals find
63
- * the cached wrapper without FFI call overhead. */
64
- /* NOTE: Parent/sibling pointers stored in compressed form in specific node types */
60
+ /* NOTE: Parent/sibling pointers stored in compressed form in specific node types.
61
+ * #1285 slice 3b: binding_wrapper (#262) moved to the document-
62
+ * owned wrapper map — see leptris_internal.h; base is 12 bytes. */
65
63
  } LeptrisNode;
66
64
 
67
65
  /* ============================================================================
@@ -296,8 +296,13 @@ static LeptrisDocument node_public_document(LeptrisNodeRef node) {
296
296
  LeptrisElement parent = leptris_node_parent(node);
297
297
  if (parent) return leptris_element_get_document(parent);
298
298
  /* Issue #519: detached non-element nodes carry their owning
299
- * document — mutations must work before any attach. */
299
+ * document — mutations must work before any attach. Text was the
300
+ * one kind #519 skipped (its pool backref covered it until #1285
301
+ * slice 4 removed it — the #1320 regression); its stamp is an
302
+ * int32 offset instead of a field. */
300
303
  switch (node->type) {
304
+ case LEPTRIS_NODE_TYPE_TEXT:
305
+ return leptris_textnode_owner_doc((const LeptrisTextNode*)node);
301
306
  case LEPTRIS_NODE_TYPE_PI:
302
307
  return ((LeptrisPINode*)node)->owner_doc;
303
308
  case LEPTRIS_NODE_TYPE_COMMENT:
@@ -329,6 +334,13 @@ LEPTRIS_API LeptrisNodeRef leptris_text_node_create(LeptrisDocument doc,
329
334
  if (!content) content = "";
330
335
  size_t len = strlen(content);
331
336
  LeptrisTextNode* n = leptris_text_create(content, len, doc->pool);
337
+ if (n) {
338
+ /* #1320: stamp the owner so detached mutations can reach the
339
+ * doc's pool (PI/comment/CDATA do this via owner_doc fields;
340
+ * explicit-doc encoder — no parse context here). */
341
+ n->owner_doc_off = leptris_compact_int32_encode_doc(
342
+ n, doc, &n->owner_doc_off, doc);
343
+ }
332
344
  return n ? (LeptrisNodeRef)n : NULL;
333
345
  }
334
346
 
@@ -688,12 +700,71 @@ LEPTRIS_API void leptris_node_source_position(LeptrisNodeRef node,
688
700
  }
689
701
  }
690
702
 
703
+ /* #1285 slice 3b: binding_wrapper storage moved out of LeptrisNode
704
+ * (both bindings have zero callers — they cache wrappers on their
705
+ * own side — and the in-node field carried the #421 stale-pointer
706
+ * bug class on recycled arena pages). Open-addressed map keyed by
707
+ * node pointer, lazily allocated per document, freed with it: map
708
+ * entries can never outlive their nodes, and documents that never
709
+ * wrap (all of them today) pay nothing. NULL before any set — the
710
+ * parse-NULL contract holds by construction. */
711
+ struct leptris_wrapper_entry {
712
+ LeptrisNodeRef node; /* 0 = empty slot */
713
+ void* wrapper;
714
+ };
715
+
716
+ static size_t wrapper_hash(LeptrisNodeRef n, size_t cap) {
717
+ uintptr_t k = (uintptr_t)n;
718
+ return ((size_t)((k * 0x9E3779B97F4A7C15ULL) >> 32)) & (cap - 1);
719
+ }
720
+
721
+ static int wrapper_grow(struct leptris_document* doc) {
722
+ size_t ncap = doc->wrapper_cap ? doc->wrapper_cap * 2 : 16;
723
+ struct leptris_wrapper_entry* nt = (struct leptris_wrapper_entry*)
724
+ calloc(ncap, sizeof(*nt));
725
+ if (!nt) return -1;
726
+ for (size_t i = 0; i < doc->wrapper_cap; i++) {
727
+ struct leptris_wrapper_entry* e = &doc->wrapper_map[i];
728
+ if (e->node) {
729
+ size_t j = wrapper_hash(e->node, ncap);
730
+ while (nt[j].node) j = (j + 1) & (ncap - 1);
731
+ nt[j] = *e;
732
+ }
733
+ }
734
+ free(doc->wrapper_map);
735
+ doc->wrapper_map = nt;
736
+ doc->wrapper_cap = ncap;
737
+ return 0;
738
+ }
739
+
691
740
  LEPTRIS_API void* leptris_node_get_binding_wrapper(LeptrisNodeRef node) {
692
- return node ? node->binding_wrapper : NULL;
741
+ if (!node) return NULL;
742
+ struct leptris_document* doc = node_public_document(node);
743
+ if (!doc || !doc->wrapper_map) return NULL;
744
+ size_t i = wrapper_hash(node, doc->wrapper_cap);
745
+ while (doc->wrapper_map[i].node) {
746
+ if (doc->wrapper_map[i].node == node)
747
+ return doc->wrapper_map[i].wrapper;
748
+ i = (i + 1) & (doc->wrapper_cap - 1);
749
+ }
750
+ return NULL;
693
751
  }
694
752
 
695
753
  LEPTRIS_API void leptris_node_set_binding_wrapper(LeptrisNodeRef node, void* wrapper) {
696
- if (node) node->binding_wrapper = wrapper;
754
+ if (!node) return;
755
+ struct leptris_document* doc = node_public_document(node);
756
+ if (!doc) return; /* detached, unregistered node: no carrier */
757
+ if (doc->wrapper_count * 2 + 2 >= doc->wrapper_cap) {
758
+ if (wrapper_grow(doc) != 0) return;
759
+ }
760
+ size_t i = wrapper_hash(node, doc->wrapper_cap);
761
+ while (doc->wrapper_map[i].node && doc->wrapper_map[i].node != node)
762
+ i = (i + 1) & (doc->wrapper_cap - 1);
763
+ if (!doc->wrapper_map[i].node) {
764
+ doc->wrapper_map[i].node = node;
765
+ doc->wrapper_count++;
766
+ }
767
+ doc->wrapper_map[i].wrapper = wrapper;
697
768
  }
698
769
 
699
770
  LEPTRIS_API int leptris_node_compare(LeptrisNodeRef a, LeptrisNodeRef b) {
@@ -46,7 +46,6 @@ LeptrisPINode* leptris_pi_create(const char* target,
46
46
  node->base.type = LEPTRIS_NODE_TYPE_PI;
47
47
  node->base.frozen = 0;
48
48
  node->base.version = 0;
49
- node->base.binding_wrapper = NULL;
50
49
  node->parent_off = 0;
51
50
  node->next_sibling_off = 0;
52
51
  node->target = target_storage;
@@ -30,12 +30,9 @@ LeptrisTextNode* leptris_text_create(const char* content,
30
30
  node->base.frozen = 0;
31
31
  node->base.version = 0;
32
32
  node->base.raw = 0; /* uninitialized: see create_borrowed */
33
- /* Explicit: pool memory is not zeroed and the retained arena
34
- * recycles dirty pages — a stale binding_wrapper would hand
35
- * bindings a dangling pointer. */
36
- node->base.binding_wrapper = NULL;
37
33
  node->parent_off = 0;
38
34
  node->next_sibling_off = 0;
35
+ node->owner_doc_off = 0; /* #1320: only the public creator stamps */
39
36
 
40
37
  if (content && content_len > 0) {
41
38
  memcpy(content_storage, content, content_len);
@@ -70,11 +67,11 @@ LeptrisTextNode* leptris_text_create_borrowed(const char* content,
70
67
  * uninitialized raw bit silently flips DOE/cdata behavior by
71
68
  * build configuration. */
72
69
  node->base.raw = 0;
73
- node->base.binding_wrapper = NULL;
74
70
  node->content = (char*)content; /* Non-owning; caller guarantees lifetime + termination. */
75
71
  node->content_len = (uint32_t)content_len;
76
72
  node->parent_off = 0;
77
73
  node->next_sibling_off = 0;
74
+ node->owner_doc_off = 0; /* #1320: only the public creator stamps */
78
75
 
79
76
  return node;
80
77
  }
@@ -26,7 +26,15 @@
26
26
  * are outside every supported target.
27
27
  *
28
28
  * Issue #168: parent_off mirrors next_sibling_off so the parent of a
29
- * text node can be queried in O(1). */
29
+ * text node can be queried in O(1).
30
+ *
31
+ * Issue #1320: owner_doc_off gives DETACHED text nodes their owning
32
+ * document — #519 added an owner_doc field to PI/comment/CDATA but
33
+ * skipped text (whose pool backref covered it at the time); #1285
34
+ * slice 4 deleted that backref and detached set_content started
35
+ * returning INVALID_ARG. Stamped by the public creator only —
36
+ * parse-carved nodes are attached by construction. int32, so the
37
+ * LP64 struct keeps its 40 bytes (padding absorbs it). */
30
38
  typedef struct leptris_text_node {
31
39
  LeptrisNode base; /* MUST be first */
32
40
  char* content; /* Text content - NEVER trim! NUL-terminated at [content_len] */
@@ -40,6 +48,7 @@ typedef struct leptris_text_node {
40
48
  * 0 = NULL. */
41
49
  int32_t next_sibling_off;
42
50
  int32_t parent_off; /* Byte offset to parent element (0=NULL) */
51
+ int32_t owner_doc_off; /* Byte offset to owning document (0=unstamped; #1320) */
43
52
  } LeptrisTextNode;
44
53
 
45
54
  /* Text node creation.
@@ -110,4 +119,17 @@ static inline void leptris_textnode_set_parent(LeptrisTextNode* t, LeptrisElemen
110
119
  t->parent_off = leptris_compact_int32_encode_inline(t, parent, &t->parent_off);
111
120
  }
112
121
 
122
+ /* Owning-document access (issue #1320). The stamp is written by
123
+ * leptris_text_node_create with the explicit-doc encoder (an
124
+ * overflow-table entry made outside a parse context must carry the
125
+ * REAL document tag, not the TLS current-doc). Decode is the
126
+ * standard edge decode. */
127
+ struct leptris_document;
128
+ static inline struct leptris_document* leptris_textnode_owner_doc(const LeptrisTextNode* t) {
129
+ return (t && t->owner_doc_off != 0)
130
+ ? (struct leptris_document*)leptris_compact_int32_decode_inline(
131
+ (void*)t, t->owner_doc_off, &t->owner_doc_off)
132
+ : NULL;
133
+ }
134
+
113
135
  #endif /* LEPTRIS_DOM_TEXT_H */
@@ -1005,7 +1005,6 @@ static inline LeptrisTextNode* dp_text_create(DParser* p,
1005
1005
  * serialize_text_internal then emitted text VERBATIM (raw < in
1006
1006
  * the output, invalid XML). */
1007
1007
  tn->base.raw = 0;
1008
- tn->base.binding_wrapper = NULL;
1009
1008
  tn->base.line = 0; /* caller stamps the offset */
1010
1009
  /* #1285 slice 4: the run is NUL-terminated in place BEFORE this
1011
1010
  * carve (dispatch-entry dp_nul / walker '&' NUL / EOF sentinel)
@@ -1015,6 +1014,7 @@ static inline LeptrisTextNode* dp_text_create(DParser* p,
1015
1014
  tn->content_len = (uint32_t)content_len;
1016
1015
  tn->parent_off = 0;
1017
1016
  tn->next_sibling_off = 0;
1017
+ tn->owner_doc_off = 0; /* #1320: parse-carved nodes attach */
1018
1018
  return tn;
1019
1019
  }
1020
1020
 
@@ -2598,6 +2598,7 @@ static struct leptris_document* dp_il_build(
2598
2598
  * run is already NUL-terminated at carve) */
2599
2599
  tn->parent_off = 0;
2600
2600
  tn->next_sibling_off = 0;
2601
+ tn->owner_doc_off = 0; /* #1320: parse-carved nodes attach */
2601
2602
  smap[i] = (LeptrisNode*)tn;
2602
2603
  pp.depth = (int)rdepth[r->parent] + 1;
2603
2604
  pp.last_child_stack[pp.depth - 1] = lc[r->parent];
@@ -2686,6 +2687,25 @@ static struct leptris_document* dp_il_try(const char* xml, size_t len,
2686
2687
  if (!gate || !*gate) return NULL;
2687
2688
  if (keep_ent || dtd_attrs) return NULL;
2688
2689
  if (len == 0 || len >= 0x7FFFFFFFu) return NULL;
2690
+ /* #1258: small documents do not amortize the record pass.
2691
+ * Canon's moxml shape (24KB / 902 elements) measured ~25%
2692
+ * SLOWER on the lane while allocations stayed identical - the
2693
+ * two-phase scan+replay pays fixed setup that a small doc
2694
+ * cannot win back; large docs take 0.69-0.74x. Below the
2695
+ * minimum the lane defers to the classic parser. LEPTRIS_IL_MIN
2696
+ * overrides (bytes; 0 = no threshold) for hosts and for the
2697
+ * parity gates, which force it to 0 so the lane stays covered
2698
+ * by the suite at fixture sizes. */
2699
+ {
2700
+ const char* mins = getenv("LEPTRIS_IL_MIN");
2701
+ size_t min_len = 64 * 1024;
2702
+ if (mins && *mins) {
2703
+ long mv = strtol(mins, NULL, 10);
2704
+ if (mv > 0) min_len = (size_t)mv;
2705
+ else min_len = 0; /* explicit 0 disables */
2706
+ }
2707
+ if (len < min_len) return NULL;
2708
+ }
2689
2709
 
2690
2710
  /* Scratch is built UP FRONT (slice 2): il_scan reads it with
2691
2711
  * the NUL-sentinel discipline, and dp_il_build reuses the same
@@ -4210,8 +4210,43 @@ static LeptrisElement h_open_element_id(HBuilder* b, const char* name,
4210
4210
  return e;
4211
4211
  }
4212
4212
 
4213
+ /* #1285 slice 5: zero-re-copy element open. `name` is a NUL-
4214
+ * terminated POOL-OWNED lowercased string (h_pooled_lower) or a
4215
+ * static literal — both live for the document, so the element
4216
+ * borrows it directly: no create_with_view copy, no QName split
4217
+ * (HTML keeps qualified names literally, tests14:1/3 — the whole
4218
+ * token IS the wanted name), no colon re-copy undoing the split.
4219
+ * Ownership matches create_with_view's pool copy exactly (rename
4220
+ * replaces, never frees). */
4221
+ static LeptrisElement h_open_element_borrowed(HBuilder* b, char* name,
4222
+ uint8_t tid) {
4223
+ LeptrisElement e = (LeptrisElement)leptris_pool_alloc(
4224
+ b->pool, sizeof(struct leptris_element));
4225
+ if (!e) return NULL;
4226
+ memset(e, 0, sizeof(struct leptris_element));
4227
+ e->base.type = LEPTRIS_NODE_TYPE_ELEMENT;
4228
+ e->name = name;
4229
+ size_t nl = strlen(name);
4230
+ e->name_len = (nl > 254) ? 0xFF : (uint8_t)nl;
4231
+ e->name_hash = leptris_name_hash_compute(name);
4232
+ leptris_root_doc_register(e, b->doc);
4233
+ b->left_initial = 1;
4234
+ h_append(b, (LeptrisNodeRef)e);
4235
+ if (b->depth == 0 && !b->root) b->root = e;
4236
+ if (b->depth < 256) {
4237
+ b->open[b->depth] = e;
4238
+ b->open_tail[b->depth] = NULL;
4239
+ b->open_ns[b->depth] = H_NS_HTML;
4240
+ b->open_id[b->depth] = tid;
4241
+ b->depth++;
4242
+ }
4243
+ return e;
4244
+ }
4245
+
4213
4246
  static LeptrisElement h_open_element(HBuilder* b, const char* name) {
4214
- return h_open_element_id(b, name, h_tag_id(name));
4247
+ /* Synthesized opens pass string literals (process lifetime) —
4248
+ * the borrowed path stores them with zero copies. */
4249
+ return h_open_element_borrowed(b, (char*)name, h_tag_id(name));
4215
4250
  }
4216
4251
 
4217
4252
  /* Case-insensitive C-string compare (ASCII). */
@@ -7699,7 +7734,7 @@ static LeptrisDocument html_parse_shared(
7699
7734
 
7700
7735
  LeptrisElement e = (elem_ns != H_NS_HTML)
7701
7736
  ? h_open_foreign(&b, name, elem_ns)
7702
- : h_open_element_id(&b, name, tid);
7737
+ : h_open_element_borrowed(&b, name, tid);
7703
7738
  if (!e) goto done;
7704
7739
  if (strcmp(name, "html") == 0) {
7705
7740
  b.html_seen = 1;
@@ -967,6 +967,11 @@ LEPTRIS_API void leptris_document_free(struct leptris_document* doc) {
967
967
  free(doc->elem_pos);
968
968
  doc->elem_pos = NULL;
969
969
  doc->elem_pos_count = doc->elem_pos_cap = 0;
970
+ /* #1285 slice 3b: binding-wrapper map — entries die with the
971
+ * document, so they can never outlive their nodes. */
972
+ free(doc->wrapper_map);
973
+ doc->wrapper_map = NULL;
974
+ doc->wrapper_count = doc->wrapper_cap = 0;
970
975
  if (doc->xml_buffer && doc->xml_buffer_needs_free) {
971
976
  /* Release through the retained-buffer free list (see
972
977
  * memory/arena.c): large inputs would otherwise be munmapped
@@ -226,6 +226,15 @@ struct leptris_document {
226
226
  * xml_buffer) and resolves queries via binary search, caching
227
227
  * the result in the node with the high bit set. NULL = not yet
228
228
  * built. Freed with the document. */
229
+ /* #1285 slice 3b: binding_wrapper left the node base (#262's
230
+ * FFI cache was never adopted — zero callers in leptris-ruby/py
231
+ * — and its stale-pointer bug class, #421, is structural). The
232
+ * public get/set API now serves from this lazily-allocated
233
+ * open-addressed map, keyed by node pointer; it dies with the
234
+ * document so entries can never outlive their nodes. NULL until
235
+ * the first set — documents that never wrap pay nothing. */
236
+ struct leptris_wrapper_entry* wrapper_map;
237
+ size_t wrapper_count, wrapper_cap;
229
238
  int html_mode; /* #1309: HTML-parsed / create_html documents
230
239
  serialize with the §16.2 HTML method by
231
240
  default (libxml2/Nokogiri behavior). */
@@ -128,6 +128,7 @@ leptris_add_test(test_xslt xpath/test_xslt.cpp)
128
128
  leptris_add_test(test_xquery xquery/test_xquery.cpp)
129
129
  leptris_add_test(test_html html/test_html.cpp)
130
130
  leptris_add_test(test_html_builder html/test_html_builder.cpp)
131
+ leptris_add_test(test_html_borrowed_names html/test_html_borrowed_names.cpp)
131
132
  leptris_add_test(test_html5lib html/test_html5lib.cpp)
132
133
  file(GLOB html5lib_dats
133
134
  "${CMAKE_CURRENT_SOURCE_DIR}/html/html5lib-tests/tree-construction/*.dat")
@@ -103,6 +103,12 @@ TEST(HeaderHygiene, ElementTreeEdgeRoundTrip) {
103
103
  alignas(kAlign) char buf[2 * sizeof(struct leptris_element)];
104
104
  struct leptris_element* a = (struct leptris_element*)buf;
105
105
  struct leptris_element* b = (struct leptris_element*)(buf + sizeof(*a));
106
+ /* The last_child getter WALKS the child list, so b's sibling
107
+ * edge is read — zero the fake nodes or the walk follows
108
+ * uninitialized stack garbage. (#1285 slice 3b moved the field
109
+ * onto a stack slot whose garbage is nonzero on x86_64, turning
110
+ * the latent UB into a real failure; it was always UB.) */
111
+ memset(buf, 0, sizeof(buf));
106
112
 
107
113
  /* parent round-trip */
108
114
  leptris_elem_set_parent(a, b);
@@ -132,6 +138,7 @@ TEST(HeaderHygiene, ElementAttributeEdgeRoundTrip) {
132
138
  struct leptris_element* e = (struct leptris_element*)buf;
133
139
  struct leptris_attribute* attr =
134
140
  (struct leptris_attribute*)(buf + sizeof(*e));
141
+ memset(buf, 0, sizeof(buf));
135
142
  leptris_attr_set_next(attr, NULL);
136
143
 
137
144
  leptris_elem_set_first_attribute(e, attr);
@@ -2014,6 +2014,51 @@ TEST(DetachedNodes, MutationWorksBeforeAttach) {
2014
2014
  leptris_document_free(doc);
2015
2015
  }
2016
2016
 
2017
+ /* Issue #1320: TEXT was the one node kind #519's detached-mutation
2018
+ * fix skipped — no owner_doc, so after #1285 slice 4 removed the
2019
+ * node-side pool backref, set_content on a created-but-unattached
2020
+ * text node resolved no document and returned INVALID_ARG. moxml's
2021
+ * create → fill → attach builder sequence hit it on every leg. */
2022
+ TEST(DetachedNodes, TextSetContentWorksBeforeAttach) {
2023
+ /* Unattached: the discriminator that regressed (1.9.223–1.9.226
2024
+ * returned EINVAL here; content stayed at the create-time value). */
2025
+ LeptrisDocument doc = leptris_document_create();
2026
+ ASSERT_NE(doc, nullptr);
2027
+ LeptrisNodeRef t = leptris_text_node_create(doc, "abc");
2028
+ ASSERT_NE(t, nullptr);
2029
+ EXPECT_EQ(leptris_text_node_set_content(t, "world"), LEPTRIS_OK);
2030
+ EXPECT_STREQ(leptris_text_node_get_content(t), "world");
2031
+
2032
+ /* The mutation survives attach + serialize. */
2033
+ LeptrisElement root = leptris_element_create(doc, "r");
2034
+ ASSERT_EQ(leptris_document_set_root(doc, root), LEPTRIS_OK);
2035
+ ASSERT_EQ(leptris_element_append_child(root, (LeptrisElement)t), LEPTRIS_OK);
2036
+ char* x = leptris_document_serialize(doc, nullptr);
2037
+ ASSERT_NE(x, nullptr);
2038
+ EXPECT_NE(std::strstr(x, "<r>world</r>"), nullptr);
2039
+ leptris_free_string(x);
2040
+
2041
+ /* Attach-then-set and set-after-parse still work (never
2042
+ * regressed — pin them so the owner path can't shadow the
2043
+ * parent-walk path). */
2044
+ LeptrisNodeRef t2 = leptris_text_node_create(doc, "one");
2045
+ ASSERT_EQ(leptris_element_append_child(root, (LeptrisElement)t2), LEPTRIS_OK);
2046
+ EXPECT_EQ(leptris_text_node_set_content(t2, "two"), LEPTRIS_OK);
2047
+ EXPECT_STREQ(leptris_text_node_get_content(t2), "two");
2048
+
2049
+ leptris_document_free(doc);
2050
+
2051
+ const char xml[] = "<r>parsed</r>";
2052
+ LeptrisDocument doc2 = leptris_parse_string(xml, std::strlen(xml), nullptr);
2053
+ ASSERT_NE(doc2, nullptr);
2054
+ LeptrisElement root2 = leptris_document_root(doc2);
2055
+ LeptrisNodeRef pt = leptris_node_first_child(leptris_element_as_node(root2));
2056
+ ASSERT_NE(pt, nullptr);
2057
+ EXPECT_EQ(leptris_text_node_set_content(pt, "replaced"), LEPTRIS_OK);
2058
+ EXPECT_STREQ(leptris_text_node_get_content(pt), "replaced");
2059
+ leptris_document_free(doc2);
2060
+ }
2061
+
2017
2062
  /* Issue #526: document-level PI enumeration + creation. */
2018
2063
  TEST(DocumentPIs, EnumerateAndAdd) {
2019
2064
  const char xml[] = "<?xml version='1.0'?><?docpi d1?><r/>";
@@ -50,25 +50,28 @@ TEST(TextBorrowed, ParsedTextNodeIsTerminatedInPlace) {
50
50
  TEST(TextBorrowed, NodeStructIsBasePointerAndThreeInt32) {
51
51
  /* #1285 slice 4: pool + borrowed removed, content_len is uint32.
52
52
  * The width-independent invariant: base + ONE content pointer +
53
- * three int32s (len, next, parent), rounded to pointer alignment
54
- * — nothing else. 64 -> 48 on LP64; 32 on ILP32. The issue's
55
- * 32B sketch assumed a 12-byte base and a 64-bit host; going
56
- * lower needs content as an int32 pool offset (slice 4b). The
53
+ * FOUR int32s (len, next, parent, owner doc — #1320), rounded to
54
+ * pointer alignment — nothing else. 40 on LP64 (the owner field
55
+ * lands in padding; sizeof unchanged); 32 on ILP32. Going lower
56
+ * needs content as an int32 pool offset (slice 4b). The
57
57
  * parse-time bulk strides (dp text block, il lane table) hardcode
58
58
  * this layout economics — a silent field growth would erode the
59
59
  * #1222 parse win this slice exists for. */
60
- /* C++11: no constexpr lambda — inline the round-up expression. */
61
- constexpr size_t kRaw = sizeof(LeptrisNode) + sizeof(void*) +
62
- 3 * sizeof(int32_t);
60
+ /* C++11: no constexpr lambda — inline the round-ups. The
61
+ * content pointer needs pointer alignment INSIDE the struct:
62
+ * base rounds up to the pointer's alignment first. */
63
63
  constexpr size_t kA = alignof(void*);
64
+ constexpr size_t kBaseA =
65
+ (sizeof(LeptrisNode) + kA - 1) / kA * kA;
66
+ constexpr size_t kRaw = kBaseA + sizeof(void*) + 4 * sizeof(int32_t);
64
67
  constexpr size_t kAligned = (kRaw + kA - 1) / kA * kA;
65
68
  static_assert(sizeof(LeptrisTextNode) == kAligned,
66
69
  "LeptrisTextNode must be base + one content pointer + "
67
- "uint32 len + int32 next + int32 parent (aligned) — "
68
- "no pool/borrowed fields (#1285 slice 4)");
70
+ "uint32 len + int32 next + int32 parent + int32 owner "
71
+ "(aligned) — no pool/borrowed fields (#1285 slice 4, #1320)");
69
72
  EXPECT_EQ(sizeof(LeptrisTextNode), kAligned);
70
73
  #if defined(__LP64__) || defined(_WIN64)
71
- EXPECT_EQ(sizeof(LeptrisTextNode), (size_t)48);
74
+ EXPECT_EQ(sizeof(LeptrisTextNode), (size_t)40);
72
75
  #else
73
76
  EXPECT_EQ(sizeof(LeptrisTextNode), (size_t)32);
74
77
  #endif
@@ -0,0 +1,143 @@
1
+ // test/html/test_html_borrowed_names.cpp — #1285 slice 5: zero-copy
2
+ // element names in the HTML builder (the in-place lowercasing carve).
3
+ //
4
+ // Tag names lowercase IN PLACE in the doc-owned input copy with a
5
+ // NUL written over the name's delimiter; elements borrow the pointer
6
+ // (no pool copy, no create_with_view copy). The delimiter byte's
7
+ // consumers — the attribute scanner and h_input_type_hidden — skip
8
+ // the NUL with the delimiter's original semantics preserved (a '>'
9
+ // delimiter ended the tag; ws/'/' were skippable). Attribute names
10
+ // still pool-copy: the #635 raw-attr views keep source case.
11
+ //
12
+ // Falsifiable: case folding, QName-literal names, attributes after
13
+ // every delimiter shape, hidden-input detection through the NUL, and
14
+ // no phantom attributes from content after a NUL-terminated '>'.
15
+
16
+ #include <gtest/gtest.h>
17
+ extern "C" {
18
+ #include "leptris.h"
19
+ #include "leptris/html.h"
20
+ }
21
+ #include <cstring>
22
+ #include <string>
23
+
24
+ namespace {
25
+
26
+ std::string FirstElemName(const char* html) {
27
+ LeptrisStatus st = LEPTRIS_OK;
28
+ LeptrisDocument d = leptris_parse_html_string(html, strlen(html), &st);
29
+ if (!d) return "(parse-failed)";
30
+ /* HTML docs get the html/body wrapper — query the FIRST element
31
+ * below the wrapper instead of the document root. */
32
+ LeptrisXPathResult xr =
33
+ leptris_xpath_eval(d, nullptr, "name(/*/*/*[1])");
34
+ std::string r = "(none)";
35
+ if (leptris_xpath_result_type(xr) == LEPTRIS_XPATH_STRING) {
36
+ char* s = leptris_xpath_result_string(xr);
37
+ if (s) {
38
+ r = s;
39
+ leptris_free_string(s);
40
+ }
41
+ }
42
+ leptris_xpath_result_free(xr);
43
+ leptris_document_free(d);
44
+ return r;
45
+ }
46
+
47
+ } // namespace
48
+
49
+ TEST(HtmlBorrowedNames, UpperCaseTagFoldsLower) {
50
+ EXPECT_EQ(FirstElemName("<DIV>t</DIV>"), "div");
51
+ EXPECT_EQ(FirstElemName("<MiXeD/>"), "mixed");
52
+ }
53
+
54
+ TEST(HtmlBorrowedNames, QNameStaysLiteralWholeToken) {
55
+ /* tests14 semantics: HTML keeps qualified names literally — the
56
+ * whole lowercased token is the name, no prefix split. */
57
+ EXPECT_EQ(FirstElemName("<xyz:ABC>t</xyz:ABC>"), "xyz:abc");
58
+ }
59
+
60
+ TEST(HtmlBorrowedNames, AttributesAfterWsDelimiter) {
61
+ LeptrisStatus st = LEPTRIS_OK;
62
+ const char* html = "<DIV CLASS=\"X\" id='q'>t</DIV>";
63
+ LeptrisDocument d = leptris_parse_html_string(html, strlen(html), &st);
64
+ ASSERT_NE(d, nullptr);
65
+ LeptrisXPathResult xr = leptris_xpath_eval(d, nullptr, "//div");
66
+ ASSERT_EQ(leptris_xpath_result_type(xr), LEPTRIS_XPATH_NODESET);
67
+ ASSERT_EQ(leptris_xpath_result_count(xr), (size_t)1);
68
+ LeptrisElement div = leptris_xpath_result_get(xr, 0);
69
+ ASSERT_NE(div, nullptr);
70
+ EXPECT_STREQ(leptris_element_name(div), "div");
71
+ EXPECT_STREQ(leptris_element_attribute(div, "class"), "X");
72
+ EXPECT_STREQ(leptris_element_attribute(div, "id"), "q");
73
+ leptris_xpath_result_free(xr);
74
+ leptris_document_free(d);
75
+ }
76
+
77
+ TEST(HtmlBorrowedNames, AttributesAfterBareSlash) {
78
+ /* '<a/ href=...>': the bare '/' after the name is ignored. */
79
+ const char* html = "<a/ href='x'>t</a>";
80
+ LeptrisStatus st = LEPTRIS_OK;
81
+ LeptrisDocument d = leptris_parse_html_string(html, strlen(html), &st);
82
+ ASSERT_NE(d, nullptr);
83
+ LeptrisXPathResult xr = leptris_xpath_eval(d, nullptr, "//a");
84
+ ASSERT_EQ(leptris_xpath_result_type(xr), LEPTRIS_XPATH_NODESET);
85
+ ASSERT_EQ(leptris_xpath_result_count(xr), (size_t)1);
86
+ LeptrisElement a = leptris_xpath_result_get(xr, 0);
87
+ ASSERT_NE(a, nullptr);
88
+ EXPECT_STREQ(leptris_element_attribute(a, "href"), "x");
89
+ leptris_xpath_result_free(xr);
90
+ leptris_document_free(d);
91
+ }
92
+
93
+ TEST(HtmlBorrowedNames, NameThenGtDoesNotEatContentAsAttrs) {
94
+ /* '<br>' — the NUL lands on the '>'; following content must not
95
+ * become phantom attributes or text loss. */
96
+ const char* html = "<div><br>x<a href='y'>z</a></div>";
97
+ LeptrisStatus st = LEPTRIS_OK;
98
+ LeptrisDocument d = leptris_parse_html_string(html, strlen(html), &st);
99
+ ASSERT_NE(d, nullptr);
100
+ /* Query the tree rather than walk the child API surface. */
101
+ LeptrisXPathResult xr = leptris_xpath_eval(d, nullptr, "//a/@href");
102
+ ASSERT_EQ(leptris_xpath_result_type(xr), LEPTRIS_XPATH_NODESET);
103
+ EXPECT_EQ(leptris_xpath_result_count(xr), (size_t)1);
104
+ leptris_xpath_result_free(xr);
105
+ xr = leptris_xpath_eval(d, nullptr, "//br");
106
+ EXPECT_EQ(leptris_xpath_result_count(xr), (size_t)1);
107
+ leptris_xpath_result_free(xr);
108
+ leptris_document_free(d);
109
+ }
110
+
111
+ TEST(HtmlBorrowedNames, HiddenInputDetectedThroughNul) {
112
+ /* webkit01:51 — input type=hidden must not clear frameset-ok;
113
+ * the type scan runs over the NUL'd delimiter. Unquoted value. */
114
+ const char* html = "<input type=hidden><frameset></frameset>";
115
+ LeptrisStatus st = LEPTRIS_OK;
116
+ LeptrisDocument d = leptris_parse_html_string(html, strlen(html), &st);
117
+ ASSERT_NE(d, nullptr);
118
+ /* hidden input does NOT clear frameset-ok: the frameset lands */
119
+ LeptrisXPathResult xr = leptris_xpath_eval(d, nullptr, "//frameset");
120
+ ASSERT_EQ(leptris_xpath_result_type(xr), LEPTRIS_XPATH_NODESET);
121
+ EXPECT_EQ(leptris_xpath_result_count(xr), (size_t)1);
122
+ leptris_xpath_result_free(xr);
123
+ leptris_document_free(d);
124
+
125
+ /* ...and a VISIBLE input clears it: no frameset */
126
+ const char* html2 = "<input type=text><frameset></frameset>";
127
+ LeptrisDocument d2 = leptris_parse_html_string(html2, strlen(html2), &st);
128
+ ASSERT_NE(d2, nullptr);
129
+ LeptrisXPathResult xr2 = leptris_xpath_eval(d2, nullptr, "//frameset");
130
+ EXPECT_EQ(leptris_xpath_result_count(xr2), (size_t)0);
131
+ leptris_xpath_result_free(xr2);
132
+ leptris_document_free(d2);
133
+ }
134
+
135
+ TEST(HtmlBorrowedNames, ImageRenamedImg) {
136
+ EXPECT_EQ(FirstElemName("<image src='x'>"), "img");
137
+ }
138
+
139
+ TEST(HtmlBorrowedNames, LongCustomTagName) {
140
+ std::string n(200, 'Q');
141
+ std::string html = "<" + n + ">t</" + n + ">";
142
+ EXPECT_EQ(FirstElemName(html.c_str()), std::string(200, 'q'));
143
+ }
@@ -55,8 +55,16 @@ const char* kBail[] = {
55
55
  };
56
56
 
57
57
  std::string parse_str(const char* xml, bool lane) {
58
- if (lane) set_env("LEPTRIS_INTERLEAVED", "1");
59
- else unset_env("LEPTRIS_INTERLEAVED");
58
+ if (lane) {
59
+ set_env("LEPTRIS_INTERLEAVED", "1");
60
+ /* #1258: fixtures are far below the 64KB default minimum —
61
+ * force the lane's floor to 1 byte so parity specs exercise
62
+ * the lane, never a silent classic fallback. */
63
+ set_env("LEPTRIS_IL_MIN", "1");
64
+ } else {
65
+ unset_env("LEPTRIS_INTERLEAVED");
66
+ unset_env("LEPTRIS_IL_MIN");
67
+ }
60
68
  LeptrisStatus st = LEPTRIS_OK;
61
69
  LeptrisDocument d = leptris_parse_string(xml, strlen(xml), &st);
62
70
  std::string out = d ? "" : "(null)";
@@ -128,6 +136,7 @@ TEST(InterleavedParity, BailFixturesByteIdentical) {
128
136
  TEST(InterleavedParity, AttrAndNameAccessIdentical) {
129
137
  const char* xml = "<r k='v' n='m'><child z='9'>tx</child></r>";
130
138
  set_env("LEPTRIS_INTERLEAVED", "1");
139
+ set_env("LEPTRIS_IL_MIN", "1");
131
140
  LeptrisStatus st = LEPTRIS_OK;
132
141
  LeptrisDocument d = leptris_parse_string(xml, strlen(xml), &st);
133
142
  ASSERT_NE(d, nullptr);
@@ -186,6 +195,7 @@ TEST(InterleavedParity, EngagementCanaryFiresOnlyWhenLaneRuns) {
186
195
  };
187
196
 
188
197
  set_env("LEPTRIS_INTERLEAVED", "1");
198
+ set_env("LEPTRIS_IL_MIN", "1");
189
199
  set_env("LEPTRIS_IL_STATS", "1");
190
200
  char lane[512] = {0};
191
201
  ssize_t n = capture(lane, sizeof(lane));
@@ -195,6 +205,7 @@ TEST(InterleavedParity, EngagementCanaryFiresOnlyWhenLaneRuns) {
195
205
  EXPECT_NE(strstr(lane, "attrs=1"), nullptr) << lane;
196
206
 
197
207
  unset_env("LEPTRIS_IL_STATS");
208
+ unset_env("LEPTRIS_IL_MIN");
198
209
  char classic[512] = {0};
199
210
  n = capture(classic, sizeof(classic));
200
211
  ASSERT_GE(n, 0);
@@ -216,8 +227,10 @@ TEST(InterleavedParity, XmlAttrNamespaceParity) {
216
227
  const char* xml = "<root><code xml:space='preserve'>t</code></root>";
217
228
  for (int lane = 0; lane <= 1; lane++) {
218
229
  SCOPED_TRACE(lane ? "interleaved" : "classic");
219
- if (lane) set_env("LEPTRIS_INTERLEAVED", "1");
220
- else unset_env("LEPTRIS_INTERLEAVED");
230
+ if (lane) { set_env("LEPTRIS_INTERLEAVED", "1");
231
+ set_env("LEPTRIS_IL_MIN", "1"); }
232
+ else { unset_env("LEPTRIS_INTERLEAVED");
233
+ unset_env("LEPTRIS_IL_MIN"); }
221
234
  LeptrisStatus st = LEPTRIS_OK;
222
235
  LeptrisDocument d = leptris_parse_string(xml, strlen(xml), &st);
223
236
  ASSERT_NE(d, nullptr);
@@ -247,8 +260,10 @@ TEST(InterleavedParity, DeclaredPrefixNamespaceParity) {
247
260
  const char* xml = "<d xmlns:q='http://example/q' q:k='v'/>";
248
261
  for (int lane = 0; lane <= 1; lane++) {
249
262
  SCOPED_TRACE(lane ? "interleaved" : "classic");
250
- if (lane) set_env("LEPTRIS_INTERLEAVED", "1");
251
- else unset_env("LEPTRIS_INTERLEAVED");
263
+ if (lane) { set_env("LEPTRIS_INTERLEAVED", "1");
264
+ set_env("LEPTRIS_IL_MIN", "1"); }
265
+ else { unset_env("LEPTRIS_INTERLEAVED");
266
+ unset_env("LEPTRIS_IL_MIN"); }
252
267
  LeptrisStatus st = LEPTRIS_OK;
253
268
  LeptrisDocument d = leptris_parse_string(xml, strlen(xml), &st);
254
269
  ASSERT_NE(d, nullptr);
@@ -266,8 +281,10 @@ TEST(InterleavedParity, DeclaredPrefixNamespaceParity) {
266
281
  TEST(InterleavedParity, DupAttrDiagnosticParity) {
267
282
  const char* xml = "<r a='1' a='2'/>";
268
283
  for (int lane = 0; lane <= 1; lane++) {
269
- if (lane) set_env("LEPTRIS_INTERLEAVED", "1");
270
- else unset_env("LEPTRIS_INTERLEAVED");
284
+ if (lane) { set_env("LEPTRIS_INTERLEAVED", "1");
285
+ set_env("LEPTRIS_IL_MIN", "1"); }
286
+ else { unset_env("LEPTRIS_INTERLEAVED");
287
+ unset_env("LEPTRIS_IL_MIN"); }
271
288
  LeptrisStatus st = LEPTRIS_OK;
272
289
  LeptrisDocument d = leptris_parse_string(xml, strlen(xml), &st);
273
290
  ASSERT_NE(d, nullptr);
@@ -280,3 +297,52 @@ TEST(InterleavedParity, DupAttrDiagnosticParity) {
280
297
  unset_env("LEPTRIS_INTERLEAVED");
281
298
  }
282
299
  }
300
+
301
+
302
+ #ifndef _WIN32
303
+ /* #1258: the size floor. Below LEPTRIS_IL_MIN (default 64KB) the
304
+ * lane defers to the classic parser even when the gate is on —
305
+ * small documents cannot amortize the record pass (canon measured
306
+ * ~25% slower at 24KB). Forced floor (IL_MIN=1) restores engagement. */
307
+ TEST(InterleavedParity, SizeFloorDefersSmallDocsToClassic) {
308
+ const char* xml = "<r><a x='1'>t</a></r>";
309
+ set_env("LEPTRIS_INTERLEAVED", "1");
310
+ set_env("LEPTRIS_IL_STATS", "1");
311
+ unset_env("LEPTRIS_IL_MIN"); /* default floor: 64KB */
312
+
313
+ /* stderr capture, same pipe discipline as the canary spec. */
314
+ auto capture_one = [&](char* buf, size_t bufsz) -> void {
315
+ int fds[2];
316
+ ASSERT_EQ(pipe(fds), 0);
317
+ int saved = dup(STDERR_FILENO);
318
+ fflush(stderr);
319
+ dup2(fds[1], STDERR_FILENO);
320
+ close(fds[1]);
321
+ LeptrisStatus st = LEPTRIS_OK;
322
+ LeptrisDocument d = leptris_parse_string(xml, strlen(xml), &st);
323
+ if (d) leptris_document_free(d);
324
+ fflush(stderr);
325
+ dup2(saved, STDERR_FILENO);
326
+ close(saved);
327
+ ssize_t n = read(fds[0], buf, bufsz - 1);
328
+ if (n >= 0) buf[n] = '\0';
329
+ close(fds[0]);
330
+ };
331
+
332
+ char small[512] = {0};
333
+ capture_one(small, sizeof(small));
334
+ EXPECT_EQ(strstr(small, "il: records="), nullptr)
335
+ << "small doc must NOT engage the lane under the default floor: "
336
+ << small;
337
+
338
+ set_env("LEPTRIS_IL_MIN", "1");
339
+ char forced[512] = {0};
340
+ capture_one(forced, sizeof(forced));
341
+ EXPECT_NE(strstr(forced, "il: records="), nullptr)
342
+ << "forced floor must restore engagement";
343
+
344
+ unset_env("LEPTRIS_IL_STATS");
345
+ unset_env("LEPTRIS_IL_MIN");
346
+ unset_env("LEPTRIS_INTERLEAVED");
347
+ }
348
+ #endif /* !_WIN32: pipe/dup2 capture */
@@ -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.225",
4
+ "version": "1.9.228",
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.225.0
4
+ version: 1.9.228.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -823,6 +823,7 @@ files:
823
823
  - vendor-src/libleptris/test/html/nokogiri-tree-tests.dat
824
824
  - vendor-src/libleptris/test/html/test_html.cpp
825
825
  - vendor-src/libleptris/test/html/test_html5lib.cpp
826
+ - vendor-src/libleptris/test/html/test_html_borrowed_names.cpp
826
827
  - vendor-src/libleptris/test/html/test_html_builder.cpp
827
828
  - vendor-src/libleptris/test/memory/test_arena.cpp
828
829
  - vendor-src/libleptris/test/memory/test_doc_block.cpp