leptris 1.9.376.0-arm-linux → 1.9.378.0-arm-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: a1d2a6ae07effed7aa6c2d7404f20d7ec0cc5021ef11a786896aa0c8c0d03b6f
4
- data.tar.gz: d495291feb1edc3009fbb7534863a4439fc92178bd6e663cd38222a7a52b0b18
3
+ metadata.gz: 58cc51cae8a4f9289c5b9723a939cda6f38b38f9bb691576eeffe596b3046b95
4
+ data.tar.gz: 6683f3b0aa19ec7e51fea7ec8bd89c4fa3b2c01f29364c5e079f362e095cd385
5
5
  SHA512:
6
- metadata.gz: 506eddafab0d94da7b74df620168e5bfbd350eb65110c040aba1aa9cf6d11e67b452801e0216e979a9b9460afad147f0ab1fd04ac606fbb4719826cb8f5a75d8
7
- data.tar.gz: 9b23d01456189d659c1356c24192013fb4cb99ee3e89dec2556178b94f702c16f3b66458cc881ea2d4ca78705b914e6bc7581a5172e5fbb03ce2d26574866f8f
6
+ metadata.gz: e5070691fcec46dbe86269f38648d5bf30433cb72de31f6c7e9f7740c44a3a08f14b7df30945148b37f896f95f0ce964f646d5b4c4a63f94595cbf56bc0bb7be
7
+ data.tar.gz: d2e3fc4fd6d615e9947dda2abb979e7d3a08bed14e9af52d10e5df8d1e708e78c7ef9aeee1b7439c158aeee567c5f7cdd83f4de212cbcce765fd19f7357c0614
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.376"
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.376.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.376
4
+ - libleptris v1.9.378
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,80 @@
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
+
57
+ ## [1.9.377] - 2026-10-20
58
+
59
+ ### Added
60
+
61
+ - **html: verdict parity 1508 to 1532 of 1555 (PR #1797).** Five
62
+ emission slices, each grounded in html5lib's parser/tokenizer
63
+ source and gated by the #1771 corpus harness with zero want=clean
64
+ regressions: misplaced in-body caption/col/…/tr and `<frame>`
65
+ (unexpected-start-tag-ignored) plus the stray `</p>` phase codes;
66
+ after-html/after-body/after-frameset character codes
67
+ (expected-eof-but-got-char, unexpected-char-after-body,
68
+ unexpected-char-after-frameset); unterminated-comment tokenizer
69
+ codes (eof-in-comment, eof-in-comment-double-dash);
70
+ plaintext-at-EOF and the `</` tokenizer EOF codes; and a
71
+ phase-aware depth-0 stray-end-tag branch covering raw-text names,
72
+ void elements and menuitem (end-tag-after-implied-root before
73
+ html, after-body/-frameset/after-html variants, otherwise
74
+ unexpected-end-tag). Tree parity stays 1555/1555; the verdict
75
+ floor rose from 1508 to 1532.
76
+
77
+
3
78
  ## [1.9.376] - 2026-10-20
4
79
 
5
80
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.376
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,6 +4035,16 @@ 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/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
4045
+ h_diag(b, "unexpected-char-after-frameset",
4046
+ b->err_line_memo_off,
4047
+ "characters after </frameset>");
4038
4048
  /* Every whitespace character inserts, every
4039
4049
  * non-whitespace one drops (tests2:7: " te st" ->
4040
4050
  * " "). Filter in place, like after-frameset. */
@@ -4057,7 +4067,23 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
4057
4067
  for (const char* q = tt; *q; q++)
4058
4068
  if (*q != ' ' && *q != '\t' && *q != '\n' &&
4059
4069
  *q != '\r') { nonws = 1; break; }
4060
- if (nonws) b->after_body_done = 1;
4070
+ if (nonws) {
4071
+ b->after_body_done = 1;
4072
+ /* AfterAfterBody/AfterAfterFrameset processCharacters
4073
+ * report expected-eof-but-got-char; AfterBody reports
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) {
4079
+ h_diag(b, "expected-eof-but-got-char",
4080
+ b->err_line_memo_off,
4081
+ "characters after </html>");
4082
+ } else
4083
+ h_diag(b, "unexpected-char-after-body",
4084
+ b->err_line_memo_off,
4085
+ "characters after </body>");
4086
+ }
4061
4087
  }
4062
4088
  /* Doc-start mixed run: the ws prefix is "before html"
4063
4089
  * whitespace and drops (doctype01:31, tests19:79). Tightest
@@ -4194,6 +4220,31 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
4194
4220
  b->err_line_memo_off,
4195
4221
  "hidden input inserted in table context");
4196
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
+ }
4197
4248
  /* #659 foster (WHATWG only): text/elements in table context
4198
4249
  * go before the nearest open table in ITS parent. */
4199
4250
  if (b->whatwg_foster && tbl_ctx && h_fosterable(b, n)) {
@@ -4632,7 +4683,7 @@ static void h_reconstruct(HBuilder* b) {
4632
4683
  /* The adoption agency proper. Returns 1 when the end tag is fully
4633
4684
  * consumed; 0 means no formatting entry matched and the caller
4634
4685
  * runs its generic (any-other-end-tag) handling. */
4635
- static int h_afe_end(HBuilder* b, const char* subject) {
4686
+ static int h_afe_end(HBuilder* b, const char* subject, size_t off) {
4636
4687
  if (b->depth > 0) {
4637
4688
  LeptrisElement cur = b->open[b->depth - 1];
4638
4689
  const char* cn = leptris_element_name(cur);
@@ -4683,6 +4734,14 @@ static int h_afe_end(HBuilder* b, const char* subject) {
4683
4734
  return 1;
4684
4735
  }
4685
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
+ }
4686
4745
  int fbi = -1;
4687
4746
  for (size_t k = (size_t)si + 1; k < b->depth; k++) {
4688
4747
  /* Only HTML-namespace elements are furthest-block
@@ -5797,12 +5856,25 @@ static LeptrisDocument html_parse_shared(
5797
5856
  size_t clen = cclose
5798
5857
  ? (size_t)(ce - cs)
5799
5858
  : (size_t)(end - cs);
5800
- if (!cclose)
5859
+ if (!cclose) {
5860
+ /* Tokenizer EOF states: a trailing "--" is
5861
+ * comment-end-double-dash (tests2:59), anything
5862
+ * else is a plain eof-in-comment (tests2:35/43). */
5863
+ if (b.whatwg) {
5864
+ int dd = clen >= 2 && cs[clen - 1] == '-' &&
5865
+ cs[clen - 2] == '-';
5866
+ h_diag(&b,
5867
+ dd ? "eof-in-comment-double-dash"
5868
+ : "eof-in-comment",
5869
+ (size_t)(end - b.input_base),
5870
+ "comment reaches end of input");
5871
+ }
5801
5872
  /* Unterminated at EOF: html5lib drops the
5802
5873
  * trailing dash run (tests2:59: <!--x-- keeps
5803
5874
  * data "x"). */
5804
5875
  while (clen > 0 && cs[clen - 1] == '-')
5805
5876
  clen--;
5877
+ }
5806
5878
  p = cclose ? ce + cclose : end;
5807
5879
  LeptrisCommentNode* c = leptris_comment_create(
5808
5880
  cs, clen, b.pool);
@@ -6074,6 +6146,15 @@ static LeptrisDocument html_parse_shared(
6074
6146
  }
6075
6147
  p = (ce + 3 <= end) ? ce + 3 : end;
6076
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");
6077
6158
  /* CDATA-ish bogus: skip to '>'. WHATWG makes
6078
6159
  * it a bogus comment (tests1:42-49) — except
6079
6160
  * doctype-shaped constructs, which the
@@ -6111,6 +6192,18 @@ static LeptrisDocument html_parse_shared(
6111
6192
  {
6112
6193
  const char* ns2 = p + 2;
6113
6194
  if (b.whatwg && ns2 >= end) {
6195
+ /* Tokenizer: EOF in the tag-open state emits
6196
+ * expected-closing-tag-but-got-eof and the two
6197
+ * characters as text (tests1:38); in the
6198
+ * initial mode that text also reports the
6199
+ * missing doctype. */
6200
+ h_diag(&b, "expected-closing-tag-but-got-eof",
6201
+ (size_t)(p - b.input_base),
6202
+ "end tag at end of input");
6203
+ if (!b.left_initial && !doc->doctype)
6204
+ h_diag(&b, "expected-doctype-but-got-chars",
6205
+ (size_t)(p - b.input_base),
6206
+ "characters in the initial mode");
6114
6207
  if (text < p) {
6115
6208
  LeptrisTextNode* t =
6116
6209
  h_text_node(&b, text, p);
@@ -6172,6 +6265,20 @@ static LeptrisDocument html_parse_shared(
6172
6265
  const char* q = ns;
6173
6266
  while (q < end && !h_is_ws(*q) && *q != '>' && *q != '/') q++;
6174
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
+ }
6175
6282
  while (q < end && *q != '>') q++;
6176
6283
  if (q < end) q++;
6177
6284
  /* Flush pending text before closing. A
@@ -6209,6 +6316,17 @@ static LeptrisDocument html_parse_shared(
6209
6316
  for (size_t i = 0; i < cl0; i++)
6210
6317
  lname0[i] = h_lower(ns[i]);
6211
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
+ }
6212
6330
  int match_foreign = 0, crossed_ip = 0;
6213
6331
  for (size_t d = b.depth; d > 0; d--) {
6214
6332
  if (b.open_ns[d - 1] == H_NS_HTML) break;
@@ -6259,27 +6377,91 @@ static LeptrisDocument html_parse_shared(
6259
6377
  text = p;
6260
6378
  continue;
6261
6379
  }
6262
- /* Stray raw-text end tag with no open stack: the raw-text
6263
- * element completed during its start-tag handling, so this
6264
- * token cannot close anything. html5lib's InHead/InBody
6265
- * endTagOther reports unexpected-end-tag once the tree has
6266
- * content (the synthesized body is the special node);
6267
- * with nothing seen yet the phase errors differently and
6268
- * the initial-mode checks above already cover it. */
6269
- if (b.whatwg && nlen && b.depth == 0 && b.top_tail) {
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
+ }
6389
+ /* Stray end tags with no open stack. The raw-text family
6390
+ * completes during start-tag handling; void elements and
6391
+ * menuitem have no in-body end handler. html5lib's
6392
+ * endTagOther reports by phase: before-html is
6393
+ * end-tag-after-implied-root; AfterBody/AfterFrameset
6394
+ * and AfterAfter* have their own codes; InHead/InBody/
6395
+ * AfterHead report unexpected-end-tag. */
6396
+ if (b.whatwg && nlen && b.depth == 0) {
6270
6397
  char rn[16];
6271
6398
  size_t rl = nlen < sizeof(rn) - 1 ? nlen : sizeof(rn) - 1;
6272
6399
  for (size_t i = 0; i < rl; i++) rn[i] = h_lower(ns[i]);
6273
6400
  rn[rl] = 0;
6274
- if (h_is_rawtext_name(rn)) {
6275
- h_diag(&b, "unexpected-end-tag",
6276
- (size_t)(p - b.input_base),
6401
+ int stray = h_is_rawtext_name(rn) ||
6402
+ (h_is_void(rn) && strcmp(rn, "br") != 0) ||
6403
+ strcmp(rn, "menuitem") == 0;
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)) {
6414
+ const char* code = "unexpected-end-tag";
6415
+ if (b.after_html)
6416
+ code = "expected-eof-but-got-end-tag";
6417
+ else if (b.after_body)
6418
+ code = "unexpected-end-tag-after-body";
6419
+ else if (b.frameset && b.after_frameset)
6420
+ code = "unexpected-end-tag-after-frameset";
6421
+ else if (b.frameset)
6422
+ code = "unexpected-end-tag-in-frameset";
6423
+ else if (before_html)
6424
+ code = "unexpected-end-tag-before-html";
6425
+ h_diag(&b, code, (size_t)(p - b.input_base),
6277
6426
  "</%s> has no matching open element", rn);
6278
6427
  p = q;
6279
6428
  text = p;
6280
6429
  continue;
6281
6430
  }
6282
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
+ }
6283
6465
  if (nlen && (b.depth > 0 ||
6284
6466
  (b.whatwg &&
6285
6467
  ((nlen == 4 &&
@@ -6295,6 +6477,19 @@ static LeptrisDocument html_parse_shared(
6295
6477
  for (size_t i = 0; i < cl; i++)
6296
6478
  lname[i] = h_lower(ns[i]);
6297
6479
  lname[cl] = 0;
6480
+ /* InBody endTagOther: a VOID element has no end
6481
+ * handler, so a stray end tag reports
6482
+ * unexpected-end-tag (tests25:15-20: </keygen>).
6483
+ * </br> is endTagBr and acts as <br> above. */
6484
+ if (b.whatwg && h_is_void(lname) &&
6485
+ strcmp(lname, "br") != 0) {
6486
+ h_diag(&b, "unexpected-end-tag",
6487
+ (size_t)(p - b.input_base),
6488
+ "</%s> is a void element", lname);
6489
+ p = q;
6490
+ text = p;
6491
+ continue;
6492
+ }
6298
6493
  if (!h_is_void(lname)) {
6299
6494
  if (b.whatwg &&
6300
6495
  strcmp(lname, "menuitem") == 0) {
@@ -6304,7 +6499,7 @@ static LeptrisDocument html_parse_shared(
6304
6499
  * at body); a SPECIAL element crossed
6305
6500
  * ignores the tag (menuitem:8: x stays in
6306
6501
  * the p). */
6307
- int msp = 0;
6502
+ int msp = 0, implied_ok = 1;
6308
6503
  for (size_t d2 = b.depth; d2 > 0; d2--) {
6309
6504
  const char* on2 =
6310
6505
  leptris_element_name(b.open[d2 - 1]);
@@ -6315,13 +6510,39 @@ static LeptrisDocument html_parse_shared(
6315
6510
  break;
6316
6511
  }
6317
6512
  if (h_is_special_ww(on2)) break;
6513
+ if (!h_is_implied_end_name(on2))
6514
+ implied_ok = 0;
6318
6515
  }
6319
- (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");
6320
6528
  p = q;
6321
6529
  text = p;
6322
6530
  continue;
6323
6531
  }
6324
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
+ }
6325
6546
  /* Vendored rule: </form> closes implied
6326
6547
  * end-tag layers (option/optgroup/li/dd/
6327
6548
  * dt/p), then pops the form ONLY when it
@@ -6433,6 +6654,16 @@ static LeptrisDocument html_parse_shared(
6433
6654
  (size_t)(p - b.input_base),
6434
6655
  "</%s> with no open heading", lname);
6435
6656
  }
6657
+ /* InHead/AfterHead endTagOther: a </p> before
6658
+ * body content is dropped with unexpected-end-tag
6659
+ * and inserts nothing (tests19:3). */
6660
+ if (b.whatwg && strcmp(lname, "p") == 0 &&
6661
+ !b.body_seen && (b.head_tag_seen ||
6662
+ b.head_end_seen) &&
6663
+ !b.frameset && !b.after_body && !b.after_html)
6664
+ h_diag(&b, "unexpected-end-tag",
6665
+ (size_t)(p - b.input_base),
6666
+ "</p> outside the body phase");
6436
6667
  /* #659 13.2.6.4.17: </p> with no p in button
6437
6668
  * scope inserts an EMPTY <p> and closes it
6438
6669
  * (tests1:29) — the stray tag still leaves a
@@ -6460,6 +6691,13 @@ static LeptrisDocument html_parse_shared(
6460
6691
  break;
6461
6692
  }
6462
6693
  if (!p_open) {
6694
+ /* InBody endTagP: no p in button scope
6695
+ * is a parse error even though the
6696
+ * empty p is still inserted
6697
+ * (tests2:60). */
6698
+ h_diag(&b, "unexpected-end-tag",
6699
+ (size_t)(p - b.input_base),
6700
+ "</p> with no p in scope");
6463
6701
  LeptrisElement pe =
6464
6702
  h_open_element(&b, "p");
6465
6703
  (void)pe;
@@ -6475,7 +6713,8 @@ static LeptrisDocument html_parse_shared(
6475
6713
  * matched; otherwise the generic path below
6476
6714
  * is the any-other-end-tag run. */
6477
6715
  if (b.whatwg_adopt && h_is_formatting(lname) &&
6478
- h_afe_end(&b, lname)) {
6716
+ h_afe_end(&b, lname,
6717
+ (size_t)(p - b.input_base))) {
6479
6718
  p = q;
6480
6719
  text = p;
6481
6720
  continue;
@@ -7166,9 +7405,14 @@ static LeptrisDocument html_parse_shared(
7166
7405
  }
7167
7406
  if (!name) goto done;
7168
7407
  /* #659 (13.2.6.4.7): <image> is renamed <img> and
7169
- * reprocessed (tests1:90). */
7170
- 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>");
7171
7414
  name = h_pooled_lower(b.pool, "img", 3);
7415
+ }
7172
7416
  /* #1218: ONE classification per start tag — every
7173
7417
  * downstream consumer (frameset-ok, void, open) reads the
7174
7418
  * resolved entry instead of re-walking the bucket. */
@@ -7278,7 +7522,41 @@ static LeptrisDocument html_parse_shared(
7278
7522
  /* InBody/AfterHead startTagHead: any <head> after
7279
7523
  * the head phase started is an unexpected start tag
7280
7524
  * (tests6:1: </head> <head>). */
7281
- 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 &&
7282
7560
  (b.head_tag_seen || b.head_end_seen ||
7283
7561
  b.body_tag_seen || b.body_seen))
7284
7562
  h_diag(&b, "unexpected-start-tag",
@@ -7596,7 +7874,30 @@ static LeptrisDocument html_parse_shared(
7596
7874
  /* 13.2.6.4.7: a <form> start tag with the form pointer
7597
7875
  * already set (a form still open) is IGNORED - <form><form>
7598
7876
  * is one form (tests6:13); likewise inside a table
7599
- * (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
+ }
7600
7901
  if (b.whatwg && strcmp(name, "form") == 0 && b.form_open) {
7601
7902
  /* The generic pre-element flush above already drained
7602
7903
  * the pending run. */
@@ -7694,6 +7995,13 @@ static LeptrisDocument html_parse_shared(
7694
7995
  }
7695
7996
  }
7696
7997
  if (!tbl_ctx) {
7998
+ /* InBody startTagMisplaced: caption/col/colgroup/
7999
+ * head/tbody/td/tfoot/th/thead/tr in body is a
8000
+ * parse error and the token is ignored
8001
+ * (tests25:7 <body><col>A). */
8002
+ h_diag(&b, "unexpected-start-tag-ignored",
8003
+ (size_t)(p - b.input_base),
8004
+ "<%s> ignored in the body phase", name);
7697
8005
  /* The TOKEN is ignored - surrounding character
7698
8006
  * tokens keep flowing into body (the html-merge
7699
8007
  * flush pattern). */
@@ -7712,6 +8020,20 @@ static LeptrisDocument html_parse_shared(
7712
8020
  * an ignored <frameset> vanishes). */
7713
8021
  if (b.whatwg && strcmp(name, "frame") == 0 &&
7714
8022
  (!b.frameset || b.after_frameset)) {
8023
+ /* InBody reports the frame as a misplaced start tag
8024
+ * (tests25:10); the after-</frameset> phase names its
8025
+ * own code (the after-</html> phase is reported at the
8026
+ * start-tag dispatch). */
8027
+ if (!b.after_html) {
8028
+ if (!b.frameset)
8029
+ h_diag(&b, "unexpected-start-tag-ignored",
8030
+ (size_t)(p - b.input_base),
8031
+ "<frame> ignored in the body phase");
8032
+ else
8033
+ h_diag(&b, "unexpected-start-tag-after-frameset",
8034
+ (size_t)(p - b.input_base),
8035
+ "<frame> after </frameset>");
8036
+ }
7715
8037
  while (q < end && *q != '>') q++;
7716
8038
  p = (q < end) ? q + 1 : end;
7717
8039
  text = p;
@@ -8221,7 +8543,7 @@ static LeptrisDocument html_parse_shared(
8221
8543
  }
8222
8544
  }
8223
8545
  if (!dup_out_of_scope) {
8224
- h_afe_end(&b, name);
8546
+ h_afe_end(&b, name, (size_t)(p - b.input_base));
8225
8547
  /* The agency's 8-iteration cap can leave the
8226
8548
  * entry — the start-tag branch removes it
8227
8549
  * unconditionally. */
@@ -8305,6 +8627,14 @@ static LeptrisDocument html_parse_shared(
8305
8627
  q += 2;
8306
8628
  break;
8307
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");
8308
8638
  q++;
8309
8639
  continue;
8310
8640
  }
@@ -8531,6 +8861,13 @@ static LeptrisDocument html_parse_shared(
8531
8861
  "input",
8532
8862
  name);
8533
8863
  }
8864
+ /* PLAINTEXT has no close form: it is still open at
8865
+ * EOF, so InBody processEOF reports the unclosed
8866
+ * element (tests18:2-7, tests19:6). */
8867
+ if (b.whatwg && cq >= end && strcmp(name, "plaintext") == 0)
8868
+ h_diag(&b, "expected-closing-tag-but-got-eof",
8869
+ (size_t)(end - b.input_base),
8870
+ "plaintext reaches end of input");
8534
8871
  b.depth--; /* the raw element is complete */
8535
8872
  text = p;
8536
8873
  continue;
@@ -8632,6 +8969,14 @@ static LeptrisDocument html_parse_shared(
8632
8969
  drop = 1;
8633
8970
  break;
8634
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");
8635
8980
  }
8636
8981
  if (!drop) {
8637
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()
@@ -990,6 +990,26 @@ TEST(Html5LibCorpus, ErrorVerdictParity) {
990
990
  * out-of-my-head tags; the select/table/foreign slice adds
991
991
  * unexpected-input-in-select, hidden-input-in-table, and
992
992
  * unexpected-html-element-in-foreign-content; the tokenizer
993
- * slice adds need-space-after-doctype. Only raises. */
994
- EXPECT_GE(parity, (size_t)1508);
993
+ * slice adds need-space-after-doctype; the misplaced-tag slice
994
+ * adds in-body caption/col/…/tr and frame (ignored), plus the
995
+ * stray </p> phase codes; the character-phase slice adds
996
+ * expected-eof-but-got-char and unexpected-char-after-body/
997
+ * -frameset; the tokenizer EOF slice adds eof-in-comment and
998
+ * eof-in-comment-double-dash; the plaintext/EOF slice adds the
999
+ * plaintext open-at-EOF and the "</" tokenizer EOF codes; the
1000
+ * stray-end-tag slice makes the depth-0 branch phase-aware for
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);
995
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.376",
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.376.0
4
+ version: 1.9.378.0
5
5
  platform: arm-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