fix(control): reject unknown entities_transform actions instead of panicking

Bug:
control_entities_transform validated the action field only inside the
transform closure - and invoked that closure from three of the match's
arms. Any other action fell into unreachable!("validated by
transform()") and aborted the process. The claim in the panic message
was false: for unknown actions the validating code never ran. The handler
is reachable from every remote surface (REST, MCP, --serve) because they
share this dispatch. Two extra triggers: "move " (trailing space, no
trim) and an all-whitespace action.

Reproduce:
1. Start OpenCADStudio with --http 8765.
2. POST /api/v1/entities (a LINE), take handles[0].
3. POST /api/v1/entities/transform with
   {"handles":["<handle>"],"action":"explode"}.
4. Before: "internal error: entered unreachable code" and the process
   exits.
5. After: {"ok":false,"code":"invalid_action",...} and the server keeps
   serving. "action":"move " is now accepted (trimmed); "   " is
   rejected.

Fix:
The action is trimmed, lowercased and checked against the six-value
whitelist after require_existing (error precedence unchanged: a bad
handle still reports first) but before push_undo_snapshot, so an invalid
request no longer dirties the undo stack. The match's fallback arm
returns the same invalid_action error instead of unreachable!, so a
future mismatch between the whitelist and the match fails closed.
This commit is contained in:
Karim Jerbi committed 2026-09-30 19:15:49 +01:00
1 parent be8a7b95c4
commit ee188a6e3c
2 files changed
+64 -2

No files matched your search

+40
View File
@@ -2271,4 +2271,44 @@ mod tests {
let _ = std::fs::remove_file(&pdf);
let _ = std::fs::remove_file(&plain);
}
/// An unknown `entities_transform` action must come back as a clean
/// validation error. Pre-fix it reached
/// `unreachable!("validated by transform()")` (entities.rs:476) and
/// killed the whole process — over REST, MCP and `--serve` alike —
/// because validation lived inside the `transform` closure, which the
/// dispatch match only invokes from some of its arms.
#[test]
fn entities_transform_rejects_unknown_actions_without_panicking() {
let mut app = OpenCADStudio::new_for_test();
request(&mut app, json!({"op":"new"}));
request(&mut app, json!({"op":"run","cmd":"LINE 0,0 10,0"}));
let lines = app.control_request(json!({"op":"query","type":"Line"})).0;
let handle = lines["entities"][0]["handle"].as_str().unwrap().to_string();
// Not one of the six whitelisted actions.
let rejected = request(&mut app, json!({
"op":"entities_transform",
"handles":[handle],
"action":"explode"
}));
assert_eq!(rejected["code"], "invalid_action", "{rejected}");
// Whitespace-only is unknown too — not a panic.
let blank = request(&mut app, json!({
"op":"entities_transform",
"handles":[handle],
"action":" "
}));
assert_eq!(blank["code"], "invalid_action", "{blank}");
// A valid action is matched case- and whitespace-insensitively.
let moved = request(&mut app, json!({
"op":"entities_transform",
"handles":[handle],
"action":" move ",
"vector":[5,0,0]
}));
assert_eq!(moved["ok"], true, "{moved}");
}
}