Skip to content

Commit d07aae4

Browse files
committed
do hunk application offset fuzzing
1 parent a5cff34 commit d07aae4

File tree

1 file changed

+124
-55
lines changed

1 file changed

+124
-55
lines changed

src/patch/apply.ts

Lines changed: 124 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from "fs-extra"
22
import { dirname } from "path"
3-
import { ParsedPatchFile, FilePatch } from "./parse"
3+
import { ParsedPatchFile, FilePatch, Hunk } from "./parse"
44
import { assertNever } from "../assertNever"
55

66
export const executeEffects = (
@@ -75,17 +75,8 @@ function isExecutable(fileMode: number) {
7575
}
7676

7777
const trimRight = (s: string) => s.replace(/\s+$/, "")
78-
function assertLineEquality(onDisk: string, expected: string) {
79-
if (trimRight(onDisk) !== trimRight(expected)) {
80-
throw new Error(
81-
`Line mismatch
82-
83-
Expected: ${JSON.stringify(expected)}
84-
Observed: ${JSON.stringify(onDisk)}
85-
86-
`,
87-
)
88-
}
78+
function linesAreEqual(a: string, b: string) {
79+
return trimRight(a) === trimRight(b)
8980
}
9081

9182
/**
@@ -120,57 +111,135 @@ function applyPatch(
120111

121112
const fileLines: string[] = fileContents.split(/\n/)
122113

123-
// when adding or removing lines from a file, gotta
124-
// make sure that the original lines in hunk headers match up
125-
// this effectively measures the total +/- in line count during the course
126-
// of the patching process
127-
let contextIndexOffset = 0
128-
129-
for (const { parts, header } of hunks) {
130-
// contextIndex is the offest from the hunk header start but in the original file
131-
let contextIndex = header.original.start - 1 + contextIndexOffset
132-
133-
contextIndexOffset += header.patched.length - header.original.length
134-
135-
for (const part of parts) {
136-
switch (part.type) {
137-
case "deletion":
138-
case "context":
139-
for (const line of part.lines) {
140-
const originalLine = fileLines[contextIndex]
141-
assertLineEquality(originalLine, line)
142-
contextIndex++
143-
}
114+
const result: Modificaiton[][] = []
144115

145-
if (part.type === "deletion") {
146-
fileLines.splice(
147-
contextIndex - part.lines.length,
148-
part.lines.length,
149-
)
150-
contextIndex -= part.lines.length
116+
for (const hunk of hunks) {
117+
let fuzzingOffset = 0
118+
while (true) {
119+
const modifications = evaluateHunk(hunk, fileLines, fuzzingOffset)
120+
if (modifications) {
121+
result.push(modifications)
122+
break
123+
}
151124

152-
if (part.noNewlineAtEndOfFile) {
153-
fileLines.push("")
154-
}
155-
}
125+
fuzzingOffset =
126+
fuzzingOffset < 0 ? fuzzingOffset * -1 : fuzzingOffset * -1 - 1
127+
128+
if (Math.abs(fuzzingOffset) > 20) {
129+
throw new Error(
130+
`Cant apply hunk ${hunks.indexOf(hunk)} for file ${path}`,
131+
)
132+
}
133+
}
134+
}
135+
136+
if (dryRun) {
137+
return
138+
}
139+
140+
let diffOffset = 0
141+
142+
for (const modifications of result) {
143+
for (const modification of modifications) {
144+
switch (modification.type) {
145+
case "splice":
146+
fileLines.splice(
147+
modification.index + diffOffset,
148+
modification.numToDelete,
149+
...modification.linesToInsert,
150+
)
151+
diffOffset +=
152+
modification.linesToInsert.length - modification.numToDelete
156153
break
157-
case "insertion":
158-
fileLines.splice(contextIndex, 0, ...part.lines)
159-
contextIndex += part.lines.length
160-
if (part.noNewlineAtEndOfFile) {
161-
if (contextIndex !== fileLines.length - 1) {
162-
throw new Error("Invalid patch application state.")
163-
}
164-
fileLines.pop()
165-
}
154+
case "pop":
155+
fileLines.pop()
156+
break
157+
case "push":
158+
fileLines.push(modification.line)
166159
break
167160
default:
168-
assertNever(part.type)
161+
assertNever(modification)
169162
}
170163
}
171164
}
172165

173-
if (!dryRun) {
174-
fs.writeFileSync(path, fileLines.join("\n"), { mode })
166+
fs.writeFileSync(path, fileLines.join("\n"), { mode })
167+
}
168+
169+
interface Push {
170+
type: "push"
171+
line: string
172+
}
173+
interface Pop {
174+
type: "pop"
175+
}
176+
interface Splice {
177+
type: "splice"
178+
index: number
179+
numToDelete: number
180+
linesToInsert: string[]
181+
}
182+
183+
type Modificaiton = Push | Pop | Splice
184+
185+
function evaluateHunk(
186+
hunk: Hunk,
187+
fileLines: string[],
188+
fuzzingOffset: number,
189+
): Modificaiton[] | null {
190+
const result: Modificaiton[] = []
191+
let contextIndex = hunk.header.original.start - 1 + fuzzingOffset
192+
// do bounds checks for index
193+
if (contextIndex < 0) {
194+
return null
195+
}
196+
if (fileLines.length - contextIndex < hunk.header.original.length) {
197+
return null
198+
}
199+
200+
for (const part of hunk.parts) {
201+
switch (part.type) {
202+
case "deletion":
203+
case "context":
204+
for (const line of part.lines) {
205+
const originalLine = fileLines[contextIndex]
206+
if (!linesAreEqual(originalLine, line)) {
207+
return null
208+
}
209+
contextIndex++
210+
}
211+
212+
if (part.type === "deletion") {
213+
result.push({
214+
type: "splice",
215+
index: contextIndex - part.lines.length,
216+
numToDelete: part.lines.length,
217+
linesToInsert: [],
218+
})
219+
220+
if (part.noNewlineAtEndOfFile) {
221+
result.push({
222+
type: "push",
223+
line: "",
224+
})
225+
}
226+
}
227+
break
228+
case "insertion":
229+
result.push({
230+
type: "splice",
231+
index: contextIndex,
232+
numToDelete: 0,
233+
linesToInsert: part.lines,
234+
})
235+
if (part.noNewlineAtEndOfFile) {
236+
result.push({ type: "pop" })
237+
}
238+
break
239+
default:
240+
assertNever(part.type)
241+
}
175242
}
243+
244+
return result
176245
}

0 commit comments

Comments
 (0)