ui: bound queued synthetic input gestures (#2197)

* ui: bound queued synthetic input gestures

* ui: reserve exact click event count
This commit is contained in:
Dylan M. Taylor
2026-10-23 08:45:10 -04:00
committed by GitHub
parent 2cf90507f3
commit 4b48482e8f
2 changed files with 47 additions and 0 deletions
+29
View File
@@ -321,6 +321,26 @@ pub fn handle(app: &mut EffectcraftApp, ctx: &egui::Context, req: &ControlReques
Err(e) => return err(e),
};
let m = modifiers(p);
// Validate the complete gesture before mutating the queue; the loopback control
// channel is externally reachable and must not amplify tiny requests into huge work.
const MAX_PENDING_EVENTS: usize = 4096;
const MAX_CLICK_COUNT: u64 = 512;
let pending = app.synthetic.len();
let gesture_events = if req.method == "ui.click" {
let count = p.get("count").and_then(Value::as_u64).unwrap_or(1);
if count == 0 || count > MAX_CLICK_COUNT {
return err(format!("`count` must be between 1 and {MAX_CLICK_COUNT}"));
}
let Some(events) = usize::try_from(count).ok().and_then(|n| n.checked_mul(2)).and_then(|n| n.checked_add(3)) else {
return err("click event count overflow");
};
events
} else {
3
};
if pending.checked_add(gesture_events).is_none_or(|n| n > MAX_PENDING_EVENTS) {
return err("synthetic input queue is full");
}
// egui keeps the modifiers from `ModifiersChanged` (not the ones on a pointer event)
// and the synthetic events arrive one per frame: hold them for the whole gesture.
app.synthetic.push(egui::Event::ModifiersChanged(m));
@@ -348,6 +368,15 @@ pub fn handle(app: &mut EffectcraftApp, ctx: &egui::Context, req: &ControlReques
(Err(e), _) | (_, Err(e)) => return err(e),
};
let steps = p.get("steps").and_then(Value::as_u64).unwrap_or(12).max(2);
const MAX_PENDING_EVENTS: usize = 4096;
const MAX_DRAG_STEPS: u64 = 2048;
if steps > MAX_DRAG_STEPS {
return err(format!("`steps` must be at most {MAX_DRAG_STEPS}"));
}
let Some(gesture_events) = usize::try_from(steps).ok().and_then(|n| n.checked_add(5)) else { return err("drag event count overflow") };
if app.synthetic.len().checked_add(gesture_events).is_none_or(|n| n > MAX_PENDING_EVENTS) {
return err("synthetic input queue is full");
}
let m = modifiers(p);
app.synthetic.push(egui::Event::ModifiersChanged(m));
app.synthetic.push(egui::Event::PointerMoved(a));
@@ -37,6 +37,24 @@ fn send(app: &mut EffectcraftApp, method: &str, params: Value) {
let _ = control::handle(app, &ctx, &req);
}
#[test]
fn control_gestures_reject_excessive_counts_without_queue_mutation() {
let mut app = EffectcraftApp::new(Session::default());
send(&mut app, "ui.click", json!({"x": 10, "y": 10, "count": u64::MAX}));
assert!(app.take_synthetic_input().is_empty(), "an oversized click must not partially queue input");
send(&mut app, "ui.drag", json!({"from": {"x": 10, "y": 10}, "to": {"x": 90, "y": 40}, "steps": u64::MAX}));
assert!(app.take_synthetic_input().is_empty(), "an oversized drag must not partially queue input");
send(&mut app, "ui.click", json!({"x": 10, "y": 10, "count": 512}));
send(&mut app, "ui.click", json!({"x": 10, "y": 10, "count": 512}));
assert_eq!(app.take_synthetic_input().len(), 2054, "two maximum accepted clicks fit under the aggregate cap");
for _ in 0..4 {
send(&mut app, "ui.click", json!({"x": 10, "y": 10, "count": 512}));
}
assert_eq!(app.take_synthetic_input().len(), 3081, "the pending event limit is enforced before requests can exceed it");
}
#[test]
fn shift_click_holds_shift_from_press_to_release() {
let mut app = EffectcraftApp::new(Session::default());