leptris 1.9.377.0-ppc64le-linux → 1.9.379.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: 8030a18609fec29e852de93b2b94f8cf957284612bc8998beb3c75516b2cffdb
4
+ data.tar.gz: 149c48560dc7f74dadeeaa0cd5bfa22cb7450d00854335793fc0459d26051016
5
5
  SHA512:
6
- metadata.gz: 2fa752f9fb34474b74f9a5dc6dce4eec80672dc9ebb6c61b890e5798f89abb9fcc20d26e1efe0c56690c8f7245c623eaf889ca0bb63b151a39761c691bcf7508
7
- data.tar.gz: 9bfa01160fb2eb3357c2312698ba3f53bc3e364af71498d40a794ab714997ce2d542ef0c172d65d976f87130d2f9509bc487ce4b4fa632f39a220fa4d2aa1b72
6
+ metadata.gz: 2cebc95b23a6523629b3aaee353687cf4d58480cd4cbcca54f90f631f8905612b8f0074c6ff93fad0e4990a5ef61d2a31a80b00286b50390c97e3645870d6594
7
+ data.tar.gz: 0b339b3c5975ecba90447688bcaef0a307d5def2ac214cb25c97f793114147d2f186eecf50ed37a6791a14ee805d7bba7c244c57627e0a9fa210d565e3cd3262
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [1.9.379.0] - 2026-10-21
2
+
3
+ ### Fixed — engine sync (libleptris 1.9.379)
4
+
5
+ - **Source builds under GCC 14 (leptris/leptris#1806)**: the
6
+ template-foster diagnostic passed a cast node where the accessor
7
+ takes a `LeptrisNodeRef`; GCC 14 treats incompatible pointer
8
+ types as an error, so `rake compile` from source failed. The
9
+ cast is gone. Prebuilt gems are unaffected in behavior.
10
+
11
+ ## [1.9.378.0] - 2026-10-21
12
+
13
+ ### Fixed — engine sync (libleptris 1.9.378)
14
+
15
+ - **Node-set XPath after sibling insertion (leptris-ruby#519,
16
+ leptris/leptris#1802)**: the lazy element index was invalidated
17
+ by `add_child` but not by the sibling/prepend splice paths, so
18
+ `xpath("//name")` after `add_next_sibling` answered from the
19
+ pre-insert tree until an unrelated child insert rebuilt it.
20
+ `insert_before`, `insert_after` and `prepend_child` now
21
+ invalidate the document index.
22
+
23
+ ### Changed — engine sync
24
+
25
+ - **WHATWG HTML verdict parity complete (leptris/leptris#1803)**:
26
+ 1555/1555 corpus documents now report parse errors exactly when
27
+ html5lib does, with tree parity unchanged at 1555/1555.
28
+
1
29
  ## [1.9.356.0] - 2026-10-14
2
30
 
3
31
  ### 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.379"
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.379.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.379
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,74 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.379] - 2026-10-21
4
+
5
+ ### Fixed
6
+
7
+ - **html: build under GCC 14 and the wasm32 zero-warning gate (PR
8
+ #1806).** The template-foster diagnostic added in #1803 passed
9
+ `(LeptrisTextNode*)n` to `leptris_text_node_get_content`, whose
10
+ parameter is `LeptrisNodeRef`. GCC 14 (manylinux) treats
11
+ incompatible pointer types as a hard error and emcc flags them as
12
+ warnings — which broke the 1.9.378 wheel builds (leptris-py) and
13
+ failed the leptris-ts zero-warning gate on every lane. The
14
+ accessor takes the node handle directly; no behavior change.
15
+ Full suite 2075/2075.
16
+
17
+
18
+ ## [1.9.378] - 2026-10-21
19
+
20
+ ### Added
21
+
22
+ - **SECURITY.md: private reporting channel and CVE-response note
23
+ (PR #1804).** Reports go through GitHub Security Advisories, with
24
+ an acknowledgment target, coordinated disclosure, and CVE
25
+ assignment when warranted. The supported-version table follows
26
+ the lockstep rule: a fix is delivered only when the binding
27
+ release carries the fixed engine. The hardening inventory records
28
+ what actually ships: zero required runtime dependencies,
29
+ ASAN+leak and macOS `leaks` lanes, nightly libFuzzer on the
30
+ parser, explicit depth/size limits, and the conformance suites.
31
+ - **Test suite runs in parallel without changing any test (PR
32
+ #1799).** `gtest_discover_tests` now bakes the case list at build
33
+ time for native builds (`POST_BUILD`; cross builds keep
34
+ `PRE_TEST`), so `ctest --parallel` expands real cases instead of
35
+ `_NOT_BUILT` placeholders. CI runs the native and matrix lanes at
36
+ `--parallel 4` (qemu s390x and ILP32 stay serial). The CLI test
37
+ helper passed `WORKING_DIRECTORY` inside `PROPERTIES`, which
38
+ silently dropped the following `RUN_SERIAL`; passing it as a
39
+ keyword restores the serialization. Same 2074 cases, all green:
40
+ 66.0 s serial → 31.0 s `-j4` locally; the export check alone
41
+ 20.7-23.3 s → 8.7-9.3 s.
42
+
43
+ ### Fixed
44
+
45
+ - **dom: node-set XPath no longer answers from a stale element
46
+ index after sibling insertion (leptris-ruby#519, PR #1802).** The
47
+ lazy TODO 132 index was invalidated by `append_child` but not by
48
+ `insert_before`, `insert_after` or `prepend_child`, so a
49
+ `//name` query after `add_next_sibling` missed the new element
50
+ until an unrelated child insert rebuilt the index. All three
51
+ splice paths now invalidate the document index; the regression
52
+ spec covers each path plus the compiled-handle FFI face the
53
+ bindings evaluate with.
54
+ - **html: WHATWG verdict parity is complete — 1555/1555, zero
55
+ misses (PR #1803).** The remaining 20 corpus documents now report
56
+ at their html5lib source sites: `adoption-agency-1.3` for
57
+ formatting end tags whose element is in scope but not the current
58
+ node; the `</menuitem>` endTagOther pair (stray, and found with
59
+ non-implied open elements above); tokenizer
60
+ `expected-dashes-or-doctype`,
61
+ `unexpected-character-after-solidus-in-tag` and
62
+ `attributes-in-end-tag`; AfterAfterFrameset trailing characters,
63
+ BeforeHead `end-tag-after-implied-root`, BeforeHtml
64
+ `unexpected-end-tag-before-html`, and foreign-content end-tag
65
+ mismatch; `unexpected-form-in-table` for both the insertion and
66
+ the ignored duplicate plus the `</form>` table-voodoo pair;
67
+ template `foster-parenting-character`; and
68
+ `two-heads-are-not-better-than-one`. Tree parity stays
69
+ 1555/1555; the verdict floor is pinned at the full corpus count.
70
+
71
+
3
72
  ## [1.9.377] - 2026-10-20
4
73
 
5
74
  ### 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.379
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,30 @@ 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 = leptris_text_node_get_content(n);
4232
+ int nonws = 0;
4233
+ if (tc)
4234
+ for (const char* w = tc; *w; w++)
4235
+ if (*w != ' ' && *w != '\t' && *w != '\n' &&
4236
+ *w != '\r') { nonws = 1; break; }
4237
+ LeptrisNodeRef last = leptris_node_last_child_internal(
4238
+ (LeptrisNode*)b->open[b->depth - 1]);
4239
+ if (nonws && last &&
4240
+ leptris_node_get_type(last) ==
4241
+ LEPTRIS_NODE_TYPE_ELEMENT &&
4242
+ h_is_table_context((LeptrisElement)last))
4243
+ h_diag(b, "foster-parenting-character",
4244
+ b->err_line_memo_off,
4245
+ "text fostered in template table content");
4246
+ }
4217
4247
  /* #659 foster (WHATWG only): text/elements in table context
4218
4248
  * go before the nearest open table in ITS parent. */
4219
4249
  if (b->whatwg_foster && tbl_ctx && h_fosterable(b, n)) {
@@ -4652,7 +4682,7 @@ static void h_reconstruct(HBuilder* b) {
4652
4682
  /* The adoption agency proper. Returns 1 when the end tag is fully
4653
4683
  * consumed; 0 means no formatting entry matched and the caller
4654
4684
  * runs its generic (any-other-end-tag) handling. */
4655
- static int h_afe_end(HBuilder* b, const char* subject) {
4685
+ static int h_afe_end(HBuilder* b, const char* subject, size_t off) {
4656
4686
  if (b->depth > 0) {
4657
4687
  LeptrisElement cur = b->open[b->depth - 1];
4658
4688
  const char* cn = leptris_element_name(cur);
@@ -4703,6 +4733,14 @@ static int h_afe_end(HBuilder* b, const char* subject) {
4703
4733
  return 1;
4704
4734
  }
4705
4735
  }
4736
+ /* html5lib adoption-agency-1.3: the formatting element is
4737
+ * in the stack and in scope, but open elements sit above
4738
+ * it (it is not the current node). Parsing proceeds. */
4739
+ if (b->whatwg_adopt && b->depth != (size_t)si + 1) {
4740
+ h_diag(b, "adoption-agency-1.3", off,
4741
+ "</%s> formatting element is not the current node",
4742
+ subject);
4743
+ }
4706
4744
  int fbi = -1;
4707
4745
  for (size_t k = (size_t)si + 1; k < b->depth; k++) {
4708
4746
  /* Only HTML-namespace elements are furthest-block
@@ -6107,6 +6145,15 @@ static LeptrisDocument html_parse_shared(
6107
6145
  }
6108
6146
  p = (ce + 3 <= end) ? ce + 3 : end;
6109
6147
  } else {
6148
+ /* Tokenizer markup-declaration-open: "<!" that
6149
+ * is neither a comment nor DOCTYPE (and, in
6150
+ * HTML content, not CDATA) reports
6151
+ * expected-dashes-or-doctype (tests10:2:
6152
+ * <![CDATA[a]]> outside foreign content). */
6153
+ if (b.whatwg)
6154
+ h_diag(&b, "expected-dashes-or-doctype",
6155
+ (size_t)(p - b.input_base),
6156
+ "<! markup declaration");
6110
6157
  /* CDATA-ish bogus: skip to '>'. WHATWG makes
6111
6158
  * it a bogus comment (tests1:42-49) — except
6112
6159
  * doctype-shaped constructs, which the
@@ -6217,6 +6264,20 @@ static LeptrisDocument html_parse_shared(
6217
6264
  const char* q = ns;
6218
6265
  while (q < end && !h_is_ws(*q) && *q != '>' && *q != '/') q++;
6219
6266
  size_t nlen = (size_t)(q - ns);
6267
+ /* Tokenizer: an end tag may carry attributes
6268
+ * syntactically (they are ignored at tree time, but
6269
+ * the token reports attributes-in-end-tag;
6270
+ * tests2:18's </b test<b ...>). A lone "/" before
6271
+ * ">" is the self-closing form, not an attribute. */
6272
+ {
6273
+ const char* tq = q;
6274
+ while (tq < end && h_is_ws(*tq)) tq++;
6275
+ if (tq < end && *tq != '>' &&
6276
+ !(*tq == '/' && tq + 1 < end && tq[1] == '>'))
6277
+ h_diag(&b, "attributes-in-end-tag",
6278
+ (size_t)(tq - b.input_base),
6279
+ "attributes in an end tag");
6280
+ }
6220
6281
  while (q < end && *q != '>') q++;
6221
6282
  if (q < end) q++;
6222
6283
  /* Flush pending text before closing. A
@@ -6254,6 +6315,17 @@ static LeptrisDocument html_parse_shared(
6254
6315
  for (size_t i = 0; i < cl0; i++)
6255
6316
  lname0[i] = h_lower(ns[i]);
6256
6317
  lname0[cl0] = 0;
6318
+ /* html5lib InForeignContent.processEndTag: an end
6319
+ * tag that does not name the current foreign node
6320
+ * reports before the walk (tests19:82: <math></p>). */
6321
+ {
6322
+ const char* topn = leptris_element_name(
6323
+ b.open[b.depth - 1]);
6324
+ if (topn && !h_ieq_raw(topn, lname0))
6325
+ h_diag(&b, "unexpected-end-tag",
6326
+ (size_t)(p - b.input_base),
6327
+ "</%s> in foreign content", lname0);
6328
+ }
6257
6329
  int match_foreign = 0, crossed_ip = 0;
6258
6330
  for (size_t d = b.depth; d > 0; d--) {
6259
6331
  if (b.open_ns[d - 1] == H_NS_HTML) break;
@@ -6304,6 +6376,15 @@ static LeptrisDocument html_parse_shared(
6304
6376
  text = p;
6305
6377
  continue;
6306
6378
  }
6379
+ /* AfterAfterBody/AfterAfterFrameset processEndTag: ANY
6380
+ * end tag is a phase error, and the token is then
6381
+ * reprocessed in body — report once and fall through
6382
+ * with the phase flags cleared (tests19:43). */
6383
+ if (b.whatwg && b.after_html) {
6384
+ h_diag(&b, "expected-eof-but-got-end-tag",
6385
+ (size_t)(p - b.input_base),
6386
+ "end tag after </html>");
6387
+ }
6307
6388
  /* Stray end tags with no open stack. The raw-text family
6308
6389
  * completes during start-tag handling; void elements and
6309
6390
  * menuitem have no in-body end handler. html5lib's
@@ -6319,7 +6400,16 @@ static LeptrisDocument html_parse_shared(
6319
6400
  int stray = h_is_rawtext_name(rn) ||
6320
6401
  (h_is_void(rn) && strcmp(rn, "br") != 0) ||
6321
6402
  strcmp(rn, "menuitem") == 0;
6322
- if (stray) {
6403
+ /* BeforeHtml endTagOther: every end tag other than
6404
+ * head/body/html/br reports here (html5lib
6405
+ * BeforeHtmlPhase; tests2:18's </b ...>). */
6406
+ int before_html = !b.top_tail && !b.head_tag_seen &&
6407
+ !b.body_seen && !b.body_tag_seen;
6408
+ int bb_exempt = strcmp(rn, "head") == 0 ||
6409
+ strcmp(rn, "body") == 0 ||
6410
+ strcmp(rn, "html") == 0 ||
6411
+ strcmp(rn, "br") == 0;
6412
+ if (stray || (before_html && !bb_exempt)) {
6323
6413
  const char* code = "unexpected-end-tag";
6324
6414
  if (b.after_html)
6325
6415
  code = "expected-eof-but-got-end-tag";
@@ -6329,9 +6419,8 @@ static LeptrisDocument html_parse_shared(
6329
6419
  code = "unexpected-end-tag-after-frameset";
6330
6420
  else if (b.frameset)
6331
6421
  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";
6422
+ else if (before_html)
6423
+ code = "unexpected-end-tag-before-html";
6335
6424
  h_diag(&b, code, (size_t)(p - b.input_base),
6336
6425
  "</%s> has no matching open element", rn);
6337
6426
  p = q;
@@ -6339,6 +6428,39 @@ static LeptrisDocument html_parse_shared(
6339
6428
  continue;
6340
6429
  }
6341
6430
  }
6431
+ /* InHead/AfterHead endTagOther: a </p> before body
6432
+ * content is dropped with unexpected-end-tag and
6433
+ * inserts no empty p (tests19:3). Depth-0 only — the
6434
+ * in-body stray-p rule below owns the body phase. */
6435
+ if (b.whatwg && nlen == 1 && h_lower(ns[0]) == 'p' &&
6436
+ b.depth == 0 && !b.body_seen &&
6437
+ (b.head_tag_seen || b.head_end_seen) &&
6438
+ !b.frameset && !b.after_body && !b.after_html)
6439
+ h_diag(&b, "unexpected-end-tag",
6440
+ (size_t)(p - b.input_base),
6441
+ "</p> outside the body phase");
6442
+ /* BeforeHead endTagOther: before the head phase starts
6443
+ * (only the html element open), any end tag other than
6444
+ * head/body/html/br is end-tag-after-implied-root
6445
+ * (tests19:2: <html></p><!--foo-->). */
6446
+ if (b.whatwg && nlen && b.depth == 1 &&
6447
+ b.open_id[0] == h_id_html &&
6448
+ !b.head_tag_seen && !b.head_end_seen &&
6449
+ !b.body_seen && !b.body_tag_seen &&
6450
+ !b.frameset && !b.after_body && !b.after_html) {
6451
+ char* ph = h_pooled_lower(b.pool, ns, nlen);
6452
+ if (ph && strcmp(ph, "head") != 0 &&
6453
+ strcmp(ph, "body") != 0 &&
6454
+ strcmp(ph, "html") != 0 &&
6455
+ strcmp(ph, "br") != 0) {
6456
+ h_diag(&b, "end-tag-after-implied-root",
6457
+ (size_t)(p - b.input_base),
6458
+ "</%s> before the head phase", ph);
6459
+ p = q;
6460
+ text = p;
6461
+ continue;
6462
+ }
6463
+ }
6342
6464
  if (nlen && (b.depth > 0 ||
6343
6465
  (b.whatwg &&
6344
6466
  ((nlen == 4 &&
@@ -6376,7 +6498,7 @@ static LeptrisDocument html_parse_shared(
6376
6498
  * at body); a SPECIAL element crossed
6377
6499
  * ignores the tag (menuitem:8: x stays in
6378
6500
  * the p). */
6379
- int msp = 0;
6501
+ int msp = 0, implied_ok = 1;
6380
6502
  for (size_t d2 = b.depth; d2 > 0; d2--) {
6381
6503
  const char* on2 =
6382
6504
  leptris_element_name(b.open[d2 - 1]);
@@ -6387,13 +6509,39 @@ static LeptrisDocument html_parse_shared(
6387
6509
  break;
6388
6510
  }
6389
6511
  if (h_is_special_ww(on2)) break;
6512
+ if (!h_is_implied_end_name(on2))
6513
+ implied_ok = 0;
6390
6514
  }
6391
- (void)msp;
6515
+ /* html5lib endTagOther reports in both
6516
+ * shapes: the walk found the element but
6517
+ * non-implied nodes sit above it, or the
6518
+ * walk stopped at a special (stray). */
6519
+ if (!msp)
6520
+ h_diag(&b, "unexpected-end-tag",
6521
+ (size_t)(p - b.input_base),
6522
+ "</menuitem> is a stray end tag");
6523
+ else if (!implied_ok)
6524
+ h_diag(&b, "unexpected-end-tag",
6525
+ (size_t)(p - b.input_base),
6526
+ "</menuitem> with open elements above");
6392
6527
  p = q;
6393
6528
  text = p;
6394
6529
  continue;
6395
6530
  }
6396
6531
  if (b.whatwg && strcmp(lname, "form") == 0) {
6532
+ /* InTable endTagOther runs the body rule
6533
+ * under table voodoo: both codes report
6534
+ * (tests16:197: <form><table></form>). */
6535
+ if (b.depth > 0 &&
6536
+ h_id_table_ctx(b.open_id[b.depth - 1])) {
6537
+ h_diag(&b,
6538
+ "unexpected-end-tag-implies-table-voodoo",
6539
+ (size_t)(p - b.input_base),
6540
+ "</form> inside a table");
6541
+ h_diag(&b, "unexpected-end-tag",
6542
+ (size_t)(p - b.input_base),
6543
+ "</form> inside a table");
6544
+ }
6397
6545
  /* Vendored rule: </form> closes implied
6398
6546
  * end-tag layers (option/optgroup/li/dd/
6399
6547
  * dt/p), then pops the form ONLY when it
@@ -6564,7 +6712,8 @@ static LeptrisDocument html_parse_shared(
6564
6712
  * matched; otherwise the generic path below
6565
6713
  * is the any-other-end-tag run. */
6566
6714
  if (b.whatwg_adopt && h_is_formatting(lname) &&
6567
- h_afe_end(&b, lname)) {
6715
+ h_afe_end(&b, lname,
6716
+ (size_t)(p - b.input_base))) {
6568
6717
  p = q;
6569
6718
  text = p;
6570
6719
  continue;
@@ -7255,9 +7404,14 @@ static LeptrisDocument html_parse_shared(
7255
7404
  }
7256
7405
  if (!name) goto done;
7257
7406
  /* #659 (13.2.6.4.7): <image> is renamed <img> and
7258
- * reprocessed (tests1:90). */
7259
- if (b.whatwg && strcmp(name, "image") == 0)
7407
+ * reprocessed (tests1:90); html5lib reports the legacy
7408
+ * spelling as image-start-tag (tests19:89). */
7409
+ if (b.whatwg && strcmp(name, "image") == 0) {
7410
+ h_diag(&b, "image-start-tag",
7411
+ (size_t)(p - b.input_base),
7412
+ "<image> acts as <img>");
7260
7413
  name = h_pooled_lower(b.pool, "img", 3);
7414
+ }
7261
7415
  /* #1218: ONE classification per start tag — every
7262
7416
  * downstream consumer (frameset-ok, void, open) reads the
7263
7417
  * resolved entry instead of re-walking the bucket. */
@@ -7367,7 +7521,41 @@ static LeptrisDocument html_parse_shared(
7367
7521
  /* InBody/AfterHead startTagHead: any <head> after
7368
7522
  * the head phase started is an unexpected start tag
7369
7523
  * (tests6:1: </head> <head>). */
7370
- if (b.whatwg &&
7524
+ /* An explicit <head> while the IMPLIED head already
7525
+ * holds head content is html5lib's InHead
7526
+ * startTagHead — two-heads-are-not-better-than-one
7527
+ * (tests3:13: <html><meta><head>). */
7528
+ int head_content_before = 0;
7529
+ if (!b.head_tag_seen && !b.head_end_seen &&
7530
+ b.depth == 1 && b.open[0] &&
7531
+ b.open_id[0] == h_id_html) {
7532
+ LeptrisNodeRef hc =
7533
+ leptris_node_last_child_internal(
7534
+ (LeptrisNode*)b.open[0]);
7535
+ if (hc && leptris_node_get_type(hc) ==
7536
+ LEPTRIS_NODE_TYPE_ELEMENT) {
7537
+ const char* hn2 = leptris_element_name(
7538
+ (LeptrisElement)hc);
7539
+ head_content_before =
7540
+ hn2 && (h_ieq_raw(hn2, "title") ||
7541
+ h_ieq_raw(hn2, "meta") ||
7542
+ h_ieq_raw(hn2, "link") ||
7543
+ h_ieq_raw(hn2, "base") ||
7544
+ h_ieq_raw(hn2, "basefont") ||
7545
+ h_ieq_raw(hn2, "bgsound") ||
7546
+ h_ieq_raw(hn2, "script") ||
7547
+ h_ieq_raw(hn2, "style") ||
7548
+ h_ieq_raw(hn2, "noframes") ||
7549
+ h_ieq_raw(hn2, "noscript") ||
7550
+ h_ieq_raw(hn2, "template"));
7551
+ }
7552
+ }
7553
+ if (b.whatwg && head_content_before)
7554
+ h_diag(&b,
7555
+ "two-heads-are-not-better-than-one",
7556
+ (size_t)(p - b.input_base),
7557
+ "<head> in the implied head");
7558
+ else if (b.whatwg &&
7371
7559
  (b.head_tag_seen || b.head_end_seen ||
7372
7560
  b.body_tag_seen || b.body_seen))
7373
7561
  h_diag(&b, "unexpected-start-tag",
@@ -7685,7 +7873,30 @@ static LeptrisDocument html_parse_shared(
7685
7873
  /* 13.2.6.4.7: a <form> start tag with the form pointer
7686
7874
  * already set (a form still open) is IGNORED - <form><form>
7687
7875
  * is one form (tests6:13); likewise inside a table
7688
- * (tests20:46/47). </form> clears the pointer. */
7876
+ * (tests20:46/47). </form> clears the pointer.
7877
+ *
7878
+ * html5lib reports both flavors: InTable startTagForm
7879
+ * always says unexpected-form-in-table (even when it
7880
+ * inserts), and InBody reports unexpected-start-tag for a
7881
+ * duplicate (tests20:48: <table><form></table><form>). */
7882
+ if (b.whatwg && strcmp(name, "form") == 0) {
7883
+ int in_tbl = b.depth > 0 &&
7884
+ h_id_table_ctx(b.open_id[b.depth - 1]);
7885
+ if (!in_tbl && b.depth >= 2 &&
7886
+ b.open_ns[b.depth - 1] == H_NS_HTML &&
7887
+ b.open_id[b.depth - 1] == h_id_form &&
7888
+ b.open_ns[b.depth - 2] == H_NS_HTML &&
7889
+ b.open_id[b.depth - 2] == h_id_table)
7890
+ in_tbl = 1; /* table > form wrapper */
7891
+ if (in_tbl)
7892
+ h_diag(&b, "unexpected-form-in-table",
7893
+ (size_t)(p - b.input_base),
7894
+ "<form> in table context");
7895
+ else if (b.form_open)
7896
+ h_diag(&b, "unexpected-start-tag",
7897
+ (size_t)(p - b.input_base),
7898
+ "<form> while a form is open");
7899
+ }
7689
7900
  if (b.whatwg && strcmp(name, "form") == 0 && b.form_open) {
7690
7901
  /* The generic pre-element flush above already drained
7691
7902
  * the pending run. */
@@ -8331,7 +8542,7 @@ static LeptrisDocument html_parse_shared(
8331
8542
  }
8332
8543
  }
8333
8544
  if (!dup_out_of_scope) {
8334
- h_afe_end(&b, name);
8545
+ h_afe_end(&b, name, (size_t)(p - b.input_base));
8335
8546
  /* The agency's 8-iteration cap can leave the
8336
8547
  * entry — the start-tag branch removes it
8337
8548
  * unconditionally. */
@@ -8415,6 +8626,14 @@ static LeptrisDocument html_parse_shared(
8415
8626
  q += 2;
8416
8627
  break;
8417
8628
  }
8629
+ /* Tokenizer self-closing-start-tag: a solidus not
8630
+ * followed by '>' reports and reprocesses in
8631
+ * before-attribute-name (tests2:58). */
8632
+ if (b.whatwg)
8633
+ h_diag(&b,
8634
+ "unexpected-character-after-solidus-in-tag",
8635
+ (size_t)(q - b.input_base),
8636
+ "'/' before an attribute name");
8418
8637
  q++;
8419
8638
  continue;
8420
8639
  }
@@ -8749,6 +8968,14 @@ static LeptrisDocument html_parse_shared(
8749
8968
  drop = 1;
8750
8969
  break;
8751
8970
  }
8971
+ /* AfterAfterFrameset processCharacters reports
8972
+ * expected-eof-but-got-char for every non-space
8973
+ * character (tests19:41). */
8974
+ if (drop)
8975
+ h_diag(&b, "expected-eof-but-got-char",
8976
+ (size_t)(text - b.input_base),
8977
+ "characters after </html> in the frameset"
8978
+ " phase");
8752
8979
  }
8753
8980
  if (!drop) {
8754
8981
  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.379",
5
5
  "description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
6
6
  "homepage": "https://github.com/leptris/leptris",
7
7
  "license": "MIT",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.377.0
4
+ version: 1.9.379.0
5
5
  platform: ppc64le-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-20 00:00:00.000000000 Z
11
+ date: 2026-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -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