| author | |
| committer | |
| log | 1cbd88aba042f3209e1538a927775a7c4f7fdb06 |
| tree | ffd87568616e3ca30b266c61d86a608ef5547ec7 |
| parent | 553867262cf22f982622df3eb7f3174f62b4ba2b |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 85 insertions(+), 2 deletions(-)
apps/server/src/terminal/Manager.test.ts+55| ... | ... | @@ -991,6 +991,61 @@ it.layer( |
| 991 | 991 | }), |
| 992 | 992 | ); |
| 993 | 993 | |
| 994 | it.effect("strips replayable CSI and DCS traffic while preserving setters", () => | |
| 995 | Effect.gen(function* () { | |
| 996 | const { manager, ptyAdapter } = yield* createManager(); | |
| 997 | yield* manager.open(openInput()); | |
| 998 | const process = ptyAdapter.processes[0]; | |
| 999 | expect(process).toBeDefined(); | |
| 1000 | if (!process) return; | |
| 1001 | ||
| 1002 | process.emitData("prompt "); | |
| 1003 | // DECRQM/DECRPM, XTVERSION, and kitty-keyboard CSI query/reply traffic. | |
| 1004 | process.emitData("\u001b[?2026$p\u001b[?2026;2$y\u001b[>q\u001b[?u\u001b[?31u"); | |
| 1005 | // DECRQSS and XTGETTCAP query/reply traffic in 7-bit DCS form. | |
| 1006 | process.emitData("\u001bP$q m\u001b\\\u001bP1$r0m\u001b\\"); | |
| 1007 | process.emitData("\u001bP+q544e\u001b\\\u001bP1+r544e=1b\u001b\\"); | |
| 1008 | // The same DCS traffic in 8-bit form. | |
| 1009 | process.emitData("\u0090$q m\u009c\u00901$r0m\u009c"); | |
| 1010 | process.emitData("\u0090+q544e\u009c\u00901+r544e=1b\u009c"); | |
| 1011 | // Setters and cursor movement share final bytes with query families but | |
| 1012 | // have visible terminal-state value and must survive replay. | |
| 1013 | process.emitData('\u001b[!p\u001b["p\u001b[4 q\u001b[u'); | |
| 1014 | process.emitData("done\n"); | |
| 1015 | ||
| 1016 | yield* manager.close({ threadId: "thread-1" }); | |
| 1017 | ||
| 1018 | const reopened = yield* manager.open(openInput()); | |
| 1019 | assert.equal(reopened.history, 'prompt \u001b[!p\u001b["p\u001b[4 q\u001b[udone\n'); | |
| 1020 | }), | |
| 1021 | ); | |
| 1022 | ||
| 1023 | it.effect("handles CSI and DCS query sequences split across output chunks", () => | |
| 1024 | Effect.gen(function* () { | |
| 1025 | const { manager, ptyAdapter } = yield* createManager(); | |
| 1026 | yield* manager.open(openInput()); | |
| 1027 | const process = ptyAdapter.processes[0]; | |
| 1028 | expect(process).toBeDefined(); | |
| 1029 | if (!process) return; | |
| 1030 | ||
| 1031 | process.emitData("before "); | |
| 1032 | process.emitData("\u001b[?2026$"); | |
| 1033 | process.emitData("pafter "); | |
| 1034 | process.emitData("\u001bP$q "); | |
| 1035 | process.emitData("m\u001b"); | |
| 1036 | process.emitData("\\after "); | |
| 1037 | process.emitData("\u009b?3"); | |
| 1038 | process.emitData("1uafter "); | |
| 1039 | process.emitData("\u0090+q544e"); | |
| 1040 | process.emitData("\u009cafter\n"); | |
| 1041 | ||
| 1042 | yield* manager.close({ threadId: "thread-1" }); | |
| 1043 | ||
| 1044 | const reopened = yield* manager.open(openInput()); | |
| 1045 | assert.equal(reopened.history, "before after after after after\n"); | |
| 1046 | }), | |
| 1047 | ); | |
| 1048 | ||
| 994 | 1049 | it.effect( |
| 995 | 1050 | "preserves clear and style control sequences while dropping chunk-split query traffic", |
| 996 | 1051 | () => |
apps/server/src/terminal/Manager.ts+30-2| ... | ... | @@ -878,9 +878,31 @@ function shouldStripCsiSequence(body: string, finalByte: string): boolean { |
| 878 | 878 | if (finalByte === "c" && /^[>0-9;?]*$/.test(body)) { |
| 879 | 879 | return true; |
| 880 | 880 | } |
| 881 | // DECRQM mode queries (…$p) and DECRPM replies (…$y): replaying a stored | |
| 882 | // query makes the terminal answer again, and the shell echoes the answer as | |
| 883 | // junk at the prompt. The `$` guard keeps setters like DECSTR (!p) and | |
| 884 | // DECSCL ("p) intact. | |
| 885 | if ((finalByte === "p" || finalByte === "y") && /^[0-9;?]*\$$/.test(body)) { | |
| 886 | return true; | |
| 887 | } | |
| 888 | // XTVERSION query (>q). DECSCUSR (space-intermediate q) stays. | |
| 889 | if (finalByte === "q" && /^>[0-9;]*$/.test(body)) { | |
| 890 | return true; | |
| 891 | } | |
| 892 | // Kitty keyboard protocol query/reply (?u). Restore-cursor (bare u) stays. | |
| 893 | if (finalByte === "u" && body.startsWith("?")) { | |
| 894 | return true; | |
| 895 | } | |
| 881 | 896 | return false; |
| 882 | 897 | } |
| 883 | 898 | |
| 899 | // DECRQSS ($q) and XTGETTCAP (+q) queries plus their replies ([01]$r / [01]+r): | |
| 900 | // pure request/response traffic with no visual value, and replaying a stored | |
| 901 | // query triggers a fresh reply. | |
| 902 | function shouldStripDcsSequence(content: string): boolean { | |
| 903 | return /^[01]?[$+][qr]/.test(content); | |
| 904 | } | |
| 905 | ||
| 884 | 906 | function shouldStripOscSequence(content: string): boolean { |
| 885 | 907 | return /^(10|11|12);(?:\?|rgb:)/.test(content); |
| 886 | 908 | } |
| ... | ... | @@ -981,7 +1003,10 @@ function sanitizeTerminalHistoryChunk( |
| 981 | 1003 | } |
| 982 | 1004 | const sequence = input.slice(index, terminatorIndex); |
| 983 | 1005 | const content = stripStringTerminator(input.slice(index + 2, terminatorIndex)); |
| 984 | if (nextCodePoint !== 0x5d || !shouldStripOscSequence(content)) { | |
| 1006 | const strip = | |
| 1007 | (nextCodePoint === 0x5d && shouldStripOscSequence(content)) || | |
| 1008 | (nextCodePoint === 0x50 && shouldStripDcsSequence(content)); | |
| 1009 | if (!strip) { | |
| 985 | 1010 | append(sequence); |
| 986 | 1011 | } |
| 987 | 1012 | index = terminatorIndex; |
| ... | ... | @@ -1024,7 +1049,10 @@ function sanitizeTerminalHistoryChunk( |
| 1024 | 1049 | } |
| 1025 | 1050 | const sequence = input.slice(index, terminatorIndex); |
| 1026 | 1051 | const content = stripStringTerminator(input.slice(index + 1, terminatorIndex)); |
| 1027 | if (codePoint !== 0x9d || !shouldStripOscSequence(content)) { | |
| 1052 | const strip = | |
| 1053 | (codePoint === 0x9d && shouldStripOscSequence(content)) || | |
| 1054 | (codePoint === 0x90 && shouldStripDcsSequence(content)); | |
| 1055 | if (!strip) { | |
| 1028 | 1056 | append(sequence); |
| 1029 | 1057 | } |
| 1030 | 1058 | index = terminatorIndex; |