Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,15 @@ async function prepareOperation(cwd: string, hunk: ParsedPatch): Promise<Prepare
if (hunk.type === "delete") return { hunk, absolutePath, destination, fuzz: 0 };
const preservedMode = sourceStat.mode & 0o777;
if (hunk.movePath !== undefined) await requireAbsent(destination);
const currentContent = await readFile(absolutePath, "utf-8");
// Sniff for null bytes before decoding: decoding binary as UTF-8 mangles
// bytes into replacement characters, so a patch could either match
// garbage or silently corrupt the file on write. Reject early with a
// distinct code instead of letting apply_patch guess at binary content.
const raw = await readFile(absolutePath);
if (raw.includes(0)) {
throw Object.assign(new Error(`Refusing to patch binary file: ${hunk.filePath}`), { code: "EBINARY" });
}
const currentContent = raw.toString("utf-8");
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
const result =
hunk.chunks.length === 0
? { content: currentContent, fuzz: 0 }
Expand Down
37 changes: 37 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
APPLY_PATCH_DESCRIPTION,
APPLY_PATCH_LARK_GRAMMAR,
ApplyPatchError,
type ApplyPatchExtensionAPI,
applyPatch,
applyPatchDetailed,
Expand Down Expand Up @@ -227,6 +228,42 @@ describe("pi-apply-patch", () => {
expect(await readFile(path.join(directory, "sample.txt"), "utf-8")).toBe("after\n");
});

it("#given binary file with null byte #when applying patch #then rejects with EBINARY and preserves file", async () => {
// given
const directory = await createTempDirectory();
const binaryPath = path.join(directory, "binary.bin");
await writeFile(binaryPath, Buffer.from([0x00, 0x01, 0x02, 0xff]));

// when
let caught: unknown;
try {
await applyPatch(
directory,
`*** Begin Patch
*** Update File: binary.bin
@@
-some
+changed
*** End Patch`,
);
} catch (error) {
caught = error;
}

// then
if (!(caught instanceof ApplyPatchError)) {
throw new Error("Expected apply_patch to reject with ApplyPatchError");
}
const failure = caught.failures[0];
expect(failure).toMatchObject({
filePath: "binary.bin",
operation: "update",
code: "EBINARY",
});
expect(failure?.message ?? "").toMatch(/binary|non-text|not text/);
expect(await readFile(binaryPath)).toEqual(Buffer.from([0x00, 0x01, 0x02, 0xff]));
});

it("#given parent traversal path #when applying patch #then rejects outside cwd", async () => {
// given
const directory = await createTempDirectory();
Expand Down