opencode-feature-factory 0.2.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/LICENSE +21 -0
- package/README.md +877 -0
- package/assets/agent/backend-builder.md +73 -0
- package/assets/agent/codebase-researcher.md +56 -0
- package/assets/agent/design-interpreter.md +37 -0
- package/assets/agent/frontend-builder.md +74 -0
- package/assets/agent/implementation-validator.md +73 -0
- package/assets/agent/security-reviewer.md +60 -0
- package/assets/agent/spec-writer.md +94 -0
- package/assets/agent/story-reader.md +38 -0
- package/assets/agent/story-writer.md +39 -0
- package/assets/agent/test-verifier.md +73 -0
- package/assets/agent/work-decomposer.md +102 -0
- package/assets/agent/work-reviewer.md +77 -0
- package/assets/command/feature.md +68 -0
- package/assets/skills/feature/SCHEMA.md +990 -0
- package/assets/skills/feature/SKILL.md +620 -0
- package/dist/tui.js +3641 -0
- package/package.json +65 -0
- package/src/cleanup-sweep-command.js +75 -0
- package/src/cleanup-sweep-eligibility.js +581 -0
- package/src/cleanup-sweep-output.js +139 -0
- package/src/cleanup-sweep-report.js +548 -0
- package/src/cleanup-sweep.js +546 -0
- package/src/cli-output.js +251 -0
- package/src/cli.js +1152 -0
- package/src/config.js +231 -0
- package/src/cost-attribution.js +327 -0
- package/src/cost-report.js +185 -0
- package/src/detached-log-supervisor.js +178 -0
- package/src/doctor.js +598 -0
- package/src/env-snapshot.js +211 -0
- package/src/factory-diagnostics.js +429 -0
- package/src/factory-paths.js +40 -0
- package/src/factory.js +4769 -0
- package/src/feature-command-payload.js +378 -0
- package/src/git.js +110 -0
- package/src/github.js +252 -0
- package/src/hardening/atomic-write.js +954 -0
- package/src/hardening/line-output.js +139 -0
- package/src/hardening/output-policy.js +365 -0
- package/src/hardening/process-verification.js +542 -0
- package/src/hardening/sensitive-data.js +341 -0
- package/src/hardening/terminal-encoding.js +224 -0
- package/src/plugin.js +246 -0
- package/src/post-pr-ci.js +754 -0
- package/src/process-evidence.js +1139 -0
- package/src/refs.js +144 -0
- package/src/run-state.js +2411 -0
- package/src/steering-conflicts.js +77 -0
- package/src/telemetry.js +419 -0
- package/src/tui-data.js +499 -0
- package/src/tui-rendering.js +148 -0
- package/src/utils.js +35 -0
- package/src/validate.js +1655 -0
- package/src/worktrees.js +56 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants as FS_CONSTANTS,
|
|
5
|
+
fstatSync,
|
|
6
|
+
linkSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { promises as fsPromises } from "node:fs";
|
|
16
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
17
|
+
|
|
18
|
+
const MAX_TEMP_ATTEMPTS = 8;
|
|
19
|
+
const FILE_TYPE_MASK = 0o170000;
|
|
20
|
+
|
|
21
|
+
const ERROR_MESSAGES = Object.freeze({
|
|
22
|
+
INVALID_ROOT: "protected file root is invalid",
|
|
23
|
+
UNTRUSTED_ROOT: "protected file root is not trusted",
|
|
24
|
+
INVALID_PATH: "protected relative path is invalid",
|
|
25
|
+
INVALID_OPTIONS: "protected file options are invalid",
|
|
26
|
+
PARENT_MISSING: "protected file parent is missing",
|
|
27
|
+
PARENT_TYPE: "protected file parent has an unsafe type",
|
|
28
|
+
PARENT_CHANGED: "protected file parent changed during the operation",
|
|
29
|
+
TARGET_TYPE: "protected file target has an unsafe type",
|
|
30
|
+
TARGET_CHANGED: "protected file target changed during the operation",
|
|
31
|
+
TARGET_EXISTS: "protected file target already exists",
|
|
32
|
+
TEMP_COLLISION: "protected temporary file name collisions exceeded the limit",
|
|
33
|
+
TEMP_OPEN_FAILED: "protected temporary file could not be opened",
|
|
34
|
+
TEMP_TYPE: "protected temporary file has an unsafe type",
|
|
35
|
+
TEMP_CHANGED: "protected temporary file changed during the operation",
|
|
36
|
+
WRITE_FAILED: "protected file write failed",
|
|
37
|
+
CLOSE_FAILED: "protected file close failed",
|
|
38
|
+
COMMIT_FAILED: "protected file commit failed",
|
|
39
|
+
CROSS_DEVICE: "protected file commit crossed filesystem devices",
|
|
40
|
+
LINK_UNSUPPORTED: "protected create-only publication is unsupported",
|
|
41
|
+
CLEANUP_INDETERMINATE: "protected temporary file cleanup is indeterminate",
|
|
42
|
+
DIRECTORY_CREATE_FAILED: "protected directory could not be created",
|
|
43
|
+
APPEND_UNSUPPORTED: "protected no-follow append is unsupported",
|
|
44
|
+
APPEND_OPEN_FAILED: "protected append file could not be opened",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export class ProtectedWriteError extends Error {
|
|
48
|
+
constructor(code, cause) {
|
|
49
|
+
super(ERROR_MESSAGES[code] || "protected file operation failed", cause === undefined ? undefined : { cause });
|
|
50
|
+
this.name = "ProtectedWriteError";
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// These checks bound portable pathname operations on supported local filesystems.
|
|
56
|
+
// They cannot close the race between the final identity check and rename/link/unlink.
|
|
57
|
+
export function writeProtectedFileAtomicSync(rootDir, relativePath, data, options = {}) {
|
|
58
|
+
const ops = syncOps(options.fsOps);
|
|
59
|
+
const settings = normalizeWriteOptions(options);
|
|
60
|
+
const bytes = normalizeData(data);
|
|
61
|
+
const context = prepareWriteSync(rootDir, relativePath, settings, ops);
|
|
62
|
+
let temp = null;
|
|
63
|
+
let descriptor = null;
|
|
64
|
+
let primaryError = null;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
invokeSyncHook(settings.hooks.afterParentCheck);
|
|
68
|
+
recheckParentsSync(context, ops);
|
|
69
|
+
|
|
70
|
+
context.target = inspectTargetSync(context.targetPath, settings.commit, ops);
|
|
71
|
+
invokeSyncHook(settings.hooks.afterTargetCheck);
|
|
72
|
+
recheckWriteStateSync(context, null, ops);
|
|
73
|
+
|
|
74
|
+
temp = createTempSync(context, settings, ops);
|
|
75
|
+
descriptor = temp.handle;
|
|
76
|
+
invokeSyncHook(settings.hooks.afterTempOpen);
|
|
77
|
+
recheckWriteStateSync(context, temp, ops, descriptor);
|
|
78
|
+
|
|
79
|
+
writeAllSync(descriptor, bytes, ops);
|
|
80
|
+
invokeSyncHook(settings.hooks.afterTempWrite);
|
|
81
|
+
recheckWriteStateSync(context, temp, ops, descriptor);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
ops.close(descriptor);
|
|
85
|
+
descriptor = null;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
descriptor = null;
|
|
88
|
+
throw protectedError("CLOSE_FAILED", error);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
invokeSyncHook(settings.hooks.afterTempClose);
|
|
92
|
+
recheckWriteStateSync(context, temp, ops);
|
|
93
|
+
invokeSyncHook(settings.hooks.beforeCommit);
|
|
94
|
+
recheckWriteStateSync(context, temp, ops);
|
|
95
|
+
|
|
96
|
+
if (settings.commit === "create-only") {
|
|
97
|
+
commitCreateOnlySync(temp.path, context.targetPath, ops);
|
|
98
|
+
const cleanup = cleanupAfterPublishedSync(context, temp, settings, ops);
|
|
99
|
+
const actualMode = temp.mode;
|
|
100
|
+
temp = null;
|
|
101
|
+
return resultFor(context, settings, cleanup, actualMode);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
commitReplaceSync(temp.path, context.targetPath, ops);
|
|
105
|
+
const actualMode = temp.mode;
|
|
106
|
+
temp = null;
|
|
107
|
+
return resultFor(context, settings, "none", actualMode);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
primaryError = asProtectedError(error, "COMMIT_FAILED");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (descriptor !== null) {
|
|
113
|
+
try {
|
|
114
|
+
ops.close(descriptor);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (!primaryError) primaryError = protectedError("CLOSE_FAILED", error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (temp) {
|
|
121
|
+
const cleanupError = cleanupFailedTempSync(context, temp, settings, ops);
|
|
122
|
+
if (cleanupError) throw cleanupError;
|
|
123
|
+
}
|
|
124
|
+
throw primaryError;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function writeProtectedFileAtomic(rootDir, relativePath, data, options = {}) {
|
|
128
|
+
const ops = asyncOps(options.fsOps);
|
|
129
|
+
const settings = normalizeWriteOptions(options);
|
|
130
|
+
const bytes = normalizeData(data);
|
|
131
|
+
const context = await prepareWrite(rootDir, relativePath, settings, ops);
|
|
132
|
+
let temp = null;
|
|
133
|
+
let descriptor = null;
|
|
134
|
+
let primaryError = null;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await invokeHook(settings.hooks.afterParentCheck);
|
|
138
|
+
await recheckParents(context, ops);
|
|
139
|
+
|
|
140
|
+
context.target = await inspectTarget(context.targetPath, settings.commit, ops);
|
|
141
|
+
await invokeHook(settings.hooks.afterTargetCheck);
|
|
142
|
+
await recheckWriteState(context, null, ops);
|
|
143
|
+
|
|
144
|
+
temp = await createTemp(context, settings, ops);
|
|
145
|
+
descriptor = temp.handle;
|
|
146
|
+
await invokeHook(settings.hooks.afterTempOpen);
|
|
147
|
+
await recheckWriteState(context, temp, ops, descriptor);
|
|
148
|
+
|
|
149
|
+
await writeAll(descriptor, bytes, ops);
|
|
150
|
+
await invokeHook(settings.hooks.afterTempWrite);
|
|
151
|
+
await recheckWriteState(context, temp, ops, descriptor);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await ops.close(descriptor);
|
|
155
|
+
descriptor = null;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
descriptor = null;
|
|
158
|
+
throw protectedError("CLOSE_FAILED", error);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await invokeHook(settings.hooks.afterTempClose);
|
|
162
|
+
await recheckWriteState(context, temp, ops);
|
|
163
|
+
await invokeHook(settings.hooks.beforeCommit);
|
|
164
|
+
await recheckWriteState(context, temp, ops);
|
|
165
|
+
|
|
166
|
+
if (settings.commit === "create-only") {
|
|
167
|
+
await commitCreateOnly(temp.path, context.targetPath, ops);
|
|
168
|
+
const cleanup = await cleanupAfterPublished(context, temp, settings, ops);
|
|
169
|
+
const actualMode = temp.mode;
|
|
170
|
+
temp = null;
|
|
171
|
+
return resultFor(context, settings, cleanup, actualMode);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await commitReplace(temp.path, context.targetPath, ops);
|
|
175
|
+
const actualMode = temp.mode;
|
|
176
|
+
temp = null;
|
|
177
|
+
return resultFor(context, settings, "none", actualMode);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
primaryError = asProtectedError(error, "COMMIT_FAILED");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (descriptor !== null) {
|
|
183
|
+
try {
|
|
184
|
+
await ops.close(descriptor);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (!primaryError) primaryError = protectedError("CLOSE_FAILED", error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (temp) {
|
|
191
|
+
const cleanupError = await cleanupFailedTemp(context, temp, settings, ops);
|
|
192
|
+
if (cleanupError) throw cleanupError;
|
|
193
|
+
}
|
|
194
|
+
throw primaryError;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function writeProtectedJsonAtomicSync(rootDir, relativePath, value, options = {}) {
|
|
198
|
+
return writeProtectedFileAtomicSync(rootDir, relativePath, serializeJson(value, options), options);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function writeProtectedJsonAtomic(rootDir, relativePath, value, options = {}) {
|
|
202
|
+
return writeProtectedFileAtomic(rootDir, relativePath, serializeJson(value, options), options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function ensureProtectedDirectoryTreeSync(rootDir, relativePath, options = {}) {
|
|
206
|
+
const ops = syncOps(options.fsOps);
|
|
207
|
+
const root = inspectRootSync(rootDir, ops);
|
|
208
|
+
const segments = normalizeRelativePath(relativePath, { allowEmpty: true });
|
|
209
|
+
const mode = normalizeMode(options.mode, 0o777);
|
|
210
|
+
const snapshots = [root];
|
|
211
|
+
|
|
212
|
+
for (const segment of segments) {
|
|
213
|
+
const path = join(snapshots[snapshots.length - 1].path, segment);
|
|
214
|
+
let entry = lstatOptionalSync(path, ops, "PARENT_TYPE");
|
|
215
|
+
if (!entry) {
|
|
216
|
+
try {
|
|
217
|
+
ops.mkdir(path, mode);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error?.code !== "EEXIST") throw protectedError("DIRECTORY_CREATE_FAILED", error);
|
|
220
|
+
}
|
|
221
|
+
entry = lstatRequiredSync(path, ops, "PARENT_TYPE");
|
|
222
|
+
}
|
|
223
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw protectedError("PARENT_TYPE");
|
|
224
|
+
assertContained(root.path, path, "PARENT_CHANGED");
|
|
225
|
+
snapshots.push(snapshot(path, entry));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
invokeSyncHook(normalizeHooks(options.hooks).afterParentCheck);
|
|
229
|
+
recheckSnapshotListSync(snapshots, ops);
|
|
230
|
+
return snapshots[snapshots.length - 1].path;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function ensureProtectedDirectoryTree(rootDir, relativePath, options = {}) {
|
|
234
|
+
const ops = asyncOps(options.fsOps);
|
|
235
|
+
const root = await inspectRoot(rootDir, ops);
|
|
236
|
+
const segments = normalizeRelativePath(relativePath, { allowEmpty: true });
|
|
237
|
+
const mode = normalizeMode(options.mode, 0o777);
|
|
238
|
+
const snapshots = [root];
|
|
239
|
+
|
|
240
|
+
for (const segment of segments) {
|
|
241
|
+
const path = join(snapshots[snapshots.length - 1].path, segment);
|
|
242
|
+
let entry = await lstatOptional(path, ops, "PARENT_TYPE");
|
|
243
|
+
if (!entry) {
|
|
244
|
+
try {
|
|
245
|
+
await ops.mkdir(path, mode);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error?.code !== "EEXIST") throw protectedError("DIRECTORY_CREATE_FAILED", error);
|
|
248
|
+
}
|
|
249
|
+
entry = await lstatRequired(path, ops, "PARENT_TYPE");
|
|
250
|
+
}
|
|
251
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw protectedError("PARENT_TYPE");
|
|
252
|
+
assertContained(root.path, path, "PARENT_CHANGED");
|
|
253
|
+
snapshots.push(snapshot(path, entry));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await invokeHook(normalizeHooks(options.hooks).afterParentCheck);
|
|
257
|
+
await recheckSnapshotList(snapshots, ops);
|
|
258
|
+
return snapshots[snapshots.length - 1].path;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function openProtectedAppendFileSync(rootDir, relativePath, options = {}) {
|
|
262
|
+
if (typeof FS_CONSTANTS.O_NOFOLLOW !== "number") throw protectedError("APPEND_UNSUPPORTED");
|
|
263
|
+
const ops = syncOps(options.fsOps);
|
|
264
|
+
const mode = normalizeMode(options.mode, 0o666);
|
|
265
|
+
const hooks = normalizeHooks(options.hooks);
|
|
266
|
+
const context = preparePathSync(rootDir, relativePath, ops);
|
|
267
|
+
|
|
268
|
+
invokeSyncHook(hooks.afterParentCheck);
|
|
269
|
+
recheckParentsSync(context, ops);
|
|
270
|
+
const initial = inspectAppendTargetSync(context.targetPath, ops);
|
|
271
|
+
invokeSyncHook(hooks.afterTargetCheck);
|
|
272
|
+
recheckParentsSync(context, ops);
|
|
273
|
+
recheckAppendTargetSync(context.targetPath, initial, ops);
|
|
274
|
+
|
|
275
|
+
let descriptor;
|
|
276
|
+
try {
|
|
277
|
+
descriptor = ops.open(
|
|
278
|
+
context.targetPath,
|
|
279
|
+
FS_CONSTANTS.O_WRONLY | FS_CONSTANTS.O_APPEND | FS_CONSTANTS.O_CREAT | FS_CONSTANTS.O_NOFOLLOW,
|
|
280
|
+
mode,
|
|
281
|
+
);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
throw protectedError("APPEND_OPEN_FAILED", error);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let opened = null;
|
|
287
|
+
try {
|
|
288
|
+
opened = ops.fstat(descriptor);
|
|
289
|
+
if (!opened.isFile() || opened.nlink !== 1) throw protectedError("TARGET_TYPE");
|
|
290
|
+
invokeSyncHook(hooks.afterTempOpen);
|
|
291
|
+
recheckParentsSync(context, ops);
|
|
292
|
+
const reopened = ops.fstat(descriptor);
|
|
293
|
+
const current = lstatRequiredSync(context.targetPath, ops, "TARGET_CHANGED");
|
|
294
|
+
if (
|
|
295
|
+
!reopened.isFile()
|
|
296
|
+
|| reopened.nlink !== 1
|
|
297
|
+
|| !current.isFile()
|
|
298
|
+
|| current.isSymbolicLink()
|
|
299
|
+
|| current.nlink !== 1
|
|
300
|
+
|| !sameIdentity(reopened, opened)
|
|
301
|
+
|| !sameIdentity(current, reopened)
|
|
302
|
+
) {
|
|
303
|
+
throw protectedError("TARGET_CHANGED");
|
|
304
|
+
}
|
|
305
|
+
if (initial && !sameIdentity(initial.entry, reopened)) throw protectedError("TARGET_CHANGED");
|
|
306
|
+
return descriptor;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
try {
|
|
309
|
+
ops.close(descriptor);
|
|
310
|
+
} catch {
|
|
311
|
+
// The validation error remains authoritative; no write was attempted.
|
|
312
|
+
}
|
|
313
|
+
throw asProtectedError(error, "TARGET_CHANGED");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function prepareWriteSync(rootDir, relativePath, settings, ops) {
|
|
318
|
+
const context = preparePathSync(rootDir, relativePath, ops);
|
|
319
|
+
context.settings = settings;
|
|
320
|
+
return context;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function prepareWrite(rootDir, relativePath, settings, ops) {
|
|
324
|
+
const context = await preparePath(rootDir, relativePath, ops);
|
|
325
|
+
context.settings = settings;
|
|
326
|
+
return context;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function preparePathSync(rootDir, relativePath, ops) {
|
|
330
|
+
const root = inspectRootSync(rootDir, ops);
|
|
331
|
+
const segments = normalizeRelativePath(relativePath);
|
|
332
|
+
const parentSegments = segments.slice(0, -1);
|
|
333
|
+
const parents = [root];
|
|
334
|
+
let current = root.path;
|
|
335
|
+
for (const segment of parentSegments) {
|
|
336
|
+
current = join(current, segment);
|
|
337
|
+
const entry = lstatOptionalSync(current, ops, "PARENT_TYPE");
|
|
338
|
+
if (!entry) throw protectedError("PARENT_MISSING");
|
|
339
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw protectedError("PARENT_TYPE");
|
|
340
|
+
assertContained(root.path, current, "PARENT_CHANGED");
|
|
341
|
+
parents.push(snapshot(current, entry));
|
|
342
|
+
}
|
|
343
|
+
return { root, parents, targetPath: join(current, segments.at(-1)), target: null };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function preparePath(rootDir, relativePath, ops) {
|
|
347
|
+
const root = await inspectRoot(rootDir, ops);
|
|
348
|
+
const segments = normalizeRelativePath(relativePath);
|
|
349
|
+
const parentSegments = segments.slice(0, -1);
|
|
350
|
+
const parents = [root];
|
|
351
|
+
let current = root.path;
|
|
352
|
+
for (const segment of parentSegments) {
|
|
353
|
+
current = join(current, segment);
|
|
354
|
+
const entry = await lstatOptional(current, ops, "PARENT_TYPE");
|
|
355
|
+
if (!entry) throw protectedError("PARENT_MISSING");
|
|
356
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw protectedError("PARENT_TYPE");
|
|
357
|
+
assertContained(root.path, current, "PARENT_CHANGED");
|
|
358
|
+
parents.push(snapshot(current, entry));
|
|
359
|
+
}
|
|
360
|
+
return { root, parents, targetPath: join(current, segments.at(-1)), target: null };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function inspectRootSync(rootDir, ops) {
|
|
364
|
+
if (typeof rootDir !== "string" || rootDir.trim() === "" || rootDir.includes("\0")) {
|
|
365
|
+
throw protectedError("INVALID_ROOT");
|
|
366
|
+
}
|
|
367
|
+
const requested = resolve(rootDir);
|
|
368
|
+
const requestedEntry = lstatOptionalSync(requested, ops, "INVALID_ROOT");
|
|
369
|
+
if (!requestedEntry || requestedEntry.isSymbolicLink() || !requestedEntry.isDirectory()) {
|
|
370
|
+
throw protectedError(requestedEntry?.isSymbolicLink() ? "UNTRUSTED_ROOT" : "INVALID_ROOT");
|
|
371
|
+
}
|
|
372
|
+
let canonical;
|
|
373
|
+
try {
|
|
374
|
+
canonical = ops.realpath(requested);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw protectedError("UNTRUSTED_ROOT", error);
|
|
377
|
+
}
|
|
378
|
+
const entry = lstatRequiredSync(canonical, ops, "UNTRUSTED_ROOT");
|
|
379
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !sameIdentity(entry, requestedEntry)) {
|
|
380
|
+
throw protectedError("UNTRUSTED_ROOT");
|
|
381
|
+
}
|
|
382
|
+
return snapshot(canonical, entry);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function inspectRoot(rootDir, ops) {
|
|
386
|
+
if (typeof rootDir !== "string" || rootDir.trim() === "" || rootDir.includes("\0")) {
|
|
387
|
+
throw protectedError("INVALID_ROOT");
|
|
388
|
+
}
|
|
389
|
+
const requested = resolve(rootDir);
|
|
390
|
+
const requestedEntry = await lstatOptional(requested, ops, "INVALID_ROOT");
|
|
391
|
+
if (!requestedEntry || requestedEntry.isSymbolicLink() || !requestedEntry.isDirectory()) {
|
|
392
|
+
throw protectedError(requestedEntry?.isSymbolicLink() ? "UNTRUSTED_ROOT" : "INVALID_ROOT");
|
|
393
|
+
}
|
|
394
|
+
let canonical;
|
|
395
|
+
try {
|
|
396
|
+
canonical = await ops.realpath(requested);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
throw protectedError("UNTRUSTED_ROOT", error);
|
|
399
|
+
}
|
|
400
|
+
const entry = await lstatRequired(canonical, ops, "UNTRUSTED_ROOT");
|
|
401
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !sameIdentity(entry, requestedEntry)) {
|
|
402
|
+
throw protectedError("UNTRUSTED_ROOT");
|
|
403
|
+
}
|
|
404
|
+
return snapshot(canonical, entry);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function inspectTargetSync(path, commit, ops) {
|
|
408
|
+
const entry = lstatOptionalSync(path, ops, "TARGET_TYPE");
|
|
409
|
+
if (!entry) return { exists: false };
|
|
410
|
+
if (commit === "create-only") throw protectedError("TARGET_EXISTS");
|
|
411
|
+
if (entry.isSymbolicLink() || !entry.isFile()) throw protectedError("TARGET_TYPE");
|
|
412
|
+
return { exists: true, identity: identity(entry) };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function inspectTarget(path, commit, ops) {
|
|
416
|
+
const entry = await lstatOptional(path, ops, "TARGET_TYPE");
|
|
417
|
+
if (!entry) return { exists: false };
|
|
418
|
+
if (commit === "create-only") throw protectedError("TARGET_EXISTS");
|
|
419
|
+
if (entry.isSymbolicLink() || !entry.isFile()) throw protectedError("TARGET_TYPE");
|
|
420
|
+
return { exists: true, identity: identity(entry) };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function inspectAppendTargetSync(path, ops) {
|
|
424
|
+
const entry = lstatOptionalSync(path, ops, "TARGET_TYPE");
|
|
425
|
+
if (!entry) return null;
|
|
426
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.nlink !== 1) throw protectedError("TARGET_TYPE");
|
|
427
|
+
return { entry };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function recheckAppendTargetSync(path, initial, ops) {
|
|
431
|
+
const entry = lstatOptionalSync(path, ops, "TARGET_CHANGED");
|
|
432
|
+
if (!initial) {
|
|
433
|
+
if (entry) throw protectedError("TARGET_CHANGED");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (!entry || entry.isSymbolicLink() || !entry.isFile() || entry.nlink !== 1 || !sameIdentity(entry, initial.entry)) {
|
|
437
|
+
throw protectedError("TARGET_CHANGED");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function createTempSync(context, settings, ops) {
|
|
442
|
+
let collision = null;
|
|
443
|
+
for (let attempt = 0; attempt < MAX_TEMP_ATTEMPTS; attempt += 1) {
|
|
444
|
+
const name = tempName(settings.randomName, context.targetPath, attempt);
|
|
445
|
+
const path = join(context.parents.at(-1).path, name);
|
|
446
|
+
let handle;
|
|
447
|
+
try {
|
|
448
|
+
handle = ops.open(path, exclusiveWriteFlags(), settings.mode);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (error?.code === "EEXIST") {
|
|
451
|
+
collision = error;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
throw protectedError("TEMP_OPEN_FAILED", error);
|
|
455
|
+
}
|
|
456
|
+
let entry;
|
|
457
|
+
try {
|
|
458
|
+
entry = ops.fstat(handle);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return { path, handle, identity: null, mode: null, deferredError: protectedError("TEMP_TYPE", error) };
|
|
461
|
+
}
|
|
462
|
+
const temp = { path, handle, identity: identity(entry), mode: entry.mode & 0o7777 };
|
|
463
|
+
if (!entry.isFile() || entry.nlink !== 1) {
|
|
464
|
+
temp.deferredError = protectedError("TEMP_TYPE");
|
|
465
|
+
return temp;
|
|
466
|
+
}
|
|
467
|
+
let pathEntry;
|
|
468
|
+
try {
|
|
469
|
+
pathEntry = lstatRequiredSync(path, ops, "TEMP_CHANGED");
|
|
470
|
+
} catch (error) {
|
|
471
|
+
temp.deferredError = asProtectedError(error, "TEMP_CHANGED");
|
|
472
|
+
return temp;
|
|
473
|
+
}
|
|
474
|
+
if (!pathEntry.isFile() || pathEntry.isSymbolicLink() || !sameIdentity(pathEntry, entry)) {
|
|
475
|
+
temp.deferredError = protectedError("TEMP_CHANGED");
|
|
476
|
+
return temp;
|
|
477
|
+
}
|
|
478
|
+
return temp;
|
|
479
|
+
}
|
|
480
|
+
throw protectedError("TEMP_COLLISION", collision);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function createTemp(context, settings, ops) {
|
|
484
|
+
let collision = null;
|
|
485
|
+
for (let attempt = 0; attempt < MAX_TEMP_ATTEMPTS; attempt += 1) {
|
|
486
|
+
const name = tempName(settings.randomName, context.targetPath, attempt);
|
|
487
|
+
const path = join(context.parents.at(-1).path, name);
|
|
488
|
+
let handle;
|
|
489
|
+
try {
|
|
490
|
+
handle = await ops.open(path, exclusiveWriteFlags(), settings.mode);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error?.code === "EEXIST") {
|
|
493
|
+
collision = error;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
throw protectedError("TEMP_OPEN_FAILED", error);
|
|
497
|
+
}
|
|
498
|
+
let entry;
|
|
499
|
+
try {
|
|
500
|
+
entry = await ops.fstat(handle);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return { path, handle, identity: null, mode: null, deferredError: protectedError("TEMP_TYPE", error) };
|
|
503
|
+
}
|
|
504
|
+
const temp = { path, handle, identity: identity(entry), mode: entry.mode & 0o7777 };
|
|
505
|
+
if (!entry.isFile() || entry.nlink !== 1) {
|
|
506
|
+
temp.deferredError = protectedError("TEMP_TYPE");
|
|
507
|
+
return temp;
|
|
508
|
+
}
|
|
509
|
+
let pathEntry;
|
|
510
|
+
try {
|
|
511
|
+
pathEntry = await lstatRequired(path, ops, "TEMP_CHANGED");
|
|
512
|
+
} catch (error) {
|
|
513
|
+
temp.deferredError = asProtectedError(error, "TEMP_CHANGED");
|
|
514
|
+
return temp;
|
|
515
|
+
}
|
|
516
|
+
if (!pathEntry.isFile() || pathEntry.isSymbolicLink() || !sameIdentity(pathEntry, entry)) {
|
|
517
|
+
temp.deferredError = protectedError("TEMP_CHANGED");
|
|
518
|
+
return temp;
|
|
519
|
+
}
|
|
520
|
+
return temp;
|
|
521
|
+
}
|
|
522
|
+
throw protectedError("TEMP_COLLISION", collision);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function recheckWriteStateSync(context, temp, ops, handle = null) {
|
|
526
|
+
recheckParentsSync(context, ops);
|
|
527
|
+
if (context.target) recheckTargetSync(context.targetPath, context.target, ops);
|
|
528
|
+
if (temp) recheckTempSync(temp, ops, handle);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function recheckWriteState(context, temp, ops, handle = null) {
|
|
532
|
+
await recheckParents(context, ops);
|
|
533
|
+
if (context.target) await recheckTarget(context.targetPath, context.target, ops);
|
|
534
|
+
if (temp) await recheckTemp(temp, ops, handle);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function recheckParentsSync(context, ops) {
|
|
538
|
+
recheckSnapshotListSync(context.parents, ops);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function recheckSnapshotListSync(snapshots, ops) {
|
|
542
|
+
for (const expected of snapshots) {
|
|
543
|
+
const entry = lstatRequiredSync(expected.path, ops, "PARENT_CHANGED");
|
|
544
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !sameIdentity(entry, expected.identity)) {
|
|
545
|
+
throw protectedError("PARENT_CHANGED");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function recheckParents(context, ops) {
|
|
551
|
+
await recheckSnapshotList(context.parents, ops);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function recheckSnapshotList(snapshots, ops) {
|
|
555
|
+
for (const expected of snapshots) {
|
|
556
|
+
const entry = await lstatRequired(expected.path, ops, "PARENT_CHANGED");
|
|
557
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !sameIdentity(entry, expected.identity)) {
|
|
558
|
+
throw protectedError("PARENT_CHANGED");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function recheckTargetSync(path, expected, ops) {
|
|
564
|
+
const entry = lstatOptionalSync(path, ops, "TARGET_CHANGED");
|
|
565
|
+
if (!expected.exists) {
|
|
566
|
+
if (entry) throw protectedError("TARGET_CHANGED");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (!entry || entry.isSymbolicLink() || !entry.isFile() || !sameIdentity(entry, expected.identity)) {
|
|
570
|
+
throw protectedError("TARGET_CHANGED");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function recheckTarget(path, expected, ops) {
|
|
575
|
+
const entry = await lstatOptional(path, ops, "TARGET_CHANGED");
|
|
576
|
+
if (!expected.exists) {
|
|
577
|
+
if (entry) throw protectedError("TARGET_CHANGED");
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (!entry || entry.isSymbolicLink() || !entry.isFile() || !sameIdentity(entry, expected.identity)) {
|
|
581
|
+
throw protectedError("TARGET_CHANGED");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function recheckTempSync(temp, ops, handle) {
|
|
586
|
+
if (temp.deferredError) throw temp.deferredError;
|
|
587
|
+
if (!temp.identity) throw protectedError("TEMP_TYPE");
|
|
588
|
+
if (handle !== null) {
|
|
589
|
+
let opened;
|
|
590
|
+
try {
|
|
591
|
+
opened = ops.fstat(handle);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
throw protectedError("TEMP_CHANGED", error);
|
|
594
|
+
}
|
|
595
|
+
if (!opened.isFile() || opened.nlink !== 1 || !sameIdentity(opened, temp.identity)) throw protectedError("TEMP_CHANGED");
|
|
596
|
+
}
|
|
597
|
+
const entry = lstatRequiredSync(temp.path, ops, "TEMP_CHANGED");
|
|
598
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.nlink !== 1 || !sameIdentity(entry, temp.identity)) {
|
|
599
|
+
throw protectedError("TEMP_CHANGED");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function recheckTemp(temp, ops, handle) {
|
|
604
|
+
if (temp.deferredError) throw temp.deferredError;
|
|
605
|
+
if (!temp.identity) throw protectedError("TEMP_TYPE");
|
|
606
|
+
if (handle !== null) {
|
|
607
|
+
let opened;
|
|
608
|
+
try {
|
|
609
|
+
opened = await ops.fstat(handle);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
throw protectedError("TEMP_CHANGED", error);
|
|
612
|
+
}
|
|
613
|
+
if (!opened.isFile() || opened.nlink !== 1 || !sameIdentity(opened, temp.identity)) throw protectedError("TEMP_CHANGED");
|
|
614
|
+
}
|
|
615
|
+
const entry = await lstatRequired(temp.path, ops, "TEMP_CHANGED");
|
|
616
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.nlink !== 1 || !sameIdentity(entry, temp.identity)) {
|
|
617
|
+
throw protectedError("TEMP_CHANGED");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function writeAllSync(handle, bytes, ops) {
|
|
622
|
+
let offset = 0;
|
|
623
|
+
while (offset < bytes.length) {
|
|
624
|
+
let count;
|
|
625
|
+
try {
|
|
626
|
+
count = ops.write(handle, bytes, offset, bytes.length - offset, null);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
throw protectedError("WRITE_FAILED", error);
|
|
629
|
+
}
|
|
630
|
+
if (!Number.isInteger(count) || count <= 0 || count > bytes.length - offset) throw protectedError("WRITE_FAILED");
|
|
631
|
+
offset += count;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function writeAll(handle, bytes, ops) {
|
|
636
|
+
let offset = 0;
|
|
637
|
+
while (offset < bytes.length) {
|
|
638
|
+
let count;
|
|
639
|
+
try {
|
|
640
|
+
count = await ops.write(handle, bytes, offset, bytes.length - offset, null);
|
|
641
|
+
} catch (error) {
|
|
642
|
+
throw protectedError("WRITE_FAILED", error);
|
|
643
|
+
}
|
|
644
|
+
if (!Number.isInteger(count) || count <= 0 || count > bytes.length - offset) throw protectedError("WRITE_FAILED");
|
|
645
|
+
offset += count;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function commitReplaceSync(tempPath, targetPath, ops) {
|
|
650
|
+
try {
|
|
651
|
+
ops.rename(tempPath, targetPath);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
throw commitError(error);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function commitReplace(tempPath, targetPath, ops) {
|
|
658
|
+
try {
|
|
659
|
+
await ops.rename(tempPath, targetPath);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
throw commitError(error);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function commitCreateOnlySync(tempPath, targetPath, ops) {
|
|
666
|
+
try {
|
|
667
|
+
ops.link(tempPath, targetPath);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
throw createOnlyCommitError(error);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function commitCreateOnly(tempPath, targetPath, ops) {
|
|
674
|
+
try {
|
|
675
|
+
await ops.link(tempPath, targetPath);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
throw createOnlyCommitError(error);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function cleanupFailedTempSync(context, temp, settings, ops) {
|
|
682
|
+
try {
|
|
683
|
+
invokeSyncHook(settings.hooks.beforeCleanup);
|
|
684
|
+
recheckParentsSync(context, ops);
|
|
685
|
+
cleanupOwnedTempSync(temp, ops);
|
|
686
|
+
return null;
|
|
687
|
+
} catch (error) {
|
|
688
|
+
return error instanceof ProtectedWriteError && error.code === "CLEANUP_INDETERMINATE"
|
|
689
|
+
? error
|
|
690
|
+
: protectedError("CLEANUP_INDETERMINATE", error);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function cleanupFailedTemp(context, temp, settings, ops) {
|
|
695
|
+
try {
|
|
696
|
+
await invokeHook(settings.hooks.beforeCleanup);
|
|
697
|
+
await recheckParents(context, ops);
|
|
698
|
+
await cleanupOwnedTemp(temp, ops);
|
|
699
|
+
return null;
|
|
700
|
+
} catch (error) {
|
|
701
|
+
return error instanceof ProtectedWriteError && error.code === "CLEANUP_INDETERMINATE"
|
|
702
|
+
? error
|
|
703
|
+
: protectedError("CLEANUP_INDETERMINATE", error);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function cleanupAfterPublishedSync(context, temp, settings, ops) {
|
|
708
|
+
try {
|
|
709
|
+
invokeSyncHook(settings.hooks.beforeCleanup);
|
|
710
|
+
recheckParentsSync(context, ops);
|
|
711
|
+
cleanupOwnedTempSync(temp, ops);
|
|
712
|
+
return "clean";
|
|
713
|
+
} catch {
|
|
714
|
+
return "owned-temp-left";
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function cleanupAfterPublished(context, temp, settings, ops) {
|
|
719
|
+
try {
|
|
720
|
+
await invokeHook(settings.hooks.beforeCleanup);
|
|
721
|
+
await recheckParents(context, ops);
|
|
722
|
+
await cleanupOwnedTemp(temp, ops);
|
|
723
|
+
return "clean";
|
|
724
|
+
} catch {
|
|
725
|
+
return "owned-temp-left";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function cleanupOwnedTempSync(temp, ops) {
|
|
730
|
+
if (!temp.identity) throw protectedError("CLEANUP_INDETERMINATE");
|
|
731
|
+
const entry = lstatOptionalSync(temp.path, ops, "CLEANUP_INDETERMINATE");
|
|
732
|
+
if (!entry) return;
|
|
733
|
+
if (!sameIdentity(entry, temp.identity)) throw protectedError("CLEANUP_INDETERMINATE");
|
|
734
|
+
try {
|
|
735
|
+
ops.unlink(temp.path);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
throw protectedError("CLEANUP_INDETERMINATE", error);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async function cleanupOwnedTemp(temp, ops) {
|
|
742
|
+
if (!temp.identity) throw protectedError("CLEANUP_INDETERMINATE");
|
|
743
|
+
const entry = await lstatOptional(temp.path, ops, "CLEANUP_INDETERMINATE");
|
|
744
|
+
if (!entry) return;
|
|
745
|
+
if (!sameIdentity(entry, temp.identity)) throw protectedError("CLEANUP_INDETERMINATE");
|
|
746
|
+
try {
|
|
747
|
+
await ops.unlink(temp.path);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
throw protectedError("CLEANUP_INDETERMINATE", error);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function lstatOptionalSync(path, ops, code) {
|
|
754
|
+
try {
|
|
755
|
+
return ops.lstat(path);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (error?.code === "ENOENT") return null;
|
|
758
|
+
throw protectedError(code, error);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function lstatRequiredSync(path, ops, code) {
|
|
763
|
+
const entry = lstatOptionalSync(path, ops, code);
|
|
764
|
+
if (!entry) throw protectedError(code);
|
|
765
|
+
return entry;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function lstatOptional(path, ops, code) {
|
|
769
|
+
try {
|
|
770
|
+
return await ops.lstat(path);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
if (error?.code === "ENOENT") return null;
|
|
773
|
+
throw protectedError(code, error);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async function lstatRequired(path, ops, code) {
|
|
778
|
+
const entry = await lstatOptional(path, ops, code);
|
|
779
|
+
if (!entry) throw protectedError(code);
|
|
780
|
+
return entry;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function normalizeWriteOptions(options) {
|
|
784
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) throw protectedError("INVALID_OPTIONS");
|
|
785
|
+
const commit = options.commit ?? "replace-regular";
|
|
786
|
+
if (commit !== "replace-regular" && commit !== "create-only") throw protectedError("INVALID_OPTIONS");
|
|
787
|
+
return {
|
|
788
|
+
commit,
|
|
789
|
+
mode: normalizeMode(options.mode, 0o666),
|
|
790
|
+
randomName: options.randomName,
|
|
791
|
+
hooks: normalizeHooks(options.hooks),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function normalizeMode(value, fallback) {
|
|
796
|
+
const mode = value ?? fallback;
|
|
797
|
+
if (!Number.isInteger(mode) || mode < 0 || mode > 0o7777) throw protectedError("INVALID_OPTIONS");
|
|
798
|
+
return mode;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function normalizeHooks(hooks) {
|
|
802
|
+
if (hooks === undefined) return Object.freeze({});
|
|
803
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) throw protectedError("INVALID_OPTIONS");
|
|
804
|
+
for (const value of Object.values(hooks)) {
|
|
805
|
+
if (value !== undefined && typeof value !== "function") throw protectedError("INVALID_OPTIONS");
|
|
806
|
+
}
|
|
807
|
+
return hooks;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function normalizeRelativePath(value, options = {}) {
|
|
811
|
+
if (typeof value !== "string" || value.includes("\0") || value.includes("\\") || isAbsolute(value)) {
|
|
812
|
+
throw protectedError("INVALID_PATH");
|
|
813
|
+
}
|
|
814
|
+
if (value === "" && options.allowEmpty) return [];
|
|
815
|
+
if (value === "") throw protectedError("INVALID_PATH");
|
|
816
|
+
const segments = value.split("/");
|
|
817
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
818
|
+
throw protectedError("INVALID_PATH");
|
|
819
|
+
}
|
|
820
|
+
return segments;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function normalizeData(data) {
|
|
824
|
+
if (typeof data === "string") return Buffer.from(data, "utf8");
|
|
825
|
+
if (Buffer.isBuffer(data)) return data;
|
|
826
|
+
if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
827
|
+
throw protectedError("INVALID_OPTIONS");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function serializeJson(value, options) {
|
|
831
|
+
const space = options.space ?? 2;
|
|
832
|
+
if (space !== 0 && space !== 2) throw protectedError("INVALID_OPTIONS");
|
|
833
|
+
let serialized;
|
|
834
|
+
try {
|
|
835
|
+
serialized = JSON.stringify(value, null, space);
|
|
836
|
+
} catch (error) {
|
|
837
|
+
throw protectedError("INVALID_OPTIONS", error);
|
|
838
|
+
}
|
|
839
|
+
if (serialized === undefined) throw protectedError("INVALID_OPTIONS");
|
|
840
|
+
return options.trailingNewline === false ? serialized : `${serialized}\n`;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function tempName(randomName, targetPath, attempt) {
|
|
844
|
+
const generated = typeof randomName === "function"
|
|
845
|
+
? randomName({ attempt, targetPath })
|
|
846
|
+
: typeof randomName === "string"
|
|
847
|
+
? randomName
|
|
848
|
+
: `.${targetPath.split(sep).at(-1)}.${process.pid}.${randomUUID()}.tmp`;
|
|
849
|
+
if (
|
|
850
|
+
typeof generated !== "string"
|
|
851
|
+
|| generated === ""
|
|
852
|
+
|| generated === "."
|
|
853
|
+
|| generated === ".."
|
|
854
|
+
|| generated.includes("\0")
|
|
855
|
+
|| generated.includes("/")
|
|
856
|
+
|| generated.includes("\\")
|
|
857
|
+
|| generated === basename(targetPath)
|
|
858
|
+
) {
|
|
859
|
+
throw protectedError("INVALID_OPTIONS");
|
|
860
|
+
}
|
|
861
|
+
return generated;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function exclusiveWriteFlags() {
|
|
865
|
+
let flags = FS_CONSTANTS.O_WRONLY | FS_CONSTANTS.O_CREAT | FS_CONSTANTS.O_EXCL;
|
|
866
|
+
if (typeof FS_CONSTANTS.O_NOFOLLOW === "number") flags |= FS_CONSTANTS.O_NOFOLLOW;
|
|
867
|
+
return flags;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function snapshot(path, entry) {
|
|
871
|
+
return { path, identity: identity(entry) };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function identity(entry) {
|
|
875
|
+
return { dev: entry.dev, ino: entry.ino, type: entry.mode & FILE_TYPE_MASK };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function sameIdentity(left, right) {
|
|
879
|
+
const expected = right.identity || right;
|
|
880
|
+
const expectedType = expected.type ?? (expected.mode & FILE_TYPE_MASK);
|
|
881
|
+
return left.dev === expected.dev && left.ino === expected.ino && (left.mode & FILE_TYPE_MASK) === expectedType;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function assertContained(root, path, code) {
|
|
885
|
+
const rel = relative(root, path);
|
|
886
|
+
if (rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))) return;
|
|
887
|
+
throw protectedError(code);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function resultFor(context, settings, cleanup, mode) {
|
|
891
|
+
return Object.freeze({ path: context.targetPath, commit: settings.commit, mode, cleanup });
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function commitError(error) {
|
|
895
|
+
return protectedError(error?.code === "EXDEV" ? "CROSS_DEVICE" : "COMMIT_FAILED", error);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function createOnlyCommitError(error) {
|
|
899
|
+
if (error?.code === "EEXIST") return protectedError("TARGET_EXISTS", error);
|
|
900
|
+
if (error?.code === "EXDEV") return protectedError("CROSS_DEVICE", error);
|
|
901
|
+
if (["ENOSYS", "ENOTSUP", "EOPNOTSUPP"].includes(error?.code)) return protectedError("LINK_UNSUPPORTED", error);
|
|
902
|
+
return protectedError("COMMIT_FAILED", error);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function protectedError(code, cause) {
|
|
906
|
+
return new ProtectedWriteError(code, cause);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function asProtectedError(error, fallback) {
|
|
910
|
+
return error instanceof ProtectedWriteError ? error : protectedError(fallback, error);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function invokeSyncHook(hook) {
|
|
914
|
+
if (hook) hook();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function invokeHook(hook) {
|
|
918
|
+
if (hook) await hook();
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function syncOps(injected = {}) {
|
|
922
|
+
if (!injected || typeof injected !== "object" || Array.isArray(injected)) throw protectedError("INVALID_OPTIONS");
|
|
923
|
+
return {
|
|
924
|
+
lstat: injected.lstatSync || injected.lstat || lstatSync,
|
|
925
|
+
realpath: injected.realpathSync || injected.realpath || realpathSync.native,
|
|
926
|
+
mkdir: injected.mkdirSync || injected.mkdir || mkdirSync,
|
|
927
|
+
open: injected.openSync || injected.open || openSync,
|
|
928
|
+
fstat: injected.fstatSync || injected.fstat || fstatSync,
|
|
929
|
+
write: injected.writeSync || injected.write || writeSync,
|
|
930
|
+
close: injected.closeSync || injected.close || closeSync,
|
|
931
|
+
rename: injected.renameSync || injected.rename || renameSync,
|
|
932
|
+
link: injected.linkSync || injected.link || linkSync,
|
|
933
|
+
unlink: injected.unlinkSync || injected.unlink || unlinkSync,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function asyncOps(injected = {}) {
|
|
938
|
+
if (!injected || typeof injected !== "object" || Array.isArray(injected)) throw protectedError("INVALID_OPTIONS");
|
|
939
|
+
return {
|
|
940
|
+
lstat: injected.lstat || ((path) => fsPromises.lstat(path)),
|
|
941
|
+
realpath: injected.realpath || ((path) => fsPromises.realpath(path)),
|
|
942
|
+
mkdir: injected.mkdir || ((path, mode) => fsPromises.mkdir(path, { mode })),
|
|
943
|
+
open: injected.open || ((path, flags, mode) => fsPromises.open(path, flags, mode)),
|
|
944
|
+
fstat: injected.fstat || ((handle) => handle.stat()),
|
|
945
|
+
write: injected.write || (async (handle, buffer, offset, length, position) => {
|
|
946
|
+
const result = await handle.write(buffer, offset, length, position);
|
|
947
|
+
return result.bytesWritten;
|
|
948
|
+
}),
|
|
949
|
+
close: injected.close || ((handle) => handle.close()),
|
|
950
|
+
rename: injected.rename || ((source, destination) => fsPromises.rename(source, destination)),
|
|
951
|
+
link: injected.link || ((source, destination) => fsPromises.link(source, destination)),
|
|
952
|
+
unlink: injected.unlink || ((path) => fsPromises.unlink(path)),
|
|
953
|
+
};
|
|
954
|
+
}
|