diff --git a/crates/codecs/src/resolution.rs b/crates/codecs/src/resolution.rs index cb86001ac9..a787cacbc3 100644 --- a/crates/codecs/src/resolution.rs +++ b/crates/codecs/src/resolution.rs @@ -429,8 +429,14 @@ fn xmp_values(xmp: &str, key: &str) -> Vec<(usize, usize)> { continue; }; let start = xmp.len() - value.len(); - let Some(len) = value.find(close) else { continue }; + // Advance past this value before searching for the next key. Without this, `from` + // only moved past the key text, so property-like text repeated inside an unrelated + // attribute re-ran this full-suffix `find(close)` for every occurrence, which is + // quadratic in the packet size. A missing close means the value is unterminated, so + // no later value can parse either; stop scanning in that case. + let Some(len) = value.find(close) else { break }; out.push((start, start + len)); + from = start + len + close.len_utf8(); } out } diff --git a/crates/codecs/tests/resolution.rs b/crates/codecs/tests/resolution.rs index 15ddc95749..28c8203c96 100644 --- a/crates/codecs/tests/resolution.rs +++ b/crates/codecs/tests/resolution.rs @@ -284,6 +284,17 @@ fn truncated_metadata_never_panics_and_falls_back() { assert_eq!(xmp_resolution(x), None, "{x}"); let _ = xmp_with_resolution(x, (1.0, 1.0)); } + + // Property-like text repeated inside the packet must not trigger a full-suffix rescan + // per occurrence (it used to be quadratic). With the cursor advancing past each located + // value, this completes well under a second even with many decoys; a timeout here would + // indicate the quadratic scan returned. A clean packet still extracts correctly. + let start = std::time::Instant::now(); + let decoys = " tiff:ResolutionUnit>".repeat(50_000); + let _ = xmp_resolution(&decoys); + assert!(start.elapsed() < std::time::Duration::from_secs(2), "xmp scan took too long: {:?}", start.elapsed()); + let clean = "300/1300/12"; + near(xmp_resolution(clean), 300.0); } proptest! {