fix(organize): find XMP attributes in one pass over the packet

Each candidate attribute match searched the whole packet before it for the
last < and >, so a long text node repeating the attribute name cost time
quadratic in the packet size (up to the 16 MiB decode cap). The scan now
keeps the last < and > as it moves forward.
This commit is contained in:
aiongg
2026-10-22 21:16:21 +00:00
parent 1f6ce5ecc7
commit cb1ddc899b
+28 -1
View File
@@ -172,11 +172,24 @@ fn element_span(xmp: &str, name: &str) -> Option<(usize, usize)> {
/// start and end of its value, and its quote character. Only matches inside a start tag.
fn attribute_span(xmp: &str, name: &str) -> Option<(usize, usize, usize, char)> {
let mut from = 0;
// The last `<` and `>` before the match, found by one pass over the packet rather than by
// searching back from every match.
let (mut scanned, mut last_open, mut last_close) = (0, None, None);
while let Some(i) = xmp.get(from..).and_then(|s| s.find(name)).map(|i| i + from) {
from = i + name.len();
let before = xmp.get(..i)?;
let gap = xmp.get(scanned..i)?;
#[cfg(test)]
crate::work(gap.len());
if let Some(at) = gap.rfind('<') {
last_open = Some(scanned + at);
}
if let Some(at) = gap.rfind('>') {
last_close = Some(scanned + at);
}
scanned = i;
// Preceded by whitespace, and inside a tag (the last `<` comes after the last `>`).
if !before.ends_with(char::is_whitespace) || before.rfind('<') < before.rfind('>') || before.rfind('<').is_none() {
if !before.ends_with(char::is_whitespace) || last_open < last_close || last_open.is_none() {
continue;
}
let rest = xmp.get(from..)?;
@@ -269,4 +282,18 @@ mod tests {
}
}
}
#[test]
fn attribute_search_scans_the_packet_once() {
// Thousands of attribute-like words in one text node: none is inside a tag.
let packet = format!("<x:xmpmeta><rdf:RDF><rdf:Description>{}</rdf:Description></rdf:RDF></x:xmpmeta>", " rdf:about".repeat(5_000));
crate::take_work();
assert_eq!(attribute_span(&packet, "rdf:about"), None);
let work = crate::take_work();
assert!(work <= 2 * packet.len(), "{work} bytes scanned in a {}-byte packet", packet.len());
// A real attribute after them is still found.
let packet = packet.replace("</rdf:Description>", "</rdf:Description><rdf:Description rdf:about='u'/>");
let (_, s, e, q) = attribute_span(&packet, "rdf:about").unwrap();
assert_eq!((packet.get(s..e), q), (Some("u"), '\''));
}
}