From 0d697ed79da3a3e9b9af8ef2deb68f7ba00abb65 Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:57:02 +0700 Subject: [PATCH 1/2] fix(distort): validate the full corner quad, source bounds and targets --- crates/engine/src/cmd/xform.rs | 62 ++++++++++++++++++++------------ crates/engine/src/tests_xform.rs | 24 +++++++++++++ 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/crates/engine/src/cmd/xform.rs b/crates/engine/src/cmd/xform.rs index 6bd63a57..58586e09 100644 --- a/crates/engine/src/cmd/xform.rs +++ b/crates/engine/src/cmd/xform.rs @@ -132,30 +132,48 @@ fn warp_node(n: &mut Node, pr: &Projective) { } fn distort(s: &mut Session, p: &Value) -> Result { - let corners: Vec = p - .get("corners") - .and_then(Value::as_array) - .map(|a| a.iter().filter_map(|c| Some(Point::new(c.get(0)?.as_f64()?, c.get(1)?.as_f64()?))).collect()) - .unwrap_or_default(); - if corners.len() != 4 { - return Err(bad("object.distort", "corners must be 4 [x,y] points (TL, TR, BR, BL)")); - } - let ids = match ids_param(p, "ids") { - Some(v) => v, - None => selected_roots(s)?, - }; - let src = match p.get("from").and_then(Value::as_array) { - Some(a) if a.len() == 4 => { - let f: Vec = a.iter().filter_map(Value::as_f64).collect(); - if f.len() != 4 { - return Err(bad("object.distort", "from must be [x0,y0,x1,y1]")); + const C: &str = "object.distort"; + let invalid_corners = || bad(C, "corners must be exactly 4 finite [x,y] points (TL, TR, BR, BL)"); + let points = p.get("corners").and_then(Value::as_array).filter(|a| a.len() == 4).ok_or_else(invalid_corners)?; + let corners: Vec = points + .iter() + .map(|v| { + let Some([x, y]) = v.as_array().map(Vec::as_slice) else { + return Err(invalid_corners()); + }; + let (Some(x), Some(y)) = (x.as_f64(), y.as_f64()) else { + return Err(invalid_corners()); + }; + if !x.is_finite() || !y.is_finite() { + return Err(invalid_corners()); } - Rect::new(f[0], f[1], f[2], f[3]) - } - _ => s.doc()?.doc.bounds_of(&ids, false).ok_or_else(|| EngineError::Other("nothing to distort".into()))?, + Ok(Point::new(x, y)) + }) + .collect::>()?; + let ids = if p.get("ids").is_some_and(|v| !v.is_null()) { + checked_ids_param(s, p, "ids", C)? + } else { + selected_roots(s)? }; - if src.width().abs() < 1e-9 || src.height().abs() < 1e-9 { - return Err(EngineError::Other("cannot distort a zero-size bounding box".into())); + if ids.is_empty() { + return Err(bad(C, "nothing to distort")); + } + let src = match p.get("from") { + None | Some(Value::Null) => s.doc()?.doc.bounds_of(&ids, false).ok_or_else(|| bad(C, "nothing to distort"))?, + Some(v) => { + let Some([x0, y0, x1, y1]) = v.as_array().map(Vec::as_slice) else { + return Err(bad(C, "from must be exactly 4 finite numbers [x0,y0,x1,y1]")); + }; + let coordinate = |v: &Value| { + v.as_f64() + .filter(|n| n.is_finite()) + .ok_or_else(|| bad(C, "from must be exactly 4 finite numbers [x0,y0,x1,y1]")) + }; + Rect::new(coordinate(x0)?, coordinate(y0)?, coordinate(x1)?, coordinate(y1)?) + } + }; + if !src.width().is_finite() || !src.height().is_finite() || src.width().abs() < 1e-9 || src.height().abs() < 1e-9 { + return Err(bad(C, "cannot distort a zero-size or non-finite bounding box")); } let pr = Projective::from_rect(src, [corners[0], corners[1], corners[2], corners[3]]); s.edit("Free Distort", |d, _| { diff --git a/crates/engine/src/tests_xform.rs b/crates/engine/src/tests_xform.rs index 0faf7cea..f5798ffb 100644 --- a/crates/engine/src/tests_xform.rs +++ b/crates/engine/src/tests_xform.rs @@ -572,3 +572,27 @@ fn copy_from_requires_source() { assert!(s.execute("appearance.copyFrom", &json!({})).is_err()); assert!(s.execute("appearance.copyFrom", &json!({"source": 9999})).is_err()); } + +#[test] +fn distort_rejects_malformed_coordinates_and_ids_without_editing_art() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0); + let original = bounds(&s, a); + let history = undo_len(&s); + let corners = json!([[10, 0], [90, 0], [100, 100], [0, 100]]); + for p in [ + json!({"corners": [[10, 0, 50], [90, 0], [100, 100], [0, 100]]}), + json!({"corners": [[10, 0], [90, 0], ["bad", 100], [0, 100]]}), + json!({"corners": [[10, 0], [90, 0], [100, 100], [0, 100], ["bad"]]}), + json!({"corners": corners, "from": [0, 0, 100]}), + json!({"corners": corners, "from": "not bounds"}), + json!({"corners": corners, "ids": [a.0, "bad"]}), + json!({"corners": corners, "ids": [a.0, u64::MAX]}), + ] { + assert!(s.execute("object.distort", &p).is_err(), "invalid input: {p}"); + assert!(close(bounds(&s, a), original)); + assert_eq!(undo_len(&s), history); + } + s.execute("object.distort", &json!({"corners": corners, "from": [0, 0, 100, 100], "ids": [a.0]})).unwrap(); + assert_eq!(undo_len(&s), history + 1); +} From 2ff5688511489be9439f8bf61b63e6f441d587b7 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 11 Oct 2026 01:51:12 +0700 Subject: [PATCH 2/2] Review #999: corners and from through finite_numbers; rustfmt Same checks in a third of the code; the corners destructure into the four points instead of being indexed. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/xform.rs | 47 ++++++++++------------------------ 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/crates/engine/src/cmd/xform.rs b/crates/engine/src/cmd/xform.rs index 58586e09..8b7d286b 100644 --- a/crates/engine/src/cmd/xform.rs +++ b/crates/engine/src/cmd/xform.rs @@ -133,49 +133,28 @@ fn warp_node(n: &mut Node, pr: &Projective) { fn distort(s: &mut Session, p: &Value) -> Result { const C: &str = "object.distort"; - let invalid_corners = || bad(C, "corners must be exactly 4 finite [x,y] points (TL, TR, BR, BL)"); - let points = p.get("corners").and_then(Value::as_array).filter(|a| a.len() == 4).ok_or_else(invalid_corners)?; - let corners: Vec = points - .iter() - .map(|v| { - let Some([x, y]) = v.as_array().map(Vec::as_slice) else { - return Err(invalid_corners()); - }; - let (Some(x), Some(y)) = (x.as_f64(), y.as_f64()) else { - return Err(invalid_corners()); - }; - if !x.is_finite() || !y.is_finite() { - return Err(invalid_corners()); - } - Ok(Point::new(x, y)) - }) - .collect::>()?; - let ids = if p.get("ids").is_some_and(|v| !v.is_null()) { - checked_ids_param(s, p, "ids", C)? - } else { - selected_roots(s)? - }; + // All four corners or nothing, each exactly [x, y]: nothing malformed is left out. + let corners: Option> = p + .get("corners") + .and_then(Value::as_array) + .filter(|a| a.len() == 4) + .and_then(|a| a.iter().map(|c| finite_numbers(c).map(|[x, y]| Point::new(x, y))).collect()); + let Some(&[tl, tr, br, bl]) = corners.as_deref() else { return Err(bad(C, "corners must be 4 [x, y] points (TL, TR, BR, BL)")) }; + let ids = if p.get("ids").is_some_and(|v| !v.is_null()) { checked_ids_param(s, p, "ids", C)? } else { selected_roots(s)? }; if ids.is_empty() { return Err(bad(C, "nothing to distort")); } - let src = match p.get("from") { - None | Some(Value::Null) => s.doc()?.doc.bounds_of(&ids, false).ok_or_else(|| bad(C, "nothing to distort"))?, + let src = match p.get("from").filter(|v| !v.is_null()) { + None => s.doc()?.doc.bounds_of(&ids, false).ok_or_else(|| bad(C, "nothing to distort"))?, Some(v) => { - let Some([x0, y0, x1, y1]) = v.as_array().map(Vec::as_slice) else { - return Err(bad(C, "from must be exactly 4 finite numbers [x0,y0,x1,y1]")); - }; - let coordinate = |v: &Value| { - v.as_f64() - .filter(|n| n.is_finite()) - .ok_or_else(|| bad(C, "from must be exactly 4 finite numbers [x0,y0,x1,y1]")) - }; - Rect::new(coordinate(x0)?, coordinate(y0)?, coordinate(x1)?, coordinate(y1)?) + let [x0, y0, x1, y1] = finite_numbers(v).ok_or_else(|| bad(C, "from must be [x0, y0, x1, y1]"))?; + Rect::new(x0, y0, x1, y1) } }; if !src.width().is_finite() || !src.height().is_finite() || src.width().abs() < 1e-9 || src.height().abs() < 1e-9 { return Err(bad(C, "cannot distort a zero-size or non-finite bounding box")); } - let pr = Projective::from_rect(src, [corners[0], corners[1], corners[2], corners[3]]); + let pr = Projective::from_rect(src, [tl, tr, br, bl]); s.edit("Free Distort", |d, _| { for id in &ids { if let Some(n) = d.node_mut(*id) {