pm-grid: hardening - bound the USB-MIDI TX queue + defensive set-list guard

Audit for panic/brick risks (a panic = black screen on this device):
- sx_send (live-sync broadcasts + 5s heartbeat) pushed to tx_q with no cap. If
  the editor disconnects without a BYE while sync_armed and nothing drains
  MIDI-IN, tx_q grows unbounded -> heap exhaustion -> brick. Now drops messages
  when tx_q > 256 (the heartbeat re-syncs when the host returns). Notes/clock
  were already capped.
- build_setlists now drops empty set lists, so load()/next_track() can never
  hit a `% 0`. (parse guarantees >=1 lane; built-ins/parsed lists are non-empty,
  this is belt-and-suspenders.)

Other unwrap()s are boot-time peripheral init; lanes[0]/items[0]/step[0] are all
safe (parse substitutes beep:4 for empty programs; built-ins lead the list).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-06-04 11:21:06 -05:00
parent dd27d553fe
commit 72dbb2ecd0

View file

@ -216,6 +216,7 @@ fn build_setlists(user: Vec<SetList>) -> Vec<SetList> {
})
.collect();
v.extend(user);
v.retain(|s| !s.items.is_empty()); // never keep an empty set list (would `% 0` in load/next)
v
}
@ -946,6 +947,12 @@ impl App {
/// Build `F0 7D <op> <text(ASCII-clamped)> F7`, packetize into 4-byte USB-MIDI events, queue them.
fn sx_send(&mut self, op: u8, text: &str) {
// Bound the TX queue: if the host stopped draining MIDI-IN (e.g. editor closed without a BYE
// while sync_armed), drop the message rather than grow the heap forever. The 5 s heartbeat
// re-syncs once the host returns.
if self.tx_q.len() > 256 {
return;
}
let mut f: Vec<u8> = Vec::with_capacity(text.len() + 4);
f.push(0xF0);
f.push(0x7D);