forked from 1b-agent/opencadstudio-upstream
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:
1 parent
be8a7b95c4
commit
ee188a6e3c
2 files changed
+64
-2
No files matched your search
@@ -334,9 +334,24 @@ impl OpenCADStudio {
|
||||
req: &Value,
|
||||
) -> Result<Task<Message>, Value> {
|
||||
let handles = hex_handles(req, "handles")?;
|
||||
let action = req["action"].as_str().unwrap_or("move").to_ascii_lowercase();
|
||||
let action = req["action"]
|
||||
.as_str()
|
||||
.unwrap_or("move")
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
let i = self.active_tab;
|
||||
require_existing(&self.tabs[i].scene.document, &handles)?;
|
||||
if !matches!(
|
||||
action.as_str(),
|
||||
"move" | "copy" | "rotate" | "scale" | "mirror" | "array"
|
||||
) {
|
||||
return Err(failure(
|
||||
"invalid_action",
|
||||
format!(
|
||||
"Unknown transform action '{action}'. Use move, copy, rotate, scale, mirror or array"
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let transform = |req: &Value| -> Parsed<EntityTransform> {
|
||||
Ok(match action.as_str() {
|
||||
@@ -473,7 +488,14 @@ impl OpenCADStudio {
|
||||
}
|
||||
affected = handles.len();
|
||||
}
|
||||
_ => unreachable!("validated by transform()"),
|
||||
other => {
|
||||
return Err(failure(
|
||||
"invalid_action",
|
||||
format!(
|
||||
"Unknown transform action '{other}'. Use move, copy, rotate, scale, mirror or array"
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
self.post_ref_op(i);
|
||||
self.set_control_result(json!({
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user