Compare commits
1
Commits
db19d08257
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e1cf20800 |
+45
-1
@@ -122,7 +122,15 @@ pub fn decode_entities(s: &str) -> String {
|
|||||||
while let Some(i) = rest.find('&') {
|
while let Some(i) = rest.find('&') {
|
||||||
out.push_str(&rest[..i]);
|
out.push_str(&rest[..i]);
|
||||||
let tail = &rest[i..];
|
let tail = &rest[i..];
|
||||||
let Some(end) = tail[..tail.len().min(12)].find(';') else {
|
// Entity names are ASCII, but malformed input is not necessarily so.
|
||||||
|
// Search the bounded byte window for the ASCII delimiter instead of
|
||||||
|
// slicing at byte 12, which may fall in the middle of a UTF-8 scalar.
|
||||||
|
let Some(end) = tail
|
||||||
|
.as_bytes()
|
||||||
|
.iter()
|
||||||
|
.take(12)
|
||||||
|
.position(|&byte| byte == b';')
|
||||||
|
else {
|
||||||
out.push('&');
|
out.push('&');
|
||||||
rest = &tail[1..];
|
rest = &tail[1..];
|
||||||
continue;
|
continue;
|
||||||
@@ -158,6 +166,20 @@ pub fn decode_entities(s: &str) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prefix of `s` that fits within `max_bytes` without splitting a UTF-8
|
||||||
|
/// character.
|
||||||
|
///
|
||||||
|
/// This is intended for byte-budgeted logs and excerpts. The returned string
|
||||||
|
/// may be shorter than the limit by up to three bytes.
|
||||||
|
pub fn truncate_utf8(s: &str, max_bytes: usize) -> &str {
|
||||||
|
let mut end = max_bytes.min(s.len());
|
||||||
|
while !s.is_char_boundary(end) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
// `end` is explicitly adjusted to a character boundary above.
|
||||||
|
s.get(..end).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Escape a string for use inside a double-quoted XML attribute.
|
/// Escape a string for use inside a double-quoted XML attribute.
|
||||||
pub fn attr_escape(s: &str) -> String {
|
pub fn attr_escape(s: &str) -> String {
|
||||||
let mut out = String::with_capacity(s.len());
|
let mut out = String::with_capacity(s.len());
|
||||||
@@ -315,6 +337,28 @@ mod tests {
|
|||||||
assert_eq!(decode_entities("&unknown; &"), "&unknown; &");
|
assert_eq!(decode_entities("&unknown; &"), "&unknown; &");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn entity_decoding_handles_unicode_at_the_scan_boundary() {
|
||||||
|
// The curly apostrophe begins at byte 11 after `&`. The bounded entity
|
||||||
|
// scan must leave malformed/non-entity text alone rather than slicing
|
||||||
|
// through the apostrophe and panicking.
|
||||||
|
let input = "&abcdefghij’ rest";
|
||||||
|
assert_eq!(decode_entities(input), input);
|
||||||
|
|
||||||
|
// A semicolon after non-ASCII text is likewise safe and remains
|
||||||
|
// unchanged because it is not one of the entities we decode.
|
||||||
|
assert_eq!(decode_entities("&é;"), "&é;");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn utf8_truncation_respects_byte_limits_and_character_boundaries() {
|
||||||
|
assert_eq!(truncate_utf8("abcdef", 4), "abcd");
|
||||||
|
assert_eq!(truncate_utf8("ab’cd", 4), "ab");
|
||||||
|
assert_eq!(truncate_utf8("ab’cd", 5), "ab’");
|
||||||
|
assert_eq!(truncate_utf8("éclair", 0), "");
|
||||||
|
assert_eq!(truncate_utf8("éclair", usize::MAX), "éclair");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn escaping_is_the_inverse_that_output_needs() {
|
fn escaping_is_the_inverse_that_output_needs() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
//! Candidates are judged by *shape*, never by publisher: a string with braces,
|
//! Candidates are judged by *shape*, never by publisher: a string with braces,
|
||||||
//! whitespace or quotes in it cannot resolve, whoever wrote it.
|
//! whitespace or quotes in it cannot resolve, whoever wrote it.
|
||||||
|
|
||||||
use crate::html::{html_to_text, parse_attrs, tag_end, tag_name};
|
use crate::html::{html_to_text, parse_attrs, tag_end, tag_name, truncate_utf8};
|
||||||
|
|
||||||
/// Elements that readability deletes outright, and which a page may nevertheless
|
/// Elements that readability deletes outright, and which a page may nevertheless
|
||||||
/// have wrapped around an image (lightbox triggers, mostly).
|
/// have wrapped around an image (lightbox triggers, mostly).
|
||||||
@@ -181,7 +181,7 @@ pub fn normalize_img_tags(html: &str) -> String {
|
|||||||
}
|
}
|
||||||
out.push_str("/>");
|
out.push_str("/>");
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!(tag = %&raw[..raw.len().min(120)], "dropping unusable img");
|
tracing::debug!(tag = %truncate_utf8(raw, 120), "dropping unusable img");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => out.push_str(raw),
|
_ => out.push_str(raw),
|
||||||
|
|||||||
Reference in New Issue
Block a user