mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 05:55:32 +00:00
Units: a very long expression reads as nothing instead of overflowing the stack
Unit::parse took one level of recursion per + - * / sign, so a text such as "1+1+...+1" with 200,000 terms overflowed the stack and aborted the process. Every number field, parse_number and every command parameter read as a length went through it. Text with more than 128 + - * / signs now reads as nothing; text with 128 or fewer reads as before. docs/mcp.md states the limit for length strings. The test a_very_long_expression_reads_as_nothing aborted with a stack overflow before the change.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<f64> {
|
||||
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<f64> {
|
||||
let s = s.trim();
|
||||
// Simple arithmetic on the right (Illustrator fields accept "10+5", "100/2", "3in*2").
|
||||
|
||||
+2
-1
@@ -6535,7 +6535,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
|
||||
|
||||
Reference in New Issue
Block a user