diff --git a/crates/doc/src/tests/units.rs b/crates/doc/src/tests/units.rs index 5a9265c702..c53f60a227 100644 --- a/crates/doc/src/tests/units.rs +++ b/crates/doc/src/tests/units.rs @@ -79,6 +79,21 @@ fn odd_text_reads_as_nothing_without_panicking() { } } +/// Text from a command or a file can be any length: a sum of thousands of terms reads as nothing +/// instead of exhausting the stack. Up to 128 `+ - * /` signs read; 129 read as nothing. +#[test] +fn a_very_long_expression_reads_as_nothing() { + let sum = |terms: usize| format!("1{}", "+1".repeat(terms - 1)); + assert_eq!(Unit::Points.parse(&sum(129)), Some(129.0)); + assert_eq!(Unit::Points.parse(&format!("10{} mm", "*1".repeat(128))), Some(Unit::Millimeters.to_pt(10.0))); + // Long text with no signs still reads. + assert_eq!(Unit::Points.parse(&format!("{}5", " ".repeat(2_000))), Some(5.0)); + for text in [sum(200_000), format!("1{}", "*1".repeat(200_000)), sum(130), format!("10{} mm", "*1".repeat(129))] { + assert_eq!(Unit::Points.parse(&text), None, "{} bytes", text.len()); + assert_eq!(crate::parse_number(&text), None, "{} bytes", text.len()); + } +} + proptest! { /// Field text made of digits, operators, `%`, spaces and unit letters reads as a finite value /// or as nothing and does not panic. diff --git a/crates/doc/src/units.rs b/crates/doc/src/units.rs index 234513df1d..5fabfece65 100644 --- a/crates/doc/src/units.rs +++ b/crates/doc/src/units.rs @@ -156,7 +156,12 @@ impl Unit { /// before it (`3cm*50%` is 1.5 cm, `3cm/50%` is 6 cm). After `+` or `-`, it adds or takes away /// that share of everything before it (`50pt+25%` is 62.5 pt, `100pt+20pt-50%` is 60 pt). A /// percentage on its own (`50%`) reads as nothing. + /// + /// Text with more than 128 `+ - * /` signs reads as nothing. pub fn parse(self, s: &str) -> Option { + if s.chars().filter(|c| matches!(c, '+' | '-' | '*' | '/')).count() > MAX_MEASURE_OPERATORS { + return None; + } parse_measure(s, self).filter(|v| v.is_finite()) } /// The value naming this unit in the Units preferences (`points`, `millimeters`, @@ -207,6 +212,9 @@ impl Unit { } } +/// The most `+ - * /` signs [`Unit::parse`] reads in one text. +const MAX_MEASURE_OPERATORS: usize = 128; + fn parse_measure(s: &str, default: Unit) -> Option { let s = s.trim(); // Simple arithmetic on the right (Illustrator fields accept "10+5", "100/2", "3in*2"). diff --git a/docs/mcp.md b/docs/mcp.md index d4e03892e9..02ead0e27e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6589,7 +6589,8 @@ report warn about it). Commands take and return lengths in points (the distances of `object.path.offsetPath`, `object.path.simplify`, `object.path.splitIntoGrid` and `text.setStyle` `size`/`leading` also take a string with a unit, `"5 mm"`). A length string takes the same arithmetic as the desktop app's fields, where `%` is a share of the value before it: -`"200mm*50%"` is 100 mm and `"50pt+25%"` is 62.5 pt. The units +`"200mm*50%"` is 100 mm and `"50pt+25%"` is 62.5 pt. A length string with more than 128 `+ - * /` signs is not read +as a length. The units only change what the UI shows and how it reads typed numbers. Preferences ▸ Units has four: General (rulers, positions and sizes, the Info panel, dialog distances, canvas measurement labels), Stroke (weights, dashes), Type (font size, leading, baseline shift) and East Asian Type (no field uses it yet). General is the active document's units