@file-viewer/renderer-chm 3.0.1
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.
- package/LICENSE +196 -0
- package/README.en.md +13 -0
- package/README.md +13 -0
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/dist/chm.d.ts +2 -0
- package/dist/chm.js +593 -0
- package/dist/chm.worker.d.ts +1 -0
- package/dist/chm.worker.js +2 -0
- package/dist/chm_wasm.js +583 -0
- package/dist/chm_wasm_bg.wasm +0 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -0
- package/dist/model.d.ts +78 -0
- package/dist/model.js +125 -0
- package/dist/security.d.ts +33 -0
- package/dist/security.js +850 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +26 -0
- package/dist/workerClient.d.ts +32 -0
- package/dist/workerClient.js +157 -0
- package/dist/workerProtocol.d.ts +70 -0
- package/dist/workerProtocol.js +1 -0
- package/file-viewer.capability.json +38 -0
- package/package.json +92 -0
- package/rust/Cargo.lock +247 -0
- package/rust/Cargo.toml +29 -0
- package/rust/LICENSE +196 -0
- package/rust/NOTICE.md +47 -0
- package/rust/THIRD_PARTY_LICENSES.md +365 -0
- package/rust/src/chm.rs +702 -0
- package/rust/src/core.rs +745 -0
- package/rust/src/error.rs +64 -0
- package/rust/src/lib.rs +104 -0
- package/rust/src/lzx.rs +632 -0
- package/rust/src/metadata.rs +839 -0
- package/rust/src/sitemap.rs +442 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
//! Bounded parser for legacy HTML Help sitemap documents (`.hhc` / `.hhk`).
|
|
2
|
+
//!
|
|
3
|
+
//! These files are HTML-shaped rather than standards-compliant HTML. A small tolerant
|
|
4
|
+
//! tokenizer is both smaller and safer in WASM than executing them in a DOM.
|
|
5
|
+
|
|
6
|
+
use encoding_rs::Encoding;
|
|
7
|
+
use serde::Serialize;
|
|
8
|
+
|
|
9
|
+
use crate::error::{CoreError, CoreResult};
|
|
10
|
+
|
|
11
|
+
const MAX_SITEMAP_FIELD_BYTES: usize = 64 * 1024;
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
|
14
|
+
#[serde(rename_all = "camelCase")]
|
|
15
|
+
pub struct SitemapNode {
|
|
16
|
+
pub name: String,
|
|
17
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
18
|
+
pub local: Option<String>,
|
|
19
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
20
|
+
pub merge: Option<String>,
|
|
21
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
22
|
+
pub see_also: Option<String>,
|
|
23
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
24
|
+
pub image_number: Option<i32>,
|
|
25
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
26
|
+
pub children: Vec<SitemapNode>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[derive(Default)]
|
|
30
|
+
struct PendingNode {
|
|
31
|
+
name: String,
|
|
32
|
+
local: Option<String>,
|
|
33
|
+
merge: Option<String>,
|
|
34
|
+
see_also: Option<String>,
|
|
35
|
+
image_number: Option<i32>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl PendingNode {
|
|
39
|
+
fn set(&mut self, name: &str, value: String) {
|
|
40
|
+
if name.eq_ignore_ascii_case("name") && self.name.is_empty() {
|
|
41
|
+
self.name = value;
|
|
42
|
+
} else if name.eq_ignore_ascii_case("local") && self.local.is_none() {
|
|
43
|
+
self.local = nonempty(value);
|
|
44
|
+
} else if name.eq_ignore_ascii_case("merge") && self.merge.is_none() {
|
|
45
|
+
self.merge = nonempty(value);
|
|
46
|
+
} else if name.eq_ignore_ascii_case("see also") && self.see_also.is_none() {
|
|
47
|
+
self.see_also = nonempty(value);
|
|
48
|
+
} else if (name.eq_ignore_ascii_case("imagenumber")
|
|
49
|
+
|| name.eq_ignore_ascii_case("image number"))
|
|
50
|
+
&& self.image_number.is_none()
|
|
51
|
+
{
|
|
52
|
+
self.image_number = value.trim().parse().ok();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn finish(self) -> Option<SitemapNode> {
|
|
57
|
+
if self.name.is_empty()
|
|
58
|
+
&& self.local.is_none()
|
|
59
|
+
&& self.merge.is_none()
|
|
60
|
+
&& self.see_also.is_none()
|
|
61
|
+
{
|
|
62
|
+
return None;
|
|
63
|
+
}
|
|
64
|
+
Some(SitemapNode {
|
|
65
|
+
name: self.name,
|
|
66
|
+
local: self.local,
|
|
67
|
+
merge: self.merge,
|
|
68
|
+
see_also: self.see_also,
|
|
69
|
+
image_number: self.image_number,
|
|
70
|
+
children: Vec::new(),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn nonempty(value: String) -> Option<String> {
|
|
76
|
+
let value = value.trim().to_owned();
|
|
77
|
+
(!value.is_empty()).then_some(value)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[derive(Clone, Copy, Default, PartialEq, Eq)]
|
|
81
|
+
enum TagKind {
|
|
82
|
+
Ul,
|
|
83
|
+
Object,
|
|
84
|
+
Param,
|
|
85
|
+
#[default]
|
|
86
|
+
Other,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[derive(Default)]
|
|
90
|
+
struct Tag {
|
|
91
|
+
kind: TagKind,
|
|
92
|
+
closing: bool,
|
|
93
|
+
object_type: Option<String>,
|
|
94
|
+
param_name: Option<String>,
|
|
95
|
+
param_value: Option<String>,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Decode an HTML Help text stream and report the selected encoding label.
|
|
99
|
+
pub fn decode_document(data: &[u8], fallback: &'static Encoding) -> (String, String) {
|
|
100
|
+
if data.starts_with(&[0xff, 0xfe]) {
|
|
101
|
+
let units: Vec<u16> = data[2..]
|
|
102
|
+
.chunks_exact(2)
|
|
103
|
+
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
|
|
104
|
+
.collect();
|
|
105
|
+
return (String::from_utf16_lossy(&units), "UTF-16LE".into());
|
|
106
|
+
}
|
|
107
|
+
if data.starts_with(&[0xfe, 0xff]) {
|
|
108
|
+
let units: Vec<u16> = data[2..]
|
|
109
|
+
.chunks_exact(2)
|
|
110
|
+
.map(|pair| u16::from_be_bytes([pair[0], pair[1]]))
|
|
111
|
+
.collect();
|
|
112
|
+
return (String::from_utf16_lossy(&units), "UTF-16BE".into());
|
|
113
|
+
}
|
|
114
|
+
let label = sniff_charset(data);
|
|
115
|
+
let encoding = label
|
|
116
|
+
.as_deref()
|
|
117
|
+
.and_then(|value| Encoding::for_label(value.as_bytes()))
|
|
118
|
+
.unwrap_or(fallback);
|
|
119
|
+
let offset = usize::from(data.starts_with(&[0xef, 0xbb, 0xbf])) * 3;
|
|
120
|
+
let (decoded, _, _) = encoding.decode(&data[offset..]);
|
|
121
|
+
(decoded.into_owned(), encoding.name().to_owned())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn sniff_charset(data: &[u8]) -> Option<String> {
|
|
125
|
+
let ascii: String = data
|
|
126
|
+
.iter()
|
|
127
|
+
.take(8192)
|
|
128
|
+
.map(|byte| {
|
|
129
|
+
if byte.is_ascii() {
|
|
130
|
+
byte.to_ascii_lowercase() as char
|
|
131
|
+
} else {
|
|
132
|
+
' '
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.collect();
|
|
136
|
+
let start = ascii.find("charset")? + "charset".len();
|
|
137
|
+
let bytes = ascii.as_bytes();
|
|
138
|
+
let mut pos = start;
|
|
139
|
+
while pos < bytes.len()
|
|
140
|
+
&& (bytes[pos].is_ascii_whitespace() || matches!(bytes[pos], b'=' | b'\'' | b'"'))
|
|
141
|
+
{
|
|
142
|
+
pos += 1;
|
|
143
|
+
}
|
|
144
|
+
let end = (pos..bytes.len())
|
|
145
|
+
.find(|&index| !matches!(bytes[index], b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.'))
|
|
146
|
+
.unwrap_or(bytes.len());
|
|
147
|
+
(pos < end).then(|| ascii[pos..end].to_owned())
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Parse a decoded sitemap into a bounded tree.
|
|
151
|
+
pub fn parse_sitemap(
|
|
152
|
+
data: &[u8],
|
|
153
|
+
fallback: &'static Encoding,
|
|
154
|
+
max_nodes: usize,
|
|
155
|
+
max_depth: usize,
|
|
156
|
+
) -> CoreResult<(Vec<SitemapNode>, String)> {
|
|
157
|
+
let (text, encoding) = decode_document(data, fallback);
|
|
158
|
+
let bytes = text.as_bytes();
|
|
159
|
+
let mut levels: Vec<Vec<SitemapNode>> = vec![Vec::new()];
|
|
160
|
+
let mut pending = PendingNode::default();
|
|
161
|
+
let mut sitemap_object = false;
|
|
162
|
+
let mut node_count = 0usize;
|
|
163
|
+
let mut pos = 0usize;
|
|
164
|
+
|
|
165
|
+
while pos < bytes.len() {
|
|
166
|
+
let Some(relative) = bytes[pos..].iter().position(|&byte| byte == b'<') else {
|
|
167
|
+
break;
|
|
168
|
+
};
|
|
169
|
+
pos += relative;
|
|
170
|
+
if bytes[pos..].starts_with(b"<!--") {
|
|
171
|
+
pos = bytes[pos + 4..]
|
|
172
|
+
.windows(3)
|
|
173
|
+
.position(|window| window == b"-->")
|
|
174
|
+
.map_or(bytes.len(), |end| pos + 4 + end + 3);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
let Some(end) = find_tag_end(bytes, pos + 1) else {
|
|
178
|
+
break;
|
|
179
|
+
};
|
|
180
|
+
let tag = parse_tag(&text[pos + 1..end])?;
|
|
181
|
+
pos = end + 1;
|
|
182
|
+
|
|
183
|
+
match (tag.closing, tag.kind) {
|
|
184
|
+
(false, TagKind::Ul) => {
|
|
185
|
+
if levels.len() >= max_depth {
|
|
186
|
+
return Err(CoreError::Limit(format!(
|
|
187
|
+
"sitemap nesting exceeds {max_depth}"
|
|
188
|
+
)));
|
|
189
|
+
}
|
|
190
|
+
levels.push(Vec::new());
|
|
191
|
+
}
|
|
192
|
+
(true, TagKind::Ul) => close_level(&mut levels),
|
|
193
|
+
(false, TagKind::Object) => {
|
|
194
|
+
sitemap_object = tag
|
|
195
|
+
.object_type
|
|
196
|
+
.as_deref()
|
|
197
|
+
.is_some_and(|value| value.eq_ignore_ascii_case("text/sitemap"));
|
|
198
|
+
if sitemap_object {
|
|
199
|
+
pending = PendingNode::default();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
(false, TagKind::Param) if sitemap_object => {
|
|
203
|
+
if let (Some(name), Some(value)) = (tag.param_name, tag.param_value) {
|
|
204
|
+
pending.set(&name, decode_entities(&value));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
(true, TagKind::Object) if sitemap_object => {
|
|
208
|
+
if let Some(node) = std::mem::take(&mut pending).finish() {
|
|
209
|
+
node_count += 1;
|
|
210
|
+
if node_count > max_nodes {
|
|
211
|
+
return Err(CoreError::Limit(format!(
|
|
212
|
+
"sitemap contains more than {max_nodes} nodes"
|
|
213
|
+
)));
|
|
214
|
+
}
|
|
215
|
+
levels.last_mut().expect("root level exists").push(node);
|
|
216
|
+
}
|
|
217
|
+
sitemap_object = false;
|
|
218
|
+
}
|
|
219
|
+
_ => {}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
while levels.len() > 1 {
|
|
223
|
+
close_level(&mut levels);
|
|
224
|
+
}
|
|
225
|
+
Ok((levels.pop().unwrap_or_default(), encoding))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn close_level(levels: &mut Vec<Vec<SitemapNode>>) {
|
|
229
|
+
if levels.len() <= 1 {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
let children = levels.pop().unwrap_or_default();
|
|
233
|
+
let parent_level = levels.last_mut().expect("root level exists");
|
|
234
|
+
if let Some(parent) = parent_level.last_mut() {
|
|
235
|
+
parent.children.extend(children);
|
|
236
|
+
} else {
|
|
237
|
+
parent_level.extend(children);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn find_tag_end(bytes: &[u8], mut pos: usize) -> Option<usize> {
|
|
242
|
+
let mut quote = None;
|
|
243
|
+
while pos < bytes.len() {
|
|
244
|
+
match (quote, bytes[pos]) {
|
|
245
|
+
(None, b'\'' | b'"') => quote = Some(bytes[pos]),
|
|
246
|
+
(Some(expected), actual) if expected == actual => quote = None,
|
|
247
|
+
(None, b'>') => return Some(pos),
|
|
248
|
+
_ => {}
|
|
249
|
+
}
|
|
250
|
+
pos += 1;
|
|
251
|
+
}
|
|
252
|
+
None
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn parse_tag(source: &str) -> CoreResult<Tag> {
|
|
256
|
+
let bytes = source.as_bytes();
|
|
257
|
+
let mut tag = Tag::default();
|
|
258
|
+
let mut pos = 0usize;
|
|
259
|
+
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
|
|
260
|
+
pos += 1;
|
|
261
|
+
}
|
|
262
|
+
if bytes.get(pos) == Some(&b'/') {
|
|
263
|
+
tag.closing = true;
|
|
264
|
+
pos += 1;
|
|
265
|
+
}
|
|
266
|
+
let name_start = pos;
|
|
267
|
+
while pos < bytes.len()
|
|
268
|
+
&& !bytes[pos].is_ascii_whitespace()
|
|
269
|
+
&& !matches!(bytes[pos], b'/' | b'>')
|
|
270
|
+
{
|
|
271
|
+
pos += 1;
|
|
272
|
+
}
|
|
273
|
+
let name = &source[name_start..pos];
|
|
274
|
+
tag.kind = if name.eq_ignore_ascii_case("ul") {
|
|
275
|
+
TagKind::Ul
|
|
276
|
+
} else if name.eq_ignore_ascii_case("object") {
|
|
277
|
+
TagKind::Object
|
|
278
|
+
} else if name.eq_ignore_ascii_case("param") {
|
|
279
|
+
TagKind::Param
|
|
280
|
+
} else {
|
|
281
|
+
TagKind::Other
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
while pos < bytes.len() {
|
|
285
|
+
while pos < bytes.len() && (bytes[pos].is_ascii_whitespace() || bytes[pos] == b'/') {
|
|
286
|
+
pos += 1;
|
|
287
|
+
}
|
|
288
|
+
let key_start = pos;
|
|
289
|
+
while pos < bytes.len()
|
|
290
|
+
&& !bytes[pos].is_ascii_whitespace()
|
|
291
|
+
&& !matches!(bytes[pos], b'=' | b'/')
|
|
292
|
+
{
|
|
293
|
+
pos += 1;
|
|
294
|
+
}
|
|
295
|
+
if key_start == pos {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
let key = &source[key_start..pos];
|
|
299
|
+
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
|
|
300
|
+
pos += 1;
|
|
301
|
+
}
|
|
302
|
+
let mut value = "";
|
|
303
|
+
if bytes.get(pos) == Some(&b'=') {
|
|
304
|
+
pos += 1;
|
|
305
|
+
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
|
|
306
|
+
pos += 1;
|
|
307
|
+
}
|
|
308
|
+
if matches!(bytes.get(pos), Some(b'\'' | b'"')) {
|
|
309
|
+
let quote = bytes[pos];
|
|
310
|
+
pos += 1;
|
|
311
|
+
let start = pos;
|
|
312
|
+
while pos < bytes.len() && bytes[pos] != quote {
|
|
313
|
+
pos += 1;
|
|
314
|
+
}
|
|
315
|
+
value = &source[start..pos];
|
|
316
|
+
pos += usize::from(pos < bytes.len());
|
|
317
|
+
} else {
|
|
318
|
+
let start = pos;
|
|
319
|
+
while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() && bytes[pos] != b'/' {
|
|
320
|
+
pos += 1;
|
|
321
|
+
}
|
|
322
|
+
value = &source[start..pos];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
let destination = match tag.kind {
|
|
326
|
+
TagKind::Object if key.eq_ignore_ascii_case("type") => &mut tag.object_type,
|
|
327
|
+
TagKind::Param if key.eq_ignore_ascii_case("name") => &mut tag.param_name,
|
|
328
|
+
TagKind::Param if key.eq_ignore_ascii_case("value") => &mut tag.param_value,
|
|
329
|
+
_ => continue,
|
|
330
|
+
};
|
|
331
|
+
if destination.is_none() {
|
|
332
|
+
if value.len() > MAX_SITEMAP_FIELD_BYTES {
|
|
333
|
+
return Err(CoreError::Limit(format!(
|
|
334
|
+
"sitemap field exceeds {MAX_SITEMAP_FIELD_BYTES} bytes"
|
|
335
|
+
)));
|
|
336
|
+
}
|
|
337
|
+
*destination = Some(value.to_owned());
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
Ok(tag)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
fn decode_entities(value: &str) -> String {
|
|
344
|
+
let mut output = String::with_capacity(value.len());
|
|
345
|
+
let mut rest = value;
|
|
346
|
+
while let Some(amp) = rest.find('&') {
|
|
347
|
+
output.push_str(&rest[..amp]);
|
|
348
|
+
rest = &rest[amp..];
|
|
349
|
+
let scan_end = rest.len().min(13);
|
|
350
|
+
let Some(semi) = rest.as_bytes()[1..scan_end]
|
|
351
|
+
.iter()
|
|
352
|
+
.position(|&byte| byte == b';')
|
|
353
|
+
.map(|index| index + 1)
|
|
354
|
+
else {
|
|
355
|
+
output.push('&');
|
|
356
|
+
rest = &rest[1..];
|
|
357
|
+
continue;
|
|
358
|
+
};
|
|
359
|
+
let entity = &rest[1..semi];
|
|
360
|
+
let decoded = match entity {
|
|
361
|
+
"amp" => Some('&'),
|
|
362
|
+
"lt" => Some('<'),
|
|
363
|
+
"gt" => Some('>'),
|
|
364
|
+
"quot" => Some('"'),
|
|
365
|
+
"apos" => Some('\''),
|
|
366
|
+
"nbsp" => Some(' '),
|
|
367
|
+
_ if entity.starts_with("#x") || entity.starts_with("#X") => {
|
|
368
|
+
u32::from_str_radix(&entity[2..], 16)
|
|
369
|
+
.ok()
|
|
370
|
+
.and_then(char::from_u32)
|
|
371
|
+
}
|
|
372
|
+
_ if entity.starts_with('#') => entity[1..].parse().ok().and_then(char::from_u32),
|
|
373
|
+
_ => None,
|
|
374
|
+
};
|
|
375
|
+
if let Some(character) = decoded {
|
|
376
|
+
output.push(character);
|
|
377
|
+
} else {
|
|
378
|
+
output.push_str(&rest[..=semi]);
|
|
379
|
+
}
|
|
380
|
+
rest = &rest[semi + 1..];
|
|
381
|
+
}
|
|
382
|
+
output.push_str(rest);
|
|
383
|
+
output
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#[cfg(test)]
|
|
387
|
+
mod tests {
|
|
388
|
+
use super::*;
|
|
389
|
+
use encoding_rs::UTF_8;
|
|
390
|
+
|
|
391
|
+
#[test]
|
|
392
|
+
fn parses_nested_contents_without_running_html() {
|
|
393
|
+
let source = br#"<UL><LI><OBJECT type='text/sitemap'><param name='Name' value='Intro & Setup'><param name='Local' value='intro.htm'></OBJECT><UL><LI><OBJECT type='text/sitemap'><param name='Name' value='Child'><param name='Local' value='child.htm'></OBJECT></UL></UL>"#;
|
|
394
|
+
let (nodes, encoding) = parse_sitemap(source, UTF_8, 20, 8).unwrap();
|
|
395
|
+
assert_eq!(encoding, "UTF-8");
|
|
396
|
+
assert_eq!(nodes[0].name, "Intro & Setup");
|
|
397
|
+
assert_eq!(nodes[0].children[0].local.as_deref(), Some("child.htm"));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#[test]
|
|
401
|
+
fn rejects_pathological_nesting() {
|
|
402
|
+
let source = b"<ul><ul><ul><ul>";
|
|
403
|
+
assert!(matches!(
|
|
404
|
+
parse_sitemap(source, UTF_8, 10, 3),
|
|
405
|
+
Err(CoreError::Limit(_))
|
|
406
|
+
));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#[test]
|
|
410
|
+
fn ignores_many_irrelevant_attributes_without_collecting_them() {
|
|
411
|
+
let mut source = String::from("<object type='text/sitemap' ");
|
|
412
|
+
source.push_str(&"ignored ".repeat(50_000));
|
|
413
|
+
source.push_str("><param name='Name' value='Bounded'></object>");
|
|
414
|
+
let (nodes, _) = parse_sitemap(source.as_bytes(), UTF_8, 10, 4).unwrap();
|
|
415
|
+
assert_eq!(nodes.len(), 1);
|
|
416
|
+
assert_eq!(nodes[0].name, "Bounded");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn rejects_oversized_relevant_attribute() {
|
|
421
|
+
let source = format!(
|
|
422
|
+
"<object type='text/sitemap'><param name='Name' value='{}'></object>",
|
|
423
|
+
"x".repeat(MAX_SITEMAP_FIELD_BYTES + 1)
|
|
424
|
+
);
|
|
425
|
+
assert!(matches!(
|
|
426
|
+
parse_sitemap(source.as_bytes(), UTF_8, 10, 4),
|
|
427
|
+
Err(CoreError::Limit(_))
|
|
428
|
+
));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#[test]
|
|
432
|
+
fn decodes_ampersand_bomb_with_bounded_entity_scan() {
|
|
433
|
+
let source = "&".repeat(MAX_SITEMAP_FIELD_BYTES);
|
|
434
|
+
let started = std::time::Instant::now();
|
|
435
|
+
assert_eq!(decode_entities(&source), source);
|
|
436
|
+
assert!(started.elapsed() < std::time::Duration::from_millis(500));
|
|
437
|
+
assert_eq!(
|
|
438
|
+
decode_entities("汉¬-an-entity 文&字"),
|
|
439
|
+
"汉¬-an-entity 文&字"
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|