From 1727d01d88dc3a53ab34fd82b053241ef5f5b8c7 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 30 Oct 2026 07:29:08 +0000 Subject: [PATCH 1/2] test(native): cover typed-array handling in the addon feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The non-Uint8Array cases fail: napi_is_buffer accepts every ArrayBufferView, so the Buffer branch takes them first. Signed-off-by: Thomas Kosiewski --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `medium`_ Change-Id: Iae93d5917e6918dd8161be33ba7cf7ba3d8c173c --- test/terminal.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/terminal.test.ts b/test/terminal.test.ts index 57c8ad7..4de34c9 100644 --- a/test/terminal.test.ts +++ b/test/terminal.test.ts @@ -522,6 +522,39 @@ describeIfNative("GhosttyVtTerminal", () => { expect((thrown as Error).message).toMatch(message); }); + it.each<[string, () => unknown, RegExp]>([ + ["Uint16Array", () => new Uint16Array([0x4241]), /^feed typed array must be Uint8Array$/], + ["Float32Array", () => new Float32Array([1]), /^feed typed array must be Uint8Array$/], + ["Uint8ClampedArray", () => new Uint8ClampedArray([65, 66]), /^feed typed array must be Uint8Array$/], + ["DataView", () => new DataView(new Uint8Array([65, 66]).buffer), /^feed data must be a string, Buffer, or Uint8Array$/], + ])("rejects a %s in the addon feed with a TypeError", (_name, makeData, message) => { + const term = loadNative().createTerminal({ cols: 10, rows: 1 }); + let thrown: unknown; + try { + term.feed(makeData() as Uint8Array); + } catch (error) { + thrown = error; + } finally { + term.dispose(); + } + expect(thrown).toBeInstanceOf(TypeError); + expect((thrown as Error).message).toMatch(message); + }); + + it.each<[string, () => Uint8Array]>([ + ["Buffer", () => Buffer.from("AB")], + ["Uint8Array", () => new Uint8Array([65, 66])], + ["Buffer slice with a byte offset", () => Buffer.from("xxABxx").subarray(2, 4)], + ])("writes a %s in the addon feed", (_name, makeData) => { + const term = loadNative().createTerminal({ cols: 10, rows: 1 }); + try { + term.feed(makeData()); + expect(term.getVisibleText()).toBe("AB"); + } finally { + term.dispose(); + } + }); + it("makes dispose idempotent and rejects use after dispose", () => { const term = createTerminal({ cols: 80, rows: 24 }); term.dispose(); From fabeeaf88eb2eab2ac071145672c2c754ed7b881 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 30 Oct 2026 07:29:37 +0000 Subject: [PATCH 2/2] fix(native): reject typed arrays other than Uint8Array in feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit napi_is_buffer is true for every ArrayBufferView, so the IsBuffer branch read Uint16Array, Float32Array, Uint8ClampedArray and DataView as bytes before the Uint8Array check could run. A Buffer is a Uint8Array, so the typed-array branch covers it. Signed-off-by: Thomas Kosiewski --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `medium`_ Change-Id: Ib6eeb52941e0da75444187109d01e5b0192d1727 --- native/README.md | 2 +- native/terminal.cc | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/native/README.md b/native/README.md index 3c7b729..0373d9b 100644 --- a/native/README.md +++ b/native/README.md @@ -76,5 +76,5 @@ C++ declarations: - Run (after `npm ci`): `npm run build:libghostty && npm run build:native && npm test` - Every native test skips when the addon does not load (`test/native-support.ts:nativeSupport`), so check the test output for skips. - `mise run ci-ubsan` builds the addon with UBSan (Linux, GCC), then runs the tests and smoke. Undefined behavior aborts the run. The task also fails when the addon's object files contain no UBSan checks, which means the flags did not reach g++ (`mise.toml:tasks.ci-ubsan`). -- `test/workers.test.ts` checks the per-environment state in child processes with `worker_threads`. `test/terminal.test.ts` covers `feed`, `snapshot`, `resize`, `dispose`, the `RangeError` cases of the `createTerminal` checks in the addon, the `encodeMouse` bytes of each mouse format, `cursorVisible`, the cell style fields, DECSTR, and whether `scrollbackLimit` keeps history. `test/snapshot-safety.test.ts` checks in child processes that `Object.prototype` accessors cannot dispose the terminal mid-`snapshot`. `test/mouse-safety.test.ts` runs the `encodeMouse` limits and the getter re-entry cases against both the package API and the raw addon object. It skips when the addon is missing, like the other native suites. Its child-process cases load `src/` through `tsx`, so they need no `dist/` build. No vitest test covers `formatPlain`, `formatHtml`, the content of `scrollbackLines`, or the typed-array errors. +- `test/workers.test.ts` checks the per-environment state in child processes with `worker_threads`. `test/terminal.test.ts` covers `feed`, `snapshot`, `resize`, `dispose`, the `RangeError` cases of the `createTerminal` checks in the addon, the `encodeMouse` bytes of each mouse format, `cursorVisible`, the cell style fields, DECSTR, and whether `scrollbackLimit` keeps history. `test/snapshot-safety.test.ts` checks in child processes that `Object.prototype` accessors cannot dispose the terminal mid-`snapshot`. `test/mouse-safety.test.ts` runs the `encodeMouse` limits and the getter re-entry cases against both the package API and the raw addon object. It skips when the addon is missing, like the other native suites. Its child-process cases load `src/` through `tsx`, so they need no `dist/` build. No vitest test covers `formatPlain`, `formatHtml`, or the content of `scrollbackLines`. - For changes to `TerminalWrap` or `InitAll`, also run the TLA+ lifecycle model: [`.agents/skills/verify-native-lifecycle/SKILL.md`](https://github.com/coder/libghostty-vt-node/blob/main/.agents/skills/verify-native-lifecycle/SKILL.md). diff --git a/native/terminal.cc b/native/terminal.cc index add2a29..d41f88a 100644 --- a/native/terminal.cc +++ b/native/terminal.cc @@ -307,10 +307,6 @@ Napi::Value TerminalWrap::Feed(const Napi::CallbackInfo& info) { string_data = info[0].As().Utf8Value(); data = reinterpret_cast(string_data.data()); len = string_data.size(); - } else if (info[0].IsBuffer()) { - const auto buffer = info[0].As>(); - data = buffer.Data(); - len = buffer.Length(); } else if (info[0].IsTypedArray()) { const auto typed_array = info[0].As(); if (typed_array.TypedArrayType() != napi_uint8_array) {