leptris 1.9.228.0 → 1.9.229.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -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 +23 -0
  17. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  18. data/vendor-src/libleptris/src/leptris/dom/compact.c +3 -1
  19. data/vendor-src/libleptris/src/leptris/dom/compact.h +13 -1
  20. data/vendor-src/libleptris/src/leptris/dom/element_modify.c +19 -13
  21. data/vendor-src/libleptris/src/leptris/dom/node_public.c +2 -2
  22. data/vendor-src/libleptris/src/leptris/dom/text.c +15 -5
  23. data/vendor-src/libleptris/src/leptris/dom/text.h +66 -11
  24. data/vendor-src/libleptris/src/leptris/dtd/validator.c +3 -1
  25. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +8 -7
  26. data/vendor-src/libleptris/src/leptris/html/html_parse.c +11 -11
  27. data/vendor-src/libleptris/src/leptris/serialize/serialize.c +5 -5
  28. data/vendor-src/libleptris/src/leptris/xinclude/xinclude.c +15 -10
  29. data/vendor-src/libleptris/src/leptris/xpath/evaluator_types.c +6 -2
  30. data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +13 -6
  31. data/vendor-src/libleptris/test/dom/test_text_borrowed.cpp +36 -35
  32. data/vendor-src/libleptris/vcpkg.json +1 -1
  33. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa53c1b21f30aa89e5992390c40deb360eb8d94d0000ca5346e381b35b086f87
4
- data.tar.gz: e667ca695bb8b10ecd251ad03d77521adacf6829cdd5bac7ec9c4f9bcb5b42a0
3
+ metadata.gz: 7f13ce441b0f9543d4a9f3d12ef01e802eb0883e3a9a3d11fe31bc91ac73509d
4
+ data.tar.gz: b78b9d374a05196a4f4c33f41b301f7ea1f9b9dec1c540495b3a19b556a25dbc
5
5
  SHA512:
6
- metadata.gz: 2927fc583597d9c607816ad16cb293a73c9ab7647a57b77429b8efbf8124b6e7ff52b871c25825b826d3a70501dbd51d4cc4b628c4992ea916825bd3f42845fa
7
- data.tar.gz: a4a6371fa8e9e461db91e4705308b6ab9bc1920d08a76cee7d32f964f75bf9c5e9d16751359e6159b39fe8c0d3a57f0a6ffe5560806114f5d36b142385cd9204
6
+ metadata.gz: eb8ff7b63f6e9cccd7a3489d3d46a69272df6909f7f807d001f19474a2e9dacfabdf9d70a60cf6feb4c47b39f53b3372233a9d5459e804f771c2f9516ce164f2
7
+ data.tar.gz: d42e6b9c9332f668b5d0b25909667e3bfa4860d6c88f07cff041ddd7287e980d97b755935210bad6653af251f7e8efd69903d50d9f85ae798fddd5cebeb389c6
data/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ 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.229.0] - 2026-09-23
9
+
10
+ ### Performance — engine sync (1.9.229)
11
+
12
+ - Vendored libleptris 1.9.229: the text node struct shrinks
13
+ 40 → 32 bytes on LP64 and ILP32 alike (#1285 slice 4b) — the
14
+ content pointer became an int32 self-relative offset with
15
+ doc-tagged overflow stamps (0 = NULL, shared sentinel for the
16
+ static empty string, byte offset otherwise; >2 GB deltas spill
17
+ to the overflow table tagged EXPLICIT). No binding-facing
18
+ surface changes; audit 351/351, suite 781/0.
19
+
8
20
  ## [1.9.228.0] - 2026-09-23
9
21
 
10
22
  ### Fixed — Text#content= on created-but-unattached text nodes (#318; engine 1.9.227–228)
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.228"
11
+ LIBLEPTRIS_VERSION = "1.9.229"
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.228.0"
4
+ VERSION = "1.9.229.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.228
4
+ - libleptris v1.9.229
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.229] - 2026-09-23
4
+
5
+ ### Performance
6
+
7
+ - **dom: text node 40 → 32 bytes — content pointer became an int32
8
+ self-relative offset, with doc-tagged overflow stamps (#1285
9
+ slice 4b).** The struct is base + five int32s on both LP64 and
10
+ ILP32 (content off, uint32 len, int32 next, int32 parent, int32
11
+ owner doc). Encoding: 0 = NULL, a shared sentinel = the static
12
+ empty string (xsl:strip-space writes land there instead of one
13
+ overflow-table entry per stripped node), any other value a byte
14
+ offset from the node. **Thread safety:** the rare >2 GB delta
15
+ spills to the overflow table tagged with the EXPLICIT owning
16
+ document — the direct-parse lane is overflow-table-free by design
17
+ (it never sets the TLS current-doc), so a TLS-tagged spill leaked
18
+ the entry past document teardown, which Linux CI ASAN/LSan
19
+ surfaced as aborting the two concurrency tests. Every content
20
+ stamp now carries its document; the untagged setter is deleted
21
+ from the codebase. Local gates: 1683/1683 both parse lanes; the
22
+ CI ASAN leg that caught the leak is green.
23
+
24
+
25
+
3
26
  ## [1.9.228] - 2026-09-23
4
27
 
5
28
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.228
6
+ VERSION 1.9.229
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -286,7 +286,9 @@ int32_t leptris_compact_int32_encode(void* base, void* target,
286
286
  const int32_t* field_addr) {
287
287
  if (!target) return 0;
288
288
  ptrdiff_t d = (char*)target - (char*)base;
289
- if (d < INT32_MIN || d > INT32_MAX) {
289
+ /* INT32_MAX is the EMPTY sentinel (compact.h) — spill exact
290
+ * deltas there too so raw offsets never collide with it. */
291
+ if (d < INT32_MIN || d >= INT32_MAX) {
290
292
  LeptrisCompactOverflowTable* table = get_overflow_table();
291
293
  struct leptris_document* doc = get_current_document();
292
294
  if (table && leptris_compact_overflow_set(table, field_addr,
@@ -127,6 +127,15 @@ int32_t leptris_compact_int32_encode_doc(void* base, void* target,
127
127
  * value is INT32_MIN). */
128
128
  #define LEPTRIS_INT32_OVERFLOW_SENTINEL_H ((int32_t)(-2147483647 - 1))
129
129
 
130
+ /* int32 sentinel for a non-NULL static "" pointer (#1285 slice 4b).
131
+ * Text content can be set to the empty string from far-away
132
+ * read-only storage (xsl:strip-space field writes); encoding that
133
+ * address would register one overflow-table entry per stripped
134
+ * node. Decoding returns a shared "" instead. Collision-free: the
135
+ * encode fast path below spills exact-INT32_MAX deltas to the
136
+ * overflow table, so no raw offset ever equals this value. */
137
+ #define LEPTRIS_COMPACT_INT32_EMPTY ((int32_t)2147483647)
138
+
130
139
  /* Inline fast paths (lane 18): the shared encode/decode live in
131
140
  * compact.c, so every tree-edge store or sibling/parent read paid an
132
141
  * out-of-line call — two per node in the parse loop and one per edge
@@ -140,7 +149,10 @@ static inline int32_t leptris_compact_int32_encode_inline(
140
149
  void* base, void* target, const int32_t* field_addr) {
141
150
  if (!target) return 0;
142
151
  ptrdiff_t d = (char*)target - (char*)base;
143
- if (d < INT32_MIN || d > INT32_MAX)
152
+ /* INT32_MAX is the EMPTY sentinel a real delta of exactly
153
+ * that size spills to the overflow table (correct, just not
154
+ * inline). */
155
+ if (d < INT32_MIN || d >= INT32_MAX)
144
156
  return leptris_compact_int32_encode(base, target, field_addr);
145
157
  return (int32_t)d;
146
158
  }
@@ -677,7 +677,8 @@ LeptrisStatus leptris_element_set_text(LeptrisElement elem, const char* text) {
677
677
  if (text) {
678
678
  /* Create new text node (even if empty to match pugixml behavior) */
679
679
  LeptrisMemoryPool* pool = leptris_element_get_pool(elem);
680
- LeptrisTextNode* text_node = leptris_text_create(text, strlen(text), pool);
680
+ LeptrisTextNode* text_node = leptris_text_create(text, strlen(text), pool,
681
+ leptris_element_get_document(elem));
681
682
  if (!text_node) {
682
683
  return LEPTRIS_ERROR_MEMORY;
683
684
  }
@@ -1341,9 +1342,10 @@ LeptrisElement leptris_element_append_copy(LeptrisElement parent, LeptrisElement
1341
1342
  LeptrisTextNode* text = (LeptrisTextNode*)child;
1342
1343
  /* SAFETY: Verify text pointer is valid before accessing content */
1343
1344
  if ((uintptr_t)text > 0x1000) {
1344
- LeptrisTextNode* text_copy = leptris_text_create(text->content,
1345
+ LeptrisTextNode* text_copy = leptris_text_create(leptris_textnode_content(text),
1345
1346
  text->content_len,
1346
- leptris_element_get_pool(copy));
1347
+ leptris_element_get_pool(copy),
1348
+ leptris_element_get_document(copy));
1347
1349
  if (text_copy) {
1348
1350
  leptris_element_append_child_internal(copy, (LeptrisNode*)text_copy);
1349
1351
  }
@@ -1507,9 +1509,10 @@ LeptrisElement leptris_element_prepend_copy(LeptrisElement parent, LeptrisElemen
1507
1509
  leptris_element_append_copy(copy, child);
1508
1510
  } else if (child_node->type == LEPTRIS_NODE_TYPE_TEXT) {
1509
1511
  LeptrisTextNode* text = (LeptrisTextNode*)child;
1510
- LeptrisTextNode* text_copy = leptris_text_create(text->content,
1512
+ LeptrisTextNode* text_copy = leptris_text_create(leptris_textnode_content(text),
1511
1513
  text->content_len,
1512
- leptris_element_get_pool(copy));
1514
+ leptris_element_get_pool(copy),
1515
+ leptris_element_get_document(copy));
1513
1516
  if (text_copy) {
1514
1517
  leptris_element_append_child_internal(copy, (LeptrisNode*)text_copy);
1515
1518
  }
@@ -1622,9 +1625,10 @@ LeptrisElement leptris_element_insert_copy_after(LeptrisElement sibling, Leptris
1622
1625
  leptris_element_append_copy(copy, child);
1623
1626
  } else if (child_node->type == LEPTRIS_NODE_TYPE_TEXT) {
1624
1627
  LeptrisTextNode* text = (LeptrisTextNode*)child;
1625
- LeptrisTextNode* text_copy = leptris_text_create(text->content,
1628
+ LeptrisTextNode* text_copy = leptris_text_create(leptris_textnode_content(text),
1626
1629
  text->content_len,
1627
- leptris_element_get_pool(copy));
1630
+ leptris_element_get_pool(copy),
1631
+ leptris_element_get_document(copy));
1628
1632
  if (text_copy) {
1629
1633
  leptris_element_append_child_internal(copy, (LeptrisNode*)text_copy);
1630
1634
  }
@@ -1737,9 +1741,10 @@ LeptrisElement leptris_element_insert_copy_before(LeptrisElement sibling, Leptri
1737
1741
  leptris_element_append_copy(copy, child);
1738
1742
  } else if (child_node->type == LEPTRIS_NODE_TYPE_TEXT) {
1739
1743
  LeptrisTextNode* text = (LeptrisTextNode*)child;
1740
- LeptrisTextNode* text_copy = leptris_text_create(text->content,
1744
+ LeptrisTextNode* text_copy = leptris_text_create(leptris_textnode_content(text),
1741
1745
  text->content_len,
1742
- leptris_element_get_pool(copy));
1746
+ leptris_element_get_pool(copy),
1747
+ leptris_element_get_document(copy));
1743
1748
  if (text_copy) {
1744
1749
  leptris_element_append_child_internal(copy, (LeptrisNode*)text_copy);
1745
1750
  }
@@ -1925,9 +1930,10 @@ static LeptrisElement leptris_element_copy_subtree_bulk_internal(
1925
1930
  } else if (child_node->type == LEPTRIS_NODE_TYPE_TEXT) {
1926
1931
  /* Copy text node (not bulk-allocated) */
1927
1932
  LeptrisTextNode* text = (LeptrisTextNode*)child;
1928
- LeptrisTextNode* text_copy = leptris_text_create(text->content,
1933
+ LeptrisTextNode* text_copy = leptris_text_create(leptris_textnode_content(text),
1929
1934
  text->content_len,
1930
- leptris_element_get_pool(copy));
1935
+ leptris_element_get_pool(copy),
1936
+ leptris_element_get_document(copy));
1931
1937
  if (text_copy) {
1932
1938
  /* Link as child using internal function */
1933
1939
  leptris_element_append_child_internal(copy, (LeptrisNode*)text_copy);
@@ -2066,8 +2072,8 @@ static LeptrisElement copy_subtree_detached(LeptrisElement source,
2066
2072
  copy, pool, doc);
2067
2073
  } else if (ty == LEPTRIS_NODE_TYPE_TEXT) {
2068
2074
  LeptrisTextNode* t = (LeptrisTextNode*)c;
2069
- cc = (LeptrisNodeRef)leptris_text_create(t->content,
2070
- t->content_len, pool);
2075
+ cc = (LeptrisNodeRef)leptris_text_create(leptris_textnode_content(t),
2076
+ t->content_len, pool, doc);
2071
2077
  if (cc) leptris_textnode_set_parent((LeptrisTextNode*)cc, copy);
2072
2078
  } else if (ty == LEPTRIS_NODE_TYPE_CDATA) {
2073
2079
  LeptrisCDATANode* cd = (LeptrisCDATANode*)c;
@@ -333,7 +333,7 @@ LEPTRIS_API LeptrisNodeRef leptris_text_node_create(LeptrisDocument doc,
333
333
  if (!doc->pool) return NULL;
334
334
  if (!content) content = "";
335
335
  size_t len = strlen(content);
336
- LeptrisTextNode* n = leptris_text_create(content, len, doc->pool);
336
+ LeptrisTextNode* n = leptris_text_create(content, len, doc->pool, doc);
337
337
  if (n) {
338
338
  /* #1320: stamp the owner so detached mutations can reach the
339
339
  * doc's pool (PI/comment/CDATA do this via owner_doc fields;
@@ -416,7 +416,7 @@ LEPTRIS_API LeptrisStatus leptris_text_node_set_content(LeptrisNodeRef node,
416
416
  if (!doc || !doc->pool) return LEPTRIS_ERROR_INVALID_ARG;
417
417
  char* copy = node_public_pool_strdup(doc->pool, content, len);
418
418
  if (!copy) return LEPTRIS_ERROR_MEMORY;
419
- t->content = copy;
419
+ leptris_textnode_set_content_ptr_doc(t, copy, doc);
420
420
  t->content_len = (uint32_t)len;
421
421
  return LEPTRIS_OK;
422
422
  }
@@ -18,7 +18,8 @@
18
18
  * CRITICAL: Content is NEVER trimmed — preserved exactly as given. */
19
19
  LeptrisTextNode* leptris_text_create(const char* content,
20
20
  size_t content_len,
21
- LeptrisMemoryPool* pool) {
21
+ LeptrisMemoryPool* pool,
22
+ struct leptris_document* doc) {
22
23
  if (!pool) return NULL;
23
24
 
24
25
  char* content_storage;
@@ -38,7 +39,10 @@ LeptrisTextNode* leptris_text_create(const char* content,
38
39
  memcpy(content_storage, content, content_len);
39
40
  }
40
41
  content_storage[content_len] = '\0';
41
- node->content = content_storage;
42
+ /* Contiguous with the node in the same pool alloc — the offset
43
+ * is a small positive delta; doc tags the oversized-content
44
+ * fallback should it ever leave the inline window. */
45
+ leptris_textnode_set_content_ptr_doc(node, content_storage, doc);
42
46
  node->content_len = (uint32_t)content_len;
43
47
 
44
48
  return node;
@@ -54,7 +58,8 @@ LeptrisTextNode* leptris_text_create(const char* content,
54
58
  * the parser's pool. */
55
59
  LeptrisTextNode* leptris_text_create_borrowed(const char* content,
56
60
  size_t content_len,
57
- LeptrisMemoryPool* pool) {
61
+ LeptrisMemoryPool* pool,
62
+ struct leptris_document* doc) {
58
63
  if (!pool) return NULL;
59
64
 
60
65
  LeptrisTextNode* node = (LeptrisTextNode*)leptris_pool_alloc(pool, sizeof(LeptrisTextNode));
@@ -67,7 +72,11 @@ LeptrisTextNode* leptris_text_create_borrowed(const char* content,
67
72
  * uninitialized raw bit silently flips DOE/cdata behavior by
68
73
  * build configuration. */
69
74
  node->base.raw = 0;
70
- node->content = (char*)content; /* Non-owning; caller guarantees lifetime + termination. */
75
+ /* Non-owning view into the doc-owned buffer; caller guarantees
76
+ * lifetime + termination (#1285 slice 4). The buffer-to-node
77
+ * distance crosses allocations — the doc-tagged stamp is
78
+ * MANDATORY here (TLS is unset on parse worker threads). */
79
+ leptris_textnode_set_content_ptr_doc(node, content, doc);
71
80
  node->content_len = (uint32_t)content_len;
72
81
  node->parent_off = 0;
73
82
  node->next_sibling_off = 0;
@@ -95,5 +104,6 @@ void leptris_text_free(LeptrisTextNode* text) {
95
104
  * here. */
96
105
  const char* leptris_text_get_content(LeptrisTextNode* text) {
97
106
  if (!text) return NULL;
98
- return text->content ? text->content : "";
107
+ const char* c = leptris_textnode_content(text);
108
+ return c ? c : "";
99
109
  }
@@ -11,12 +11,12 @@
11
11
  #include "node.h"
12
12
  #include "compact.h" /* compact-pointer helpers (TODO 121, TODO 178) */
13
13
 
14
- /* Text node - inherits from LeptrisNode. 48 bytes (#1285 slice 4,
15
- * was 64 — the pool/borrowed fields are gone because content is
16
- * ALWAYS NUL-terminated: every producer terminates in place, so
17
- * leptris_text_get_content is a plain field read).
14
+ /* Text node - inherits from LeptrisNode. 32 bytes (#1285 slice 4b +
15
+ * #1320, was 40 — the content pointer became an int32 self-relative
16
+ * byte offset and the detached-owner backref joined it; base 12 +
17
+ * five int32s on both LP64 and ILP32).
18
18
  *
19
- * #1285 slice 4 invariants:
19
+ * #1285 slice 4 invariants (unchanged):
20
20
  * - `content` is NUL-terminated at content[content_len] in ALL
21
21
  * cases — pooled copies (leptris_text_create) and in-place runs
22
22
  * into the doc-owned input buffer alike (every parse lane
@@ -25,6 +25,17 @@
25
25
  * (dp_text_create rejects beyond that); parse buffers that size
26
26
  * are outside every supported target.
27
27
  *
28
+ * #1285 slice 4b content_off encoding:
29
+ * - 0 = NULL (zero-init pool memory reads as no content).
30
+ * - LEPTRIS_COMPACT_INT32_EMPTY = the static "" (xsl:strip-space
31
+ * field writes point at a read-only literal far from any node;
32
+ * the shared sentinel avoids one overflow-table entry per
33
+ * stripped node).
34
+ * - anything else = byte offset from the node itself; deltas beyond
35
+ * int32 range spill to the overflow table (compact.h machinery,
36
+ * same as parent/sibling edges — ASLR can place the pool and the
37
+ * doc-owned input buffer > 2 GB apart).
38
+ *
28
39
  * Issue #168: parent_off mirrors next_sibling_off so the parent of a
29
40
  * text node can be queried in O(1).
30
41
  *
@@ -37,7 +48,7 @@
37
48
  * LP64 struct keeps its 40 bytes (padding absorbs it). */
38
49
  typedef struct leptris_text_node {
39
50
  LeptrisNode base; /* MUST be first */
40
- char* content; /* Text content - NEVER trim! NUL-terminated at [content_len] */
51
+ int32_t content_off; /* Self-relative offset to NUL-terminated content (0=NULL, EMPTY=static "") */
41
52
  uint32_t content_len; /* Byte length (excl. NUL); 4 GB cap */
42
53
  /* (#450) int32 sibling edge — was cp16 (±256 KB). Text nodes
43
54
  * link to ELEMENT siblings across the parse-time element↔text
@@ -61,28 +72,37 @@ typedef struct leptris_text_node {
61
72
  * have a length-bounded view). Passing 0 with non-NULL content is
62
73
  * treated as an empty string.
63
74
  *
75
+ * `doc` (#1285 slice 4b thread safety): the content offset is
76
+ * node-relative int32; the rare >2GB delta (oversized-content pool
77
+ * path under far-apart allocators) spills to the overflow table and
78
+ * MUST be tagged with the owning document — a NULL/TLS tag leaks the
79
+ * entry past doc teardown (Linux-ASAN/LSan catches it). Every caller
80
+ * has the doc.
81
+ *
64
82
  * Ownership: pool-allocated; leptris_document_free releases via pool. */
65
83
  LeptrisTextNode* leptris_text_create(const char* content,
66
84
  size_t content_len,
67
- LeptrisMemoryPool* pool);
85
+ LeptrisMemoryPool* pool,
86
+ struct leptris_document* doc);
68
87
 
69
88
  /* Create a text node borrowing the caller's bytes (parse hot path).
70
89
  *
71
90
  * The node struct is pool-allocated; `content` is stored as a
72
- * non-owning pointer into the caller's buffer — no memcpy. CALLER
91
+ * non-owning view into the caller's buffer — no memcpy. CALLER
73
92
  * CONTRACT (#1285 slice 4): the run MUST already be NUL-terminated
74
93
  * at content[content_len] and must outlive the document (parse
75
94
  * lanes terminate the run in the doc-owned input copy before
76
- * carving the node). */
95
+ * carving the node). `doc` tags the overflow entry when the
96
+ * buffer-to-node distance exceeds int32 range (see leptris_text_create). */
77
97
  LeptrisTextNode* leptris_text_create_borrowed(const char* content,
78
98
  size_t content_len,
79
- LeptrisMemoryPool* pool);
99
+ LeptrisMemoryPool* pool,
100
+ struct leptris_document* doc);
80
101
 
81
102
  void leptris_text_free(LeptrisTextNode* text);
82
103
 
83
104
  /* Content access */
84
105
  const char* leptris_text_get_content(LeptrisTextNode* text);
85
- void leptris_text_set_content(LeptrisTextNode* text, const char* content);
86
106
 
87
107
  /* Casting helpers */
88
108
  #define LEPTRIS_NODE_AS_TEXT(node) \
@@ -119,6 +139,41 @@ static inline void leptris_textnode_set_parent(LeptrisTextNode* t, LeptrisElemen
119
139
  t->parent_off = leptris_compact_int32_encode_inline(t, parent, &t->parent_off);
120
140
  }
121
141
 
142
+ /* Compact content accessors (#1285 slice 4b). The content pointer
143
+ * is a self-relative int32 byte offset — same encoding as the edges
144
+ * above. Writers that store an empty string land on the shared
145
+ * EMPTY sentinel; pooled and in-place runs are a small positive
146
+ * offset from the node (the create paths allocate struct + content
147
+ * contiguously); far targets (static literals, cross-block buffer
148
+ * runs) spill to the overflow table. */
149
+ static inline const char* leptris_textnode_content(const LeptrisTextNode* t) {
150
+ if (!t || t->content_off == 0) return NULL;
151
+ if (t->content_off == LEPTRIS_COMPACT_INT32_EMPTY) return "";
152
+ return (const char*)leptris_compact_int32_decode_inline(
153
+ (void*)t, t->content_off, &t->content_off);
154
+ }
155
+
156
+ /* Doc-tagged stamp (#1285 slice 4b thread safety). Identical to
157
+ * set_content_ptr except the >2GB spill registers in the overflow
158
+ * table tagged with the EXPLICIT document (the TLS current-doc slot
159
+ * is unset on parse worker threads — dp is overflow-table-free by
160
+ * design — so a TLS-tagged entry leaks past doc teardown and Linux
161
+ * CI ASAN/LSan aborts the run). Parse-lane and builder sites MUST
162
+ * use this form; the pooled-create contiguous path never spills but
163
+ * takes doc anyway so the oversized-content fallback is covered. */
164
+ static inline void leptris_textnode_set_content_ptr_doc(
165
+ LeptrisTextNode* t, const char* p, struct leptris_document* doc) {
166
+ if (!t) return;
167
+ if (!p) { t->content_off = 0; return; }
168
+ if (*p == '\0') { t->content_off = LEPTRIS_COMPACT_INT32_EMPTY; return; }
169
+ ptrdiff_t d = (char*)p - (char*)t;
170
+ if (d < INT32_MIN || d >= INT32_MAX)
171
+ t->content_off = leptris_compact_int32_encode_doc(
172
+ t, (void*)p, &t->content_off, doc);
173
+ else
174
+ t->content_off = (int32_t)d;
175
+ }
176
+
122
177
  /* Owning-document access (issue #1320). The stamp is written by
123
178
  * leptris_text_node_create with the explicit-doc encoder (an
124
179
  * overflow-table entry made outside a parse context must carry the
@@ -173,8 +173,10 @@ static int element_has_char_content(LeptrisElement elem) {
173
173
  unsigned t = leptris_node_get_type(child);
174
174
  if (t == LEPTRIS_NODE_TYPE_TEXT) {
175
175
  const LeptrisTextNode* tn = (const LeptrisTextNode*)child;
176
+ const char* tc = leptris_textnode_content(tn);
177
+ if (!tc) continue;
176
178
  for (size_t i = 0; i < tn->content_len; i++) {
177
- if (!isspace((unsigned char)tn->content[i])) return 1;
179
+ if (!isspace((unsigned char)tc[i])) return 1;
178
180
  }
179
181
  } else if (t == LEPTRIS_NODE_TYPE_CDATA) {
180
182
  const LeptrisCDATANode* cn = (const LeptrisCDATANode*)child;
@@ -991,7 +991,7 @@ static inline LeptrisTextNode* dp_text_create(DParser* p,
991
991
  p->pool, 128 * sizeof(LeptrisTextNode));
992
992
  if (!tn) {
993
993
  return leptris_text_create_borrowed(content, content_len,
994
- p->pool);
994
+ p->pool, p->doc);
995
995
  }
996
996
  p->text_end = tn + 128;
997
997
  /* cursor == tn: carve below */
@@ -1010,7 +1010,7 @@ static inline LeptrisTextNode* dp_text_create(DParser* p,
1010
1010
  * carve (dispatch-entry dp_nul / walker '&' NUL / EOF sentinel)
1011
1011
  * — the terminator write lives with the byte's last reader, not
1012
1012
  * here. */
1013
- tn->content = (char*)content;
1013
+ leptris_textnode_set_content_ptr_doc(tn, content, p->doc);
1014
1014
  tn->content_len = (uint32_t)content_len;
1015
1015
  tn->parent_off = 0;
1016
1016
  tn->next_sibling_off = 0;
@@ -1443,7 +1443,7 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1443
1443
  leptris_decode_entities_view(&ssv, pool);
1444
1444
  if (sexp) {
1445
1445
  rt = leptris_text_create(
1446
- sexp, strlen(sexp), pool);
1446
+ sexp, strlen(sexp), pool, p.doc);
1447
1447
  }
1448
1448
  }
1449
1449
  if (!rt) {
@@ -1482,7 +1482,7 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1482
1482
  LeptrisStringView ssv = leptris_sv_from_ptr(seg, slen);
1483
1483
  char* sexp = leptris_decode_entities_view(&ssv, pool);
1484
1484
  if (sexp) {
1485
- rt = leptris_text_create(sexp, strlen(sexp), pool);
1485
+ rt = leptris_text_create(sexp, strlen(sexp), pool, p.doc);
1486
1486
  }
1487
1487
  }
1488
1488
  if (!rt) {
@@ -1512,7 +1512,7 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1512
1512
  &sv, p.dtd, pool)
1513
1513
  : leptris_decode_entities_view(&sv, pool);
1514
1514
  if (expanded) {
1515
- tn = leptris_text_create(expanded, strlen(expanded), pool);
1515
+ tn = leptris_text_create(expanded, strlen(expanded), pool, p.doc);
1516
1516
  } else {
1517
1517
  tn = dp_text_create(&p, text_start, tlen);
1518
1518
  }
@@ -2592,7 +2592,7 @@ static struct leptris_document* dp_il_build(
2592
2592
  tn->base.type = LEPTRIS_NODE_TYPE_TEXT;
2593
2593
  tn->base.frozen = 1;
2594
2594
  tn->base.line = r->line;
2595
- tn->content = (char*)content;
2595
+ leptris_textnode_set_content_ptr_doc(tn, content, doc);
2596
2596
  tn->content_len = r->len;
2597
2597
  /* (slice 4: pool/borrowed fields gone; the il scratch
2598
2598
  * run is already NUL-terminated at carve) */
@@ -2639,7 +2639,8 @@ oom_early:
2639
2639
  int leptris_il_run(const char* xml, size_t len, int drop_ws,
2640
2640
  IlCtx* c, char** scratch_out, uint32_t* root_out) {
2641
2641
  *scratch_out = NULL;
2642
- c->recs = c->attrs = NULL;
2642
+ c->recs = NULL;
2643
+ c->attrs = NULL;
2643
2644
  c->nrec = c->crec = c->nattr = c->cattr = 0;
2644
2645
  c->depth = 0;
2645
2646
  if (!xml || len == 0 || len >= 0x7FFFFFFFu) return 0;
@@ -3908,7 +3908,7 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
3908
3908
  w9++;
3909
3909
  if (t9 && w9 && t9[w9]) {
3910
3910
  LeptrisTextNode* tn9 = (LeptrisTextNode*)n;
3911
- tn9->content = t9 + w9;
3911
+ leptris_textnode_set_content_ptr_doc(tn9, t9 + w9, b->doc);
3912
3912
  tn9->content_len -= w9;
3913
3913
  } else if (t9 && w9 && !t9[w9]) {
3914
3914
  return; /* pure before-head ws: dropped (tests7:7) */
@@ -3986,7 +3986,7 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
3986
3986
  if (ct && ct[cw] && cw > 0) {
3987
3987
  LeptrisTextNode* tail =
3988
3988
  leptris_text_create(ct + cw, strlen(ct + cw),
3989
- b->pool);
3989
+ b->pool, b->doc);
3990
3990
  ((char*)ct)[cw] = '\0';
3991
3991
  ((LeptrisTextNode*)n)->content_len = cw;
3992
3992
  leptris_element_append_child_internal_doc(
@@ -4916,7 +4916,7 @@ head_end =
4916
4916
  if (w2 > tx && *w2) {
4917
4917
  LeptrisTextNode* tail =
4918
4918
  leptris_text_create(
4919
- w2, strlen(w2), b->pool);
4919
+ w2, strlen(w2), b->pool, b->doc);
4920
4920
  if (tail) {
4921
4921
  leptris_node_set_next_sibling(
4922
4922
  (LeptrisNodeRef)tail,
@@ -5438,12 +5438,12 @@ static LeptrisTextNode* h_text_node(HBuilder* b, const char* s,
5438
5438
  }
5439
5439
  }
5440
5440
  *(char*)e = '\0';
5441
- return leptris_text_create_borrowed(s, len, b->pool);
5441
+ return leptris_text_create_borrowed(s, len, b->pool, b->doc);
5442
5442
  }
5443
5443
  size_t dlen = 0;
5444
5444
  char* dec = h_decode_text(b, s, e, &dlen);
5445
5445
  if (!dec || !*dec) return NULL;
5446
- return leptris_text_create(dec, dlen, b->pool);
5446
+ return leptris_text_create(dec, dlen, b->pool, b->doc);
5447
5447
  }
5448
5448
 
5449
5449
  static LeptrisDocument html_parse_shared(
@@ -5783,13 +5783,13 @@ static LeptrisDocument html_parse_shared(
5783
5783
  }
5784
5784
  }
5785
5785
  LeptrisTextNode* t =
5786
- leptris_text_create(buf, o, b.pool);
5786
+ leptris_text_create(buf, o, b.pool, b.doc);
5787
5787
  if (t)
5788
5788
  h_append(&b, (LeptrisNodeRef)t);
5789
5789
  }
5790
5790
  } else {
5791
5791
  LeptrisTextNode* t = leptris_text_create(
5792
- cs, clen, b.pool);
5792
+ cs, clen, b.pool, b.doc);
5793
5793
  if (t) h_append(&b, (LeptrisNodeRef)t);
5794
5794
  }
5795
5795
  }
@@ -5838,7 +5838,7 @@ static LeptrisDocument html_parse_shared(
5838
5838
  if (t) h_append(&b, (LeptrisNodeRef)t);
5839
5839
  }
5840
5840
  LeptrisTextNode* t =
5841
- leptris_text_create("</", 2, b.pool);
5841
+ leptris_text_create("</", 2, b.pool, b.doc);
5842
5842
  if (t) h_append(&b, (LeptrisNodeRef)t);
5843
5843
  p = end;
5844
5844
  text = p;
@@ -7907,7 +7907,7 @@ static LeptrisDocument html_parse_shared(
7907
7907
  b.whatwg, &dlen);
7908
7908
  if (dec && dlen) {
7909
7909
  LeptrisTextNode* t = leptris_text_create(
7910
- dec, dlen, b.pool);
7910
+ dec, dlen, b.pool, b.doc);
7911
7911
  if (t)
7912
7912
  leptris_element_append_child_internal_doc(
7913
7913
  e, (LeptrisNodeRef)t, b.doc);
@@ -7917,14 +7917,14 @@ static LeptrisDocument html_parse_shared(
7917
7917
  h_nul_fffd_copy(b.pool, cs, clen, eof_fffd);
7918
7918
  if (mapped && mapped[0]) {
7919
7919
  LeptrisTextNode* t = leptris_text_create(
7920
- mapped, strlen(mapped), b.pool);
7920
+ mapped, strlen(mapped), b.pool, b.doc);
7921
7921
  if (t)
7922
7922
  leptris_element_append_child_internal_doc(
7923
7923
  et, (LeptrisNodeRef)t, b.doc);
7924
7924
  }
7925
7925
  } else {
7926
7926
  LeptrisTextNode* t = leptris_text_create(
7927
- cs, clen, b.pool);
7927
+ cs, clen, b.pool, b.doc);
7928
7928
  if (t)
7929
7929
  leptris_element_append_child_internal_doc(
7930
7930
  et, (LeptrisNodeRef)t, b.doc);
@@ -937,7 +937,7 @@ static int ser_children_have_text(LeptrisNode* fc) {
937
937
  * entity-decoded at parse time — length-bounded read,
938
938
  * no serve-mode branch. */
939
939
  LeptrisTextNode* tn = (LeptrisTextNode*)c;
940
- const char* tc = tn->content;
940
+ const char* tc = leptris_textnode_content(tn);
941
941
  size_t tl = tc ? tn->content_len : 0;
942
942
  for (size_t i = 0; i < tl; i++) {
943
943
  if (tc[i] != ' ' && tc[i] != '\t' &&
@@ -1016,7 +1016,7 @@ void serialize_element_internal(LeptrisElement root_elem, SerializeBuffer* buf,
1016
1016
  cur->type == LEPTRIS_NODE_TYPE_TEXT &&
1017
1017
  !((sp > 0) && st[sp - 1].mixed)) {
1018
1018
  LeptrisTextNode* wsn = (LeptrisTextNode*)cur;
1019
- const char* wsc = wsn->content;
1019
+ const char* wsc = leptris_textnode_content(wsn);
1020
1020
  size_t wsl = wsc ? wsn->content_len : 0;
1021
1021
  int ws_only = 1;
1022
1022
  for (size_t i = 0; i < wsl; i++) {
@@ -1033,7 +1033,7 @@ void serialize_element_internal(LeptrisElement root_elem, SerializeBuffer* buf,
1033
1033
  if (buf->indent_text && buf->indent_spaces > 0 &&
1034
1034
  cur->type == LEPTRIS_NODE_TYPE_TEXT) {
1035
1035
  LeptrisTextNode* itn = (LeptrisTextNode*)cur;
1036
- const char* isc = itn->content;
1036
+ const char* isc = leptris_textnode_content(itn);
1037
1037
  size_t isl = isc ? itn->content_len : 0;
1038
1038
  int ws_only = 1;
1039
1039
  for (size_t k = 0; k < isl; k++) {
@@ -1174,7 +1174,7 @@ void serialize_element_internal(LeptrisElement root_elem, SerializeBuffer* buf,
1174
1174
  * NUL-terminate, which the escaper doesn't need
1175
1175
  * (it's length-bounded). Entity-bearing text still
1176
1176
  * materializes for correct expansion. */
1177
- const char* tc0 = tn0->content;
1177
+ const char* tc0 = leptris_textnode_content(tn0);
1178
1178
  size_t tl0 = tc0 ? tn0->content_len : 0;
1179
1179
  /* Pretty mode fuses newline + indent + open + text +
1180
1180
  * close into the same single reservation — leaves
@@ -1545,7 +1545,7 @@ void serialize_element_internal(LeptrisElement root_elem, SerializeBuffer* buf,
1545
1545
  }
1546
1546
  if (buf->indent_spaces == 0) {
1547
1547
  LeptrisTextNode* tn = (LeptrisTextNode*)fc;
1548
- const char* tc = tn->content;
1548
+ const char* tc = leptris_textnode_content(tn);
1549
1549
  size_t tlen = tc ? tn->content_len : 0;
1550
1550
  buffer_ensure_capacity(buf, 2 + epl + nl + 6 * tlen + 2 + 1);
1551
1551
  char* te = buf->data + buf->size;
@@ -121,10 +121,12 @@ static char* load_file_content(const char* path, size_t* out_len) {
121
121
  * Returns NULL on allocation failure (partial copies are abandoned
122
122
  * along with the pool, which is freed document-scoped). */
123
123
  static LeptrisNode* deep_copy_node(const LeptrisNode* src,
124
- LeptrisMemoryPool* target_pool);
124
+ LeptrisMemoryPool* target_pool,
125
+ struct leptris_document* target_doc);
125
126
 
126
127
  static LeptrisElement deep_copy_element(const LeptrisElement src,
127
- LeptrisMemoryPool* target_pool) {
128
+ LeptrisMemoryPool* target_pool,
129
+ struct leptris_document* target_doc) {
128
130
  const char* name = leptris_element_get_name(src);
129
131
  LeptrisElement dst = leptris_element_create_pooled(name, target_pool);
130
132
  if (!dst) return NULL;
@@ -144,7 +146,7 @@ static LeptrisElement deep_copy_element(const LeptrisElement src,
144
146
  child;
145
147
  child = leptris_node_get_next_sibling(child)) {
146
148
 
147
- LeptrisNode* child_copy = deep_copy_node(child, target_pool);
149
+ LeptrisNode* child_copy = deep_copy_node(child, target_pool, target_doc);
148
150
  if (!child_copy) return NULL;
149
151
 
150
152
  if (prev_copy) {
@@ -161,18 +163,21 @@ static LeptrisElement deep_copy_element(const LeptrisElement src,
161
163
  }
162
164
 
163
165
  static LeptrisNode* deep_copy_node(const LeptrisNode* src,
164
- LeptrisMemoryPool* target_pool) {
166
+ LeptrisMemoryPool* target_pool,
167
+ struct leptris_document* target_doc) {
165
168
  if (!src) return NULL;
166
169
 
167
170
  switch (src->type) {
168
171
  case LEPTRIS_NODE_TYPE_ELEMENT:
169
- return (LeptrisNode*)deep_copy_element((LeptrisElement)src, target_pool);
172
+ return (LeptrisNode*)deep_copy_element((LeptrisElement)src, target_pool, target_doc);
170
173
 
171
174
  case LEPTRIS_NODE_TYPE_TEXT: {
172
175
  const LeptrisTextNode* t = (const LeptrisTextNode*)src;
173
- /* Source text may be borrowed (non-NUL-terminated) — content_len
174
- * is authoritative. TODO 115 Phase B. */
175
- return (LeptrisNode*)leptris_text_create(t->content, t->content_len, target_pool);
176
+ /* #1285 slice 4: runs are always NUL-terminated;
177
+ * content_len is authoritative. TODO 115 Phase B. */
178
+ return (LeptrisNode*)leptris_text_create(leptris_textnode_content(t),
179
+ t->content_len, target_pool,
180
+ target_doc);
176
181
  }
177
182
 
178
183
  case LEPTRIS_NODE_TYPE_CDATA: {
@@ -725,7 +730,7 @@ static int process_element_xinclude(LeptrisElement elem,
725
730
  content_len = conv_len;
726
731
  #endif
727
732
  }
728
- substitute = (LeptrisNode*)leptris_text_create(content, content_len, doc->pool);
733
+ substitute = (LeptrisNode*)leptris_text_create(content, content_len, doc->pool, doc);
729
734
  /* parse="text" doesn't recurse into another file's body
730
735
  * (no risk of A -> B -> A via text), so no push/pop here. */
731
736
  }
@@ -744,7 +749,7 @@ fallback:
744
749
  if (fb) {
745
750
  char* fb_text = leptris_element_get_text_content(fb);
746
751
  if (fb_text) {
747
- substitute = (LeptrisNode*)leptris_text_create(fb_text, strlen(fb_text), doc->pool);
752
+ substitute = (LeptrisNode*)leptris_text_create(fb_text, strlen(fb_text), doc->pool, doc);
748
753
  leptris_free(fb_text);
749
754
  }
750
755
  }
@@ -175,8 +175,12 @@ char* get_node_text(void* node) {
175
175
  int ty = leptris_node_get_type(c);
176
176
  if (ty == LEPTRIS_NODE_TYPE_TEXT ||
177
177
  ty == LEPTRIS_NODE_TYPE_CDATA) {
178
- const char* t =
179
- leptris_text_get_content((LeptrisTextNode*)c);
178
+ /* #1285 slice 4b: text and CDATA structs no
179
+ * longer share `content`'s offset — use the
180
+ * matching accessor. */
181
+ const char* t = (ty == LEPTRIS_NODE_TYPE_CDATA)
182
+ ? leptris_cdata_get_content((LeptrisCDATANode*)c)
183
+ : leptris_text_get_content((LeptrisTextNode*)c);
180
184
  if (t && *t) {
181
185
  any_text = 1;
182
186
  size_t tl = strlen(t);
@@ -460,7 +460,9 @@ static char* string_value_deep(LeptrisElement e) {
460
460
  LeptrisNodeRef n = stack[--sp];
461
461
  int ty = leptris_node_get_type(n);
462
462
  if (ty == LEPTRIS_NODE_TYPE_TEXT || ty == LEPTRIS_NODE_TYPE_CDATA) {
463
- const char* t = leptris_text_get_content((LeptrisTextNode*)n);
463
+ const char* t = (ty == LEPTRIS_NODE_TYPE_CDATA)
464
+ ? leptris_cdata_get_content((LeptrisCDATANode*)n)
465
+ : leptris_text_get_content((LeptrisTextNode*)n);
464
466
  if (t) {
465
467
  size_t tl = strlen(t);
466
468
  if (len + tl + 1 > cap) {
@@ -2734,8 +2736,12 @@ static int copy_node_deep(XsltExec* ex, LeptrisElement node,
2734
2736
  if (ty == LEPTRIS_NODE_TYPE_ELEMENT) {
2735
2737
  copy_node_deep(ex, (LeptrisElement)c, e);
2736
2738
  } else if (ty == LEPTRIS_NODE_TYPE_CDATA) {
2739
+ /* #1285 slice 4b: text and CDATA structs no longer
2740
+ * share `content`'s offset — use the CDATA accessor on
2741
+ * CDATA nodes; the cross-cast `text_get_content((TextNode*)c)`
2742
+ * reads garbage here. */
2737
2743
  const char* t =
2738
- leptris_text_get_content((LeptrisTextNode*)c);
2744
+ leptris_cdata_get_content((LeptrisCDATANode*)c);
2739
2745
  LeptrisNodeRef cc = (LeptrisNodeRef)leptris_cdata_create(
2740
2746
  t, t ? strlen(t) : 0,
2741
2747
  ((struct leptris_document*)ex->result)->pool);
@@ -3035,11 +3041,12 @@ static void strip_source_whitespace(XsltExec* ex) {
3035
3041
  if (!ws_only(t)) continue;
3036
3042
  /* Empty in place — never unlink (compact-parse text
3037
3043
  * nodes link via int32 offsets that unlinking can
3038
- * orphan for following siblings). Direct field write
3039
- * to a static "" keeps the node readable and the
3040
- * pointer NUL-terminated (#1285 slice 4 contract). */
3044
+ * orphan for following siblings). Store the shared
3045
+ * EMPTY sentinel: the node stays readable and the
3046
+ * content decodes to a NUL-terminated "" (#1285 slice
3047
+ * 4b — no overflow-table entry per stripped node). */
3041
3048
  LeptrisTextNode* tn = (LeptrisTextNode*)c;
3042
- tn->content = (char*)"";
3049
+ leptris_textnode_set_content_ptr_doc(tn, "", ex->result);
3043
3050
  tn->content_len = 0;
3044
3051
  }
3045
3052
  }
@@ -36,45 +36,47 @@ TEST(TextBorrowed, ParsedTextNodeIsTerminatedInPlace) {
36
36
  LeptrisTextNode* text = (LeptrisTextNode*)child;
37
37
  EXPECT_EQ(text->content_len, std::strlen("hello world"));
38
38
  /* #1285 slice 4: the run is NUL-terminated IN PLACE in the
39
- * doc-owned input copy — no pool copy, no serve mode. */
40
- ASSERT_NE(text->content, nullptr);
41
- EXPECT_EQ(text->content[text->content_len], '\0')
39
+ * doc-owned input copy — no pool copy, no serve mode. #1285
40
+ * slice 4b: content is an int32 self-relative offset; decode
41
+ * twice — the pointer must be stable across calls. */
42
+ const char* c1 = leptris_textnode_content(text);
43
+ const char* c2 = leptris_textnode_content(text);
44
+ ASSERT_NE(c1, nullptr);
45
+ EXPECT_EQ(c1, c2) << "decode is deterministic";
46
+ EXPECT_EQ(c1[text->content_len], '\0')
42
47
  << "run must be terminated at [content_len]";
43
- /* get_content is a plain field read: same pointer, stable. */
44
- EXPECT_EQ(leptris_text_get_content(text), text->content);
48
+ EXPECT_EQ(leptris_text_get_content(text), c1);
45
49
  EXPECT_STREQ(leptris_text_get_content(text), "hello world");
46
50
 
47
51
  leptris_document_free(doc);
48
52
  }
49
53
 
50
- TEST(TextBorrowed, NodeStructIsBasePointerAndThreeInt32) {
51
- /* #1285 slice 4: pool + borrowed removed, content_len is uint32.
52
- * The width-independent invariant: base + ONE content pointer +
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
- * parse-time bulk strides (dp text block, il lane table) hardcode
58
- * this layout economics a silent field growth would erode the
59
- * #1222 parse win this slice exists for. */
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
- 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);
54
+ TEST(TextBorrowed, NodeStructIsBaseAndFiveInt32) {
55
+ /* #1285 slice 4b + #1320: the content pointer became an int32
56
+ * self-relative byte offset, and the detached-owner backref
57
+ * (#1320) joined it as a fifth int32. Width-independent
58
+ * invariant: base + FIVE int32s (content off, uint32 len,
59
+ * int32 next, int32 parent, int32 owner doc), int32-aligned
60
+ * nothing else. 40 -> 32 on LP64; 32 on ILP32. Encoding:
61
+ * content_off 0 = NULL, LEPTRIS_COMPACT_INT32_EMPTY = static ""
62
+ * (ws-strip sentinel, no overflow-table entry per stripped
63
+ * node), any other value = self-relative offset with the
64
+ * overflow-table fallback (same machinery as the tree edges);
65
+ * owner_doc_off 0 = unstamped (parse-carved nodes attach). The
66
+ * parse-time bulk strides (dp text block, il lane table)
67
+ * hardcode this layout economics — a silent field growth would
68
+ * erode the #1222 parse win. */
69
+ /* C++11: no constexpr lambda inline the round-ups. */
70
+ constexpr size_t kA = alignof(int32_t);
71
+ constexpr size_t kRaw = sizeof(LeptrisNode) + 5 * sizeof(int32_t);
67
72
  constexpr size_t kAligned = (kRaw + kA - 1) / kA * kA;
68
73
  static_assert(sizeof(LeptrisTextNode) == kAligned,
69
- "LeptrisTextNode must be base + one content pointer + "
74
+ "LeptrisTextNode must be base + int32 content off + "
70
75
  "uint32 len + int32 next + int32 parent + int32 owner "
71
- "(aligned) — no pool/borrowed fields (#1285 slice 4, #1320)");
76
+ "(int32-aligned) — no content pointer "
77
+ "(#1285 slice 4b, #1320)");
72
78
  EXPECT_EQ(sizeof(LeptrisTextNode), kAligned);
73
- #if defined(__LP64__) || defined(_WIN64)
74
- EXPECT_EQ(sizeof(LeptrisTextNode), (size_t)40);
75
- #else
76
79
  EXPECT_EQ(sizeof(LeptrisTextNode), (size_t)32);
77
- #endif
78
80
  }
79
81
 
80
82
  TEST(TextBorrowed, PublicAccessorsReturnCorrectContent) {
@@ -143,8 +145,9 @@ TEST(TextBorrowed, MaterializationIsStableAcrossCalls) {
143
145
 
144
146
  LeptrisNodeRef child = leptris_node_first_child(leptris_element_as_node(root));
145
147
  LeptrisTextNode* text = (LeptrisTextNode*)child;
146
- EXPECT_EQ(leptris_text_get_content(text), text->content)
147
- << "get_content is a plain field read (#1285 slice 4)";
148
+ EXPECT_EQ(leptris_text_get_content(text),
149
+ leptris_textnode_content(text))
150
+ << "get_content decodes the same offset (#1285 slice 4b)";
148
151
 
149
152
  const char* second = leptris_element_text(root);
150
153
  EXPECT_EQ(second, first) << "repeated calls return the same pointer";
@@ -183,14 +186,12 @@ TEST(TextBorrowed, ServeContractHoldsInBothParseLanes) {
183
186
  LeptrisTextNode* text = (LeptrisTextNode*)child;
184
187
  EXPECT_EQ(text->content_len, std::strlen("hello world"));
185
188
  /* terminated in place in BOTH lanes (#1285 slice 4) */
186
- ASSERT_NE(text->content, nullptr);
187
- EXPECT_EQ(text->content[text->content_len], '\0');
188
-
189
189
  const char* a = leptris_text_get_content(text);
190
190
  ASSERT_NE(a, nullptr);
191
+ EXPECT_EQ(a[text->content_len], '\0');
191
192
  EXPECT_STREQ(a, "hello world");
192
- EXPECT_EQ(a, text->content)
193
- << "get_content returns the in-place pointer in both lanes";
193
+ EXPECT_EQ(a, leptris_textnode_content(text))
194
+ << "get_content returns the in-place view in both lanes";
194
195
  EXPECT_STREQ(leptris_element_text(root), "hello world");
195
196
 
196
197
  leptris_document_free(doc);
@@ -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.228",
4
+ "version": "1.9.229",
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.228.0
4
+ version: 1.9.229.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.