From fffec4a714e02078df291e9e0eabae6025b6bd8d Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Sun, 11 Oct 2026 04:52:22 +0700 Subject: [PATCH] fix(views): validate saved-view coordinates and rotation --- crates/engine/src/cmd/views.rs | 38 +++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/views.rs b/crates/engine/src/cmd/views.rs index 65c55f93..9ec77836 100644 --- a/crates/engine/src/cmd/views.rs +++ b/crates/engine/src/cmd/views.rs @@ -55,9 +55,18 @@ fn list_views(s: &mut Session, _: &Value) -> Result { fn new_view(s: &mut Session, p: &Value) -> Result { const C: &str = "view.saved.new"; - let center = p.get("center").and_then(Value::as_array).and_then(|a| Some(Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))); - let center = center.ok_or_else(|| bad(C, "missing center [x, y]"))?; + let center = match p.get("center").and_then(Value::as_array).map(Vec::as_slice) { + Some([x, y]) => match (x.as_f64(), y.as_f64()) { + (Some(x), Some(y)) if x.is_finite() && y.is_finite() => Point::new(x, y), + _ => return Err(bad(C, "center must contain two finite numbers")), + }, + _ => return Err(bad(C, "center must be [x, y]")), + }; let zoom = f64_req(p, "zoom", C)?; + let rotation = match p.get("rotation") { + Some(value) => value.as_f64().filter(|n| n.is_finite()).ok_or_else(|| bad(C, "rotation must be a finite number"))?, + None => 0.0, + }; if !(zoom.is_finite() && zoom > 0.0) { return Err(bad(C, "zoom must be positive")); } @@ -72,7 +81,7 @@ fn new_view(s: &mut Session, p: &Value) -> Result { if st.doc.views.iter().any(|v| v.name == name) { return Err(bad(C, format!("a view named `{name}` exists"))); } - let view = SavedView { name: name.clone(), center, zoom, rotation: f64_or(p, "rotation", 0.0) }; + let view = SavedView { name: name.clone(), center, zoom, rotation }; let index = s.edit("New View", |d, _| { d.views.push(view); Ok(d.views.len() - 1) @@ -108,6 +117,29 @@ mod tests { use crate::Session; + /// Malformed dimensions are rejected rather than saving an arbitrary or partial viewport. + #[test] + fn saved_views_require_exact_finite_coordinates_and_rotation() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let before = s.doc().unwrap().history.undo.len(); + for params in [ + json!({"center": [10], "zoom": 2}), + json!({"center": [10, 20, 30], "zoom": 2}), + json!({"center": [10, "bad"], "zoom": 2}), + json!({"center": [10, 20], "zoom": 2, "rotation": "bad"}), + json!({"center": [10, 20], "zoom": 2, "rotation": null}), + ] { + assert!(s.execute("view.saved.new", ¶ms).is_err(), "accepted {params}"); + assert!(s.doc().unwrap().doc.views.is_empty()); + assert_eq!(s.doc().unwrap().history.undo.len(), before); + } + s.execute("view.saved.new", &json!({"center": [10, 20], "zoom": 2, "rotation": -15})).unwrap(); + let views = &s.doc().unwrap().doc.views; + assert_eq!(views.len(), 1); + assert_eq!(views[0].rotation, -15.0); + } + #[test] fn save_rename_delete_views() { let mut s = Session::new();