fix(place): reject malformed rectangle and position parameters

This commit is contained in:
Nigel Ihza Firdausy 2026-10-10 23:39:32 +07:00 committed by bflatastic
parent e860b01ed8
commit 004d015cfe
2 changed files with 45 additions and 3 deletions

View File

@ -309,9 +309,11 @@ fn transformed(mut node: Node, m: Affine) -> Node {
/// `[x, y, width, height]` with a positive size.
fn rect_param(p: &Value) -> Result<Option<Rect>> {
let Some(v) = p.get("rect") else { return Ok(None) };
let n: Vec<f64> = v.as_array().map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default();
match n[..] {
[x, y, w, h] if w > 0.0 && h > 0.0 => Ok(Some(Rect::new(x, y, x + w, y + h))),
// Reject invalid components instead of dropping them: filtering would turn a malformed
// five-element box such as [0, "bad", 10, 40, 50] into a different valid rectangle.
let numbers = v.as_array().filter(|a| a.len() == 4).and_then(|a| a.iter().map(Value::as_f64).collect::<Option<Vec<_>>>());
match numbers.as_deref() {
Some([x, y, w, h]) if *w > 0.0 && *h > 0.0 => Ok(Some(Rect::new(*x, *y, x + w, y + h))),
_ => Err(bad(PLACE, "rect must be [x, y, width, height] with a positive width and height")),
}
}
@ -343,6 +345,9 @@ fn place(s: &mut Session, p: &Value) -> Result<Value> {
let replace = bool_or(p, "replace", false);
let template = bool_or(p, "template", false);
let at = point_param(p, "at");
if p.get("at").is_some() && (at.is_none() || p["at"].as_array().is_none_or(|a| a.len() != 2)) {
return Err(bad(PLACE, "at must be [x, y] with two numeric coordinates"));
}
let rect = rect_param(p)?;
if replace && (template || at.is_some() || rect.is_some()) {
return Err(bad(PLACE, "replace keeps the replaced object's place and transform: drop template, at and rect"));

View File

@ -57,6 +57,43 @@ fn serialize(s: &mut Session, format: &str) -> Vec<u8> {
vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap()
}
#[test]
fn malformed_place_coordinates_are_rejected_instead_of_silently_changed() {
let mut s = session();
let png = png(4, 2, [20, 40, 60, 255], None);
let original = s.doc().unwrap().doc.layers.clone();
// Previously filter_map dropped the string from a five-element rectangle and
// treated the remaining four values as a different, valid placement.
for rect in [
json!([0, "ignored", 10, 40, 50]),
json!([10, 20, 40, null, 50]),
json!([10, 20, 40, 50, 60]),
] {
let mut params = file("tiny.png", &png);
params["rect"] = rect;
assert!(s.execute("file.place", &params).is_err(), "{params}");
assert_eq!(s.doc().unwrap().doc.layers, original, "failed Place must not add art");
}
// An explicitly supplied invalid position formerly behaved as though no
// position were supplied and centred the image on the artboard.
for at in [
json!(["not-a-number", 20]),
json!([10, null]),
json!([10, 20, 30]),
json!("invalid"),
] {
let mut params = file("tiny.png", &png);
params["at"] = at;
assert!(s.execute("file.place", &params).is_err(), "{params}");
assert_eq!(s.doc().unwrap().doc.layers, original);
}
let mut params = file("tiny.png", &png);
params["rect"] = json!([10, 20, 80, 60]);
let placed = s.execute("file.place", &params).unwrap();
assert_eq!(placed["ids"].as_array().unwrap().len(), 1);
}
#[test]
fn a_300_px_png_at_300_ppi_places_as_72_pt_centred_selected_and_undoable() {
let mut s = session();