leptris 1.9.204.0 → 1.9.207.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -19
- data/Rakefile +1 -1
- data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/ffi.rb +28 -0
- data/vendor-src/libleptris/CHANGELOG.md +51 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/src/include/leptris/error.h +56 -0
- data/vendor-src/libleptris/src/include/leptris/types.h +29 -1
- data/vendor-src/libleptris/src/leptris/dom/diag.h +2 -13
- data/vendor-src/libleptris/src/leptris/dom/digest.c +5 -1
- data/vendor-src/libleptris/src/leptris/error.c +22 -0
- data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +61 -6
- data/vendor-src/libleptris/src/leptris/html/html_parse.c +125 -39
- data/vendor-src/libleptris/src/leptris/leptris.c +7 -0
- data/vendor-src/libleptris/src/leptris/leptris_internal.h +23 -1
- data/vendor-src/libleptris/src/leptris/sax/pull.c +52 -17
- data/vendor-src/libleptris/src/leptris/sax/streaming.c +90 -5
- data/vendor-src/libleptris/test/dom/test_digest.cpp +36 -0
- data/vendor-src/libleptris/test/dom/test_dom.cpp +24 -0
- data/vendor-src/libleptris/test/parser/test_parser.cpp +43 -0
- data/vendor-src/libleptris/test/sax/test_pull.cpp +32 -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: b10f689b2142684316157264cfd935433d6d0d22db8cf90b4a27e9c4ba79bd88
|
|
4
|
+
data.tar.gz: f3e625c375d995087bec8cd6c058a7a9c900791125f1422f6da8b2436e90047b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ddf28875bb34e2eb14074c51834614f5cb5acddf0a33c58f55a50d9e6970a065eb70cb6f3da51c1114cad3a8c062a6dec98d6ce271ff27a81a77033f92e774ea
|
|
7
|
+
data.tar.gz: b1c7bc0629b2840a7a23d71bd27b08303426ab7b013d8a272d9a1fedc82b8f40ba8fdd6805724f878fbf4374752912658ea5922e79548f16a53a859cf4de3164
|
data/CHANGELOG.md
CHANGED
|
@@ -54,25 +54,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
54
54
|
### Not fixed despite upstream closes
|
|
55
55
|
|
|
56
56
|
- **Iterparse attribute reads: all-nil runs on ubuntu-latest Ruby
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
57
|
+
## [1.9.207.0] - 2026-09-19
|
|
58
|
+
|
|
59
|
+
### Fixed — engine sync: big-endian close-tag parse (#1197 upstream)
|
|
60
|
+
|
|
61
|
+
- Vendored libleptris 1.9.207: the close-tag fast-path compare
|
|
62
|
+
masked the wrong end of its 8-byte name load on big-endian hosts,
|
|
63
|
+
rejecting ordinary paired-tag documents with "malformed input"
|
|
64
|
+
(434/727 binding-suite failures under qemu s390x). The mask now
|
|
65
|
+
follows memory order per byte dialect; the s390x-linux-musl
|
|
66
|
+
platform gem becomes viable with this release.
|
|
67
|
+
|
|
68
|
+
### Added — FFI mirror adopts the 1.9.202–1.9.207 engine surface
|
|
69
|
+
|
|
70
|
+
- audit:symbols lockstep attachments: recover diagnostics
|
|
71
|
+
(leptris_document_parse_diag[_count]), the standalone DTD family
|
|
72
|
+
(leptris_dtd_*), declaration/doctype removal, engine-heap buffer
|
|
73
|
+
allocation, and one-shot SAX. leptris_set_error stays internal —
|
|
74
|
+
the engine now hides it on ELF too (leptris#1243), so the export
|
|
75
|
+
surface agrees across Linux/macOS/Windows.
|
|
76
76
|
|
|
77
77
|
## [1.9.201.3] - 2026-09-19
|
|
78
78
|
|
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.207"
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -137,6 +137,7 @@ module Leptris
|
|
|
137
137
|
typedef :pointer, :leptris_iterparse
|
|
138
138
|
typedef :pointer, :leptris_xpath_var_set
|
|
139
139
|
typedef :pointer, :leptris_sax_parser
|
|
140
|
+
typedef :pointer, :leptris_dtd
|
|
140
141
|
typedef :int, :leptris_status
|
|
141
142
|
|
|
142
143
|
class SAXHandler < ::FFI::Struct
|
|
@@ -1156,6 +1157,33 @@ attach_function :leptris_parse_string,
|
|
|
1156
1157
|
[:leptris_document, :pointer, :pointer], :leptris_status
|
|
1157
1158
|
attach_function :leptris_status_string, [:leptris_status], :string
|
|
1158
1159
|
attach_function :leptris_error_message, [:leptris_status], :string
|
|
1160
|
+
# Engine surface adopted with libleptris 1.9.206 (the FFI
|
|
1161
|
+
# mirror audit keeps attachments and exports in lockstep):
|
|
1162
|
+
# recover diagnostics (#1200), the standalone DTD family
|
|
1163
|
+
# (leptris_dtd_*), declaration/doctype removal (#1094), the
|
|
1164
|
+
# engine-heap buffer (#1219), one-shot SAX (#1178), and the
|
|
1165
|
+
# exported error-recorder entry point.
|
|
1166
|
+
attach_function :leptris_alloc_buffer, [:size_t], :pointer
|
|
1167
|
+
attach_function :leptris_document_clear_declaration,
|
|
1168
|
+
[:leptris_document], :leptris_status
|
|
1169
|
+
attach_function :leptris_document_remove_doctype,
|
|
1170
|
+
[:leptris_document], :leptris_status
|
|
1171
|
+
attach_function :leptris_document_parse_diag_count,
|
|
1172
|
+
[:leptris_document], :size_t
|
|
1173
|
+
attach_function :leptris_document_parse_diag,
|
|
1174
|
+
[:leptris_document, :size_t, :pointer, :pointer, :size_t], :int
|
|
1175
|
+
attach_function :leptris_dtd_parse,
|
|
1176
|
+
[:string, :size_t], :leptris_dtd
|
|
1177
|
+
attach_function :leptris_dtd_free, [:leptris_dtd], :void
|
|
1178
|
+
attach_function :leptris_dtd_parse_external_subset,
|
|
1179
|
+
[:leptris_dtd, :string, :size_t], :int
|
|
1180
|
+
attach_function :leptris_dtd_set_pe_loader,
|
|
1181
|
+
[:leptris_dtd, :pointer, :pointer], :void
|
|
1182
|
+
attach_function :leptris_dtd_validate,
|
|
1183
|
+
[:leptris_document, :leptris_dtd, :pointer], :int
|
|
1184
|
+
attach_function :leptris_dtd_error_free, [:pointer], :void
|
|
1185
|
+
attach_function :leptris_sax_parser_set_one_shot,
|
|
1186
|
+
[:leptris_sax_parser, :int], :void
|
|
1159
1187
|
# Thread-global last-failure position (error.h): companion to
|
|
1160
1188
|
# leptris_last_error; populated by recover parses (#547).
|
|
1161
1189
|
attach_function :leptris_last_error_position,
|
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.207] - 2026-09-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- stop leptris_set_error leaking into the ELF dynamic table (build)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [1.9.206] - 2026-09-19
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- endian-neutral close-tag name compare (#1197) (parse)
|
|
16
|
+
|
|
17
|
+
### Performance
|
|
18
|
+
|
|
19
|
+
- memchr finds the text span end (TODO.max-perf/2-3 slice 3) (sax)
|
|
20
|
+
- zero-copy slice emission for one-shot pulls (#1178 tail) (sax)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [1.9.205] - 2026-09-19
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Parse recover diagnostics (#1200): the DOM parse now records
|
|
29
|
+
duplicate-attribute events on the document — `LeptrisDiagKind`
|
|
30
|
+
(with the new `LEPTRIS_DIAG_RECOVER` kind) is public in
|
|
31
|
+
types.h, and the diag list reads through
|
|
32
|
+
`leptris_document_parse_diag_count` /
|
|
33
|
+
`leptris_document_parse_diag` (error.h). First definition wins,
|
|
34
|
+
matching the SAX lane's libxml2-recover surface; the digest
|
|
35
|
+
gate no longer needs a SAX probe pass.
|
|
36
|
+
- `LEPTRIS_DIGEST_ATTR_ORDER` (#1200 part 1): opt-in digest flag
|
|
37
|
+
that makes attribute order identity-bearing instead of sorted
|
|
38
|
+
away.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- MSVC C2027 in `leptris_internal.h`: the `LeptrisDiag` forward
|
|
43
|
+
declaration moved to file scope (broke the windows-dll-exports
|
|
44
|
+
leg).
|
|
45
|
+
|
|
46
|
+
### Performance
|
|
47
|
+
|
|
48
|
+
- First-char-bucketed tag classifier for the WHATWG HTML path:
|
|
49
|
+
the tokenizer's name-list strcmp scans become one-load bucket
|
|
50
|
+
lookups — parse +31-37% on the #1218 bench shapes.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
3
54
|
## [1.9.204] - 2026-09-19
|
|
4
55
|
|
|
5
56
|
### Added
|
|
@@ -96,6 +96,62 @@ LEPTRIS_API void leptris_last_error_position(int* line, int* column);
|
|
|
96
96
|
*/
|
|
97
97
|
LEPTRIS_API const char* leptris_document_last_error(LeptrisDocument doc);
|
|
98
98
|
|
|
99
|
+
/* ============================================================================
|
|
100
|
+
* Parse diagnostics (#1200): recover-class events recorded on the
|
|
101
|
+
* DOM parse (duplicate attributes today) — the SAX lane's recover
|
|
102
|
+
* error list, unified engine-side so both lanes report the same
|
|
103
|
+
* surface. Documents stay usable; the diagnostics are advisory.
|
|
104
|
+
* ============================================================================ */
|
|
105
|
+
|
|
106
|
+
#ifndef LEPTRIS_API
|
|
107
|
+
# ifdef _WIN32
|
|
108
|
+
# ifdef LEPTRIS_BUILD_SHARED
|
|
109
|
+
# define LEPTRIS_API __declspec(dllexport)
|
|
110
|
+
# elif defined(LEPTRIS_BUILDING_DLL)
|
|
111
|
+
# define LEPTRIS_API __declspec(dllexport)
|
|
112
|
+
# elif defined(LEPTRIS_USE_SHARED)
|
|
113
|
+
# define LEPTRIS_API __declspec(dllimport)
|
|
114
|
+
# else
|
|
115
|
+
# define LEPTRIS_API
|
|
116
|
+
# endif
|
|
117
|
+
# else
|
|
118
|
+
# if defined(__GNUC__) && !defined(__clang__)
|
|
119
|
+
# define LEPTRIS_API __attribute__((visibility("default"), used, \
|
|
120
|
+
externally_visible))
|
|
121
|
+
# else
|
|
122
|
+
# define LEPTRIS_API __attribute__((visibility("default"), used))
|
|
123
|
+
# endif
|
|
124
|
+
# endif
|
|
125
|
+
#endif
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Number of recover-class diagnostics recorded during the
|
|
129
|
+
* document's parse.
|
|
130
|
+
*
|
|
131
|
+
* @param doc Document handle
|
|
132
|
+
* @return Diagnostic count (0 for NULL doc or a clean parse)
|
|
133
|
+
*/
|
|
134
|
+
LEPTRIS_API size_t leptris_document_parse_diag_count(LeptrisDocument doc);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Read one parse diagnostic by index.
|
|
138
|
+
*
|
|
139
|
+
* @param doc Document handle
|
|
140
|
+
* @param index Diagnostic index (< leptris_document_parse_diag_count)
|
|
141
|
+
* @param kind Out: diagnostic kind (may be NULL)
|
|
142
|
+
* @param message Out: NUL-terminated message buffer (may be NULL)
|
|
143
|
+
* @param message_cap Capacity of message (ignored when message NULL)
|
|
144
|
+
* @return 1 on success; 0 for NULL doc or out-of-range index
|
|
145
|
+
*
|
|
146
|
+
* Memory: Message is copied into the caller's buffer; diagnostics
|
|
147
|
+
* are owned by the document until leptris_document_free.
|
|
148
|
+
*/
|
|
149
|
+
LEPTRIS_API int leptris_document_parse_diag(LeptrisDocument doc,
|
|
150
|
+
size_t index,
|
|
151
|
+
LeptrisDiagKind* kind,
|
|
152
|
+
char* message,
|
|
153
|
+
size_t message_cap);
|
|
154
|
+
|
|
99
155
|
#ifdef __cplusplus
|
|
100
156
|
}
|
|
101
157
|
#endif
|
|
@@ -246,6 +246,28 @@ typedef enum {
|
|
|
246
246
|
LEPTRIS_DIFF_IGNORE_WS_TEXT = 1u
|
|
247
247
|
} LeptrisDiffFlags;
|
|
248
248
|
|
|
249
|
+
/* Unified narration record kinds (#1126). One structured record
|
|
250
|
+
* per validation/recover event, shared by every validator (RNG
|
|
251
|
+
* today; XSD/Schematron follow) and the DOM parse's recover
|
|
252
|
+
* channel (#1200). The kind selects the reporting convention;
|
|
253
|
+
* append-only. */
|
|
254
|
+
typedef enum {
|
|
255
|
+
LEPTRIS_DIAG_INVALID = 0,
|
|
256
|
+
LEPTRIS_DIAG_NOT_ALLOWED_ANYWHERE, /* name never valid in model */
|
|
257
|
+
LEPTRIS_DIAG_NOT_ALLOWED_HERE, /* valid name, wrong position */
|
|
258
|
+
LEPTRIS_DIAG_NOT_ALLOWED_YET, /* later; missing required first */
|
|
259
|
+
LEPTRIS_DIAG_INCOMPLETE, /* content ends before required */
|
|
260
|
+
LEPTRIS_DIAG_MISSING_REQUIRED_ATTR,
|
|
261
|
+
LEPTRIS_DIAG_ATTR_NOT_ALLOWED,
|
|
262
|
+
LEPTRIS_DIAG_ATTR_VALUE_INVALID,
|
|
263
|
+
LEPTRIS_DIAG_CHAR_CONTENT_INVALID, /* data/value/list mismatch */
|
|
264
|
+
/* Recover-class parse event: the document is usable, the input
|
|
265
|
+
* was not conformant (duplicate attribute, ...). Not a model
|
|
266
|
+
* error; SAX-lane "recover" parity (#1200). */
|
|
267
|
+
LEPTRIS_DIAG_RECOVER,
|
|
268
|
+
LEPTRIS_DIAG_COUNT_
|
|
269
|
+
} LeptrisDiagKind;
|
|
270
|
+
|
|
249
271
|
/* Subtree digest flags (issue #869). The digest is content-defined
|
|
250
272
|
* (no pointers/addresses — stable across processes); equality
|
|
251
273
|
* implies subtree equivalence under the flag semantics. */
|
|
@@ -253,7 +275,13 @@ typedef enum {
|
|
|
253
275
|
LEPTRIS_DIGEST_DEFAULT = 0,
|
|
254
276
|
/* Skip whitespace-only text nodes entirely (comparator's
|
|
255
277
|
* strip-profile default). */
|
|
256
|
-
LEPTRIS_DIGEST_DROP_WS_TEXT = 1u
|
|
278
|
+
LEPTRIS_DIGEST_DROP_WS_TEXT = 1u,
|
|
279
|
+
/* Hash attributes in DOCUMENT order instead of the sorted
|
|
280
|
+
* canonical order — documents differing only in attribute
|
|
281
|
+
* order digest DIFFERENT under this flag (canon's verbose
|
|
282
|
+
* diff gate, #1200). Equality under this flag additionally
|
|
283
|
+
* implies attribute-order identity. */
|
|
284
|
+
LEPTRIS_DIGEST_ATTR_ORDER = 2u
|
|
257
285
|
} LeptrisDigestFlags;
|
|
258
286
|
|
|
259
287
|
typedef struct {
|
|
@@ -17,20 +17,9 @@
|
|
|
17
17
|
extern "C" {
|
|
18
18
|
#endif
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
LEPTRIS_DIAG_INVALID = 0,
|
|
22
|
-
LEPTRIS_DIAG_NOT_ALLOWED_ANYWHERE, /* name never valid in model */
|
|
23
|
-
LEPTRIS_DIAG_NOT_ALLOWED_HERE, /* valid name, wrong position */
|
|
24
|
-
LEPTRIS_DIAG_NOT_ALLOWED_YET, /* later; missing required first */
|
|
25
|
-
LEPTRIS_DIAG_INCOMPLETE, /* content ends before required */
|
|
26
|
-
LEPTRIS_DIAG_MISSING_REQUIRED_ATTR,
|
|
27
|
-
LEPTRIS_DIAG_ATTR_NOT_ALLOWED,
|
|
28
|
-
LEPTRIS_DIAG_ATTR_VALUE_INVALID,
|
|
29
|
-
LEPTRIS_DIAG_CHAR_CONTENT_INVALID, /* data/value/list mismatch */
|
|
30
|
-
LEPTRIS_DIAG_COUNT_
|
|
31
|
-
} LeptrisDiagKind;
|
|
20
|
+
/* LeptrisDiagKind lives in leptris/types.h (public, #1200). */
|
|
32
21
|
|
|
33
|
-
typedef struct {
|
|
22
|
+
typedef struct LeptrisDiag {
|
|
34
23
|
LeptrisDiagKind kind;
|
|
35
24
|
char* offender_name; /* captured at emit time (strdup'd);
|
|
36
25
|
* NULL when the diag has no offender.
|
|
@@ -181,7 +181,11 @@ static uint64_t digest_element(LeptrisElement e, LeptrisDigestFlags flags) {
|
|
|
181
181
|
attrs[n].value_len = leptris_attr_value_sv(a).length;
|
|
182
182
|
n++;
|
|
183
183
|
}
|
|
184
|
-
|
|
184
|
+
/* ATTR_ORDER (#1200): document order is canonical — the
|
|
185
|
+
* walk above already collected in document order, so
|
|
186
|
+
* skip the sort and attribute order feeds the hash. */
|
|
187
|
+
if (!(flags & LEPTRIS_DIGEST_ATTR_ORDER))
|
|
188
|
+
qsort(attrs, n, sizeof(DigestAttr), digest_attr_cmp);
|
|
185
189
|
}
|
|
186
190
|
h = digest_mix_u64(h, n);
|
|
187
191
|
for (size_t i = 0; i < n; i++) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
#include "common/port.h" /* LEPTRIS_THREAD_LOCAL (TODO.concurrency/01) */
|
|
8
8
|
#include <stdio.h>
|
|
9
9
|
#include <string.h>
|
|
10
|
+
#include "dom/diag.h" /* LeptrisDiag layout (#1200) */
|
|
10
11
|
|
|
11
12
|
/* Error state — THREAD-LOCAL since TODO.concurrency/01: concurrent
|
|
12
13
|
* parses (one document per thread) no longer race the channel. The
|
|
@@ -131,4 +132,25 @@ LEPTRIS_API const char* leptris_status_string(LeptrisStatus status) {
|
|
|
131
132
|
case LEPTRIS_ERROR_IO: return "I/O error";
|
|
132
133
|
default: return "Unknown error";
|
|
133
134
|
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ---- #1200: recover-class parse diagnostics ------------------- */
|
|
138
|
+
|
|
139
|
+
LEPTRIS_API size_t leptris_document_parse_diag_count(LeptrisDocument doc) {
|
|
140
|
+
return doc ? (size_t)doc->parse_diag_count : 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
LEPTRIS_API int leptris_document_parse_diag(LeptrisDocument doc,
|
|
144
|
+
size_t index,
|
|
145
|
+
LeptrisDiagKind* kind,
|
|
146
|
+
char* message,
|
|
147
|
+
size_t message_cap) {
|
|
148
|
+
if (!doc || index >= (size_t)doc->parse_diag_count) return 0;
|
|
149
|
+
struct LeptrisDiag* d = (struct LeptrisDiag*)doc->parse_diags + index;
|
|
150
|
+
if (kind) *kind = d->kind;
|
|
151
|
+
if (message && message_cap) {
|
|
152
|
+
strncpy(message, d->message, message_cap - 1);
|
|
153
|
+
message[message_cap - 1] = '\0';
|
|
154
|
+
}
|
|
155
|
+
return 1;
|
|
134
156
|
}
|
|
@@ -437,7 +437,47 @@ static inline int dp_add_attr_inline(DParser* p, LeptrisElement elem,
|
|
|
437
437
|
* Careful: first_attribute_off == 0 means empty list. We can't decode
|
|
438
438
|
* the pointer and check for NULL — at offset 0 the decoded pointer
|
|
439
439
|
* is `elem` itself, which is non-NULL. Check the offset field. */
|
|
440
|
+
/* #1200: duplicate-attribute detection. Prefilter on (len,
|
|
441
|
+
* first byte) — a mismatching pair costs two loads; the memcmp
|
|
442
|
+
* only runs on genuine candidates. First definition wins
|
|
443
|
+
* (libxml2 recover semantics, and the digest's first-wins
|
|
444
|
+
* becomes official); the duplicate never enters the chain, it
|
|
445
|
+
* costs its already-carved slot. */
|
|
440
446
|
if (elem->first_attribute_off != 0) {
|
|
447
|
+
int dup = 0;
|
|
448
|
+
for (struct leptris_attribute* a =
|
|
449
|
+
(struct leptris_attribute*)((char*)elem +
|
|
450
|
+
elem->first_attribute_off);
|
|
451
|
+
a; a = leptris_attr_next(a)) {
|
|
452
|
+
if (a->name_view.length == attr->name_view.length &&
|
|
453
|
+
a->name_view.data[0] == attr->name_view.data[0] &&
|
|
454
|
+
memcmp(a->name_view.data, attr->name_view.data,
|
|
455
|
+
attr->name_view.length) == 0) {
|
|
456
|
+
dup = 1;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (dup) {
|
|
461
|
+
if (p->doc) {
|
|
462
|
+
extern struct LeptrisDiag* leptris_diag_emit(
|
|
463
|
+
struct LeptrisDiag**, int*, int*,
|
|
464
|
+
LeptrisDiagKind, LeptrisNodeRef,
|
|
465
|
+
const char*, ...);
|
|
466
|
+
char qname[130];
|
|
467
|
+
size_t ql = attr->name_view.length < 129
|
|
468
|
+
? attr->name_view.length : 129;
|
|
469
|
+
memcpy(qname, attr->name_view.data, ql);
|
|
470
|
+
qname[ql] = '\0';
|
|
471
|
+
leptris_diag_emit(&p->doc->parse_diags,
|
|
472
|
+
&p->doc->parse_diag_count,
|
|
473
|
+
&p->doc->parse_diag_cap,
|
|
474
|
+
LEPTRIS_DIAG_RECOVER, NULL,
|
|
475
|
+
"Attribute %s redefined", qname);
|
|
476
|
+
}
|
|
477
|
+
/* 1 = duplicate skipped (keep first): the caller skips
|
|
478
|
+
* the raw-attr registration so BOTH surfaces agree. */
|
|
479
|
+
return 1;
|
|
480
|
+
}
|
|
441
481
|
/* Cache should always be valid mid-parse. Fall back to walk
|
|
442
482
|
* only if cache is NULL (defensive — shouldn't happen). */
|
|
443
483
|
struct leptris_attribute* tail = p->current_elem_last_attr;
|
|
@@ -845,13 +885,16 @@ static int dp_parse_attrs(DParser* p, LeptrisElement elem) {
|
|
|
845
885
|
continue;
|
|
846
886
|
}
|
|
847
887
|
|
|
848
|
-
/* Regular attribute — zero-copy name/value, bulk-allocated
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
888
|
+
/* Regular attribute — zero-copy name/value, bulk-allocated
|
|
889
|
+
* struct. 1 = duplicate skipped: neither surface sees it. */
|
|
890
|
+
int arc = dp_add_attr_inline(p, elem, name_start, name_len,
|
|
891
|
+
val_start, val_len, has_amp,
|
|
892
|
+
has_ws, attr_colon);
|
|
893
|
+
if (arc < 0) return -1;
|
|
894
|
+
if (arc == 0 &&
|
|
895
|
+
dp_raw_attr(p, elem, name_start, name_len,
|
|
896
|
+
val_start, val_len) != 0)
|
|
852
897
|
return -1;
|
|
853
|
-
if (dp_raw_attr(p, elem, name_start, name_len,
|
|
854
|
-
val_start, val_len) != 0) return -1;
|
|
855
898
|
}
|
|
856
899
|
/* for (;;): every exit is a return inside the loop (0 = tag
|
|
857
900
|
* closed, 1 = self-closing, -1 = error) — there is no
|
|
@@ -1571,9 +1614,21 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
|
|
|
1571
1614
|
if (close_local_len <= 8 &&
|
|
1572
1615
|
(p.probe_slack ||
|
|
1573
1616
|
close_local + 8 <= p.end + 1)) {
|
|
1617
|
+
/* The mask follows MEMORY order: memcpy fills the low
|
|
1618
|
+
* bytes on little-endian and the high bytes on
|
|
1619
|
+
* big-endian, so each dialect must mask the end its
|
|
1620
|
+
* load populated. The unconditional low-bytes mask
|
|
1621
|
+
* compared bytes past the name on big-endian and
|
|
1622
|
+
* failed every close tag (#1197). */
|
|
1623
|
+
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
|
1624
|
+
uint64_t mask = (close_local_len == 8)
|
|
1625
|
+
? ~(uint64_t)0
|
|
1626
|
+
: (~(uint64_t)0 << (64 - close_local_len * 8));
|
|
1627
|
+
#else
|
|
1574
1628
|
uint64_t mask = (close_local_len == 8)
|
|
1575
1629
|
? ~(uint64_t)0
|
|
1576
1630
|
: ((1ull << (close_local_len * 8)) - 1);
|
|
1631
|
+
#endif
|
|
1577
1632
|
uint64_t a, b;
|
|
1578
1633
|
memcpy(&a, open_name, 8);
|
|
1579
1634
|
memcpy(&b, close_local, 8);
|
|
@@ -3061,30 +3061,130 @@ static int h_is_formatting(const char* n) {
|
|
|
3061
3061
|
|
|
3062
3062
|
/* WHATWG "special" category (13.2.4.2) — the adoption agency's
|
|
3063
3063
|
* furthest-block candidates. */
|
|
3064
|
+
/* Generated tag classifier (#1218 hot path): one first-char
|
|
3065
|
+
* bucket walk replaces the ~78-entry strcmp scans that the
|
|
3066
|
+
* sampler pinned at ~21% of WHATWG parse (h_is_special_ww +
|
|
3067
|
+
* h_reconstructs). Keep in sync with the WHATWG special and
|
|
3068
|
+
* no-formatting sets. */
|
|
3069
|
+
typedef struct {
|
|
3070
|
+
char name[12];
|
|
3071
|
+
uint8_t len;
|
|
3072
|
+
uint8_t special; /* WHATWG 13.2.4 special set */
|
|
3073
|
+
uint8_t no_fmt; /* never reconstructed (formatting) */
|
|
3074
|
+
} HTagInfo;
|
|
3075
|
+
|
|
3076
|
+
static const HTagInfo h_tag_infos[] = {
|
|
3077
|
+
{"address", 7, 1, 1},
|
|
3078
|
+
{"applet", 6, 1, 0},
|
|
3079
|
+
{"area", 4, 1, 0},
|
|
3080
|
+
{"article", 7, 1, 1},
|
|
3081
|
+
{"aside", 5, 1, 1},
|
|
3082
|
+
{"base", 4, 1, 1},
|
|
3083
|
+
{"basefont", 8, 1, 1},
|
|
3084
|
+
{"bgsound", 7, 1, 1},
|
|
3085
|
+
{"blockquote", 10, 1, 1},
|
|
3086
|
+
{"body", 4, 1, 1},
|
|
3087
|
+
{"br", 2, 1, 0},
|
|
3088
|
+
{"button", 6, 1, 0},
|
|
3089
|
+
{"caption", 7, 1, 1},
|
|
3090
|
+
{"center", 6, 1, 1},
|
|
3091
|
+
{"col", 3, 1, 1},
|
|
3092
|
+
{"colgroup", 8, 1, 1},
|
|
3093
|
+
{"dd", 2, 1, 1},
|
|
3094
|
+
{"details", 7, 1, 1},
|
|
3095
|
+
{"dialog", 6, 1, 1},
|
|
3096
|
+
{"dir", 3, 1, 1},
|
|
3097
|
+
{"div", 3, 1, 1},
|
|
3098
|
+
{"dl", 2, 1, 1},
|
|
3099
|
+
{"dt", 2, 1, 1},
|
|
3100
|
+
{"embed", 5, 1, 0},
|
|
3101
|
+
{"fieldset", 8, 1, 1},
|
|
3102
|
+
{"figcaption", 10, 1, 1},
|
|
3103
|
+
{"figure", 6, 1, 1},
|
|
3104
|
+
{"footer", 6, 1, 1},
|
|
3105
|
+
{"form", 4, 1, 1},
|
|
3106
|
+
{"frame", 5, 1, 1},
|
|
3107
|
+
{"frameset", 8, 1, 1},
|
|
3108
|
+
{"h1", 2, 1, 1},
|
|
3109
|
+
{"h2", 2, 1, 1},
|
|
3110
|
+
{"h3", 2, 1, 1},
|
|
3111
|
+
{"h4", 2, 1, 1},
|
|
3112
|
+
{"h5", 2, 1, 1},
|
|
3113
|
+
{"h6", 2, 1, 1},
|
|
3114
|
+
{"head", 4, 1, 1},
|
|
3115
|
+
{"header", 6, 1, 1},
|
|
3116
|
+
{"hgroup", 6, 1, 1},
|
|
3117
|
+
{"hr", 2, 1, 1},
|
|
3118
|
+
{"html", 4, 1, 1},
|
|
3119
|
+
{"iframe", 6, 1, 0},
|
|
3120
|
+
{"img", 3, 1, 0},
|
|
3121
|
+
{"input", 5, 1, 0},
|
|
3122
|
+
{"keygen", 6, 1, 0},
|
|
3123
|
+
{"li", 2, 1, 1},
|
|
3124
|
+
{"link", 4, 1, 1},
|
|
3125
|
+
{"listing", 7, 1, 1},
|
|
3126
|
+
{"main", 4, 1, 1},
|
|
3127
|
+
{"marquee", 7, 1, 0},
|
|
3128
|
+
{"menu", 4, 1, 1},
|
|
3129
|
+
{"meta", 4, 1, 1},
|
|
3130
|
+
{"nav", 3, 1, 1},
|
|
3131
|
+
{"noembed", 7, 1, 0},
|
|
3132
|
+
{"noframes", 8, 1, 1},
|
|
3133
|
+
{"noscript", 8, 1, 0},
|
|
3134
|
+
{"object", 6, 1, 0},
|
|
3135
|
+
{"ol", 2, 1, 1},
|
|
3136
|
+
{"p", 1, 1, 1},
|
|
3137
|
+
{"param", 5, 1, 0},
|
|
3138
|
+
{"plaintext", 9, 1, 1},
|
|
3139
|
+
{"pre", 3, 1, 1},
|
|
3140
|
+
{"script", 6, 1, 1},
|
|
3141
|
+
{"search", 6, 1, 1},
|
|
3142
|
+
{"section", 7, 1, 1},
|
|
3143
|
+
{"select", 6, 1, 0},
|
|
3144
|
+
{"source", 6, 1, 0},
|
|
3145
|
+
{"style", 5, 1, 1},
|
|
3146
|
+
{"summary", 7, 1, 1},
|
|
3147
|
+
{"table", 5, 1, 1},
|
|
3148
|
+
{"tbody", 5, 1, 1},
|
|
3149
|
+
{"td", 2, 1, 1},
|
|
3150
|
+
{"template", 8, 1, 1},
|
|
3151
|
+
{"textarea", 8, 1, 1},
|
|
3152
|
+
{"tfoot", 5, 1, 1},
|
|
3153
|
+
{"th", 2, 1, 1},
|
|
3154
|
+
{"thead", 5, 1, 1},
|
|
3155
|
+
{"title", 5, 1, 1},
|
|
3156
|
+
{"tr", 2, 1, 1},
|
|
3157
|
+
{"track", 5, 1, 0},
|
|
3158
|
+
{"ul", 2, 1, 1},
|
|
3159
|
+
{"wbr", 3, 1, 0},
|
|
3160
|
+
{"xmp", 3, 1, 0},
|
|
3161
|
+
};
|
|
3162
|
+
|
|
3163
|
+
/* bucket[first_char] = start index; bucket[first_char+1] = end */
|
|
3164
|
+
static const uint8_t h_tag_bucket[27] = {
|
|
3165
|
+
0, 5, 12, 16, 23, 24, 31, 31, 42,
|
|
3166
|
+
45, 45, 46, 49, 53, 57, 59, 63, 63,
|
|
3167
|
+
63, 70, 81, 82, 82, 83, 84, 84, 84,
|
|
3168
|
+
};
|
|
3169
|
+
|
|
3170
|
+
/* Resolve a (lowercased) tag name's flags; NULL when unknown. */
|
|
3171
|
+
static const HTagInfo* h_tag_lookup(const char* n) {
|
|
3172
|
+
if (!n) return NULL;
|
|
3173
|
+
unsigned char c = (unsigned char)n[0];
|
|
3174
|
+
if (c < 'a' || c > 'z') return NULL;
|
|
3175
|
+
for (int i = h_tag_bucket[c - 'a'];
|
|
3176
|
+
i < h_tag_bucket[c - 'a' + 1]; i++) {
|
|
3177
|
+
const HTagInfo* t = &h_tag_infos[i];
|
|
3178
|
+
if ((uint8_t)n[t->len] == 0 && t->name[t->len - 1] == n[t->len - 1] &&
|
|
3179
|
+
memcmp(n, t->name, t->len) == 0)
|
|
3180
|
+
return t;
|
|
3181
|
+
}
|
|
3182
|
+
return NULL;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3064
3185
|
static int h_is_special_ww(const char* n) {
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
"base", "basefont", "bgsound", "blockquote", "body",
|
|
3068
|
-
"br", "button", "caption", "center", "col",
|
|
3069
|
-
"colgroup", "dd", "details", "dialog", "dir",
|
|
3070
|
-
"div",
|
|
3071
|
-
"dl", "dt", "embed", "fieldset", "figcaption",
|
|
3072
|
-
"figure", "footer", "form", "frame", "frameset",
|
|
3073
|
-
"h1", "h2", "h3", "h4", "h5",
|
|
3074
|
-
"h6", "head", "header", "hgroup", "hr",
|
|
3075
|
-
"html", "iframe", "img", "input", "keygen",
|
|
3076
|
-
"li", "link", "listing", "main", "marquee",
|
|
3077
|
-
"menu", "meta", "nav", "noembed", "noframes",
|
|
3078
|
-
"noscript", "object", "ol", "p", "param",
|
|
3079
|
-
"plaintext", "pre", "script", "search", "section",
|
|
3080
|
-
"select", "source", "style", "summary", "table",
|
|
3081
|
-
"tbody", "td", "template", "textarea", "tfoot",
|
|
3082
|
-
"th", "thead", "title", "tr", "track",
|
|
3083
|
-
"ul", "wbr", "xmp", NULL};
|
|
3084
|
-
if (!n) return 0;
|
|
3085
|
-
for (int i = 0; k[i]; i++)
|
|
3086
|
-
if (strcmp(n, k[i]) == 0) return 1;
|
|
3087
|
-
return 0;
|
|
3186
|
+
const HTagInfo* t = h_tag_lookup(n);
|
|
3187
|
+
return t && t->special;
|
|
3088
3188
|
}
|
|
3089
3189
|
|
|
3090
3190
|
#define H_NS_HTML 0
|
|
@@ -3256,22 +3356,8 @@ static int h_input_type_hidden(const char* q, const char* end) {
|
|
|
3256
3356
|
* head set, the block-level set (they close p instead), the table
|
|
3257
3357
|
* family, and raw-text elements. */
|
|
3258
3358
|
static int h_reconstructs(const char* n) {
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
"bgsound", "link", "meta", "script", "style", "template",
|
|
3262
|
-
"title", "noframes", "address", "article", "aside",
|
|
3263
|
-
"blockquote", "center", "details", "dialog", "dir",
|
|
3264
|
-
"div", "dl", "fieldset", "figcaption", "figure",
|
|
3265
|
-
"footer", "header", "hgroup", "main", "menu", "nav",
|
|
3266
|
-
"ol", "p", "search", "section", "summary", "ul",
|
|
3267
|
-
"h1", "h2", "h3", "h4", "h5", "h6",
|
|
3268
|
-
"pre", "listing", "form", "li", "dd", "dt",
|
|
3269
|
-
"plaintext", "textarea", "table", "hr", "caption", "col",
|
|
3270
|
-
"colgroup", "frame", "tbody", "tfoot", "thead", "td",
|
|
3271
|
-
"th", "tr", NULL};
|
|
3272
|
-
for (int i = 0; no[i]; i++)
|
|
3273
|
-
if (strcmp(n, no[i]) == 0) return 0;
|
|
3274
|
-
return 1;
|
|
3359
|
+
const HTagInfo* t = h_tag_lookup(n);
|
|
3360
|
+
return !(t && t->no_fmt);
|
|
3275
3361
|
}
|
|
3276
3362
|
|
|
3277
3363
|
/* ---- #659 foreign content (WHATWG 12.2.6.5) ---- */
|
|
@@ -836,6 +836,13 @@ LEPTRIS_API void leptris_document_free(struct leptris_document* doc) {
|
|
|
836
836
|
/* The pool will be destroyed below, freeing all elements */
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
+
/* Parse diagnostics (#1200): the recover-channel list. */
|
|
840
|
+
{
|
|
841
|
+
extern void leptris_diag_free(struct LeptrisDiag**, int*, int*);
|
|
842
|
+
leptris_diag_free(&doc->parse_diags, &doc->parse_diag_count,
|
|
843
|
+
&doc->parse_diag_cap);
|
|
844
|
+
}
|
|
845
|
+
|
|
839
846
|
/* Free document fields */
|
|
840
847
|
if (doc->encoding) {
|
|
841
848
|
LEPTRIS_FREE(doc->encoding);
|
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
#ifndef LEPTRIS_INTERNAL_H
|
|
10
10
|
#define LEPTRIS_INTERNAL_H
|
|
11
11
|
|
|
12
|
+
/* Internal entry points must not leak into the shared-library ABI.
|
|
13
|
+
* Windows already hides them (no __declspec(dllexport)) and macOS
|
|
14
|
+
* build flags hide non-exported globals, but plain ELF keeps
|
|
15
|
+
* default visibility — leptris_set_error escaped into the Linux
|
|
16
|
+
* dynamic table while being absent from the PE exports, which no
|
|
17
|
+
* cross-platform FFI mirror can attach against (#1197 release
|
|
18
|
+
* cascade). */
|
|
19
|
+
#if defined(_WIN32) || !(defined(__GNUC__) || defined(__clang__))
|
|
20
|
+
#define LEPTRIS_LOCAL
|
|
21
|
+
#else
|
|
22
|
+
#define LEPTRIS_LOCAL __attribute__((visibility("hidden")))
|
|
23
|
+
#endif
|
|
24
|
+
|
|
12
25
|
#include <stddef.h>
|
|
13
26
|
#include <stdint.h>
|
|
14
27
|
|
|
@@ -104,6 +117,8 @@ struct leptris_mut_attr_block {
|
|
|
104
117
|
char bytes[];
|
|
105
118
|
};
|
|
106
119
|
|
|
120
|
+
struct LeptrisDiag;
|
|
121
|
+
|
|
107
122
|
struct leptris_document {
|
|
108
123
|
struct leptris_element* root; /* Root element (legacy API) */
|
|
109
124
|
/* Snapshot of this document's last failing parse message
|
|
@@ -111,6 +126,13 @@ struct leptris_document {
|
|
|
111
126
|
* thread-local channel). */
|
|
112
127
|
char last_error_message[256];
|
|
113
128
|
|
|
129
|
+
/* Recover-class parse diagnostics (#1200): the DOM parse's
|
|
130
|
+
* parity surface with the SAX lane's recover errors. Pool-free
|
|
131
|
+
* list (leptris_diag_* owns it); released in document_free. */
|
|
132
|
+
struct LeptrisDiag* parse_diags;
|
|
133
|
+
int parse_diag_count;
|
|
134
|
+
int parse_diag_cap;
|
|
135
|
+
|
|
114
136
|
/* EXSLT-style extension pack enabled via leptris_exslt_enable
|
|
115
137
|
* (TODO.concurrency/06). */
|
|
116
138
|
int exslt_enabled;
|
|
@@ -883,7 +905,7 @@ static inline int leptris_strcmp(const char* s1, const char* s2) {
|
|
|
883
905
|
* ============================================================================ */
|
|
884
906
|
|
|
885
907
|
/* Set error with basic message */
|
|
886
|
-
void leptris_set_error(leptris_error_code code, const char* message);
|
|
908
|
+
LEPTRIS_LOCAL void leptris_set_error(leptris_error_code code, const char* message);
|
|
887
909
|
|
|
888
910
|
/* Set error with line/column position */
|
|
889
911
|
void leptris_set_parse_error_position(int line, int column);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* by the slice, not the document. All event strings are owned by the
|
|
9
9
|
* puller and remain valid until the NEXT leptris_pull_next call. */
|
|
10
10
|
#include "../../include/leptris/sax/sax.h"
|
|
11
|
+
#include "sax_internal.h"
|
|
11
12
|
#include <stdio.h>
|
|
12
13
|
#include <stdlib.h>
|
|
13
14
|
#include <string.h>
|
|
@@ -22,6 +23,11 @@ typedef struct pull_event {
|
|
|
22
23
|
/* START_ELEMENT attributes: owned flat copies [n1,v1,n2,v2...] */
|
|
23
24
|
char** attrs;
|
|
24
25
|
size_t attr_count;
|
|
26
|
+
/* One-shot buffer pulls (TODO.max-perf/2-3 slice 1): strings
|
|
27
|
+
* borrow the sax parser's stable storage (input buffer + scratch,
|
|
28
|
+
* alive until parser free) — queue_reset_event frees only the
|
|
29
|
+
* attrs array itself. */
|
|
30
|
+
int borrowed;
|
|
25
31
|
} pull_event;
|
|
26
32
|
|
|
27
33
|
struct leptris_pull_parser {
|
|
@@ -57,9 +63,14 @@ struct leptris_pull_parser {
|
|
|
57
63
|
};
|
|
58
64
|
|
|
59
65
|
static void queue_reset_event(pull_event* e) {
|
|
60
|
-
|
|
66
|
+
if (!e->borrowed) {
|
|
67
|
+
free(e->name);
|
|
68
|
+
free(e->text);
|
|
69
|
+
}
|
|
61
70
|
if (e->attrs) {
|
|
62
|
-
|
|
71
|
+
if (!e->borrowed)
|
|
72
|
+
for (size_t i = 0; i < e->attr_count * 2; i++)
|
|
73
|
+
free(e->attrs[i]);
|
|
63
74
|
free(e->attrs);
|
|
64
75
|
}
|
|
65
76
|
memset(e, 0, sizeof(*e));
|
|
@@ -73,6 +84,18 @@ static char* pull_strdup_n(const char* s, size_t n) {
|
|
|
73
84
|
return c;
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
/* Event strings: borrow in one-shot mode (stable until parser free),
|
|
88
|
+
* strdup otherwise (streaming file mode — the source chunk dies at
|
|
89
|
+
* the next feed). */
|
|
90
|
+
static char* ev_string(pull_event* e, const struct leptris_pull_parser* p,
|
|
91
|
+
const char* s, size_t n) {
|
|
92
|
+
if (p->sax->one_shot) {
|
|
93
|
+
e->borrowed = 1;
|
|
94
|
+
return (char*)(s ? s : "");
|
|
95
|
+
}
|
|
96
|
+
return pull_strdup_n(s ? s : "", n);
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
static pull_event* queue_push(struct leptris_pull_parser* p) {
|
|
77
100
|
size_t used = (p->tail + p->cap - p->head) % p->cap;
|
|
78
101
|
if (used + 1 == p->cap) { /* full: grow */
|
|
@@ -104,16 +127,25 @@ static void cb_start_element(void* ud, const char* name, const char** attrs) {
|
|
|
104
127
|
pull_event* e = queue_push(p);
|
|
105
128
|
if (!e) return;
|
|
106
129
|
e->type = LEPTRIS_PULL_START_ELEMENT;
|
|
107
|
-
e->name =
|
|
130
|
+
e->name = ev_string(e, p, name, strlen(name));
|
|
108
131
|
if (attrs) {
|
|
109
132
|
size_t n = 0;
|
|
110
133
|
while (attrs[n]) n += 2; /* flat [name, value, ..., NULL] */
|
|
111
134
|
e->attr_count = n / 2;
|
|
112
135
|
e->attrs = (char**)calloc(n + 1, sizeof(char*));
|
|
113
136
|
if (e->attrs) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
if (p->sax->one_shot) {
|
|
138
|
+
/* Names/values are frame/scratch-owned (parser
|
|
139
|
+
* lifetime); the array itself stays queue-owned. */
|
|
140
|
+
e->borrowed = 1;
|
|
141
|
+
for (size_t i = 0; i < n; i++)
|
|
142
|
+
e->attrs[i] = (char*)(attrs[i] ? attrs[i] : "");
|
|
143
|
+
} else {
|
|
144
|
+
for (size_t i = 0; i < n; i++)
|
|
145
|
+
e->attrs[i] = pull_strdup_n(
|
|
146
|
+
attrs[i] ? attrs[i] : "",
|
|
147
|
+
attrs[i] ? strlen(attrs[i]) : 0);
|
|
148
|
+
}
|
|
117
149
|
}
|
|
118
150
|
}
|
|
119
151
|
}
|
|
@@ -123,7 +155,7 @@ static void cb_end_element(void* ud, const char* name) {
|
|
|
123
155
|
pull_event* e = queue_push(p);
|
|
124
156
|
if (!e) return;
|
|
125
157
|
e->type = LEPTRIS_PULL_END_ELEMENT;
|
|
126
|
-
e->name =
|
|
158
|
+
e->name = ev_string(e, p, name, strlen(name));
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
static void cb_characters(void* ud, const char* text, size_t len) {
|
|
@@ -131,7 +163,10 @@ static void cb_characters(void* ud, const char* text, size_t len) {
|
|
|
131
163
|
pull_event* e = queue_push(p);
|
|
132
164
|
if (!e) return;
|
|
133
165
|
e->type = LEPTRIS_PULL_TEXT;
|
|
134
|
-
|
|
166
|
+
/* Text spans are raw input-buffer slices — no NUL terminator in
|
|
167
|
+
* stable storage, so they always copy even in one-shot mode
|
|
168
|
+
* (the public event contract is NUL-terminated strings). */
|
|
169
|
+
e->text = pull_strdup_n(text ? text : "", len);
|
|
135
170
|
e->text_len = len;
|
|
136
171
|
}
|
|
137
172
|
|
|
@@ -140,7 +175,7 @@ static void cb_comment(void* ud, const char* comment) {
|
|
|
140
175
|
pull_event* e = queue_push(p);
|
|
141
176
|
if (!e) return;
|
|
142
177
|
e->type = LEPTRIS_PULL_COMMENT;
|
|
143
|
-
e->text =
|
|
178
|
+
e->text = ev_string(e, p, comment, strlen(comment));
|
|
144
179
|
e->text_len = strlen(comment);
|
|
145
180
|
}
|
|
146
181
|
|
|
@@ -149,7 +184,7 @@ static void cb_cdata(void* ud, const char* cdata) {
|
|
|
149
184
|
pull_event* e = queue_push(p);
|
|
150
185
|
if (!e) return;
|
|
151
186
|
e->type = LEPTRIS_PULL_CDATA;
|
|
152
|
-
e->text =
|
|
187
|
+
e->text = ev_string(e, p, cdata, strlen(cdata));
|
|
153
188
|
e->text_len = strlen(cdata);
|
|
154
189
|
}
|
|
155
190
|
|
|
@@ -158,9 +193,9 @@ static void cb_pi(void* ud, const char* target, const char* data) {
|
|
|
158
193
|
pull_event* e = queue_push(p);
|
|
159
194
|
if (!e) return;
|
|
160
195
|
e->type = LEPTRIS_PULL_PI;
|
|
161
|
-
e->name =
|
|
196
|
+
e->name = ev_string(e, p, target, strlen(target));
|
|
162
197
|
if (data) {
|
|
163
|
-
e->text =
|
|
198
|
+
e->text = ev_string(e, p, data, strlen(data));
|
|
164
199
|
e->text_len = strlen(data);
|
|
165
200
|
}
|
|
166
201
|
}
|
|
@@ -177,9 +212,9 @@ static void cb_start_prefix(void* ud, const char* prefix, const char* uri) {
|
|
|
177
212
|
pull_event* e = queue_push(p);
|
|
178
213
|
if (!e) return;
|
|
179
214
|
e->type = LEPTRIS_PULL_START_PREFIX;
|
|
180
|
-
e->name =
|
|
181
|
-
|
|
182
|
-
e->text =
|
|
215
|
+
e->name = ev_string(e, p, prefix ? prefix : "",
|
|
216
|
+
prefix ? strlen(prefix) : 0);
|
|
217
|
+
e->text = ev_string(e, p, uri ? uri : "", uri ? strlen(uri) : 0);
|
|
183
218
|
e->text_len = uri ? strlen(uri) : 0;
|
|
184
219
|
}
|
|
185
220
|
|
|
@@ -188,8 +223,8 @@ static void cb_end_prefix(void* ud, const char* prefix) {
|
|
|
188
223
|
pull_event* e = queue_push(p);
|
|
189
224
|
if (!e) return;
|
|
190
225
|
e->type = LEPTRIS_PULL_END_PREFIX;
|
|
191
|
-
e->name =
|
|
192
|
-
|
|
226
|
+
e->name = ev_string(e, p, prefix ? prefix : "",
|
|
227
|
+
prefix ? strlen(prefix) : 0);
|
|
193
228
|
}
|
|
194
229
|
|
|
195
230
|
static void cb_error(void* ud, const char* message, int line, int column) {
|
|
@@ -222,6 +222,36 @@ static int sxs_elem_push(LeptrisSAXParser* p, const char* name, size_t name_len)
|
|
|
222
222
|
return 0;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/* One-shot push: the name lives in the append-only scratch arena
|
|
226
|
+
* (parser lifetime) instead of the reusable frame slot, so
|
|
227
|
+
* handler-visible START/END names stay valid for borrowed consumers
|
|
228
|
+
* (pull.c ev_string) even after the stack slot serves a later
|
|
229
|
+
* sibling. */
|
|
230
|
+
static int sxs_elem_push_oneshot(LeptrisSAXParser* p, const char* name,
|
|
231
|
+
size_t name_len) {
|
|
232
|
+
if (p->elem_depth >= LEPTRIS_SAX_MAX_DEPTH) {
|
|
233
|
+
sxs_set_error(p, "Element nesting too deep");
|
|
234
|
+
return -1;
|
|
235
|
+
}
|
|
236
|
+
const char* nm = sxs_scratch_append(p, name, name_len);
|
|
237
|
+
if (!nm) {
|
|
238
|
+
sxs_set_error(p, "out of memory");
|
|
239
|
+
return -1;
|
|
240
|
+
}
|
|
241
|
+
SaxElementFrame* f = &p->elem_stack[p->elem_depth];
|
|
242
|
+
f->name = (char*)nm; /* frame only ever reads the name */
|
|
243
|
+
f->name_heap = NULL;
|
|
244
|
+
f->name_len = name_len;
|
|
245
|
+
f->attrs = f->attrs_inline;
|
|
246
|
+
f->attrs_inline[0] = NULL;
|
|
247
|
+
f->attrs_heap = NULL;
|
|
248
|
+
f->attr_count = 0;
|
|
249
|
+
f->attr_cap = SAX_ATTRS_INLINE;
|
|
250
|
+
f->self_closing = 0;
|
|
251
|
+
p->elem_depth++;
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
225
255
|
static void sxs_elem_pop(LeptrisSAXParser* p) {
|
|
226
256
|
if (p->elem_depth == 0) return;
|
|
227
257
|
p->elem_depth--;
|
|
@@ -591,7 +621,7 @@ static int sxs_step_elem_open_name(LeptrisSAXParser* p, int is_final) {
|
|
|
591
621
|
}
|
|
592
622
|
const char* nb = p->pos;
|
|
593
623
|
while (p->pos < p->end && sxs_is_name_char(*p->pos)) p->pos++;
|
|
594
|
-
if (
|
|
624
|
+
if (sxs_elem_push_oneshot(p, nb, (size_t)(p->pos - nb)) < 0) {
|
|
595
625
|
return SAX_STEP_ERR;
|
|
596
626
|
}
|
|
597
627
|
p->column += (int)(p->pos - nb);
|
|
@@ -716,6 +746,32 @@ static int sxs_step_attr_list(LeptrisSAXParser* p, int is_final) {
|
|
|
716
746
|
* ============================================================================ */
|
|
717
747
|
|
|
718
748
|
static int sxs_step_attr_name(LeptrisSAXParser* p, int is_final) {
|
|
749
|
+
if (p->one_shot) {
|
|
750
|
+
/* Slice 1 (TODO.max-perf/2-3): single-feed input never
|
|
751
|
+
* splits the token — scan in place, materialize the name
|
|
752
|
+
* with ONE scratch copy; the carry path below costs a
|
|
753
|
+
* reserve+store call per name byte. */
|
|
754
|
+
if (p->pos >= p->end) {
|
|
755
|
+
return is_final ? (sxs_set_error(p, "Expected attribute name"), SAX_STEP_ERR)
|
|
756
|
+
: SAX_STEP_NEED_MORE;
|
|
757
|
+
}
|
|
758
|
+
if (!sxs_is_name_start(*p->pos)) {
|
|
759
|
+
sxs_set_error(p, "Expected attribute name");
|
|
760
|
+
return SAX_STEP_ERR;
|
|
761
|
+
}
|
|
762
|
+
p->pending_attr_line = p->line;
|
|
763
|
+
p->pending_attr_column = p->column;
|
|
764
|
+
const char* nb = p->pos;
|
|
765
|
+
while (p->pos < p->end && sxs_is_name_char(*p->pos)) {
|
|
766
|
+
sxs_pos_putc(p, *p->pos);
|
|
767
|
+
p->pos++;
|
|
768
|
+
}
|
|
769
|
+
const char* nm = sxs_scratch_append(p, nb, (size_t)(p->pos - nb));
|
|
770
|
+
if (!nm) { sxs_set_error(p, "out of memory"); return SAX_STEP_ERR; }
|
|
771
|
+
p->pending_attr_name = nm;
|
|
772
|
+
p->state = SAX_ST_ATTR_EQ;
|
|
773
|
+
return SAX_STEP_OK;
|
|
774
|
+
}
|
|
719
775
|
/* Diagnostic point for recoverable errors: the name's start
|
|
720
776
|
* (issue #647) — captured before any byte of the name is
|
|
721
777
|
* consumed, including across chunk-boundary resumes (the first
|
|
@@ -821,6 +877,26 @@ static int sxs_step_attr_value_quote(LeptrisSAXParser* p, int is_final) {
|
|
|
821
877
|
|
|
822
878
|
static int sxs_step_attr_value(LeptrisSAXParser* p, int is_final) {
|
|
823
879
|
char quote = p->carry[0];
|
|
880
|
+
const char* raw;
|
|
881
|
+
size_t raw_len;
|
|
882
|
+
if (p->one_shot) {
|
|
883
|
+
/* Slice 1: scan the value span in place (same position
|
|
884
|
+
* accounting as the carry loop); the span below is
|
|
885
|
+
* materialized with one scratch copy instead of a
|
|
886
|
+
* reserve+store call per value byte. */
|
|
887
|
+
raw = p->pos;
|
|
888
|
+
while (p->pos < p->end && *p->pos != quote) {
|
|
889
|
+
sxs_pos_putc(p, *p->pos);
|
|
890
|
+
p->pos++;
|
|
891
|
+
}
|
|
892
|
+
if (p->pos >= p->end) {
|
|
893
|
+
return is_final ? (sxs_set_error(p, "Unterminated attribute value"), SAX_STEP_ERR)
|
|
894
|
+
: SAX_STEP_NEED_MORE;
|
|
895
|
+
}
|
|
896
|
+
raw_len = (size_t)(p->pos - raw);
|
|
897
|
+
sxs_pos_putc(p, *p->pos);
|
|
898
|
+
p->pos++;
|
|
899
|
+
} else {
|
|
824
900
|
/* p->carry[1..] may already have value bytes if we resumed mid-value. */
|
|
825
901
|
while (p->pos < p->end && *p->pos != quote) {
|
|
826
902
|
if (sxs_carry_putc(p, *p->pos) < 0) {
|
|
@@ -838,14 +914,15 @@ static int sxs_step_attr_value(LeptrisSAXParser* p, int is_final) {
|
|
|
838
914
|
/* p->pos points at closing quote. */
|
|
839
915
|
sxs_pos_putc(p, *p->pos);
|
|
840
916
|
p->pos++;
|
|
841
|
-
|
|
917
|
+
raw = p->carry + 1;
|
|
918
|
+
raw_len = p->carry_len - 1;
|
|
919
|
+
}
|
|
920
|
+
/* Build value.
|
|
842
921
|
*
|
|
843
922
|
* XML 1.0 3.3.3: attribute values must be delivered with
|
|
844
923
|
* entity/character references expanded. Values without '&'
|
|
845
924
|
* (the common case) copy verbatim; values with '&' decode
|
|
846
925
|
* through a temporary buffer first. */
|
|
847
|
-
const char* raw = p->carry + 1;
|
|
848
|
-
size_t raw_len = p->carry_len - 1;
|
|
849
926
|
const char* value;
|
|
850
927
|
if (raw_len && memchr(raw, '&', raw_len)) {
|
|
851
928
|
LeptrisStringView sv = leptris_sv_from_ptr(raw, raw_len);
|
|
@@ -995,7 +1072,15 @@ static int sxs_step_elem_content(LeptrisSAXParser* p, int is_final) {
|
|
|
995
1072
|
|
|
996
1073
|
static int sxs_step_text(LeptrisSAXParser* p, int is_final) {
|
|
997
1074
|
const char* start = p->pos;
|
|
998
|
-
|
|
1075
|
+
/* memchr finds the span end at libc speeds; the loop form costs
|
|
1076
|
+
* a load+compare+branch per byte. Position accounting is
|
|
1077
|
+
* unaffected — this state never updates line/column per text
|
|
1078
|
+
* byte (tag states own the counters). */
|
|
1079
|
+
{
|
|
1080
|
+
const char* lt = (const char*)memchr(p->pos, '<',
|
|
1081
|
+
(size_t)(p->end - p->pos));
|
|
1082
|
+
p->pos = lt ? lt : p->end;
|
|
1083
|
+
}
|
|
999
1084
|
size_t len = (size_t)(p->pos - start);
|
|
1000
1085
|
|
|
1001
1086
|
if (p->carry_len == 0 && (len == 0 || !memchr(start, '&', len))) {
|
|
@@ -103,6 +103,42 @@ TEST(Digest, DropWsTextSkipsWhitespaceOnlyNodes) {
|
|
|
103
103
|
digest_of("<r><a/> <b/></r>", LEPTRIS_DIGEST_DEFAULT));
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/* #1200: ATTR_ORDER keeps document order in the hash — reordered
|
|
107
|
+
* attributes digest DIFFERENT, same-order documents stay equal,
|
|
108
|
+
* and the flag composes with DROP_WS_TEXT. The dedup (first-wins
|
|
109
|
+
* on the resolved key) still applies. */
|
|
110
|
+
TEST(Digest, AttrOrderSensitiveVariant) {
|
|
111
|
+
EXPECT_NE(digest_of("<r a='1' b='2'/>", LEPTRIS_DIGEST_ATTR_ORDER),
|
|
112
|
+
digest_of("<r b='2' a='1'/>", LEPTRIS_DIGEST_ATTR_ORDER));
|
|
113
|
+
EXPECT_EQ(digest_of("<r a='1' b='2'/>", LEPTRIS_DIGEST_ATTR_ORDER),
|
|
114
|
+
digest_of("<r a='1' b='2'/>", LEPTRIS_DIGEST_ATTR_ORDER));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
TEST(Digest, AttrOrderComposesWithDropWs) {
|
|
118
|
+
EXPECT_EQ(
|
|
119
|
+
digest_of("<r a='1' b='2'><a/><b/></r>",
|
|
120
|
+
(LeptrisDigestFlags)(LEPTRIS_DIGEST_ATTR_ORDER |
|
|
121
|
+
LEPTRIS_DIGEST_DROP_WS_TEXT)),
|
|
122
|
+
digest_of("<r a='1' b='2'><a/> <b/></r>",
|
|
123
|
+
(LeptrisDigestFlags)(LEPTRIS_DIGEST_ATTR_ORDER |
|
|
124
|
+
LEPTRIS_DIGEST_DROP_WS_TEXT)));
|
|
125
|
+
EXPECT_NE(
|
|
126
|
+
digest_of("<r a='1' b='2'><a/></r>",
|
|
127
|
+
(LeptrisDigestFlags)(LEPTRIS_DIGEST_ATTR_ORDER |
|
|
128
|
+
LEPTRIS_DIGEST_DROP_WS_TEXT)),
|
|
129
|
+
digest_of("<r b='2' a='1'><a/></r>",
|
|
130
|
+
(LeptrisDigestFlags)(LEPTRIS_DIGEST_ATTR_ORDER |
|
|
131
|
+
LEPTRIS_DIGEST_DROP_WS_TEXT)));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
TEST(Digest, AttrOrderRecursesIntoChildren) {
|
|
135
|
+
/* The child's attribute order participates too: the Merkle
|
|
136
|
+
* combine feeds each child's digest (with the flag) upward. */
|
|
137
|
+
EXPECT_NE(
|
|
138
|
+
digest_of("<r><c x='1' y='2'/></r>", LEPTRIS_DIGEST_ATTR_ORDER),
|
|
139
|
+
digest_of("<r><c y='2' x='1'/></r>", LEPTRIS_DIGEST_ATTR_ORDER));
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
/* Null contract: no node, no digest. */
|
|
107
143
|
TEST(Digest, NullNodeIsZero) {
|
|
108
144
|
EXPECT_EQ(leptris_node_digest(nullptr, LEPTRIS_DIGEST_DEFAULT), 0u);
|
|
@@ -32,6 +32,30 @@ TEST(DomBasics, EmptyDocumentRoundTrips) {
|
|
|
32
32
|
leptris_document_free(doc);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// #1197: the close-tag fast-path compare masked the wrong end of the
|
|
36
|
+
// 8-byte load on big-endian and failed every paired close tag. The
|
|
37
|
+
// unit corpus was self-closing-heavy and never caught it; sweep the
|
|
38
|
+
// compare across all mask lengths plus the wide-family report.
|
|
39
|
+
TEST(DomBasics, CloseTagCompareAcrossNameLengths) {
|
|
40
|
+
for (int len = 1; len <= 9; len++) {
|
|
41
|
+
std::string name(static_cast<size_t>(len), 'n');
|
|
42
|
+
std::string xml = "<" + name + "><" + name + "/></" + name + ">";
|
|
43
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
44
|
+
LeptrisDocument doc =
|
|
45
|
+
leptris_parse_string(xml.data(), xml.size(), &st);
|
|
46
|
+
ASSERT_NE(doc, nullptr)
|
|
47
|
+
<< "close-tag name of length " << len << " failed to parse";
|
|
48
|
+
leptris_document_free(doc);
|
|
49
|
+
}
|
|
50
|
+
std::string xml = "<r>";
|
|
51
|
+
for (int i = 1; i <= 80; i++) xml += "<n" + std::to_string(i) + "/>";
|
|
52
|
+
xml += "</r>";
|
|
53
|
+
LeptrisDocument doc =
|
|
54
|
+
leptris_parse_string(xml.data(), xml.size(), NULL);
|
|
55
|
+
ASSERT_NE(doc, nullptr) << "80-child family failed to parse";
|
|
56
|
+
leptris_document_free(doc);
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
TEST(DomBasics, DocumentCreateHasNoRoot) {
|
|
36
60
|
LeptrisDocument doc = leptris_document_create();
|
|
37
61
|
ASSERT_NE(doc, nullptr);
|
|
@@ -563,3 +563,46 @@ TEST(FreshDocumentContract, TruncatedLengthIsACleanParseError) {
|
|
|
563
563
|
EXPECT_EQ(doc, nullptr);
|
|
564
564
|
EXPECT_EQ(st, LEPTRIS_ERROR_PARSE);
|
|
565
565
|
}
|
|
566
|
+
|
|
567
|
+
// ---- #1200 part 2: recover diagnostics on the DOM parse -------
|
|
568
|
+
|
|
569
|
+
TEST(ParseDiag, DuplicateAttributeKeepsFirstAndReports) {
|
|
570
|
+
/* libxml2/SAX-lane parity: recover-class diagnostic on the DOM
|
|
571
|
+
* parse instead of silence. First definition wins (the digest's
|
|
572
|
+
* first-wins semantics made official). */
|
|
573
|
+
const char xml[] =
|
|
574
|
+
"<body lang=\"en\" xml:lang=\"en\" xml:lang=\"fr\"><div/></body>";
|
|
575
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
576
|
+
LeptrisDocument doc = leptris_parse_string(xml, strlen(xml), NULL);
|
|
577
|
+
ASSERT_NE(doc, nullptr);
|
|
578
|
+
|
|
579
|
+
EXPECT_EQ(leptris_document_parse_diag_count(doc), 1u);
|
|
580
|
+
LeptrisDiagKind kind = (LeptrisDiagKind)-1;
|
|
581
|
+
char msg[192] = {0};
|
|
582
|
+
ASSERT_EQ(leptris_document_parse_diag(doc, 0, &kind, msg,
|
|
583
|
+
sizeof(msg)), 1);
|
|
584
|
+
EXPECT_EQ(kind, LEPTRIS_DIAG_RECOVER);
|
|
585
|
+
EXPECT_STREQ(msg, "Attribute xml:lang redefined");
|
|
586
|
+
|
|
587
|
+
/* First value survived; the duplicate never entered the list. */
|
|
588
|
+
LeptrisElement root = leptris_document_root(doc);
|
|
589
|
+
ASSERT_NE(root, nullptr);
|
|
590
|
+
EXPECT_EQ(leptris_element_attribute_count(root), 2u);
|
|
591
|
+
EXPECT_STREQ(leptris_element_attribute(root, "xml:lang"), "en");
|
|
592
|
+
leptris_document_free(doc);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
TEST(ParseDiag, CleanDocumentHasNoDiags) {
|
|
596
|
+
const char xml[] = "<body lang=\"en\" xml:lang=\"en\"><div/></body>";
|
|
597
|
+
LeptrisDocument doc = leptris_parse_string(xml, strlen(xml), NULL);
|
|
598
|
+
ASSERT_NE(doc, nullptr);
|
|
599
|
+
EXPECT_EQ(leptris_document_parse_diag_count(doc), 0u);
|
|
600
|
+
LeptrisDiagKind kind;
|
|
601
|
+
char msg[64];
|
|
602
|
+
EXPECT_EQ(leptris_document_parse_diag(doc, 0, &kind, msg,
|
|
603
|
+
sizeof(msg)), 0);
|
|
604
|
+
EXPECT_EQ(leptris_document_parse_diag(NULL, 0, &kind, msg,
|
|
605
|
+
sizeof(msg)), 0);
|
|
606
|
+
EXPECT_EQ(leptris_document_parse_diag_count(NULL), 0u);
|
|
607
|
+
leptris_document_free(doc);
|
|
608
|
+
}
|
|
@@ -88,6 +88,38 @@ TEST(Pull, CommentsCdataPiAndEndDocument) {
|
|
|
88
88
|
EXPECT_EQ(end_doc, 1);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
TEST(Pull, EntityTextAndAttrValuesExpandInBufferMode) {
|
|
92
|
+
/* Buffer-mode (one-shot) pulls borrow event strings from the
|
|
93
|
+
* input/scratch — entities must still arrive EXPANDED, for both
|
|
94
|
+
* text and attribute values (borrow-safety pin for
|
|
95
|
+
* TODO.max-perf/2-3 slice 1). */
|
|
96
|
+
const char* xml =
|
|
97
|
+
"<r tag='r&d' code='A'>a & b <c></r>";
|
|
98
|
+
LeptrisPullParser p = leptris_pull_new(xml, strlen(xml));
|
|
99
|
+
ASSERT_NE(p, nullptr);
|
|
100
|
+
const LeptrisPullEvent* ev = leptris_pull_next(p);
|
|
101
|
+
ASSERT_NE(ev, nullptr);
|
|
102
|
+
ASSERT_EQ(ev->type, LEPTRIS_PULL_START_ELEMENT);
|
|
103
|
+
EXPECT_EQ(leptris_pull_attr_count(p), 2u);
|
|
104
|
+
EXPECT_STREQ(leptris_pull_attr_name(p, 0), "tag");
|
|
105
|
+
EXPECT_STREQ(leptris_pull_attr_value(p, 0), "r&d");
|
|
106
|
+
EXPECT_STREQ(leptris_pull_attr_name(p, 1), "code");
|
|
107
|
+
EXPECT_STREQ(leptris_pull_attr_value(p, 1), "A");
|
|
108
|
+
ev = leptris_pull_next(p);
|
|
109
|
+
ASSERT_NE(ev, nullptr);
|
|
110
|
+
ASSERT_EQ(ev->type, LEPTRIS_PULL_TEXT);
|
|
111
|
+
ASSERT_NE(ev->text, nullptr);
|
|
112
|
+
EXPECT_STREQ(ev->text, "a & b <c>");
|
|
113
|
+
ev = leptris_pull_next(p);
|
|
114
|
+
ASSERT_NE(ev, nullptr);
|
|
115
|
+
ASSERT_EQ(ev->type, LEPTRIS_PULL_END_ELEMENT);
|
|
116
|
+
EXPECT_STREQ(ev->name, "r");
|
|
117
|
+
ev = leptris_pull_next(p);
|
|
118
|
+
ASSERT_NE(ev, nullptr);
|
|
119
|
+
EXPECT_EQ(ev->type, LEPTRIS_PULL_END_DOCUMENT);
|
|
120
|
+
leptris_pull_free(p);
|
|
121
|
+
}
|
|
122
|
+
|
|
91
123
|
TEST(Pull, MalformedInputYieldsErrorEvent) {
|
|
92
124
|
LeptrisPullParser p = leptris_pull_new("<a><b></a>", 10);
|
|
93
125
|
ASSERT_NE(p, nullptr);
|
|
@@ -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.207",
|
|
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",
|