From 8f81e142a5e014a61d8ec6e563705b166c2362b3 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 5 Nov 2026 23:42:30 -0800 Subject: [PATCH] Reject invalid Tab Stop string parameters --- crates/engine/src/cmd/tabs.rs | 31 +++++++++++++++++++++++++++---- docs/mcp.md | 5 ++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/tabs.rs b/crates/engine/src/cmd/tabs.rs index 570a93c77..331227e3b 100644 --- a/crates/engine/src/cmd/tabs.rs +++ b/crates/engine/src/cmd/tabs.rs @@ -13,7 +13,7 @@ pub fn specs() -> Vec { "Set Tab Stops", ["Window", "Tabs"], None, - "{stops: [{position: pt, align?: left|center|right|decimal, leader?: \". \", alignOn?: \".\"}], ids?|id?, start?: byte, end?: byte} replace the tab stops of the selected text (with a range: of the paragraphs it touches)", + "{stops: [{position: pt, align?: left|center|right|decimal, leader?: \". \", alignOn?: \".\"}], ids?|id?, start?: byte, end?: byte} replace the tab stops of the selected text (with a range: of the paragraphs it touches); supplied align, leader and alignOn must be strings, and alignOn cannot be empty", has_selection, set ), @@ -44,15 +44,31 @@ fn parse_stops(p: &Value) -> Result> { if !(0.0..=100_000.0).contains(&position) { return Err(bad(C, "positions must be between 0 and 100000 pt")); } - let align = match s.get("align").and_then(Value::as_str).unwrap_or("left").to_ascii_lowercase().as_str() { + let align = match s.get("align") { + None | Some(Value::Null) => "left", + Some(Value::String(value)) => value.as_str(), + Some(_) => return Err(bad(C, "`align` must be a string")), + }; + let align = match align.to_ascii_lowercase().as_str() { "left" => TabAlign::Left, "center" | "centre" => TabAlign::Center, "right" => TabAlign::Right, "decimal" => TabAlign::Decimal, other => return Err(bad(C, format!("unknown align `{other}`"))), }; - let leader = s.get("leader").and_then(Value::as_str).unwrap_or("").chars().take(8).collect(); - let align_on = s.get("alignOn").and_then(Value::as_str).and_then(|a| a.chars().next()).unwrap_or('.'); + let leader = match s.get("leader") { + None | Some(Value::Null) => "", + Some(Value::String(value)) => value.as_str(), + Some(_) => return Err(bad(C, "`leader` must be a string")), + } + .chars() + .take(8) + .collect(); + let align_on = match s.get("alignOn") { + None | Some(Value::Null) => '.', + Some(Value::String(value)) => value.chars().next().ok_or_else(|| bad(C, "`alignOn` cannot be empty"))?, + Some(_) => return Err(bad(C, "`alignOn` must be a string")), + }; stops.push(TabStop { position, align, leader, align_on }); } stops.sort_by(|a, b| a.position.total_cmp(&b.position)); @@ -116,6 +132,13 @@ mod tests { use crate::Session; + #[test] + fn supplied_tab_stop_fields_require_strings() { + for stop in [json!({"position": 40, "align": false}), json!({"position": 40, "leader": 1}), json!({"position": 40, "alignOn": ""})] { + assert!(super::parse_stops(&json!({"stops": [stop.clone()]})).is_err(), "accepted {stop}"); + } + } + #[test] fn set_get_clear_tabs() { let mut s = Session::new(); diff --git a/docs/mcp.md b/docs/mcp.md index 6400a9099..c70283b70 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5718,7 +5718,10 @@ spaceBefore, spaceAfter, hyphenate, mojikumi, burasagari, direction, leadingMode touches (a caret, `start == end`, touches its paragraph). Without a range they change every paragraph of the targeted objects, as before. With a range, the character attributes of `text.setStyle` and `text.setFormat` style that range of characters. When every paragraph that `text.tabs.set` or `text.tabs.clear` targets already has those -stops, the command changes nothing and records no undo step. `text.tabs.get {start?}` and `paraStyle.new` / `paraStyle.redefine {id, start?}` read the +stops, the command changes nothing and records no undo step. `text.tabs.set` rejects a supplied `align`, `leader`, or +`alignOn` value that is not a string, and an empty `alignOn` value. + +`text.tabs.get {start?}` and `paraStyle.new` / `paraStyle.redefine {id, start?}` read the paragraph at `start`; `paraStyle.list` counts paragraphs as uses, and `charStyle.list` counts runs; both count the type in the layers, opacity masks, symbols, graph designs, pattern tiles and the art of brushes. `charStyle.unused` and `paraStyle.unused` (Select All Unused in the Character Styles and Paragraph Styles panel menus) list the styles with no