leptris 1.9.375.0-arm-linux → 1.9.377.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 +4 -4
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/libleptris.so +0 -0
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +61 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/src/include/leptris.h +10 -0
- data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +126 -0
- data/vendor-src/libleptris/src/leptris/html/html_parse.c +411 -13
- data/vendor-src/libleptris/src/leptris/serialize/serialize.c +15 -43
- data/vendor-src/libleptris/test/html/test_html5lib.cpp +23 -5
- data/vendor-src/libleptris/test/parser/test_encoding.cpp +87 -0
- data/vendor-src/libleptris/test/serializer/test_serialize.cpp +65 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4af3684e876518a4183a065b8602a37505e4c5164bb4e2868716cfa1ae287656
|
|
4
|
+
data.tar.gz: 9f4b4e3eea18ada16652af23468ebf1a988736bc477241c53f7e3ea41632b96b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05db72ae97248e804e3b20a6db311f6dd0f9543c17b5a863a922d7c9e78fdccd7c6332f2b8a80661544296d8374daf5db3722e74e88a94c92f0d90d1674da1c2
|
|
7
|
+
data.tar.gz: 29bed9de894efbe76f20cf5fe0f87369f47e7142723e4acce90956ee8f5a215bf812eade0416e10bea57d0c3de3928689ac16dc7898969898cdac7e0610f32f6
|
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.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.377"
|
|
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
|
data/lib/leptris/version.rb
CHANGED
data/lib/libleptris.so
CHANGED
|
Binary file
|
data/vendor-src/README.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.377] - 2026-10-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **html: verdict parity 1508 to 1532 of 1555 (PR #1797).** Five
|
|
8
|
+
emission slices, each grounded in html5lib's parser/tokenizer
|
|
9
|
+
source and gated by the #1771 corpus harness with zero want=clean
|
|
10
|
+
regressions: misplaced in-body caption/col/…/tr and `<frame>`
|
|
11
|
+
(unexpected-start-tag-ignored) plus the stray `</p>` phase codes;
|
|
12
|
+
after-html/after-body/after-frameset character codes
|
|
13
|
+
(expected-eof-but-got-char, unexpected-char-after-body,
|
|
14
|
+
unexpected-char-after-frameset); unterminated-comment tokenizer
|
|
15
|
+
codes (eof-in-comment, eof-in-comment-double-dash);
|
|
16
|
+
plaintext-at-EOF and the `</` tokenizer EOF codes; and a
|
|
17
|
+
phase-aware depth-0 stray-end-tag branch covering raw-text names,
|
|
18
|
+
void elements and menuitem (end-tag-after-implied-root before
|
|
19
|
+
html, after-body/-frameset/after-html variants, otherwise
|
|
20
|
+
unexpected-end-tag). Tree parity stays 1555/1555; the verdict
|
|
21
|
+
floor rose from 1508 to 1532.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [1.9.376] - 2026-10-20
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **serialize: decoded entity-shaped text is escaped
|
|
29
|
+
unconditionally (leptris-ruby#515, PR #1793).** Parse expands
|
|
30
|
+
entities into text (named references that survive under
|
|
31
|
+
`KEEP_ENTITY_REFS` become `ENTITY_REF` nodes), but both text
|
|
32
|
+
emitters kept an entity lookahead that re-emitted decoded data
|
|
33
|
+
verbatim: `A` reparsed as `A`, and `<UK>` minted a
|
|
34
|
+
`<UK>` element that swallowed siblings. Every `&` in text is now
|
|
35
|
+
escaped; `ENTITY_REF` nodes still serialize as markup.
|
|
36
|
+
- **parse: strict loads validate the effective encoding
|
|
37
|
+
(leptris-ruby#514, PR #1793).** Non-RECOVER parses reject
|
|
38
|
+
invalid UTF-8 under XML 1.0 §4.3.3 (RFC 3629 strictness:
|
|
39
|
+
overlongs, surrogates, >U+10FFFF, truncation are fatal), for
|
|
40
|
+
documents that are UTF-8 by declaration or default. A declared
|
|
41
|
+
non-UTF-8 encoding is exempt (legacy encodings still transcode
|
|
42
|
+
through the iconv lane), and RECOVER keeps the #1660
|
|
43
|
+
best-effort-tree contract. The ASCII fast path scans
|
|
44
|
+
word-at-a-time (~14 GB/s, ~0.2% of a full parse).
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- **html: verdict parity 1446 to 1508 of 1555 (PR #1794).** Seven
|
|
49
|
+
emission slices, each grounded in html5lib's source and gated by
|
|
50
|
+
the #1771 corpus harness with zero want=clean regressions:
|
|
51
|
+
depth-0 stray raw-text end tags (`</script>`, `</style>`, …);
|
|
52
|
+
later `<html>` tokens (non-html-root); the end-tag-too-early
|
|
53
|
+
family (heading mismatch, implied-end crossings, and the same
|
|
54
|
+
rule on li/dd/dt/p implied closes); after-body/frameset phase
|
|
55
|
+
codes; InBody frameset, duplicate body/head, and AfterHead
|
|
56
|
+
out-of-my-head tags; select/table/foreign codes
|
|
57
|
+
(unexpected-input-in-select, hidden-input-in-table,
|
|
58
|
+
unexpected-html-element-in-foreign-content); and tokenizer
|
|
59
|
+
need-space-after-doctype. Tree parity stays 1555/1555; the
|
|
60
|
+
verdict floor rose from 1409 to 1508.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
3
64
|
## [1.9.375] - 2026-10-20
|
|
4
65
|
|
|
5
66
|
### Added
|
|
@@ -838,6 +838,11 @@ LEPTRIS_API LeptrisStatus leptris_document_set_root_ex(
|
|
|
838
838
|
/**
|
|
839
839
|
* Parse XML string into document
|
|
840
840
|
*
|
|
841
|
+
* Encoding: the input must be valid in its effective encoding. With
|
|
842
|
+
* no XML declaration (or a UTF-8/US-ASCII one) that is UTF-8 (XML
|
|
843
|
+
* 1.0 §4.3.3) and invalid byte sequences are a fatal load error. A
|
|
844
|
+
* declared legacy encoding is transcoded to UTF-8 before parsing.
|
|
845
|
+
*
|
|
841
846
|
* @param xml XML string (must be valid UTF-8)
|
|
842
847
|
* @param length Length of XML string in bytes
|
|
843
848
|
* @param status Output status code (can be NULL)
|
|
@@ -979,6 +984,11 @@ LEPTRIS_API int leptris_rng_error_line(LeptrisRelaxNG rng, size_t i);
|
|
|
979
984
|
LEPTRIS_API int leptris_rng_error_column(LeptrisRelaxNG rng, size_t i);
|
|
980
985
|
|
|
981
986
|
|
|
987
|
+
/* Parse with explicit flags. Strict (non-RECOVER) parses validate
|
|
988
|
+
* the input against its effective encoding (see leptris_parse_string);
|
|
989
|
+
* RECOVER keeps the best-effort #1660 contract and skips the fatal
|
|
990
|
+
* encoding check. A declared non-UTF-8 encoding is exempt from UTF-8
|
|
991
|
+
* validation and passes through raw in this lane. */
|
|
982
992
|
LEPTRIS_API LeptrisDocument leptris_parse_string_flags(const char* xml,
|
|
983
993
|
size_t length,
|
|
984
994
|
LeptrisParseFlags flags,
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
#include <string.h>
|
|
44
44
|
#include <stdlib.h>
|
|
45
45
|
#include <stdio.h>
|
|
46
|
+
#include <stdint.h>
|
|
46
47
|
|
|
47
48
|
/* File-scope externs: engine config TLS defined in core.c (declared
|
|
48
49
|
* in leptris_internal.h; this TU reads it without the umbrella) and
|
|
@@ -3415,6 +3416,123 @@ static struct leptris_document* dp_il_try(const char* xml, size_t len,
|
|
|
3415
3416
|
return d;
|
|
3416
3417
|
}
|
|
3417
3418
|
|
|
3419
|
+
/* ---- Load-time encoding validation (leptris-ruby#514) ----
|
|
3420
|
+
*
|
|
3421
|
+
* XML 1.0 §4.3.3: a document without a declaration is UTF-8; a
|
|
3422
|
+
* declared non-UTF-8 encoding is exempt (its bytes are legitimately
|
|
3423
|
+
* not UTF-8 — leptris_parse_string transcodes declared legacy
|
|
3424
|
+
* encodings through iconv before they reach this funnel). Structural
|
|
3425
|
+
* UTF-8 (RFC 3629) is strict: overlongs, surrogates, >U+10FFFF and
|
|
3426
|
+
* truncated sequences are fatal, matching libxml2's load-time
|
|
3427
|
+
* "Invalid bytes in character encoding". */
|
|
3428
|
+
|
|
3429
|
+
static int dp_ascii_case_eq(const char* a, const char* b, size_t n) {
|
|
3430
|
+
for (size_t i = 0; i < n; i++) {
|
|
3431
|
+
char x = a[i];
|
|
3432
|
+
char y = b[i];
|
|
3433
|
+
if (x >= 'A' && x <= 'Z') x += 32;
|
|
3434
|
+
if (y >= 'A' && y <= 'Z') y += 32;
|
|
3435
|
+
if (x != y) return 0;
|
|
3436
|
+
}
|
|
3437
|
+
return 1;
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
/* 1 when the XML declaration names an encoding other than UTF-8 /
|
|
3441
|
+
* US-ASCII. No (or unreadable) declaration -> 0, the UTF-8 default. */
|
|
3442
|
+
static int dp_declares_non_utf8(const char* s, size_t len) {
|
|
3443
|
+
size_t i = 0;
|
|
3444
|
+
if (len >= 3 && (unsigned char)s[0] == 0xEF &&
|
|
3445
|
+
(unsigned char)s[1] == 0xBB && (unsigned char)s[2] == 0xBF) {
|
|
3446
|
+
i = 3;
|
|
3447
|
+
}
|
|
3448
|
+
if (len - i < 5 || memcmp(s + i, "<?xml", 5) != 0) return 0;
|
|
3449
|
+
if (len - i > 5) {
|
|
3450
|
+
char c = s[i + 5];
|
|
3451
|
+
if (c != ' ' && c != '\t' && c != '\r' && c != '\n' && c != '?')
|
|
3452
|
+
return 0;
|
|
3453
|
+
}
|
|
3454
|
+
size_t j = i + 5;
|
|
3455
|
+
size_t end = j;
|
|
3456
|
+
while (end + 1 < len && !(s[end] == '?' && s[end + 1] == '>')) end++;
|
|
3457
|
+
if (end + 1 >= len) return 0;
|
|
3458
|
+
while (j < end) {
|
|
3459
|
+
if (end - j >= 8 && memcmp(s + j, "encoding", 8) == 0 &&
|
|
3460
|
+
(j == i + 5 || s[j - 1] == ' ' || s[j - 1] == '\t' ||
|
|
3461
|
+
s[j - 1] == '\r' || s[j - 1] == '\n')) {
|
|
3462
|
+
size_t k = j + 8;
|
|
3463
|
+
while (k < end && (s[k] == ' ' || s[k] == '\t' ||
|
|
3464
|
+
s[k] == '\r' || s[k] == '\n')) k++;
|
|
3465
|
+
if (k < end && s[k] == '=') {
|
|
3466
|
+
k++;
|
|
3467
|
+
while (k < end && (s[k] == ' ' || s[k] == '\t' ||
|
|
3468
|
+
s[k] == '\r' || s[k] == '\n')) k++;
|
|
3469
|
+
if (k < end && (s[k] == '"' || s[k] == '\'')) {
|
|
3470
|
+
char q = s[k++];
|
|
3471
|
+
size_t v = k;
|
|
3472
|
+
while (k < end && s[k] != q) k++;
|
|
3473
|
+
size_t vl = k - v;
|
|
3474
|
+
if (vl == 5 && dp_ascii_case_eq(s + v, "UTF-8", 5)) return 0;
|
|
3475
|
+
if (vl == 4 && dp_ascii_case_eq(s + v, "UTF8", 4)) return 0;
|
|
3476
|
+
if (vl == 8 && dp_ascii_case_eq(s + v, "US-ASCII", 8))
|
|
3477
|
+
return 0;
|
|
3478
|
+
return 1;
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
j++;
|
|
3483
|
+
}
|
|
3484
|
+
return 0;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
static int dp_utf8_valid(const char* s, size_t len) {
|
|
3488
|
+
const unsigned char* p = (const unsigned char*)s;
|
|
3489
|
+
size_t i = 0;
|
|
3490
|
+
/* ASCII fast path: 8 bytes per iteration. The DFA below runs
|
|
3491
|
+
* only when a high byte is present; ordinary documents pay one
|
|
3492
|
+
* memory-bandwidth pass. */
|
|
3493
|
+
while (i + 8 <= len) {
|
|
3494
|
+
uint64_t w;
|
|
3495
|
+
memcpy(&w, p + i, 8);
|
|
3496
|
+
if (w & UINT64_C(0x8080808080808080)) break;
|
|
3497
|
+
i += 8;
|
|
3498
|
+
}
|
|
3499
|
+
while (i < len && p[i] < 0x80) i++;
|
|
3500
|
+
while (i < len) {
|
|
3501
|
+
unsigned c = p[i];
|
|
3502
|
+
if (c < 0x80) {
|
|
3503
|
+
i++;
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3506
|
+
size_t n;
|
|
3507
|
+
unsigned lo = 0x80;
|
|
3508
|
+
unsigned hi = 0xBF;
|
|
3509
|
+
if (c >= 0xC2 && c <= 0xDF) {
|
|
3510
|
+
n = 1;
|
|
3511
|
+
} else if (c >= 0xE0 && c <= 0xEF) {
|
|
3512
|
+
n = 2;
|
|
3513
|
+
if (c == 0xE0) lo = 0xA0;
|
|
3514
|
+
else if (c == 0xED) hi = 0x9F;
|
|
3515
|
+
} else if (c >= 0xF0 && c <= 0xF4) {
|
|
3516
|
+
n = 3;
|
|
3517
|
+
if (c == 0xF0) lo = 0x90;
|
|
3518
|
+
else if (c == 0xF4) hi = 0x8F;
|
|
3519
|
+
} else {
|
|
3520
|
+
return 0;
|
|
3521
|
+
}
|
|
3522
|
+
if (len - i <= n) return 0;
|
|
3523
|
+
if (p[i + 1] < lo || p[i + 1] > hi) return 0;
|
|
3524
|
+
for (size_t k = 2; k <= n; k++) {
|
|
3525
|
+
if (p[i + k] < 0x80 || p[i + k] > 0xBF) return 0;
|
|
3526
|
+
}
|
|
3527
|
+
i += n + 1;
|
|
3528
|
+
}
|
|
3529
|
+
return 1;
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
static int dp_input_encoding_ok(const char* s, size_t len) {
|
|
3533
|
+
return dp_declares_non_utf8(s, len) || dp_utf8_valid(s, len);
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3418
3536
|
/* Public: copy the input then parse (standard path).
|
|
3419
3537
|
*
|
|
3420
3538
|
* The copy and the arena-sizing pre-scan are ONE fused SIMD pass
|
|
@@ -3424,6 +3542,7 @@ static struct leptris_document* dp_il_try(const char* xml, size_t len,
|
|
|
3424
3542
|
* arena holds only nodes and strings. */
|
|
3425
3543
|
struct leptris_document* direct_parse(const char* xml, size_t len) {
|
|
3426
3544
|
if (!xml || len == 0) return NULL;
|
|
3545
|
+
if (!dp_input_encoding_ok(xml, len)) return NULL;
|
|
3427
3546
|
{
|
|
3428
3547
|
struct leptris_document* d = dp_il_try(xml, len, 0, 0, 0);
|
|
3429
3548
|
if (d) return d;
|
|
@@ -3435,6 +3554,12 @@ struct leptris_document* direct_parse(const char* xml, size_t len) {
|
|
|
3435
3554
|
struct leptris_document* direct_parse_flags(const char* xml, size_t len,
|
|
3436
3555
|
unsigned parse_flags) {
|
|
3437
3556
|
if (!xml || len == 0) return NULL;
|
|
3557
|
+
/* RECOVER keeps the #1660 best-effort contract (never NULL for
|
|
3558
|
+
* malformed input); the fatal encoding check is strict-lane. */
|
|
3559
|
+
if (!(parse_flags & LEPTRIS_PARSE_RECOVER) &&
|
|
3560
|
+
!dp_input_encoding_ok(xml, len)) {
|
|
3561
|
+
return NULL;
|
|
3562
|
+
}
|
|
3438
3563
|
/* The interleaved lane has no recovery support (#1660) — RECOVER
|
|
3439
3564
|
* parses take the default lane. */
|
|
3440
3565
|
if (!(parse_flags & LEPTRIS_PARSE_RECOVER)) {
|
|
@@ -3462,6 +3587,7 @@ struct leptris_document* direct_parse_flags(const char* xml, size_t len,
|
|
|
3462
3587
|
/* Public: parse a caller-owned writable buffer without copying. */
|
|
3463
3588
|
struct leptris_document* direct_parse_inplace(char* buf, size_t len) {
|
|
3464
3589
|
if (!buf || len == 0) return NULL;
|
|
3590
|
+
if (!dp_input_encoding_ok(buf, len)) return NULL;
|
|
3465
3591
|
buf[len] = '\0'; /* Ensure NUL termination */
|
|
3466
3592
|
return direct_parse_internal(buf, len, 0, 0, 0, 0, 0, 0);
|
|
3467
3593
|
}
|
|
@@ -2767,6 +2767,9 @@ typedef struct {
|
|
|
2767
2767
|
* switch, so the divert stays armed. Re-armed by each
|
|
2768
2768
|
* </body>/</html> token. */
|
|
2769
2769
|
int after_body_done;
|
|
2770
|
+
/* Verdict bookkeeping only (never gates tree placement): the
|
|
2771
|
+
* AfterBody startTagOther error has fired for this phase. */
|
|
2772
|
+
int after_body_diag_done;
|
|
2770
2773
|
/* #659 two-mode split: leptris_parse_html_string is the WHATWG
|
|
2771
2774
|
* engine (full "in head" set: script/style/noscript/template
|
|
2772
2775
|
* ... lift into the implied head); the new
|
|
@@ -3299,6 +3302,53 @@ static int h_is_formatting(const char* n) {
|
|
|
3299
3302
|
return h_fmt4_lut[h_tag_id(n)];
|
|
3300
3303
|
}
|
|
3301
3304
|
|
|
3305
|
+
/* Raw-text tokenizer targets. In body they have no dedicated end-tag
|
|
3306
|
+
* handler: html5lib's endTagOther walks the stack and reports
|
|
3307
|
+
* unexpected-end-tag at the first special node, so every such token
|
|
3308
|
+
* reaching the generic walk is stray (the tokenizer consumes the
|
|
3309
|
+
* matching terminator when the element is actually open). */
|
|
3310
|
+
static int h_is_rawtext_name(const char* n) {
|
|
3311
|
+
return strcmp(n, "script") == 0 || strcmp(n, "style") == 0 ||
|
|
3312
|
+
strcmp(n, "title") == 0 || strcmp(n, "textarea") == 0 ||
|
|
3313
|
+
strcmp(n, "xmp") == 0 || strcmp(n, "iframe") == 0 ||
|
|
3314
|
+
strcmp(n, "noembed") == 0 || strcmp(n, "noframes") == 0;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
/* html5lib generateImpliedEndTags set (§13.2.4.5): closing a block
|
|
3318
|
+
* or list item may pop these silently; anything else above the
|
|
3319
|
+
* target makes the close "too early". */
|
|
3320
|
+
static int h_is_implied_end_name(const char* n) {
|
|
3321
|
+
return strcmp(n, "p") == 0 || strcmp(n, "li") == 0 ||
|
|
3322
|
+
strcmp(n, "dd") == 0 || strcmp(n, "dt") == 0 ||
|
|
3323
|
+
strcmp(n, "option") == 0 || strcmp(n, "optgroup") == 0 ||
|
|
3324
|
+
strcmp(n, "rb") == 0 || strcmp(n, "rt") == 0 ||
|
|
3325
|
+
strcmp(n, "rtc") == 0 || strcmp(n, "rp") == 0;
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
/* End tags html5lib implements with endTagBlock / endTagListItem /
|
|
3329
|
+
* endTagAppletMarqueeObject — the handlers that raise
|
|
3330
|
+
* end-tag-too-early when the target is not the current node after
|
|
3331
|
+
* implied-end generation. */
|
|
3332
|
+
static int h_is_tooearly_target(const char* n) {
|
|
3333
|
+
return strcmp(n, "p") == 0 || strcmp(n, "li") == 0 ||
|
|
3334
|
+
strcmp(n, "dd") == 0 || strcmp(n, "dt") == 0 ||
|
|
3335
|
+
strcmp(n, "applet") == 0 || strcmp(n, "marquee") == 0 ||
|
|
3336
|
+
strcmp(n, "object") == 0 ||
|
|
3337
|
+
strcmp(n, "address") == 0 || strcmp(n, "article") == 0 ||
|
|
3338
|
+
strcmp(n, "aside") == 0 || strcmp(n, "blockquote") == 0 ||
|
|
3339
|
+
strcmp(n, "button") == 0 || strcmp(n, "center") == 0 ||
|
|
3340
|
+
strcmp(n, "details") == 0 || strcmp(n, "dialog") == 0 ||
|
|
3341
|
+
strcmp(n, "dir") == 0 || strcmp(n, "div") == 0 ||
|
|
3342
|
+
strcmp(n, "dl") == 0 || strcmp(n, "fieldset") == 0 ||
|
|
3343
|
+
strcmp(n, "figcaption") == 0 || strcmp(n, "figure") == 0 ||
|
|
3344
|
+
strcmp(n, "footer") == 0 || strcmp(n, "header") == 0 ||
|
|
3345
|
+
strcmp(n, "hgroup") == 0 || strcmp(n, "listing") == 0 ||
|
|
3346
|
+
strcmp(n, "main") == 0 || strcmp(n, "menu") == 0 ||
|
|
3347
|
+
strcmp(n, "nav") == 0 || strcmp(n, "ol") == 0 ||
|
|
3348
|
+
strcmp(n, "pre") == 0 || strcmp(n, "section") == 0 ||
|
|
3349
|
+
strcmp(n, "summary") == 0 || strcmp(n, "ul") == 0;
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3302
3352
|
/* WHATWG "special" category (13.2.4.2) — the adoption agency's
|
|
3303
3353
|
* furthest-block candidates. */
|
|
3304
3354
|
/* Generated tag classifier (#1218 hot path): one first-char
|
|
@@ -3985,6 +4035,13 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
|
|
|
3985
4035
|
if (*q != ' ' && *q != '\t' && *q != '\n' &&
|
|
3986
4036
|
*q != '\r') { ws = 0; break; }
|
|
3987
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)
|
|
4042
|
+
h_diag(b, "unexpected-char-after-frameset",
|
|
4043
|
+
b->err_line_memo_off,
|
|
4044
|
+
"characters after </frameset>");
|
|
3988
4045
|
/* Every whitespace character inserts, every
|
|
3989
4046
|
* non-whitespace one drops (tests2:7: " te st" ->
|
|
3990
4047
|
* " "). Filter in place, like after-frameset. */
|
|
@@ -4007,7 +4064,20 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
|
|
|
4007
4064
|
for (const char* q = tt; *q; q++)
|
|
4008
4065
|
if (*q != ' ' && *q != '\t' && *q != '\n' &&
|
|
4009
4066
|
*q != '\r') { nonws = 1; break; }
|
|
4010
|
-
if (nonws)
|
|
4067
|
+
if (nonws) {
|
|
4068
|
+
b->after_body_done = 1;
|
|
4069
|
+
/* AfterAfterBody/AfterAfterFrameset processCharacters
|
|
4070
|
+
* report expected-eof-but-got-char; AfterBody reports
|
|
4071
|
+
* unexpected-char-after-body (tests2:55, tests19:41). */
|
|
4072
|
+
if (b->after_html)
|
|
4073
|
+
h_diag(b, "expected-eof-but-got-char",
|
|
4074
|
+
b->err_line_memo_off,
|
|
4075
|
+
"characters after </html>");
|
|
4076
|
+
else
|
|
4077
|
+
h_diag(b, "unexpected-char-after-body",
|
|
4078
|
+
b->err_line_memo_off,
|
|
4079
|
+
"characters after </body>");
|
|
4080
|
+
}
|
|
4011
4081
|
}
|
|
4012
4082
|
/* Doc-start mixed run: the ws prefix is "before html"
|
|
4013
4083
|
* whitespace and drops (doctype01:31, tests19:79). Tightest
|
|
@@ -4133,6 +4203,17 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
|
|
|
4133
4203
|
b->open_ns[b->depth - 2] == H_NS_HTML &&
|
|
4134
4204
|
b->open_id[b->depth - 2] == h_id_table)
|
|
4135
4205
|
tbl_ctx = 1;
|
|
4206
|
+
/* InTable startTagInput with type=hidden: html5lib reports
|
|
4207
|
+
* unexpected-hidden-input-in-table and inserts AT the spot
|
|
4208
|
+
* (no fostering — h_fosterable exempts it, tests7:16-20). */
|
|
4209
|
+
if (b->whatwg_foster && tbl_ctx && b->input_hidden_tag &&
|
|
4210
|
+
leptris_node_get_type(n) == LEPTRIS_NODE_TYPE_ELEMENT &&
|
|
4211
|
+
h_tag_id(leptris_element_name((LeptrisElement)n)) ==
|
|
4212
|
+
h_id_input) {
|
|
4213
|
+
h_diag(b, "unexpected-hidden-input-in-table",
|
|
4214
|
+
b->err_line_memo_off,
|
|
4215
|
+
"hidden input inserted in table context");
|
|
4216
|
+
}
|
|
4136
4217
|
/* #659 foster (WHATWG only): text/elements in table context
|
|
4137
4218
|
* go before the nearest open table in ITS parent. */
|
|
4138
4219
|
if (b->whatwg_foster && tbl_ctx && h_fosterable(b, n)) {
|
|
@@ -5736,12 +5817,25 @@ static LeptrisDocument html_parse_shared(
|
|
|
5736
5817
|
size_t clen = cclose
|
|
5737
5818
|
? (size_t)(ce - cs)
|
|
5738
5819
|
: (size_t)(end - cs);
|
|
5739
|
-
if (!cclose)
|
|
5820
|
+
if (!cclose) {
|
|
5821
|
+
/* Tokenizer EOF states: a trailing "--" is
|
|
5822
|
+
* comment-end-double-dash (tests2:59), anything
|
|
5823
|
+
* else is a plain eof-in-comment (tests2:35/43). */
|
|
5824
|
+
if (b.whatwg) {
|
|
5825
|
+
int dd = clen >= 2 && cs[clen - 1] == '-' &&
|
|
5826
|
+
cs[clen - 2] == '-';
|
|
5827
|
+
h_diag(&b,
|
|
5828
|
+
dd ? "eof-in-comment-double-dash"
|
|
5829
|
+
: "eof-in-comment",
|
|
5830
|
+
(size_t)(end - b.input_base),
|
|
5831
|
+
"comment reaches end of input");
|
|
5832
|
+
}
|
|
5740
5833
|
/* Unterminated at EOF: html5lib drops the
|
|
5741
5834
|
* trailing dash run (tests2:59: <!--x-- keeps
|
|
5742
5835
|
* data "x"). */
|
|
5743
5836
|
while (clen > 0 && cs[clen - 1] == '-')
|
|
5744
5837
|
clen--;
|
|
5838
|
+
}
|
|
5745
5839
|
p = cclose ? ce + cclose : end;
|
|
5746
5840
|
LeptrisCommentNode* c = leptris_comment_create(
|
|
5747
5841
|
cs, clen, b.pool);
|
|
@@ -5851,6 +5945,13 @@ static LeptrisDocument html_parse_shared(
|
|
|
5851
5945
|
}
|
|
5852
5946
|
if (is_dt) {
|
|
5853
5947
|
q += 7;
|
|
5948
|
+
/* Tokenizer: need-space-after-doctype when the
|
|
5949
|
+
* keyword is not followed by whitespace
|
|
5950
|
+
* (doctype01:3, tests2:26-28). */
|
|
5951
|
+
if (b.whatwg && q < end && *q != '>' && !h_is_ws(*q))
|
|
5952
|
+
h_diag(&b, "need-space-after-doctype",
|
|
5953
|
+
(size_t)(q - b.input_base),
|
|
5954
|
+
"no space after <!doctype");
|
|
5854
5955
|
while (q < end && h_is_ws(*q)) q++;
|
|
5855
5956
|
const char* nstart = q;
|
|
5856
5957
|
while (q < end && !h_is_ws(*q) && *q != '>') q++;
|
|
@@ -6043,6 +6144,18 @@ static LeptrisDocument html_parse_shared(
|
|
|
6043
6144
|
{
|
|
6044
6145
|
const char* ns2 = p + 2;
|
|
6045
6146
|
if (b.whatwg && ns2 >= end) {
|
|
6147
|
+
/* Tokenizer: EOF in the tag-open state emits
|
|
6148
|
+
* expected-closing-tag-but-got-eof and the two
|
|
6149
|
+
* characters as text (tests1:38); in the
|
|
6150
|
+
* initial mode that text also reports the
|
|
6151
|
+
* missing doctype. */
|
|
6152
|
+
h_diag(&b, "expected-closing-tag-but-got-eof",
|
|
6153
|
+
(size_t)(p - b.input_base),
|
|
6154
|
+
"end tag at end of input");
|
|
6155
|
+
if (!b.left_initial && !doc->doctype)
|
|
6156
|
+
h_diag(&b, "expected-doctype-but-got-chars",
|
|
6157
|
+
(size_t)(p - b.input_base),
|
|
6158
|
+
"characters in the initial mode");
|
|
6046
6159
|
if (text < p) {
|
|
6047
6160
|
LeptrisTextNode* t =
|
|
6048
6161
|
h_text_node(&b, text, p);
|
|
@@ -6191,6 +6304,41 @@ static LeptrisDocument html_parse_shared(
|
|
|
6191
6304
|
text = p;
|
|
6192
6305
|
continue;
|
|
6193
6306
|
}
|
|
6307
|
+
/* Stray end tags with no open stack. The raw-text family
|
|
6308
|
+
* completes during start-tag handling; void elements and
|
|
6309
|
+
* menuitem have no in-body end handler. html5lib's
|
|
6310
|
+
* endTagOther reports by phase: before-html is
|
|
6311
|
+
* end-tag-after-implied-root; AfterBody/AfterFrameset
|
|
6312
|
+
* and AfterAfter* have their own codes; InHead/InBody/
|
|
6313
|
+
* AfterHead report unexpected-end-tag. */
|
|
6314
|
+
if (b.whatwg && nlen && b.depth == 0) {
|
|
6315
|
+
char rn[16];
|
|
6316
|
+
size_t rl = nlen < sizeof(rn) - 1 ? nlen : sizeof(rn) - 1;
|
|
6317
|
+
for (size_t i = 0; i < rl; i++) rn[i] = h_lower(ns[i]);
|
|
6318
|
+
rn[rl] = 0;
|
|
6319
|
+
int stray = h_is_rawtext_name(rn) ||
|
|
6320
|
+
(h_is_void(rn) && strcmp(rn, "br") != 0) ||
|
|
6321
|
+
strcmp(rn, "menuitem") == 0;
|
|
6322
|
+
if (stray) {
|
|
6323
|
+
const char* code = "unexpected-end-tag";
|
|
6324
|
+
if (b.after_html)
|
|
6325
|
+
code = "expected-eof-but-got-end-tag";
|
|
6326
|
+
else if (b.after_body)
|
|
6327
|
+
code = "unexpected-end-tag-after-body";
|
|
6328
|
+
else if (b.frameset && b.after_frameset)
|
|
6329
|
+
code = "unexpected-end-tag-after-frameset";
|
|
6330
|
+
else if (b.frameset)
|
|
6331
|
+
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";
|
|
6335
|
+
h_diag(&b, code, (size_t)(p - b.input_base),
|
|
6336
|
+
"</%s> has no matching open element", rn);
|
|
6337
|
+
p = q;
|
|
6338
|
+
text = p;
|
|
6339
|
+
continue;
|
|
6340
|
+
}
|
|
6341
|
+
}
|
|
6194
6342
|
if (nlen && (b.depth > 0 ||
|
|
6195
6343
|
(b.whatwg &&
|
|
6196
6344
|
((nlen == 4 &&
|
|
@@ -6206,6 +6354,19 @@ static LeptrisDocument html_parse_shared(
|
|
|
6206
6354
|
for (size_t i = 0; i < cl; i++)
|
|
6207
6355
|
lname[i] = h_lower(ns[i]);
|
|
6208
6356
|
lname[cl] = 0;
|
|
6357
|
+
/* InBody endTagOther: a VOID element has no end
|
|
6358
|
+
* handler, so a stray end tag reports
|
|
6359
|
+
* unexpected-end-tag (tests25:15-20: </keygen>).
|
|
6360
|
+
* </br> is endTagBr and acts as <br> above. */
|
|
6361
|
+
if (b.whatwg && h_is_void(lname) &&
|
|
6362
|
+
strcmp(lname, "br") != 0) {
|
|
6363
|
+
h_diag(&b, "unexpected-end-tag",
|
|
6364
|
+
(size_t)(p - b.input_base),
|
|
6365
|
+
"</%s> is a void element", lname);
|
|
6366
|
+
p = q;
|
|
6367
|
+
text = p;
|
|
6368
|
+
continue;
|
|
6369
|
+
}
|
|
6209
6370
|
if (!h_is_void(lname)) {
|
|
6210
6371
|
if (b.whatwg &&
|
|
6211
6372
|
strcmp(lname, "menuitem") == 0) {
|
|
@@ -6274,6 +6435,29 @@ static LeptrisDocument html_parse_shared(
|
|
|
6274
6435
|
const char* hn =
|
|
6275
6436
|
leptris_element_name(b.open[d - 1]);
|
|
6276
6437
|
if (hn && h_is_heading(hn)) {
|
|
6438
|
+
/* html5lib endTagHeading: after the
|
|
6439
|
+
* implied-end pass, a current
|
|
6440
|
+
* heading of a different name makes
|
|
6441
|
+
* the close too early (tests19:24:
|
|
6442
|
+
* </h2> over an open h3), and so
|
|
6443
|
+
* does a non-implied element above
|
|
6444
|
+
* the heading. */
|
|
6445
|
+
int early = strcmp(hn, lname) != 0;
|
|
6446
|
+
for (size_t k = d; k < b.depth; k++) {
|
|
6447
|
+
const char* an2 = leptris_element_name(
|
|
6448
|
+
b.open[k]);
|
|
6449
|
+
if (an2 &&
|
|
6450
|
+
b.open_ns[k] == H_NS_HTML &&
|
|
6451
|
+
!h_is_implied_end_name(an2)) {
|
|
6452
|
+
early = 1;
|
|
6453
|
+
break;
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
if (early)
|
|
6457
|
+
h_diag(&b, "end-tag-too-early",
|
|
6458
|
+
(size_t)(p - b.input_base),
|
|
6459
|
+
"</%s> closes <%s> early",
|
|
6460
|
+
lname, hn);
|
|
6277
6461
|
h_pop_to(&b, d - 1);
|
|
6278
6462
|
/* Old-suite rule (tests1:21/91):
|
|
6279
6463
|
* </table> also closes open
|
|
@@ -6314,7 +6498,23 @@ static LeptrisDocument html_parse_shared(
|
|
|
6314
6498
|
text = p;
|
|
6315
6499
|
continue;
|
|
6316
6500
|
}
|
|
6501
|
+
/* No heading in scope: html5lib still checks
|
|
6502
|
+
* the current node, which is never the token
|
|
6503
|
+
* (tests19:23: </h3> after <p>). */
|
|
6504
|
+
h_diag(&b, "end-tag-too-early",
|
|
6505
|
+
(size_t)(p - b.input_base),
|
|
6506
|
+
"</%s> with no open heading", lname);
|
|
6317
6507
|
}
|
|
6508
|
+
/* InHead/AfterHead endTagOther: a </p> before
|
|
6509
|
+
* body content is dropped with unexpected-end-tag
|
|
6510
|
+
* and inserts nothing (tests19:3). */
|
|
6511
|
+
if (b.whatwg && strcmp(lname, "p") == 0 &&
|
|
6512
|
+
!b.body_seen && (b.head_tag_seen ||
|
|
6513
|
+
b.head_end_seen) &&
|
|
6514
|
+
!b.frameset && !b.after_body && !b.after_html)
|
|
6515
|
+
h_diag(&b, "unexpected-end-tag",
|
|
6516
|
+
(size_t)(p - b.input_base),
|
|
6517
|
+
"</p> outside the body phase");
|
|
6318
6518
|
/* #659 13.2.6.4.17: </p> with no p in button
|
|
6319
6519
|
* scope inserts an EMPTY <p> and closes it
|
|
6320
6520
|
* (tests1:29) — the stray tag still leaves a
|
|
@@ -6342,6 +6542,13 @@ static LeptrisDocument html_parse_shared(
|
|
|
6342
6542
|
break;
|
|
6343
6543
|
}
|
|
6344
6544
|
if (!p_open) {
|
|
6545
|
+
/* InBody endTagP: no p in button scope
|
|
6546
|
+
* is a parse error even though the
|
|
6547
|
+
* empty p is still inserted
|
|
6548
|
+
* (tests2:60). */
|
|
6549
|
+
h_diag(&b, "unexpected-end-tag",
|
|
6550
|
+
(size_t)(p - b.input_base),
|
|
6551
|
+
"</p> with no p in scope");
|
|
6345
6552
|
LeptrisElement pe =
|
|
6346
6553
|
h_open_element(&b, "p");
|
|
6347
6554
|
(void)pe;
|
|
@@ -6623,7 +6830,14 @@ static LeptrisDocument html_parse_shared(
|
|
|
6623
6830
|
strcmp(lname, "dd") == 0 ||
|
|
6624
6831
|
strcmp(lname, "dt") == 0) &&
|
|
6625
6832
|
(h_ieq_raw(on, "ul") ||
|
|
6626
|
-
h_ieq_raw(on, "ol")))
|
|
6833
|
+
h_ieq_raw(on, "ol"))) ||
|
|
6834
|
+
/* html5lib endTagOther: a
|
|
6835
|
+
* raw-text name has no
|
|
6836
|
+
* in-body handler, so the
|
|
6837
|
+
* walk errors at the first
|
|
6838
|
+
* special node. */
|
|
6839
|
+
(h_is_rawtext_name(lname) &&
|
|
6840
|
+
h_is_special_ww(on));
|
|
6627
6841
|
}
|
|
6628
6842
|
if (fenced) {
|
|
6629
6843
|
/* #1771: html5lib's walk reports
|
|
@@ -6677,6 +6891,27 @@ static LeptrisDocument html_parse_shared(
|
|
|
6677
6891
|
(int)(d - 1) <= ti)
|
|
6678
6892
|
break;
|
|
6679
6893
|
}
|
|
6894
|
+
/* html5lib endTagBlock / endTagListItem /
|
|
6895
|
+
* endTagAppletMarqueeObject: after the
|
|
6896
|
+
* implied-end pass, a non-implied element
|
|
6897
|
+
* above the target makes the close too
|
|
6898
|
+
* early (tests3:11: </pre> over <div>). */
|
|
6899
|
+
if (b.whatwg && b.open_ns[d - 1] == H_NS_HTML &&
|
|
6900
|
+
h_is_tooearly_target(lname)) {
|
|
6901
|
+
for (size_t k = d; k < b.depth; k++) {
|
|
6902
|
+
const char* an2 =
|
|
6903
|
+
leptris_element_name(b.open[k]);
|
|
6904
|
+
if (an2 &&
|
|
6905
|
+
b.open_ns[k] == H_NS_HTML &&
|
|
6906
|
+
!h_is_implied_end_name(an2)) {
|
|
6907
|
+
h_diag(&b, "end-tag-too-early",
|
|
6908
|
+
(size_t)(p - b.input_base),
|
|
6909
|
+
"</%s> closes over <%s>",
|
|
6910
|
+
lname, an2);
|
|
6911
|
+
break;
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6680
6915
|
h_pop_to(&b, d - 1);
|
|
6681
6916
|
/* Old-suite </table> rule: closes
|
|
6682
6917
|
* formatting BELOW the table - only
|
|
@@ -6977,6 +7212,47 @@ static LeptrisDocument html_parse_shared(
|
|
|
6977
7212
|
while (q < end && !h_is_ws(*q) && *q != '>' && *q != '/') q++;
|
|
6978
7213
|
size_t nlen = (size_t)(q - ns);
|
|
6979
7214
|
char* name = h_pooled_lower(b.pool, ns, nlen);
|
|
7215
|
+
if (name && b.whatwg && strcmp(name, "html") != 0) {
|
|
7216
|
+
if (b.after_html &&
|
|
7217
|
+
!(b.frameset && strcmp(name, "noframes") == 0)) {
|
|
7218
|
+
/* AfterAfterBody / AfterAfterFrameset startTagOther
|
|
7219
|
+
* (only html — and noframes in the frameset flavor —
|
|
7220
|
+
* is accepted). */
|
|
7221
|
+
h_diag(&b, "expected-eof-but-got-start-tag",
|
|
7222
|
+
(size_t)(p - b.input_base),
|
|
7223
|
+
"<%s> after </html>", name);
|
|
7224
|
+
} else if (b.after_body && !b.after_body_done &&
|
|
7225
|
+
!b.frameset && !b.after_body_diag_done) {
|
|
7226
|
+
/* AfterBody startTagOther: the first non-html start
|
|
7227
|
+
* tag after </body> errors and moves the phase back
|
|
7228
|
+
* to in-body (tests15:4/6, tests9:21, tests10:20).
|
|
7229
|
+
* Bookkeeping is diagnostic-only: after_body_done
|
|
7230
|
+
* also gates comment placement. */
|
|
7231
|
+
h_diag(&b, "unexpected-start-tag-after-body",
|
|
7232
|
+
(size_t)(p - b.input_base),
|
|
7233
|
+
"<%s> after </body>", name);
|
|
7234
|
+
b.after_body_diag_done = 1;
|
|
7235
|
+
}
|
|
7236
|
+
/* AfterHead startTagFromHead: a head-only start tag in
|
|
7237
|
+
* the window after </head> and before body content is
|
|
7238
|
+
* processed in head but reported out-of-my-head
|
|
7239
|
+
* (tests7:3/4). */
|
|
7240
|
+
if (b.head_end_seen && !b.body_seen && !b.body_tag_seen &&
|
|
7241
|
+
!b.frameset && !b.after_body &&
|
|
7242
|
+
(strcmp(name, "base") == 0 ||
|
|
7243
|
+
strcmp(name, "basefont") == 0 ||
|
|
7244
|
+
strcmp(name, "bgsound") == 0 ||
|
|
7245
|
+
strcmp(name, "link") == 0 ||
|
|
7246
|
+
strcmp(name, "meta") == 0 ||
|
|
7247
|
+
strcmp(name, "noframes") == 0 ||
|
|
7248
|
+
strcmp(name, "script") == 0 ||
|
|
7249
|
+
strcmp(name, "style") == 0 ||
|
|
7250
|
+
strcmp(name, "title") == 0)) {
|
|
7251
|
+
h_diag(&b, "unexpected-start-tag-out-of-my-head",
|
|
7252
|
+
(size_t)(p - b.input_base),
|
|
7253
|
+
"<%s> after </head>", name);
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
6980
7256
|
if (!name) goto done;
|
|
6981
7257
|
/* #659 (13.2.6.4.7): <image> is renamed <img> and
|
|
6982
7258
|
* reprocessed (tests1:90). */
|
|
@@ -7070,6 +7346,13 @@ static LeptrisDocument html_parse_shared(
|
|
|
7070
7346
|
if (t) h_append(&b, (LeptrisNodeRef)t);
|
|
7071
7347
|
}
|
|
7072
7348
|
if (strcmp(name, "body") == 0) {
|
|
7349
|
+
/* InBody startTagBody always reports
|
|
7350
|
+
* unexpected-start-tag for a second <body>
|
|
7351
|
+
* (tests2:16); the first one is the phase entry. */
|
|
7352
|
+
if (b.whatwg && (b.body_tag_seen || b.body_seen))
|
|
7353
|
+
h_diag(&b, "unexpected-start-tag",
|
|
7354
|
+
(size_t)(p - b.input_base),
|
|
7355
|
+
"<body> after the body exists");
|
|
7073
7356
|
b.body_tag_seen = 1;
|
|
7074
7357
|
if (b.whatwg) b.frameset_ok = 0;
|
|
7075
7358
|
/* Only the FIRST structural <body> marks the
|
|
@@ -7081,6 +7364,15 @@ static LeptrisDocument html_parse_shared(
|
|
|
7081
7364
|
b.lift_boundary = b.top_tail;
|
|
7082
7365
|
}
|
|
7083
7366
|
} else {
|
|
7367
|
+
/* InBody/AfterHead startTagHead: any <head> after
|
|
7368
|
+
* the head phase started is an unexpected start tag
|
|
7369
|
+
* (tests6:1: </head> <head>). */
|
|
7370
|
+
if (b.whatwg &&
|
|
7371
|
+
(b.head_tag_seen || b.head_end_seen ||
|
|
7372
|
+
b.body_tag_seen || b.body_seen))
|
|
7373
|
+
h_diag(&b, "unexpected-start-tag",
|
|
7374
|
+
(size_t)(p - b.input_base),
|
|
7375
|
+
"<head> after the head phase");
|
|
7084
7376
|
/* Before-head ends here: later comments are head
|
|
7085
7377
|
* content; earlier ones stay html-prefix children. */
|
|
7086
7378
|
b.head_tag_seen = 1;
|
|
@@ -7109,18 +7401,18 @@ static LeptrisDocument html_parse_shared(
|
|
|
7109
7401
|
/* frameset is NOT in the 13.2.6.5 breakout list: inside
|
|
7110
7402
|
* foreign content it is a plain foreign element
|
|
7111
7403
|
* (plain-text-unsafe:16-23). */
|
|
7112
|
-
/* #1771: html5lib's
|
|
7113
|
-
*
|
|
7114
|
-
*
|
|
7115
|
-
* ignored
|
|
7116
|
-
*
|
|
7117
|
-
*
|
|
7118
|
-
*
|
|
7119
|
-
if (b.
|
|
7404
|
+
/* #1771: html5lib's InBody startTagFrameset always
|
|
7405
|
+
* reports unexpected-start-tag — the frameset either
|
|
7406
|
+
* converts (body element absent, frameset-ok set) or is
|
|
7407
|
+
* ignored. frameset_ok==0 marks the InBody phase: any
|
|
7408
|
+
* body tag/content (or a frameset-ok-clearing start)
|
|
7409
|
+
* has already been seen. AfterHead converters keep
|
|
7410
|
+
* frameset_ok==1 and stay clean (webkit01:51). */
|
|
7411
|
+
if (!b.frameset_ok && !b.frameset &&
|
|
7120
7412
|
h_template_idx(&b) < 0)
|
|
7121
7413
|
h_diag(&b, "unexpected-start-tag",
|
|
7122
7414
|
(size_t)(p - b.input_base),
|
|
7123
|
-
"frameset
|
|
7415
|
+
"frameset in the body phase");
|
|
7124
7416
|
/* depth<=1: at most the (explicit or synthesized) html
|
|
7125
7417
|
* element is open; frameset-ok is the spec gate and the
|
|
7126
7418
|
* body is removed wholesale when it converts. */
|
|
@@ -7195,6 +7487,20 @@ static LeptrisDocument html_parse_shared(
|
|
|
7195
7487
|
strcmp(name, "frame") != 0 &&
|
|
7196
7488
|
strcmp(name, "noframes") != 0 &&
|
|
7197
7489
|
strcmp(name, "html") != 0) {
|
|
7490
|
+
/* html5lib phase verdicts for the dropped token (the
|
|
7491
|
+
* after-</html> flavor is emitted at the start-tag
|
|
7492
|
+
* dispatch): AfterFrameset names its own code; inside an
|
|
7493
|
+
* open frameset it is InFrameset's. */
|
|
7494
|
+
if (!b.after_html) {
|
|
7495
|
+
if (b.after_frameset)
|
|
7496
|
+
h_diag(&b, "unexpected-start-tag-after-frameset",
|
|
7497
|
+
(size_t)(p - b.input_base),
|
|
7498
|
+
"<%s> after </frameset>", name);
|
|
7499
|
+
else
|
|
7500
|
+
h_diag(&b, "unexpected-start-tag-in-frameset",
|
|
7501
|
+
(size_t)(p - b.input_base),
|
|
7502
|
+
"<%s> inside <frameset>", name);
|
|
7503
|
+
}
|
|
7198
7504
|
if (text < p) {
|
|
7199
7505
|
LeptrisTextNode* t = h_text_node(&b, text, p);
|
|
7200
7506
|
if (t) {
|
|
@@ -7329,6 +7635,12 @@ static LeptrisDocument html_parse_shared(
|
|
|
7329
7635
|
(strcmp(name, "input") == 0 ||
|
|
7330
7636
|
strcmp(name, "keygen") == 0 ||
|
|
7331
7637
|
strcmp(name, "textarea") == 0)) {
|
|
7638
|
+
/* InSelect startTagInput (shared by input/keygen/
|
|
7639
|
+
* textarea) reports before popping the select
|
|
7640
|
+
* (tests7:14). */
|
|
7641
|
+
h_diag(&b, "unexpected-input-in-select",
|
|
7642
|
+
(size_t)(p - b.input_base),
|
|
7643
|
+
"<%s> inside <select>", name);
|
|
7332
7644
|
for (size_t d2 = b.depth; d2 > 0; d2--)
|
|
7333
7645
|
if (strcmp(leptris_element_name(b.open[d2 - 1]),
|
|
7334
7646
|
"select") == 0) {
|
|
@@ -7383,6 +7695,17 @@ static LeptrisDocument html_parse_shared(
|
|
|
7383
7695
|
continue;
|
|
7384
7696
|
}
|
|
7385
7697
|
|
|
7698
|
+
/* html5lib startTagHtml: an <html> token processed once the
|
|
7699
|
+
* html/head/body phases exist is not the first start tag —
|
|
7700
|
+
* parse error non-html-root, then the attributes merge onto
|
|
7701
|
+
* the root (tests14:3/4/6, tests19:37/38/88, tests2:52-54). */
|
|
7702
|
+
if (b.whatwg && strcmp(name, "html") == 0 &&
|
|
7703
|
+
(b.html_seen || b.head_tag_seen || b.body_tag_seen ||
|
|
7704
|
+
b.after_body || b.body_seen)) {
|
|
7705
|
+
h_diag(&b, "non-html-root",
|
|
7706
|
+
(size_t)(p - b.input_base),
|
|
7707
|
+
"<html> after the root element exists");
|
|
7708
|
+
}
|
|
7386
7709
|
/* 13.2.6.3: an <html> start tag inside content (never
|
|
7387
7710
|
* seen yet, deep in a table/foreign subtree) merges attrs
|
|
7388
7711
|
* and is ignored - no element (domjs-unsafe:36:
|
|
@@ -7460,6 +7783,13 @@ static LeptrisDocument html_parse_shared(
|
|
|
7460
7783
|
}
|
|
7461
7784
|
}
|
|
7462
7785
|
if (!tbl_ctx) {
|
|
7786
|
+
/* InBody startTagMisplaced: caption/col/colgroup/
|
|
7787
|
+
* head/tbody/td/tfoot/th/thead/tr in body is a
|
|
7788
|
+
* parse error and the token is ignored
|
|
7789
|
+
* (tests25:7 <body><col>A). */
|
|
7790
|
+
h_diag(&b, "unexpected-start-tag-ignored",
|
|
7791
|
+
(size_t)(p - b.input_base),
|
|
7792
|
+
"<%s> ignored in the body phase", name);
|
|
7463
7793
|
/* The TOKEN is ignored - surrounding character
|
|
7464
7794
|
* tokens keep flowing into body (the html-merge
|
|
7465
7795
|
* flush pattern). */
|
|
@@ -7478,6 +7808,20 @@ static LeptrisDocument html_parse_shared(
|
|
|
7478
7808
|
* an ignored <frameset> vanishes). */
|
|
7479
7809
|
if (b.whatwg && strcmp(name, "frame") == 0 &&
|
|
7480
7810
|
(!b.frameset || b.after_frameset)) {
|
|
7811
|
+
/* InBody reports the frame as a misplaced start tag
|
|
7812
|
+
* (tests25:10); the after-</frameset> phase names its
|
|
7813
|
+
* own code (the after-</html> phase is reported at the
|
|
7814
|
+
* start-tag dispatch). */
|
|
7815
|
+
if (!b.after_html) {
|
|
7816
|
+
if (!b.frameset)
|
|
7817
|
+
h_diag(&b, "unexpected-start-tag-ignored",
|
|
7818
|
+
(size_t)(p - b.input_base),
|
|
7819
|
+
"<frame> ignored in the body phase");
|
|
7820
|
+
else
|
|
7821
|
+
h_diag(&b, "unexpected-start-tag-after-frameset",
|
|
7822
|
+
(size_t)(p - b.input_base),
|
|
7823
|
+
"<frame> after </frameset>");
|
|
7824
|
+
}
|
|
7481
7825
|
while (q < end && *q != '>') q++;
|
|
7482
7826
|
p = (q < end) ? q + 1 : end;
|
|
7483
7827
|
text = p;
|
|
@@ -7502,7 +7846,13 @@ static LeptrisDocument html_parse_shared(
|
|
|
7502
7846
|
if (h_is_breakout(name) ||
|
|
7503
7847
|
(strcmp(name, "font") == 0 && h_font_break(q, end))) {
|
|
7504
7848
|
/* Breakout: pop the foreign scope, reprocess
|
|
7505
|
-
* under HTML rules below.
|
|
7849
|
+
* under HTML rules below. html5lib's foreign
|
|
7850
|
+
* startTagOther reports the HTML element as
|
|
7851
|
+
* unexpected in foreign content
|
|
7852
|
+
* (tests9:15, tests10:14). */
|
|
7853
|
+
h_diag(&b, "unexpected-html-element-in-foreign-content",
|
|
7854
|
+
(size_t)(p - b.input_base),
|
|
7855
|
+
"<%s> breaks out of foreign content", name);
|
|
7506
7856
|
while (b.depth > 0 &&
|
|
7507
7857
|
b.open_ns[b.depth - 1] != H_NS_HTML &&
|
|
7508
7858
|
!h_is_int_point(&b, b.depth - 1))
|
|
@@ -7520,6 +7870,12 @@ static LeptrisDocument html_parse_shared(
|
|
|
7520
7870
|
/* Implied end tags this start tag triggers (HTML rules
|
|
7521
7871
|
* only — foreign content has none). */
|
|
7522
7872
|
if (elem_ns == H_NS_HTML) {
|
|
7873
|
+
/* html5lib startTagListItem/startTagBlock: an implied
|
|
7874
|
+
* close whose target is buried under a non-implied
|
|
7875
|
+
* element (div, article, ...) reports end-tag-too-early
|
|
7876
|
+
* on this START tag (tests2:11, tests3:20). One per
|
|
7877
|
+
* token, like the reference walk. */
|
|
7878
|
+
const char* implied_early = NULL;
|
|
7523
7879
|
/* WHATWG: a dd/dt start closes the NEAREST open dd/dt
|
|
7524
7880
|
* in default scope — sibling definition items even
|
|
7525
7881
|
* with content in between (13.2.6.4.7, tests19:30:
|
|
@@ -7529,6 +7885,15 @@ static LeptrisDocument html_parse_shared(
|
|
|
7529
7885
|
for (size_t d = b.depth; d > 0; d--) {
|
|
7530
7886
|
uint8_t oid = b.open_id[d - 1];
|
|
7531
7887
|
if (oid == h_id_dd || oid == h_id_dt) {
|
|
7888
|
+
for (size_t k = d; k < b.depth; k++) {
|
|
7889
|
+
const char* an2 =
|
|
7890
|
+
leptris_element_name(b.open[k]);
|
|
7891
|
+
if (an2 && b.open_ns[k] == H_NS_HTML &&
|
|
7892
|
+
!h_is_implied_end_name(an2)) {
|
|
7893
|
+
implied_early = an2;
|
|
7894
|
+
break;
|
|
7895
|
+
}
|
|
7896
|
+
}
|
|
7532
7897
|
b.depth = d - 1;
|
|
7533
7898
|
break;
|
|
7534
7899
|
}
|
|
@@ -7550,6 +7915,17 @@ static LeptrisDocument html_parse_shared(
|
|
|
7550
7915
|
if ((tid == h_id_li && oid == h_id_li) ||
|
|
7551
7916
|
(tid != h_id_li &&
|
|
7552
7917
|
(oid == h_id_dd || oid == h_id_dt))) {
|
|
7918
|
+
if (!implied_early) {
|
|
7919
|
+
for (size_t k = d; k < b.depth; k++) {
|
|
7920
|
+
const char* an2 =
|
|
7921
|
+
leptris_element_name(b.open[k]);
|
|
7922
|
+
if (an2 && b.open_ns[k] == H_NS_HTML &&
|
|
7923
|
+
!h_is_implied_end_name(an2)) {
|
|
7924
|
+
implied_early = an2;
|
|
7925
|
+
break;
|
|
7926
|
+
}
|
|
7927
|
+
}
|
|
7928
|
+
}
|
|
7553
7929
|
b.depth = d - 1;
|
|
7554
7930
|
break;
|
|
7555
7931
|
}
|
|
@@ -7570,6 +7946,17 @@ static LeptrisDocument html_parse_shared(
|
|
|
7570
7946
|
for (size_t d = b.depth; d > 0; d--) {
|
|
7571
7947
|
uint8_t oid = b.open_id[d - 1];
|
|
7572
7948
|
if (oid == h_id_p) {
|
|
7949
|
+
if (!implied_early) {
|
|
7950
|
+
for (size_t k = d; k < b.depth; k++) {
|
|
7951
|
+
const char* an2 =
|
|
7952
|
+
leptris_element_name(b.open[k]);
|
|
7953
|
+
if (an2 && b.open_ns[k] == H_NS_HTML &&
|
|
7954
|
+
!h_is_implied_end_name(an2)) {
|
|
7955
|
+
implied_early = an2;
|
|
7956
|
+
break;
|
|
7957
|
+
}
|
|
7958
|
+
}
|
|
7959
|
+
}
|
|
7573
7960
|
b.depth = d - 1;
|
|
7574
7961
|
break;
|
|
7575
7962
|
}
|
|
@@ -7578,6 +7965,10 @@ static LeptrisDocument html_parse_shared(
|
|
|
7578
7965
|
break;
|
|
7579
7966
|
}
|
|
7580
7967
|
}
|
|
7968
|
+
if (b.whatwg && implied_early)
|
|
7969
|
+
h_diag(&b, "end-tag-too-early",
|
|
7970
|
+
(size_t)(p - b.input_base),
|
|
7971
|
+
"implied close crosses <%s>", implied_early);
|
|
7581
7972
|
uint8_t tmpl_last_popped = H_ID_UNKNOWN;
|
|
7582
7973
|
while (b.depth > 0) {
|
|
7583
7974
|
uint8_t oid = b.open_id[b.depth - 1];
|
|
@@ -8250,6 +8641,13 @@ static LeptrisDocument html_parse_shared(
|
|
|
8250
8641
|
"input",
|
|
8251
8642
|
name);
|
|
8252
8643
|
}
|
|
8644
|
+
/* PLAINTEXT has no close form: it is still open at
|
|
8645
|
+
* EOF, so InBody processEOF reports the unclosed
|
|
8646
|
+
* element (tests18:2-7, tests19:6). */
|
|
8647
|
+
if (b.whatwg && cq >= end && strcmp(name, "plaintext") == 0)
|
|
8648
|
+
h_diag(&b, "expected-closing-tag-but-got-eof",
|
|
8649
|
+
(size_t)(end - b.input_base),
|
|
8650
|
+
"plaintext reaches end of input");
|
|
8253
8651
|
b.depth--; /* the raw element is complete */
|
|
8254
8652
|
text = p;
|
|
8255
8653
|
continue;
|
|
@@ -203,10 +203,12 @@ int buffer_has_error(SerializeBuffer* buf) {
|
|
|
203
203
|
|
|
204
204
|
|
|
205
205
|
/* Inline escaped-text emitter (TODO 194d): writes into a caller-
|
|
206
|
-
* reserved worst-case buffer (6 bytes per input byte)
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
206
|
+
* reserved worst-case buffer (6 bytes per input byte). Every '&' is
|
|
207
|
+
* escaped — entity references are parse-time markup and either
|
|
208
|
+
* already decoded into the text (predefined/char refs) or carried as
|
|
209
|
+
* ENTITY_REF nodes; a decoded text value shaped like "&" is
|
|
210
|
+
* data, not markup (leptris-ruby#515). Quotes are ordinary in text
|
|
211
|
+
* mode and escaped in attribute mode. */
|
|
210
212
|
/* Escape-class table (serialize endgame): the run scan was 3-4
|
|
211
213
|
* compares per byte — the dominant serializer cost at 57% of the
|
|
212
214
|
* text-heavy profile. One indexed load + one test per byte, the
|
|
@@ -300,23 +302,9 @@ static char* emit_escaped_inline(char* out, const char* content,
|
|
|
300
302
|
}
|
|
301
303
|
|
|
302
304
|
if (content[i] == '&') {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (content[j] == ';') { found_semicolon = 1; break; }
|
|
307
|
-
if (!isalnum((unsigned char)content[j]) && content[j] != '#' && content[j] != '-') break;
|
|
308
|
-
j++;
|
|
309
|
-
}
|
|
310
|
-
if (!attr_mode && found_semicolon && j > i + 1) {
|
|
311
|
-
size_t n = j - i + 1;
|
|
312
|
-
memcpy(out, &content[i], n);
|
|
313
|
-
out += n;
|
|
314
|
-
i = j + 1;
|
|
315
|
-
} else {
|
|
316
|
-
memcpy(out, "&", 5);
|
|
317
|
-
out += 5;
|
|
318
|
-
i++;
|
|
319
|
-
}
|
|
305
|
+
memcpy(out, "&", 5);
|
|
306
|
+
out += 5;
|
|
307
|
+
i++;
|
|
320
308
|
continue;
|
|
321
309
|
}
|
|
322
310
|
if (content[i] == '<') { memcpy(out, "<", 4); out += 4; }
|
|
@@ -716,8 +704,8 @@ void serialize_text_internal(LeptrisTextNode* text, SerializeBuffer* buf) {
|
|
|
716
704
|
}
|
|
717
705
|
|
|
718
706
|
/* Run-batched (TODO 194b): bulk-append ordinary bytes up to the
|
|
719
|
-
* next special character
|
|
720
|
-
*
|
|
707
|
+
* next special character. Quotes and apostrophes are ordinary in
|
|
708
|
+
* text content. */
|
|
721
709
|
size_t i = 0;
|
|
722
710
|
while (i < content_len) {
|
|
723
711
|
size_t run = i;
|
|
@@ -733,26 +721,10 @@ void serialize_text_internal(LeptrisTextNode* text, SerializeBuffer* buf) {
|
|
|
733
721
|
}
|
|
734
722
|
|
|
735
723
|
if (content[i] == '&') {
|
|
736
|
-
/*
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if (content[j] == ';') {
|
|
741
|
-
found_semicolon = 1;
|
|
742
|
-
break;
|
|
743
|
-
}
|
|
744
|
-
if (!isalnum((unsigned char)content[j]) && content[j] != '#' && content[j] != '-') {
|
|
745
|
-
break;
|
|
746
|
-
}
|
|
747
|
-
j++;
|
|
748
|
-
}
|
|
749
|
-
if (found_semicolon && j > i + 1) {
|
|
750
|
-
buffer_append_len(buf, &content[i], j - i + 1);
|
|
751
|
-
i = j + 1;
|
|
752
|
-
} else {
|
|
753
|
-
buffer_append_len(buf, "&", 5);
|
|
754
|
-
i++;
|
|
755
|
-
}
|
|
724
|
+
/* Always escape: decoded text is data. Named references
|
|
725
|
+
* that survived parsing live in ENTITY_REF nodes. */
|
|
726
|
+
buffer_append_len(buf, "&", 5);
|
|
727
|
+
i++;
|
|
756
728
|
continue;
|
|
757
729
|
}
|
|
758
730
|
|
|
@@ -977,9 +977,27 @@ TEST(Html5LibCorpus, ErrorVerdictParity) {
|
|
|
977
977
|
for (size_t i = 0; i < failures.size() && i < 20; i++)
|
|
978
978
|
printf(" VERDICT %s\n", failures[i].c_str());
|
|
979
979
|
EXPECT_GT(total, (size_t)1500);
|
|
980
|
-
/* Verdict-parity floor —
|
|
981
|
-
* (1409
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
|
|
980
|
+
/* Verdict-parity floor — main measured 1446/1555 before these
|
|
981
|
+
* slices (1409 was the slice-2 baseline; was 190 before the
|
|
982
|
+
* #1771 emission slices). Raw-text stray end tags add 12 depth-0
|
|
983
|
+
* cases; non-html-root adds the 9 later-<html> cases; the
|
|
984
|
+
* end-tag-too-early slices add heading name mismatches, block
|
|
985
|
+
* closes over non-implied elements, and the same rule on the
|
|
986
|
+
* implied close of li/dd/dt/p start tags; the after-body and
|
|
987
|
+
* frameset phase slices add the AfterBody/AfterFrameset/
|
|
988
|
+
* AfterAfter* start-tag codes; the start-tag phase slice adds
|
|
989
|
+
* InBody frameset, duplicate body/head, and AfterHead
|
|
990
|
+
* out-of-my-head tags; the select/table/foreign slice adds
|
|
991
|
+
* unexpected-input-in-select, hidden-input-in-table, and
|
|
992
|
+
* unexpected-html-element-in-foreign-content; the tokenizer
|
|
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. Only raises. */
|
|
1002
|
+
EXPECT_GE(parity, (size_t)1532);
|
|
985
1003
|
}
|
|
@@ -257,6 +257,93 @@ TEST(EncodingEdgeCases, EmptyXmlElementRoundTrips) {
|
|
|
257
257
|
leptris_document_free(doc);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/* leptris-ruby#514: strict load validates the input against the
|
|
261
|
+
* effective encoding. Without a declaration (or with a UTF-8 one)
|
|
262
|
+
* the entity is UTF-8 in XML 1.0; invalid byte sequences are a
|
|
263
|
+
* fatal load error, matching libxml2's "Invalid bytes in character
|
|
264
|
+
* encoding". A declared non-UTF-8 encoding is exempt — those bytes
|
|
265
|
+
* are legitimately not UTF-8. */
|
|
266
|
+
TEST(EncodingUtf8Strict, RejectsInvalidBytesWithoutDeclaration) {
|
|
267
|
+
const char bad[] = "<r>\xC3\x28</r>";
|
|
268
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
269
|
+
LeptrisDocument doc = leptris_parse_string(bad, sizeof(bad) - 1, &st);
|
|
270
|
+
EXPECT_EQ(doc, nullptr);
|
|
271
|
+
EXPECT_EQ(st, LEPTRIS_ERROR_PARSE);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
TEST(EncodingUtf8Strict, RejectsInvalidBytesWithUtf8Declaration) {
|
|
275
|
+
const char bad[] =
|
|
276
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><r>\xC3\x28</r>";
|
|
277
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
278
|
+
LeptrisDocument doc = leptris_parse_string(bad, sizeof(bad) - 1, &st);
|
|
279
|
+
EXPECT_EQ(doc, nullptr);
|
|
280
|
+
EXPECT_EQ(st, LEPTRIS_ERROR_PARSE);
|
|
281
|
+
|
|
282
|
+
const char bad_lower[] =
|
|
283
|
+
"<?xml version='1.0' encoding='utf-8'?><r>\xC3\x28</r>";
|
|
284
|
+
LeptrisDocument doc2 = leptris_parse_string(
|
|
285
|
+
bad_lower, sizeof(bad_lower) - 1, &st);
|
|
286
|
+
EXPECT_EQ(doc2, nullptr);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
TEST(EncodingUtf8Strict, RejectsEveryInvalidSequenceClass) {
|
|
290
|
+
struct { const char* xml; size_t len; } cases[] = {
|
|
291
|
+
{"<r>\xC0\x80</r>", 0}, /* overlong NUL */
|
|
292
|
+
{"<r>\xED\xA0\x80</r>", 0}, /* surrogate U+D800 */
|
|
293
|
+
{"<r>\xF5\x80\x80\x80</r>", 0}, /* > U+10FFFF */
|
|
294
|
+
{"<r>\x80</r>", 0}, /* lone continuation */
|
|
295
|
+
{"<r>abc\xC3", 0}, /* truncated at EOF */
|
|
296
|
+
{"<r a='\xC3\x28'/>", 0}, /* invalid in attribute */
|
|
297
|
+
};
|
|
298
|
+
for (auto& c : cases) {
|
|
299
|
+
size_t len = c.len ? c.len : std::strlen(c.xml);
|
|
300
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
301
|
+
LeptrisDocument doc = leptris_parse_string(c.xml, len, &st);
|
|
302
|
+
EXPECT_EQ(doc, nullptr) << "accepted invalid bytes";
|
|
303
|
+
if (doc) leptris_document_free(doc);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
TEST(EncodingUtf8Strict, AcceptsValidMultibyte) {
|
|
308
|
+
const char xml[] = "<r>caf\xC3\xA9 \xE4\xB8\xAD \xF0\x9F\x98\x80</r>";
|
|
309
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
310
|
+
LeptrisDocument doc = leptris_parse_string(xml, std::strlen(xml), &st);
|
|
311
|
+
ASSERT_NE(doc, nullptr);
|
|
312
|
+
EXPECT_STREQ(leptris_element_text(leptris_document_root(doc)),
|
|
313
|
+
"caf\xC3\xA9 \xE4\xB8\xAD \xF0\x9F\x98\x80");
|
|
314
|
+
leptris_document_free(doc);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
TEST(EncodingUtf8Strict, DeclaredNonUtf8IsExempt) {
|
|
318
|
+
/* ISO-8859-1 0xE9 is not valid UTF-8 but is valid in the
|
|
319
|
+
* declared encoding: the load must not reject it. Depending on
|
|
320
|
+
* the iconv build it either transcodes to UTF-8 ("café") or
|
|
321
|
+
* passes the raw declared byte through — both are documented
|
|
322
|
+
* shapes (#514); only the bogus UTF-8 rejection would fail. */
|
|
323
|
+
const char xml[] =
|
|
324
|
+
"<?xml version='1.0' encoding='ISO-8859-1'?><r>caf\xE9</r>";
|
|
325
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
326
|
+
LeptrisDocument doc = leptris_parse_string(xml, sizeof(xml) - 1, &st);
|
|
327
|
+
ASSERT_NE(doc, nullptr);
|
|
328
|
+
const char* text = leptris_element_text(leptris_document_root(doc));
|
|
329
|
+
ASSERT_NE(text, nullptr);
|
|
330
|
+
EXPECT_TRUE(std::string(text) == "caf\xC3\xA9" ||
|
|
331
|
+
std::string(text) == "caf\xE9")
|
|
332
|
+
<< "unexpected content byte(s)";
|
|
333
|
+
leptris_document_free(doc);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
TEST(EncodingUtf8Strict, RecoverLaneKeepsBestEffortTree) {
|
|
337
|
+
/* #1660: RECOVER never returns NULL for malformed input; the
|
|
338
|
+
* encoding check must not break that contract. */
|
|
339
|
+
const char bad[] = "<r>\xC3\x28</r>";
|
|
340
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
341
|
+
LeptrisDocument doc = leptris_parse_string_flags(
|
|
342
|
+
bad, sizeof(bad) - 1, LEPTRIS_PARSE_RECOVER, &st);
|
|
343
|
+
ASSERT_NE(doc, nullptr);
|
|
344
|
+
leptris_document_free(doc);
|
|
345
|
+
}
|
|
346
|
+
|
|
260
347
|
} // namespace
|
|
261
348
|
|
|
262
349
|
/* Issue #613: with_encoding on declared ISO-8859-1 must deliver
|
|
@@ -39,6 +39,71 @@ TEST(SerializeRoundTrip, EscapesBareAmpersandInText) {
|
|
|
39
39
|
leptris_document_free(doc);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
TEST(SerializeRoundTrip, EscapesEntityShapedTextUnconditionally) {
|
|
43
|
+
// leptris-ruby#515: parse decodes entities into text; a decoded
|
|
44
|
+
// text value that LOOKS like an entity reference ("&",
|
|
45
|
+
// "&", "<UK>") must still escape its '&' on output.
|
|
46
|
+
// The old serializer passed entity-shaped runs through verbatim,
|
|
47
|
+
// so the next parse produced a different value (A -> A,
|
|
48
|
+
// <UK> -> a literal <UK> start tag).
|
|
49
|
+
struct { const char* xml; const char* want; const char* want_text; } cases[] = {
|
|
50
|
+
{"<r>a &#x26; b</r>", "<r>a &#x26; b</r>", "a & b"},
|
|
51
|
+
{"<r>a &amp; b</r>", "<r>a &amp; b</r>", "a & b"},
|
|
52
|
+
{"<r>a &lt;UK&gt; b</r>", "<r>a &lt;UK&gt; b</r>",
|
|
53
|
+
"a <UK> b"},
|
|
54
|
+
{"<r>a &#x41; b</r>", "<r>a &#x41; b</r>", "a A b"},
|
|
55
|
+
// Attribute forces the non-fused text emitter; the first four
|
|
56
|
+
// cases take the fused <name>text</name> fast path.
|
|
57
|
+
{"<r k='v'>a &#x26; b</r>", "<r k=\"v\">a &#x26; b</r>",
|
|
58
|
+
"a & b"},
|
|
59
|
+
};
|
|
60
|
+
for (auto& c : cases) {
|
|
61
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
62
|
+
LeptrisDocument doc = leptris_parse_string(c.xml, std::strlen(c.xml), &st);
|
|
63
|
+
ASSERT_NE(doc, nullptr) << c.xml;
|
|
64
|
+
char* serialized = leptris_document_serialize(doc, nullptr);
|
|
65
|
+
ASSERT_NE(serialized, nullptr) << c.xml;
|
|
66
|
+
EXPECT_STREQ(serialized, c.want) << c.xml;
|
|
67
|
+
|
|
68
|
+
// The emitted bytes must reparse to the same string value.
|
|
69
|
+
LeptrisDocument again = leptris_parse_string(
|
|
70
|
+
serialized, std::strlen(serialized), &st);
|
|
71
|
+
ASSERT_NE(again, nullptr) << serialized;
|
|
72
|
+
LeptrisElement root = leptris_document_root(again);
|
|
73
|
+
ASSERT_NE(root, nullptr) << serialized;
|
|
74
|
+
const char* content = leptris_element_text(root);
|
|
75
|
+
ASSERT_NE(content, nullptr) << serialized;
|
|
76
|
+
EXPECT_STREQ(content, c.want_text) << c.xml;
|
|
77
|
+
if (std::string(c.xml).find("UK") != std::string::npos) {
|
|
78
|
+
// No <UK> element may have been minted.
|
|
79
|
+
EXPECT_EQ(leptris_element_first_child(root, "UK"), nullptr) << c.xml;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
leptris_document_free(again);
|
|
83
|
+
leptris_free_string(serialized);
|
|
84
|
+
leptris_document_free(doc);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
TEST(SerializeRoundTrip, KeepsEntityRefNodesUnescaped) {
|
|
89
|
+
// KEEP_ENTITY_REFS stores named references as ENTITY_REF nodes;
|
|
90
|
+
// those still serialize as &name; (they are markup, not text).
|
|
91
|
+
const char xml[] =
|
|
92
|
+
"<!DOCTYPE r [<!ENTITY foo \"x\">]><r>a &foo; b</r>";
|
|
93
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
94
|
+
LeptrisDocument doc = leptris_parse_string_flags(
|
|
95
|
+
xml, std::strlen(xml), LEPTRIS_PARSE_KEEP_ENTITY_REFS, &st);
|
|
96
|
+
ASSERT_NE(doc, nullptr);
|
|
97
|
+
char* serialized = leptris_document_serialize(doc, nullptr);
|
|
98
|
+
ASSERT_NE(serialized, nullptr);
|
|
99
|
+
EXPECT_NE(std::string(serialized).find("&foo;"), std::string::npos)
|
|
100
|
+
<< serialized;
|
|
101
|
+
EXPECT_EQ(std::string(serialized).find("&foo;"), std::string::npos)
|
|
102
|
+
<< serialized;
|
|
103
|
+
leptris_free_string(serialized);
|
|
104
|
+
leptris_document_free(doc);
|
|
105
|
+
}
|
|
106
|
+
|
|
42
107
|
TEST(SerializeRoundTrip, GrowsBufferForHugeTextContent) {
|
|
43
108
|
// Regression for TODO 08: buffer_ensure_capacity must grow without
|
|
44
109
|
// integer overflow or silent realloc-failure corruption.
|
|
@@ -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.
|
|
4
|
+
"version": "1.9.377",
|
|
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",
|