c707efd5
test: harden wasm client state resets
a73x 2026-08-18 12:27
Commit message
src/wasm_core.zig
| Old | New | ||
|---|---|---|---|
| @@ -254,13 +254,13 @@ export fn mux_apply_frame(msg_type: u32, len: u32) i32 { | |||
| 254 | /// the host stages the next frame; getters never copy or allocate them. | 254 | /// the host stages the next frame; getters never copy or allocate them. |
| 255 | export fn mux_client_frame(msg_type: u32, len: u32) i32 { | 255 | export fn mux_client_frame(msg_type: u32, len: u32) i32 { |
| 256 | const c = core orelse return -1; | 256 | const c = core orelse return -1; |
| 257 | c.clipboard = .{ .target = 0, .base64 = &.{} }; | ||
| 257 | if (len > input_buf.len) return -2; | 258 | if (len > input_buf.len) return -2; |
| 258 | if (msg_type > 0xff) return @intFromEnum(ClientAction.ignored); | 259 | if (msg_type > 0xff) return @intFromEnum(ClientAction.ignored); |
| 259 | 260 | ||
| 260 | // MsgType is deliberately non-exhaustive, so every u8 is a valid enum | 261 | // MsgType is deliberately non-exhaustive, so every u8 is a valid enum |
| 261 | // value. Unknown wire bytes reach ClientCore and are ignored safely. | 262 | // value. Unknown wire bytes reach ClientCore and are ignored safely. |
| 262 | const t: proto.MsgType = @enumFromInt(@as(u8, @intCast(msg_type))); | 263 | const t: proto.MsgType = @enumFromInt(@as(u8, @intCast(msg_type))); |
| 263 | c.clipboard = .{ .target = 0, .base64 = &.{} }; | ||
| 264 | return switch (c.client.receive(t, input_buf[0..len])) { | 264 | return switch (c.client.receive(t, input_buf[0..len])) { |
| 265 | .ignored => @intFromEnum(ClientAction.ignored), | 265 | .ignored => @intFromEnum(ClientAction.ignored), |
| 266 | .state => |state| switch (state) { | 266 | .state => |state| switch (state) { |
web/verify.js
| Old | New | ||
|---|---|---|---|
| @@ -104,8 +104,12 @@ async function main() { | |||
| 104 | 104 | ||
| 105 | // --- shared client semantics ABI --- | 105 | // --- shared client semantics ABI --- |
| 106 | // Pasting is raw until the daemon explicitly samples mode 2004 as on. | 106 | // Pasting is raw until the daemon explicitly samples mode 2004 as on. |
| 107 | stage(Buffer.from('seed')); | ||
| 108 | check('seed output before disabled begin', e.mux_text_encode(4), 4); | ||
| 107 | check('paste begin disabled', e.mux_paste_begin(), 0); | 109 | check('paste begin disabled', e.mux_paste_begin(), 0); |
| 108 | check('paste begin disabled clears output', e.mux_output_len(), 0); | 110 | check('paste begin disabled clears output', e.mux_output_len(), 0); |
| 111 | stage(Buffer.from('again')); | ||
| 112 | check('seed output before disabled end', e.mux_text_encode(5), 5); | ||
| 109 | check('paste end disabled', e.mux_paste_end(), 0); | 113 | check('paste end disabled', e.mux_paste_end(), 0); |
| 110 | check('paste end disabled clears output', e.mux_output_len(), 0); | 114 | check('paste end disabled clears output', e.mux_output_len(), 0); |
| 111 | 115 | ||
| @@ -134,6 +138,15 @@ async function main() { | |||
| 134 | check('ignored event clears clipboard getter', e.mux_clipboard_len(), 0); | 138 | check('ignored event clears clipboard getter', e.mux_clipboard_len(), 0); |
| 135 | check('client unknown u8 type ignored', e.mux_client_frame(0x40, 0), clientAction.ignored); | 139 | check('client unknown u8 type ignored', e.mux_client_frame(0x40, 0), clientAction.ignored); |
| 136 | 140 | ||
| 141 | check('clipboard before oversize guard', e.mux_client_frame(0x8f, stage(clipboard)), clientAction.clipboard); | ||
| 142 | check('client oversize length', e.mux_client_frame(0x8f, e.mux_input_cap() + 1), -2); | ||
| 143 | check('oversize clears clipboard len', e.mux_clipboard_len(), 0); | ||
| 144 | check('oversize clears clipboard target', e.mux_clipboard_target(), 0); | ||
| 145 | check('clipboard before wide-type guard', e.mux_client_frame(0x8f, stage(clipboard)), clientAction.clipboard); | ||
| 146 | check('client wide type ignored', e.mux_client_frame(0x100, 0), clientAction.ignored); | ||
| 147 | check('wide type clears clipboard len', e.mux_clipboard_len(), 0); | ||
| 148 | check('wide type clears clipboard target', e.mux_clipboard_target(), 0); | ||
| 149 | |||
| 137 | // --- attach payload before any state: quotes (0,0); wall spelling 1x1 --- | 150 | // --- attach payload before any state: quotes (0,0); wall spelling 1x1 --- |
| 138 | check('attach len', e.mux_attach_payload(1, 1, 0), 20); | 151 | check('attach len', e.mux_attach_payload(1, 1, 0), 20); |
| 139 | let att = outBytes(); | 152 | let att = outBytes(); |
| @@ -304,10 +317,15 @@ async function main() { | |||
| 304 | check('instantiate(bytes) is the OTHER shape', fromBytes.instance instanceof WebAssembly.Instance, true); | 317 | check('instantiate(bytes) is the OTHER shape', fromBytes.instance instanceof WebAssembly.Instance, true); |
| 305 | 318 | ||
| 306 | // --- deinit / re-init --- | 319 | // --- deinit / re-init --- |
| 320 | check('clipboard before lifecycle reset', e.mux_client_frame(0x8f, stage(clipboard)), clientAction.clipboard); | ||
| 321 | check('clipboard populated before lifecycle reset', e.mux_clipboard_len(), 4); | ||
| 322 | check('modes populated before lifecycle reset', e.mux_bracketed_paste(), 1); | ||
| 307 | e.mux_deinit(); | 323 | e.mux_deinit(); |
| 308 | check('apply after deinit', e.mux_apply_frame(0x81, 0), -1); | 324 | check('apply after deinit', e.mux_apply_frame(0x81, 0), -1); |
| 309 | check('client frame after deinit', e.mux_client_frame(0x8d, 0), -1); | 325 | check('client frame after deinit', e.mux_client_frame(0x8d, 0), -1); |
| 310 | check('re-init', e.mux_init(80, 24), 0); | 326 | check('re-init', e.mux_init(80, 24), 0); |
| 327 | check('re-init resets bracketed paste', e.mux_bracketed_paste(), 0); | ||
| 328 | check('re-init resets clipboard', e.mux_clipboard_len(), 0); | ||
| 311 | e.mux_deinit(); | 329 | e.mux_deinit(); |
| 312 | 330 | ||
| 313 | console.log(`verify: ${passed} passed, ${failed} failed`); | 331 | console.log(`verify: ${passed} passed, ${failed} failed`); |