From c7dbc96654d356fd9d00c159be63f3ecc48fcfaf Mon Sep 17 00:00:00 2001 From: Aikiooo <78739437+Aikiooo@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:23:03 +0200 Subject: [PATCH] fix(fs): detect binary files that contain no NUL byte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FS_READ` decided binary vs text with `buffer.includes(0)`. JPEG, PNG, PDF and ZIP files do not have to contain a NUL byte, and Node's `toString("utf-8")` is not fatal — invalid sequences become U+FFFD instead of being reported. Those files were sent with `encoding: "utf-8"`, so the client re-encoded the replaced characters and the bytes were lost. Use the heuristic the app already applies in `isLikelyBinaryBytes` — a NUL byte, a fatal UTF-8 decode, then a control-character ratio — so both ends agree on what counts as binary. --- cli/src/filesystem/index.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/cli/src/filesystem/index.ts b/cli/src/filesystem/index.ts index 5b5f8df..bc53f99 100644 --- a/cli/src/filesystem/index.ts +++ b/cli/src/filesystem/index.ts @@ -61,6 +61,36 @@ function safePath(rootDir: string, requestedPath: string): string | null { return resolved; } +/** + * Whether a buffer should be sent as base64 rather than decoded as UTF-8. + * + * A NUL-byte check alone misses binary files that happen to have no NUL early + * on — JPEG, PNG, PDF and ZIP do not have to contain one. `toString("utf-8")` + * is not fatal, so those bytes become U+FFFD and `FS_READ_RESULT` carries + * `encoding: "utf-8"`, which makes the client re-encode the damaged string. + * Mirrors `isLikelyBinaryBytes` on the app side so both ends agree. + */ +function looksBinary(buffer: Buffer): boolean { + const sample = buffer.subarray(0, 8000); + if (sample.length === 0) return false; + + let suspicious = 0; + for (const byte of sample) { + if (byte === 0) return true; + if (byte < 7 || (byte > 13 && byte < 32) || byte === 127) { + suspicious++; + } + } + + try { + new TextDecoder("utf-8", { fatal: true }).decode(sample); + } catch { + return true; + } + + return suspicious / sample.length > 0.3; +} + function findNearestExistingDir(targetPath: string): string | null { let current = targetPath; while (true) { @@ -185,7 +215,7 @@ export function initFilesystemHandler(conn: Connection, rootDir: string) { } const buffer = fs.readFileSync(filePath); - const isBinary = buffer.includes(0); + const isBinary = looksBinary(buffer); const respMsg: FsReadResultMsg = { type: MsgType.FS_READ_RESULT,