@openclaw/fs-safe 0.13.0 → 0.14.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/CHANGELOG.md +26 -1
- package/dist/copy-file-input.d.ts +1 -1
- package/dist/copy-file-input.d.ts.map +1 -1
- package/dist/copy-file-input.js +2 -0
- package/dist/file-store-sync-directory.d.ts.map +1 -1
- package/dist/file-store-sync-directory.js +14 -7
- package/dist/guest-dispatch-python.js +1 -1
- package/dist/native-binding.d.ts +9 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-binding.js +8 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +6 -4
- package/dist/native-parent-admission.d.ts +1 -0
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +6 -3
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +8 -6
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +200 -163
- package/dist/native-policy-directory-observation.d.ts +12 -0
- package/dist/native-policy-directory-observation.d.ts.map +1 -0
- package/dist/native-policy-directory-observation.js +61 -0
- package/dist/native-staged-file.d.ts +4 -4
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +20 -10
- package/dist/native.d.ts +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +7 -2
- package/dist/pinned-mutation-admission.d.ts.map +1 -1
- package/dist/pinned-mutation-admission.js +13 -24
- package/dist/pinned-mutation-observation.d.ts +7 -1
- package/dist/pinned-mutation-observation.d.ts.map +1 -1
- package/dist/pinned-mutation-observation.js +87 -5
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +5 -3
- package/dist/root-directory-creation.d.ts.map +1 -1
- package/dist/root-directory-creation.js +5 -4
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +2 -1
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +1 -2
- package/dist/root-write-compatibility.d.ts +1 -2
- package/dist/root-write-compatibility.d.ts.map +1 -1
- package/dist/root-write-compatibility.js +17 -68
- package/dist/root-write-complete-parent.d.ts.map +1 -1
- package/dist/root-write-complete-parent.js +3 -1
- package/dist/root-write-lock-binding.d.ts +15 -0
- package/dist/root-write-lock-binding.d.ts.map +1 -0
- package/dist/root-write-lock-binding.js +162 -0
- package/dist/staged-directory.d.ts +5 -2
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +37 -1
- package/dist/temp-workspace-admission.d.ts.map +1 -1
- package/dist/temp-workspace-admission.js +28 -4
- package/docs/guest.md +1 -1
- package/docs/native-helper.md +14 -1
- package/docs/native.md +8 -0
- package/docs/writing.md +6 -0
- package/package.json +8 -8
|
@@ -57,6 +57,8 @@ import { FsSafeError } from "./errors.js";
|
|
|
57
57
|
import { runPinnedWriteWindows } from "./native-pinned-write-windows.js";
|
|
58
58
|
import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
|
|
59
59
|
import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
|
|
60
|
+
import { captureNativeFdClose } from "./native-binding.js";
|
|
61
|
+
import { NativePolicyDirectoryMismatch } from "./native-policy-directory-observation.js";
|
|
60
62
|
import { assertPolicyStagedDirectoryCurrent, assertStagedDirectoryCurrent, describePolicyStagedDirectory, describeStagedDirectory, refreshPolicyStagedDirectoryObservation, } from "./staged-directory.js";
|
|
61
63
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
62
64
|
import { realpathSync } from "./realpath.js";
|
|
@@ -117,8 +119,8 @@ async function describePosixParent(parentFd, pathname) {
|
|
|
117
119
|
observation: checkedMutationDirectory(parentPath, directory.realPath, parentPathStat),
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
|
-
function describePolicyPosixParent(parentFd, pathname) {
|
|
121
|
-
const policyDirectory = describePolicyStagedDirectory(parentFd, pathname);
|
|
122
|
+
function describePolicyPosixParent(binding, parentFd, pathname) {
|
|
123
|
+
const policyDirectory = describePolicyStagedDirectory(parentFd, pathname, binding);
|
|
122
124
|
return {
|
|
123
125
|
parentFd,
|
|
124
126
|
parentPath: policyDirectory.directory.realPath,
|
|
@@ -130,11 +132,12 @@ function describePolicyPosixParent(parentFd, pathname) {
|
|
|
130
132
|
}
|
|
131
133
|
function policyParentCaptureDeopt(error) {
|
|
132
134
|
return error instanceof FsSafeError &&
|
|
135
|
+
!(error instanceof NativePolicyDirectoryMismatch) &&
|
|
133
136
|
(error.code === "path-mismatch" || error.code === "not-file");
|
|
134
137
|
}
|
|
135
|
-
function tryDescribePolicyPosixParent(parentFd, pathname) {
|
|
138
|
+
function tryDescribePolicyPosixParent(binding, parentFd, pathname) {
|
|
136
139
|
try {
|
|
137
|
-
return describePolicyPosixParent(parentFd, pathname);
|
|
140
|
+
return describePolicyPosixParent(binding, parentFd, pathname);
|
|
138
141
|
}
|
|
139
142
|
catch (error) {
|
|
140
143
|
if (!policyParentCaptureDeopt(error))
|
|
@@ -148,183 +151,217 @@ function assertPosixParentCurrent(parent) {
|
|
|
148
151
|
: assertStagedDirectoryCurrent(parent.directory);
|
|
149
152
|
}
|
|
150
153
|
async function capturePolicyAwarePosixParent(binding, params, rootFd, directoryFlags) {
|
|
151
|
-
const
|
|
152
|
-
const parentSpelling = segments.length
|
|
153
|
-
? path.join(params.rootPath, ...segments)
|
|
154
|
-
: params.rootPath;
|
|
155
|
-
const segmentRoute = createPathSegmentRoute(segments);
|
|
156
|
-
const initialTarget = prospectiveTargetPath(params.rootPath, segmentRoute, 0, params.basename);
|
|
157
|
-
let retainedTargetPath = params.mutationAdmission?.beginParentWalk?.();
|
|
158
|
-
if (retainedTargetPath && !sameAbsolutePath(retainedTargetPath, initialTarget)) {
|
|
159
|
-
retainedTargetPath = undefined;
|
|
160
|
-
}
|
|
161
|
-
// Preserve the one-open hot path when the complete parent still exists.
|
|
162
|
-
// Policy is attached only after the opened descriptor is associated with its
|
|
163
|
-
// current canonical pathname, so a contained redirect cannot retain the old
|
|
164
|
-
// preflight authorization.
|
|
165
|
-
let completeParentFd;
|
|
154
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
166
155
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
156
|
+
const closeFd = captureNativeFdClose(binding);
|
|
157
|
+
const observationDisposers = new Map();
|
|
158
|
+
const disposeObservation = (fd) => {
|
|
159
|
+
const dispose = observationDisposers.get(fd);
|
|
160
|
+
observationDisposers.delete(fd);
|
|
161
|
+
dispose?.();
|
|
162
|
+
};
|
|
163
|
+
const observationScope = __addDisposableResource(env_1, {
|
|
164
|
+
[Symbol.dispose]() { for (const dispose of observationDisposers.values())
|
|
165
|
+
dispose(); },
|
|
166
|
+
}, false);
|
|
167
|
+
const capturePolicyParent = (fd, pathname) => {
|
|
168
|
+
const captured = tryDescribePolicyPosixParent(binding, fd, pathname);
|
|
169
|
+
const dispose = captured?.policyDirectory?.disposeObservation;
|
|
170
|
+
if (dispose)
|
|
171
|
+
observationDisposers.set(fd, dispose);
|
|
172
|
+
return captured;
|
|
173
|
+
};
|
|
174
|
+
const segments = relativeParentSegments(params.relativeParentPath);
|
|
175
|
+
const parentSpelling = segments.length
|
|
176
|
+
? path.join(params.rootPath, ...segments)
|
|
177
|
+
: params.rootPath;
|
|
178
|
+
const segmentRoute = createPathSegmentRoute(segments);
|
|
179
|
+
const initialTarget = prospectiveTargetPath(params.rootPath, segmentRoute, 0, params.basename);
|
|
180
|
+
let retainedTargetPath = params.mutationAdmission?.beginParentWalk?.();
|
|
181
|
+
if (retainedTargetPath && !sameAbsolutePath(retainedTargetPath, initialTarget)) {
|
|
182
|
+
retainedTargetPath = undefined;
|
|
183
|
+
}
|
|
184
|
+
// Preserve the one-open hot path when the complete parent still exists.
|
|
185
|
+
// Policy is attached only after the opened descriptor is associated with its
|
|
186
|
+
// current canonical pathname, so a contained redirect cannot retain the old
|
|
187
|
+
// preflight authorization.
|
|
188
|
+
let completeParentFd;
|
|
175
189
|
try {
|
|
176
|
-
|
|
177
|
-
? tryDescribePolicyPosixParent(parentFd, parentSpelling)
|
|
178
|
-
: undefined;
|
|
179
|
-
if (!admitted) {
|
|
180
|
-
retainedTargetPath = undefined;
|
|
181
|
-
admitted = await describePosixParent(parentFd, parentSpelling);
|
|
182
|
-
}
|
|
183
|
-
const targetPath = path.join(admitted.parentPath, params.basename);
|
|
184
|
-
await authorizePinnedMutation(params, {
|
|
185
|
-
targetPath,
|
|
186
|
-
mutationPath: targetPath,
|
|
187
|
-
phase: "parent",
|
|
188
|
-
});
|
|
189
|
-
assertPosixParentCurrent(admitted);
|
|
190
|
-
return admitted;
|
|
190
|
+
completeParentFd = binding.openBeneath(rootFd, params.relativeParentPath, directoryFlags).fd;
|
|
191
191
|
}
|
|
192
192
|
catch (error) {
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
if (error?.code !== "ENOENT" || !params.mkdir)
|
|
194
|
+
throw error;
|
|
195
195
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// existing prefix. Walk missing-parent cases one direct child at a time so
|
|
199
|
-
// every existing/opened object is authorized and every mkdir is authorized
|
|
200
|
-
// before dispatch. O_NOFOLLOW makes a concurrently introduced link fail
|
|
201
|
-
// before the next component can be created through it.
|
|
202
|
-
const secureDirectoryFlags = directoryFlags | (fsSync.constants.O_NOFOLLOW ?? 0);
|
|
203
|
-
let currentFd = rootFd;
|
|
204
|
-
let currentOwnedFd;
|
|
205
|
-
let currentPath = params.rootPath;
|
|
206
|
-
let current = retainedTargetPath
|
|
207
|
-
? tryDescribePolicyPosixParent(rootFd, currentPath)
|
|
208
|
-
: undefined;
|
|
209
|
-
if (!current) {
|
|
210
|
-
retainedTargetPath = undefined;
|
|
211
|
-
current = await describePosixParent(rootFd, currentPath);
|
|
212
|
-
}
|
|
213
|
-
try {
|
|
214
|
-
await authorizePinnedMutation(params, {
|
|
215
|
-
targetPath: retainedTargetPath ?? initialTarget,
|
|
216
|
-
mutationPath: retainedTargetPath ?? initialTarget,
|
|
217
|
-
phase: "parent",
|
|
218
|
-
});
|
|
219
|
-
assertPosixParentCurrent(current);
|
|
220
|
-
for (let index = 0; index < segments.length; index += 1) {
|
|
221
|
-
const segment = segments[index];
|
|
222
|
-
const childPath = path.join(currentPath, segment);
|
|
223
|
-
let childFd;
|
|
224
|
-
let createdByMkdir = false;
|
|
225
|
-
let createReceipt;
|
|
196
|
+
if (completeParentFd !== undefined) {
|
|
197
|
+
const parentFd = completeParentFd;
|
|
226
198
|
try {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (!
|
|
231
|
-
|
|
199
|
+
let admitted = retainedTargetPath
|
|
200
|
+
? capturePolicyParent(parentFd, parentSpelling)
|
|
201
|
+
: undefined;
|
|
202
|
+
if (!admitted) {
|
|
203
|
+
retainedTargetPath = undefined;
|
|
204
|
+
admitted = await describePosixParent(parentFd, parentSpelling);
|
|
232
205
|
}
|
|
233
|
-
const targetPath =
|
|
234
|
-
|
|
206
|
+
const targetPath = path.join(admitted.parentPath, params.basename);
|
|
207
|
+
await authorizePinnedMutation(params, {
|
|
235
208
|
targetPath,
|
|
236
|
-
mutationPath:
|
|
237
|
-
phase: "parent
|
|
209
|
+
mutationPath: targetPath,
|
|
210
|
+
phase: "parent",
|
|
238
211
|
});
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
212
|
+
assertPosixParentCurrent(admitted);
|
|
213
|
+
return admitted;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
disposeObservation(parentFd);
|
|
217
|
+
closeFd(parentFd);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// The canonical preflight spelling has no intentional symlinks in its
|
|
222
|
+
// existing prefix. Walk missing-parent cases one direct child at a time so
|
|
223
|
+
// every existing/opened object is authorized and every mkdir is authorized
|
|
224
|
+
// before dispatch. O_NOFOLLOW makes a concurrently introduced link fail
|
|
225
|
+
// before the next component can be created through it.
|
|
226
|
+
const secureDirectoryFlags = directoryFlags | (fsSync.constants.O_NOFOLLOW ?? 0);
|
|
227
|
+
let currentFd = rootFd;
|
|
228
|
+
let currentOwnedFd;
|
|
229
|
+
let currentPath = params.rootPath;
|
|
230
|
+
let current = retainedTargetPath
|
|
231
|
+
? capturePolicyParent(rootFd, currentPath)
|
|
232
|
+
: undefined;
|
|
233
|
+
if (!current) {
|
|
234
|
+
retainedTargetPath = undefined;
|
|
235
|
+
current = await describePosixParent(rootFd, currentPath);
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
await authorizePinnedMutation(params, {
|
|
239
|
+
targetPath: retainedTargetPath ?? initialTarget,
|
|
240
|
+
mutationPath: retainedTargetPath ?? initialTarget,
|
|
241
|
+
phase: "parent",
|
|
242
|
+
});
|
|
243
|
+
assertPosixParentCurrent(current);
|
|
244
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
245
|
+
const segment = segments[index];
|
|
246
|
+
const childPath = path.join(currentPath, segment);
|
|
247
|
+
let childFd;
|
|
248
|
+
let createdByMkdir = false;
|
|
249
|
+
let createReceipt;
|
|
248
250
|
try {
|
|
249
251
|
childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
|
|
250
252
|
}
|
|
251
|
-
catch (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (createdByMkdir && createReceipt && params.mutationAdmission?.advanceCreatedDirectory) {
|
|
271
|
-
let evidence;
|
|
253
|
+
catch (openError) {
|
|
254
|
+
if (!isNotFoundPathError(openError)) {
|
|
255
|
+
throw normalizePolicyParentOpenError(openError, params);
|
|
256
|
+
}
|
|
257
|
+
const targetPath = retainedTargetPath ?? prospectiveTargetPath(currentPath, segmentRoute, index, params.basename);
|
|
258
|
+
const request = Object.freeze({
|
|
259
|
+
targetPath,
|
|
260
|
+
mutationPath: childPath,
|
|
261
|
+
phase: "parent-create",
|
|
262
|
+
});
|
|
263
|
+
createReceipt = params.mutationAdmission?.tryAuthorizeAtParent?.(request, current.observation);
|
|
264
|
+
if (!createReceipt) {
|
|
265
|
+
createReceipt = await authorizePinnedMutation(params, request);
|
|
266
|
+
assertPosixParentCurrent(current);
|
|
267
|
+
}
|
|
268
|
+
params.assertBeforeMutation?.();
|
|
269
|
+
// Do not carry pathname freshness across the live authority callback.
|
|
270
|
+
assertPosixParentCurrent(current);
|
|
271
|
+
createdByMkdir = mkdirPolicyChild(binding, currentFd, segment, 0o777);
|
|
272
272
|
try {
|
|
273
|
-
|
|
274
|
-
? refreshPolicyStagedDirectoryObservation(current.policyDirectory)
|
|
275
|
-
: checkedMutationDirectory(currentPath, current.directory.realPath, assertStagedDirectoryCurrent(current.directory));
|
|
276
|
-
evidence = Object.freeze({
|
|
277
|
-
admission: createReceipt,
|
|
278
|
-
parent,
|
|
279
|
-
child: child.observation,
|
|
280
|
-
});
|
|
273
|
+
childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
|
|
281
274
|
}
|
|
282
|
-
catch {
|
|
283
|
-
|
|
275
|
+
catch (createdOpenError) {
|
|
276
|
+
throw normalizePolicyParentOpenError(createdOpenError, params);
|
|
284
277
|
}
|
|
285
|
-
|
|
286
|
-
|
|
278
|
+
}
|
|
279
|
+
let child;
|
|
280
|
+
try {
|
|
281
|
+
const acceleratedChild = retainedTargetPath
|
|
282
|
+
? capturePolicyParent(childFd, childPath)
|
|
283
|
+
: undefined;
|
|
284
|
+
if (acceleratedChild) {
|
|
285
|
+
child = acceleratedChild;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
retainedTargetPath = undefined;
|
|
289
|
+
child = await describePosixParent(childFd, childPath);
|
|
290
|
+
}
|
|
291
|
+
if (!sameAbsolutePath(child.parentPath, childPath))
|
|
292
|
+
retainedTargetPath = undefined;
|
|
293
|
+
let childAuthorization;
|
|
294
|
+
if (createdByMkdir && createReceipt && params.mutationAdmission?.advanceCreatedDirectory) {
|
|
295
|
+
let evidence;
|
|
296
|
+
try {
|
|
297
|
+
const parent = current.policyDirectory
|
|
298
|
+
? refreshPolicyStagedDirectoryObservation(current.policyDirectory)
|
|
299
|
+
: checkedMutationDirectory(currentPath, current.directory.realPath, assertStagedDirectoryCurrent(current.directory));
|
|
300
|
+
evidence = Object.freeze({
|
|
301
|
+
admission: createReceipt,
|
|
302
|
+
parent,
|
|
303
|
+
child: child.observation,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Leave failed evidence to the full ordered admission below.
|
|
308
|
+
}
|
|
309
|
+
if (evidence) {
|
|
310
|
+
childAuthorization = params.mutationAdmission.advanceCreatedDirectory(evidence);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const targetPath = retainedTargetPath ?? prospectiveTargetPath(child.parentPath, segmentRoute, index + 1, params.basename);
|
|
314
|
+
const request = Object.freeze({
|
|
315
|
+
targetPath,
|
|
316
|
+
mutationPath: targetPath,
|
|
317
|
+
phase: "parent",
|
|
318
|
+
});
|
|
319
|
+
childAuthorization ??= params.mutationAdmission?.tryAuthorizeAtParent?.(request, child.observation);
|
|
320
|
+
if (!childAuthorization) {
|
|
321
|
+
await authorizePinnedMutation(params, request);
|
|
322
|
+
assertPosixParentCurrent(child);
|
|
287
323
|
}
|
|
288
324
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
325
|
+
catch (error) {
|
|
326
|
+
disposeObservation(childFd);
|
|
327
|
+
closeFd(childFd);
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
const previousOwnedFd = currentOwnedFd;
|
|
331
|
+
disposeObservation(current.parentFd);
|
|
332
|
+
currentOwnedFd = child.parentFd;
|
|
333
|
+
currentFd = child.parentFd;
|
|
334
|
+
currentPath = child.parentPath;
|
|
335
|
+
current = child;
|
|
336
|
+
if (previousOwnedFd !== undefined)
|
|
337
|
+
closeFd(previousOwnedFd);
|
|
338
|
+
if (index === segments.length - 1) {
|
|
339
|
+
currentOwnedFd = undefined;
|
|
340
|
+
return child;
|
|
299
341
|
}
|
|
300
342
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
currentPath = child.parentPath;
|
|
309
|
-
current = child;
|
|
310
|
-
if (previousOwnedFd !== undefined)
|
|
311
|
-
fsSync.closeSync(previousOwnedFd);
|
|
312
|
-
if (index === segments.length - 1) {
|
|
313
|
-
currentOwnedFd = undefined;
|
|
314
|
-
return child;
|
|
343
|
+
// Empty relative parents are handled by the complete-parent fast path.
|
|
344
|
+
throw new FsSafeError("path-mismatch", "native write parent admission did not complete");
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
if (currentOwnedFd !== undefined) {
|
|
348
|
+
disposeObservation(currentOwnedFd);
|
|
349
|
+
closeFd(currentOwnedFd);
|
|
315
350
|
}
|
|
316
351
|
}
|
|
317
|
-
|
|
318
|
-
|
|
352
|
+
}
|
|
353
|
+
catch (e_1) {
|
|
354
|
+
env_1.error = e_1;
|
|
355
|
+
env_1.hasError = true;
|
|
319
356
|
}
|
|
320
357
|
finally {
|
|
321
|
-
|
|
322
|
-
fsSync.closeSync(currentOwnedFd);
|
|
358
|
+
__disposeResources(env_1);
|
|
323
359
|
}
|
|
324
360
|
}
|
|
325
361
|
export async function runPinnedWriteNative(binding, params) {
|
|
326
|
-
const
|
|
362
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
327
363
|
try {
|
|
364
|
+
const closeFd = captureNativeFdClose(binding);
|
|
328
365
|
const windows = process.platform === "win32";
|
|
329
366
|
if (!windows) {
|
|
330
367
|
assertNativeStaging(binding);
|
|
@@ -336,15 +373,15 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
336
373
|
operation: "native write",
|
|
337
374
|
});
|
|
338
375
|
const root = rootAdmission.root;
|
|
339
|
-
const posixRoot = __addDisposableResource(
|
|
376
|
+
const posixRoot = __addDisposableResource(env_2, windows ? undefined : root, true);
|
|
340
377
|
let parentFd;
|
|
341
378
|
let windowsOwnsDirectories = false;
|
|
342
379
|
// Until stage construction takes ownership, even admission failures must close
|
|
343
380
|
// the raw POSIX parent. Disposal preserves both admission and close failures.
|
|
344
|
-
const parentGuard = __addDisposableResource(
|
|
381
|
+
const parentGuard = __addDisposableResource(env_2, {
|
|
345
382
|
[Symbol.dispose]() {
|
|
346
383
|
if (!windows && parentFd !== undefined) {
|
|
347
|
-
|
|
384
|
+
closeFd(parentFd);
|
|
348
385
|
}
|
|
349
386
|
},
|
|
350
387
|
}, false);
|
|
@@ -400,7 +437,7 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
400
437
|
}
|
|
401
438
|
const ownedParent = parentFd;
|
|
402
439
|
parentFd = undefined;
|
|
403
|
-
return await writeNativeStage(binding, ownedParent, directory, params, verificationGuard);
|
|
440
|
+
return await writeNativeStage(binding, ownedParent, closeFd, directory, params, verificationGuard);
|
|
404
441
|
}
|
|
405
442
|
finally {
|
|
406
443
|
if (windows && !windowsOwnsDirectories) {
|
|
@@ -410,7 +447,7 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
410
447
|
// a close attempt. In particular, a parent close failure must not skip
|
|
411
448
|
// the root FileHandle close.
|
|
412
449
|
try {
|
|
413
|
-
|
|
450
|
+
closeFd(parentFd);
|
|
414
451
|
}
|
|
415
452
|
catch {
|
|
416
453
|
// Best effort while propagating the operation failure.
|
|
@@ -420,12 +457,12 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
420
457
|
}
|
|
421
458
|
}
|
|
422
459
|
}
|
|
423
|
-
catch (
|
|
424
|
-
|
|
425
|
-
|
|
460
|
+
catch (e_2) {
|
|
461
|
+
env_2.error = e_2;
|
|
462
|
+
env_2.hasError = true;
|
|
426
463
|
}
|
|
427
464
|
finally {
|
|
428
|
-
const result_1 = __disposeResources(
|
|
465
|
+
const result_1 = __disposeResources(env_2);
|
|
429
466
|
if (result_1)
|
|
430
467
|
await result_1;
|
|
431
468
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import type { NativeBinding } from "./native-binding.js";
|
|
3
|
+
import type { MutationDirectoryObserver } from "./pinned-mutation-observation.js";
|
|
4
|
+
export declare class NativePolicyDirectoryMismatch extends FsSafeError {
|
|
5
|
+
constructor(cause?: Error);
|
|
6
|
+
}
|
|
7
|
+
export type NativePolicyDirectoryObserver = MutationDirectoryObserver & {
|
|
8
|
+
dispose(): void;
|
|
9
|
+
};
|
|
10
|
+
/** Fresh name/metadata/canonical facts from a caller-owned POSIX descriptor. */
|
|
11
|
+
export declare function nativePolicyDirectoryObserver(binding: NativeBinding | undefined, fd: number, pathname: string): NativePolicyDirectoryObserver | undefined;
|
|
12
|
+
//# sourceMappingURL=native-policy-directory-observation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-policy-directory-observation.d.ts","sourceRoot":"","sources":["../src/native-policy-directory-observation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAElF,qBAAa,6BAA8B,SAAQ,WAAW;IAC5D,YAAY,KAAK,CAAC,EAAE,KAAK,EAExB;CACF;AAED,MAAM,MAAM,6BAA6B,GAAG,yBAAyB,GAAG;IAAE,OAAO,IAAI,IAAI,CAAA;CAAE,CAAC;AAE5F,gFAAgF;AAChF,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,GACf,6BAA6B,GAAG,SAAS,CA2C3C"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
4
|
+
export class NativePolicyDirectoryMismatch extends FsSafeError {
|
|
5
|
+
constructor(cause) {
|
|
6
|
+
super("path-mismatch", "staging directory pathname changed", { cause });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Fresh name/metadata/canonical facts from a caller-owned POSIX descriptor. */
|
|
10
|
+
export function nativePolicyDirectoryObserver(binding, fd, pathname) {
|
|
11
|
+
if (!binding || process.platform === "win32" || process.versions.bun)
|
|
12
|
+
return undefined;
|
|
13
|
+
const observe = binding.observeDirectoryFd;
|
|
14
|
+
if (typeof observe !== "function")
|
|
15
|
+
return undefined;
|
|
16
|
+
const mode = getFsSafeNativeConfig().mode;
|
|
17
|
+
let disposed = false;
|
|
18
|
+
const inspect = () => {
|
|
19
|
+
if (disposed)
|
|
20
|
+
throw new NativePolicyDirectoryMismatch();
|
|
21
|
+
if (getFsSafeNativeConfig().mode !== mode)
|
|
22
|
+
return undefined;
|
|
23
|
+
let observed;
|
|
24
|
+
try {
|
|
25
|
+
observed = observe.call(binding, fd, pathname);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error?.code === "OBSERVATION_UNAVAILABLE")
|
|
29
|
+
return undefined;
|
|
30
|
+
if (error?.code === "OBSERVATION_REDIRECTED") {
|
|
31
|
+
// A contained redirect requires ordinary selected-object admission;
|
|
32
|
+
// it has not yet supplied inconsistent descriptor/name evidence.
|
|
33
|
+
throw new FsSafeError("path-mismatch", "staging directory pathname redirected");
|
|
34
|
+
}
|
|
35
|
+
if (error?.code === "path-mismatch") {
|
|
36
|
+
throw new NativePolicyDirectoryMismatch(error instanceof Error ? error : undefined);
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (getFsSafeNativeConfig().mode !== mode)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (!observed || typeof observed.dev !== "bigint" || observed.dev < 0n || observed.dev > 0xffffffffffffffffn ||
|
|
43
|
+
typeof observed.ino !== "bigint" || observed.ino < 0n || observed.ino > 0xffffffffffffffffn ||
|
|
44
|
+
typeof observed.mode !== "bigint" || observed.mode < 0n || observed.mode > 0xffffffffn ||
|
|
45
|
+
typeof observed.nlink !== "bigint" || observed.nlink < 0n || observed.nlink > 0xffffffffffffffffn ||
|
|
46
|
+
typeof observed.realPath !== "string" || observed.realPath.includes("\0") ||
|
|
47
|
+
!path.isAbsolute(observed.realPath))
|
|
48
|
+
return undefined;
|
|
49
|
+
if ((observed.mode & 61440n) !== 16384n || observed.nlink === 0n ||
|
|
50
|
+
observed.realPath !== pathname) {
|
|
51
|
+
throw new NativePolicyDirectoryMismatch();
|
|
52
|
+
}
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
canonicalPath: observed.realPath,
|
|
55
|
+
identity: Object.freeze({
|
|
56
|
+
dev: observed.dev, ino: observed.ino, mode: observed.mode, nlink: observed.nlink,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
return Object.assign(inspect, { dispose() { disposed = true; } });
|
|
61
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { DirectoryReceipt } from "./directory-durability.js";
|
|
2
2
|
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type NativeBinding } from "./native-binding.js";
|
|
5
5
|
import type { PinnedWriteInput, PinnedWriteParams } from "./pinned-write.js";
|
|
6
6
|
import type { PublishedFileReceipt, StagedFile, StagedFileCleanupReceipt, StagedFileReceipt } from "./staged-file-types.js";
|
|
7
7
|
export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding, "createStagedFile" | "stagedFileMatches" | "removeStagedFile">>;
|
|
8
8
|
export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
|
|
9
9
|
declare class NativeStagedFile implements StagedFile {
|
|
10
10
|
#private;
|
|
11
|
-
constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
|
|
12
|
-
static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
|
|
13
|
-
static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
|
|
11
|
+
constructor(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
|
|
12
|
+
static create(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
|
|
13
|
+
static write(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
|
|
14
14
|
get receipt(): StagedFileReceipt;
|
|
15
15
|
assertCurrent(): Promise<void>;
|
|
16
16
|
publish(basename: string, options: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAMlE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"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"}
|