leptris 1.9.209.1 → 1.9.210.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7f9ee6f6fa740c91bbb444f605bd68fcae043d8184f220774e89b749eb521fc
4
- data.tar.gz: b22ed76f401235b0731cbeeeec011655a07ffd189e02a25dd675659fd076798f
3
+ metadata.gz: 6a8b73b57e537bd42a2f62759cd8b36fb842ed36c5046894453ae4b6c1540ca0
4
+ data.tar.gz: b089afc26fe2c7d8e8396a25a88c00baaa947c732896cf3aa70097c3b88b4302
5
5
  SHA512:
6
- metadata.gz: a9bd90a1a9d941ad5814a13dd659213de9ab86e21e38a8064b681d6d127fe91c935ba5635a2b2e4938699769aca76953e8bcb4db42fee893eef5ab6f55166cb3
7
- data.tar.gz: f261d4206b02aefa6bfbc57866da62f82c627059fba9fdbab640178e3e7ab6cf19272b6590f20587e50efe170872368930ce5f117d604f1c9c225746643d3cf9
6
+ metadata.gz: 1661a5db0ca010813f02bb90a24537fd92c5849cb797775ec7bb98c5217144bbdf95b8eca4ae3ec0a42a18f7cbebc57fd13b33500b1e402c69f903de4be85bff
7
+ data.tar.gz: 6724e22c525398f6ecead93260b6716188242a5f3722bc29fc6810bb00d89f66e8fc2a512729a7915b3be001b0a7c9b3dbbc2250623a22b528a819cc3f7faa49
data/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ 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.210.0] - 2026-09-20
9
+
10
+ ### Performance — engine sync (1.9.210)
11
+
12
+ - Vendored libleptris 1.9.210: the node-layout fork's document
13
+ block (slice 1, #1253) — two contiguous geometrically-grown
14
+ regions per document (dense node array + string arena) with
15
+ region-relative byte-offset edges, so growth never invalidates
16
+ an offset; and the parse-arena retain ceiling raised 32 → 96MB
17
+ with a thread-cleanup drain (#1222/#1238 lane): repeated large
18
+ parses stop freeing/re-mallocing tens of MB per cycle. Measured
19
+ engine-side: 13MB attr shape 341 → 472 MB/s (+38%); the text
20
+ shape's size-decline eliminated (~605-627 flat across
21
+ 1.5-13MB). No binding-facing surface changes; audit holds.
22
+ - Note: #1242 was closed upstream before our strikes 7-10
23
+ forensics landed; reopened with the post-fix evidence (the
24
+ 1.9.208 memo clear is incomplete — direct-call NULL + empty
25
+ chain on a live element; set_root cross-document false
26
+ positive on windows-4.0). The ubuntu-3.4 gate and armed
27
+ forensics stay until the engine ships the residual fix.
28
+
8
29
  ## [1.9.209.0] - 2026-09-20
9
30
 
10
31
  ### Performance — engine sync (1.9.209)
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.209"
11
+ LIBLEPTRIS_VERSION = "1.9.210"
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
@@ -117,6 +117,35 @@ task :compile do
117
117
  rm_rf([File.join(dest, "build"), File.join(dest, "prefix")])
118
118
  end
119
119
 
120
+ # Rebuild notes ride every gem beside the sources (packaging
121
+ # doctrine): the exact pins and the commands to reproduce the
122
+ # vendored libraries. The ruby variant's extconf runs these
123
+ # trees automatically at install time.
124
+ File.write(File.join(vsrc, "README.md"), <<~README)
125
+ Sources vendored in this gem (recompile rights; the ruby
126
+ variant builds them at install via extconf.rb):
127
+
128
+ - libleptris v#{LIBLEPTRIS_VERSION}
129
+ - utf8proc v#{UTF8PROC_VERSION}
130
+
131
+ Rebuild by hand:
132
+
133
+ cmake -B vendor-src/utf8proc/build -S vendor-src/utf8proc \
134
+ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
135
+ -DUTF8PROC_ENABLE_TESTING=OFF
136
+ cmake --build vendor-src/utf8proc/build --config Release
137
+ cmake --install vendor-src/utf8proc/build --prefix <prefix>
138
+
139
+ cmake -B vendor-src/libleptris/build -S vendor-src/libleptris \
140
+ -DCMAKE_BUILD_TYPE=Release \
141
+ -DLEPTRIS_BUILD_SHARED=ON -DLEPTRIS_BUILD_STATIC=OFF \
142
+ -DBUILD_TESTING=OFF -DLEPTRIS_BUILD_CLI=OFF \
143
+ -DLEPTRIS_BUILD_BENCHMARKS=OFF -DLEPTRIS_BUILD_MAN_PAGES=OFF \
144
+ -DLEPTRIS_ENABLE_UTF8PROC=ON \
145
+ -DCMAKE_PREFIX_PATH=<utf8proc-prefix>
146
+ cmake --build vendor-src/libleptris/build --config Release
147
+ README
148
+
120
149
  # libleptris 1.9.18's xslt_functions.c:270 assigns LeptrisElement
121
150
  # to LeptrisNodeRef — GCC 14 (Alpine/musl) makes incompatible
122
151
  # pointer types an error by default and the musl platform gems
@@ -357,6 +386,46 @@ namespace :audit do
357
386
  end
358
387
  end
359
388
 
389
+ # Packaging doctrine (#271): every BUILT gem carries the engine
390
+ # source (vendor-src/libleptris build inputs) and the ext sources;
391
+ # platform gems additionally carry the prebuilt libraries and must
392
+ # NOT carry extensions; the pure ruby gem compiles at install
393
+ # (extensions present). Run over pkg/*.gem before publishing —
394
+ # the gate is what keeps the doctrine from regressing silently.
395
+ desc "Audit built gems for the packaging doctrine (source + binaries + extension policy)"
396
+ task :doctrine do
397
+ gems = Dir.glob("pkg/*.gem")
398
+ abort "audit:doctrine: no gems in pkg/ — build first (rake gem:native:...)" if gems.empty?
399
+
400
+ gems.each do |path|
401
+ spec = Gem::Package.new(path).spec
402
+ files = spec.files.map { |f| f.gsub("\\", "/") }
403
+
404
+ missing = []
405
+ missing << "vendor-src/libleptris/CMakeLists.txt" unless
406
+ files.include?("vendor-src/libleptris/CMakeLists.txt")
407
+ missing << "vendor-src/README.md" unless files.include?("vendor-src/README.md")
408
+ missing << "ext/leptris/native/extconf.rb" unless
409
+ files.include?("ext/leptris/native/extconf.rb")
410
+
411
+ if spec.platform.to_s == "ruby"
412
+ missing << "extensions (compile-at-install)" if spec.extensions.empty?
413
+ else
414
+ binary = files.any? { |f| f =~ %r{\Alib/libleptris\.(so|dylib|dll)\z} } ||
415
+ files.any? { |f| f.start_with?("lib/leptris/vendor/") }
416
+ missing << "prebuilt engine binary" unless binary
417
+ missing << "extensions (platform gems never rebuild)" unless spec.extensions.empty?
418
+ end
419
+
420
+ if missing.empty?
421
+ puts "audit:doctrine: #{File.basename(path)} OK " \
422
+ "(#{spec.platform}, #{files.length} files)"
423
+ else
424
+ abort "audit:doctrine: #{File.basename(path)} MISSING: #{missing.join(', ')}"
425
+ end
426
+ end
427
+ end
428
+
360
429
  # @api private
361
430
  def nm_available?
362
431
  system("nm", "--version", out: File::NULL, err: File::NULL)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.209.1"
4
+ VERSION = "1.9.210.0"
5
5
  end
data/lib/leptris.rb CHANGED
@@ -29,10 +29,13 @@ begin
29
29
  rescue LoadError => e
30
30
  raise LoadError, <<~MSG
31
31
  Leptris could not load the native libleptris library.
32
- The ruby-platform gem is a fallback variant and ships no
33
- binary; install the platform-specific variant for your system
34
- (e.g. `gem install leptris --platform=arm64-darwin`), or set
35
- LEPTRIS_LIB_PATH to a libleptris.{so,dylib,dll} file.
32
+ The ruby-platform gem carries the engine sources under
33
+ vendor-src/ and builds them at install time a failed build
34
+ here usually means cmake or a C toolchain is missing (check
35
+ the gem install output). Alternatively install the
36
+ platform-specific variant for your system (e.g. `gem install
37
+ leptris --platform=arm64-darwin`), or set LEPTRIS_LIB_PATH to
38
+ a libleptris.{so,dylib,dll} file.
36
39
  (Underlying error: #{e.message})
37
40
  MSG
38
41
  end
@@ -0,0 +1,14 @@
1
+ Sources vendored in this gem (recompile rights; the ruby
2
+ variant builds them at install via extconf.rb):
3
+
4
+ - libleptris v1.9.210
5
+ - utf8proc v2.11.0
6
+
7
+ Rebuild by hand:
8
+
9
+ cmake -B vendor-src/utf8proc/build -S vendor-src/utf8proc -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DUTF8PROC_ENABLE_TESTING=OFF
10
+ cmake --build vendor-src/utf8proc/build --config Release
11
+ cmake --install vendor-src/utf8proc/build --prefix <prefix>
12
+
13
+ cmake -B vendor-src/libleptris/build -S vendor-src/libleptris -DCMAKE_BUILD_TYPE=Release -DLEPTRIS_BUILD_SHARED=ON -DLEPTRIS_BUILD_STATIC=OFF -DBUILD_TESTING=OFF -DLEPTRIS_BUILD_CLI=OFF -DLEPTRIS_BUILD_BENCHMARKS=OFF -DLEPTRIS_BUILD_MAN_PAGES=OFF -DLEPTRIS_ENABLE_UTF8PROC=ON -DCMAKE_PREFIX_PATH=<utf8proc-prefix>
14
+ cmake --build vendor-src/libleptris/build --config Release
@@ -1,5 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.210] - 2026-09-20
4
+
5
+ ### Added
6
+
7
+ - The node-layout fork's document block (slice 1, #1253): two
8
+ contiguous geometrically-grown regions per document (dense node
9
+ array + string arena) with region-relative byte-offset edges —
10
+ growth never invalidates an offset. Foundation for the pugixml-
11
+ class parse layout (TODO.max-perf/node-layout-fork.md).
12
+
13
+ ### Performance
14
+
15
+ - Large-document parse (#1222/#1238 lane, #1255): the parse arena
16
+ (~4x input) exceeded the arena retain cache's 32MB ceiling, so
17
+ every repeated-parse cycle freed and re-malloc'd tens of MB.
18
+ Ceiling raised to 96MB (one document's worth of parking) with a
19
+ thread-cleanup drain and ASAN free-for-real. Measured: 13MB
20
+ attr shape 341 -> 472 MB/s (+38%); the text shape's size-decline
21
+ eliminated (465 -> 163 MB/s falling across 1.5-13MB, now
22
+ ~605-627 flat).
23
+
24
+
25
+
3
26
  ## [1.9.209] - 2026-09-20
4
27
 
5
28
  ### Performance
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.209
6
+ VERSION 1.9.210
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -47,6 +47,7 @@ set(LEPTRIS_SOURCES
47
47
 
48
48
  # Memory management
49
49
  leptris/memory/pool.c
50
+ leptris/memory/doc_block.c
50
51
  leptris/memory/arena.c
51
52
  leptris/dom/compact.c
52
53
  leptris/dom/root_doc_map.c
@@ -74,9 +74,11 @@ LEPTRIS_API void leptris_thread_cleanup(void) {
74
74
  extern void leptris_xpath_drain_thread_caches(void);
75
75
  extern void leptris_root_doc_drain_thread_caches(void);
76
76
  extern void leptris_pool_drain_recycler(void);
77
+ extern void leptris_arena_retain_drain(void);
77
78
  leptris_xpath_drain_thread_caches();
78
79
  leptris_root_doc_drain_thread_caches();
79
80
  leptris_pool_drain_recycler();
81
+ leptris_arena_retain_drain();
80
82
  }
81
83
 
82
84
  /* ---- Memory management API ---- */
@@ -51,7 +51,20 @@ static inline size_t align_up(size_t n) {
51
51
  * twice per document lifetime, so contention is irrelevant. */
52
52
  #define ARENA_RETAIN_MIN (256u * 1024u)
53
53
  #define ARENA_RETAIN_MAX_BLOCKS 4u
54
- #define ARENA_RETAIN_MAX_BYTES (32u * 1024u * 1024u)
54
+ /* 96MB: a large-document parse arena (~4x input with the
55
+ * pre-sized estimate) must survive the free to be reused by the
56
+ * next parse — at the old 32MB ceiling, every big-doc cycle
57
+ * freed and re-malloc'd tens of MB (libc madvise showed ~2% of
58
+ * repeated-parse wall time). One document's worth of parking. */
59
+ #define ARENA_RETAIN_MAX_BYTES (96u * 1024u * 1024u)
60
+
61
+ #if defined(__has_feature)
62
+ #if __has_feature(address_sanitizer)
63
+ #define ARENA_RETAIN_ASAN 1
64
+ #endif
65
+ #elif defined(__SANITIZE_ADDRESS__)
66
+ #define ARENA_RETAIN_ASAN 1
67
+ #endif
55
68
 
56
69
  static struct {
57
70
  char* base[ARENA_RETAIN_MAX_BLOCKS];
@@ -106,6 +119,14 @@ static char* retain_take(size_t request, size_t* capacity) {
106
119
  }
107
120
 
108
121
  static void retain_give(char* base, size_t size) {
122
+ #ifdef ARENA_RETAIN_ASAN
123
+ /* ASAN: retained blocks have no TLS destructor and read as
124
+ * LSan leaks — free for real (the recycle win is measurable
125
+ * only in repeated-parse loops, which ASAN runs don't gate). */
126
+ (void)size;
127
+ leptris_free_hook(base);
128
+ return;
129
+ #endif
109
130
  if (size < ARENA_RETAIN_MIN) {
110
131
  leptris_free_hook(base);
111
132
  return;
@@ -124,6 +145,18 @@ static void retain_give(char* base, size_t size) {
124
145
  retain_unlock();
125
146
  }
126
147
 
148
+ /* Host-invoked drain (leptris_thread_cleanup): the retained
149
+ * blocks are process-lifetime parking by design; release them so
150
+ * embedded hosts and leak checkers see a clean exit. */
151
+ void leptris_arena_retain_drain(void) {
152
+ retain_lock();
153
+ for (size_t i = 0; i < g_retain.count; i++)
154
+ leptris_free_hook(g_retain.base[i]);
155
+ g_retain.count = 0;
156
+ g_retain.bytes = 0;
157
+ retain_unlock();
158
+ }
159
+
127
160
  char* leptris_arena_buffer_alloc(size_t size) {
128
161
  size_t capacity = size;
129
162
  char* base = retain_take(size, &capacity);
@@ -55,6 +55,9 @@ void leptris_arena_reset(LeptrisArena* arena);
55
55
  * All pointers handed out by the arena become invalid. */
56
56
  void leptris_arena_destroy(LeptrisArena* arena);
57
57
 
58
+ /* Release retained arena blocks (leptris_thread_cleanup drain). */
59
+ void leptris_arena_retain_drain(void);
60
+
58
61
  /* Bump-allocate `size` bytes, 8-byte aligned. NOT zeroed.
59
62
  * Returns NULL when the request does not fit in remaining space —
60
63
  * no fallback allocation, ever (see the contract above). */
@@ -0,0 +1,108 @@
1
+ /* doc_block.c — the node-layout fork's document block (slice 1).
2
+ * See doc_block.h for the design contract. */
3
+
4
+ #include "doc_block.h"
5
+ #include <stdlib.h>
6
+ #include <string.h>
7
+
8
+ #define DOC_BLOCK_MIN_CAP ((size_t)4096)
9
+ #define DOC_BLOCK_ALIGN ((size_t)8)
10
+
11
+ struct LeptrisDocBlock {
12
+ char* nodes; /* node region base (8-aligned) */
13
+ size_t nodes_cap;
14
+ size_t nodes_used;
15
+ char* strs; /* string region base */
16
+ size_t strs_cap;
17
+ size_t strs_used;
18
+ };
19
+
20
+ static char* region_grow(char* p, size_t* cap, size_t need) {
21
+ size_t want = *cap ? *cap : DOC_BLOCK_MIN_CAP;
22
+ while (want < need) want *= 2;
23
+ char* grown = (char*)realloc(p, want);
24
+ if (grown) *cap = want;
25
+ return grown;
26
+ }
27
+
28
+ LeptrisDocBlock* leptris_doc_block_create(size_t initial_capacity) {
29
+ LeptrisDocBlock* b =
30
+ (LeptrisDocBlock*)calloc(1, sizeof(*b));
31
+ if (!b) return NULL;
32
+ if (initial_capacity && initial_capacity < DOC_BLOCK_MIN_CAP)
33
+ initial_capacity = DOC_BLOCK_MIN_CAP;
34
+ if (initial_capacity) {
35
+ b->nodes = (char*)malloc(initial_capacity);
36
+ b->strs = (char*)malloc(initial_capacity);
37
+ if (!b->nodes || !b->strs) {
38
+ free(b->nodes);
39
+ free(b->strs);
40
+ free(b);
41
+ return NULL;
42
+ }
43
+ b->nodes_cap = b->strs_cap = initial_capacity;
44
+ }
45
+ return b;
46
+ }
47
+
48
+ void leptris_doc_block_free(LeptrisDocBlock* b) {
49
+ if (!b) return;
50
+ free(b->nodes);
51
+ free(b->strs);
52
+ free(b);
53
+ }
54
+
55
+ size_t leptris_doc_block_node_alloc(LeptrisDocBlock* b, size_t sz) {
56
+ if (!b || sz == 0 || sz > (size_t)-1 / 2) return (size_t)-1;
57
+ size_t off = (b->nodes_used + DOC_BLOCK_ALIGN - 1) &
58
+ ~(DOC_BLOCK_ALIGN - 1);
59
+ size_t need = off + sz;
60
+ if (need > b->nodes_cap) {
61
+ char* grown = region_grow(b->nodes, &b->nodes_cap, need);
62
+ if (!grown) return (size_t)-1;
63
+ b->nodes = grown;
64
+ }
65
+ b->nodes_used = need;
66
+ return off;
67
+ }
68
+
69
+ size_t leptris_doc_block_str(LeptrisDocBlock* b, const char* s,
70
+ size_t len) {
71
+ if (!b || (!s && len)) return (size_t)-1;
72
+ size_t need = b->strs_used + len + 1;
73
+ if (need > b->strs_cap) {
74
+ char* grown = region_grow(b->strs, &b->strs_cap, need);
75
+ if (!grown) return (size_t)-1;
76
+ b->strs = grown;
77
+ }
78
+ size_t off = b->strs_used;
79
+ if (len) memcpy(b->strs + off, s, len);
80
+ b->strs[off + len] = '\0';
81
+ b->strs_used = need;
82
+ return off;
83
+ }
84
+
85
+ void* leptris_doc_block_node(const LeptrisDocBlock* b, size_t off) {
86
+ return (b && off < b->nodes_used) ? b->nodes + off : NULL;
87
+ }
88
+
89
+ void* leptris_doc_block_nodes_base(const LeptrisDocBlock* b) {
90
+ return b ? b->nodes : NULL;
91
+ }
92
+
93
+ size_t leptris_doc_block_nodes_used(const LeptrisDocBlock* b) {
94
+ return b ? b->nodes_used : 0;
95
+ }
96
+
97
+ const char* leptris_doc_block_str_at(const LeptrisDocBlock* b,
98
+ size_t off) {
99
+ return (b && off < b->strs_used) ? b->strs + off : NULL;
100
+ }
101
+
102
+ size_t leptris_doc_block_bytes_used(const LeptrisDocBlock* b) {
103
+ return b ? b->nodes_used + b->strs_used : 0;
104
+ }
105
+
106
+ size_t leptris_doc_block_capacity(const LeptrisDocBlock* b) {
107
+ return b ? b->nodes_cap + b->strs_cap : 0;
108
+ }
@@ -0,0 +1,61 @@
1
+ /* lib/src/memory/doc_block.h — the node-layout fork's document
2
+ * block (TODO.max-perf/node-layout-fork, slice 1).
3
+ *
4
+ * Two contiguous, geometrically-grown regions per document — a
5
+ * dense node array and a string arena. Every reference to either
6
+ * is a byte OFFSET within its region, so growth (realloc) never
7
+ * invalidates an edge: callers hold offsets, not pointers, and
8
+ * refetch region bases after any allocation.
9
+ *
10
+ * Differences vs memory/pool.c (which stays for mutation paths):
11
+ * the pool chains fixed 32KB pages, scattering one tree across
12
+ * pages; each parse node pays compact-edge encoding and detached
13
+ * nodes pay root-doc registration. Here the tree is contiguous
14
+ * (walk locality), edges are plain offsets (no cp16 overflow
15
+ * table), and the owning document is implied by the region base.
16
+ */
17
+ #ifndef LEPTRIS_MEMORY_DOC_BLOCK_H
18
+ #define LEPTRIS_MEMORY_DOC_BLOCK_H
19
+
20
+ #include <stddef.h>
21
+ #include <stdint.h>
22
+
23
+ #ifdef __cplusplus
24
+ extern "C" {
25
+ #endif
26
+
27
+ typedef struct LeptrisDocBlock LeptrisDocBlock;
28
+
29
+ /* Create with an initial byte capacity per region (grown
30
+ * geometrically on demand; 0 = lazy). NULL on failure. */
31
+ LeptrisDocBlock* leptris_doc_block_create(size_t initial_capacity);
32
+
33
+ void leptris_doc_block_free(LeptrisDocBlock* b);
34
+
35
+ /* Node region: bump-allocate sz bytes, 8-byte aligned. Returns
36
+ * the node's offset in the region (stable across growth) — never
37
+ * a pointer; resolve via leptris_doc_block_node. (size_t)-1 on
38
+ * failure. Growth may realloc: refetch any held pointers. */
39
+ size_t leptris_doc_block_node_alloc(LeptrisDocBlock* b, size_t sz);
40
+
41
+ /* String region: append len bytes plus a NUL. Returns the
42
+ * string's offset in ITS region (stable across growth) or
43
+ * (size_t)-1 on failure. */
44
+ size_t leptris_doc_block_str(LeptrisDocBlock* b, const char* s,
45
+ size_t len);
46
+
47
+ /* Resolve an offset (region base + off, one add). */
48
+ void* leptris_doc_block_node(const LeptrisDocBlock* b, size_t off);
49
+
50
+ void* leptris_doc_block_nodes_base(const LeptrisDocBlock* b);
51
+ size_t leptris_doc_block_nodes_used(const LeptrisDocBlock* b);
52
+ const char* leptris_doc_block_str_at(const LeptrisDocBlock* b,
53
+ size_t off);
54
+ size_t leptris_doc_block_bytes_used(const LeptrisDocBlock* b);
55
+ size_t leptris_doc_block_capacity(const LeptrisDocBlock* b);
56
+
57
+ #ifdef __cplusplus
58
+ }
59
+ #endif
60
+
61
+ #endif /* LEPTRIS_MEMORY_DOC_BLOCK_H */
@@ -116,6 +116,7 @@ leptris_add_test(test_simd_spans common/test_simd_spans.cpp)
116
116
  leptris_add_test(test_pool memory/test_pool.cpp)
117
117
  leptris_add_test(test_oom memory/test_oom_injection.cpp)
118
118
  leptris_add_test(test_arena memory/test_arena.cpp)
119
+ leptris_add_test(test_doc_block memory/test_doc_block.cpp)
119
120
  leptris_add_test(test_pool_arena memory/test_pool_arena.cpp)
120
121
  leptris_add_test(test_xpath xpath/test_xpath.cpp)
121
122
  leptris_add_test(test_exslt xpath/test_exslt.cpp)
@@ -192,11 +192,25 @@ TEST(Arena, AllPointersWithinSpanForCompactEncoding) {
192
192
 
193
193
  /* ---- Retained-block free list (parse fault fix) ---------------------- */
194
194
 
195
+ #if defined(__has_feature)
196
+ #if __has_feature(address_sanitizer)
197
+ #define ARENA_TESTS_ASAN 1
198
+ #endif
199
+ #elif defined(__SANITIZE_ADDRESS__)
200
+ #define ARENA_TESTS_ASAN 1
201
+ #endif
202
+
195
203
  TEST(LeptrisArena, RetainsAndReusesLargeBlocks) {
196
204
  /* Blocks >= 256 KB must round-trip through the free list: the
197
205
  * next same-size create gets the SAME mapping back (no munmap,
198
206
  * no page faults). Machine-independent by construction: it
199
207
  * asserts pointer identity, not timing. */
208
+ /* Under ASAN the retain cache frees for real (no TLS
209
+ * destructor; parked blocks read as LSan leaks) — the reuse
210
+ * contract is deliberately disabled there. */
211
+ #if defined(ARENA_TESTS_ASAN)
212
+ GTEST_SKIP() << "retain cache disabled under ASAN";
213
+ #endif
200
214
  const size_t big = 512u * 1024u;
201
215
  LeptrisArena* a1 = leptris_arena_create(big);
202
216
  ASSERT_NE(a1, nullptr);
@@ -239,6 +253,13 @@ TEST(LeptrisArena, SmallBlocksStillMallocSemantics) {
239
253
  }
240
254
 
241
255
  TEST(LeptrisArena, BufferRoundTripReusesMapping) {
256
+ /* Under ASAN the retain cache frees for real (no TLS
257
+ * destructor; parked blocks read as LSan leaks) — the reuse
258
+ * contract is deliberately disabled there. */
259
+ #if defined(ARENA_TESTS_ASAN)
260
+ GTEST_SKIP() << "retain cache disabled under ASAN";
261
+ #endif
262
+
242
263
  char* b1 = leptris_arena_buffer_alloc(512u * 1024u);
243
264
  ASSERT_NE(b1, nullptr);
244
265
  memset(b1, 1, 512u * 1024u);
@@ -0,0 +1,94 @@
1
+ // test/memory/test_doc_block.cpp - the node-layout fork's document
2
+ // block (slice 1). Falsifiable contract: offsets stay valid across
3
+ // geometric growth, regions stay dense/aligned, boundary inputs
4
+ // are refused.
5
+
6
+ #include <gtest/gtest.h>
7
+ #include <cstring>
8
+ #include <cstdio>
9
+ #include <vector>
10
+ #include "../../src/leptris/memory/doc_block.h"
11
+
12
+ typedef struct {
13
+ uint32_t first_child;
14
+ uint32_t next;
15
+ uint32_t name_off;
16
+ } TNode;
17
+
18
+ TEST(DocBlock, OffsetsSurviveGrowth) {
19
+ LeptrisDocBlock* b = leptris_doc_block_create(0);
20
+ ASSERT_NE(b, nullptr);
21
+ std::vector<uint32_t> offs;
22
+ const int N = 20000;
23
+ for (int i = 0; i < N; i++) {
24
+ size_t off = leptris_doc_block_node_alloc(b, sizeof(TNode));
25
+ ASSERT_NE(off, (size_t)-1);
26
+ ASSERT_EQ(off % 8u, 0u);
27
+ TNode* n = (TNode*)leptris_doc_block_node(b, off);
28
+ ASSERT_NE(n, nullptr);
29
+ n->first_child = 0;
30
+ n->next = i > 0 ? offs[i - 1] : 0;
31
+ n->name_off = 0;
32
+ offs.push_back((uint32_t)off);
33
+ if (i % 1000 == 0) {
34
+ for (size_t k = 1; k < offs.size(); k++) {
35
+ const TNode* p =
36
+ (const TNode*)leptris_doc_block_node(b, offs[k]);
37
+ ASSERT_NE(p, nullptr);
38
+ ASSERT_EQ(p->next, offs[k - 1]);
39
+ }
40
+ }
41
+ }
42
+ EXPECT_GT(leptris_doc_block_capacity(b), (size_t)0);
43
+ leptris_doc_block_free(b);
44
+ }
45
+
46
+ TEST(DocBlock, StringRegionStableAcrossGrowth) {
47
+ LeptrisDocBlock* b = leptris_doc_block_create(64);
48
+ ASSERT_NE(b, nullptr);
49
+ std::vector<size_t> offs;
50
+ char buf[32];
51
+ for (int i = 0; i < 5000; i++) {
52
+ int n = snprintf(buf, sizeof(buf), "name-%05d", i);
53
+ size_t off = leptris_doc_block_str(b, buf, (size_t)n);
54
+ ASSERT_NE(off, (size_t)-1);
55
+ offs.push_back(off);
56
+ }
57
+ for (int i = 0; i < 5000; i += 500) {
58
+ int n = snprintf(buf, sizeof(buf), "name-%05d", i);
59
+ const char* s = leptris_doc_block_str_at(b, offs[i]);
60
+ ASSERT_NE(s, nullptr);
61
+ EXPECT_EQ(std::memcmp(s, buf, (size_t)n), 0);
62
+ EXPECT_EQ(s[n], '\0');
63
+ }
64
+ leptris_doc_block_free(b);
65
+ }
66
+
67
+ TEST(DocBlock, BoundaryInputsRefused) {
68
+ LeptrisDocBlock* b = leptris_doc_block_create(0);
69
+ ASSERT_NE(b, nullptr);
70
+ EXPECT_EQ(leptris_doc_block_node_alloc(b, 0), (size_t)-1);
71
+ EXPECT_EQ(leptris_doc_block_node_alloc(nullptr, 8), (size_t)-1);
72
+ EXPECT_EQ(leptris_doc_block_str(b, nullptr, 3), (size_t)-1);
73
+ size_t off = leptris_doc_block_str(b, "", 0);
74
+ EXPECT_NE(off, (size_t)-1);
75
+ EXPECT_STREQ(leptris_doc_block_str_at(b, off), "");
76
+ EXPECT_EQ(leptris_doc_block_node(b, 1 << 20), nullptr);
77
+ leptris_doc_block_free(b);
78
+ leptris_doc_block_free(nullptr);
79
+ }
80
+
81
+ TEST(DocBlock, NodesAndStringsIndependent) {
82
+ LeptrisDocBlock* b = leptris_doc_block_create(0);
83
+ ASSERT_NE(b, nullptr);
84
+ size_t n1 = leptris_doc_block_node_alloc(b, 16);
85
+ size_t s1 = leptris_doc_block_str(b, "abc", 3);
86
+ size_t n2 = leptris_doc_block_node_alloc(b, 16);
87
+ size_t s2 = leptris_doc_block_str(b, "defg", 4);
88
+ EXPECT_NE(n1, n2);
89
+ EXPECT_NE(s1, s2);
90
+ EXPECT_STREQ(leptris_doc_block_str_at(b, s1), "abc");
91
+ EXPECT_STREQ(leptris_doc_block_str_at(b, s2), "defg");
92
+ EXPECT_EQ(leptris_doc_block_nodes_used(b), 32u);
93
+ leptris_doc_block_free(b);
94
+ }
@@ -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.209",
4
+ "version": "1.9.210",
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.209.1
4
+ version: 1.9.210.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -205,6 +205,7 @@ files:
205
205
  - lib/leptris/xml/xquery.rb
206
206
  - lib/leptris/xml/xslt.rb
207
207
  - scripts/gem_smoke.rb
208
+ - vendor-src/README.md
208
209
  - vendor-src/libleptris/CHANGELOG.md
209
210
  - vendor-src/libleptris/CLAUDE.md
210
211
  - vendor-src/libleptris/CMakeLists.txt
@@ -662,6 +663,8 @@ files:
662
663
  - vendor-src/libleptris/src/leptris/leptris_parse.h
663
664
  - vendor-src/libleptris/src/leptris/memory/arena.c
664
665
  - vendor-src/libleptris/src/leptris/memory/arena.h
666
+ - vendor-src/libleptris/src/leptris/memory/doc_block.c
667
+ - vendor-src/libleptris/src/leptris/memory/doc_block.h
665
668
  - vendor-src/libleptris/src/leptris/memory/pool.c
666
669
  - vendor-src/libleptris/src/leptris/memory/pool.h
667
670
  - vendor-src/libleptris/src/leptris/rng/parse.c
@@ -816,6 +819,7 @@ files:
816
819
  - vendor-src/libleptris/test/html/test_html.cpp
817
820
  - vendor-src/libleptris/test/html/test_html5lib.cpp
818
821
  - vendor-src/libleptris/test/memory/test_arena.cpp
822
+ - vendor-src/libleptris/test/memory/test_doc_block.cpp
819
823
  - vendor-src/libleptris/test/memory/test_oom_injection.cpp
820
824
  - vendor-src/libleptris/test/memory/test_pool.cpp
821
825
  - vendor-src/libleptris/test/memory/test_pool_arena.cpp