From 3867e870362874802984ef7a7ab6f7d4114363e9 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:55:57 +0000 Subject: [PATCH] Fix quadratic PostScript search in the EPS importer Fixes #3582 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- Cargo.lock | 1 + Cargo.toml | 2 ++ crates/eps/Cargo.toml | 2 ++ crates/eps/src/import/interp.rs | 1 + crates/eps/src/import/lex.rs | 2 +- crates/eps/src/import/tests.rs | 9 +++++++++ 6 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 193dc0946..7c6755fa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4767,6 +4767,7 @@ dependencies = [ "hayro-ccitt", "image", "kurbo 0.13.1", + "memchr", "ruzstd", "serde_json", "vectorcraft-brush", diff --git a/Cargo.toml b/Cargo.toml index e6efb9342..c6a4b4521 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,8 @@ ruzstd = { version = "0.9.1", default-features = false, features = ["std"] } # CCITT Group 3 and Group 4 decoder (ITU-T T.4 and T.6): EPS images read through # `CCITTFaxDecode`. The PDF reader's hayro-syntax uses the same version. hayro-ccitt = "0.3" +# Substring search for the EPS reader's PostScript `search`. +memchr = "2" jpeg-encoder = { version = "0.6", default-features = false, features = ["std"] } gif = { version = "0.14", default-features = false, features = ["std", "raii_no_panic"] } # CMYK image samples (vectorcraft-doc's cmyk module): the image crate's own TIFF and JPEG diff --git a/crates/eps/Cargo.toml b/crates/eps/Cargo.toml index d6af59010..6743c2a2a 100644 --- a/crates/eps/Cargo.toml +++ b/crates/eps/Cargo.toml @@ -29,6 +29,8 @@ ruzstd = { workspace = true } hayro-ccitt = { workspace = true } # Live effects' params, as the art styles that name them give them. serde_json = { workspace = true } +# Substring search for PostScript `search`. +memchr = { workspace = true } [dev-dependencies] vectorcraft-testkit = { workspace = true } diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 547d66a11..fc8c9de37 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -951,6 +951,7 @@ impl<'a> Interp<'a> { let seek = self.pop_str()?.to_vec(); let s = self.pop_str()?; let text = s.to_vec(); + self.spend((text.len() / 64) as u64)?; let at = if op == Search { super::lex::find(&text, &seek) } else { text.starts_with(&seek).then_some(0) }; match at { Some(i) => { diff --git a/crates/eps/src/import/lex.rs b/crates/eps/src/import/lex.rs index 1d4918393..94f2b541b 100644 --- a/crates/eps/src/import/lex.rs +++ b/crates/eps/src/import/lex.rs @@ -252,7 +252,7 @@ pub(crate) fn find(hay: &[u8], needle: &[u8]) -> Option { if needle.is_empty() { return Some(0); } - hay.windows(needle.len()).position(|w| w == needle) + memchr::memmem::find(hay, needle) } /// Hexadecimal digits (whitespace ignored, an odd last digit padded with 0); `None` on others. diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index c446ecca5..99dcee8bd 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -1122,3 +1122,12 @@ fn a_damaged_palette_preview_is_refused_without_panicking() { assert!(e.contains("can't be read"), "{e}"); } } + +/// #3582: `search` in a loop on multi-megabyte strings, with a needle half as +/// long as the string that differs in its last byte, finish quickly and run out of the operation +/// budget. +#[test] +fn search_in_a_loop_runs_out_of_the_budget() { + let make = "/h 4194304 string def /n 2097152 string def n 2097152 1 sub 1 put"; + runs_out_of_budget(&format!("{make} {{ h n search {{ pop pop pop }} {{ pop }} ifelse }} loop")); +}