blockpatch 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +18 -0
- package/README.md +470 -45
- package/dist/cli.js +2466 -246
- package/package.json +23 -11
- package/dist/cli.d.ts +0 -2
- package/dist/engine.d.ts +0 -19
- package/dist/errors.d.ts +0 -5
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -263
- package/dist/parser.d.ts +0 -2
- package/dist/types.d.ts +0 -20
package/dist/cli.js
CHANGED
|
@@ -1,265 +1,2151 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import {
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { resolve as resolve3 } from "node:path";
|
|
5
6
|
|
|
6
7
|
// src/errors.ts
|
|
8
|
+
var maxErrorRanges = 10;
|
|
9
|
+
|
|
7
10
|
class BlockPatchError extends Error {
|
|
8
11
|
code;
|
|
9
|
-
|
|
12
|
+
details;
|
|
13
|
+
constructor(code, message, details = {}) {
|
|
10
14
|
super(message);
|
|
11
15
|
this.name = "BlockPatchError";
|
|
12
16
|
this.code = code;
|
|
17
|
+
this.details = details;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function fail(code, message, details) {
|
|
21
|
+
throw new BlockPatchError(code, message, details);
|
|
22
|
+
}
|
|
23
|
+
function boundedMatchRanges(matches, byteLength) {
|
|
24
|
+
const ranges = [];
|
|
25
|
+
for (const start of matches) {
|
|
26
|
+
if (ranges.length >= maxErrorRanges) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
ranges.push({ start, end: start + byteLength });
|
|
30
|
+
}
|
|
31
|
+
return ranges;
|
|
32
|
+
}
|
|
33
|
+
function boundedRanges(matches) {
|
|
34
|
+
const ranges = [];
|
|
35
|
+
for (const range of matches) {
|
|
36
|
+
if (ranges.length >= maxErrorRanges) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
ranges.push({ start: range.start, end: range.end });
|
|
40
|
+
}
|
|
41
|
+
return ranges;
|
|
42
|
+
}
|
|
43
|
+
function boundedMatchLineRanges(file, matches, byteLength) {
|
|
44
|
+
const lineRanges = [];
|
|
45
|
+
for (const start of matches) {
|
|
46
|
+
if (lineRanges.length >= maxErrorRanges) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
lineRanges.push(byteRangeToLineRange(file, { start, end: start + byteLength }));
|
|
50
|
+
}
|
|
51
|
+
return lineRanges;
|
|
52
|
+
}
|
|
53
|
+
function boundedLineRanges(file, ranges) {
|
|
54
|
+
return boundedRanges(ranges).map((range) => byteRangeToLineRange(file, range));
|
|
55
|
+
}
|
|
56
|
+
function byteRangeToLineRange(file, range) {
|
|
57
|
+
const start = clamp(range.start, 0, file.length);
|
|
58
|
+
const end = clamp(range.end, start, file.length);
|
|
59
|
+
const endByte = end > start ? end - 1 : start;
|
|
60
|
+
return {
|
|
61
|
+
start: lineNumberAt(file, start),
|
|
62
|
+
end: lineNumberAt(file, endByte)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function lineNumberAt(file, byteIndex) {
|
|
66
|
+
let line = 1;
|
|
67
|
+
const end = Math.min(byteIndex, file.length);
|
|
68
|
+
for (let index = 0;index < end; index += 1) {
|
|
69
|
+
if (file[index] === 10) {
|
|
70
|
+
line += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return line;
|
|
74
|
+
}
|
|
75
|
+
function clamp(value, min, max) {
|
|
76
|
+
return Math.min(Math.max(value, min), max);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/engine.ts
|
|
80
|
+
import { Buffer as Buffer3 } from "node:buffer";
|
|
81
|
+
import { randomBytes } from "node:crypto";
|
|
82
|
+
import { chmod, mkdir, rename, stat as stat2, unlink, writeFile } from "node:fs/promises";
|
|
83
|
+
import { basename, dirname, join as join2, resolve as resolve2 } from "node:path";
|
|
84
|
+
|
|
85
|
+
// src/files.ts
|
|
86
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
87
|
+
import { readFile, stat } from "node:fs/promises";
|
|
88
|
+
async function readFileChecked(path, label) {
|
|
89
|
+
try {
|
|
90
|
+
return await readFile(path);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
failFileSystem(error, path, `Could not read ${label}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function statChecked(path, label) {
|
|
96
|
+
try {
|
|
97
|
+
return await stat(path);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
failFileSystem(error, path, `Could not stat ${label}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function lstatSyncChecked(path, label, userPath = path) {
|
|
103
|
+
try {
|
|
104
|
+
return lstatSync(path);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
failFileSystem(error, userPath, `Could not stat ${label}`, "path");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function realpathSyncChecked(path, label, userPath = path) {
|
|
110
|
+
try {
|
|
111
|
+
return realpathSync(path);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
failFileSystem(error, userPath, `Could not resolve ${label}`, "path");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function assertRegularFile(info, path, label, phase = "io") {
|
|
117
|
+
if (!info.isFile()) {
|
|
118
|
+
fail("not_regular_file", `${label} must be a regular file: ${path}`, { path, phase });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function failFileSystem(error, path, action, phase = "io") {
|
|
122
|
+
if (error instanceof BlockPatchError) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
const message = error instanceof Error ? `${action}: ${error.message}` : `${action}: ${path}`;
|
|
126
|
+
fail(fileSystemErrorCode(error), message, { path, phase });
|
|
127
|
+
}
|
|
128
|
+
function fileSystemErrorCode(error) {
|
|
129
|
+
switch (error.code) {
|
|
130
|
+
case "ENOENT":
|
|
131
|
+
case "ENOTDIR":
|
|
132
|
+
return "file_not_found";
|
|
133
|
+
case "EISDIR":
|
|
134
|
+
return "not_regular_file";
|
|
135
|
+
case "EACCES":
|
|
136
|
+
case "EPERM":
|
|
137
|
+
return "permission_denied";
|
|
138
|
+
default:
|
|
139
|
+
return "io_error";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/parser.ts
|
|
144
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
145
|
+
import { createHash } from "node:crypto";
|
|
146
|
+
import { posix } from "node:path";
|
|
147
|
+
var devNull = "/dev/null";
|
|
148
|
+
var noNewlineMarker = "\";
|
|
149
|
+
var movePrefix = "blockpatch move ";
|
|
150
|
+
var allowedMetadataKeys = new Set(["id", "payload-sha256", "role"]);
|
|
151
|
+
var hunkPattern = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@ blockpatch-(source|target) id=([^\s]+)(?: .*)?$/;
|
|
152
|
+
function parseBlockPatch(input, options = {}) {
|
|
153
|
+
const lines = splitLines(input);
|
|
154
|
+
const sections = parseSections(lines, normalizeStripComponents(options.stripComponents ?? 1));
|
|
155
|
+
if (sections.length === 1) {
|
|
156
|
+
return parseSingleSectionMove(sections[0]);
|
|
157
|
+
}
|
|
158
|
+
if (sections.length === 2) {
|
|
159
|
+
return parseSplitSectionMove(sections);
|
|
160
|
+
}
|
|
161
|
+
fail("parse_error", "Patch must contain one same-file move or one split cross-file move");
|
|
162
|
+
}
|
|
163
|
+
function parseSingleSectionMove(section) {
|
|
164
|
+
if (section.src === null && section.dst === null) {
|
|
165
|
+
fail("parse_error", "Patch cannot use /dev/null for both endpoints");
|
|
166
|
+
}
|
|
167
|
+
if (section.role !== undefined) {
|
|
168
|
+
if (section.role === "source" && section.dst !== null) {
|
|
169
|
+
fail("parse_error", "A source role section without a target role section must target /dev/null");
|
|
170
|
+
}
|
|
171
|
+
if (section.role === "target" && section.src !== null) {
|
|
172
|
+
fail("parse_error", "A target role section without a source role section must source /dev/null");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (section.src !== null && section.dst !== null && !samePatchPath(section.src, section.dst)) {
|
|
176
|
+
fail("parse_error", "Cross-file moves must use separate source and target file sections");
|
|
177
|
+
}
|
|
178
|
+
if (section.hunks.length < 1 || section.hunks.length > 2) {
|
|
179
|
+
fail("parse_error", "Patch must contain one or two blockpatch hunks");
|
|
180
|
+
}
|
|
181
|
+
const sourceHunks = section.hunks.filter((hunk) => hunk.kind === "source");
|
|
182
|
+
const targetHunks = section.hunks.filter((hunk) => hunk.kind === "target");
|
|
183
|
+
if (sourceHunks.length > 1 || targetHunks.length > 1) {
|
|
184
|
+
fail("parse_error", "Patch must contain at most one source hunk and at most one target hunk");
|
|
185
|
+
}
|
|
186
|
+
const sourceHunk = sourceHunks[0];
|
|
187
|
+
const targetHunk = targetHunks[0];
|
|
188
|
+
for (const hunk of section.hunks) {
|
|
189
|
+
if (hunk.id !== section.moveId) {
|
|
190
|
+
fail("parse_error", "Move metadata id must match source and target hunk ids");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (section.src === null) {
|
|
194
|
+
if (sourceHunk !== undefined || targetHunk === undefined) {
|
|
195
|
+
fail("parse_error", "A move from /dev/null must contain exactly one blockpatch-target hunk");
|
|
196
|
+
}
|
|
197
|
+
return buildTargetOnlyBlockPatch(section, targetHunk, { pathState: true });
|
|
198
|
+
}
|
|
199
|
+
if (section.dst === null) {
|
|
200
|
+
if (sourceHunk === undefined || targetHunk !== undefined) {
|
|
201
|
+
fail("parse_error", "A move to /dev/null must contain exactly one blockpatch-source hunk");
|
|
202
|
+
}
|
|
203
|
+
return buildSourceOnlyBlockPatch(section, sourceHunk, { pathState: true });
|
|
204
|
+
}
|
|
205
|
+
if (sourceHunk !== undefined && targetHunk !== undefined) {
|
|
206
|
+
return buildPairedBlockPatch(section.src, section.dst, section.moveId, section.payloadSha256, sourceHunk, targetHunk);
|
|
207
|
+
}
|
|
208
|
+
if (sourceHunk !== undefined) {
|
|
209
|
+
return buildSourceOnlyBlockPatch(section, sourceHunk, { pathState: false });
|
|
210
|
+
}
|
|
211
|
+
if (targetHunk !== undefined) {
|
|
212
|
+
return buildTargetOnlyBlockPatch(section, targetHunk, { pathState: false });
|
|
213
|
+
}
|
|
214
|
+
fail("parse_error", "Patch must contain at least one blockpatch hunk");
|
|
215
|
+
}
|
|
216
|
+
function parseSplitSectionMove(sections) {
|
|
217
|
+
if (sections.some((section) => section.src === null || section.dst === null)) {
|
|
218
|
+
fail("parse_error", "/dev/null endpoints are only valid in single-section moves");
|
|
219
|
+
}
|
|
220
|
+
if (sections.some((section) => section.role === undefined)) {
|
|
221
|
+
fail("parse_error", "Split cross-file move sections must include role=source or role=target");
|
|
222
|
+
}
|
|
223
|
+
const sourceSections = sections.filter((section) => section.role === "source");
|
|
224
|
+
const targetSections = sections.filter((section) => section.role === "target");
|
|
225
|
+
if (sourceSections.length !== 1 || targetSections.length !== 1) {
|
|
226
|
+
fail("parse_error", "Split cross-file moves must contain one source role section and one target role section");
|
|
227
|
+
}
|
|
228
|
+
const sourceSection = sourceSections[0];
|
|
229
|
+
const targetSection = targetSections[0];
|
|
230
|
+
const sourceFile = sourceSection.src;
|
|
231
|
+
const targetFile = targetSection.src;
|
|
232
|
+
if (!samePatchPath(sourceFile, sourceSection.dst) || !samePatchPath(targetFile, targetSection.dst)) {
|
|
233
|
+
fail("parse_error", "Split cross-file section headers must name the same file in --- and +++");
|
|
234
|
+
}
|
|
235
|
+
if (samePatchPath(sourceFile, targetFile)) {
|
|
236
|
+
fail("parse_error", "Split cross-file moves require different source and target files");
|
|
237
|
+
}
|
|
238
|
+
if (sourceSection.moveId !== targetSection.moveId) {
|
|
239
|
+
fail("parse_error", "Split cross-file move sections must use the same move id");
|
|
240
|
+
}
|
|
241
|
+
if (sourceSection.payloadSha256 !== targetSection.payloadSha256) {
|
|
242
|
+
fail("parse_error", "Split cross-file move sections must use the same payload-sha256");
|
|
243
|
+
}
|
|
244
|
+
if (sourceSection.hunks.length !== 1 || sourceSection.hunks[0].kind !== "source") {
|
|
245
|
+
fail("parse_error", "Source role section must contain exactly one blockpatch-source hunk");
|
|
246
|
+
}
|
|
247
|
+
if (targetSection.hunks.length !== 1 || targetSection.hunks[0].kind !== "target") {
|
|
248
|
+
fail("parse_error", "Target role section must contain exactly one blockpatch-target hunk");
|
|
249
|
+
}
|
|
250
|
+
const sourceHunk = sourceSection.hunks[0];
|
|
251
|
+
const targetHunk = targetSection.hunks[0];
|
|
252
|
+
if (sourceHunk.id !== sourceSection.moveId || targetHunk.id !== targetSection.moveId) {
|
|
253
|
+
fail("parse_error", "Move metadata id must match source and target hunk ids");
|
|
254
|
+
}
|
|
255
|
+
return buildPairedBlockPatch(sourceFile, targetFile, sourceSection.moveId, sourceSection.payloadSha256, sourceHunk, targetHunk);
|
|
256
|
+
}
|
|
257
|
+
function buildPairedBlockPatch(src, dst, moveId, payloadSha256, sourceHunk, targetHunk) {
|
|
258
|
+
const source = parseSourceHunk(sourceHunk);
|
|
259
|
+
const target = parseTargetHunk(targetHunk);
|
|
260
|
+
const targetPayload = payloadBytes(targetHunk, "+");
|
|
261
|
+
if (!source.payload.equals(targetPayload)) {
|
|
262
|
+
fail("payload_mismatch", "Target added payload does not match source removed payload", {
|
|
263
|
+
phase: "payload",
|
|
264
|
+
anchor: "blockpatch-target"
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
verifyPayloadHash(source.payload, payloadSha256);
|
|
268
|
+
return {
|
|
269
|
+
type: "move",
|
|
270
|
+
id: moveId,
|
|
271
|
+
src,
|
|
272
|
+
dst,
|
|
273
|
+
payloadSha256,
|
|
274
|
+
hasSourceHunk: true,
|
|
275
|
+
sourceBefore: source.before,
|
|
276
|
+
sourcePayload: source.payload,
|
|
277
|
+
sourceAfter: source.after,
|
|
278
|
+
target
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function buildSourceOnlyBlockPatch(section, sourceHunk, options) {
|
|
282
|
+
const source = parseSourceHunk(sourceHunk, { allowEmptyPayload: options.pathState });
|
|
283
|
+
if (options.pathState && (source.before.length !== 0 || source.after.length !== 0)) {
|
|
284
|
+
fail("parse_error", "A move to /dev/null must describe whole-file payload without context");
|
|
285
|
+
}
|
|
286
|
+
verifyPayloadHash(source.payload, section.payloadSha256);
|
|
287
|
+
return {
|
|
288
|
+
type: "move",
|
|
289
|
+
id: section.moveId,
|
|
290
|
+
src: section.src,
|
|
291
|
+
dst: section.dst,
|
|
292
|
+
payloadSha256: section.payloadSha256,
|
|
293
|
+
hasSourceHunk: true,
|
|
294
|
+
sourceBefore: source.before,
|
|
295
|
+
sourcePayload: source.payload,
|
|
296
|
+
sourceAfter: source.after,
|
|
297
|
+
target: null
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function buildTargetOnlyBlockPatch(section, targetHunk, options) {
|
|
301
|
+
const target = parseTargetHunk(targetHunk, {
|
|
302
|
+
allowEmptyAnchors: options.pathState,
|
|
303
|
+
allowEmptyPayload: options.pathState
|
|
304
|
+
});
|
|
305
|
+
if (options.pathState && (target.before.length !== 0 || target.after.length !== 0)) {
|
|
306
|
+
fail("parse_error", "A move from /dev/null must describe whole-file payload without context");
|
|
307
|
+
}
|
|
308
|
+
const payload = payloadBytes(targetHunk, "+");
|
|
309
|
+
verifyPayloadHash(payload, section.payloadSha256);
|
|
310
|
+
return {
|
|
311
|
+
type: "move",
|
|
312
|
+
id: section.moveId,
|
|
313
|
+
src: section.src,
|
|
314
|
+
dst: section.dst,
|
|
315
|
+
payloadSha256: section.payloadSha256,
|
|
316
|
+
hasSourceHunk: false,
|
|
317
|
+
sourceBefore: Buffer2.alloc(0),
|
|
318
|
+
sourcePayload: payload,
|
|
319
|
+
sourceAfter: Buffer2.alloc(0),
|
|
320
|
+
target
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function verifyPayloadHash(payload, payloadSha256) {
|
|
324
|
+
const actualHash = createHash("sha256").update(payload).digest("hex");
|
|
325
|
+
if (actualHash !== payloadSha256) {
|
|
326
|
+
fail("hash_mismatch", "payload-sha256 does not match moved payload", {
|
|
327
|
+
phase: "payload",
|
|
328
|
+
anchor: "payload-sha256"
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function parseSections(lines, stripComponents) {
|
|
333
|
+
if (text(lines[0])?.startsWith("diff --blockpatch ") !== true) {
|
|
334
|
+
fail("parse_error", "Patch must start with diff --blockpatch");
|
|
335
|
+
}
|
|
336
|
+
const starts = [];
|
|
337
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
338
|
+
if (text(lines[index])?.startsWith("diff --blockpatch ") === true) {
|
|
339
|
+
starts.push(index);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return starts.map((start, index) => {
|
|
343
|
+
const end = starts[index + 1] ?? lines.length;
|
|
344
|
+
return parseSection(lines, start, end, stripComponents);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function parseSection(lines, start, end, stripComponents) {
|
|
348
|
+
if (text(lines[start + 1]) !== "blockpatch version 1") {
|
|
349
|
+
fail("parse_error", "Patch must declare blockpatch version 1");
|
|
350
|
+
}
|
|
351
|
+
const moveLine = text(lines[start + 2]);
|
|
352
|
+
if (moveLine?.startsWith(movePrefix) !== true) {
|
|
353
|
+
fail("parse_error", "Patch must declare blockpatch move metadata");
|
|
354
|
+
}
|
|
355
|
+
const metadata = parseMetadata(moveLine.slice(movePrefix.length));
|
|
356
|
+
const moveId = metadata.get("id");
|
|
357
|
+
const payloadSha256 = metadata.get("payload-sha256");
|
|
358
|
+
const role = metadata.get("role");
|
|
359
|
+
if (!moveId) {
|
|
360
|
+
fail("parse_error", "blockpatch move metadata must include id=<id>");
|
|
361
|
+
}
|
|
362
|
+
if (!payloadSha256 || !/^[a-f0-9]{64}$/.test(payloadSha256)) {
|
|
363
|
+
fail("parse_error", "blockpatch move metadata must include payload-sha256=<64 hex chars>");
|
|
364
|
+
}
|
|
365
|
+
if (role !== undefined && role !== "source" && role !== "target") {
|
|
366
|
+
fail("parse_error", "blockpatch move role must be source or target");
|
|
367
|
+
}
|
|
368
|
+
const oldRawPath = parseFileHeader(text(lines[start + 3]), "---", "a/");
|
|
369
|
+
const newRawPath = parseFileHeader(text(lines[start + 4]), "+++", "b/");
|
|
370
|
+
if (text(lines[start])?.trimEnd() !== `diff --blockpatch ${oldRawPath} ${newRawPath}`) {
|
|
371
|
+
fail("parse_error", "diff --blockpatch paths must match the --- and +++ headers");
|
|
372
|
+
}
|
|
373
|
+
let hunkStart = start + 5;
|
|
374
|
+
while (hunkStart < end && text(lines[hunkStart]) === "") {
|
|
375
|
+
hunkStart += 1;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
src: oldRawPath === devNull ? null : stripPath(oldRawPath, stripComponents),
|
|
379
|
+
dst: newRawPath === devNull ? null : stripPath(newRawPath, stripComponents),
|
|
380
|
+
moveId,
|
|
381
|
+
payloadSha256,
|
|
382
|
+
role,
|
|
383
|
+
hunks: parseHunks(lines, hunkStart, end)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function normalizeStripComponents(value) {
|
|
387
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
388
|
+
fail("parse_error", "stripComponents must be a non-negative integer");
|
|
389
|
+
}
|
|
390
|
+
return value;
|
|
391
|
+
}
|
|
392
|
+
function parseHunks(lines, start, end) {
|
|
393
|
+
const hunks = [];
|
|
394
|
+
let index = start;
|
|
395
|
+
while (index < end) {
|
|
396
|
+
const header = text(lines[index]);
|
|
397
|
+
if (header === "") {
|
|
398
|
+
index += 1;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const parsedHeader = parseHunkHeader(header);
|
|
402
|
+
if (parsedHeader === undefined) {
|
|
403
|
+
fail("parse_error", "Expected blockpatch source/target hunk header");
|
|
404
|
+
}
|
|
405
|
+
const hunk = {
|
|
406
|
+
kind: parsedHeader.kind,
|
|
407
|
+
id: parsedHeader.id,
|
|
408
|
+
oldCount: parsedHeader.oldCount,
|
|
409
|
+
newCount: parsedHeader.newCount,
|
|
410
|
+
lines: []
|
|
411
|
+
};
|
|
412
|
+
index += 1;
|
|
413
|
+
while (index < end) {
|
|
414
|
+
const maybeHeader = text(lines[index]);
|
|
415
|
+
if (parseHunkHeader(maybeHeader) !== undefined) {
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
const body = lines[index].body;
|
|
419
|
+
if (body.length === 0) {
|
|
420
|
+
let lookahead = index + 1;
|
|
421
|
+
while (lookahead < end && lines[lookahead].body.length === 0) {
|
|
422
|
+
lookahead += 1;
|
|
423
|
+
}
|
|
424
|
+
if (lookahead < end && parseHunkHeader(text(lines[lookahead])) === undefined) {
|
|
425
|
+
fail("parse_error", "Hunk bodies must not contain blank lines; encode an empty context line as a single space");
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
const first = body[0];
|
|
430
|
+
if (first !== 32 && first !== 45 && first !== 43) {
|
|
431
|
+
fail("parse_error", "Hunk body lines must start with space, -, or +");
|
|
432
|
+
}
|
|
433
|
+
let content = Buffer2.concat([body.subarray(1), lines[index].eol]);
|
|
434
|
+
if (index + 1 < lines.length && text(lines[index + 1]) === noNewlineMarker) {
|
|
435
|
+
const bareCr = lines[index].eol[0] === 13 ? lines[index].eol.subarray(0, 1) : Buffer2.alloc(0);
|
|
436
|
+
content = Buffer2.concat([body.subarray(1), bareCr]);
|
|
437
|
+
index += 1;
|
|
438
|
+
}
|
|
439
|
+
hunk.lines.push({
|
|
440
|
+
prefix: String.fromCharCode(first),
|
|
441
|
+
content
|
|
442
|
+
});
|
|
443
|
+
index += 1;
|
|
444
|
+
}
|
|
445
|
+
validateHunkCounts(hunk);
|
|
446
|
+
hunks.push(hunk);
|
|
447
|
+
}
|
|
448
|
+
return hunks;
|
|
449
|
+
}
|
|
450
|
+
function samePatchPath(left, right) {
|
|
451
|
+
return posix.normalize(left) === posix.normalize(right);
|
|
452
|
+
}
|
|
453
|
+
function parseHunkHeader(header) {
|
|
454
|
+
const match = header?.match(hunkPattern);
|
|
455
|
+
if (match === undefined || match === null) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
oldCount: match[1] === undefined ? 1 : Number(match[1]),
|
|
460
|
+
newCount: match[2] === undefined ? 1 : Number(match[2]),
|
|
461
|
+
kind: match[3] === "source" ? "source" : "target",
|
|
462
|
+
id: match[4]
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function validateHunkCounts(hunk) {
|
|
466
|
+
const oldCount = hunk.lines.filter((line) => line.prefix === " " || line.prefix === "-").length;
|
|
467
|
+
const newCount = hunk.lines.filter((line) => line.prefix === " " || line.prefix === "+").length;
|
|
468
|
+
if (oldCount !== hunk.oldCount || newCount !== hunk.newCount) {
|
|
469
|
+
fail("parse_error", `Hunk line counts do not match header for blockpatch-${hunk.kind} id=${hunk.id}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function parseSourceHunk(hunk, options = {}) {
|
|
473
|
+
const removedIndexes = hunk.lines.map((line, index) => line.prefix === "-" ? index : -1).filter((index) => index !== -1);
|
|
474
|
+
if (removedIndexes.length === 0) {
|
|
475
|
+
if (options.allowEmptyPayload === true && hunk.lines.length === 0) {
|
|
476
|
+
return { before: Buffer2.alloc(0), payload: Buffer2.alloc(0), after: Buffer2.alloc(0) };
|
|
477
|
+
}
|
|
478
|
+
fail("parse_error", "Source hunk must contain removed payload lines");
|
|
479
|
+
}
|
|
480
|
+
assertContiguous(removedIndexes, "Source payload lines must be contiguous");
|
|
481
|
+
const firstRemoved = removedIndexes[0];
|
|
482
|
+
const lastRemoved = removedIndexes[removedIndexes.length - 1];
|
|
483
|
+
const beforeLines = hunk.lines.slice(0, firstRemoved);
|
|
484
|
+
const afterLines = hunk.lines.slice(lastRemoved + 1);
|
|
485
|
+
if (!beforeLines.every((line) => line.prefix === " ") || !afterLines.every((line) => line.prefix === " ")) {
|
|
486
|
+
fail("parse_error", "Source hunk may only contain context lines around removed payload");
|
|
487
|
+
}
|
|
488
|
+
const before = concatLines(beforeLines);
|
|
489
|
+
const payload = payloadBytes(hunk, "-");
|
|
490
|
+
const after = concatLines(afterLines);
|
|
491
|
+
return { before, payload, after };
|
|
492
|
+
}
|
|
493
|
+
function parseTargetHunk(hunk, options = {}) {
|
|
494
|
+
const addedIndexes = hunk.lines.map((line, index) => line.prefix === "+" ? index : -1).filter((index) => index !== -1);
|
|
495
|
+
if (addedIndexes.length === 0) {
|
|
496
|
+
if (options.allowEmptyPayload === true && hunk.lines.length === 0) {
|
|
497
|
+
return { before: Buffer2.alloc(0), after: Buffer2.alloc(0) };
|
|
498
|
+
}
|
|
499
|
+
fail("parse_error", "Target hunk must contain added payload lines");
|
|
500
|
+
}
|
|
501
|
+
assertContiguous(addedIndexes, "Target payload lines must be contiguous");
|
|
502
|
+
const firstAdded = addedIndexes[0];
|
|
503
|
+
const lastAdded = addedIndexes[addedIndexes.length - 1];
|
|
504
|
+
const beforeLines = hunk.lines.slice(0, firstAdded);
|
|
505
|
+
const afterLines = hunk.lines.slice(lastAdded + 1);
|
|
506
|
+
if (!beforeLines.every((line) => line.prefix === " ") || !afterLines.every((line) => line.prefix === " ")) {
|
|
507
|
+
fail("parse_error", "Target hunk may only contain context lines around added payload");
|
|
508
|
+
}
|
|
509
|
+
const before = concatLines(beforeLines);
|
|
510
|
+
const after = concatLines(afterLines);
|
|
511
|
+
if (before.length === 0 && after.length === 0 && options.allowEmptyAnchors !== true) {
|
|
512
|
+
fail("parse_error", "Target hunk must include context before or after the moved payload");
|
|
513
|
+
}
|
|
514
|
+
return { before, after };
|
|
515
|
+
}
|
|
516
|
+
function payloadBytes(hunk, prefix) {
|
|
517
|
+
return concatLines(hunk.lines.filter((line) => line.prefix === prefix));
|
|
518
|
+
}
|
|
519
|
+
function concatLines(lines) {
|
|
520
|
+
return Buffer2.concat(lines.map((line) => line.content));
|
|
521
|
+
}
|
|
522
|
+
function assertContiguous(indexes, message) {
|
|
523
|
+
for (let index = 1;index < indexes.length; index += 1) {
|
|
524
|
+
if (indexes[index] !== indexes[index - 1] + 1) {
|
|
525
|
+
fail("parse_error", message);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function parseMetadata(input) {
|
|
530
|
+
const metadata = new Map;
|
|
531
|
+
for (const part of input.trim().split(/\s+/)) {
|
|
532
|
+
const equals = part.indexOf("=");
|
|
533
|
+
if (equals <= 0) {
|
|
534
|
+
fail("parse_error", `Invalid blockpatch move metadata field: ${part}`);
|
|
535
|
+
}
|
|
536
|
+
const key = part.slice(0, equals);
|
|
537
|
+
if (metadata.has(key)) {
|
|
538
|
+
fail("parse_error", `Duplicate blockpatch move metadata field: ${key}`);
|
|
539
|
+
}
|
|
540
|
+
if (!allowedMetadataKeys.has(key) && !key.startsWith("x-")) {
|
|
541
|
+
fail("parse_error", `Unknown blockpatch move metadata field: ${key}`);
|
|
542
|
+
}
|
|
543
|
+
metadata.set(key, part.slice(equals + 1));
|
|
544
|
+
}
|
|
545
|
+
return metadata;
|
|
546
|
+
}
|
|
547
|
+
function parseFileHeader(line, prefix, requiredPathPrefix) {
|
|
548
|
+
const marker = `${prefix} `;
|
|
549
|
+
if (line !== undefined && line.slice(marker.length).trim() === devNull && line.startsWith(marker)) {
|
|
550
|
+
return devNull;
|
|
551
|
+
}
|
|
552
|
+
if (line?.startsWith(`${marker}${requiredPathPrefix}`) !== true) {
|
|
553
|
+
fail("parse_error", `Patch must include a ${marker}${requiredPathPrefix}<path> or ${marker}${devNull} header`);
|
|
554
|
+
}
|
|
555
|
+
const path = line.slice(marker.length).trim();
|
|
556
|
+
if (!path || path === requiredPathPrefix) {
|
|
557
|
+
fail("parse_error", `${prefix} file header must include a path`);
|
|
558
|
+
}
|
|
559
|
+
return path;
|
|
560
|
+
}
|
|
561
|
+
function stripPath(path, stripComponents) {
|
|
562
|
+
const stripped = path.split("/").slice(stripComponents).join("/");
|
|
563
|
+
if (!stripped) {
|
|
564
|
+
fail("parse_error", `-p${stripComponents} removes the entire path: ${path}`);
|
|
565
|
+
}
|
|
566
|
+
return stripped;
|
|
567
|
+
}
|
|
568
|
+
function splitLines(input) {
|
|
569
|
+
const lines = [];
|
|
570
|
+
let position = 0;
|
|
571
|
+
while (position < input.length) {
|
|
572
|
+
const lf = input.indexOf(10, position);
|
|
573
|
+
const lineEnd = lf === -1 ? input.length : lf;
|
|
574
|
+
const hasCr = lf !== -1 && lineEnd > position && input[lineEnd - 1] === 13;
|
|
575
|
+
const bodyEnd = hasCr ? lineEnd - 1 : lineEnd;
|
|
576
|
+
const eol = lf === -1 ? Buffer2.alloc(0) : input.subarray(hasCr ? lineEnd - 1 : lineEnd, lf + 1);
|
|
577
|
+
lines.push({
|
|
578
|
+
body: Buffer2.from(input.subarray(position, bodyEnd)),
|
|
579
|
+
eol: Buffer2.from(eol)
|
|
580
|
+
});
|
|
581
|
+
if (lf === -1) {
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
position = lf + 1;
|
|
585
|
+
}
|
|
586
|
+
return lines;
|
|
587
|
+
}
|
|
588
|
+
function text(line) {
|
|
589
|
+
return line?.body.toString("utf8");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/paths.ts
|
|
593
|
+
import { lstatSync as lstatSync2 } from "node:fs";
|
|
594
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
595
|
+
function resolvePath(cwd, path, label) {
|
|
596
|
+
if (path === "" || path.includes("\x00")) {
|
|
597
|
+
fail("invalid_path", `Invalid ${label}: ${path}`, { path, phase: "path" });
|
|
598
|
+
}
|
|
599
|
+
if (isAbsolute(path)) {
|
|
600
|
+
fail("path_outside_cwd", `${label} must be relative to the working directory: ${path}`, {
|
|
601
|
+
path,
|
|
602
|
+
phase: "path"
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const root = resolve(cwd);
|
|
606
|
+
const resolved = resolve(root, path);
|
|
607
|
+
const realRoot = realpathSyncChecked(root, "working directory", root);
|
|
608
|
+
if (!isInside(root, resolved)) {
|
|
609
|
+
fail("path_outside_cwd", `${label} escapes the working directory: ${path}`, { path, phase: "path" });
|
|
610
|
+
}
|
|
611
|
+
rejectSymlinkComponents(root, resolved, path, label);
|
|
612
|
+
const info = lstatSyncChecked(resolved, label, path);
|
|
613
|
+
assertRegularFile(info, path, label, "path");
|
|
614
|
+
const realResolved = realpathSyncChecked(resolved, label, path);
|
|
615
|
+
if (!isInside(realRoot, realResolved)) {
|
|
616
|
+
fail("path_outside_cwd", `${label} resolves outside the working directory: ${path}`, { path, phase: "path" });
|
|
617
|
+
}
|
|
618
|
+
return resolved;
|
|
619
|
+
}
|
|
620
|
+
function resolvePathAllowMissing(cwd, path, label) {
|
|
621
|
+
if (path === "" || path.includes("\x00")) {
|
|
622
|
+
fail("invalid_path", `Invalid ${label}: ${path}`, { path, phase: "path" });
|
|
623
|
+
}
|
|
624
|
+
if (isAbsolute(path)) {
|
|
625
|
+
fail("path_outside_cwd", `${label} must be relative to the working directory: ${path}`, {
|
|
626
|
+
path,
|
|
627
|
+
phase: "path"
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const root = resolve(cwd);
|
|
631
|
+
const resolved = resolve(root, path);
|
|
632
|
+
const realRoot = realpathSyncChecked(root, "working directory", root);
|
|
633
|
+
if (!isInside(root, resolved)) {
|
|
634
|
+
fail("path_outside_cwd", `${label} escapes the working directory: ${path}`, { path, phase: "path" });
|
|
635
|
+
}
|
|
636
|
+
const deepestExisting = rejectExistingSymlinkComponents(root, resolved, path, label);
|
|
637
|
+
if (deepestExisting !== resolved) {
|
|
638
|
+
return { path: resolved, exists: false };
|
|
639
|
+
}
|
|
640
|
+
const info = lstatSyncChecked(resolved, label, path);
|
|
641
|
+
assertRegularFile(info, path, label, "path");
|
|
642
|
+
const realResolved = realpathSyncChecked(resolved, label, path);
|
|
643
|
+
if (!isInside(realRoot, realResolved)) {
|
|
644
|
+
fail("path_outside_cwd", `${label} resolves outside the working directory: ${path}`, { path, phase: "path" });
|
|
645
|
+
}
|
|
646
|
+
return { path: resolved, exists: true };
|
|
647
|
+
}
|
|
648
|
+
async function sameFileIdentity(left, right) {
|
|
649
|
+
if (left === right) {
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
const [leftInfo, rightInfo] = await Promise.all([
|
|
653
|
+
statChecked(left, "source path"),
|
|
654
|
+
statChecked(right, "destination path")
|
|
655
|
+
]);
|
|
656
|
+
return leftInfo.dev === rightInfo.dev && leftInfo.ino === rightInfo.ino;
|
|
657
|
+
}
|
|
658
|
+
function rejectSymlinkComponents(root, resolved, originalPath, label) {
|
|
659
|
+
const relativePath = relative(root, resolved);
|
|
660
|
+
if (relativePath === "") {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
let current = root;
|
|
664
|
+
for (const part of relativePath.split(sep)) {
|
|
665
|
+
current = join(current, part);
|
|
666
|
+
if (lstatSyncChecked(current, label, originalPath).isSymbolicLink()) {
|
|
667
|
+
fail("symlink_path", `${label} must not contain symbolic links: ${originalPath}`, {
|
|
668
|
+
path: originalPath,
|
|
669
|
+
phase: "path"
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function rejectExistingSymlinkComponents(root, resolved, originalPath, label) {
|
|
675
|
+
const relativePath = relative(root, resolved);
|
|
676
|
+
if (relativePath === "") {
|
|
677
|
+
return resolved;
|
|
678
|
+
}
|
|
679
|
+
let current = root;
|
|
680
|
+
for (const part of relativePath.split(sep)) {
|
|
681
|
+
const next = join(current, part);
|
|
682
|
+
let info;
|
|
683
|
+
try {
|
|
684
|
+
info = lstatSync2(next);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
const code = error.code;
|
|
687
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
688
|
+
return current;
|
|
689
|
+
}
|
|
690
|
+
failFileSystem(error, originalPath, `Could not stat ${label}`, "path");
|
|
691
|
+
}
|
|
692
|
+
if (info.isSymbolicLink()) {
|
|
693
|
+
fail("symlink_path", `${label} must not contain symbolic links: ${originalPath}`, {
|
|
694
|
+
path: originalPath,
|
|
695
|
+
phase: "path"
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
current = next;
|
|
699
|
+
}
|
|
700
|
+
return current;
|
|
701
|
+
}
|
|
702
|
+
function isInside(root, path) {
|
|
703
|
+
const fromRoot = relative(root, path);
|
|
704
|
+
return fromRoot === "" || !fromRoot.startsWith("..") && !isAbsolute(fromRoot);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/engine.ts
|
|
708
|
+
async function checkPatchFile(patchPath, options = {}) {
|
|
709
|
+
return runPatchFile(patchPath, { ...options, dryRun: true });
|
|
710
|
+
}
|
|
711
|
+
async function checkPatchBytes(patchBytes, options = {}) {
|
|
712
|
+
return runPatchBytes(patchBytes, { ...options, dryRun: true });
|
|
713
|
+
}
|
|
714
|
+
function checkPatchBytesInMemory(patchBytes, files, options = {}) {
|
|
715
|
+
const patch = parseBlockPatch(patchBytes, { stripComponents: options.stripComponents });
|
|
716
|
+
return checkMovePatchInMemory(patch, memoryFileMap(files), options.reverse ?? false);
|
|
717
|
+
}
|
|
718
|
+
async function applyPatchFile(patchPath, options = {}) {
|
|
719
|
+
return runPatchFile(patchPath, options);
|
|
720
|
+
}
|
|
721
|
+
async function applyPatchBytes(patchBytes, options = {}) {
|
|
722
|
+
return runPatchBytes(patchBytes, options);
|
|
723
|
+
}
|
|
724
|
+
async function runPatchFile(patchPath, options) {
|
|
725
|
+
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
726
|
+
const patchBytes = await readFileChecked(resolve2(cwd, patchPath), "patch file");
|
|
727
|
+
return runPatchBytes(patchBytes, { ...options, cwd });
|
|
728
|
+
}
|
|
729
|
+
async function runPatchBytes(patchBytes, options) {
|
|
730
|
+
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
731
|
+
const patch = parseBlockPatch(patchBytes, { stripComponents: options.stripComponents });
|
|
732
|
+
return applyMovePatch(patch, cwd, options.dryRun ?? false, options.reverse ?? false);
|
|
733
|
+
}
|
|
734
|
+
async function applyMovePatch(patch, cwd, dryRun, reverse) {
|
|
735
|
+
const effectivePatch = reverse ? reverseMovePatch(patch) : patch;
|
|
736
|
+
if (effectivePatch.src === null || effectivePatch.dst === null) {
|
|
737
|
+
if (effectivePatch.src === null && effectivePatch.dst !== null && !effectivePatch.hasSourceHunk) {
|
|
738
|
+
return applyPathCreationMove(effectivePatch, effectivePatch.dst, cwd, dryRun);
|
|
739
|
+
}
|
|
740
|
+
if (effectivePatch.dst === null && effectivePatch.src !== null && effectivePatch.hasSourceHunk) {
|
|
741
|
+
return applyPathDeletionMove(effectivePatch, effectivePatch.src, cwd, dryRun);
|
|
742
|
+
}
|
|
743
|
+
fail("parse_error", "Invalid /dev/null endpoint move shape");
|
|
744
|
+
}
|
|
745
|
+
if (!effectivePatch.hasSourceHunk) {
|
|
746
|
+
return applyInFileInsertionMove(effectivePatch, effectivePatch.src, effectivePatch.dst, cwd, dryRun);
|
|
747
|
+
}
|
|
748
|
+
if (effectivePatch.target === null) {
|
|
749
|
+
return applyInFileDeletionMove(effectivePatch, effectivePatch.src, effectivePatch.dst, cwd, dryRun);
|
|
750
|
+
}
|
|
751
|
+
const srcLabel = effectivePatch.src;
|
|
752
|
+
const dstLabel = effectivePatch.dst;
|
|
753
|
+
const srcPath = resolvePath(cwd, srcLabel, "source path");
|
|
754
|
+
const dstPath = resolvePath(cwd, dstLabel, "destination path");
|
|
755
|
+
const sameFile = await sameFileIdentity(srcPath, dstPath);
|
|
756
|
+
const srcOriginal = await readFileChecked(srcPath, "source file");
|
|
757
|
+
const dstOriginal = sameFile ? srcOriginal : await readFileChecked(dstPath, "destination file");
|
|
758
|
+
const plan = selectMovePlan(srcOriginal, dstOriginal, effectivePatch, sameFile);
|
|
759
|
+
if (plan.status === "already_applied") {
|
|
760
|
+
return {
|
|
761
|
+
changed: [],
|
|
762
|
+
affected: unique([srcLabel, dstLabel]),
|
|
763
|
+
written: false,
|
|
764
|
+
noop: true,
|
|
765
|
+
status: "already_applied",
|
|
766
|
+
moves: [plan.details]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const selection = plan.selection;
|
|
770
|
+
const changed = await commitMove({
|
|
771
|
+
srcPath,
|
|
772
|
+
dstPath,
|
|
773
|
+
sameFile,
|
|
774
|
+
dryRun,
|
|
775
|
+
srcOriginal,
|
|
776
|
+
dstOriginal,
|
|
777
|
+
selection,
|
|
778
|
+
srcLabel,
|
|
779
|
+
dstLabel
|
|
780
|
+
});
|
|
781
|
+
return {
|
|
782
|
+
changed,
|
|
783
|
+
affected: unique([srcLabel, dstLabel]),
|
|
784
|
+
written: !dryRun && changed.length > 0,
|
|
785
|
+
noop: changed.length === 0,
|
|
786
|
+
status: changed.length === 0 ? "noop" : "applied",
|
|
787
|
+
moves: [
|
|
788
|
+
moveResultDetails({
|
|
789
|
+
id: effectivePatch.id,
|
|
790
|
+
src: srcLabel,
|
|
791
|
+
dst: dstLabel,
|
|
792
|
+
payloadSha256: effectivePatch.payloadSha256,
|
|
793
|
+
selection
|
|
794
|
+
})
|
|
795
|
+
]
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function checkMovePatchInMemory(patch, files, reverse) {
|
|
799
|
+
const effectivePatch = reverse ? reverseMovePatch(patch) : patch;
|
|
800
|
+
if (effectivePatch.src === null || effectivePatch.dst === null) {
|
|
801
|
+
if (effectivePatch.src === null && effectivePatch.dst !== null && !effectivePatch.hasSourceHunk) {
|
|
802
|
+
return checkPathCreationMoveInMemory(effectivePatch, effectivePatch.dst, files);
|
|
803
|
+
}
|
|
804
|
+
if (effectivePatch.dst === null && effectivePatch.src !== null && effectivePatch.hasSourceHunk) {
|
|
805
|
+
return checkPathDeletionMoveInMemory(effectivePatch, effectivePatch.src, files);
|
|
806
|
+
}
|
|
807
|
+
fail("parse_error", "Invalid /dev/null endpoint move shape");
|
|
808
|
+
}
|
|
809
|
+
if (!effectivePatch.hasSourceHunk) {
|
|
810
|
+
return checkInFileInsertionMoveInMemory(effectivePatch, effectivePatch.src, effectivePatch.dst, files);
|
|
811
|
+
}
|
|
812
|
+
if (effectivePatch.target === null) {
|
|
813
|
+
return checkInFileDeletionMoveInMemory(effectivePatch, effectivePatch.src, effectivePatch.dst, files);
|
|
814
|
+
}
|
|
815
|
+
const srcLabel = effectivePatch.src;
|
|
816
|
+
const dstLabel = effectivePatch.dst;
|
|
817
|
+
const srcFile = readMemoryFile(files, srcLabel, "source file");
|
|
818
|
+
const sameFile = sameMemoryFile(files, srcLabel, dstLabel);
|
|
819
|
+
const dstFile = sameFile ? srcFile : readMemoryFile(files, dstLabel, "destination file");
|
|
820
|
+
const plan = selectMovePlan(srcFile, dstFile, effectivePatch, sameFile);
|
|
821
|
+
if (plan.status === "already_applied") {
|
|
822
|
+
return {
|
|
823
|
+
changed: [],
|
|
824
|
+
affected: unique([srcLabel, dstLabel]),
|
|
825
|
+
written: false,
|
|
826
|
+
noop: true,
|
|
827
|
+
status: "already_applied",
|
|
828
|
+
moves: [plan.details]
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
const selection = plan.selection;
|
|
832
|
+
const next = sameFile ? applyMove(srcFile, selection) : applyCrossFileMove(srcFile, dstFile, selection);
|
|
833
|
+
const changed = changedMoveLabels(srcLabel, dstLabel, sameFile, srcFile, dstFile, next);
|
|
834
|
+
return {
|
|
835
|
+
changed,
|
|
836
|
+
affected: unique([srcLabel, dstLabel]),
|
|
837
|
+
written: false,
|
|
838
|
+
noop: changed.length === 0,
|
|
839
|
+
status: changed.length === 0 ? "noop" : "applied",
|
|
840
|
+
moves: [
|
|
841
|
+
moveResultDetails({
|
|
842
|
+
id: effectivePatch.id,
|
|
843
|
+
src: srcLabel,
|
|
844
|
+
dst: dstLabel,
|
|
845
|
+
payloadSha256: effectivePatch.payloadSha256,
|
|
846
|
+
selection
|
|
847
|
+
})
|
|
848
|
+
]
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function reverseMovePatch(patch) {
|
|
852
|
+
const reversedTarget = patch.hasSourceHunk ? {
|
|
853
|
+
before: patch.sourceBefore,
|
|
854
|
+
after: patch.sourceAfter
|
|
855
|
+
} : null;
|
|
856
|
+
return {
|
|
857
|
+
...patch,
|
|
858
|
+
src: patch.dst,
|
|
859
|
+
dst: patch.src,
|
|
860
|
+
hasSourceHunk: patch.target !== null,
|
|
861
|
+
sourceBefore: patch.target?.before ?? Buffer3.alloc(0),
|
|
862
|
+
sourcePayload: patch.sourcePayload,
|
|
863
|
+
sourceAfter: patch.target?.after ?? Buffer3.alloc(0),
|
|
864
|
+
target: reversedTarget
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
async function applyInFileInsertionMove(patch, srcLabel, dstLabel, cwd, dryRun) {
|
|
868
|
+
const target = requireTarget(patch);
|
|
869
|
+
const dstPath = resolvePath(cwd, dstLabel, "destination path");
|
|
870
|
+
const original = await readFileChecked(dstPath, "destination file");
|
|
871
|
+
const alreadyApplied = findAlreadyAppliedTargetSelection(original, patch, dstLabel);
|
|
872
|
+
if (alreadyApplied !== undefined) {
|
|
873
|
+
return oneSidedResult({
|
|
874
|
+
patch,
|
|
875
|
+
src: srcLabel,
|
|
876
|
+
dst: dstLabel,
|
|
877
|
+
changed: [],
|
|
878
|
+
dryRun,
|
|
879
|
+
status: "already_applied",
|
|
880
|
+
sourceRange: null,
|
|
881
|
+
targetRange: alreadyApplied.range,
|
|
882
|
+
insertIndex: alreadyApplied.insertIndex
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
const selection = findTargetSelection(original, target.before, target.after, dstLabel, {
|
|
886
|
+
phase: "target",
|
|
887
|
+
anchor: "blockpatch-target"
|
|
888
|
+
});
|
|
889
|
+
const next = Buffer3.concat([
|
|
890
|
+
original.subarray(0, selection.insertIndex),
|
|
891
|
+
patch.sourcePayload,
|
|
892
|
+
original.subarray(selection.insertIndex)
|
|
893
|
+
]);
|
|
894
|
+
if (!dryRun) {
|
|
895
|
+
await writeAtomically([{ path: dstPath, bytes: next }]);
|
|
896
|
+
}
|
|
897
|
+
return oneSidedResult({
|
|
898
|
+
patch,
|
|
899
|
+
src: srcLabel,
|
|
900
|
+
dst: dstLabel,
|
|
901
|
+
changed: next.equals(original) ? [] : unique([srcLabel, dstLabel]),
|
|
902
|
+
dryRun,
|
|
903
|
+
status: next.equals(original) ? "noop" : "applied",
|
|
904
|
+
sourceRange: null,
|
|
905
|
+
targetRange: selection.range,
|
|
906
|
+
insertIndex: selection.insertIndex
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function checkInFileInsertionMoveInMemory(patch, srcLabel, dstLabel, files) {
|
|
910
|
+
const target = requireTarget(patch);
|
|
911
|
+
const original = readMemoryFile(files, dstLabel, "destination file");
|
|
912
|
+
const alreadyApplied = findAlreadyAppliedTargetSelection(original, patch, dstLabel);
|
|
913
|
+
if (alreadyApplied !== undefined) {
|
|
914
|
+
return oneSidedResult({
|
|
915
|
+
patch,
|
|
916
|
+
src: srcLabel,
|
|
917
|
+
dst: dstLabel,
|
|
918
|
+
changed: [],
|
|
919
|
+
dryRun: true,
|
|
920
|
+
status: "already_applied",
|
|
921
|
+
sourceRange: null,
|
|
922
|
+
targetRange: alreadyApplied.range,
|
|
923
|
+
insertIndex: alreadyApplied.insertIndex
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
const selection = findTargetSelection(original, target.before, target.after, dstLabel, {
|
|
927
|
+
phase: "target",
|
|
928
|
+
anchor: "blockpatch-target"
|
|
929
|
+
});
|
|
930
|
+
const next = Buffer3.concat([
|
|
931
|
+
original.subarray(0, selection.insertIndex),
|
|
932
|
+
patch.sourcePayload,
|
|
933
|
+
original.subarray(selection.insertIndex)
|
|
934
|
+
]);
|
|
935
|
+
return oneSidedResult({
|
|
936
|
+
patch,
|
|
937
|
+
src: srcLabel,
|
|
938
|
+
dst: dstLabel,
|
|
939
|
+
changed: next.equals(original) ? [] : unique([srcLabel, dstLabel]),
|
|
940
|
+
dryRun: true,
|
|
941
|
+
status: next.equals(original) ? "noop" : "applied",
|
|
942
|
+
sourceRange: null,
|
|
943
|
+
targetRange: selection.range,
|
|
944
|
+
insertIndex: selection.insertIndex
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
async function applyInFileDeletionMove(patch, srcLabel, dstLabel, cwd, dryRun) {
|
|
948
|
+
const srcPath = resolvePath(cwd, srcLabel, "source path");
|
|
949
|
+
const original = await readFileChecked(srcPath, "source file");
|
|
950
|
+
const source = findDeletionSourceRange(original, patch, srcLabel);
|
|
951
|
+
if (source === undefined) {
|
|
952
|
+
return oneSidedResult({
|
|
953
|
+
patch,
|
|
954
|
+
src: srcLabel,
|
|
955
|
+
dst: dstLabel,
|
|
956
|
+
changed: [],
|
|
957
|
+
dryRun,
|
|
958
|
+
status: "already_applied",
|
|
959
|
+
sourceRange: null,
|
|
960
|
+
targetRange: null,
|
|
961
|
+
insertIndex: null
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const next = Buffer3.concat([original.subarray(0, source.start), original.subarray(source.end)]);
|
|
965
|
+
if (!dryRun) {
|
|
966
|
+
await writeAtomically([{ path: srcPath, bytes: next }]);
|
|
967
|
+
}
|
|
968
|
+
return oneSidedResult({
|
|
969
|
+
patch,
|
|
970
|
+
src: srcLabel,
|
|
971
|
+
dst: dstLabel,
|
|
972
|
+
changed: next.equals(original) ? [] : unique([srcLabel, dstLabel]),
|
|
973
|
+
dryRun,
|
|
974
|
+
status: next.equals(original) ? "noop" : "applied",
|
|
975
|
+
sourceRange: source,
|
|
976
|
+
targetRange: null,
|
|
977
|
+
insertIndex: null
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
function checkInFileDeletionMoveInMemory(patch, srcLabel, dstLabel, files) {
|
|
981
|
+
const original = readMemoryFile(files, srcLabel, "source file");
|
|
982
|
+
const source = findDeletionSourceRange(original, patch, srcLabel);
|
|
983
|
+
if (source === undefined) {
|
|
984
|
+
return oneSidedResult({
|
|
985
|
+
patch,
|
|
986
|
+
src: srcLabel,
|
|
987
|
+
dst: dstLabel,
|
|
988
|
+
changed: [],
|
|
989
|
+
dryRun: true,
|
|
990
|
+
status: "already_applied",
|
|
991
|
+
sourceRange: null,
|
|
992
|
+
targetRange: null,
|
|
993
|
+
insertIndex: null
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
const next = Buffer3.concat([original.subarray(0, source.start), original.subarray(source.end)]);
|
|
997
|
+
return oneSidedResult({
|
|
998
|
+
patch,
|
|
999
|
+
src: srcLabel,
|
|
1000
|
+
dst: dstLabel,
|
|
1001
|
+
changed: next.equals(original) ? [] : unique([srcLabel, dstLabel]),
|
|
1002
|
+
dryRun: true,
|
|
1003
|
+
status: next.equals(original) ? "noop" : "applied",
|
|
1004
|
+
sourceRange: source,
|
|
1005
|
+
targetRange: null,
|
|
1006
|
+
insertIndex: null
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
function oneSidedResult(args) {
|
|
1010
|
+
return {
|
|
1011
|
+
changed: args.changed,
|
|
1012
|
+
affected: unique([args.src, args.dst]),
|
|
1013
|
+
written: args.status === "applied" && !args.dryRun && args.changed.length > 0,
|
|
1014
|
+
noop: args.status !== "applied" || args.changed.length === 0,
|
|
1015
|
+
status: args.status,
|
|
1016
|
+
moves: [
|
|
1017
|
+
{
|
|
1018
|
+
id: args.patch.id,
|
|
1019
|
+
src: args.src,
|
|
1020
|
+
dst: args.dst,
|
|
1021
|
+
payload_sha256: args.patch.payloadSha256,
|
|
1022
|
+
payload_bytes: args.patch.sourcePayload.length,
|
|
1023
|
+
source_range: args.sourceRange,
|
|
1024
|
+
target_range: args.targetRange,
|
|
1025
|
+
insert_index: args.insertIndex
|
|
1026
|
+
}
|
|
1027
|
+
]
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function checkPathCreationMoveInMemory(patch, dstLabel, files) {
|
|
1031
|
+
const fullTarget = fullTargetBytes(patch);
|
|
1032
|
+
const original = files.get(dstLabel)?.bytes;
|
|
1033
|
+
if (original !== undefined) {
|
|
1034
|
+
if (original.equals(fullTarget)) {
|
|
1035
|
+
return nullSourceResult(patch, dstLabel, true, "already_applied", { start: 0, end: original.length }, 0);
|
|
1036
|
+
}
|
|
1037
|
+
fail("destination_exists", `Destination path for file creation already exists with different bytes: ${dstLabel}`, {
|
|
1038
|
+
path: dstLabel,
|
|
1039
|
+
phase: "target",
|
|
1040
|
+
anchor: "blockpatch-target"
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
return nullSourceResult(patch, dstLabel, true, "applied", { start: 0, end: 0 }, 0);
|
|
1044
|
+
}
|
|
1045
|
+
async function applyPathCreationMove(patch, dstLabel, cwd, dryRun) {
|
|
1046
|
+
const resolved = resolvePathAllowMissing(cwd, dstLabel, "destination path");
|
|
1047
|
+
const fullTarget = fullTargetBytes(patch);
|
|
1048
|
+
if (resolved.exists) {
|
|
1049
|
+
const original = await readFileChecked(resolved.path, "destination file");
|
|
1050
|
+
if (original.equals(fullTarget)) {
|
|
1051
|
+
return nullSourceResult(patch, dstLabel, dryRun, "already_applied", { start: 0, end: original.length }, 0);
|
|
1052
|
+
}
|
|
1053
|
+
fail("destination_exists", `Destination path for file creation already exists with different bytes: ${dstLabel}`, {
|
|
1054
|
+
path: dstLabel,
|
|
1055
|
+
phase: "target",
|
|
1056
|
+
anchor: "blockpatch-target"
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
if (!dryRun) {
|
|
1060
|
+
await writeAtomically([{ path: resolved.path, bytes: fullTarget, create: true }]);
|
|
1061
|
+
}
|
|
1062
|
+
return nullSourceResult(patch, dstLabel, dryRun, "applied", { start: 0, end: 0 }, 0);
|
|
1063
|
+
}
|
|
1064
|
+
function checkPathDeletionMoveInMemory(patch, srcLabel, files) {
|
|
1065
|
+
const original = files.get(srcLabel)?.bytes;
|
|
1066
|
+
if (original === undefined) {
|
|
1067
|
+
return nullTargetResult(patch, srcLabel, true, "already_applied", null);
|
|
1068
|
+
}
|
|
1069
|
+
const fullSource = Buffer3.concat([patch.sourceBefore, patch.sourcePayload, patch.sourceAfter]);
|
|
1070
|
+
if (!original.equals(fullSource)) {
|
|
1071
|
+
fail("source_not_found", `Whole-file source payload was not found in ${srcLabel}`, {
|
|
1072
|
+
path: srcLabel,
|
|
1073
|
+
phase: "source",
|
|
1074
|
+
anchor: "blockpatch-source",
|
|
1075
|
+
matches: 0
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
return nullTargetResult(patch, srcLabel, true, "applied", {
|
|
1079
|
+
start: patch.sourceBefore.length,
|
|
1080
|
+
end: patch.sourceBefore.length + patch.sourcePayload.length
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
async function applyPathDeletionMove(patch, srcLabel, cwd, dryRun) {
|
|
1084
|
+
const resolved = resolvePathAllowMissing(cwd, srcLabel, "source path");
|
|
1085
|
+
if (!resolved.exists) {
|
|
1086
|
+
return nullTargetResult(patch, srcLabel, dryRun, "already_applied", null);
|
|
1087
|
+
}
|
|
1088
|
+
const original = await readFileChecked(resolved.path, "source file");
|
|
1089
|
+
const fullSource = Buffer3.concat([patch.sourceBefore, patch.sourcePayload, patch.sourceAfter]);
|
|
1090
|
+
if (!original.equals(fullSource)) {
|
|
1091
|
+
fail("source_not_found", `Whole-file source payload was not found in ${srcLabel}`, {
|
|
1092
|
+
path: srcLabel,
|
|
1093
|
+
phase: "source",
|
|
1094
|
+
anchor: "blockpatch-source",
|
|
1095
|
+
matches: 0
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (!dryRun) {
|
|
1099
|
+
try {
|
|
1100
|
+
await unlink(resolved.path);
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
failFileSystem(error, srcLabel, "Could not remove file");
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return nullTargetResult(patch, srcLabel, dryRun, "applied", {
|
|
1106
|
+
start: patch.sourceBefore.length,
|
|
1107
|
+
end: patch.sourceBefore.length + patch.sourcePayload.length
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
function fullTargetBytes(patch) {
|
|
1111
|
+
const target = requireTarget(patch);
|
|
1112
|
+
return Buffer3.concat([target.before, patch.sourcePayload, target.after]);
|
|
1113
|
+
}
|
|
1114
|
+
function requireTarget(patch) {
|
|
1115
|
+
if (patch.target === null) {
|
|
1116
|
+
fail("parse_error", "Patch shape requires a blockpatch-target hunk");
|
|
1117
|
+
}
|
|
1118
|
+
return patch.target;
|
|
1119
|
+
}
|
|
1120
|
+
function findDeletionSourceRange(file, patch, srcLabel) {
|
|
1121
|
+
const fullSource = Buffer3.concat([patch.sourceBefore, patch.sourcePayload, patch.sourceAfter]);
|
|
1122
|
+
const fullMatches = indexesOf(file, fullSource);
|
|
1123
|
+
if (fullMatches.length === 1) {
|
|
1124
|
+
const start = fullMatches[0] + patch.sourceBefore.length;
|
|
1125
|
+
return { start, end: start + patch.sourcePayload.length };
|
|
1126
|
+
}
|
|
1127
|
+
if (fullMatches.length > 1) {
|
|
1128
|
+
fail("source_ambiguous", `Source block is ambiguous in ${srcLabel}; matched ${fullMatches.length} locations`, {
|
|
1129
|
+
path: srcLabel,
|
|
1130
|
+
phase: "source",
|
|
1131
|
+
anchor: "blockpatch-source",
|
|
1132
|
+
matches: fullMatches.length,
|
|
1133
|
+
ranges: boundedMatchRanges(fullMatches, fullSource.length),
|
|
1134
|
+
line_ranges: boundedMatchLineRanges(file, fullMatches, fullSource.length)
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
return deletionAlreadyAppliedOrFail(file, patch, srcLabel);
|
|
1138
|
+
}
|
|
1139
|
+
function deletionAlreadyAppliedOrFail(file, patch, srcLabel) {
|
|
1140
|
+
const anchorless = patch.sourceBefore.length === 0 && patch.sourceAfter.length === 0;
|
|
1141
|
+
if (anchorless) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const adjacent = Buffer3.concat([patch.sourceBefore, patch.sourceAfter]);
|
|
1145
|
+
const adjacentMatches = indexesOf(file, adjacent);
|
|
1146
|
+
if (adjacentMatches.length === 1) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (adjacentMatches.length > 1) {
|
|
1150
|
+
fail("source_ambiguous", `Already-deleted source anchors are ambiguous in ${srcLabel}`, {
|
|
1151
|
+
path: srcLabel,
|
|
1152
|
+
phase: "source",
|
|
1153
|
+
anchor: "blockpatch-source",
|
|
1154
|
+
matches: adjacentMatches.length,
|
|
1155
|
+
ranges: boundedMatchRanges(adjacentMatches, adjacent.length),
|
|
1156
|
+
line_ranges: boundedMatchLineRanges(file, adjacentMatches, adjacent.length)
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
const envelopes = findSourceEnvelopes(file, patch);
|
|
1160
|
+
if (envelopes.length === 1) {
|
|
1161
|
+
fail("payload_mismatch", `Source payload does not match located source anchors in ${srcLabel}`, {
|
|
1162
|
+
path: srcLabel,
|
|
1163
|
+
phase: "payload",
|
|
1164
|
+
anchor: "blockpatch-source"
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
if (envelopes.length > 1) {
|
|
1168
|
+
fail("source_ambiguous", `Source anchors are ambiguous in ${srcLabel}; matched ${envelopes.length} locations`, {
|
|
1169
|
+
path: srcLabel,
|
|
1170
|
+
phase: "source",
|
|
1171
|
+
anchor: "blockpatch-source",
|
|
1172
|
+
matches: envelopes.length,
|
|
1173
|
+
ranges: boundedRanges(envelopes),
|
|
1174
|
+
line_ranges: boundedLineRanges(file, envelopes)
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
fail("source_not_found", `Source anchors were not found in ${srcLabel}`, {
|
|
1178
|
+
path: srcLabel,
|
|
1179
|
+
phase: "source",
|
|
1180
|
+
anchor: "blockpatch-source",
|
|
1181
|
+
matches: 0
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
function nullSourceResult(patch, dstLabel, dryRun, status, targetRange, insertIndex) {
|
|
1185
|
+
const applied = status === "applied";
|
|
1186
|
+
return {
|
|
1187
|
+
changed: applied ? [dstLabel] : [],
|
|
1188
|
+
affected: [dstLabel],
|
|
1189
|
+
written: applied && !dryRun,
|
|
1190
|
+
noop: !applied,
|
|
1191
|
+
status,
|
|
1192
|
+
moves: [
|
|
1193
|
+
{
|
|
1194
|
+
id: patch.id,
|
|
1195
|
+
src: devNull,
|
|
1196
|
+
dst: dstLabel,
|
|
1197
|
+
payload_sha256: patch.payloadSha256,
|
|
1198
|
+
payload_bytes: patch.sourcePayload.length,
|
|
1199
|
+
source_range: null,
|
|
1200
|
+
target_range: targetRange,
|
|
1201
|
+
insert_index: insertIndex
|
|
1202
|
+
}
|
|
1203
|
+
]
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function nullTargetResult(patch, srcLabel, dryRun, status, sourceRange) {
|
|
1207
|
+
const applied = status === "applied";
|
|
1208
|
+
return {
|
|
1209
|
+
changed: applied ? [srcLabel] : [],
|
|
1210
|
+
affected: [srcLabel],
|
|
1211
|
+
written: applied && !dryRun,
|
|
1212
|
+
noop: !applied,
|
|
1213
|
+
status,
|
|
1214
|
+
moves: [
|
|
1215
|
+
{
|
|
1216
|
+
id: patch.id,
|
|
1217
|
+
src: srcLabel,
|
|
1218
|
+
dst: devNull,
|
|
1219
|
+
payload_sha256: patch.payloadSha256,
|
|
1220
|
+
payload_bytes: patch.sourcePayload.length,
|
|
1221
|
+
source_range: sourceRange,
|
|
1222
|
+
target_range: null,
|
|
1223
|
+
insert_index: null
|
|
1224
|
+
}
|
|
1225
|
+
]
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
function selectMovePlan(srcFile, dstFile, patch, sameFile) {
|
|
1229
|
+
const srcLabel = patch.src ?? devNull;
|
|
1230
|
+
const dstLabel = patch.dst ?? devNull;
|
|
1231
|
+
const targetAnchor = requireTarget(patch);
|
|
1232
|
+
const source = findSourceRange(srcFile, dstFile, patch);
|
|
1233
|
+
if (source === undefined) {
|
|
1234
|
+
const target2 = findAlreadyAppliedTargetSelection(dstFile, patch);
|
|
1235
|
+
if (target2 === undefined) {
|
|
1236
|
+
fail("source_not_found", `Source anchors were not found in ${srcLabel}`, {
|
|
1237
|
+
path: srcLabel,
|
|
1238
|
+
phase: "source",
|
|
1239
|
+
anchor: "blockpatch-source",
|
|
1240
|
+
matches: 0
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
return {
|
|
1244
|
+
status: "already_applied",
|
|
1245
|
+
details: alreadyAppliedMoveResultDetails({
|
|
1246
|
+
id: patch.id,
|
|
1247
|
+
src: srcLabel,
|
|
1248
|
+
dst: dstLabel,
|
|
1249
|
+
payloadSha256: patch.payloadSha256,
|
|
1250
|
+
payload: patch.sourcePayload,
|
|
1251
|
+
target: target2
|
|
1252
|
+
})
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
const target = findTargetSelection(dstFile, targetAnchor.before, targetAnchor.after, dstLabel, {
|
|
1256
|
+
phase: "target",
|
|
1257
|
+
anchor: "blockpatch-target"
|
|
1258
|
+
});
|
|
1259
|
+
return {
|
|
1260
|
+
status: "pending",
|
|
1261
|
+
selection: buildMoveSelection(srcFile, source, target, sameFile, dstLabel)
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function buildMoveSelection(srcFile, source, target, sameFile, dstLabel) {
|
|
1265
|
+
if (sameFile && rangesOverlap(source, target.range)) {
|
|
1266
|
+
fail("target_overlaps_source", `Target anchor for ${dstLabel} overlaps the source block`, {
|
|
1267
|
+
path: dstLabel,
|
|
1268
|
+
phase: "target",
|
|
1269
|
+
anchor: "blockpatch-target"
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
source,
|
|
1274
|
+
target,
|
|
1275
|
+
payload: Buffer3.from(srcFile.subarray(source.start, source.end))
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
async function commitMove(args) {
|
|
1279
|
+
const next = args.sameFile ? applyMove(args.srcOriginal, args.selection) : applyCrossFileMove(args.srcOriginal, args.dstOriginal, args.selection);
|
|
1280
|
+
const srcChanged = !next.src.equals(args.srcOriginal);
|
|
1281
|
+
const dstChanged = !args.sameFile && !next.dst.equals(args.dstOriginal);
|
|
1282
|
+
const sameFileAlias = args.sameFile && args.srcPath !== args.dstPath;
|
|
1283
|
+
if (!args.dryRun) {
|
|
1284
|
+
const writes = [];
|
|
1285
|
+
if (dstChanged || sameFileAlias && srcChanged) {
|
|
1286
|
+
writes.push({ path: args.dstPath, bytes: next.dst });
|
|
1287
|
+
}
|
|
1288
|
+
if (srcChanged) {
|
|
1289
|
+
writes.push({ path: args.srcPath, bytes: next.src });
|
|
1290
|
+
}
|
|
1291
|
+
await writeAtomically(writes);
|
|
1292
|
+
}
|
|
1293
|
+
const changed = [];
|
|
1294
|
+
if (srcChanged) {
|
|
1295
|
+
changed.push(args.srcLabel);
|
|
1296
|
+
if (sameFileAlias) {
|
|
1297
|
+
changed.push(args.dstLabel);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (dstChanged) {
|
|
1301
|
+
changed.push(args.dstLabel);
|
|
1302
|
+
}
|
|
1303
|
+
return unique(changed);
|
|
1304
|
+
}
|
|
1305
|
+
function applyMove(file, selection) {
|
|
1306
|
+
const withoutSource = Buffer3.concat([
|
|
1307
|
+
file.subarray(0, selection.source.start),
|
|
1308
|
+
file.subarray(selection.source.end)
|
|
1309
|
+
]);
|
|
1310
|
+
const targetIndex = selection.target.insertIndex >= selection.source.end ? selection.target.insertIndex - selection.payload.length : selection.target.insertIndex;
|
|
1311
|
+
const next = Buffer3.concat([
|
|
1312
|
+
withoutSource.subarray(0, targetIndex),
|
|
1313
|
+
selection.payload,
|
|
1314
|
+
withoutSource.subarray(targetIndex)
|
|
1315
|
+
]);
|
|
1316
|
+
return { src: next, dst: next };
|
|
1317
|
+
}
|
|
1318
|
+
function applyCrossFileMove(srcFile, dstFile, selection) {
|
|
1319
|
+
return {
|
|
1320
|
+
src: Buffer3.concat([srcFile.subarray(0, selection.source.start), srcFile.subarray(selection.source.end)]),
|
|
1321
|
+
dst: Buffer3.concat([
|
|
1322
|
+
dstFile.subarray(0, selection.target.insertIndex),
|
|
1323
|
+
selection.payload,
|
|
1324
|
+
dstFile.subarray(selection.target.insertIndex)
|
|
1325
|
+
])
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function findSourceRange(srcFile, dstFile, patch) {
|
|
1329
|
+
const srcLabel = patch.src ?? devNull;
|
|
1330
|
+
const fullSource = Buffer3.concat([patch.sourceBefore, patch.sourcePayload, patch.sourceAfter]);
|
|
1331
|
+
const fullMatches = indexesOf(srcFile, fullSource);
|
|
1332
|
+
if (fullMatches.length === 1) {
|
|
1333
|
+
const start = fullMatches[0] + patch.sourceBefore.length;
|
|
1334
|
+
return { start, end: start + patch.sourcePayload.length };
|
|
1335
|
+
}
|
|
1336
|
+
if (fullMatches.length > 1) {
|
|
1337
|
+
fail("source_ambiguous", `Source block is ambiguous in ${srcLabel}; matched ${fullMatches.length} locations`, {
|
|
1338
|
+
path: srcLabel,
|
|
1339
|
+
phase: "source",
|
|
1340
|
+
anchor: "blockpatch-source",
|
|
1341
|
+
matches: fullMatches.length,
|
|
1342
|
+
ranges: boundedMatchRanges(fullMatches, fullSource.length),
|
|
1343
|
+
line_ranges: boundedMatchLineRanges(srcFile, fullMatches, fullSource.length)
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
const alreadyApplied = findAlreadyAppliedTargetSelection(dstFile, patch);
|
|
1347
|
+
if (alreadyApplied !== undefined) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const envelopes = findSourceEnvelopes(srcFile, patch);
|
|
1351
|
+
if (envelopes.length === 1) {
|
|
1352
|
+
fail("payload_mismatch", `Source payload does not match located source anchors in ${srcLabel}`, {
|
|
1353
|
+
path: srcLabel,
|
|
1354
|
+
phase: "payload",
|
|
1355
|
+
anchor: "blockpatch-source"
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
if (envelopes.length > 1) {
|
|
1359
|
+
fail("source_ambiguous", `Source anchors are ambiguous in ${srcLabel}; matched ${envelopes.length} locations`, {
|
|
1360
|
+
path: srcLabel,
|
|
1361
|
+
phase: "source",
|
|
1362
|
+
anchor: "blockpatch-source",
|
|
1363
|
+
matches: envelopes.length,
|
|
1364
|
+
ranges: boundedRanges(envelopes),
|
|
1365
|
+
line_ranges: boundedLineRanges(srcFile, envelopes)
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
fail("source_not_found", `Source anchors were not found in ${srcLabel}`, {
|
|
1369
|
+
path: srcLabel,
|
|
1370
|
+
phase: "source",
|
|
1371
|
+
anchor: "blockpatch-source",
|
|
1372
|
+
matches: 0
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
function findAlreadyAppliedTargetSelection(file, patch, dstLabel = patch.dst ?? devNull) {
|
|
1376
|
+
const target = requireTarget(patch);
|
|
1377
|
+
const alreadyApplied = Buffer3.concat([target.before, patch.sourcePayload, target.after]);
|
|
1378
|
+
const matches = indexesOf(file, alreadyApplied);
|
|
1379
|
+
if (matches.length === 0) {
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (matches.length > 1) {
|
|
1383
|
+
fail("target_ambiguous", `Already-applied target is ambiguous in ${dstLabel}; matched ${matches.length} locations`, {
|
|
1384
|
+
path: dstLabel,
|
|
1385
|
+
phase: "target",
|
|
1386
|
+
anchor: "blockpatch-target",
|
|
1387
|
+
matches: matches.length,
|
|
1388
|
+
ranges: boundedMatchRanges(matches, alreadyApplied.length),
|
|
1389
|
+
line_ranges: boundedMatchLineRanges(file, matches, alreadyApplied.length)
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
const start = matches[0];
|
|
1393
|
+
return {
|
|
1394
|
+
range: { start, end: start + alreadyApplied.length },
|
|
1395
|
+
insertIndex: start + target.before.length
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function findSourceEnvelopes(file, patch) {
|
|
1399
|
+
const beforeMatches = indexesOf(file, patch.sourceBefore);
|
|
1400
|
+
const afterMatches = indexesOf(file, patch.sourceAfter);
|
|
1401
|
+
const ranges = [];
|
|
1402
|
+
for (const beforeStart of beforeMatches) {
|
|
1403
|
+
const payloadStart = beforeStart + patch.sourceBefore.length;
|
|
1404
|
+
const afterStart = afterMatches.find((candidate) => candidate >= payloadStart);
|
|
1405
|
+
if (afterStart !== undefined) {
|
|
1406
|
+
ranges.push({ start: payloadStart, end: afterStart });
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return ranges;
|
|
1410
|
+
}
|
|
1411
|
+
function findTargetSelection(file, before, after, dstLabel, details = {}) {
|
|
1412
|
+
const anchor = Buffer3.concat([before, after]);
|
|
1413
|
+
const matches = indexesOf(file, anchor);
|
|
1414
|
+
if (matches.length === 0) {
|
|
1415
|
+
fail("target_not_found", `Target anchor was not found in ${dstLabel}`, {
|
|
1416
|
+
path: dstLabel,
|
|
1417
|
+
...details,
|
|
1418
|
+
matches: 0
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
if (matches.length > 1) {
|
|
1422
|
+
fail("target_ambiguous", `Target anchor is ambiguous in ${dstLabel}; matched ${matches.length} locations`, {
|
|
1423
|
+
path: dstLabel,
|
|
1424
|
+
...details,
|
|
1425
|
+
matches: matches.length,
|
|
1426
|
+
ranges: boundedMatchRanges(matches, anchor.length),
|
|
1427
|
+
line_ranges: boundedMatchLineRanges(file, matches, anchor.length)
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
const start = matches[0];
|
|
1431
|
+
return {
|
|
1432
|
+
range: { start, end: start + anchor.length },
|
|
1433
|
+
insertIndex: start + before.length
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function indexesOf(haystack, needle) {
|
|
1437
|
+
if (needle.length === 0) {
|
|
1438
|
+
return [];
|
|
1439
|
+
}
|
|
1440
|
+
const indexes = [];
|
|
1441
|
+
let index = haystack.indexOf(needle);
|
|
1442
|
+
while (index !== -1) {
|
|
1443
|
+
indexes.push(index);
|
|
1444
|
+
index = haystack.indexOf(needle, index + 1);
|
|
1445
|
+
}
|
|
1446
|
+
return indexes;
|
|
1447
|
+
}
|
|
1448
|
+
function rangesOverlap(left, right) {
|
|
1449
|
+
return left.start < right.end && right.start < left.end;
|
|
1450
|
+
}
|
|
1451
|
+
function moveResultDetails(args) {
|
|
1452
|
+
return {
|
|
1453
|
+
id: args.id,
|
|
1454
|
+
src: args.src,
|
|
1455
|
+
dst: args.dst,
|
|
1456
|
+
payload_sha256: args.payloadSha256,
|
|
1457
|
+
payload_bytes: args.selection.payload.length,
|
|
1458
|
+
source_range: args.selection.source,
|
|
1459
|
+
target_range: args.selection.target.range,
|
|
1460
|
+
insert_index: args.selection.target.insertIndex
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function alreadyAppliedMoveResultDetails(args) {
|
|
1464
|
+
return {
|
|
1465
|
+
id: args.id,
|
|
1466
|
+
src: args.src,
|
|
1467
|
+
dst: args.dst,
|
|
1468
|
+
payload_sha256: args.payloadSha256,
|
|
1469
|
+
payload_bytes: args.payload.length,
|
|
1470
|
+
source_range: null,
|
|
1471
|
+
target_range: args.target.range,
|
|
1472
|
+
insert_index: args.target.insertIndex
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function unique(paths) {
|
|
1476
|
+
return [...new Set(paths)];
|
|
1477
|
+
}
|
|
1478
|
+
function changedMoveLabels(srcLabel, dstLabel, sameFile, srcOriginal, dstOriginal, next) {
|
|
1479
|
+
const srcChanged = !next.src.equals(srcOriginal);
|
|
1480
|
+
const dstChanged = !sameFile && !next.dst.equals(dstOriginal);
|
|
1481
|
+
const sameFileAlias = sameFile && srcLabel !== dstLabel;
|
|
1482
|
+
const changed = [];
|
|
1483
|
+
if (srcChanged) {
|
|
1484
|
+
changed.push(srcLabel);
|
|
1485
|
+
if (sameFileAlias) {
|
|
1486
|
+
changed.push(dstLabel);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (dstChanged) {
|
|
1490
|
+
changed.push(dstLabel);
|
|
1491
|
+
}
|
|
1492
|
+
return unique(changed);
|
|
1493
|
+
}
|
|
1494
|
+
function memoryFileMap(files) {
|
|
1495
|
+
const mapped = new Map;
|
|
1496
|
+
for (const file of files) {
|
|
1497
|
+
if (mapped.has(file.path)) {
|
|
1498
|
+
fail("parse_error", `Duplicate in-memory file: ${file.path}`, { path: file.path, phase: "check" });
|
|
1499
|
+
}
|
|
1500
|
+
mapped.set(file.path, {
|
|
1501
|
+
bytes: file.bytes,
|
|
1502
|
+
identity: file.identity ?? file.path
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
return mapped;
|
|
1506
|
+
}
|
|
1507
|
+
function readMemoryFile(files, path, label) {
|
|
1508
|
+
const file = files.get(path);
|
|
1509
|
+
if (file === undefined) {
|
|
1510
|
+
fail("file_not_found", `Could not read ${label}: ${path}`, { path, phase: "io" });
|
|
1511
|
+
}
|
|
1512
|
+
return file.bytes;
|
|
1513
|
+
}
|
|
1514
|
+
function sameMemoryFile(files, left, right) {
|
|
1515
|
+
if (left === right) {
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
const leftFile = files.get(left);
|
|
1519
|
+
const rightFile = files.get(right);
|
|
1520
|
+
return leftFile !== undefined && rightFile !== undefined && leftFile.identity === rightFile.identity;
|
|
1521
|
+
}
|
|
1522
|
+
async function writeAtomic(path, bytes) {
|
|
1523
|
+
await writeAtomically([{ path, bytes }]);
|
|
1524
|
+
}
|
|
1525
|
+
async function writeAtomically(writes) {
|
|
1526
|
+
const staged = [];
|
|
1527
|
+
try {
|
|
1528
|
+
for (const write of writes) {
|
|
1529
|
+
staged.push(await stageAtomicWrite(write.path, write.bytes, write.create === true));
|
|
1530
|
+
}
|
|
1531
|
+
for (const write of staged) {
|
|
1532
|
+
try {
|
|
1533
|
+
await rename(write.temp, write.path);
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
failFileSystem(error, write.path, "Could not replace file");
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
await Promise.all(staged.map((write) => unlink(write.temp).catch(() => {
|
|
1540
|
+
return;
|
|
1541
|
+
})));
|
|
1542
|
+
throw error;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
async function stageAtomicWrite(path, bytes, create = false) {
|
|
1546
|
+
let mode;
|
|
1547
|
+
let createMissing = false;
|
|
1548
|
+
if (create) {
|
|
1549
|
+
const info = await statOptional(path);
|
|
1550
|
+
if (info !== undefined) {
|
|
1551
|
+
assertRegularFile(info, path, "output file");
|
|
1552
|
+
mode = info.mode;
|
|
1553
|
+
}
|
|
1554
|
+
if (mode === undefined) {
|
|
1555
|
+
createMissing = true;
|
|
1556
|
+
mode = 420;
|
|
1557
|
+
}
|
|
1558
|
+
} else {
|
|
1559
|
+
const info = await statChecked(path, "output file");
|
|
1560
|
+
assertRegularFile(info, path, "output file");
|
|
1561
|
+
mode = info.mode;
|
|
1562
|
+
}
|
|
1563
|
+
const dir = dirname(path);
|
|
1564
|
+
const base = basename(path);
|
|
1565
|
+
const temp = join2(dir, `.${base}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
1566
|
+
try {
|
|
1567
|
+
if (createMissing) {
|
|
1568
|
+
await mkdir(dir, { recursive: true });
|
|
1569
|
+
}
|
|
1570
|
+
await writeFile(temp, bytes, { flag: "wx" });
|
|
1571
|
+
if (mode !== undefined) {
|
|
1572
|
+
await chmod(temp, mode);
|
|
1573
|
+
}
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
await unlink(temp).catch(() => {
|
|
1576
|
+
return;
|
|
1577
|
+
});
|
|
1578
|
+
failFileSystem(error, path, "Could not stage file replacement");
|
|
1579
|
+
}
|
|
1580
|
+
return { path, temp };
|
|
1581
|
+
}
|
|
1582
|
+
async function statOptional(path) {
|
|
1583
|
+
try {
|
|
1584
|
+
return await stat2(path);
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
const code = error.code;
|
|
1587
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
failFileSystem(error, path, "Could not stat output file");
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/move.ts
|
|
1595
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1596
|
+
import { posix as posix2 } from "node:path";
|
|
1597
|
+
import { TextDecoder } from "node:util";
|
|
1598
|
+
var moveArgTypes = {
|
|
1599
|
+
src: "string",
|
|
1600
|
+
src_start: "string",
|
|
1601
|
+
src_end: "string",
|
|
1602
|
+
dst: "string",
|
|
1603
|
+
payload: "string",
|
|
1604
|
+
target_before: "string",
|
|
1605
|
+
target_after: "string",
|
|
1606
|
+
expected_payload_sha256: "string",
|
|
1607
|
+
dry_run: "boolean"
|
|
1608
|
+
};
|
|
1609
|
+
var utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1610
|
+
async function moveBlock(args, options = {}) {
|
|
1611
|
+
const validated = validateMoveArgs(args);
|
|
1612
|
+
const normalized = normalizeArgs(validated);
|
|
1613
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1614
|
+
const dryRun = options.dryRun ?? validated.dry_run ?? false;
|
|
1615
|
+
if (normalized.kind === "insertion") {
|
|
1616
|
+
return insertPayload(normalized, validated, cwd, dryRun, options);
|
|
1617
|
+
}
|
|
1618
|
+
if (normalized.kind === "deletion") {
|
|
1619
|
+
return deletePayload(normalized, validated, cwd, dryRun, options);
|
|
1620
|
+
}
|
|
1621
|
+
const srcPath = resolvePath(cwd, normalized.src, "source path");
|
|
1622
|
+
const dstPath = resolvePath(cwd, normalized.dst, "destination path");
|
|
1623
|
+
const sameFile = await sameFileIdentity(srcPath, dstPath);
|
|
1624
|
+
const srcOriginal = await readFileChecked(srcPath, "source file");
|
|
1625
|
+
const dstOriginal = sameFile ? srcOriginal : await readFileChecked(dstPath, "destination file");
|
|
1626
|
+
const source = findSource(srcOriginal, normalized);
|
|
1627
|
+
const target = findTargetSelection(dstOriginal, normalized.targetBefore, normalized.targetAfter, normalized.dst, {
|
|
1628
|
+
phase: "target",
|
|
1629
|
+
anchor: targetAnchorName(normalized)
|
|
1630
|
+
});
|
|
1631
|
+
const selection = buildMoveSelection(srcOriginal, source, target, sameFile, normalized.dst);
|
|
1632
|
+
const payloadSha256 = createHash2("sha256").update(selection.payload).digest("hex");
|
|
1633
|
+
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
1634
|
+
const samePatchLabel = samePatchPath2(normalized.src, normalized.dst);
|
|
1635
|
+
const patch = options.diff ? renderMovePatch(normalized, srcOriginal, dstOriginal, selection, sameFile && samePatchLabel, payloadSha256) : undefined;
|
|
1636
|
+
await selfCheckRenderedPatch(patch, pairedSelfCheckFiles(normalized.src, normalized.dst, srcOriginal, dstOriginal, sameFile));
|
|
1637
|
+
const writeSuppressed = dryRun || options.diff === true;
|
|
1638
|
+
const changed = await commitMove({
|
|
1639
|
+
srcPath,
|
|
1640
|
+
dstPath,
|
|
1641
|
+
sameFile,
|
|
1642
|
+
dryRun: writeSuppressed,
|
|
1643
|
+
srcOriginal,
|
|
1644
|
+
dstOriginal,
|
|
1645
|
+
selection,
|
|
1646
|
+
srcLabel: normalized.src,
|
|
1647
|
+
dstLabel: normalized.dst
|
|
1648
|
+
});
|
|
1649
|
+
return {
|
|
1650
|
+
changed,
|
|
1651
|
+
affected: unique([normalized.src, normalized.dst]),
|
|
1652
|
+
written: !writeSuppressed && changed.length > 0,
|
|
1653
|
+
noop: changed.length === 0,
|
|
1654
|
+
status: changed.length === 0 ? "noop" : "applied",
|
|
1655
|
+
moves: [
|
|
1656
|
+
moveResultDetails({
|
|
1657
|
+
id: "move-1",
|
|
1658
|
+
src: normalized.src,
|
|
1659
|
+
dst: normalized.dst,
|
|
1660
|
+
payloadSha256,
|
|
1661
|
+
selection
|
|
1662
|
+
})
|
|
1663
|
+
],
|
|
1664
|
+
patch
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function validateMoveArgs(value) {
|
|
1668
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1669
|
+
fail("invalid_move_args", "move arguments must be a JSON object");
|
|
1670
|
+
}
|
|
1671
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
1672
|
+
const expected = moveArgTypes[key];
|
|
1673
|
+
if (expected === undefined) {
|
|
1674
|
+
fail("invalid_move_args", `Unknown move argument: ${key}`, { field: key });
|
|
1675
|
+
}
|
|
1676
|
+
if (typeof fieldValue !== expected) {
|
|
1677
|
+
fail("invalid_move_args", `move argument ${key} must be a ${expected}`, { field: key });
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
const args = value;
|
|
1681
|
+
if (args.expected_payload_sha256 !== undefined && !/^[a-f0-9]{64}$/.test(args.expected_payload_sha256)) {
|
|
1682
|
+
fail("invalid_move_args", "expected_payload_sha256 must be a 64-character lowercase sha256 hex digest", {
|
|
1683
|
+
field: "expected_payload_sha256"
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
return args;
|
|
1687
|
+
}
|
|
1688
|
+
function normalizeArgs(args) {
|
|
1689
|
+
if (!args.src) {
|
|
1690
|
+
fail("invalid_move_args", "move requires src", { field: "src" });
|
|
1691
|
+
}
|
|
1692
|
+
const dst = args.dst ?? args.src;
|
|
1693
|
+
if (args.src === devNull && dst === devNull) {
|
|
1694
|
+
fail("invalid_move_args", "move cannot use /dev/null for both src and dst", { field: "dst" });
|
|
1695
|
+
}
|
|
1696
|
+
if (args.src === devNull) {
|
|
1697
|
+
if (args.dst === undefined) {
|
|
1698
|
+
fail("invalid_move_args", "move from /dev/null requires dst", { field: "dst" });
|
|
1699
|
+
}
|
|
1700
|
+
if (args.src_start !== undefined || args.src_end !== undefined) {
|
|
1701
|
+
fail("invalid_move_args", "move from /dev/null uses payload instead of src_start/src_end", {
|
|
1702
|
+
field: args.src_start !== undefined ? "src_start" : "src_end"
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
if (args.payload === undefined) {
|
|
1706
|
+
fail("invalid_move_args", "move from /dev/null requires payload", { field: "payload" });
|
|
1707
|
+
}
|
|
1708
|
+
if (args.payload.length === 0) {
|
|
1709
|
+
fail("invalid_move_args", "move from /dev/null requires non-empty payload", { field: "payload" });
|
|
1710
|
+
}
|
|
1711
|
+
const target2 = normalizeTargetArgs(args);
|
|
1712
|
+
return {
|
|
1713
|
+
kind: "insertion",
|
|
1714
|
+
src: devNull,
|
|
1715
|
+
dst: args.dst,
|
|
1716
|
+
payload: Buffer.from(args.payload, "utf8"),
|
|
1717
|
+
targetBefore: target2.before,
|
|
1718
|
+
targetAfter: target2.after
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
if (dst === devNull) {
|
|
1722
|
+
if (args.payload !== undefined) {
|
|
1723
|
+
fail("invalid_move_args", "move to /dev/null selects payload from src_start/src_end", { field: "payload" });
|
|
1724
|
+
}
|
|
1725
|
+
if (args.target_before !== undefined || args.target_after !== undefined) {
|
|
1726
|
+
fail("invalid_move_args", "move to /dev/null must not include target anchors", {
|
|
1727
|
+
field: args.target_before !== undefined ? "target_before" : "target_after"
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
return {
|
|
1731
|
+
kind: "deletion",
|
|
1732
|
+
src: args.src,
|
|
1733
|
+
srcStart: requiredBuffer(args.src_start, "src_start"),
|
|
1734
|
+
srcEnd: requiredBuffer(args.src_end, "src_end"),
|
|
1735
|
+
dst: devNull
|
|
1736
|
+
};
|
|
13
1737
|
}
|
|
1738
|
+
if (args.payload !== undefined) {
|
|
1739
|
+
fail("invalid_move_args", "payload is only valid when src is /dev/null", { field: "payload" });
|
|
1740
|
+
}
|
|
1741
|
+
const target = normalizeTargetArgs(args);
|
|
1742
|
+
return {
|
|
1743
|
+
kind: "relocation",
|
|
1744
|
+
src: args.src,
|
|
1745
|
+
srcStart: requiredBuffer(args.src_start, "src_start"),
|
|
1746
|
+
srcEnd: requiredBuffer(args.src_end, "src_end"),
|
|
1747
|
+
dst,
|
|
1748
|
+
targetBefore: target.before,
|
|
1749
|
+
targetAfter: target.after
|
|
1750
|
+
};
|
|
14
1751
|
}
|
|
15
|
-
function
|
|
16
|
-
|
|
1752
|
+
function normalizeTargetArgs(args) {
|
|
1753
|
+
const hasTargetBefore = args.target_before !== undefined;
|
|
1754
|
+
const hasTargetAfter = args.target_after !== undefined;
|
|
1755
|
+
if (!hasTargetBefore && !hasTargetAfter) {
|
|
1756
|
+
fail("invalid_move_args", "move requires target_before or target_after", { field: "target_before" });
|
|
1757
|
+
}
|
|
1758
|
+
const targetBefore = Buffer.from(args.target_before ?? "", "utf8");
|
|
1759
|
+
const targetAfter = Buffer.from(args.target_after ?? "", "utf8");
|
|
1760
|
+
if (targetBefore.length === 0 && targetAfter.length === 0) {
|
|
1761
|
+
fail("invalid_move_args", "move requires non-empty target context", { field: "target_before" });
|
|
1762
|
+
}
|
|
1763
|
+
return { before: targetBefore, after: targetAfter };
|
|
17
1764
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1765
|
+
function requiredBuffer(value, field) {
|
|
1766
|
+
if (!value) {
|
|
1767
|
+
fail("invalid_move_args", `move requires ${field}`, { field });
|
|
1768
|
+
}
|
|
1769
|
+
return Buffer.from(value, "utf8");
|
|
1770
|
+
}
|
|
1771
|
+
async function insertPayload(args, validated, cwd, dryRun, options) {
|
|
1772
|
+
const dstPath = resolvePath(cwd, args.dst, "destination path");
|
|
1773
|
+
const original = await readFileChecked(dstPath, "destination file");
|
|
1774
|
+
const payloadSha256 = createHash2("sha256").update(args.payload).digest("hex");
|
|
1775
|
+
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
1776
|
+
const alreadyApplied = findAlreadyAppliedTarget(original, args, args.dst);
|
|
1777
|
+
if (alreadyApplied !== undefined) {
|
|
1778
|
+
const renderedPatch2 = options.diff ? renderInsertionPatch(args, original, alreadyApplied, payloadSha256) : undefined;
|
|
1779
|
+
await selfCheckRenderedPatch(renderedPatch2, [{ path: args.dst, bytes: original }]);
|
|
1780
|
+
return {
|
|
1781
|
+
changed: [],
|
|
1782
|
+
affected: [args.dst],
|
|
1783
|
+
written: false,
|
|
1784
|
+
noop: true,
|
|
1785
|
+
status: "already_applied",
|
|
1786
|
+
moves: [
|
|
1787
|
+
{
|
|
1788
|
+
id: "move-1",
|
|
1789
|
+
src: devNull,
|
|
1790
|
+
dst: args.dst,
|
|
1791
|
+
payload_sha256: payloadSha256,
|
|
1792
|
+
payload_bytes: args.payload.length,
|
|
1793
|
+
source_range: null,
|
|
1794
|
+
target_range: alreadyApplied.range,
|
|
1795
|
+
insert_index: alreadyApplied.insertIndex
|
|
1796
|
+
}
|
|
1797
|
+
],
|
|
1798
|
+
patch: renderedPatch2
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
const target = findTargetSelection(original, args.targetBefore, args.targetAfter, args.dst, {
|
|
1802
|
+
phase: "target",
|
|
1803
|
+
anchor: targetAnchorName(args)
|
|
1804
|
+
});
|
|
1805
|
+
const renderedPatch = options.diff ? renderInsertionPatch(args, original, target, payloadSha256) : undefined;
|
|
1806
|
+
await selfCheckRenderedPatch(renderedPatch, [{ path: args.dst, bytes: original }]);
|
|
1807
|
+
const writeSuppressed = dryRun || options.diff === true;
|
|
1808
|
+
const next = Buffer.concat([
|
|
1809
|
+
original.subarray(0, target.insertIndex),
|
|
1810
|
+
args.payload,
|
|
1811
|
+
original.subarray(target.insertIndex)
|
|
1812
|
+
]);
|
|
1813
|
+
const changed = next.equals(original) ? [] : [args.dst];
|
|
1814
|
+
if (!writeSuppressed && changed.length > 0) {
|
|
1815
|
+
await writeAtomic(dstPath, next);
|
|
1816
|
+
}
|
|
1817
|
+
return {
|
|
1818
|
+
changed,
|
|
1819
|
+
affected: [args.dst],
|
|
1820
|
+
written: !writeSuppressed && changed.length > 0,
|
|
1821
|
+
noop: changed.length === 0,
|
|
1822
|
+
status: changed.length === 0 ? "noop" : "applied",
|
|
1823
|
+
moves: [
|
|
1824
|
+
{
|
|
1825
|
+
id: "move-1",
|
|
1826
|
+
src: devNull,
|
|
1827
|
+
dst: args.dst,
|
|
1828
|
+
payload_sha256: payloadSha256,
|
|
1829
|
+
payload_bytes: args.payload.length,
|
|
1830
|
+
source_range: null,
|
|
1831
|
+
target_range: target.range,
|
|
1832
|
+
insert_index: target.insertIndex
|
|
1833
|
+
}
|
|
1834
|
+
],
|
|
1835
|
+
patch: renderedPatch
|
|
58
1836
|
};
|
|
59
|
-
|
|
60
|
-
|
|
1837
|
+
}
|
|
1838
|
+
function verifyExpectedPayloadHash(args, payloadSha256) {
|
|
1839
|
+
if (args.expected_payload_sha256 !== undefined && args.expected_payload_sha256 !== payloadSha256) {
|
|
1840
|
+
fail("hash_mismatch", "expected_payload_sha256 does not match selected source payload", {
|
|
1841
|
+
field: "expected_payload_sha256",
|
|
1842
|
+
phase: "payload",
|
|
1843
|
+
anchor: "expected_payload_sha256"
|
|
1844
|
+
});
|
|
61
1845
|
}
|
|
62
|
-
|
|
63
|
-
|
|
1846
|
+
}
|
|
1847
|
+
function findAlreadyAppliedTarget(file, args, dstLabel) {
|
|
1848
|
+
const alreadyApplied = Buffer.concat([args.targetBefore, args.payload, args.targetAfter]);
|
|
1849
|
+
const matches = indexesOf(file, alreadyApplied);
|
|
1850
|
+
if (matches.length === 0) {
|
|
1851
|
+
return;
|
|
64
1852
|
}
|
|
65
|
-
if (
|
|
66
|
-
|
|
1853
|
+
if (matches.length > 1) {
|
|
1854
|
+
const ranges = boundedRanges(matches.map((start2) => ({ start: start2, end: start2 + alreadyApplied.length })));
|
|
1855
|
+
fail("target_ambiguous", `Already-applied target is ambiguous in ${dstLabel}; matched ${matches.length} locations`, {
|
|
1856
|
+
path: dstLabel,
|
|
1857
|
+
phase: "target",
|
|
1858
|
+
anchor: "target_before+payload+target_after",
|
|
1859
|
+
matches: matches.length,
|
|
1860
|
+
ranges,
|
|
1861
|
+
line_ranges: boundedLineRanges(file, ranges)
|
|
1862
|
+
});
|
|
67
1863
|
}
|
|
68
|
-
|
|
69
|
-
|
|
1864
|
+
const start = matches[0];
|
|
1865
|
+
return {
|
|
1866
|
+
range: { start, end: start + alreadyApplied.length },
|
|
1867
|
+
insertIndex: start + args.targetBefore.length
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
async function deletePayload(args, validated, cwd, dryRun, options) {
|
|
1871
|
+
const srcPath = resolvePath(cwd, args.src, "source path");
|
|
1872
|
+
const original = await readFileChecked(srcPath, "source file");
|
|
1873
|
+
const source = findSource(original, args);
|
|
1874
|
+
const payload = Buffer.from(original.subarray(source.start, source.end));
|
|
1875
|
+
const payloadSha256 = createHash2("sha256").update(payload).digest("hex");
|
|
1876
|
+
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
1877
|
+
const renderedPatch = options.diff ? renderDeletionPatch(args, original, source, payload, payloadSha256) : undefined;
|
|
1878
|
+
await selfCheckRenderedPatch(renderedPatch, [{ path: args.src, bytes: original }]);
|
|
1879
|
+
const writeSuppressed = dryRun || options.diff === true;
|
|
1880
|
+
const next = Buffer.concat([original.subarray(0, source.start), original.subarray(source.end)]);
|
|
1881
|
+
const changed = next.equals(original) ? [] : [args.src];
|
|
1882
|
+
if (!writeSuppressed && changed.length > 0) {
|
|
1883
|
+
await writeAtomic(srcPath, next);
|
|
70
1884
|
}
|
|
71
1885
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
1886
|
+
changed,
|
|
1887
|
+
affected: [args.src],
|
|
1888
|
+
written: !writeSuppressed && changed.length > 0,
|
|
1889
|
+
noop: changed.length === 0,
|
|
1890
|
+
status: changed.length === 0 ? "noop" : "applied",
|
|
1891
|
+
moves: [
|
|
1892
|
+
{
|
|
1893
|
+
id: "move-1",
|
|
1894
|
+
src: args.src,
|
|
1895
|
+
dst: devNull,
|
|
1896
|
+
payload_sha256: payloadSha256,
|
|
1897
|
+
payload_bytes: payload.length,
|
|
1898
|
+
source_range: source,
|
|
1899
|
+
target_range: null,
|
|
1900
|
+
insert_index: null
|
|
1901
|
+
}
|
|
1902
|
+
],
|
|
1903
|
+
patch: renderedPatch
|
|
78
1904
|
};
|
|
79
1905
|
}
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const textEnd = lineEnd > lineStart && input[lineEnd - 1] === 13 ? lineEnd - 1 : lineEnd;
|
|
89
|
-
const line = input.subarray(lineStart, textEnd).toString("utf8");
|
|
90
|
-
const marker = markerForLine(line, lineStart, contentStart);
|
|
91
|
-
if (marker !== undefined) {
|
|
92
|
-
markers.push(marker);
|
|
93
|
-
}
|
|
94
|
-
if (lf === -1) {
|
|
95
|
-
break;
|
|
1906
|
+
function findSource(file, args) {
|
|
1907
|
+
const startMatches = indexesOf(file, args.srcStart);
|
|
1908
|
+
const ranges = [];
|
|
1909
|
+
for (const start of startMatches) {
|
|
1910
|
+
const searchFrom = start + args.srcStart.length;
|
|
1911
|
+
const endStart = file.indexOf(args.srcEnd, searchFrom);
|
|
1912
|
+
if (endStart !== -1) {
|
|
1913
|
+
ranges.push({ start, end: endStart + args.srcEnd.length });
|
|
96
1914
|
}
|
|
97
|
-
position = lf + 1;
|
|
98
1915
|
}
|
|
99
|
-
if (
|
|
100
|
-
fail("
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (line === targetAfterLine)
|
|
119
|
-
return { name: "targetAfter", lineStart, contentStart };
|
|
120
|
-
if (line === endLine)
|
|
121
|
-
return { name: "end", lineStart, contentStart };
|
|
122
|
-
return;
|
|
1916
|
+
if (ranges.length === 0) {
|
|
1917
|
+
fail("source_not_found", `Source delimiters were not found in ${args.src}`, {
|
|
1918
|
+
path: args.src,
|
|
1919
|
+
phase: "source",
|
|
1920
|
+
anchor: "src_start/src_end",
|
|
1921
|
+
matches: 0
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
if (ranges.length > 1) {
|
|
1925
|
+
fail("source_ambiguous", `Source delimiters are ambiguous in ${args.src}; matched ${ranges.length} locations`, {
|
|
1926
|
+
path: args.src,
|
|
1927
|
+
phase: "source",
|
|
1928
|
+
anchor: "src_start/src_end",
|
|
1929
|
+
matches: ranges.length,
|
|
1930
|
+
ranges: boundedRanges(ranges),
|
|
1931
|
+
line_ranges: boundedLineRanges(file, ranges)
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
return ranges[0];
|
|
123
1935
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
end -= 1;
|
|
128
|
-
if (end > marker.contentStart && input[end - 1] === 13) {
|
|
129
|
-
end -= 1;
|
|
130
|
-
}
|
|
1936
|
+
function targetAnchorName(args) {
|
|
1937
|
+
if (args.targetBefore.length > 0 && args.targetAfter.length > 0) {
|
|
1938
|
+
return "target_before+target_after";
|
|
131
1939
|
}
|
|
132
|
-
return
|
|
1940
|
+
return args.targetBefore.length > 0 ? "target_before" : "target_after";
|
|
133
1941
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
async function checkPatchFile(patchPath, options = {}) {
|
|
137
|
-
return runPatchFile(patchPath, { ...options, dryRun: true });
|
|
1942
|
+
function samePatchPath2(left, right) {
|
|
1943
|
+
return posix2.normalize(left) === posix2.normalize(right);
|
|
138
1944
|
}
|
|
139
|
-
async function
|
|
140
|
-
|
|
1945
|
+
async function selfCheckRenderedPatch(patch, files) {
|
|
1946
|
+
if (patch === undefined) {
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
checkPatchBytesInMemory(Buffer.from(patch, "utf8"), files);
|
|
141
1950
|
}
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return { changed: [changedPath] };
|
|
148
|
-
}
|
|
149
|
-
async function applyMovePatch(patch, cwd, dryRun) {
|
|
150
|
-
const absolutePath = resolve(cwd, patch.path);
|
|
151
|
-
const original = await readFile(absolutePath);
|
|
152
|
-
const selection = selectMove(original, patch);
|
|
153
|
-
const next = applyMove(original, selection);
|
|
154
|
-
if (!dryRun && !next.equals(original)) {
|
|
155
|
-
await writeAtomic(absolutePath, next);
|
|
156
|
-
}
|
|
157
|
-
return patch.path;
|
|
158
|
-
}
|
|
159
|
-
function selectMove(file, patch) {
|
|
160
|
-
const source = findSourceRange(file, patch);
|
|
161
|
-
const target = findTarget(file, patch);
|
|
162
|
-
if (rangesOverlap(source, target.range)) {
|
|
163
|
-
fail("target_overlaps_source", `Target anchor for ${patch.path} overlaps the source block`);
|
|
1951
|
+
function pairedSelfCheckFiles(srcLabel, dstLabel, srcOriginal, dstOriginal, sameFile) {
|
|
1952
|
+
const identity = sameFile ? "move-file" : undefined;
|
|
1953
|
+
const files = [{ path: srcLabel, bytes: srcOriginal, identity }];
|
|
1954
|
+
if (dstLabel !== srcLabel) {
|
|
1955
|
+
files.push({ path: dstLabel, bytes: dstOriginal, identity });
|
|
164
1956
|
}
|
|
165
|
-
return
|
|
166
|
-
source,
|
|
167
|
-
target,
|
|
168
|
-
payload: Buffer.from(file.subarray(source.start, source.end))
|
|
169
|
-
};
|
|
1957
|
+
return files;
|
|
170
1958
|
}
|
|
171
|
-
function
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
1959
|
+
function renderMovePatch(args, srcOriginal, dstOriginal, selection, sameFile, payloadSha256) {
|
|
1960
|
+
const id = "move-1";
|
|
1961
|
+
const { source, target, payload } = selection;
|
|
1962
|
+
const sourceBefore = adjacentLineBefore(srcOriginal, source.start);
|
|
1963
|
+
const sourceAfter = adjacentLineAfter(srcOriginal, source.end);
|
|
1964
|
+
const targetBefore = args.targetBefore.length === 0 ? adjacentLineBefore(dstOriginal, target.range.start) : args.targetBefore;
|
|
1965
|
+
const targetAfter = args.targetAfter.length === 0 ? adjacentLineAfter(dstOriginal, target.range.end) : args.targetAfter;
|
|
1966
|
+
const payloadLines = countLines(payload);
|
|
1967
|
+
const sourceHunkStart = source.start - sourceBefore.length;
|
|
1968
|
+
const sourceOldStart = lineNumberAt2(srcOriginal, sourceHunkStart);
|
|
1969
|
+
const sourceOldCount = countLines(sourceBefore) + payloadLines + countLines(sourceAfter);
|
|
1970
|
+
const sourceNewStart = sameFile && target.insertIndex <= sourceHunkStart ? sourceOldStart + payloadLines : sourceOldStart;
|
|
1971
|
+
const sourceNewCount = sourceOldCount - payloadLines;
|
|
1972
|
+
const targetHunkStart = target.insertIndex - targetBefore.length;
|
|
1973
|
+
const targetOldStart = lineNumberAt2(dstOriginal, targetHunkStart);
|
|
1974
|
+
const targetOldCount = countLines(targetBefore) + countLines(targetAfter);
|
|
1975
|
+
const targetNewStart = sameFile && source.end <= targetHunkStart ? targetOldStart - payloadLines : targetOldStart;
|
|
1976
|
+
const targetNewCount = targetOldCount + payloadLines;
|
|
1977
|
+
const sourceBody = joinPatchChunks([
|
|
1978
|
+
renderHunkBytes(sourceBefore, " "),
|
|
1979
|
+
renderHunkBytes(payload, "-"),
|
|
1980
|
+
renderHunkBytes(sourceAfter, " ")
|
|
175
1981
|
]);
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
withoutSource.subarray(targetIndex)
|
|
1982
|
+
const targetBody = joinPatchChunks([
|
|
1983
|
+
renderHunkBytes(targetBefore, " "),
|
|
1984
|
+
renderHunkBytes(payload, "+"),
|
|
1985
|
+
renderHunkBytes(targetAfter, " ")
|
|
181
1986
|
]);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
1987
|
+
const sourceHunkHeader = `@@ -${sourceOldStart},${sourceOldCount} +${sourceNewStart},${sourceNewCount} @@ blockpatch-source id=${id}`;
|
|
1988
|
+
const targetHunkHeader = `@@ -${targetOldStart},${targetOldCount} +${targetNewStart},${targetNewCount} @@ blockpatch-target id=${id}`;
|
|
1989
|
+
if (!sameFile) {
|
|
1990
|
+
return [
|
|
1991
|
+
`diff --blockpatch a/${args.src} b/${args.src}`,
|
|
1992
|
+
"blockpatch version 1",
|
|
1993
|
+
`blockpatch move id=${id} role=source payload-sha256=${payloadSha256}`,
|
|
1994
|
+
`--- a/${args.src}`,
|
|
1995
|
+
`+++ b/${args.src}`,
|
|
1996
|
+
"",
|
|
1997
|
+
sourceHunkHeader,
|
|
1998
|
+
sourceBody,
|
|
1999
|
+
"",
|
|
2000
|
+
`diff --blockpatch a/${args.dst} b/${args.dst}`,
|
|
2001
|
+
"blockpatch version 1",
|
|
2002
|
+
`blockpatch move id=${id} role=target payload-sha256=${payloadSha256}`,
|
|
2003
|
+
`--- a/${args.dst}`,
|
|
2004
|
+
`+++ b/${args.dst}`,
|
|
2005
|
+
"",
|
|
2006
|
+
targetHunkHeader,
|
|
2007
|
+
targetBody
|
|
2008
|
+
].join(`
|
|
2009
|
+
`) + `
|
|
2010
|
+
`;
|
|
199
2011
|
}
|
|
200
|
-
|
|
2012
|
+
return [
|
|
2013
|
+
`diff --blockpatch a/${args.src} b/${args.dst}`,
|
|
2014
|
+
"blockpatch version 1",
|
|
2015
|
+
`blockpatch move id=${id} payload-sha256=${payloadSha256}`,
|
|
2016
|
+
`--- a/${args.src}`,
|
|
2017
|
+
`+++ b/${args.dst}`,
|
|
2018
|
+
"",
|
|
2019
|
+
sourceHunkHeader,
|
|
2020
|
+
sourceBody,
|
|
2021
|
+
targetHunkHeader,
|
|
2022
|
+
targetBody
|
|
2023
|
+
].join(`
|
|
2024
|
+
`) + `
|
|
2025
|
+
`;
|
|
201
2026
|
}
|
|
202
|
-
function
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
2027
|
+
function renderInsertionPatch(args, dstOriginal, target, payloadSha256) {
|
|
2028
|
+
const id = "move-1";
|
|
2029
|
+
const targetBefore = args.targetBefore.length === 0 ? adjacentLineBefore(dstOriginal, target.range.start) : args.targetBefore;
|
|
2030
|
+
const targetAfter = args.targetAfter.length === 0 ? adjacentLineAfter(dstOriginal, target.range.end) : args.targetAfter;
|
|
2031
|
+
const payloadLines = countLines(args.payload);
|
|
2032
|
+
const targetHunkStart = target.insertIndex - targetBefore.length;
|
|
2033
|
+
const targetOldStart = lineNumberAt2(dstOriginal, targetHunkStart);
|
|
2034
|
+
const targetOldCount = countLines(targetBefore) + countLines(targetAfter);
|
|
2035
|
+
const targetNewCount = targetOldCount + payloadLines;
|
|
2036
|
+
const targetBody = joinPatchChunks([
|
|
2037
|
+
renderHunkBytes(targetBefore, " "),
|
|
2038
|
+
renderHunkBytes(args.payload, "+"),
|
|
2039
|
+
renderHunkBytes(targetAfter, " ")
|
|
2040
|
+
]);
|
|
2041
|
+
return [
|
|
2042
|
+
`diff --blockpatch a/${args.dst} b/${args.dst}`,
|
|
2043
|
+
"blockpatch version 1",
|
|
2044
|
+
`blockpatch move id=${id} payload-sha256=${payloadSha256}`,
|
|
2045
|
+
`--- a/${args.dst}`,
|
|
2046
|
+
`+++ b/${args.dst}`,
|
|
2047
|
+
"",
|
|
2048
|
+
`@@ -${targetOldStart},${targetOldCount} +${targetOldStart},${targetNewCount} @@ blockpatch-target id=${id}`,
|
|
2049
|
+
targetBody
|
|
2050
|
+
].join(`
|
|
2051
|
+
`) + `
|
|
2052
|
+
`;
|
|
2053
|
+
}
|
|
2054
|
+
function renderDeletionPatch(args, srcOriginal, source, payload, payloadSha256) {
|
|
2055
|
+
const id = "move-1";
|
|
2056
|
+
const sourceBefore = adjacentLineBefore(srcOriginal, source.start);
|
|
2057
|
+
const sourceAfter = adjacentLineAfter(srcOriginal, source.end);
|
|
2058
|
+
const payloadLines = countLines(payload);
|
|
2059
|
+
const sourceHunkStart = source.start - sourceBefore.length;
|
|
2060
|
+
const sourceOldStart = lineNumberAt2(srcOriginal, sourceHunkStart);
|
|
2061
|
+
const sourceOldCount = countLines(sourceBefore) + payloadLines + countLines(sourceAfter);
|
|
2062
|
+
const sourceNewCount = sourceOldCount - payloadLines;
|
|
2063
|
+
const sourceBody = joinPatchChunks([
|
|
2064
|
+
renderHunkBytes(sourceBefore, " "),
|
|
2065
|
+
renderHunkBytes(payload, "-"),
|
|
2066
|
+
renderHunkBytes(sourceAfter, " ")
|
|
2067
|
+
]);
|
|
2068
|
+
return [
|
|
2069
|
+
`diff --blockpatch a/${args.src} b/${args.src}`,
|
|
2070
|
+
"blockpatch version 1",
|
|
2071
|
+
`blockpatch move id=${id} payload-sha256=${payloadSha256}`,
|
|
2072
|
+
`--- a/${args.src}`,
|
|
2073
|
+
`+++ b/${args.src}`,
|
|
2074
|
+
"",
|
|
2075
|
+
`@@ -${sourceOldStart},${sourceOldCount} +${sourceOldStart},${sourceNewCount} @@ blockpatch-source id=${id}`,
|
|
2076
|
+
sourceBody
|
|
2077
|
+
].join(`
|
|
2078
|
+
`) + `
|
|
2079
|
+
`;
|
|
2080
|
+
}
|
|
2081
|
+
function lineNumberAt2(file, byteIndex) {
|
|
2082
|
+
let line = 1;
|
|
2083
|
+
for (let index = 0;index < byteIndex; index += 1) {
|
|
2084
|
+
if (file[index] === 10) {
|
|
2085
|
+
line += 1;
|
|
211
2086
|
}
|
|
212
2087
|
}
|
|
213
|
-
return
|
|
2088
|
+
return line;
|
|
214
2089
|
}
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
fail("target_not_found", `Target anchor was not found in ${patch.path}`);
|
|
2090
|
+
function countLines(bytes) {
|
|
2091
|
+
if (bytes.length === 0) {
|
|
2092
|
+
return 0;
|
|
219
2093
|
}
|
|
220
|
-
|
|
221
|
-
|
|
2094
|
+
let newlines = 0;
|
|
2095
|
+
for (const byte of bytes) {
|
|
2096
|
+
if (byte === 10) {
|
|
2097
|
+
newlines += 1;
|
|
2098
|
+
}
|
|
222
2099
|
}
|
|
223
|
-
|
|
224
|
-
const end = start + patch.target.anchor.length;
|
|
225
|
-
return {
|
|
226
|
-
range: { start, end },
|
|
227
|
-
insertIndex: patch.target.kind === "before" ? start : end
|
|
228
|
-
};
|
|
2100
|
+
return bytes[bytes.length - 1] === 10 ? newlines : newlines + 1;
|
|
229
2101
|
}
|
|
230
|
-
function
|
|
231
|
-
|
|
232
|
-
|
|
2102
|
+
function joinPatchChunks(chunks) {
|
|
2103
|
+
return chunks.filter((chunk) => chunk.length > 0).join(`
|
|
2104
|
+
`);
|
|
2105
|
+
}
|
|
2106
|
+
function adjacentLineBefore(file, index) {
|
|
2107
|
+
if (index === 0) {
|
|
2108
|
+
return Buffer.alloc(0);
|
|
233
2109
|
}
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
2110
|
+
const previousLf = file.lastIndexOf(10, index - 2);
|
|
2111
|
+
return Buffer.from(file.subarray(previousLf === -1 ? 0 : previousLf + 1, index));
|
|
2112
|
+
}
|
|
2113
|
+
function adjacentLineAfter(file, index) {
|
|
2114
|
+
if (index >= file.length) {
|
|
2115
|
+
return Buffer.alloc(0);
|
|
239
2116
|
}
|
|
240
|
-
|
|
2117
|
+
const nextLf = file.indexOf(10, index);
|
|
2118
|
+
return Buffer.from(file.subarray(index, nextLf === -1 ? file.length : nextLf + 1));
|
|
241
2119
|
}
|
|
242
|
-
function
|
|
243
|
-
|
|
2120
|
+
function renderHunkBytes(bytes, prefix) {
|
|
2121
|
+
const text2 = decodeUtf8(bytes);
|
|
2122
|
+
if (text2.length === 0) {
|
|
2123
|
+
return "";
|
|
2124
|
+
}
|
|
2125
|
+
const lines = text2.split(/(?<=\n)/);
|
|
2126
|
+
return lines.filter((line) => line.length > 0).map((line) => {
|
|
2127
|
+
if (line.endsWith(`
|
|
2128
|
+
`)) {
|
|
2129
|
+
return `${prefix}${line.slice(0, -1)}`;
|
|
2130
|
+
}
|
|
2131
|
+
return `${prefix}${line}
|
|
2132
|
+
\`;
|
|
2133
|
+
}).join(`
|
|
2134
|
+
`);
|
|
244
2135
|
}
|
|
245
|
-
|
|
246
|
-
const info = await stat(path);
|
|
247
|
-
const dir = dirname(path);
|
|
248
|
-
const base = basename(path);
|
|
249
|
-
const temp = join(dir, `.${base}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
2136
|
+
function decodeUtf8(bytes) {
|
|
250
2137
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return;
|
|
2138
|
+
return utf8Decoder.decode(bytes);
|
|
2139
|
+
} catch {
|
|
2140
|
+
fail("invalid_utf8", "move --diff cannot render invalid UTF-8 bytes", {
|
|
2141
|
+
phase: "render",
|
|
2142
|
+
anchor: "move --diff"
|
|
257
2143
|
});
|
|
258
|
-
throw error;
|
|
259
2144
|
}
|
|
260
2145
|
}
|
|
261
2146
|
|
|
262
2147
|
// src/cli.ts
|
|
2148
|
+
var packageJson = createRequire(import.meta.url)("../package.json");
|
|
263
2149
|
async function main(argv) {
|
|
264
2150
|
const options = parseArgs(argv);
|
|
265
2151
|
if (options.command === "help") {
|
|
@@ -267,84 +2153,418 @@ async function main(argv) {
|
|
|
267
2153
|
return 0;
|
|
268
2154
|
}
|
|
269
2155
|
if (options.command === "version") {
|
|
270
|
-
|
|
2156
|
+
writeSuccess(options, { version: packageJson.version });
|
|
2157
|
+
return 0;
|
|
2158
|
+
}
|
|
2159
|
+
if (options.command === "move" || options.command === "plan") {
|
|
2160
|
+
const args = await loadMoveArgs(options);
|
|
2161
|
+
const result2 = await moveBlock(args, {
|
|
2162
|
+
cwd: options.cwd,
|
|
2163
|
+
dryRun: options.dryRun ? true : undefined,
|
|
2164
|
+
diff: options.diff
|
|
2165
|
+
});
|
|
2166
|
+
if (options.diff && result2.patch !== undefined) {
|
|
2167
|
+
writeSuccess(options, result2, result2.patch);
|
|
2168
|
+
return 0;
|
|
2169
|
+
}
|
|
2170
|
+
writeChangeResult(options, result2, options.dryRun || args.dry_run === true ? "would change" : "changed");
|
|
271
2171
|
return 0;
|
|
272
2172
|
}
|
|
273
|
-
|
|
274
|
-
|
|
2173
|
+
const result = await runPatchCommand(options);
|
|
2174
|
+
const verb = options.command === "check" || options.dryRun ? "would change" : "changed";
|
|
2175
|
+
writeChangeResult(options, result, verb);
|
|
2176
|
+
return 0;
|
|
2177
|
+
}
|
|
2178
|
+
async function runPatchCommand(options) {
|
|
2179
|
+
const patchPath = options.patchPath ?? "-";
|
|
2180
|
+
if (options.command === "check") {
|
|
2181
|
+
return patchPath === "-" ? checkPatchBytes(await readStdin(), {
|
|
2182
|
+
cwd: options.cwd,
|
|
2183
|
+
reverse: options.reverse,
|
|
2184
|
+
stripComponents: options.stripComponents
|
|
2185
|
+
}) : checkPatchFile(patchPath, {
|
|
2186
|
+
cwd: options.cwd,
|
|
2187
|
+
reverse: options.reverse,
|
|
2188
|
+
stripComponents: options.stripComponents
|
|
2189
|
+
});
|
|
275
2190
|
}
|
|
276
|
-
|
|
2191
|
+
return patchPath === "-" ? applyPatchBytes(await readStdin(), {
|
|
277
2192
|
cwd: options.cwd,
|
|
278
|
-
dryRun: options.dryRun
|
|
2193
|
+
dryRun: options.dryRun,
|
|
2194
|
+
reverse: options.reverse,
|
|
2195
|
+
stripComponents: options.stripComponents
|
|
2196
|
+
}) : applyPatchFile(patchPath, {
|
|
2197
|
+
cwd: options.cwd,
|
|
2198
|
+
dryRun: options.dryRun,
|
|
2199
|
+
reverse: options.reverse,
|
|
2200
|
+
stripComponents: options.stripComponents
|
|
279
2201
|
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
2202
|
+
}
|
|
2203
|
+
async function loadMoveArgs(options) {
|
|
2204
|
+
if (options.moveArgs !== undefined) {
|
|
2205
|
+
return options.moveArgs;
|
|
2206
|
+
}
|
|
2207
|
+
if (options.moveJsonPath === undefined) {
|
|
2208
|
+
throw new BlockPatchError("missing_move_args", `${options.command} requires --json or --src flags`);
|
|
2209
|
+
}
|
|
2210
|
+
const jsonBytes = options.moveJsonPath === "-" ? await readStdin() : await readFileChecked(resolve3(options.cwd, options.moveJsonPath), "move JSON file");
|
|
2211
|
+
try {
|
|
2212
|
+
return JSON.parse(jsonBytes.toString("utf8"));
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2215
|
+
throw new BlockPatchError("invalid_json", `Invalid move JSON: ${message}`);
|
|
283
2216
|
}
|
|
284
|
-
return 0;
|
|
285
2217
|
}
|
|
286
2218
|
function parseArgs(argv) {
|
|
287
2219
|
const args = [...argv];
|
|
2220
|
+
const outputFlags = takeLeadingOutputFlags(args);
|
|
288
2221
|
const first = args.shift();
|
|
289
2222
|
if (first === undefined || first === "help" || first === "--help" || first === "-h") {
|
|
290
|
-
return
|
|
2223
|
+
return base("help", outputFlags.jsonOutput);
|
|
291
2224
|
}
|
|
292
2225
|
if (first === "version" || first === "--version" || first === "-v") {
|
|
293
|
-
|
|
2226
|
+
const options = base("version", outputFlags.jsonOutput);
|
|
2227
|
+
for (const arg of args) {
|
|
2228
|
+
if (arg === "--json-output" || arg === "--explain") {
|
|
2229
|
+
options.jsonOutput = true;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return options;
|
|
294
2233
|
}
|
|
295
|
-
if (first !== "apply" && first !== "check") {
|
|
2234
|
+
if (first !== "apply" && first !== "check" && first !== "move" && first !== "plan") {
|
|
296
2235
|
throw new BlockPatchError("unknown_command", `Unknown command: ${first}`);
|
|
297
2236
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
2237
|
+
if (first === "move" || first === "plan") {
|
|
2238
|
+
return parseMoveArgs(first, args, first === "plan" ? true : outputFlags.jsonOutput, outputFlags.explain);
|
|
2239
|
+
}
|
|
2240
|
+
return parsePatchArgs(first, args, outputFlags.jsonOutput, outputFlags.explain);
|
|
2241
|
+
}
|
|
2242
|
+
function parsePatchArgs(command, args, jsonOutput, explain) {
|
|
2243
|
+
const options = base(command, jsonOutput);
|
|
2244
|
+
if (explain && command === "apply") {
|
|
2245
|
+
options.dryRun = true;
|
|
2246
|
+
}
|
|
301
2247
|
while (args.length > 0) {
|
|
302
2248
|
const arg = args.shift();
|
|
2249
|
+
if (takeOutputFlag(options, arg)) {
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
303
2252
|
if (arg === "--dry-run") {
|
|
304
|
-
dryRun = true;
|
|
2253
|
+
options.dryRun = true;
|
|
305
2254
|
continue;
|
|
306
2255
|
}
|
|
307
|
-
if (arg === "--
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
cwd =
|
|
2256
|
+
if (arg === "--reverse" || arg === "-R") {
|
|
2257
|
+
options.reverse = true;
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
if (arg === "--cwd" || arg === "--directory" || arg === "-d") {
|
|
2261
|
+
options.cwd = requireValue(args, arg, true);
|
|
313
2262
|
continue;
|
|
314
2263
|
}
|
|
315
2264
|
if (arg?.startsWith("--cwd=")) {
|
|
316
|
-
cwd =
|
|
2265
|
+
options.cwd = resolve3(arg.slice("--cwd=".length));
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
if (arg?.startsWith("--directory=")) {
|
|
2269
|
+
options.cwd = resolve3(arg.slice("--directory=".length));
|
|
317
2270
|
continue;
|
|
318
2271
|
}
|
|
319
|
-
if (arg
|
|
2272
|
+
if (arg === "-i" || arg === "--input") {
|
|
2273
|
+
setPatchPath(options, requireValue(args, arg, false));
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
if (arg?.startsWith("--input=")) {
|
|
2277
|
+
setPatchPath(options, arg.slice("--input=".length));
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
const stripComponents = parseStripOption(arg, args);
|
|
2281
|
+
if (stripComponents !== undefined) {
|
|
2282
|
+
options.stripComponents = stripComponents;
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
if (arg?.startsWith("-") && arg !== "-") {
|
|
320
2286
|
throw new BlockPatchError("unknown_option", `Unknown option: ${arg}`);
|
|
321
2287
|
}
|
|
322
|
-
|
|
323
|
-
|
|
2288
|
+
setPatchPath(options, arg ?? "");
|
|
2289
|
+
}
|
|
2290
|
+
if (command === "check" && options.dryRun) {
|
|
2291
|
+
throw new BlockPatchError("invalid_option", "--dry-run is only valid with apply or move");
|
|
2292
|
+
}
|
|
2293
|
+
return options;
|
|
2294
|
+
}
|
|
2295
|
+
function parseMoveArgs(command, args, jsonOutput, explain) {
|
|
2296
|
+
const options = base(command, jsonOutput);
|
|
2297
|
+
if (command === "plan") {
|
|
2298
|
+
options.dryRun = true;
|
|
2299
|
+
options.diff = true;
|
|
2300
|
+
options.jsonOutput = true;
|
|
2301
|
+
}
|
|
2302
|
+
if (explain) {
|
|
2303
|
+
options.dryRun = true;
|
|
2304
|
+
}
|
|
2305
|
+
const moveArgs = {};
|
|
2306
|
+
let sawFlagArgs = false;
|
|
2307
|
+
while (args.length > 0) {
|
|
2308
|
+
const arg = args.shift();
|
|
2309
|
+
if (takeOutputFlag(options, arg)) {
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
if (arg === "--dry-run") {
|
|
2313
|
+
options.dryRun = true;
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
if (arg === "--diff") {
|
|
2317
|
+
options.diff = true;
|
|
2318
|
+
continue;
|
|
2319
|
+
}
|
|
2320
|
+
if (arg === "--cwd" || arg === "--directory" || arg === "-d") {
|
|
2321
|
+
options.cwd = requireValue(args, arg, true);
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
if (arg?.startsWith("--cwd=")) {
|
|
2325
|
+
options.cwd = resolve3(arg.slice("--cwd=".length));
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (arg?.startsWith("--directory=")) {
|
|
2329
|
+
options.cwd = resolve3(arg.slice("--directory=".length));
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (arg === "--json") {
|
|
2333
|
+
options.moveJsonPath = requireValue(args, "--json", false);
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
if (arg?.startsWith("--json=")) {
|
|
2337
|
+
options.moveJsonPath = arg.slice("--json=".length);
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
const key = argToMoveKey(arg);
|
|
2341
|
+
if (key !== undefined) {
|
|
2342
|
+
moveArgs[key] = requireValue(args, arg ?? "", false);
|
|
2343
|
+
sawFlagArgs = true;
|
|
2344
|
+
continue;
|
|
2345
|
+
}
|
|
2346
|
+
throw new BlockPatchError("unknown_option", `Unknown option: ${String(arg)}`);
|
|
2347
|
+
}
|
|
2348
|
+
if (options.moveJsonPath !== undefined && sawFlagArgs) {
|
|
2349
|
+
throw new BlockPatchError("invalid_option", "move cannot combine --json with --src flags");
|
|
2350
|
+
}
|
|
2351
|
+
if (sawFlagArgs) {
|
|
2352
|
+
options.moveArgs = moveArgs;
|
|
2353
|
+
}
|
|
2354
|
+
return options;
|
|
2355
|
+
}
|
|
2356
|
+
function argToMoveKey(arg) {
|
|
2357
|
+
switch (arg) {
|
|
2358
|
+
case "--src":
|
|
2359
|
+
return "src";
|
|
2360
|
+
case "--src-start":
|
|
2361
|
+
return "src_start";
|
|
2362
|
+
case "--src-end":
|
|
2363
|
+
return "src_end";
|
|
2364
|
+
case "--dst":
|
|
2365
|
+
return "dst";
|
|
2366
|
+
case "--payload":
|
|
2367
|
+
return "payload";
|
|
2368
|
+
case "--target-before":
|
|
2369
|
+
return "target_before";
|
|
2370
|
+
case "--target-after":
|
|
2371
|
+
return "target_after";
|
|
2372
|
+
case "--expected-payload-sha256":
|
|
2373
|
+
return "expected_payload_sha256";
|
|
2374
|
+
default:
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
function base(command, jsonOutput) {
|
|
2379
|
+
return {
|
|
2380
|
+
command,
|
|
2381
|
+
cwd: process.cwd(),
|
|
2382
|
+
dryRun: false,
|
|
2383
|
+
diff: false,
|
|
2384
|
+
jsonOutput,
|
|
2385
|
+
reverse: false,
|
|
2386
|
+
stripComponents: 1
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
function setPatchPath(options, path) {
|
|
2390
|
+
if (options.patchPath !== undefined) {
|
|
2391
|
+
throw new BlockPatchError("too_many_args", `Unexpected argument: ${path}`);
|
|
2392
|
+
}
|
|
2393
|
+
options.patchPath = path;
|
|
2394
|
+
}
|
|
2395
|
+
function parseStripOption(arg, args) {
|
|
2396
|
+
if (arg === "-p" || arg === "--strip") {
|
|
2397
|
+
return parseStripComponents(requireValue(args, arg, false), arg);
|
|
2398
|
+
}
|
|
2399
|
+
if (arg?.startsWith("-p") === true && arg.length > 2) {
|
|
2400
|
+
return parseStripComponents(arg.slice(2), "-p");
|
|
2401
|
+
}
|
|
2402
|
+
if (arg?.startsWith("--strip=") === true) {
|
|
2403
|
+
return parseStripComponents(arg.slice("--strip=".length), "--strip");
|
|
2404
|
+
}
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
function parseStripComponents(value, option) {
|
|
2408
|
+
if (!/^\d+$/.test(value)) {
|
|
2409
|
+
throw new BlockPatchError("invalid_option", `Invalid strip count for ${option}: ${value}`);
|
|
2410
|
+
}
|
|
2411
|
+
const parsed = Number(value);
|
|
2412
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
2413
|
+
throw new BlockPatchError("invalid_option", `Invalid strip count for ${option}: ${value}`);
|
|
2414
|
+
}
|
|
2415
|
+
return parsed;
|
|
2416
|
+
}
|
|
2417
|
+
function takeLeadingOutputFlags(args) {
|
|
2418
|
+
let jsonOutput = false;
|
|
2419
|
+
let explain = false;
|
|
2420
|
+
while (args[0] === "--json-output" || args[0] === "--explain") {
|
|
2421
|
+
const arg = args.shift();
|
|
2422
|
+
jsonOutput = true;
|
|
2423
|
+
explain = explain || arg === "--explain";
|
|
2424
|
+
}
|
|
2425
|
+
return { jsonOutput, explain };
|
|
2426
|
+
}
|
|
2427
|
+
function takeOutputFlag(options, arg) {
|
|
2428
|
+
if (arg === "--json-output") {
|
|
2429
|
+
options.jsonOutput = true;
|
|
2430
|
+
return true;
|
|
2431
|
+
}
|
|
2432
|
+
if (arg === "--explain") {
|
|
2433
|
+
options.jsonOutput = true;
|
|
2434
|
+
if (options.command === "apply" || options.command === "move" || options.command === "plan") {
|
|
2435
|
+
options.dryRun = true;
|
|
2436
|
+
}
|
|
2437
|
+
return true;
|
|
2438
|
+
}
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2441
|
+
function requireValue(args, option, pathValue) {
|
|
2442
|
+
const value = args.shift();
|
|
2443
|
+
if (value === undefined) {
|
|
2444
|
+
throw new BlockPatchError("missing_option_value", `Missing value for ${option}`);
|
|
2445
|
+
}
|
|
2446
|
+
return pathValue ? resolve3(value) : value;
|
|
2447
|
+
}
|
|
2448
|
+
async function readStdin() {
|
|
2449
|
+
const chunks = [];
|
|
2450
|
+
for await (const chunk of process.stdin) {
|
|
2451
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2452
|
+
}
|
|
2453
|
+
return Buffer.concat(chunks);
|
|
2454
|
+
}
|
|
2455
|
+
function writeChangeResult(options, result, verb) {
|
|
2456
|
+
if (options.jsonOutput) {
|
|
2457
|
+
writeSuccess(options, result);
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
for (const path of result.changed) {
|
|
2461
|
+
console.log(`${verb} ${path}`);
|
|
2462
|
+
}
|
|
2463
|
+
if (result.changed.length === 0) {
|
|
2464
|
+
for (const path of result.affected) {
|
|
2465
|
+
console.log(`unchanged ${path}`);
|
|
324
2466
|
}
|
|
325
|
-
patchPath = arg;
|
|
326
2467
|
}
|
|
327
|
-
|
|
328
|
-
|
|
2468
|
+
}
|
|
2469
|
+
function writeSuccess(options, result, plainText) {
|
|
2470
|
+
if (options.jsonOutput) {
|
|
2471
|
+
console.log(JSON.stringify({ ok: true, ...objectResult(result), ...jsonSuccessMetadata(options) }));
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
if (plainText !== undefined) {
|
|
2475
|
+
console.log(plainText);
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
if (typeof result === "object" && result !== null && "version" in result) {
|
|
2479
|
+
console.log(`blockpatch ${result.version}`);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
function objectResult(result) {
|
|
2483
|
+
return typeof result === "object" && result !== null ? result : {};
|
|
2484
|
+
}
|
|
2485
|
+
function jsonSuccessMetadata(options) {
|
|
2486
|
+
if (options.command === "apply" || options.command === "check") {
|
|
2487
|
+
return { strip_components: options.stripComponents };
|
|
329
2488
|
}
|
|
330
|
-
return {
|
|
2489
|
+
return {};
|
|
331
2490
|
}
|
|
332
2491
|
function printHelp() {
|
|
333
2492
|
console.log(`blockpatch
|
|
334
2493
|
|
|
335
2494
|
Usage:
|
|
336
|
-
blockpatch check
|
|
337
|
-
blockpatch apply <patch.blockpatch> [
|
|
2495
|
+
blockpatch check [patch.blockpatch|-] [-d <dir>] [-pN] [-R|--reverse] [--json-output|--explain]
|
|
2496
|
+
blockpatch apply [patch.blockpatch|-] [-i <patch.blockpatch>] [-d <dir>] [-pN] [-R|--reverse] [--dry-run] [--json-output|--explain]
|
|
2497
|
+
blockpatch plan --json <path.json|-> [--cwd <dir>]
|
|
2498
|
+
blockpatch move --json <path.json|-> [--cwd <dir>] [--dry-run] [--diff] [--json-output|--explain]
|
|
2499
|
+
blockpatch move --src <path> --src-start <text> --src-end <text> --dst <path> --target-before <text> --target-after <text> [--expected-payload-sha256 <sha256>]
|
|
2500
|
+
blockpatch move --src /dev/null --dst <path> --payload <text> --target-before <text>
|
|
2501
|
+
blockpatch move --src <path> --src-start <text> --src-end <text> --dst /dev/null
|
|
338
2502
|
blockpatch version
|
|
339
2503
|
`);
|
|
340
2504
|
}
|
|
341
2505
|
main(process.argv.slice(2)).then((code) => {
|
|
342
2506
|
process.exitCode = code;
|
|
343
2507
|
}, (error) => {
|
|
2508
|
+
const jsonOutput = hasJsonOutputFlag(process.argv.slice(2));
|
|
344
2509
|
if (error instanceof BlockPatchError) {
|
|
345
|
-
|
|
2510
|
+
writeError(error.code, error.message, jsonOutput, error.details);
|
|
346
2511
|
process.exitCode = 1;
|
|
347
2512
|
return;
|
|
348
2513
|
}
|
|
349
|
-
|
|
2514
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2515
|
+
writeError("unexpected_error", message, jsonOutput);
|
|
2516
|
+
process.exitCode = 1;
|
|
350
2517
|
});
|
|
2518
|
+
function hasJsonOutputFlag(argv) {
|
|
2519
|
+
const args = [...argv];
|
|
2520
|
+
const outputFlags = takeLeadingOutputFlags(args);
|
|
2521
|
+
if (outputFlags.jsonOutput) {
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2524
|
+
const command = args.shift();
|
|
2525
|
+
if (command === "apply" || command === "check") {
|
|
2526
|
+
return hasPatchJsonOutputFlag(args);
|
|
2527
|
+
}
|
|
2528
|
+
if (command === "move") {
|
|
2529
|
+
return hasMoveJsonOutputFlag(args);
|
|
2530
|
+
}
|
|
2531
|
+
if (command === "plan") {
|
|
2532
|
+
return true;
|
|
2533
|
+
}
|
|
2534
|
+
return args.includes("--json-output") || args.includes("--explain");
|
|
2535
|
+
}
|
|
2536
|
+
function hasPatchJsonOutputFlag(args) {
|
|
2537
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
2538
|
+
const arg = args[index];
|
|
2539
|
+
if (arg === "--json-output" || arg === "--explain") {
|
|
2540
|
+
return true;
|
|
2541
|
+
}
|
|
2542
|
+
if (arg === "--cwd" || arg === "--directory" || arg === "-d" || arg === "-i" || arg === "--input") {
|
|
2543
|
+
index += 1;
|
|
2544
|
+
continue;
|
|
2545
|
+
}
|
|
2546
|
+
if (arg === "-p" || arg === "--strip") {
|
|
2547
|
+
index += 1;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return false;
|
|
2551
|
+
}
|
|
2552
|
+
function hasMoveJsonOutputFlag(args) {
|
|
2553
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
2554
|
+
const arg = args[index];
|
|
2555
|
+
if (arg === "--json-output" || arg === "--explain") {
|
|
2556
|
+
return true;
|
|
2557
|
+
}
|
|
2558
|
+
if (arg === "--cwd" || arg === "--directory" || arg === "-d" || arg === "--json" || argToMoveKey(arg) !== undefined) {
|
|
2559
|
+
index += 1;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return false;
|
|
2563
|
+
}
|
|
2564
|
+
function writeError(code, message, jsonOutput, details = {}) {
|
|
2565
|
+
if (jsonOutput) {
|
|
2566
|
+
console.error(JSON.stringify({ ok: false, error: { code, message, ...details } }));
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
console.error(`blockpatch: ${message}`);
|
|
2570
|
+
}
|