leptris 1.9.377.0-ppc64le-linux → 1.9.378.0-ppc64le-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1181a6b1b3b35d18bb9a5cb4b6fcc94dfc71e4966d6433189eacc1c981b91408
4
- data.tar.gz: d3f0a67142ed581bda06ae7ff57caebd9e2c51b8253e8da77f624a7c04242139
3
+ metadata.gz: 2e0cdfa6d823436207fbbf463c8b9b6a438a31ce2648d480d8aad8f13b91a09e
4
+ data.tar.gz: 7edb327bb6616c4b4903f75d35ba7d0e8c0bd0c158d869cc5244d1a1d3055f42
5
5
  SHA512:
6
- metadata.gz: 2fa752f9fb34474b74f9a5dc6dce4eec80672dc9ebb6c61b890e5798f89abb9fcc20d26e1efe0c56690c8f7245c623eaf889ca0bb63b151a39761c691bcf7508
7
- data.tar.gz: 9bfa01160fb2eb3357c2312698ba3f53bc3e364af71498d40a794ab714997ce2d542ef0c172d65d976f87130d2f9509bc487ce4b4fa632f39a220fa4d2aa1b72
6
+ metadata.gz: a6155747b60810186d71b3457b3905b868c39c5ca0e51224d958ca741d4235f5716bc6922d712c05c51d2b4fc75c4ac8f9ddd688cf97b127ecf66e7398d37729
7
+ data.tar.gz: e16ac5676044a897748800051207685eecf8568d52a4bee73790e4f4e7140aae1d5a9894b9bff23e9a44e3cc81bbddbf02bb6ba2f6c4b3ca3c75fbbdad2bf193
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [1.9.378.0] - 2026-10-21
2
+
3
+ ### Fixed — engine sync (libleptris 1.9.378)
4
+
5
+ - **Node-set XPath after sibling insertion (leptris-ruby#519,
6
+ leptris/leptris#1802)**: the lazy element index was invalidated
7
+ by `add_child` but not by the sibling/prepend splice paths, so
8
+ `xpath("//name")` after `add_next_sibling` answered from the
9
+ pre-insert tree until an unrelated child insert rebuilt it.
10
+ `insert_before`, `insert_after` and `prepend_child` now
11
+ invalidate the document index.
12
+
13
+ ### Changed — engine sync
14
+
15
+ - **WHATWG HTML verdict parity complete (leptris/leptris#1803)**:
16
+ 1555/1555 corpus documents now report parse errors exactly when
17
+ html5lib does, with tree parity unchanged at 1555/1555.
18
+
1
19
  ## [1.9.356.0] - 2026-10-14
2
20
 
3
21
  ### Fixed — engine sync (libleptris 1.9.356)
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.377"
11
+ LIBLEPTRIS_VERSION = "1.9.378"
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.377.0"
4
+ VERSION = "1.9.378.0"
5
5
  end
data/lib/libleptris.so CHANGED
Binary file
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.377
4
+ - libleptris v1.9.378
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,59 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.378] - 2026-10-21
4
+
5
+ ### Added
6
+
7
+ - **SECURITY.md: private reporting channel and CVE-response note
8
+ (PR #1804).** Reports go through GitHub Security Advisories, with
9
+ an acknowledgment target, coordinated disclosure, and CVE
10
+ assignment when warranted. The supported-version table follows
11
+ the lockstep rule: a fix is delivered only when the binding
12
+ release carries the fixed engine. The hardening inventory records
13
+ what actually ships: zero required runtime dependencies,
14
+ ASAN+leak and macOS `leaks` lanes, nightly libFuzzer on the
15
+ parser, explicit depth/size limits, and the conformance suites.
16
+ - **Test suite runs in parallel without changing any test (PR
17
+ #1799).** `gtest_discover_tests` now bakes the case list at build
18
+ time for native builds (`POST_BUILD`; cross builds keep
19
+ `PRE_TEST`), so `ctest --parallel` expands real cases instead of
20
+ `_NOT_BUILT` placeholders. CI runs the native and matrix lanes at
21
+ `--parallel 4` (qemu s390x and ILP32 stay serial). The CLI test
22
+ helper passed `WORKING_DIRECTORY` inside `PROPERTIES`, which
23
+ silently dropped the following `RUN_SERIAL`; passing it as a
24
+ keyword restores the serialization. Same 2074 cases, all green:
25
+ 66.0 s serial → 31.0 s `-j4` locally; the export check alone
26
+ 20.7-23.3 s → 8.7-9.3 s.
27
+
28
+ ### Fixed
29
+
30
+ - **dom: node-set XPath no longer answers from a stale element
31
+ index after sibling insertion (leptris-ruby#519, PR #1802).** The
32
+ lazy TODO 132 index was invalidated by `append_child` but not by
33
+ `insert_before`, `insert_after` or `prepend_child`, so a
34
+ `//name` query after `add_next_sibling` missed the new element
35
+ until an unrelated child insert rebuilt the index. All three
36
+ splice paths now invalidate the document index; the regression
37
+ spec covers each path plus the compiled-handle FFI face the
38
+ bindings evaluate with.
39
+ - **html: WHATWG verdict parity is complete — 1555/1555, zero
40
+ misses (PR #1803).** The remaining 20 corpus documents now report
41
+ at their html5lib source sites: `adoption-agency-1.3` for
42
+ formatting end tags whose element is in scope but not the current
43
+ node; the `</menuitem>` endTagOther pair (stray, and found with
44
+ non-implied open elements above); tokenizer
45
+ `expected-dashes-or-doctype`,
46
+ `unexpected-character-after-solidus-in-tag` and
47
+ `attributes-in-end-tag`; AfterAfterFrameset trailing characters,
48
+ BeforeHead `end-tag-after-implied-root`, BeforeHtml
49
+ `unexpected-end-tag-before-html`, and foreign-content end-tag
50
+ mismatch; `unexpected-form-in-table` for both the insertion and
51
+ the ignored duplicate plus the `</form>` table-voodoo pair;
52
+ template `foster-parenting-character`; and
53
+ `two-heads-are-not-better-than-one`. Tree parity stays
54
+ 1555/1555; the verdict floor is pinned at the full corpus count.
55
+
56
+
3
57
  ## [1.9.377] - 2026-10-20
4
58
 
5
59
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.377
6
+ VERSION 1.9.378
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -0,0 +1,94 @@
1
+ # Security Policy
2
+
3
+ libleptris is a parser and transformation engine: it reads untrusted
4
+ documents. Memory safety and predictable resource behavior on hostile
5
+ input are part of the product, not an afterthought.
6
+
7
+ ## Supported versions
8
+
9
+ Security fixes land on the latest release of the current major line and
10
+ are shipped in the next patch release. The engine follows the lockstep
11
+ runtime rule — binding versions are `{engine-semver}.{binding-patch}` —
12
+ so a fix is considered delivered only when the corresponding binding
13
+ release carries the fixed engine.
14
+
15
+ | Version | Supported |
16
+ |---|---|
17
+ | latest `1.x` release | yes |
18
+ | any earlier release/tag | no (upgrade to the latest patch) |
19
+
20
+ ## Reporting a vulnerability
21
+
22
+ **Do not open a public issue for a security report.**
23
+
24
+ Use GitHub's private vulnerability reporting on this repository:
25
+ **Security → Advisories → Report a vulnerability** (direct link:
26
+ <https://github.com/leptris/leptris/security/advisories/new>).
27
+
28
+ Please include, when available:
29
+
30
+ - the engine version (or binding version and its pinned engine),
31
+ - the input document/expression that triggers the problem,
32
+ - the build configuration (sanitizer output, platform, compiler),
33
+ - the observed impact (crash, out-of-bounds access, infinite loop,
34
+ excessive memory, code execution, information disclosure).
35
+
36
+ If you report a bug that affects a binding, include both the binding
37
+ version and the engine version it pins; the engine is the shared
38
+ implementation and the fix belongs there first.
39
+
40
+ ## What to expect
41
+
42
+ - **Acknowledgment** within 5 business days.
43
+ - **Triage and reproduction** in the private advisory thread. We may
44
+ ask for the exact input and a minimized reproducer.
45
+ - **Fix and coordinated disclosure.** The fix lands on a private or
46
+ public branch as circumstances require, is released through the
47
+ normal release workflow, and the advisory is published with credit
48
+ (unless you prefer to remain anonymous).
49
+ - **CVE assignment** is requested through the GitHub advisory when the
50
+ issue warrants one.
51
+
52
+ Please give us a reasonable window to ship a patch before public
53
+ disclosure. We will keep you informed of the timeline in the advisory
54
+ thread.
55
+
56
+ ## In scope
57
+
58
+ - The C engine: XML/HTML parsing, DTD validation, XPath, XSLT,
59
+ XInclude, serialization, encoding conversion, SAX and pull APIs.
60
+ - The shipped CLI (`leptris`) and public C API surface.
61
+ - The FFI layer and descriptor surface consumed by the language
62
+ bindings (leptris-ruby, leptris-py, leptris-ts, leptris-rs).
63
+ - Resource-exhaustion behavior on untrusted input (unbounded
64
+ recursion, allocation amplification, algorithmic complexity) when it
65
+ is reachable without a misconfiguration.
66
+
67
+ ## Out of scope
68
+
69
+ - Vulnerabilities in third-party dependencies of the language
70
+ bindings, unless the engine's handling of their output is unsafe.
71
+ - Builds with sanitizers or assertions disabled by the reporter.
72
+ - Reports that require a modified engine binary.
73
+ - Denial of service by simply feeding a very large document when the
74
+ configured limits are explicitly disabled by the caller.
75
+
76
+ ## Hardening in the repository
77
+
78
+ - **Zero required runtime dependencies** by default; `utf8proc` and
79
+ `iconv` are optional compile-time features.
80
+ - **Sanitizers in CI**: AddressSanitizer with leak detection on Linux
81
+ (`asan.yml`), plus the macOS `leaks` lane.
82
+ - **Fuzzing**: nightly libFuzzer runs over the parser
83
+ (`fuzz-nightly.yml`), with the crash corpus triaged into regression
84
+ specs; the XPath and mutation harnesses live alongside it in
85
+ `test/fuzz/`.
86
+ - **Depth and size limits** enforced by the parser; all limits are
87
+ explicit and documented in the public headers.
88
+ - **Conformance suites** (W3C XPath, libxslt, WHATWG html5lib, XSD,
89
+ DTD) run on every change, so hardening fixes cannot regress silently.
90
+
91
+ ## Recognition
92
+
93
+ We are happy to credit reporters in the published advisory and in the
94
+ release notes. Thank you for helping keep document parsing safe.
@@ -28,9 +28,23 @@ docs/MISSION-PLAN.md and is updated at every release.
28
28
  ## Gate A — Conformance: zero reds everywhere
29
29
 
30
30
  ### A1 WHATWG verdict parity: 1394/1555 → 1555/1555
31
- The remaining 161 verdict cases + ~270 code-set divergences
32
- (html5lib-verdict-redlist.txt). Ground every slice in html5lib's
33
- actual source (~/src/external/html5lib):
31
+ **Measured 2026-10-21 (main after #1800): 1535/1555 verdict,
32
+ 1555/1555 tree, zero false positives; floor pinned at 1535.** The
33
+ remaining 20 verdict misses: adoption-agency (tests2:10, tests19:98,
34
+ tests19:99, tests26:1, tests26:9), menuitem strays (menuitem:10/12),
35
+ table/caption singletons (tests9:16, tests10:15, tests16:197,
36
+ pending-spec-changes:2, tests20:48), tokenizer singletons (tests10:2,
37
+ tests2:18, tests2:58, tests19:41, tests19:82) and two-heads
38
+ (tests3:13). The remaining ~270 code-set divergences are prose-dialect
39
+ labels from older corpus datasets; the verdict gate is presence-only.
40
+ The slices already shipped: initial mode, raw-text EOF, unterminated
41
+ close, frameset, fenced walk, InBody EOF allowed-set, in-select,
42
+ foster-by-kind, unknown-doctype (#1778-#1791), raw-text strays,
43
+ non-html-root, end-tag-too-early, after-body/frameset phases,
44
+ start-tag phases, select/table/foreign, need-space-after-doctype
45
+ (#1794), misplaced/stray-p, after-html codes, image (#1797, #1800).
46
+ Ground every further slice in html5lib's actual source
47
+ (~/src/external/html5lib):
34
48
  - [ ] escaped-state EOF codes (`unexpected-eof-in-text-mode`,
35
49
  `eof-in-script-in-script`) — the script-data state machine's
36
50
  EOF classification
@@ -76,6 +90,14 @@ actual source (~/src/external/html5lib):
76
90
  ## Gate B — Performance: every published row ≥1.00x
77
91
 
78
92
  ### B1 build row: ~2.9x → ≤1.0x (the #242 campaign)
93
+ **Measured 2026-10-21: 1.86-1.92x** (moxml `sota/04-performance`
94
+ branch + leptris 1.9.376.0; harness `wt/460-harness/build_bench.rb`,
95
+ 31 interleaved reps: 4.08ms vs Nokogiri 2.20ms for 250 records).
96
+ Was 2.95x at the 1.9.363 era; the add_child alias and same-doc fast
97
+ path landed since. StackProf attribution: `NativeNode.create_element`
98
+ 14.6%, GC mark+sweep 19%, `prime_contract` 7.7%, attach path 45% of
99
+ tree; the `invalidate_namespace_cache!` line attribution is a
100
+ sampling artifact (deleting both attach invalidations gains ~5%).
79
101
  Harness: wt/460-harness/build_bench.rb. Decomposition on record.
80
102
  - [ ] per-element object budget: one Ruby object per created node
81
103
  (drop the shell entirely post-create-priming; the mint cache
@@ -161,3 +161,15 @@ One table, updated at every release train: gate, command, last run,
161
161
  verdict. When every row of Gate A and Gate B reads "green (dated)"
162
162
  and Gate D1 is green, the mission is achieved — and v1.0.0 is the
163
163
  tag that says so.
164
+
165
+ | Gate | Command | Last run | Verdict |
166
+ |------|---------|----------|---------|
167
+ | A1 WHATWG tree parity | `test_html5lib --gtest_filter=Html5LibCorpus.*` | 2026-10-21 | green, 1555/1555 |
168
+ | A1 WHATWG verdict parity | `Html5LibCorpus.ErrorVerdictParity` | 2026-10-21 | 1535/1555 (floor 1535; **red**) |
169
+ | A7 libxslt | `LibxsltSuite` | 2026-10-20 | 205/205 (204 + 1 macOS skip) |
170
+ | B1 build row | `wt/460-harness/build_bench.rb` | 2026-10-21 | 1.86-1.92x (**red**, sota/04 branch) |
171
+ | B2 walk row | `wt/460-harness/walk_read_bench.rb` | 2026-10-20 | 0.88x (green) |
172
+ | Test-suite runtime (fidelity) | `ctest`, 2074 cases | 2026-10-21 | 66.0 s serial → 31.0 s `-j4` (same 2074/2074; PR #1799) |
173
+ | Rides 1.9.377 | registries | 2026-10-21 | rubygems/PyPI 1.9.377.0, npm 0.1.21 |
174
+ | Issues | all repos | 2026-10-21 | 0 open (ruby#514/#515 closed with gem verification) |
175
+
@@ -26,7 +26,7 @@ cmake -B "$BUILD" -S "$ROOT" \
26
26
  -DLEPTRIS_BUILD_BENCHMARKS=OFF \
27
27
  > /dev/null
28
28
 
29
- cmake --build "$BUILD" --target leptris_shared > /dev/null
29
+ cmake --build "$BUILD" --target leptris_shared --parallel > /dev/null
30
30
 
31
31
  LIB=$(find "$BUILD" -name 'libleptris.*.dylib' -o -name 'libleptris.so*' \
32
32
  2>/dev/null | grep -v '[0-9]\.dylib$' | head -1)
@@ -98,13 +98,17 @@ REQUIRED=(
98
98
  )
99
99
 
100
100
  rc=0
101
+ # One nm pass instead of one per symbol: the export table is a few KB
102
+ # but nm re-reads the whole dylib each time (~55 spawns + reads).
103
+ # macOS: nm -gU; Linux: nm -D --defined-only. The macOS spelling
104
+ # prefixes symbols with '_', hence the optional underscore.
105
+ EXPORTS=$(mktemp)
106
+ trap 'rm -f "$EXPORTS"' EXIT
107
+ if ! nm -gU "$LIB" > "$EXPORTS" 2>/dev/null; then
108
+ nm -D --defined-only "$LIB" > "$EXPORTS" 2>/dev/null || true
109
+ fi
101
110
  for sym in "${REQUIRED[@]}"; do
102
- # nm flags: -g externals only, -U exclude undefined, -C no demangle.
103
- # macOS: nm -gU <lib>
104
- # Linux: nm -D --defined-only <lib>
105
- if nm -gU "$LIB" 2>/dev/null | grep -qE " _?${sym}\$"; then
106
- echo " OK $sym"
107
- elif nm -D --defined-only "$LIB" 2>/dev/null | grep -qE " ${sym}\$"; then
111
+ if grep -qE "[[:space:]]_?${sym}\$" "$EXPORTS"; then
108
112
  echo " OK $sym"
109
113
  else
110
114
  echo " MISS $sym"
@@ -543,6 +543,13 @@ LeptrisStatus leptris_element_prepend_child(LeptrisElement parent, LeptrisElemen
543
543
  /* Call internal void function, assume success */
544
544
  leptris_element_prepend_child_internal(parent, (LeptrisNode*)child);
545
545
  leptris_element_invalidate_child_cache(parent);
546
+
547
+ /* TODO 132: the child splice changed the element set — a cached
548
+ * index would describe the pre-insert tree (leptris-ruby #519). */
549
+ if (leptris_element_get_document(parent)) {
550
+ leptris_element_index_invalidate(
551
+ leptris_element_get_document(parent));
552
+ }
546
553
  return LEPTRIS_OK;
547
554
  }
548
555
 
@@ -671,6 +678,13 @@ LeptrisStatus leptris_element_insert_before(LeptrisElement sibling, LeptrisEleme
671
678
 
672
679
  leptris_node_increment_version(LEPTRIS_ELEMENT_AS_NODE(parent));
673
680
  leptris_element_invalidate_child_cache(parent);
681
+
682
+ /* TODO 132: the sibling splice changed the element set — a cached
683
+ * index would describe the pre-insert tree (leptris-ruby #519). */
684
+ if (leptris_element_get_document(parent)) {
685
+ leptris_element_index_invalidate(
686
+ leptris_element_get_document(parent));
687
+ }
674
688
  return LEPTRIS_OK;
675
689
  }
676
690
 
@@ -785,6 +799,13 @@ LeptrisStatus leptris_element_insert_after(LeptrisElement sibling, LeptrisElemen
785
799
 
786
800
  leptris_node_increment_version(LEPTRIS_ELEMENT_AS_NODE(parent));
787
801
  leptris_element_invalidate_child_cache(parent);
802
+
803
+ /* TODO 132: the sibling splice changed the element set — a cached
804
+ * index would describe the pre-insert tree (leptris-ruby #519). */
805
+ if (leptris_element_get_document(parent)) {
806
+ leptris_element_index_invalidate(
807
+ leptris_element_get_document(parent));
808
+ }
788
809
  return LEPTRIS_OK;
789
810
  }
790
811
 
@@ -4035,10 +4035,13 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
4035
4035
  if (*q != ' ' && *q != '\t' && *q != '\n' &&
4036
4036
  *q != '\r') { ws = 0; break; }
4037
4037
  if (!ws) {
4038
- /* AfterFrameset processCharacters reports
4039
- * unexpected-char-after-frameset; the after-</html>
4040
- * flavor is reported by the expected-eof block below. */
4041
- if (!b->after_html)
4038
+ /* AfterFrameset/AfterAfterFrameset processCharacters
4039
+ * both report and DROP the run (no phase switch). */
4040
+ if (b->after_html)
4041
+ h_diag(b, "expected-eof-but-got-char",
4042
+ b->err_line_memo_off,
4043
+ "characters after </html> in the frameset phase");
4044
+ else
4042
4045
  h_diag(b, "unexpected-char-after-frameset",
4043
4046
  b->err_line_memo_off,
4044
4047
  "characters after </frameset>");
@@ -4068,12 +4071,15 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
4068
4071
  b->after_body_done = 1;
4069
4072
  /* AfterAfterBody/AfterAfterFrameset processCharacters
4070
4073
  * report expected-eof-but-got-char; AfterBody reports
4071
- * unexpected-char-after-body (tests2:55, tests19:41). */
4072
- if (b->after_html)
4074
+ * unexpected-char-after-body (tests2:55, tests19:41).
4075
+ * AfterAfterBody then switches to in-body and inserts
4076
+ * the run — clear the phase flags so later comments are
4077
+ * body content, not document epilog. */
4078
+ if (b->after_html) {
4073
4079
  h_diag(b, "expected-eof-but-got-char",
4074
4080
  b->err_line_memo_off,
4075
4081
  "characters after </html>");
4076
- else
4082
+ } else
4077
4083
  h_diag(b, "unexpected-char-after-body",
4078
4084
  b->err_line_memo_off,
4079
4085
  "characters after </body>");
@@ -4214,6 +4220,31 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
4214
4220
  b->err_line_memo_off,
4215
4221
  "hidden input inserted in table context");
4216
4222
  }
4223
+ /* Template content whose last node is a table-context
4224
+ * element inserts text as that node's sibling (the template
4225
+ * flavor of foster parenting). html5lib still reports the
4226
+ * foster (template.dat:112: <template><tr>…</tr>a). */
4227
+ if (b->whatwg && b->whatwg_foster &&
4228
+ leptris_node_get_type(n) == LEPTRIS_NODE_TYPE_TEXT &&
4229
+ b->depth > 0 &&
4230
+ b->open_id[b->depth - 1] == h_id_template) {
4231
+ const char* tc =
4232
+ leptris_text_node_get_content((LeptrisTextNode*)n);
4233
+ int nonws = 0;
4234
+ if (tc)
4235
+ for (const char* w = tc; *w; w++)
4236
+ if (*w != ' ' && *w != '\t' && *w != '\n' &&
4237
+ *w != '\r') { nonws = 1; break; }
4238
+ LeptrisNodeRef last = leptris_node_last_child_internal(
4239
+ (LeptrisNode*)b->open[b->depth - 1]);
4240
+ if (nonws && last &&
4241
+ leptris_node_get_type(last) ==
4242
+ LEPTRIS_NODE_TYPE_ELEMENT &&
4243
+ h_is_table_context((LeptrisElement)last))
4244
+ h_diag(b, "foster-parenting-character",
4245
+ b->err_line_memo_off,
4246
+ "text fostered in template table content");
4247
+ }
4217
4248
  /* #659 foster (WHATWG only): text/elements in table context
4218
4249
  * go before the nearest open table in ITS parent. */
4219
4250
  if (b->whatwg_foster && tbl_ctx && h_fosterable(b, n)) {
@@ -4652,7 +4683,7 @@ static void h_reconstruct(HBuilder* b) {
4652
4683
  /* The adoption agency proper. Returns 1 when the end tag is fully
4653
4684
  * consumed; 0 means no formatting entry matched and the caller
4654
4685
  * runs its generic (any-other-end-tag) handling. */
4655
- static int h_afe_end(HBuilder* b, const char* subject) {
4686
+ static int h_afe_end(HBuilder* b, const char* subject, size_t off) {
4656
4687
  if (b->depth > 0) {
4657
4688
  LeptrisElement cur = b->open[b->depth - 1];
4658
4689
  const char* cn = leptris_element_name(cur);
@@ -4703,6 +4734,14 @@ static int h_afe_end(HBuilder* b, const char* subject) {
4703
4734
  return 1;
4704
4735
  }
4705
4736
  }
4737
+ /* html5lib adoption-agency-1.3: the formatting element is
4738
+ * in the stack and in scope, but open elements sit above
4739
+ * it (it is not the current node). Parsing proceeds. */
4740
+ if (b->whatwg_adopt && b->depth != (size_t)si + 1) {
4741
+ h_diag(b, "adoption-agency-1.3", off,
4742
+ "</%s> formatting element is not the current node",
4743
+ subject);
4744
+ }
4706
4745
  int fbi = -1;
4707
4746
  for (size_t k = (size_t)si + 1; k < b->depth; k++) {
4708
4747
  /* Only HTML-namespace elements are furthest-block
@@ -6107,6 +6146,15 @@ static LeptrisDocument html_parse_shared(
6107
6146
  }
6108
6147
  p = (ce + 3 <= end) ? ce + 3 : end;
6109
6148
  } else {
6149
+ /* Tokenizer markup-declaration-open: "<!" that
6150
+ * is neither a comment nor DOCTYPE (and, in
6151
+ * HTML content, not CDATA) reports
6152
+ * expected-dashes-or-doctype (tests10:2:
6153
+ * <![CDATA[a]]> outside foreign content). */
6154
+ if (b.whatwg)
6155
+ h_diag(&b, "expected-dashes-or-doctype",
6156
+ (size_t)(p - b.input_base),
6157
+ "<! markup declaration");
6110
6158
  /* CDATA-ish bogus: skip to '>'. WHATWG makes
6111
6159
  * it a bogus comment (tests1:42-49) — except
6112
6160
  * doctype-shaped constructs, which the
@@ -6217,6 +6265,20 @@ static LeptrisDocument html_parse_shared(
6217
6265
  const char* q = ns;
6218
6266
  while (q < end && !h_is_ws(*q) && *q != '>' && *q != '/') q++;
6219
6267
  size_t nlen = (size_t)(q - ns);
6268
+ /* Tokenizer: an end tag may carry attributes
6269
+ * syntactically (they are ignored at tree time, but
6270
+ * the token reports attributes-in-end-tag;
6271
+ * tests2:18's </b test<b ...>). A lone "/" before
6272
+ * ">" is the self-closing form, not an attribute. */
6273
+ {
6274
+ const char* tq = q;
6275
+ while (tq < end && h_is_ws(*tq)) tq++;
6276
+ if (tq < end && *tq != '>' &&
6277
+ !(*tq == '/' && tq + 1 < end && tq[1] == '>'))
6278
+ h_diag(&b, "attributes-in-end-tag",
6279
+ (size_t)(tq - b.input_base),
6280
+ "attributes in an end tag");
6281
+ }
6220
6282
  while (q < end && *q != '>') q++;
6221
6283
  if (q < end) q++;
6222
6284
  /* Flush pending text before closing. A
@@ -6254,6 +6316,17 @@ static LeptrisDocument html_parse_shared(
6254
6316
  for (size_t i = 0; i < cl0; i++)
6255
6317
  lname0[i] = h_lower(ns[i]);
6256
6318
  lname0[cl0] = 0;
6319
+ /* html5lib InForeignContent.processEndTag: an end
6320
+ * tag that does not name the current foreign node
6321
+ * reports before the walk (tests19:82: <math></p>). */
6322
+ {
6323
+ const char* topn = leptris_element_name(
6324
+ b.open[b.depth - 1]);
6325
+ if (topn && !h_ieq_raw(topn, lname0))
6326
+ h_diag(&b, "unexpected-end-tag",
6327
+ (size_t)(p - b.input_base),
6328
+ "</%s> in foreign content", lname0);
6329
+ }
6257
6330
  int match_foreign = 0, crossed_ip = 0;
6258
6331
  for (size_t d = b.depth; d > 0; d--) {
6259
6332
  if (b.open_ns[d - 1] == H_NS_HTML) break;
@@ -6304,6 +6377,15 @@ static LeptrisDocument html_parse_shared(
6304
6377
  text = p;
6305
6378
  continue;
6306
6379
  }
6380
+ /* AfterAfterBody/AfterAfterFrameset processEndTag: ANY
6381
+ * end tag is a phase error, and the token is then
6382
+ * reprocessed in body — report once and fall through
6383
+ * with the phase flags cleared (tests19:43). */
6384
+ if (b.whatwg && b.after_html) {
6385
+ h_diag(&b, "expected-eof-but-got-end-tag",
6386
+ (size_t)(p - b.input_base),
6387
+ "end tag after </html>");
6388
+ }
6307
6389
  /* Stray end tags with no open stack. The raw-text family
6308
6390
  * completes during start-tag handling; void elements and
6309
6391
  * menuitem have no in-body end handler. html5lib's
@@ -6319,7 +6401,16 @@ static LeptrisDocument html_parse_shared(
6319
6401
  int stray = h_is_rawtext_name(rn) ||
6320
6402
  (h_is_void(rn) && strcmp(rn, "br") != 0) ||
6321
6403
  strcmp(rn, "menuitem") == 0;
6322
- if (stray) {
6404
+ /* BeforeHtml endTagOther: every end tag other than
6405
+ * head/body/html/br reports here (html5lib
6406
+ * BeforeHtmlPhase; tests2:18's </b ...>). */
6407
+ int before_html = !b.top_tail && !b.head_tag_seen &&
6408
+ !b.body_seen && !b.body_tag_seen;
6409
+ int bb_exempt = strcmp(rn, "head") == 0 ||
6410
+ strcmp(rn, "body") == 0 ||
6411
+ strcmp(rn, "html") == 0 ||
6412
+ strcmp(rn, "br") == 0;
6413
+ if (stray || (before_html && !bb_exempt)) {
6323
6414
  const char* code = "unexpected-end-tag";
6324
6415
  if (b.after_html)
6325
6416
  code = "expected-eof-but-got-end-tag";
@@ -6329,9 +6420,8 @@ static LeptrisDocument html_parse_shared(
6329
6420
  code = "unexpected-end-tag-after-frameset";
6330
6421
  else if (b.frameset)
6331
6422
  code = "unexpected-end-tag-in-frameset";
6332
- else if (!b.top_tail && !b.head_tag_seen &&
6333
- !b.body_seen && !b.body_tag_seen)
6334
- code = "end-tag-after-implied-root";
6423
+ else if (before_html)
6424
+ code = "unexpected-end-tag-before-html";
6335
6425
  h_diag(&b, code, (size_t)(p - b.input_base),
6336
6426
  "</%s> has no matching open element", rn);
6337
6427
  p = q;
@@ -6339,6 +6429,39 @@ static LeptrisDocument html_parse_shared(
6339
6429
  continue;
6340
6430
  }
6341
6431
  }
6432
+ /* InHead/AfterHead endTagOther: a </p> before body
6433
+ * content is dropped with unexpected-end-tag and
6434
+ * inserts no empty p (tests19:3). Depth-0 only — the
6435
+ * in-body stray-p rule below owns the body phase. */
6436
+ if (b.whatwg && nlen == 1 && h_lower(ns[0]) == 'p' &&
6437
+ b.depth == 0 && !b.body_seen &&
6438
+ (b.head_tag_seen || b.head_end_seen) &&
6439
+ !b.frameset && !b.after_body && !b.after_html)
6440
+ h_diag(&b, "unexpected-end-tag",
6441
+ (size_t)(p - b.input_base),
6442
+ "</p> outside the body phase");
6443
+ /* BeforeHead endTagOther: before the head phase starts
6444
+ * (only the html element open), any end tag other than
6445
+ * head/body/html/br is end-tag-after-implied-root
6446
+ * (tests19:2: <html></p><!--foo-->). */
6447
+ if (b.whatwg && nlen && b.depth == 1 &&
6448
+ b.open_id[0] == h_id_html &&
6449
+ !b.head_tag_seen && !b.head_end_seen &&
6450
+ !b.body_seen && !b.body_tag_seen &&
6451
+ !b.frameset && !b.after_body && !b.after_html) {
6452
+ char* ph = h_pooled_lower(b.pool, ns, nlen);
6453
+ if (ph && strcmp(ph, "head") != 0 &&
6454
+ strcmp(ph, "body") != 0 &&
6455
+ strcmp(ph, "html") != 0 &&
6456
+ strcmp(ph, "br") != 0) {
6457
+ h_diag(&b, "end-tag-after-implied-root",
6458
+ (size_t)(p - b.input_base),
6459
+ "</%s> before the head phase", ph);
6460
+ p = q;
6461
+ text = p;
6462
+ continue;
6463
+ }
6464
+ }
6342
6465
  if (nlen && (b.depth > 0 ||
6343
6466
  (b.whatwg &&
6344
6467
  ((nlen == 4 &&
@@ -6376,7 +6499,7 @@ static LeptrisDocument html_parse_shared(
6376
6499
  * at body); a SPECIAL element crossed
6377
6500
  * ignores the tag (menuitem:8: x stays in
6378
6501
  * the p). */
6379
- int msp = 0;
6502
+ int msp = 0, implied_ok = 1;
6380
6503
  for (size_t d2 = b.depth; d2 > 0; d2--) {
6381
6504
  const char* on2 =
6382
6505
  leptris_element_name(b.open[d2 - 1]);
@@ -6387,13 +6510,39 @@ static LeptrisDocument html_parse_shared(
6387
6510
  break;
6388
6511
  }
6389
6512
  if (h_is_special_ww(on2)) break;
6513
+ if (!h_is_implied_end_name(on2))
6514
+ implied_ok = 0;
6390
6515
  }
6391
- (void)msp;
6516
+ /* html5lib endTagOther reports in both
6517
+ * shapes: the walk found the element but
6518
+ * non-implied nodes sit above it, or the
6519
+ * walk stopped at a special (stray). */
6520
+ if (!msp)
6521
+ h_diag(&b, "unexpected-end-tag",
6522
+ (size_t)(p - b.input_base),
6523
+ "</menuitem> is a stray end tag");
6524
+ else if (!implied_ok)
6525
+ h_diag(&b, "unexpected-end-tag",
6526
+ (size_t)(p - b.input_base),
6527
+ "</menuitem> with open elements above");
6392
6528
  p = q;
6393
6529
  text = p;
6394
6530
  continue;
6395
6531
  }
6396
6532
  if (b.whatwg && strcmp(lname, "form") == 0) {
6533
+ /* InTable endTagOther runs the body rule
6534
+ * under table voodoo: both codes report
6535
+ * (tests16:197: <form><table></form>). */
6536
+ if (b.depth > 0 &&
6537
+ h_id_table_ctx(b.open_id[b.depth - 1])) {
6538
+ h_diag(&b,
6539
+ "unexpected-end-tag-implies-table-voodoo",
6540
+ (size_t)(p - b.input_base),
6541
+ "</form> inside a table");
6542
+ h_diag(&b, "unexpected-end-tag",
6543
+ (size_t)(p - b.input_base),
6544
+ "</form> inside a table");
6545
+ }
6397
6546
  /* Vendored rule: </form> closes implied
6398
6547
  * end-tag layers (option/optgroup/li/dd/
6399
6548
  * dt/p), then pops the form ONLY when it
@@ -6564,7 +6713,8 @@ static LeptrisDocument html_parse_shared(
6564
6713
  * matched; otherwise the generic path below
6565
6714
  * is the any-other-end-tag run. */
6566
6715
  if (b.whatwg_adopt && h_is_formatting(lname) &&
6567
- h_afe_end(&b, lname)) {
6716
+ h_afe_end(&b, lname,
6717
+ (size_t)(p - b.input_base))) {
6568
6718
  p = q;
6569
6719
  text = p;
6570
6720
  continue;
@@ -7255,9 +7405,14 @@ static LeptrisDocument html_parse_shared(
7255
7405
  }
7256
7406
  if (!name) goto done;
7257
7407
  /* #659 (13.2.6.4.7): <image> is renamed <img> and
7258
- * reprocessed (tests1:90). */
7259
- if (b.whatwg && strcmp(name, "image") == 0)
7408
+ * reprocessed (tests1:90); html5lib reports the legacy
7409
+ * spelling as image-start-tag (tests19:89). */
7410
+ if (b.whatwg && strcmp(name, "image") == 0) {
7411
+ h_diag(&b, "image-start-tag",
7412
+ (size_t)(p - b.input_base),
7413
+ "<image> acts as <img>");
7260
7414
  name = h_pooled_lower(b.pool, "img", 3);
7415
+ }
7261
7416
  /* #1218: ONE classification per start tag — every
7262
7417
  * downstream consumer (frameset-ok, void, open) reads the
7263
7418
  * resolved entry instead of re-walking the bucket. */
@@ -7367,7 +7522,41 @@ static LeptrisDocument html_parse_shared(
7367
7522
  /* InBody/AfterHead startTagHead: any <head> after
7368
7523
  * the head phase started is an unexpected start tag
7369
7524
  * (tests6:1: </head> <head>). */
7370
- if (b.whatwg &&
7525
+ /* An explicit <head> while the IMPLIED head already
7526
+ * holds head content is html5lib's InHead
7527
+ * startTagHead — two-heads-are-not-better-than-one
7528
+ * (tests3:13: <html><meta><head>). */
7529
+ int head_content_before = 0;
7530
+ if (!b.head_tag_seen && !b.head_end_seen &&
7531
+ b.depth == 1 && b.open[0] &&
7532
+ b.open_id[0] == h_id_html) {
7533
+ LeptrisNodeRef hc =
7534
+ leptris_node_last_child_internal(
7535
+ (LeptrisNode*)b.open[0]);
7536
+ if (hc && leptris_node_get_type(hc) ==
7537
+ LEPTRIS_NODE_TYPE_ELEMENT) {
7538
+ const char* hn2 = leptris_element_name(
7539
+ (LeptrisElement)hc);
7540
+ head_content_before =
7541
+ hn2 && (h_ieq_raw(hn2, "title") ||
7542
+ h_ieq_raw(hn2, "meta") ||
7543
+ h_ieq_raw(hn2, "link") ||
7544
+ h_ieq_raw(hn2, "base") ||
7545
+ h_ieq_raw(hn2, "basefont") ||
7546
+ h_ieq_raw(hn2, "bgsound") ||
7547
+ h_ieq_raw(hn2, "script") ||
7548
+ h_ieq_raw(hn2, "style") ||
7549
+ h_ieq_raw(hn2, "noframes") ||
7550
+ h_ieq_raw(hn2, "noscript") ||
7551
+ h_ieq_raw(hn2, "template"));
7552
+ }
7553
+ }
7554
+ if (b.whatwg && head_content_before)
7555
+ h_diag(&b,
7556
+ "two-heads-are-not-better-than-one",
7557
+ (size_t)(p - b.input_base),
7558
+ "<head> in the implied head");
7559
+ else if (b.whatwg &&
7371
7560
  (b.head_tag_seen || b.head_end_seen ||
7372
7561
  b.body_tag_seen || b.body_seen))
7373
7562
  h_diag(&b, "unexpected-start-tag",
@@ -7685,7 +7874,30 @@ static LeptrisDocument html_parse_shared(
7685
7874
  /* 13.2.6.4.7: a <form> start tag with the form pointer
7686
7875
  * already set (a form still open) is IGNORED - <form><form>
7687
7876
  * is one form (tests6:13); likewise inside a table
7688
- * (tests20:46/47). </form> clears the pointer. */
7877
+ * (tests20:46/47). </form> clears the pointer.
7878
+ *
7879
+ * html5lib reports both flavors: InTable startTagForm
7880
+ * always says unexpected-form-in-table (even when it
7881
+ * inserts), and InBody reports unexpected-start-tag for a
7882
+ * duplicate (tests20:48: <table><form></table><form>). */
7883
+ if (b.whatwg && strcmp(name, "form") == 0) {
7884
+ int in_tbl = b.depth > 0 &&
7885
+ h_id_table_ctx(b.open_id[b.depth - 1]);
7886
+ if (!in_tbl && b.depth >= 2 &&
7887
+ b.open_ns[b.depth - 1] == H_NS_HTML &&
7888
+ b.open_id[b.depth - 1] == h_id_form &&
7889
+ b.open_ns[b.depth - 2] == H_NS_HTML &&
7890
+ b.open_id[b.depth - 2] == h_id_table)
7891
+ in_tbl = 1; /* table > form wrapper */
7892
+ if (in_tbl)
7893
+ h_diag(&b, "unexpected-form-in-table",
7894
+ (size_t)(p - b.input_base),
7895
+ "<form> in table context");
7896
+ else if (b.form_open)
7897
+ h_diag(&b, "unexpected-start-tag",
7898
+ (size_t)(p - b.input_base),
7899
+ "<form> while a form is open");
7900
+ }
7689
7901
  if (b.whatwg && strcmp(name, "form") == 0 && b.form_open) {
7690
7902
  /* The generic pre-element flush above already drained
7691
7903
  * the pending run. */
@@ -8331,7 +8543,7 @@ static LeptrisDocument html_parse_shared(
8331
8543
  }
8332
8544
  }
8333
8545
  if (!dup_out_of_scope) {
8334
- h_afe_end(&b, name);
8546
+ h_afe_end(&b, name, (size_t)(p - b.input_base));
8335
8547
  /* The agency's 8-iteration cap can leave the
8336
8548
  * entry — the start-tag branch removes it
8337
8549
  * unconditionally. */
@@ -8415,6 +8627,14 @@ static LeptrisDocument html_parse_shared(
8415
8627
  q += 2;
8416
8628
  break;
8417
8629
  }
8630
+ /* Tokenizer self-closing-start-tag: a solidus not
8631
+ * followed by '>' reports and reprocesses in
8632
+ * before-attribute-name (tests2:58). */
8633
+ if (b.whatwg)
8634
+ h_diag(&b,
8635
+ "unexpected-character-after-solidus-in-tag",
8636
+ (size_t)(q - b.input_base),
8637
+ "'/' before an attribute name");
8418
8638
  q++;
8419
8639
  continue;
8420
8640
  }
@@ -8749,6 +8969,14 @@ static LeptrisDocument html_parse_shared(
8749
8969
  drop = 1;
8750
8970
  break;
8751
8971
  }
8972
+ /* AfterAfterFrameset processCharacters reports
8973
+ * expected-eof-but-got-char for every non-space
8974
+ * character (tests19:41). */
8975
+ if (drop)
8976
+ h_diag(&b, "expected-eof-but-got-char",
8977
+ (size_t)(text - b.input_base),
8978
+ "characters after </html> in the frameset"
8979
+ " phase");
8752
8980
  }
8753
8981
  if (!drop) {
8754
8982
  if (!b.post_table_text) h_reconstruct(&b);
@@ -19,6 +19,17 @@ endif()
19
19
 
20
20
  include(GoogleTest)
21
21
 
22
+ # Native builds discover the gtest cases at BUILD time so the list is
23
+ # baked into CTestTestfile and `ctest --parallel` can schedule the
24
+ # whole suite. PRE_TEST expands the list while ctest runs, which only
25
+ # works serially — under -j the _NOT_BUILT placeholders execute. Cross
26
+ # builds must keep PRE_TEST: their binaries cannot run on the host.
27
+ if(CMAKE_CROSSCOMPILING)
28
+ set(LEPTRIS_GTEST_DISCOVERY_MODE PRE_TEST)
29
+ else()
30
+ set(LEPTRIS_GTEST_DISCOVERY_MODE POST_BUILD)
31
+ endif()
32
+
22
33
  # Convenience aggregate target — `cmake --build build --target check_all`.
23
34
  add_custom_target(check_all
24
35
  COMMENT "Running all Leptris specs")
@@ -52,10 +63,12 @@ function(leptris_add_test name)
52
63
  # Timing specs measure ratios; concurrent processes steal cache
53
64
  # and skew short measurements — run test_perf's cases serially.
54
65
  if(name STREQUAL "test_perf")
55
- gtest_discover_tests(${name} DISCOVERY_MODE PRE_TEST
66
+ gtest_discover_tests(${name}
67
+ DISCOVERY_MODE ${LEPTRIS_GTEST_DISCOVERY_MODE}
56
68
  PROPERTIES RUN_SERIAL TRUE)
57
69
  else()
58
- gtest_discover_tests(${name} DISCOVERY_MODE PRE_TEST)
70
+ gtest_discover_tests(${name}
71
+ DISCOVERY_MODE ${LEPTRIS_GTEST_DISCOVERY_MODE})
59
72
  endif()
60
73
  add_dependencies(check_all ${name})
61
74
  endfunction()
@@ -69,13 +82,15 @@ function(leptris_add_cli_test name)
69
82
  target_compile_definitions(${name} PRIVATE
70
83
  LEPTRIS_CLI_BIN="${CMAKE_BINARY_DIR}/cli/leptris"
71
84
  LEPTRIS_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
85
+ # WORKING_DIRECTORY is a first-class keyword in the modern
86
+ # gtest_discover_tests signature. Passing it inside PROPERTIES
87
+ # made the parser eat RUN_SERIAL as an unparsed argument, so the
88
+ # CLI cases were never actually serialized (ctest -j4 then races
89
+ # the shared fixture cwd — the schematron/rng flakes).
72
90
  gtest_discover_tests(${name}
73
- DISCOVERY_MODE PRE_TEST
74
- PROPERTIES
91
+ DISCOVERY_MODE ${LEPTRIS_GTEST_DISCOVERY_MODE}
75
92
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
76
- # CLI cases spawn subprocesses; fork/exec contention under
77
- # parallel ctest flakes them — run serially.
78
- RUN_SERIAL TRUE
93
+ PROPERTIES RUN_SERIAL TRUE
79
94
  )
80
95
  add_dependencies(check_all ${name} leptris-cli)
81
96
  endfunction()
@@ -998,6 +998,18 @@ TEST(Html5LibCorpus, ErrorVerdictParity) {
998
998
  * eof-in-comment-double-dash; the plaintext/EOF slice adds the
999
999
  * plaintext open-at-EOF and the "</" tokenizer EOF codes; the
1000
1000
  * stray-end-tag slice makes the depth-0 branch phase-aware for
1001
- * raw-text, void and menuitem names. Only raises. */
1002
- EXPECT_GE(parity, (size_t)1532);
1001
+ * raw-text, void and menuitem names; this batch adds the
1002
+ * depth-0 </p> phase code, the after-</html> end-tag code, the
1003
+ * frameset after-html character code, and image-start-tag; the
1004
+ * agency slice adds adoption-agency-1.3 for formatting end tags
1005
+ * whose element is in scope but not the current node; the
1006
+ * menuitem slice adds the endTagOther pair for </menuitem>
1007
+ * (stray before a special, or found with non-implied open
1008
+ * elements above); the closing slice adds the frameset
1009
+ * trailing-character code, BeforeHead end-tag-after-implied-
1010
+ * root, foreign-content end-tag mismatch, markup-declaration
1011
+ * (expected-dashes-or-doctype), solidus-in-tag, attributes-in-
1012
+ * end-tag, before-html end tags, form-in-table, template foster
1013
+ * characters and two-heads. Only raises. */
1014
+ EXPECT_GE(parity, (size_t)1555);
1003
1015
  }
@@ -94,6 +94,111 @@ TEST(XPathFunctions, CountReturnsNodeSetSize) {
94
94
  leptris_document_free(doc);
95
95
  }
96
96
 
97
+ /* leptris-ruby #519: a node-set XPath run primes the TODO 132 element
98
+ * index. Sibling insertions change the element set but did not
99
+ * invalidate it, so the next //name query answered from the stale
100
+ * index and missed the inserted element. Child insertion (append)
101
+ * always invalidated; every splice path must too. */
102
+ TEST(XPathIndexInvalidation, SiblingInsertionsRefreshDescendantAxis) {
103
+ const char xml[] = "<root><anchor/></root>";
104
+
105
+ auto eval_count = [](LeptrisDocument doc, const char* expr) -> int {
106
+ LeptrisXPathResult r = leptris_xpath_eval(doc, nullptr, expr);
107
+ if (!r) return -1;
108
+ int n = (int)leptris_xpath_result_count(r);
109
+ leptris_xpath_result_free(r);
110
+ return n;
111
+ };
112
+
113
+ /* TODO 190: the index builds on the SECOND axis query. Prime it so
114
+ * the post-insert query is served from the cached index — the
115
+ * stale-read shape in the report. */
116
+ auto prime = [&eval_count](LeptrisDocument doc) {
117
+ EXPECT_EQ(eval_count(doc, "//anchor"), 1);
118
+ EXPECT_EQ(eval_count(doc, "//anchor"), 1);
119
+ };
120
+
121
+ /* insert_after — the shape in the report (add_next_sibling). */
122
+ {
123
+ LeptrisDocument doc =
124
+ leptris_parse_string(xml, strlen(xml), nullptr);
125
+ ASSERT_NE(doc, nullptr);
126
+ prime(doc);
127
+ EXPECT_EQ(eval_count(doc, "//ext"), 0);
128
+ LeptrisElement anchor = leptris_element_first_child_any(
129
+ leptris_document_root(doc));
130
+ ASSERT_NE(anchor, nullptr);
131
+ LeptrisElement ext = leptris_element_create(doc, "ext");
132
+ ASSERT_NE(ext, nullptr);
133
+ ASSERT_EQ(leptris_element_insert_after(anchor, ext), LEPTRIS_OK);
134
+ EXPECT_EQ(eval_count(doc, "//ext"), 1);
135
+ leptris_document_free(doc);
136
+ }
137
+
138
+ /* insert_before. */
139
+ {
140
+ LeptrisDocument doc =
141
+ leptris_parse_string(xml, strlen(xml), nullptr);
142
+ ASSERT_NE(doc, nullptr);
143
+ prime(doc);
144
+ EXPECT_EQ(eval_count(doc, "//ext"), 0);
145
+ LeptrisElement anchor = leptris_element_first_child_any(
146
+ leptris_document_root(doc));
147
+ ASSERT_NE(anchor, nullptr);
148
+ LeptrisElement ext = leptris_element_create(doc, "ext");
149
+ ASSERT_NE(ext, nullptr);
150
+ ASSERT_EQ(leptris_element_insert_before(anchor, ext), LEPTRIS_OK);
151
+ EXPECT_EQ(eval_count(doc, "//ext"), 1);
152
+ leptris_document_free(doc);
153
+ }
154
+
155
+ /* prepend_child — the other child splice that skipped invalidation. */
156
+ {
157
+ LeptrisDocument doc =
158
+ leptris_parse_string(xml, strlen(xml), nullptr);
159
+ ASSERT_NE(doc, nullptr);
160
+ prime(doc);
161
+ EXPECT_EQ(eval_count(doc, "//ext"), 0);
162
+ LeptrisElement root = leptris_document_root(doc);
163
+ ASSERT_NE(root, nullptr);
164
+ LeptrisElement ext = leptris_element_create(doc, "ext");
165
+ ASSERT_NE(ext, nullptr);
166
+ ASSERT_EQ(leptris_element_prepend_child(root, ext), LEPTRIS_OK);
167
+ EXPECT_EQ(eval_count(doc, "//ext"), 1);
168
+ leptris_document_free(doc);
169
+ }
170
+
171
+ /* Compiled handle — the raw FFI face the binding evaluates with. */
172
+ {
173
+ LeptrisDocument doc =
174
+ leptris_parse_string(xml, strlen(xml), nullptr);
175
+ ASSERT_NE(doc, nullptr);
176
+ prime(doc);
177
+ LeptrisXPathCompiled compiled = leptris_xpath_compile("//ext");
178
+ ASSERT_NE(compiled, nullptr);
179
+ LeptrisXPathResult first =
180
+ leptris_xpath_compiled_eval(compiled, doc, nullptr);
181
+ ASSERT_NE(first, nullptr);
182
+ EXPECT_EQ(leptris_xpath_result_count(first), 0u);
183
+ leptris_xpath_result_free(first);
184
+
185
+ LeptrisElement anchor = leptris_element_first_child_any(
186
+ leptris_document_root(doc));
187
+ ASSERT_NE(anchor, nullptr);
188
+ LeptrisElement ext = leptris_element_create(doc, "ext");
189
+ ASSERT_NE(ext, nullptr);
190
+ ASSERT_EQ(leptris_element_insert_after(anchor, ext), LEPTRIS_OK);
191
+
192
+ LeptrisXPathResult second =
193
+ leptris_xpath_compiled_eval(compiled, doc, nullptr);
194
+ ASSERT_NE(second, nullptr);
195
+ EXPECT_EQ(leptris_xpath_result_count(second), 1u);
196
+ leptris_xpath_result_free(second);
197
+ leptris_xpath_compiled_free(compiled);
198
+ leptris_document_free(doc);
199
+ }
200
+ }
201
+
97
202
  /* Lane 07 tail (#692-B): function items report a distinct public
98
203
  * result type at the boundary — closures and named references. */
99
204
  TEST(XPathResultTypes, FunctionItemsReportFunctionType) {
@@ -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.377",
4
+ "version": "1.9.378",
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.377.0
4
+ version: 1.9.378.0
5
5
  platform: ppc64le-linux
6
6
  authors:
7
7
  - Ribose Inc.
@@ -206,6 +206,7 @@ files:
206
206
  - vendor-src/libleptris/CONTEXT.md
207
207
  - vendor-src/libleptris/LICENSE.md
208
208
  - vendor-src/libleptris/README.adoc
209
+ - vendor-src/libleptris/SECURITY.md
209
210
  - vendor-src/libleptris/TODO.SOTA.md
210
211
  - vendor-src/libleptris/TODO.assemble/README.md
211
212
  - vendor-src/libleptris/TODO.bindings/01-mutation-construction.md