mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 15:52:52 +00:00
Commands: pointer events refuse pen values and spaces of the wrong type
ui.pointer and the headless pointer call read a pointer event's pressure, holdMs, tiltX, tiltY and rotation with a default for any value that was not a number, and treated any space other than screen as document space. Each event is now checked before the packet runs: those values must be numbers and space must be doc or screen, with null keeping the default. docs/mcp.md says so.
This commit is contained in:
@@ -239,6 +239,7 @@ impl Headless {
|
||||
let mods = packet_mods(e)?.unwrap_or(base);
|
||||
let kind = s(e, "kind").unwrap_or("");
|
||||
let kind = pointer_kind(kind).ok_or_else(|| format!("unknown pointer kind `{kind}` (down|drag|up|move|doubleclick)"))?;
|
||||
PointerEvent::json_check(e)?;
|
||||
if s(e, "space") == Some("screen") {
|
||||
return Err("screen-space pointer events need the desktop app; use document coordinates".to_string());
|
||||
}
|
||||
|
||||
@@ -1474,6 +1474,11 @@ fn headless_methods_refuse_params_of_the_wrong_type() {
|
||||
// A whole rectangle comes before the malformed event, and none of it is drawn.
|
||||
("ui.pointer", after_rect(json!({"kind": "move", "x": "bad", "y": 0})), "x"),
|
||||
("ui.pointer", after_rect(json!({"kind": "wiggle", "x": 0, "y": 0})), "wiggle"),
|
||||
("ui.pointer", after_rect(json!({"kind": "move", "x": 1, "y": 1, "pressure": "bad"})), "pressure"),
|
||||
("ui.pointer", after_rect(json!({"kind": "move", "x": 1, "y": 1, "holdMs": true})), "holdMs"),
|
||||
("ui.pointer", after_rect(json!({"kind": "move", "x": 1, "y": 1, "tiltX": "5"})), "tiltX"),
|
||||
("ui.pointer", after_rect(json!({"kind": "move", "x": 1, "y": 1, "rotation": [0]})), "rotation"),
|
||||
("ui.pointer", after_rect(json!({"kind": "move", "x": 1, "y": 1, "space": "screeen"})), "space"),
|
||||
] {
|
||||
let e = h.call(method, params.clone()).unwrap_err();
|
||||
assert!(e.contains(&format!("`{key}`")), "{method} {params}: {e}");
|
||||
|
||||
@@ -109,6 +109,19 @@ impl PointerEvent {
|
||||
pub fn json_backquote(v: &Value, default: bool) -> Result<bool, String> {
|
||||
v.get("backquote").map_or(Ok(default), |b| b.as_bool().ok_or_else(|| "`backquote` must be a boolean".to_owned()))
|
||||
}
|
||||
/// Checks the optional keys of a JSON pointer event (control channel, MCP): `pressure`,
|
||||
/// `holdMs`, `tiltX`, `tiltY` and `rotation` must be numbers, and `space` must be `doc` or
|
||||
/// `screen`. A key that is absent or null keeps its default.
|
||||
pub fn json_check(e: &Value) -> Result<(), String> {
|
||||
let given = |k: &str| e.get(k).filter(|v| !v.is_null());
|
||||
if let Some(k) = ["pressure", "holdMs", "tiltX", "tiltY", "rotation"].into_iter().find(|k| given(k).is_some_and(|v| v.as_f64().is_none())) {
|
||||
return Err(format!("`{k}` must be a number"));
|
||||
}
|
||||
if given("space").is_some_and(|v| !matches!(v.as_str(), Some("doc" | "screen"))) {
|
||||
return Err("`space` must be doc or screen".to_owned());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -135,6 +148,27 @@ mod tests {
|
||||
assert!(PointerEvent::json_backquote(&json!({"backquote": bad}), false).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_check_refuses_pen_values_and_spaces_of_the_wrong_type() {
|
||||
use serde_json::json;
|
||||
for ok in [
|
||||
json!({}),
|
||||
json!({"pressure": 0.5, "holdMs": 10, "tiltX": -5, "tiltY": 5, "rotation": 400, "space": "screen"}),
|
||||
json!({"pressure": null, "space": null}),
|
||||
json!({"space": "doc"}),
|
||||
] {
|
||||
assert_eq!(PointerEvent::json_check(&ok), Ok(()), "{ok}");
|
||||
}
|
||||
for k in ["pressure", "holdMs", "tiltX", "tiltY", "rotation"] {
|
||||
for bad in [json!("1"), json!(true), json!([1])] {
|
||||
assert_eq!(PointerEvent::json_check(&json!({k: bad})), Err(format!("`{k}` must be a number")), "{k}: {bad}");
|
||||
}
|
||||
}
|
||||
for bad in [json!("screeen"), json!(true), json!(1)] {
|
||||
assert_eq!(PointerEvent::json_check(&json!({"space": bad})), Err("`space` must be doc or screen".to_owned()), "{bad}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keys tools care about.
|
||||
|
||||
@@ -984,6 +984,7 @@ fn pointer_events_inner(app: &mut VectorcraftApp, ctx: &egui::Context, req: &Con
|
||||
"doubleclick" | "dblclick" => PointerKind::DoubleClick,
|
||||
other => return Err(format!("unknown pointer kind `{other}`")),
|
||||
};
|
||||
PointerEvent::json_check(e)?;
|
||||
let at = |k: &str| crate::menus::opt_number(e, k).map(|n| n.unwrap_or(0.0));
|
||||
Ok((kind, at("x")?, at("y")?, packet_mods(e.get("mods"))?.unwrap_or(base)))
|
||||
})
|
||||
|
||||
@@ -457,8 +457,8 @@ fn synthetic_pointer_rejects_a_backquote_that_is_not_a_boolean() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `ui.pointer` refuses an unknown event kind, or a screen-space event with no canvas, before any
|
||||
/// of the packet's events runs.
|
||||
/// `ui.pointer` refuses an unknown event kind, a pen value or `space` of the wrong type, or a
|
||||
/// screen-space event with no canvas, before any of the packet's events runs.
|
||||
#[test]
|
||||
fn synthetic_pointer_rejects_an_unknown_kind_before_any_event_runs() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
||||
@@ -471,6 +471,12 @@ fn synthetic_pointer_rejects_an_unknown_kind_before_any_event_runs() {
|
||||
for (last, error) in [
|
||||
(json!({"kind": "wiggle", "x": 145, "y": 125}), "unknown pointer kind `wiggle`"),
|
||||
(json!({"kind": "move", "x": 145, "y": 125, "space": "screen"}), "no canvas yet"),
|
||||
(json!({"kind": "move", "x": 145, "y": 125, "pressure": "bad"}), "`pressure` must be a number"),
|
||||
(json!({"kind": "move", "x": 145, "y": 125, "holdMs": "800"}), "`holdMs` must be a number"),
|
||||
(json!({"kind": "move", "x": 145, "y": 125, "tiltY": true}), "`tiltY` must be a number"),
|
||||
(json!({"kind": "move", "x": 145, "y": 125, "rotation": "90"}), "`rotation` must be a number"),
|
||||
(json!({"kind": "move", "x": 145, "y": 125, "space": "screeen"}), "`space` must be doc or screen"),
|
||||
(json!({"kind": "move", "x": 145, "y": 125, "space": true}), "`space` must be doc or screen"),
|
||||
] {
|
||||
let events = moves.iter().cloned().chain([last]).collect::<Vec<_>>();
|
||||
let r = input_request(&mut app, "ui.pointer", json!({"events": events}));
|
||||
|
||||
+3
-2
@@ -1272,8 +1272,9 @@ type and change nothing: `view.setZoom` (`zoom`, `center`), `view.screenMode` (`
|
||||
control channel's `ui.key`, `ui.move`, `ui.wheel`, `ui.click`, `ui.drag`, `ui.pointer`, `ui.resize`, `ui.screenshot`,
|
||||
`ui.render` and `ui.set` for their numbers, switches and `mods`, and the headless session's key, pointer and screenshot
|
||||
calls. Omitting a param or giving null keeps its default, with two exceptions: `ui.click`'s `count` and `ui.drag`'s
|
||||
`count` and `steps` refuse null, and a headless pointer event needs its `x` and `y`. A pointer packet with one
|
||||
malformed event sends none of its events:
|
||||
`count` and `steps` refuse null, and a headless pointer event needs its `x` and `y`. A pointer event's `pressure`,
|
||||
`holdMs`, `tiltX`, `tiltY` and `rotation` must be numbers and its `space` must be `doc` or `screen`. A pointer packet
|
||||
with one malformed event sends none of its events:
|
||||
|
||||
```json
|
||||
{"id": "view.setZoom", "params": {"zoom": "200"}}
|
||||
|
||||
Reference in New Issue
Block a user