diff --git a/crates/engine/src/cmd/freeform.rs b/crates/engine/src/cmd/freeform.rs index 21fd5c92..7d984a0f 100644 --- a/crates/engine/src/cmd/freeform.rs +++ b/crates/engine/src/cmd/freeform.rs @@ -148,9 +148,19 @@ pub(crate) fn parse_freeform(v: &Value) -> std::result::Result point_fields(pv, &mut pt, Affine::IDENTITY).map_err(|e| format!("freeform point {i}: {e}"))?; f.points.push(pt); } - for l in v.get("lines").and_then(Value::as_array).into_iter().flatten() { - let ix = l.as_array().map(|a| a.iter().filter_map(Value::as_u64).map(|i| i as usize).collect()).unwrap_or_default(); - f.add_line(ix)?; + if let Some(lines) = v.get("lines") { + let lines = lines.as_array().ok_or("`freeform.lines` must be an array")?; + for (line, values) in lines.iter().enumerate() { + let values = values.as_array().ok_or_else(|| format!("freeform line {line} must be an array of point indices"))?; + let indices: Vec = values + .iter() + .map(|v| { + let id = v.as_u64().ok_or_else(|| format!("freeform line {line} has a non-integer point index: {v}"))?; + usize::try_from(id).map_err(|_| format!("freeform line {line} has an out-of-range point index: {id}")) + }) + .collect::>()?; + f.add_line(indices).map_err(|e| format!("freeform line {line}: {e}"))?; + } } if let Some(m) = str_param(v, "mode") { f.mode = parse_mode(m)?; @@ -323,3 +333,30 @@ fn get(s: &mut Session, p: &Value) -> Result { v["selected"] = json!(selected); Ok(v) } + + +#[cfg(test)] +mod parse_tests { + use super::*; + + #[test] + fn malformed_freeform_lines_are_not_silently_shortened() { + let points = json!([ + {"at": [0, 0], "color": "#ff0000"}, + {"at": [50, 0], "color": "#00ff00"}, + {"at": [100, 0], "color": "#0000ff"} + ]); + for lines in [ + json!([[0, "broken", 2]]), + json!([[0, -1, 2]]), + json!([[0, 1.5, 2]]), + json!([{"points": [0, 1]}]), + json!("not an array"), + ] { + let value = json!({"points": points, "lines": lines, "mode": "lines"}); + assert!(parse_freeform(&value).is_err(), "invalid lines accepted: {value}"); + } + let good = parse_freeform(&json!({"points": points, "lines": [[0, 1, 2]], "mode": "lines"})).unwrap(); + assert_eq!(good.lines, vec![vec![0, 1, 2]]); + } +}