From 0876f6dbc9ca5ea3c01165c1d0c6055da2651266 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:51:04 +0100 Subject: [PATCH] Reject non-boolean values for node.set flag properties Found while testing crates/engine/src/cmd/props.rs:614 (also :708, :717, :718, :826 and :1007) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/props.rs | 14 ++++++++------ crates/engine/src/node_find_tests.rs | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/props.rs b/crates/engine/src/cmd/props.rs index ee5ba4f6..d89b25ae 100644 --- a/crates/engine/src/cmd/props.rs +++ b/crates/engine/src/cmd/props.rs @@ -611,7 +611,7 @@ fn apply_corners(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) "cornerSmoothing" => doc.get_mut(id)?.corners.smoothing = f64_of(v).unwrap_or(0.0).clamp(0.0, 1.0), "effects" => doc.get_mut(id)?.effects = effects_from(cmd, v)?, "clip" | "clipsContent" => doc.get_mut(id)?.clip = v.as_bool().ok_or_else(|| bad(cmd, "`clip` must be a boolean"))?, - "isMask" => doc.get_mut(id)?.is_mask = v.as_bool().unwrap_or(false), + "isMask" => doc.get_mut(id)?.is_mask = v.as_bool().ok_or_else(|| bad(cmd, format!("`{k}` must be a boolean")))?, "devStatus" => { let st = if v.is_null() { Some(DevStatus::None) @@ -705,7 +705,7 @@ fn apply_layout(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) - "contentAlign" | "counterAxisAlignContent" => { layout_mut(cmd, k, doc.get_mut(id)?)?.content_align = enum_of(v).ok_or_else(|| bad(cmd, "contentAlign: auto|spaceBetween"))? } - "wrap" => layout_mut(cmd, k, doc.get_mut(id)?)?.wrap = v.as_bool().unwrap_or(false), + "wrap" => layout_mut(cmd, k, doc.get_mut(id)?)?.wrap = v.as_bool().ok_or_else(|| bad(cmd, format!("`{k}` must be a boolean")))?, "gridColumns" | "gridRows" => { let count = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.clamp(1.0, 1000.0) as u32; let l = layout_mut(cmd, k, doc.get_mut(id)?)?; @@ -714,8 +714,10 @@ fn apply_layout(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) - *n = count; tracks.truncate(count as usize); } - "strokesIncluded" => layout_mut(cmd, k, doc.get_mut(id)?)?.strokes_included = v.as_bool().unwrap_or(false), - "reverseZ" => layout_mut(cmd, k, doc.get_mut(id)?)?.reverse_z = v.as_bool().unwrap_or(false), + "strokesIncluded" => { + layout_mut(cmd, k, doc.get_mut(id)?)?.strokes_included = v.as_bool().ok_or_else(|| bad(cmd, format!("`{k}` must be a boolean")))? + } + "reverseZ" => layout_mut(cmd, k, doc.get_mut(id)?)?.reverse_z = v.as_bool().ok_or_else(|| bad(cmd, format!("`{k}` must be a boolean")))?, _ => return Ok(false), } Ok(true) @@ -823,7 +825,7 @@ fn apply_meta(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) -> "interactions" => doc.get_mut(id)?.interactions = serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("interactions: {e}")))?, "overflow" => doc.get_mut(id)?.overflow = enum_of(v).ok_or_else(|| bad(cmd, "overflow: none|horizontal|vertical|both"))?, "scrollPosition" => doc.get_mut(id)?.scroll_position = enum_of(v).ok_or_else(|| bad(cmd, "scrollPosition: scrolls|fixed|sticky"))?, - "constrainProportions" => doc.get_mut(id)?.constrain_proportions = v.as_bool().unwrap_or(false), + "constrainProportions" => doc.get_mut(id)?.constrain_proportions = v.as_bool().ok_or_else(|| bad(cmd, format!("`{k}` must be a boolean")))?, _ => return Ok(false), } Ok(true) @@ -1004,7 +1006,7 @@ fn apply_shape(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) -> "operandOperation" => set_operand_op(doc, id, v, cmd)?, "sectionContentsHidden" => { if let NodeKind::Section { contents_hidden } = &mut doc.get_mut(id)?.kind { - *contents_hidden = v.as_bool().unwrap_or(false); + *contents_hidden = v.as_bool().ok_or_else(|| bad(cmd, "`sectionContentsHidden` must be a boolean"))?; } } "background" => { diff --git a/crates/engine/src/node_find_tests.rs b/crates/engine/src/node_find_tests.rs index d8729357..a60383be 100644 --- a/crates/engine/src/node_find_tests.rs +++ b/crates/engine/src/node_find_tests.rs @@ -194,3 +194,27 @@ fn node_find_leaves_document_selection_page_and_history_untouched() { assert_ne!(s.doc().unwrap().get(layer).unwrap().name, "Previous edit"); assert_eq!(s.ds().unwrap().history.undo.len(), undo - 1); } + +#[test] +fn node_set_rejects_non_boolean_flags() { + let mut s = Session::new(); + let page = s.page().unwrap(); + let rect = add(&mut s, page, NodeKind::Rectangle, "R"); + let frame = add(&mut s, page, NodeKind::Frame, "F"); + ex(&mut s, "node.set", json!({"ids": [rect], "props": {"isMask": true, "constrainProportions": true}})); + ex(&mut s, "node.set", json!({"ids": [frame], "props": {"layoutMode": "HORIZONTAL", "wrap": true, "strokesIncluded": true, "reverseZ": true}})); + for (id, key) in [(rect, "isMask"), (rect, "constrainProportions"), (frame, "wrap"), (frame, "strokesIncluded"), (frame, "reverseZ")] { + let r = s.execute("node.set", &json!({"ids": [id], "props": {key: "yes"}})); + assert!(r.is_err(), "{key} accepted a non-boolean"); + } + let sec = add(&mut s, page, NodeKind::Section { contents_hidden: false }, "S"); + ex(&mut s, "node.set", json!({"ids": [sec], "props": {"sectionContentsHidden": true}})); + let r = s.execute("node.set", &json!({"ids": [sec], "props": {"sectionContentsHidden": "yes"}})); + assert!(r.is_err(), "sectionContentsHidden accepted a non-boolean"); + let doc = s.doc().unwrap(); + assert!(matches!(doc.get(sec).unwrap().kind, figcraft_doc::NodeKind::Section { contents_hidden: true })); + assert!(doc.get(rect).unwrap().is_mask && doc.get(rect).unwrap().constrain_proportions); + let l = doc.get(frame).unwrap(); + let l = l.layout.as_ref().unwrap(); + assert!(l.wrap && l.strokes_included && l.reverse_z); +}