a73x

93103d11

fix: narrow web osc 52 writes to the clipboard target

a73x   2026-08-18 12:27

Commit message
fix: narrow web osc 52 writes to the clipboard target

web/mux.js
Old New
@@ -19,6 +19,8 @@ const CLIENT_ACTION = {
19 ignored: 0, terminalModes: 1, clipboard: 2, bell: 3, selection: 4, 19 ignored: 0, terminalModes: 1, clipboard: 2, bell: 3, selection: 4,
20 }; 20 };
21 const ENV_FRAME = 0x00, ENV_CONTROL = 0x01; 21 const ENV_FRAME = 0x00, ENV_CONTROL = 0x01;
22 // OSC 52's Pc for the system clipboard, the only target this page can honour.
23 const CLIPBOARD_TARGET_C = 0x63; // 'c'
22 24
23 // keymap.Key by @intFromEnum (wasm_core.zig pins the table). 25 // keymap.Key by @intFromEnum (wasm_core.zig pins the table).
24 const KEY = { 26 const KEY = {
@@ -548,6 +550,15 @@ class Tile {
548 550
549 onClipboardEffect() { 551 onClipboardEffect() {
550 if (!this.zoomed) return; 552 if (!this.zoomed) return;
553 // The shared core accepts every Pc xterm defines, because the native
554 // client can honour them: `p` is PRIMARY, `s` the selection, `0`-`7`
555 // the cut buffers. A browser has one destination and no way to route
556 // the others, so writing them all to the system clipboard would let an
557 // ordinary X11 mouse drag inside the session — which emits
558 // `ESC]52;p;…` — silently clobber what the human last copied on their
559 // own machine. Non-`c` targets stay valid on the wire and are simply
560 // not this tile's business.
561 if (this.core.mux_clipboard_target() !== CLIPBOARD_TARGET_C) return;
551 562
552 // The semantic core's clipboard payload borrows the staging buffer. 563 // The semantic core's clipboard payload borrows the staging buffer.
553 // Copy it before calling or awaiting anything: either can move WASM 564 // Copy it before calling or awaiting anything: either can move WASM
web/verify.js
Old New
@@ -465,6 +465,7 @@ async function verifyClipboardShell(shell, html) {
465 new Uint8Array(memory.buffer).set(Buffer.from(encoded, 'ascii'), 17); 465 new Uint8Array(memory.buffer).set(Buffer.from(encoded, 'ascii'), 17);
466 tile.core = { 466 tile.core = {
467 memory, 467 memory,
468 mux_clipboard_target: () => 'c'.charCodeAt(0),
468 mux_clipboard_ptr: () => 17, 469 mux_clipboard_ptr: () => 17,
469 mux_clipboard_len: () => encoded.length, 470 mux_clipboard_len: () => encoded.length,
470 }; 471 };
@@ -500,6 +501,24 @@ async function verifyClipboardShell(shell, html) {
500 check('invalid UTF-8 effect does not advance version', invalidEffect.tile.clipboardVersion, 0); 501 check('invalid UTF-8 effect does not advance version', invalidEffect.tile.clipboardVersion, 0);
501 check('invalid UTF-8 effect leaves copy control reset', `${invalidEffect.tile.copyButton.className}|${invalidEffect.tile.copyButton.textContent}`, 'copy-request|Copy'); 502 check('invalid UTF-8 effect leaves copy control reset', `${invalidEffect.tile.copyButton.className}|${invalidEffect.tile.copyButton.textContent}`, 'copy-request|Copy');
502 503
504 // The protocol accepts every Pc the native client can route, but this
505 // page has exactly one destination. `p` is what an ordinary X11 mouse
506 // drag writes, and proxying it here would clobber the browser user's
507 // clipboard from a program that only ever asked for PRIMARY.
508 const primaryTarget = tileFor('PRIMARY, not the clipboard');
509 primaryTarget.tile.core.mux_clipboard_target = () => 'p'.charCodeAt(0);
510 h.navigator.clipboard = { writeText: () => { throw new Error('must not write'); } };
511 await primaryTarget.tile.onClipboardEffect();
512 check('non-c OSC 52 target creates no pending clipboard text', primaryTarget.tile.pendingClipboard, null);
513 check('non-c OSC 52 target does not advance the clipboard version', primaryTarget.tile.clipboardVersion, 0);
514 check('non-c OSC 52 target leaves the copy control reset', `${primaryTarget.tile.copyButton.className}|${primaryTarget.tile.copyButton.textContent}`, 'copy-request|Copy');
515
516 const clipboardTarget = tileFor('the real clipboard');
517 const clipboardTargetWrites = [];
518 h.navigator.clipboard = { writeText: (text) => { clipboardTargetWrites.push(text); return Promise.resolve(); } };
519 await clipboardTarget.tile.onClipboardEffect();
520 check('target c OSC 52 still reaches the system clipboard', clipboardTargetWrites.join('|'), 'the real clipboard');
521
503 ignored.tile.pendingClipboard = 'must stay local'; 522 ignored.tile.pendingClipboard = 'must stay local';
504 h.navigator.clipboard = { writeText: () => { throw new Error('must not write'); } }; 523 h.navigator.clipboard = { writeText: () => { throw new Error('must not write'); } };
505 check('manual clipboard retry is ignored while unzoomed', ignored.tile.copyPendingClipboard(), undefined); 524 check('manual clipboard retry is ignored while unzoomed', ignored.tile.copyPendingClipboard(), undefined);
@@ -792,6 +811,7 @@ async function verifySelectionShell(shell, html) {
792 mux_init: () => 0, 811 mux_init: () => 0,
793 mux_attach_payload: () => 20, 812 mux_attach_payload: () => 20,
794 mux_client_frame: (type) => type === 0x90 ? 4 : 0, 813 mux_client_frame: (type) => type === 0x90 ? 4 : 0,
814 mux_clipboard_target: () => 'c'.charCodeAt(0),
795 mux_selection_id: () => result.id | 0, 815 mux_selection_id: () => result.id | 0,
796 mux_selection_status: () => result.status, 816 mux_selection_status: () => result.status,
797 mux_selection_history_rows: () => result.historyRows | 0, 817 mux_selection_history_rows: () => result.historyRows | 0,