core: keep parseKeypress from changing the caller's Buffer (#1653)

`parseKeypress` decoded a single byte above 127 (the legacy 8-bit meta
encoding) by subtracting 128 from the caller's Buffer in place. Parsing
the same Buffer again returned the key without meta, and a caller that
logs, retries, or forwards the raw bytes saw changed data. The renderer
was not affected, because the stdin parser passes a fresh one-byte
Buffer, so only direct callers of the exported function saw it.

It now decodes the byte with `String.fromCharCode(byte - 128)` and
leaves
the Buffer alone. The returned key is the same as before for every
input; no other code in `parseKeypress` or its callers writes into an
input Buffer.

The Buffer tests in `parse.keypress.test.ts` are now one table that
checks the key and that the Buffer is byte for byte unchanged after the
call: ASCII, a two-byte character, 127, 128, the high-bit meta bytes
160, 225, and 255, a Kitty sequence, and an SGR mouse report. Restoring
the in-place write, decoding the wrong byte, or moving the bounds of the
single-byte check each fail it.

Fixes #1638
This commit is contained in:
Simon Klee authored and GitHub committed 2026-10-12 07:56:37 +02:00
1 parent ae3193f169
commit 399fcac46c
2 files changed
+25 -32

No files matched your search

+24 -30
View File
@@ -887,36 +887,30 @@ test("parseKeypress - backspace key with modifiers (Kitty keyboard protocol)", (
expect(shiftBackspace.source).toBe("kitty")
})
test("parseKeypress - Buffer input", () => {
const buf = Buffer.from("a")
expect(parseKeypress(buf)).toEqual({
eventType: "press",
name: "a",
ctrl: false,
meta: false,
shift: false,
option: false,
number: false,
sequence: "a",
raw: "a",
source: "raw",
})
})
test("parseKeypress - high byte buffer handling", () => {
const buf = Buffer.from([160]) // 128 + 32, should become \x1b + " "
expect(parseKeypress(buf)).toEqual({
eventType: "press",
name: "space",
ctrl: false,
meta: true,
shift: false,
option: false,
number: false,
sequence: "\x1b ",
raw: "\x1b ",
source: "raw",
})
test("parseKeypress - Buffer input leaves the buffer unchanged", () => {
const press = { ctrl: false, shift: false, option: false, number: false, eventType: "press", source: "raw" } as const
const key = (name: string, sequence: string, meta = false) => ({ ...press, name, meta, sequence, raw: sequence })
const kittyFlags = { super: false, hyper: false, capsLock: false, numLock: false }
const cases: Array<[Buffer, boolean, ParsedKey | null]> = [
[Buffer.from("a"), false, key("a", "a")],
[Buffer.from("é"), false, key("é", "é")], // two bytes, the first above 127
[Buffer.from([127]), false, key("backspace", "\x7f")],
[Buffer.from([128]), false, { ...key("space", "\x1b\x00", true), ctrl: true }],
[Buffer.from([160]), false, key("space", "\x1b ", true)], // 128 + 32
[Buffer.from([225]), false, key("a", "\x1ba", true)],
[Buffer.from([255]), false, key("backspace", "\x1b\x7f", true)],
[
Buffer.from("\x1b[97;5u"),
true,
{ ...key("a", "a"), ...kittyFlags, ctrl: true, raw: "\x1b[97;5u", source: "kitty" },
],
[Buffer.from("\x1b[<0;1;1M"), false, null],
]
for (const [buf, useKittyKeyboard, expected] of cases) {
const before = Buffer.from(buf)
expect(parseKeypress(buf, { useKittyKeyboard })).toEqual(expected)
expect(buf).toEqual(before)
}
})
test("parseKeypress - empty input", () => {
+1 -2
View File
@@ -216,8 +216,7 @@ export const parseKeypress = (s: Buffer | string = "", options: ParseKeypressOpt
if (Buffer.isBuffer(s)) {
if (s[0]! > 127 && s[1] === undefined) {
;(s[0] as unknown as number) -= 128
s = "\x1b" + String(s)
s = "\x1b" + String.fromCharCode(s[0]! - 128)
} else {
s = String(s)
}