fix(core): enforce patch EOF anchors
This commit is contained in:
@@ -157,6 +157,10 @@ function parseUpdate(lines: ReadonlyArray<string>, start: number, end: number) {
|
||||
else if (line.startsWith("+")) newLines.push(line.slice(1))
|
||||
index++
|
||||
}
|
||||
if (lines[index]?.trim() === "*** End of File") {
|
||||
endOfFile = true
|
||||
index++
|
||||
}
|
||||
chunks.push({ oldLines, newLines, changeContext, endOfFile: endOfFile || undefined })
|
||||
}
|
||||
return { chunks, next: index }
|
||||
@@ -192,11 +196,15 @@ function computeReplacements(lines: ReadonlyArray<string>, path: string, chunks:
|
||||
|
||||
function seek(lines: ReadonlyArray<string>, pattern: ReadonlyArray<string>, start: number, eof = false) {
|
||||
if (pattern.length === 0) return -1
|
||||
for (const compare of [exact, rstrip, trim, normalized]) {
|
||||
if (eof) {
|
||||
const offset = lines.length - pattern.length
|
||||
if (offset >= start && matches(lines, pattern, offset, compare)) return offset
|
||||
if (eof) {
|
||||
const offset = lines.length - pattern.length
|
||||
if (offset < start) return -1
|
||||
for (const compare of [exact, rstrip, trim, normalized]) {
|
||||
if (matches(lines, pattern, offset, compare)) return offset
|
||||
}
|
||||
return -1
|
||||
}
|
||||
for (const compare of [exact, rstrip, trim, normalized]) {
|
||||
for (let offset = start; offset <= lines.length - pattern.length; offset++) {
|
||||
if (matches(lines, pattern, offset, compare)) return offset
|
||||
}
|
||||
|
||||
@@ -113,6 +113,21 @@ describe("Patch", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("preserves the end-of-file marker", () => {
|
||||
expect(
|
||||
parse(
|
||||
"*** Begin Patch\n*** Update File: file.txt\n@@\n+quux\n*** End of File\n\n*** End Patch",
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
type: "update",
|
||||
path: "file.txt",
|
||||
movePath: undefined,
|
||||
chunks: [{ oldLines: [], newLines: ["quux"], changeContext: undefined, endOfFile: true }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("derives fuzzy line updates while preserving BOM", () => {
|
||||
const update = Patch.derive("update.txt", [{ oldLines: [" old "], newLines: ["new"] }], "\uFEFFold\n")
|
||||
expect(update).toEqual({ content: "new\n", bom: true })
|
||||
@@ -174,6 +189,16 @@ describe("Patch", () => {
|
||||
).toBe("marker\nmiddle\nmarker changed\nend\n")
|
||||
})
|
||||
|
||||
test("does not fall back to a non-EOF match", () => {
|
||||
expect(() =>
|
||||
Patch.derive(
|
||||
"update.txt",
|
||||
[{ oldLines: ["marker", "end"], newLines: ["changed", "end"], endOfFile: true }],
|
||||
"marker\nend\nmiddle\n",
|
||||
),
|
||||
).toThrow("Failed to find expected lines")
|
||||
})
|
||||
|
||||
test("matches V1 lenient parsing of malformed hunk bodies", () => {
|
||||
expect(parse("*** Begin Patch\n*** Add File: add.txt\nmissing plus\n*** End Patch")).toEqual([
|
||||
{ type: "add", path: "add.txt", contents: "" },
|
||||
|
||||
@@ -392,14 +392,32 @@ describe("PatchTool", () => {
|
||||
withTempTool((directory, registry) =>
|
||||
Effect.gen(function* () {
|
||||
const target = path.join(directory, "tail.txt")
|
||||
yield* Effect.promise(() => fs.writeFile(target, "alpha\nlast\n"))
|
||||
yield* Effect.promise(() => fs.writeFile(target, "first\nsecond"))
|
||||
yield* executeTool(
|
||||
registry,
|
||||
call(
|
||||
"*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch",
|
||||
"*** Begin Patch\n*** Update File: tail.txt\n@@\n first\n-second\n+second updated\n*** End of File\n*** End Patch",
|
||||
),
|
||||
)
|
||||
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("alpha\nend\n")
|
||||
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("first\nsecond updated\n")
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("applies an end-of-file chunk to the final duplicate", () =>
|
||||
withTempTool((directory, registry) =>
|
||||
Effect.gen(function* () {
|
||||
const target = path.join(directory, "duplicates.txt")
|
||||
yield* Effect.promise(() => fs.writeFile(target, "marker\nend\nmiddle\nmarker\nend\n"))
|
||||
yield* executeTool(
|
||||
registry,
|
||||
call(
|
||||
"*** Begin Patch\n*** Update File: duplicates.txt\n@@\n-marker\n-end\n+marker changed\n+end\n*** End of File\n*** End Patch",
|
||||
),
|
||||
)
|
||||
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe(
|
||||
"marker\nend\nmiddle\nmarker changed\nend\n",
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user