@effect-vfs/core 0.8.0 → 0.10.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.
Files changed (105) hide show
  1. package/README.md +9 -0
  2. package/dist/BytePath.d.ts +4 -1
  3. package/dist/BytePath.d.ts.map +1 -1
  4. package/dist/BytePath.js +6 -4
  5. package/dist/Caller.d.ts +107 -66
  6. package/dist/Caller.d.ts.map +1 -1
  7. package/dist/Caller.js +16 -86
  8. package/dist/LiveVolume.d.ts +75 -2
  9. package/dist/LiveVolume.d.ts.map +1 -1
  10. package/dist/LiveVolume.js +87 -3
  11. package/dist/Search.d.ts +32 -8
  12. package/dist/Search.d.ts.map +1 -1
  13. package/dist/Search.js +19 -10
  14. package/dist/SynchronousAdapter.d.ts +58 -0
  15. package/dist/SynchronousAdapter.d.ts.map +1 -0
  16. package/dist/SynchronousAdapter.js +116 -0
  17. package/dist/Target.d.ts +7 -91
  18. package/dist/Target.d.ts.map +1 -1
  19. package/dist/Target.js +7 -47
  20. package/dist/Testing.d.ts +5 -2
  21. package/dist/Testing.d.ts.map +1 -1
  22. package/dist/Testing.js +2 -1
  23. package/dist/VfsError.d.ts +4 -2
  24. package/dist/VfsError.d.ts.map +1 -1
  25. package/dist/VfsError.js +3 -1
  26. package/dist/VirtualFileSystem.d.ts +151 -44
  27. package/dist/VirtualFileSystem.d.ts.map +1 -1
  28. package/dist/VirtualFileSystem.js +31 -21
  29. package/dist/Volume.d.ts +6 -1
  30. package/dist/Volume.d.ts.map +1 -1
  31. package/dist/Volume.js +6 -2
  32. package/dist/Watch.d.ts +18 -0
  33. package/dist/Watch.d.ts.map +1 -1
  34. package/dist/Watch.js +12 -0
  35. package/dist/internal/adapterBinding.d.ts +13 -0
  36. package/dist/internal/adapterBinding.d.ts.map +1 -0
  37. package/dist/internal/adapterBinding.js +13 -0
  38. package/dist/internal/bytePath.d.ts +6 -1
  39. package/dist/internal/bytePath.d.ts.map +1 -1
  40. package/dist/internal/callerWatch.d.ts +36 -0
  41. package/dist/internal/callerWatch.d.ts.map +1 -0
  42. package/dist/internal/callerWatch.js +185 -0
  43. package/dist/internal/engine.d.ts +1305 -0
  44. package/dist/internal/engine.d.ts.map +1 -0
  45. package/dist/internal/engine.js +3458 -0
  46. package/dist/internal/errors.d.ts +1 -33
  47. package/dist/internal/errors.d.ts.map +1 -1
  48. package/dist/internal/errors.js +1 -45
  49. package/dist/internal/fixture.d.ts +2 -1
  50. package/dist/internal/fixture.d.ts.map +1 -1
  51. package/dist/internal/fixture.js +8 -4
  52. package/dist/internal/image.d.ts +4 -1
  53. package/dist/internal/image.d.ts.map +1 -1
  54. package/dist/internal/image.js +6 -3
  55. package/dist/internal/lines.d.ts.map +1 -1
  56. package/dist/internal/lines.js +3 -2
  57. package/dist/internal/liveImage.d.ts +4 -2
  58. package/dist/internal/liveImage.d.ts.map +1 -1
  59. package/dist/internal/liveImage.js +3 -1
  60. package/dist/internal/merkle.d.ts +7 -4
  61. package/dist/internal/merkle.d.ts.map +1 -1
  62. package/dist/internal/merkle.js +6 -4
  63. package/dist/internal/search.d.ts +3 -1
  64. package/dist/internal/search.d.ts.map +1 -1
  65. package/dist/internal/search.js +4 -3
  66. package/dist/internal/snapshotDelta.d.ts +11 -7
  67. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  68. package/dist/internal/snapshotDelta.js +15 -9
  69. package/dist/internal/snapshotMerge.d.ts +1 -1
  70. package/dist/internal/snapshotMerge.d.ts.map +1 -1
  71. package/dist/internal/snapshotMerge.js +5 -2
  72. package/dist/internal/tree.d.ts +4 -2
  73. package/dist/internal/tree.d.ts.map +1 -1
  74. package/dist/internal/tree.js +4 -2
  75. package/dist/internal/virtualFileSystem.d.ts +20 -49
  76. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  77. package/dist/internal/virtualFileSystem.js +85 -1927
  78. package/dist/internal/volumeLimits.d.ts +1 -0
  79. package/dist/internal/volumeLimits.d.ts.map +1 -1
  80. package/dist/internal/volumeLimits.js +3 -2
  81. package/dist/internal/volumeState.d.ts +10 -79
  82. package/dist/internal/volumeState.d.ts.map +1 -1
  83. package/dist/internal/volumeState.js +9 -79
  84. package/dist/internal/watchHub.d.ts +9 -5
  85. package/dist/internal/watchHub.d.ts.map +1 -1
  86. package/dist/internal/watchHub.js +35 -8
  87. package/package.json +16 -9
  88. package/dist/internal/inodeTable.d.ts +0 -20
  89. package/dist/internal/inodeTable.d.ts.map +0 -1
  90. package/dist/internal/inodeTable.js +0 -48
  91. package/dist/internal/path.d.ts +0 -56
  92. package/dist/internal/path.d.ts.map +0 -1
  93. package/dist/internal/path.js +0 -141
  94. package/dist/internal/resolution.d.ts +0 -135
  95. package/dist/internal/resolution.d.ts.map +0 -1
  96. package/dist/internal/resolution.js +0 -277
  97. package/dist/internal/testSeams.d.ts +0 -17
  98. package/dist/internal/testSeams.d.ts.map +0 -1
  99. package/dist/internal/testSeams.js +0 -20
  100. package/dist/internal/tokenRegistry.d.ts +0 -77
  101. package/dist/internal/tokenRegistry.d.ts.map +0 -1
  102. package/dist/internal/tokenRegistry.js +0 -128
  103. package/dist/internal/turnstile.d.ts +0 -8
  104. package/dist/internal/turnstile.d.ts.map +0 -1
  105. package/dist/internal/turnstile.js +0 -43
@@ -0,0 +1,3458 @@
1
+ import * as Arr from "effect/Array";
2
+ import * as Brand from "effect/Brand";
3
+ import * as ByteSize from "effect/ByteSize";
4
+ import * as Cause from "effect/Cause";
5
+ import * as Clock from "effect/Clock";
6
+ import * as Context from "effect/Context";
7
+ import * as Data from "effect/Data";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Hex from "effect/encoding/Hex";
10
+ import * as Equal from "effect/Equal";
11
+ import * as Exit from "effect/Exit";
12
+ import { dual } from "effect/Function";
13
+ import * as HashMap from "effect/HashMap";
14
+ import * as Match from "effect/Match";
15
+ import * as MutableRef from "effect/MutableRef";
16
+ import * as Option from "effect/Option";
17
+ import * as Order from "effect/Order";
18
+ import * as Predicate from "effect/Predicate";
19
+ import * as Result from "effect/Result";
20
+ import * as Scheduler from "effect/Scheduler";
21
+ import * as Schema from "effect/Schema";
22
+ import * as Scope from "effect/Scope";
23
+ import * as Semaphore from "effect/Semaphore";
24
+ import * as Stream from "effect/Stream";
25
+ import { DirectoryHandleId, FileHandleId, SeekMode } from "../FileHandle.js";
26
+ import * as BytePathStorage from "./bytePath.js";
27
+ import { Mode, OwnerUpdate, Times, Timestamp } from "../Metadata.js";
28
+ import { make as makeError, VfsError } from "../VfsError.js";
29
+ /** Filesystem error helpers; image/codec helpers remain community-owned. @internal */
30
+ export var Errors;
31
+ (function (Errors) {
32
+ const encoder = new TextEncoder();
33
+ // Omit invalid paths rather than attach a BytePath that breaks its invariant.
34
+ /** @internal */
35
+ Errors.errorPath = (input) => {
36
+ const bytes = Predicate.isString(input)
37
+ ? encoder.encode(input)
38
+ : BytePathStorage.isBytePath(input)
39
+ ? BytePathStorage.getBytes(input)
40
+ : undefined;
41
+ if (bytes === undefined || !BytePathStorage.isPathBytes(bytes))
42
+ return undefined;
43
+ return BytePathStorage.isBytePath(input) ? input : BytePathStorage.make(bytes);
44
+ };
45
+ const configurationField = (issue) => {
46
+ if (Predicate.isTagged("Pointer")(issue))
47
+ return issue.path.map(String).join(".");
48
+ if (Predicate.isTagged("Composite")(issue))
49
+ return configurationField(issue.issues[0]);
50
+ if (Predicate.isTagged("Encoding")(issue))
51
+ return configurationField(issue.issue);
52
+ return "options";
53
+ };
54
+ const build = (code, operation, details) => makeError({
55
+ code,
56
+ operation,
57
+ field: details?.field,
58
+ path: details?.path === undefined ? undefined : Errors.errorPath(details.path),
59
+ cause: details?.cause
60
+ });
61
+ /** @internal */
62
+ Errors.argumentFailure = (operation, field, cause) => build("InvalidArgument", operation, { field, cause });
63
+ /** @internal */
64
+ Errors.decodeConfiguration = (schema, value, operation) => Schema.decodeUnknownResult(schema, { onExcessProperty: "error" })(value).pipe(Result.mapError((error) => Errors.argumentFailure(operation, configurationField(error.issue), error)));
65
+ /** @internal */
66
+ Errors.fsFailure = (code, operation, details) => build(code, operation, details);
67
+ const makeOpContext = (operation, located) => ({
68
+ operation,
69
+ fail: (code, details) => Errors.fsFailure(code, operation, located === undefined ? details : { ...details, path: located }),
70
+ at: (path) => makeOpContext(operation, Predicate.isString(path) || BytePathStorage.isBytePath(path) ? path : undefined)
71
+ });
72
+ /** @internal */
73
+ Errors.OpContext = {
74
+ make: (operation) => makeOpContext(operation)
75
+ };
76
+ })(Errors || (Errors = {}));
77
+ const { argumentFailure, decodeConfiguration, errorPath, fsFailure, OpContext } = Errors;
78
+ /** Byte-name rules and filesystem lookup preparation. @internal */
79
+ export var Paths;
80
+ (function (Paths) {
81
+ /** @internal */
82
+ Paths.SLASH_HEX = "2f";
83
+ /** @internal */
84
+ Paths.DOT_HEX = "2e";
85
+ /** @internal */
86
+ Paths.DOT_DOT_HEX = "2e2e";
87
+ /** @internal */
88
+ Paths.SLASH_BYTE = 47;
89
+ const DOT_BYTE = 46;
90
+ /** @internal */
91
+ Paths.NUL_BYTE = 0;
92
+ /** @internal */
93
+ Paths.MAX_SYMLINK_TRAVERSALS = 40;
94
+ /** @internal */
95
+ Paths.MAX_NAME_BYTES = 255;
96
+ /** @internal */
97
+ Paths.isNameBytes = (name) => name.length >= 1 && name.length <= Paths.MAX_NAME_BYTES && !name.includes(Paths.NUL_BYTE) && !name.includes(Paths.SLASH_BYTE) &&
98
+ !(name.length === 1 && name[0] === DOT_BYTE) && !(name.length === 2 && name[0] === DOT_BYTE && name[1] === DOT_BYTE);
99
+ /** @internal */
100
+ Paths.ROOT_PATH = new Uint8Array([Paths.SLASH_BYTE]);
101
+ /** @internal */
102
+ Paths.joinPath = (prefix, name) => {
103
+ const separator = prefix.length === 0 || prefix[prefix.length - 1] === Paths.SLASH_BYTE ? 0 : 1;
104
+ const joined = new Uint8Array(prefix.length + separator + name.length);
105
+ joined.set(prefix);
106
+ if (separator === 1)
107
+ joined[prefix.length] = Paths.SLASH_BYTE;
108
+ joined.set(name, prefix.length + separator);
109
+ return joined;
110
+ };
111
+ // A missing final component follows the same rejection path as "." and ".."; callers choose the POSIX error code.
112
+ /** @internal */
113
+ Paths.isDotComponent = (name) => name === undefined || name === Paths.DOT_HEX || name === Paths.DOT_DOT_HEX;
114
+ /** @internal */
115
+ Paths.ownedPath = (bytes) => BytePathStorage.make(bytes);
116
+ /** @internal */
117
+ Paths.strictString = (bytes, operation) => Effect.try({
118
+ try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes),
119
+ catch: () => fsFailure("UnrepresentableName", operation)
120
+ });
121
+ /** @internal */
122
+ Paths.nameBytes = (name) => Result.getOrThrow(Hex.decode(name));
123
+ // Constructing a view detects detached buffers, including empty ones.
124
+ const attachedBuffer = (bytes) => {
125
+ try {
126
+ const probe = new Uint8Array(bytes.buffer, bytes.byteOffset, 0);
127
+ return probe.byteLength === 0;
128
+ }
129
+ catch (error) {
130
+ if (error instanceof TypeError)
131
+ return false;
132
+ throw error;
133
+ }
134
+ };
135
+ // Reject shared or detached buffers before copying; Buffer subclasses remain valid.
136
+ /** @internal */
137
+ Paths.isAttachedBytes = (bytes) => Predicate.isUint8Array(bytes) && bytes.buffer instanceof ArrayBuffer && attachedBuffer(bytes);
138
+ /** @internal */
139
+ Paths.pathFromBytes = Effect.fnUntraced(function* (bytes) {
140
+ if (!Paths.isAttachedBytes(bytes))
141
+ return yield* fsFailure("InvalidArgument", "pathFromBytes");
142
+ const owned = new Uint8Array(bytes);
143
+ if (owned.length === 0 || owned.includes(0)) {
144
+ return yield* fsFailure("InvalidArgument", "pathFromBytes");
145
+ }
146
+ return BytePathStorage.make(owned);
147
+ });
148
+ /** @internal */
149
+ Paths.pathToBytes = Effect.fnUntraced(function* (path) {
150
+ const bytes = BytePathStorage.getBytes(path);
151
+ if (bytes === undefined)
152
+ return yield* fsFailure("InvalidArgument", "pathToBytes");
153
+ return new Uint8Array(bytes);
154
+ });
155
+ // String.prototype.isWellFormed requires ES2024; this package targets ES2023.
156
+ /** @internal */
157
+ Paths.isWellFormed = (value) => {
158
+ for (let index = 0; index < value.length; index++) {
159
+ const code = value.charCodeAt(index);
160
+ if (code >= 0xd800 && code <= 0xdbff) {
161
+ const next = value.charCodeAt(++index);
162
+ if (!(next >= 0xdc00 && next <= 0xdfff))
163
+ return false;
164
+ }
165
+ else if (code >= 0xdc00 && code <= 0xdfff)
166
+ return false;
167
+ }
168
+ return true;
169
+ };
170
+ const UTF8_ENCODER = new TextEncoder();
171
+ /** @internal */
172
+ Paths.inputBytes = (input) => Predicate.isString(input)
173
+ ? Paths.isWellFormed(input) ? Result.succeed(UTF8_ENCODER.encode(input)) : Result.fail("InvalidPathEncoding")
174
+ : Result.fromNullishOr(BytePathStorage.getBytes(input), () => "InvalidArgument");
175
+ /** @internal */
176
+ Paths.preparePath = (input, operation, maxPathBytes) => {
177
+ const encoded = Paths.inputBytes(input);
178
+ if (Result.isFailure(encoded)) {
179
+ return Result.fail(encoded.failure === "InvalidPathEncoding"
180
+ ? fsFailure("InvalidPathEncoding", operation, { path: input })
181
+ : fsFailure("InvalidArgument", operation));
182
+ }
183
+ const bytes = encoded.success;
184
+ if (bytes.length === 0)
185
+ return Result.fail(fsFailure("NotFound", operation, { path: input }));
186
+ if (bytes.includes(0))
187
+ return Result.fail(fsFailure("InvalidArgument", operation, { path: input }));
188
+ if (maxPathBytes !== undefined && ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxPathBytes)) {
189
+ return Result.fail(fsFailure("PathTooLong", operation, { path: input }));
190
+ }
191
+ const components = [];
192
+ const suffixes = [];
193
+ let start = 0;
194
+ for (let index = 0; index <= bytes.length; index++) {
195
+ if (index !== bytes.length && bytes[index] !== Paths.SLASH_BYTE)
196
+ continue;
197
+ if (index > start) {
198
+ if (index - start > Paths.MAX_NAME_BYTES) {
199
+ return Result.fail(fsFailure("PathTooLong", operation, { path: input }));
200
+ }
201
+ components.push(Hex.encode(bytes.subarray(start, index)));
202
+ suffixes.push(bytes.subarray(index));
203
+ }
204
+ start = index + 1;
205
+ }
206
+ return Result.succeed({
207
+ input,
208
+ absolute: bytes[0] === Paths.SLASH_BYTE,
209
+ trailingSlash: bytes.at(-1) === Paths.SLASH_BYTE,
210
+ bytes,
211
+ suffixes,
212
+ components
213
+ });
214
+ };
215
+ })(Paths || (Paths = {}));
216
+ const { DOT_DOT_HEX, DOT_HEX, inputBytes, isAttachedBytes, isDotComponent, isWellFormed, joinPath, MAX_NAME_BYTES, MAX_SYMLINK_TRAVERSALS, nameBytes, ownedPath, preparePath, SLASH_BYTE, SLASH_HEX } = Paths;
217
+ /**
218
+ * Read in the caller's fiber, so a test supplies it with `withVolumeTestSeams` on the call under test.
219
+ *
220
+ * @internal
221
+ */
222
+ export const VolumeTestSeams = Context.Reference("@effect-vfs/core/VolumeTestSeams", {
223
+ defaultValue: () => ({
224
+ afterSubscribe: Effect.void,
225
+ betweenSnapshotAndSummary: Effect.void,
226
+ beforeTreeRemoval: Effect.void
227
+ })
228
+ });
229
+ /** @internal */
230
+ export const withVolumeTestSeams = (seams) => Effect.updateService(VolumeTestSeams, (current) => ({
231
+ afterSubscribe: seams.afterSubscribe ?? current.afterSubscribe,
232
+ betweenSnapshotAndSummary: seams.betweenSnapshotAndSummary ?? current.betweenSnapshotAndSummary,
233
+ beforeTreeRemoval: seams.beforeTreeRemoval ?? current.beforeTreeRemoval
234
+ }));
235
+ // A Semaphore can let a newcomer overtake a scheduled waiter. Transfer ownership before scheduling the oldest waiter.
236
+ /** @internal */
237
+ const makeTurnstile = () => {
238
+ let held = false;
239
+ const waiting = [];
240
+ const release = () => {
241
+ const next = waiting.shift();
242
+ if (next === undefined) {
243
+ held = false;
244
+ return;
245
+ }
246
+ next.granted = true;
247
+ next.dispatcher.scheduleTask(() => next.wake?.(), 0);
248
+ };
249
+ const waitFor = (ticket) => Effect.callback((resume) => {
250
+ if (ticket.granted)
251
+ return resume(Effect.void);
252
+ ticket.wake = () => resume(Effect.void);
253
+ });
254
+ return {
255
+ // Check ticket ownership after interruption so a granted turn is released rather than stranded.
256
+ withTurn: (effect) => Effect.uninterruptibleMask((restore) => Effect.withFiber((fiber) => {
257
+ const run = Effect.ensuring(restore(effect), Effect.sync(release));
258
+ if (!held) {
259
+ held = true;
260
+ return run;
261
+ }
262
+ const ticket = { dispatcher: fiber.currentDispatcher, granted: false, wake: undefined };
263
+ waiting.push(ticket);
264
+ return Effect.flatMap(Effect.exit(restore(waitFor(ticket))), (waited) => {
265
+ if (Exit.isSuccess(waited))
266
+ return run;
267
+ if (ticket.granted)
268
+ release();
269
+ else
270
+ waiting.splice(waiting.indexOf(ticket), 1);
271
+ return Effect.failCause(waited.cause);
272
+ });
273
+ }))
274
+ };
275
+ };
276
+ /** Private addressing; the public Target module is a compatibility view. @internal */
277
+ export var Address;
278
+ (function (Address) {
279
+ /**
280
+ * How a caller names what it operates on. A `Target` is a path, an object
281
+ * reference, or an open handle; an `Entry` is a child of a directory target
282
+ * called `name`. Every verb takes one or the other, so the three addressing
283
+ * modes share one body and one option vocabulary.
284
+ *
285
+ * @since 0.6.0
286
+ */
287
+ const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value) && value[FileHandleId] === true;
288
+ const isDirectoryHandle = (value) => Predicate.hasProperty(DirectoryHandleId)(value) && value[DirectoryHandleId] === true;
289
+ const enumeration = Data.taggedEnum();
290
+ /**
291
+ * Whether a value is a `Target`.
292
+ *
293
+ * @category predicates
294
+ * @since 0.6.0
295
+ */
296
+ Address.isTarget = (value) => Predicate.isTagged("Path")(value) || Predicate.isTagged("Reference")(value) || Predicate.isTagged("Handle")(value);
297
+ /**
298
+ * Constructors and matchers for `Target`, plus `of`, which wraps a bare path,
299
+ * reference, or handle.
300
+ *
301
+ * @category constructors
302
+ * @since 0.6.0
303
+ */
304
+ Address.Target = {
305
+ Path: enumeration.Path,
306
+ Reference: enumeration.Reference,
307
+ Handle: enumeration.Handle,
308
+ $is: enumeration.$is,
309
+ $match: enumeration.$match,
310
+ // A string or byte path is a path and a handle is a handle; anything else is taken for a reference, so a forged
311
+ // token reaches the registry and is reported as one.
312
+ of: (input) => {
313
+ if (Address.isTarget(input))
314
+ return input;
315
+ if (isFileHandle(input) || isDirectoryHandle(input))
316
+ return enumeration.Handle({ handle: input });
317
+ if (Predicate.isString(input) || BytePathStorage.isBytePath(input))
318
+ return enumeration.Path({ path: input });
319
+ return enumeration.Reference({ reference: input });
320
+ }
321
+ };
322
+ /**
323
+ * Schema for a well-formed entry name, as bytes.
324
+ *
325
+ * @category schemas
326
+ * @since 0.6.0
327
+ */
328
+ Address.Name = Schema.Uint8Array.check(Schema.makeFilter((bytes) => bytes.length === 0
329
+ ? "must not be empty"
330
+ : bytes.length > 255
331
+ ? "must be at most 255 bytes"
332
+ : bytes.includes(0)
333
+ ? "must not contain NUL"
334
+ : bytes.includes(0x2f)
335
+ ? "must not contain a slash"
336
+ : undefined));
337
+ /**
338
+ * Whether a value is an `Entry`.
339
+ *
340
+ * @category predicates
341
+ * @since 0.6.0
342
+ */
343
+ Address.isEntry = (value) => Predicate.hasProperty(value, "directory") && Predicate.hasProperty(value, "name") && Address.isTarget(value.directory);
344
+ /**
345
+ * Names a child of a directory. The directory may be given bare.
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * import { Entry, Target } from "@effect-vfs/core/Target"
350
+ *
351
+ * const explicit = Entry(Target.Path({ path: "/var" }), "log")
352
+ * const bare = Entry("/var", "log")
353
+ * ```
354
+ *
355
+ * @category constructors
356
+ * @since 0.6.0
357
+ */
358
+ Address.Entry = dual(2, (directory, name) => ({ directory: Address.Target.of(directory), name }));
359
+ /**
360
+ * Whether a path input is a byte path rather than a string.
361
+ *
362
+ * @category predicates
363
+ * @since 0.6.0
364
+ */
365
+ Address.isBytePathInput = (input) => BytePathStorage.isBytePath(input);
366
+ })(Address || (Address = {}));
367
+ const isTarget = Address.isTarget;
368
+ const Target = Address.Target;
369
+ const isEntry = Address.isEntry;
370
+ /**
371
+ * Brand key that marks an object reference.
372
+ *
373
+ * @category type IDs
374
+ * @since 0.6.0
375
+ */
376
+ export const ObjectReferenceId = Symbol.for("@effect-vfs/core/ObjectReference");
377
+ /**
378
+ * Schema for a caller's credentials. `privileged` is explicit; uid zero alone
379
+ * grants nothing.
380
+ *
381
+ * @category schemas
382
+ * @since 0.6.0
383
+ */
384
+ export const Identity = Schema.Struct({
385
+ uid: Schema.Natural,
386
+ gid: Schema.Natural,
387
+ groups: Schema.Array(Schema.Natural),
388
+ privileged: Schema.Boolean
389
+ });
390
+ /**
391
+ * Schema for the options of a root caller.
392
+ *
393
+ * @category schemas
394
+ * @since 0.6.0
395
+ */
396
+ export const RootCallerOptions = Schema.Struct({
397
+ identity: Schema.optionalKey(Identity),
398
+ umask: Schema.optionalKey(Schema.Natural.check(Schema.isLessThanOrEqualTo(0o777)))
399
+ });
400
+ /**
401
+ * Schema for the options of `open` on a target: the access mode, whether a
402
+ * missing file is created, the creation mode, and append and truncate flags.
403
+ *
404
+ * @category schemas
405
+ * @since 0.6.0
406
+ */
407
+ export const OpenOptions = Schema.Struct({
408
+ access: Schema.Literals(["read", "write", "readWrite"]),
409
+ create: Schema.optionalKey(Schema.Literals(["never", "ifMissing", "exclusive"])),
410
+ mode: Schema.optionalKey(Mode),
411
+ append: Schema.optionalKey(Schema.Boolean),
412
+ truncate: Schema.optionalKey(Schema.Boolean)
413
+ });
414
+ /**
415
+ * Schema for the options of `writeFile`: the open options plus whether a
416
+ * final symbolic link is replaced or followed and the mode the file ends with.
417
+ *
418
+ * @category schemas
419
+ * @since 0.6.0
420
+ */
421
+ export const WriteFileOptions = Schema.Struct({
422
+ ...OpenOptions.fields,
423
+ followFinalSymlink: Schema.optionalKey(Schema.Boolean),
424
+ replaceFinalSymlink: Schema.optionalKey(Schema.Boolean),
425
+ finalMode: Schema.optionalKey(Mode)
426
+ });
427
+ /**
428
+ * Schema for a directory's revision before and after a change.
429
+ *
430
+ * @category schemas
431
+ * @since 0.6.0
432
+ */
433
+ export const DirectoryChange = Schema.Struct({
434
+ before: Schema.BigInt,
435
+ after: Schema.BigInt
436
+ });
437
+ /**
438
+ * Schema for the result of `rename`.
439
+ *
440
+ * @category schemas
441
+ * @since 0.6.0
442
+ */
443
+ export const RenameReferenceResult = Schema.TaggedUnion({
444
+ SameDirectory: { directory: DirectoryChange },
445
+ DifferentDirectories: {
446
+ sourceDirectory: DirectoryChange,
447
+ destinationDirectory: DirectoryChange
448
+ }
449
+ });
450
+ /**
451
+ * Schema for the options of `mkdir`. `exactMode` skips the caller's umask for
452
+ * an explicit creation mode; permission policy still applies. `recursive`
453
+ * creates every missing directory on the path in one change, as `mkdir -p`
454
+ * does, and succeeds when the final directory already exists; the mode
455
+ * applies to every directory it creates and the times to the final one.
456
+ *
457
+ * @category schemas
458
+ * @since 0.6.0
459
+ */
460
+ export const MkdirOptions = Schema.Struct({
461
+ mode: Schema.optionalKey(Mode),
462
+ exactMode: Schema.optionalKey(Schema.Boolean),
463
+ times: Schema.optionalKey(Times),
464
+ recursive: Schema.optionalKey(Schema.Boolean)
465
+ });
466
+ /**
467
+ * Schema for the options of `walk`: the order entries arrive in and the
468
+ * optional bounds on how deep, how many, and how many bytes the walk may
469
+ * reach. `order` defaults to `"pre"`, each directory before its entries; a
470
+ * `"post"` walk reports each directory after them. Every bound defaults to
471
+ * unbounded, and a walk that would pass one fails with `LimitExceeded` naming
472
+ * the bound in `field`.
473
+ *
474
+ * @category schemas
475
+ * @since 0.6.0
476
+ */
477
+ export const WalkOptions = Schema.Struct({
478
+ order: Schema.optionalKey(Schema.Literals(["pre", "post"])),
479
+ maxDepth: Schema.optionalKey(Schema.Natural),
480
+ maxEntries: Schema.optionalKey(Schema.Natural),
481
+ maxBytes: Schema.optionalKey(Schema.ByteSize)
482
+ });
483
+ /**
484
+ * Schema for the options of `remove`. `recursive` removes a directory and
485
+ * everything under it; `force` succeeds when the target itself is missing.
486
+ * `expected` pins object identity at each coordinated removal stage. A replacement fails `VolumeBusy`.
487
+ *
488
+ * @category schemas
489
+ * @since 0.6.0
490
+ */
491
+ export const RemoveOptions = Schema.Struct({
492
+ recursive: Schema.optionalKey(Schema.Boolean),
493
+ force: Schema.optionalKey(Schema.Boolean),
494
+ expected: Schema.optionalKey(Schema.suspend(() => ObjectReferenceSchema))
495
+ });
496
+ /**
497
+ * Schema for the options of `symlink`.
498
+ *
499
+ * @category schemas
500
+ * @since 0.6.0
501
+ */
502
+ export const SymlinkOptions = Schema.Struct({
503
+ times: Schema.optionalKey(Times)
504
+ });
505
+ /**
506
+ * Schema for the attributes `setattr` changes together: a regular file's
507
+ * size, the permission mode, the owner, and the access and modification
508
+ * times. An omitted attribute keeps its current value. `expected` pins the
509
+ * target's revision the caller last observed, checked inside the same change,
510
+ * and fails as `StaleReference` naming `expected` when the target has moved
511
+ * on, so a change decided from that observation never applies to a newer one.
512
+ *
513
+ * @category schemas
514
+ * @since 0.6.0
515
+ */
516
+ export const SetattrOptions = Schema.Struct({
517
+ size: Schema.optionalKey(Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n))),
518
+ mode: Schema.optionalKey(Mode),
519
+ owner: Schema.optionalKey(OwnerUpdate),
520
+ times: Schema.optionalKey(Times),
521
+ expected: Schema.optionalKey(Schema.Struct({ revision: Schema.BigInt }))
522
+ });
523
+ /**
524
+ * Schema for an object reference: any value carrying the reference brand. Whether the volume issued it is checked
525
+ * where it is used.
526
+ *
527
+ * @internal
528
+ */
529
+ export const ObjectReferenceSchema = Schema.declare((input) => Predicate.hasProperty(ObjectReferenceId)(input) && input[ObjectReferenceId] === true);
530
+ /**
531
+ * Schema for the options of `open` on an entry, a lookup-or-create-and-open in
532
+ * one gate hold. `expected` pins the child the caller last observed under the
533
+ * name (or `null` for none) and fails as `VolumeBusy` when another has
534
+ * appeared; `expectedChild` pins its revision and times as well and fails as
535
+ * `StaleReference` when they moved.
536
+ *
537
+ * @category schemas
538
+ * @since 0.6.0
539
+ */
540
+ export const OpenEntryOptions = Schema.Struct({
541
+ ...OpenOptions.fields,
542
+ followFinalSymlink: Schema.optionalKey(Schema.Boolean),
543
+ times: Schema.optionalKey(Times),
544
+ initialSize: Schema.optionalKey(Schema.BigInt),
545
+ exactMode: Schema.optionalKey(Schema.Boolean),
546
+ owner: Schema.optionalKey(OwnerUpdate),
547
+ expected: Schema.optionalKey(Schema.NullOr(ObjectReferenceSchema)),
548
+ expectedChild: Schema.optionalKey(Schema.NullOr(Schema.Struct({
549
+ reference: ObjectReferenceSchema,
550
+ revision: Schema.BigInt,
551
+ atimeNs: Timestamp,
552
+ mtimeNs: Timestamp
553
+ })))
554
+ });
555
+ /** State shared with community codecs through the legacy volumeState view. @internal */
556
+ export var State;
557
+ (function (State) {
558
+ /** @internal */
559
+ State.Ino = Brand.nominal();
560
+ /** @internal */
561
+ State.ROOT_INO = State.Ino(1);
562
+ // The largest inode number a value holds. The allocator resumes one above it and must stay exactly representable,
563
+ // since the inode table is keyed by a number.
564
+ /** @internal */
565
+ State.MAX_INO = Number.MAX_SAFE_INTEGER - 1;
566
+ // File sizes fit an unsigned 32-bit count in the volume and its public options.
567
+ /** @internal */
568
+ State.MAX_FILE_BYTES = 0xffffffff;
569
+ // Nodes walked between yields. Whole-tree reads are one synchronous tick otherwise, which
570
+ // starves the event loop and leaves nothing for interruption to act on.
571
+ /** @internal */
572
+ State.WALK_YIELD_INTERVAL = 128;
573
+ /** @internal */
574
+ State.getNode = (state, ino) => Option.getOrUndefined(HashMap.get(state.inodes, ino));
575
+ // Only filesystem-owned records enter this boundary. Identity equality keeps a metadata update from hashing
576
+ // shared file bytes or directory contents; nested payloads and public metadata retain their value semantics.
577
+ /** @internal */
578
+ State.setNode = (inodes, node) => HashMap.set(inodes, node.ino, Equal.byReferenceUnsafe(node));
579
+ /** @internal */
580
+ State.byIno = Order.mapInput(Order.Number, (node) => node.ino);
581
+ /** @internal */
582
+ State.byEntryName = Order.mapInput(Order.String, ([name]) => name);
583
+ const inNameOrder = (entries) => {
584
+ let previous;
585
+ for (const name of entries.keys()) {
586
+ if (previous !== undefined && previous > name)
587
+ return false;
588
+ previous = name;
589
+ }
590
+ return true;
591
+ };
592
+ State.inEntryOrder = (directory) => inNameOrder(directory.entries)
593
+ ? directory
594
+ : { ...directory, entries: new Map([...directory.entries].sort(State.byEntryName)) };
595
+ /** @internal */
596
+ State.payloadOf = (node) => node.kind === "file" ? node.data : node.kind === "symlink" ? node.target : undefined;
597
+ /** @internal */
598
+ State.directoryMetadata = (ino, uid, gid, mode, now) => ({
599
+ kind: "directory",
600
+ ino,
601
+ uid,
602
+ gid,
603
+ mode,
604
+ nlink: 2,
605
+ size: 0n,
606
+ atimeNs: now,
607
+ mtimeNs: now,
608
+ ctimeNs: now,
609
+ birthtimeNs: now
610
+ });
611
+ /** @internal */
612
+ State.pathOf = (get, ino) => {
613
+ const names = [];
614
+ let node = get(ino);
615
+ if (node?.kind !== "directory" || node.metadata.nlink === 0)
616
+ return undefined;
617
+ while (node.ino !== State.ROOT_INO) {
618
+ names.push(node.name);
619
+ const parent = get(node.parent);
620
+ if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
621
+ return undefined;
622
+ node = parent;
623
+ }
624
+ return SLASH_HEX + names.reverse().join(SLASH_HEX);
625
+ };
626
+ /** @internal */
627
+ State.entryPath = (prefix, name) => ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name));
628
+ /** @internal */
629
+ State.emptyRoot = (now) => ({
630
+ kind: "directory",
631
+ ino: State.ROOT_INO,
632
+ parent: State.ROOT_INO,
633
+ name: "",
634
+ entries: new Map(),
635
+ metadata: State.directoryMetadata(BigInt(State.ROOT_INO), 0, 0, 0o755, now),
636
+ revision: 1n
637
+ });
638
+ /** @internal */
639
+ State.emptyState = (now) => ({
640
+ inodes: State.setNode(HashMap.empty(), State.emptyRoot(now)),
641
+ open: new Map(),
642
+ nextInode: State.Ino(2),
643
+ revision: 1n,
644
+ entries: 0,
645
+ usedBytes: 0n
646
+ });
647
+ })(State || (State = {}));
648
+ const Ino = State.Ino;
649
+ const ROOT_INO = State.ROOT_INO;
650
+ const MAX_FILE_BYTES = State.MAX_FILE_BYTES;
651
+ const WALK_YIELD_INTERVAL = State.WALK_YIELD_INTERVAL;
652
+ const { entryPath, pathOf } = State;
653
+ const getNode = State.getNode;
654
+ const setNode = State.setNode;
655
+ const byEntryName = State.byEntryName;
656
+ const directoryMetadata = State.directoryMetadata;
657
+ const emptyState = State.emptyState;
658
+ /** Persistent name/slot representation belongs to this engine. @internal */
659
+ var DirectoryEntries;
660
+ (function (DirectoryEntries_1) {
661
+ // Name lookup and insertion order share immutable trees. Deleted slots are compacted once they outnumber live entries.
662
+ class DirectoryEntries {
663
+ names;
664
+ slots;
665
+ length;
666
+ [Symbol.toStringTag] = "Map";
667
+ constructor(names, slots, length) {
668
+ this.names = names;
669
+ this.slots = slots;
670
+ this.length = length;
671
+ }
672
+ get size() {
673
+ return HashMap.size(this.names);
674
+ }
675
+ get(name) {
676
+ const slot = HashMap.get(this.names, name);
677
+ return Option.isSome(slot) ? slot.value.ino : undefined;
678
+ }
679
+ has(name) {
680
+ return HashMap.has(this.names, name);
681
+ }
682
+ *entries() {
683
+ for (let index = 0; index < this.length; index++) {
684
+ const entry = Option.getOrUndefined(HashMap.get(this.slots, index));
685
+ if (entry !== undefined)
686
+ yield [entry.name, entry.ino];
687
+ }
688
+ }
689
+ *keys() {
690
+ for (const [name] of this)
691
+ yield name;
692
+ }
693
+ *values() {
694
+ for (const [, ino] of this)
695
+ yield ino;
696
+ }
697
+ [Symbol.iterator]() {
698
+ return this.entries();
699
+ }
700
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- ReadonlyMap forwards the opaque callback receiver without inspecting it.
701
+ forEach(callback, thisArg) {
702
+ for (const [name, ino] of this)
703
+ callback.call(thisArg, ino, name, this);
704
+ }
705
+ }
706
+ const fromEntries = (entries) => {
707
+ const slots = HashMap.beginMutation(HashMap.empty());
708
+ let length = 0;
709
+ const names = HashMap.mutate(HashMap.empty(), (mutable) => {
710
+ for (const [name, ino] of entries) {
711
+ const entry = { name, ino, index: length };
712
+ HashMap.set(mutable, name, entry);
713
+ HashMap.set(slots, length, entry);
714
+ length++;
715
+ }
716
+ });
717
+ return new DirectoryEntries(names, HashMap.endMutation(slots), length);
718
+ };
719
+ /** @internal */
720
+ DirectoryEntries_1.set = (entries, name, ino) => {
721
+ const current = entries instanceof DirectoryEntries ? entries : fromEntries(entries);
722
+ const existing = HashMap.get(current.names, name);
723
+ const index = Option.isSome(existing) ? existing.value.index : current.length;
724
+ const entry = { name, ino, index };
725
+ return new DirectoryEntries(HashMap.set(current.names, name, entry), HashMap.set(current.slots, index, entry), Option.isSome(existing) ? current.length : current.length + 1);
726
+ };
727
+ /** @internal */
728
+ DirectoryEntries_1.remove = (entries, name) => {
729
+ const current = entries instanceof DirectoryEntries ? entries : fromEntries(entries);
730
+ const existing = HashMap.get(current.names, name);
731
+ if (Option.isNone(existing))
732
+ return current;
733
+ const removed = new DirectoryEntries(HashMap.remove(current.names, name), HashMap.remove(current.slots, existing.value.index), current.length);
734
+ return removed.length > removed.size * 2 ? fromEntries(removed) : removed;
735
+ };
736
+ // Full directory observations already materialize every entry. Populate indices once instead of hashing every
737
+ // slot lookup; lazy iterators above keep their incremental traversal for bounded snapshot/search consumers.
738
+ /** @internal */
739
+ DirectoryEntries_1.toArray = (entries) => {
740
+ // Larger logical indices are not array elements; retain the incremental fallback for that domain.
741
+ if (!(entries instanceof DirectoryEntries) || entries.length > 0xffffffff)
742
+ return Array.from(entries);
743
+ const ordered = [];
744
+ HashMap.forEach(entries.slots, (entry) => {
745
+ ordered[entry.index] = [entry.name, entry.ino];
746
+ });
747
+ return ordered.filter((entry) => entry !== undefined);
748
+ };
749
+ })(DirectoryEntries || (DirectoryEntries = {}));
750
+ /** @internal */
751
+ export var TokenRegistry;
752
+ (function (TokenRegistry) {
753
+ // Shared across volumes so a genuine token from another registry is distinguishable from a forgery.
754
+ const references = new WeakMap();
755
+ const files = new WeakMap();
756
+ const directories = new WeakMap();
757
+ /** @internal */
758
+ TokenRegistry.inode = (reference) => reference.lifecycle.kind === "open" || reference.lifecycle.kind === "releasedPendingCommit"
759
+ ? reference.lifecycle.ino
760
+ : undefined;
761
+ /** @internal */
762
+ TokenRegistry.isOpen = (reference) => reference.lifecycle.kind === "open";
763
+ /** @internal */
764
+ TokenRegistry.publish = (reference, ino) => {
765
+ reference.lifecycle = reference.lifecycle.kind === "releasedPendingCommit" || reference.lifecycle.kind === "closed"
766
+ ? { kind: "releasedPendingCommit", ino }
767
+ : { kind: "open", ino };
768
+ };
769
+ /** @internal */
770
+ TokenRegistry.released = (reference) => {
771
+ reference.lifecycle = reference.lifecycle.kind === "acquiring" ||
772
+ (reference.lifecycle.kind === "releasedPendingCommit" && reference.lifecycle.ino === undefined)
773
+ ? { kind: "releasedPendingCommit", ino: undefined }
774
+ : { kind: "closed" };
775
+ };
776
+ // A release callback changes the engine's draft; publication may be immediate cleanup or after installation.
777
+ /** @internal */
778
+ TokenRegistry.release = Effect.fnUntraced(function* (reference, releaseInode, afterRelease) {
779
+ const ino = TokenRegistry.inode(reference);
780
+ yield* afterRelease(() => TokenRegistry.released(reference));
781
+ if (ino !== undefined)
782
+ yield* releaseInode(ino);
783
+ });
784
+ /** @internal */
785
+ TokenRegistry.closeReleasedScope = (reference) => Effect.suspend(() => TokenRegistry.isOpen(reference) || reference.scope === undefined ? Effect.void : Scope.close(reference.scope, Exit.void));
786
+ // The inode, rather than the open tag, decides whether a rerun must release a late publication.
787
+ /** @internal */
788
+ TokenRegistry.finalize = (reference, close, fallback) => Effect.uninterruptible(Effect.suspend(() => {
789
+ if (TokenRegistry.inode(reference) === undefined) {
790
+ // Check and mark together: acquisition can publish between separate Effect steps.
791
+ TokenRegistry.released(reference);
792
+ return Effect.void;
793
+ }
794
+ return Effect.ignore(close).pipe(Effect.andThen(Effect.suspend(() => TokenRegistry.inode(reference) === undefined ? Effect.void : fallback)));
795
+ }));
796
+ // Register before waiting: a closed scope runs its finalizer immediately, and engine permits are not reentrant.
797
+ /** @internal */
798
+ TokenRegistry.acquire = Effect.fnUntraced(function* (reference, coordinate, acquisition, releaseAcquired, finalizer) {
799
+ const scope = yield* Scope.fork(yield* Effect.scope);
800
+ reference.scope = scope;
801
+ yield* Scope.addFinalizer(scope, finalizer);
802
+ const closed = () => Predicate.isTagged(scope.state, "Closed");
803
+ return yield* coordinate(Effect.suspend(() => closed() ? Effect.interrupt : acquisition).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.andThen(releaseAcquired(), Effect.interrupt) : Effect.void)))).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.interrupt : Effect.void)), Effect.onError(() => Effect.andThen(finalizer, Scope.close(scope, Exit.void))), Effect.onError(() => Effect.sync(() => {
804
+ if (reference.lifecycle.kind === "releasedPendingCommit" && reference.lifecycle.ino === undefined) {
805
+ reference.lifecycle = { kind: "closed" };
806
+ }
807
+ })));
808
+ });
809
+ /** @internal */
810
+ TokenRegistry.nodeOrFail = (result, addressing, op) => {
811
+ switch (result.kind) {
812
+ case "live":
813
+ return Effect.succeed(result.node);
814
+ case "unknown":
815
+ return Effect.fail(op.fail(addressing === "reference" ? "InvalidReference" : "InvalidHandle"));
816
+ case "foreign":
817
+ return Effect.fail(op.fail(addressing === "reference" ? "ForeignReference" : "ForeignHandle"));
818
+ case "stale":
819
+ return Effect.fail(op.fail(addressing === "reference" ? "StaleReference" : "InvalidHandle"));
820
+ }
821
+ };
822
+ /** @internal */
823
+ TokenRegistry.make = (volume, get) => {
824
+ const tokens = new Map();
825
+ const resolveInode = (ino) => Effect.map(get(ino), (node) => node === undefined || (node.kind === "directory" && node.metadata.nlink === 0)
826
+ ? { kind: "stale" }
827
+ : { kind: "live", node });
828
+ const resolveReference = (token) => Effect.suspend(() => {
829
+ const known = Predicate.isObject(token) ? references.get(token) : undefined;
830
+ if (known === undefined)
831
+ return Effect.succeed({ kind: "unknown" });
832
+ if (known.volume !== volume)
833
+ return Effect.succeed({ kind: "foreign" });
834
+ return resolveInode(known.ino);
835
+ });
836
+ const resolveHandle = (token, directoryOnly = false) => Effect.suspend(() => {
837
+ const known = Predicate.isObject(token)
838
+ ? directories.get(token) ?? (directoryOnly ? undefined : files.get(token))
839
+ : undefined;
840
+ if (known === undefined)
841
+ return Effect.succeed({ kind: "unknown" });
842
+ if (known.volume !== volume)
843
+ return Effect.succeed({ kind: "foreign" });
844
+ if (!TokenRegistry.isOpen(known))
845
+ return Effect.succeed({ kind: "stale" });
846
+ const ino = TokenRegistry.inode(known);
847
+ return ino === undefined
848
+ ? Effect.succeed({ kind: "stale" })
849
+ : Effect.map(get(ino), (node) => node === undefined ? { kind: "stale" } : { kind: "live", node });
850
+ });
851
+ return {
852
+ referenceIdentity: (token, op) => {
853
+ const known = Predicate.isObject(token) ? references.get(token) : undefined;
854
+ if (known === undefined)
855
+ return Effect.fail(op.fail("InvalidReference"));
856
+ if (known.volume !== volume)
857
+ return Effect.fail(op.fail("ForeignReference"));
858
+ return Effect.succeed(known.ino);
859
+ },
860
+ handleBoundaries: (token) => (files.get(token) ?? directories.get(token))?.boundaries ?? [],
861
+ authorizeHandle: (token, node, op) => {
862
+ const reference = files.get(token) ?? directories.get(token);
863
+ return reference?.authority?.(node, op) ?? Effect.void;
864
+ },
865
+ resolve: (token, addressing) => addressing === "reference" ? resolveReference(token) : resolveHandle(token, addressing === "directory"),
866
+ resolveInode,
867
+ referenceFor: (ino) => {
868
+ const existing = tokens.get(ino);
869
+ if (existing !== undefined)
870
+ return existing;
871
+ const reference = Object.freeze({ [ObjectReferenceId]: true });
872
+ references.set(reference, { volume, ino });
873
+ tokens.set(ino, reference);
874
+ return reference;
875
+ },
876
+ forget: (ino) => tokens.delete(ino),
877
+ file: (access, append) => ({
878
+ volume,
879
+ lifecycle: { kind: "acquiring" },
880
+ scope: undefined,
881
+ offset: 0n,
882
+ access,
883
+ append
884
+ }),
885
+ directory: (ino) => ({
886
+ volume,
887
+ lifecycle: ino === undefined ? { kind: "acquiring" } : { kind: "open", ino },
888
+ scope: undefined
889
+ }),
890
+ registerFile: (handle, reference) => files.set(handle, reference),
891
+ registerDirectory: (handle, reference) => directories.set(handle, reference)
892
+ };
893
+ };
894
+ })(TokenRegistry || (TokenRegistry = {}));
895
+ // Only engine-owned sections set this reference; cooperative callers keep explicit walk yields.
896
+ const SyncSection = Context.Reference("@effect-vfs/core/SyncSection", { defaultValue: () => false });
897
+ /** @internal */
898
+ var Confinement;
899
+ (function (Confinement) {
900
+ /** @internal */
901
+ Confinement.make = (view, roots) => {
902
+ const descends = Effect.fnUntraced(function* (get, ino, root) {
903
+ let node = get(ino);
904
+ let steps = 0;
905
+ while (node?.kind === "directory" && node.metadata.nlink > 0) {
906
+ if (node.ino === root)
907
+ return true;
908
+ if (node.ino === ROOT_INO)
909
+ return false;
910
+ const parent = get(node.parent);
911
+ if (parent?.kind !== "directory" || parent.entries.get(node.name) !== node.ino)
912
+ return false;
913
+ node = parent;
914
+ if (++steps % WALK_YIELD_INTERVAL === 0 && !(yield* SyncSection))
915
+ yield* Effect.yieldNow;
916
+ }
917
+ return false;
918
+ });
919
+ const aliveIn = Effect.fnUntraced(function* (get, op, handle) {
920
+ for (let index = 0; index < roots.length; index++) {
921
+ const root = roots[index];
922
+ const node = get(root);
923
+ if (node?.kind !== "directory" || node.metadata.nlink === 0) {
924
+ return yield* op.fail(handle ? "InvalidHandle" : "ClosedCaller");
925
+ }
926
+ if (index > 0 && !(yield* descends(get, root, roots[index - 1])))
927
+ return yield* op.fail("AccessDenied");
928
+ }
929
+ });
930
+ const containsIn = Effect.fnUntraced(function* (get, node) {
931
+ const root = roots.at(-1);
932
+ if (root === undefined)
933
+ return true;
934
+ if (node.kind === "directory")
935
+ return yield* descends(get, node.ino, root);
936
+ for (const link of node.links) {
937
+ const parent = get(link.parent);
938
+ if (parent?.kind === "directory" && parent.entries.get(link.name) === node.ino &&
939
+ (yield* descends(get, parent.ino, root)))
940
+ return true;
941
+ }
942
+ return false;
943
+ });
944
+ const alive = (op, handle = false) => Effect.flatMap(view, (get) => aliveIn(get, op, handle));
945
+ const contains = (node) => Effect.flatMap(view, (get) => containsIn(get, node));
946
+ const check = Effect.fnUntraced(function* (node, op, handle = false) {
947
+ // Share the current lookup within authorization, never its result between operations.
948
+ const get = yield* view;
949
+ yield* aliveIn(get, op, handle);
950
+ if (!(yield* containsIn(get, node)))
951
+ return yield* op.fail("AccessDenied");
952
+ });
953
+ const path = Effect.fnUntraced(function* (ino) {
954
+ const names = [];
955
+ const get = yield* view;
956
+ let node = get(ino);
957
+ let steps = 0;
958
+ if (node?.kind !== "directory" || node.metadata.nlink === 0)
959
+ return undefined;
960
+ const root = roots.at(-1) ?? ROOT_INO;
961
+ while (node.ino !== root) {
962
+ if (node.ino === ROOT_INO)
963
+ return undefined;
964
+ names.push(node.name);
965
+ const parent = get(node.parent);
966
+ if (parent?.kind !== "directory" || parent.metadata.nlink === 0 || parent.entries.get(node.name) !== node.ino) {
967
+ return undefined;
968
+ }
969
+ node = parent;
970
+ if (++steps % WALK_YIELD_INTERVAL === 0 && !(yield* SyncSection))
971
+ yield* Effect.yieldNow;
972
+ }
973
+ return "2f" + names.reverse().join("2f");
974
+ });
975
+ return { roots, root: roots.at(-1) ?? ROOT_INO, alive, contains, check, path };
976
+ };
977
+ })(Confinement || (Confinement = {}));
978
+ /** @internal */
979
+ export var Resolution;
980
+ (function (Resolution) {
981
+ const encoder = new TextEncoder();
982
+ const EMPTY_AUTHORITIES = [];
983
+ /** @internal */
984
+ Resolution.inGroup = (identity, gid) => identity.gid === gid || identity.groups.includes(gid);
985
+ /** @internal */
986
+ Resolution.permitted = (node, identity) => {
987
+ const shift = identity.uid === node.metadata.uid ? 6 : Resolution.inGroup(identity, node.metadata.gid) ? 3 : 0;
988
+ return (node.metadata.mode >> shift) & 0o7;
989
+ };
990
+ /** @internal */
991
+ Resolution.authorize = (node, identity, bits, op) => identity.privileged || (Resolution.permitted(node, identity) & bits) === bits
992
+ ? Effect.void
993
+ : Effect.fail(op.fail("AccessDenied"));
994
+ /** @internal */
995
+ Resolution.entryName = (input, op) => {
996
+ if (Predicate.isString(input) && !isWellFormed(input))
997
+ return Result.fail(op.fail("InvalidPathEncoding"));
998
+ const bytes = Predicate.isString(input) ? encoder.encode(input) : input;
999
+ if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
1000
+ bytes.includes(SLASH_BYTE))
1001
+ return Result.fail(op.fail("InvalidArgument"));
1002
+ return Result.succeed(Hex.encode(new Uint8Array(bytes)));
1003
+ };
1004
+ /** @internal */
1005
+ Resolution.make = ({ caller, get, identity, maxPathBytes, registry, confinement, additional, authorityView }) => {
1006
+ const visibleRoot = confinement?.root ?? ROOT_INO;
1007
+ const prepare = (input, op) => preparePath(input, op.operation, maxPathBytes);
1008
+ const prepareEntry = (input, op) => isEntry(input)
1009
+ ? Result.map(Resolution.entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
1010
+ : isTarget(input)
1011
+ ? Result.map(prepare(input.path, op), (path) => ({ kind: "path", path, base: input.relativeTo }))
1012
+ : Result.map(prepare(input, op), (path) => ({ kind: "path", path, base: undefined }));
1013
+ const nodeAt = Effect.fnUntraced(function* (lookup, ino) {
1014
+ const node = lookup(ino);
1015
+ return node === undefined ? yield* Effect.die("Inode left the table during resolution") : node;
1016
+ });
1017
+ const directoryAt = Effect.fnUntraced(function* (lookup, ino) {
1018
+ const node = yield* nodeAt(lookup, ino);
1019
+ return node.kind !== "directory" ? yield* Effect.die("Directory left the table during resolution") : node;
1020
+ });
1021
+ const callerDirectory = Effect.fnUntraced(function* (lookup, op) {
1022
+ const ino = TokenRegistry.inode(caller);
1023
+ const node = ino === undefined ? undefined : lookup(ino);
1024
+ return node?.kind !== "directory" ? yield* op.fail("ClosedCaller") : node;
1025
+ });
1026
+ const walk = Effect.fnUntraced(function* (path, base, op, mode, referencedBase) {
1027
+ const pathOp = op.at(path.input);
1028
+ const baseAuthorities = base === undefined
1029
+ ? EMPTY_AUTHORITIES
1030
+ : registry.handleBoundaries(base).map((roots) => Confinement.make(authorityView, roots));
1031
+ const authorizeDirectory = baseAuthorities.length === 0 ? Resolution.authorize : Effect.fnUntraced(function* (node, callerIdentity, bits, context) {
1032
+ for (const authority of baseAuthorities)
1033
+ yield* authority.check(node, context, true);
1034
+ yield* Resolution.authorize(node, callerIdentity, bits, context);
1035
+ });
1036
+ const parentOnly = mode.kind === "Parent";
1037
+ const allowMissing = mode.kind === "OrCreate" && mode.create !== "never";
1038
+ const followFinalSymlink = mode.kind === "Node"
1039
+ ? mode.followFinalSymlink !== false
1040
+ : mode.kind === "Parent" || mode.finalSymlink === "follow";
1041
+ const createMissing = mode.kind === "Node" ? mode.createMissing : undefined;
1042
+ if (!TokenRegistry.isOpen(caller)) {
1043
+ return yield* pathOp.fail("ClosedCaller");
1044
+ }
1045
+ if (additional !== undefined && additional.length > 0) {
1046
+ for (const authority of additional)
1047
+ yield* authority.alive(pathOp);
1048
+ }
1049
+ if (confinement !== undefined)
1050
+ yield* confinement.alive(pathOp);
1051
+ if (path.absolute && base !== undefined &&
1052
+ (confinement !== undefined || (additional?.length ?? 0) > 0 || baseAuthorities.length > 0)) {
1053
+ const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(base, "directory"), "handle", pathOp);
1054
+ yield* registry.authorizeHandle(base, node, pathOp);
1055
+ if (confinement !== undefined)
1056
+ yield* confinement.check(node, pathOp);
1057
+ if (additional !== undefined && additional.length > 0) {
1058
+ for (const authority of additional)
1059
+ yield* authority.check(node, pathOp);
1060
+ }
1061
+ }
1062
+ // Reads remain live so recursive creation can revisit nodes added earlier in this walk.
1063
+ const lookup = yield* authorityView;
1064
+ let current = path.absolute ? (yield* nodeAt(lookup, visibleRoot)) : yield* callerDirectory(lookup, pathOp);
1065
+ if (!path.absolute && referencedBase !== undefined) {
1066
+ current = referencedBase;
1067
+ yield* Resolution.authorize(current, identity, 0o1, op);
1068
+ }
1069
+ else if (!path.absolute && base !== undefined) {
1070
+ const directory = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(base, "directory"), "handle", pathOp);
1071
+ if (directory.kind !== "directory")
1072
+ return yield* pathOp.fail("InvalidHandle");
1073
+ yield* registry.authorizeHandle(base, directory, pathOp);
1074
+ current = directory;
1075
+ yield* Resolution.authorize(current, identity, 0o1, pathOp);
1076
+ }
1077
+ if (confinement !== undefined)
1078
+ yield* confinement.check(current, pathOp);
1079
+ if (additional !== undefined && additional.length > 0) {
1080
+ for (const authority of additional)
1081
+ yield* authority.check(current, pathOp);
1082
+ }
1083
+ if (current.metadata.nlink === 0) {
1084
+ return yield* pathOp.fail("NotFound");
1085
+ }
1086
+ let work = path;
1087
+ let parent;
1088
+ let name;
1089
+ let traversals = 0;
1090
+ // Leading components that came from a symbolic link's target. Only the components the caller wrote are
1091
+ // created, so a dangling link stays missing, as it does for mkdir -p.
1092
+ let linked = 0;
1093
+ for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
1094
+ if (current.kind !== "directory") {
1095
+ return yield* pathOp.fail("NotDirectory");
1096
+ }
1097
+ yield* authorizeDirectory(current, identity, 0o1, pathOp);
1098
+ const component = work.components[index];
1099
+ if (component === undefined)
1100
+ break;
1101
+ if (component === DOT_HEX)
1102
+ continue;
1103
+ if (component === DOT_DOT_HEX) {
1104
+ current = yield* directoryAt(lookup, confinement !== undefined && current.ino === visibleRoot ? current.ino : current.parent);
1105
+ parent = undefined;
1106
+ name = undefined;
1107
+ continue;
1108
+ }
1109
+ parent = current;
1110
+ name = component;
1111
+ const childIno = current.entries.get(component);
1112
+ const child = childIno === undefined ? undefined : lookup(childIno);
1113
+ if (child === undefined) {
1114
+ if (allowMissing && index === work.components.length - 1) {
1115
+ return { kind: "missing", parent, name, op: pathOp };
1116
+ }
1117
+ if (createMissing !== undefined && index >= linked) {
1118
+ current = yield* createMissing(current, component, index === work.components.length - 1);
1119
+ continue;
1120
+ }
1121
+ return yield* pathOp.fail("NotFound");
1122
+ }
1123
+ if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
1124
+ if (child.target.length === 0) {
1125
+ return yield* pathOp.fail("NotFound");
1126
+ }
1127
+ if (++traversals > MAX_SYMLINK_TRAVERSALS) {
1128
+ return yield* pathOp.fail("SymlinkLoop");
1129
+ }
1130
+ const suffix = work.suffixes[index] ?? new Uint8Array(0);
1131
+ const remaining = work.components.length - index - 1;
1132
+ if (maxPathBytes !== undefined &&
1133
+ ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), maxPathBytes)) {
1134
+ return yield* pathOp.fail("PathTooLong");
1135
+ }
1136
+ const expansion = new Uint8Array(child.target.length + suffix.length);
1137
+ expansion.set(child.target);
1138
+ expansion.set(suffix, child.target.length);
1139
+ const expanded = preparePath(ownedPath(expansion), op.operation, maxPathBytes);
1140
+ // The expansion is synthetic: its per-component limits are the caller's to hear about,
1141
+ // but the path in the error has to be the one the caller passed in, so the
1142
+ // expansion's own failure is not kept as the cause.
1143
+ if (Result.isFailure(expanded)) {
1144
+ return yield* pathOp.fail(expanded.failure.code);
1145
+ }
1146
+ work = expanded.success;
1147
+ linked = work.components.length - remaining + Math.max(0, linked - index - 1);
1148
+ if (work.absolute)
1149
+ current = yield* nodeAt(lookup, visibleRoot);
1150
+ index = -1;
1151
+ }
1152
+ else
1153
+ current = child;
1154
+ }
1155
+ if (baseAuthorities.length > 0) {
1156
+ for (const authority of baseAuthorities)
1157
+ yield* authority.check(current, pathOp, true);
1158
+ }
1159
+ if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
1160
+ return yield* pathOp.fail("NotDirectory");
1161
+ }
1162
+ return parent === undefined || name === undefined
1163
+ ? { kind: "node", node: current, op: pathOp }
1164
+ : { kind: "entry", node: current, parent, name, op: pathOp };
1165
+ });
1166
+ const resolveNode = Effect.fnUntraced(function* (target, mode, op) {
1167
+ if (Predicate.isTagged(target, "Path")) {
1168
+ const path = yield* Effect.fromResult(prepare(target.path, op));
1169
+ const result = yield* walk(path, target.relativeTo, op, {
1170
+ ...mode,
1171
+ followFinalSymlink: mode.final === true
1172
+ ? mode.followFinalSymlink ?? true
1173
+ : target.followFinalSymlink ?? mode.followFinalSymlink ?? true
1174
+ });
1175
+ return result.kind === "missing" ? yield* result.op.fail("NotFound") : result;
1176
+ }
1177
+ if (!TokenRegistry.isOpen(caller))
1178
+ return yield* op.fail("ClosedCaller");
1179
+ const node = yield* TokenRegistry.nodeOrFail(yield* (Predicate.isTagged(target, "Reference")
1180
+ ? registry.resolve(target.reference, "reference")
1181
+ : registry.resolve(target.handle, "handle")), Predicate.isTagged(target, "Reference") ? "reference" : "handle", op);
1182
+ if (!Predicate.isTagged(target, "Reference"))
1183
+ yield* registry.authorizeHandle(target.handle, node, op);
1184
+ if (confinement !== undefined)
1185
+ yield* confinement.check(node, op);
1186
+ if (additional !== undefined && additional.length > 0) {
1187
+ for (const authority of additional)
1188
+ yield* authority.check(node, op);
1189
+ }
1190
+ return { kind: "node", node, op };
1191
+ });
1192
+ const directory = Effect.fnUntraced(function* (target, op) {
1193
+ const resolved = yield* resolveNode(target, { kind: "Node" }, op);
1194
+ if (resolved.node.kind !== "directory")
1195
+ return yield* resolved.op.fail("NotDirectory");
1196
+ return { node: resolved.node, op: resolved.op };
1197
+ });
1198
+ const entryDirectory = Effect.fnUntraced(function* (target, op) {
1199
+ const resolved = yield* directory(target, op);
1200
+ if (resolved.node.metadata.nlink === 0)
1201
+ return yield* resolved.op.fail("NotFound");
1202
+ return resolved;
1203
+ });
1204
+ const parent = Effect.fnUntraced(function* (prepared, op) {
1205
+ if (prepared.kind === "entry") {
1206
+ const resolved = yield* entryDirectory(prepared.directory, op);
1207
+ return {
1208
+ kind: "parent",
1209
+ parent: resolved.node.ino,
1210
+ name: prepared.name,
1211
+ trailingSlash: false,
1212
+ addressing: "entry",
1213
+ op
1214
+ };
1215
+ }
1216
+ const resolved = yield* walk(prepared.path, prepared.base, op, { kind: "Parent" });
1217
+ if (resolved.kind === "missing")
1218
+ return yield* resolved.op.fail("NotFound");
1219
+ if (resolved.node.kind !== "directory")
1220
+ return yield* resolved.op.fail("NotDirectory");
1221
+ return {
1222
+ kind: "parent",
1223
+ parent: resolved.node.ino,
1224
+ name: prepared.path.components.at(-1),
1225
+ trailingSlash: prepared.path.trailingSlash,
1226
+ addressing: "path",
1227
+ op: resolved.op
1228
+ };
1229
+ });
1230
+ const creationResult = (result, trailingSlash, addressing, op, origin) => {
1231
+ if (result.kind === "node")
1232
+ return Effect.fail(op.fail("IsDirectory"));
1233
+ const entry = {
1234
+ parent: result.parent,
1235
+ name: result.name,
1236
+ trailingSlash,
1237
+ addressing,
1238
+ op,
1239
+ origin: origin ?? { parent: result.parent, name: result.name }
1240
+ };
1241
+ return Effect.succeed(result.kind === "missing" ? { ...entry, kind: "missing" } : { ...entry, kind: "existing", node: result.node });
1242
+ };
1243
+ const orCreate = Effect.fnUntraced(function* (prepared, mode, op) {
1244
+ if (prepared.kind === "path") {
1245
+ const pathOp = op.at(prepared.path.input);
1246
+ if (mode.create === "exclusive") {
1247
+ const exists = yield* Effect.result(walk(prepared.path, prepared.base, op, { kind: "Node", followFinalSymlink: false }));
1248
+ if (Result.isSuccess(exists))
1249
+ return yield* pathOp.fail("AlreadyExists");
1250
+ if (exists.failure.code !== "NotFound")
1251
+ return yield* exists.failure;
1252
+ }
1253
+ return yield* creationResult(yield* walk(prepared.path, prepared.base, op, mode), prepared.path.trailingSlash, "path", pathOp);
1254
+ }
1255
+ const { node: parent } = yield* entryDirectory(prepared.directory, op);
1256
+ if (isDotComponent(prepared.name))
1257
+ return yield* op.fail("InvalidArgument");
1258
+ yield* Resolution.authorize(parent, identity, 0o1, op);
1259
+ const directIno = parent.entries.get(prepared.name);
1260
+ const direct = directIno === undefined ? undefined : yield* get(directIno);
1261
+ if (mode.beforeFollow !== undefined)
1262
+ yield* mode.beforeFollow(parent, direct);
1263
+ if (mode.create === "exclusive" && mode.entryExclusive !== "afterSymlink" && direct !== undefined) {
1264
+ return yield* op.fail("AlreadyExists");
1265
+ }
1266
+ let result = direct === undefined
1267
+ ? { kind: "missing", parent, name: prepared.name, op }
1268
+ : { kind: "entry", node: direct, parent, name: prepared.name, op };
1269
+ if (direct?.kind === "symlink" && mode.finalSymlink === "follow") {
1270
+ const path = yield* Effect.fromResult(preparePath(ownedPath(nameBytes(prepared.name)), op.operation, undefined));
1271
+ // Following an entry link must retain the directory capability's boundaries.
1272
+ const base = Predicate.isTagged(prepared.directory, "Handle")
1273
+ ? prepared.directory.handle
1274
+ : Predicate.isTagged(prepared.directory, "Path")
1275
+ ? prepared.directory.relativeTo
1276
+ : undefined;
1277
+ result = yield* walk(path, base, op, mode, parent);
1278
+ // Entry writeFile rejects a followed directory before checking exclusive creation.
1279
+ if (mode.create === "exclusive" && mode.entryExclusive === "afterSymlink" &&
1280
+ (result.kind === "node" || (result.kind === "entry" && result.node.kind === "directory"))) {
1281
+ return yield* op.fail("IsDirectory");
1282
+ }
1283
+ }
1284
+ if (result.kind === "missing" && mode.create === "never")
1285
+ return yield* op.fail("NotFound");
1286
+ if (mode.create === "exclusive" && mode.entryExclusive === "afterSymlink" && result.kind !== "missing") {
1287
+ return yield* op.fail("AlreadyExists");
1288
+ }
1289
+ return yield* creationResult(result, false, "entry", op, { parent, name: prepared.name });
1290
+ });
1291
+ function resolve(input, mode, op) {
1292
+ if (isTarget(input)) {
1293
+ return mode.kind === "Node"
1294
+ ? resolveNode(input, mode, op)
1295
+ : Effect.die("A node target requires Node resolution");
1296
+ }
1297
+ return mode.kind === "Parent"
1298
+ ? parent(input, op)
1299
+ : mode.kind === "OrCreate"
1300
+ ? orCreate(input, mode, op)
1301
+ : Effect.die("An entry requires Parent or OrCreate resolution");
1302
+ }
1303
+ return { prepare, prepareEntry, resolve, directory, entryDirectory };
1304
+ };
1305
+ })(Resolution || (Resolution = {}));
1306
+ /** @internal */
1307
+ export const BasicLimits = Schema.Struct({
1308
+ maxEntries: Schema.optional(Schema.Natural),
1309
+ maxBytes: Schema.optional(Schema.ByteSize),
1310
+ maxFileBytes: Schema.optional(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.toBigInt(size) <= BigInt(MAX_FILE_BYTES) ? undefined : "file limit is too large"))),
1311
+ maxPathBytes: Schema.optional(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.toBigInt(size) >= 1n ? undefined : "path limit must be positive"))),
1312
+ maxPendingOperations: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)))
1313
+ });
1314
+ const readLimits = (options) => Result.map(decodeConfiguration(BasicLimits, options ?? {}, "make"), (value) => Object.freeze({
1315
+ ...value,
1316
+ maxEntries: value.maxEntries,
1317
+ maxBytes: value.maxBytes,
1318
+ maxPathBytes: value.maxPathBytes,
1319
+ maxFileBytes: value.maxFileBytes ?? ByteSize.bytes(MAX_FILE_BYTES),
1320
+ maxPendingOperations: value.maxPendingOperations ?? 64
1321
+ }));
1322
+ const EXECUTE = 0o1;
1323
+ const WRITE = 0o2;
1324
+ const READ = 0o4;
1325
+ const ANY_EXECUTE = 0o111;
1326
+ const SET_ID_BITS = 0o6000;
1327
+ const STICKY_BIT = 0o1000;
1328
+ const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
1329
+ /** @internal */
1330
+ export const memoryCommitProvider = Object.freeze({
1331
+ mode: "memory",
1332
+ commit: () => Effect.succeed("committed"),
1333
+ shutdown: Effect.void
1334
+ });
1335
+ // An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
1336
+ const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
1337
+ if (Exit.isSuccess(committed) && Predicate.isTagged("Rejected")(committed.value)) {
1338
+ return { available: !cleanup, failure: undefined, cause: committed.value.cause };
1339
+ }
1340
+ // TODO(#186): Preserve the cause of a provider defect or interruption.
1341
+ if (Exit.isFailure(committed)) {
1342
+ if (Cause.hasDies(committed.cause) || Cause.hasInterrupts(committed.cause)) {
1343
+ return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
1344
+ }
1345
+ const failure = Cause.findErrorOption(committed.cause);
1346
+ return Option.isSome(failure)
1347
+ ? { available: !cleanup, failure: failure.value }
1348
+ : { available: false, failure: fsFailure("OutcomeUnknown", operation) };
1349
+ }
1350
+ if (committed.value === "rejected") {
1351
+ return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
1352
+ }
1353
+ if (committed.value === "unknown") {
1354
+ return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
1355
+ }
1356
+ return { available: true, failure: undefined };
1357
+ });
1358
+ const WALK_CHUNK_ENTRIES = 128;
1359
+ const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
1360
+ const isMode = Schema.is(Mode);
1361
+ const isNatural = Schema.is(Schema.Natural);
1362
+ const isLength = Schema.is(SetattrOptions.fields.size.schema);
1363
+ const inGroup = Resolution.inGroup;
1364
+ const isTimestamp = Schema.is(Timestamp);
1365
+ const isSeekMode = Schema.is(SeekMode);
1366
+ const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
1367
+ const decodeTimes = Schema.decodeEffect(Times, { onExcessProperty: "error" });
1368
+ const decodeExpected = Schema.decodeEffect(SetattrOptions.fields.expected.schema, { onExcessProperty: "error" });
1369
+ const SETATTR_FIELDS = Object.keys(SetattrOptions.fields);
1370
+ const timeAt = (update, value, now) => update === undefined ? value : Match.value(update).pipe(Match.discriminator("kind")("omit", () => value), Match.discriminator("kind")("now", () => now), Match.discriminator("kind")("value", ({ nanoseconds }) => nanoseconds), Match.exhaustive);
1371
+ const decodeWriteFileOptions = Schema.decodeEffect(WriteFileOptions, { onExcessProperty: "error" });
1372
+ const decodeOpenOptions = Schema.decodeEffect(OpenOptions, { onExcessProperty: "error" });
1373
+ const decodeMkdirOptions = Schema.decodeEffect(MkdirOptions, { onExcessProperty: "error" });
1374
+ const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessProperty: "error" });
1375
+ const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
1376
+ const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
1377
+ const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
1378
+ // Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
1379
+ // status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
1380
+ // skips it, so reads at one instant (a frozen or coarse clock) store nothing.
1381
+ const accessDue = (metadata, now) => now !== metadata.atimeNs && (metadata.atimeNs <= metadata.mtimeNs || metadata.atimeNs <= metadata.ctimeNs ||
1382
+ now - metadata.atimeNs >= RELATIME_INTERVAL_NS);
1383
+ const refreshDue = (read) => read.access !== undefined && accessDue(read.access.node.metadata, read.access.now);
1384
+ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSystem/Draft", {
1385
+ make: Effect.fnUntraced(function* (base) {
1386
+ const revision = base.revision + 1n;
1387
+ const cell = MutableRef.make({
1388
+ pending: new Map(),
1389
+ opens: undefined,
1390
+ nextInode: base.nextInode,
1391
+ changed: false,
1392
+ finished: undefined,
1393
+ entries: base.entries,
1394
+ usedBytes: base.usedBytes,
1395
+ renames: [],
1396
+ events: [],
1397
+ after: [],
1398
+ removed: []
1399
+ });
1400
+ const lookup = (ino) => {
1401
+ const draft = MutableRef.get(cell);
1402
+ return draft.pending.has(ino) ? draft.pending.get(ino) : getNode(base, ino);
1403
+ };
1404
+ const get = (ino) => Effect.sync(() => lookup(ino));
1405
+ const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
1406
+ const put = (node, changed) => Effect.sync(() => {
1407
+ MutableRef.update(cell, (draft) => {
1408
+ // This map belongs only to this transition. Copying it on every inode write makes recursive changes quadratic.
1409
+ draft.pending.set(node.ino, changed ? { ...node, revision } : node);
1410
+ return { ...draft, changed: draft.changed || changed };
1411
+ });
1412
+ });
1413
+ return {
1414
+ revision,
1415
+ lookup,
1416
+ get,
1417
+ put: (node) => put(node, true),
1418
+ putQuiet: (node) => put(node, false),
1419
+ unchanged: Effect.sync(() => {
1420
+ const draft = MutableRef.get(cell);
1421
+ return draft.pending.size === 0 && draft.opens === undefined && draft.nextInode === base.nextInode &&
1422
+ draft.entries === base.entries && draft.usedBytes === base.usedBytes;
1423
+ }),
1424
+ remove: (ino) => Effect.sync(() => {
1425
+ const draft = MutableRef.get(cell);
1426
+ draft.pending.set(ino, undefined);
1427
+ draft.removed.push(ino);
1428
+ }),
1429
+ allocate: Effect.sync(() => {
1430
+ const next = MutableRef.get(cell).nextInode;
1431
+ MutableRef.update(cell, (draft) => ({ ...draft, nextInode: Ino(next + 1) }));
1432
+ return next;
1433
+ }),
1434
+ canAllocate: Effect.sync(() => MutableRef.get(cell).nextInode < Number.MAX_SAFE_INTEGER),
1435
+ openCount,
1436
+ retain: (ino) => Effect.sync(() => {
1437
+ const count = MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0;
1438
+ MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, count + 1) }));
1439
+ }),
1440
+ release: (ino) => Effect.sync(() => {
1441
+ const count = (MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0) - 1;
1442
+ MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, Math.max(0, count)) }));
1443
+ return count;
1444
+ }),
1445
+ entries: Effect.sync(() => MutableRef.get(cell).entries),
1446
+ addEntries: (amount) => Effect.sync(() => {
1447
+ MutableRef.update(cell, (draft) => ({ ...draft, entries: draft.entries + amount }));
1448
+ }),
1449
+ usedBytes: Effect.sync(() => MutableRef.get(cell).usedBytes),
1450
+ addUsedBytes: (amount) => Effect.sync(() => {
1451
+ MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
1452
+ }),
1453
+ rename: (rename) => Effect.sync(() => {
1454
+ MutableRef.get(cell).renames.push(rename);
1455
+ }),
1456
+ publish: (event) => Effect.sync(() => {
1457
+ MutableRef.get(cell).events.push(event);
1458
+ }),
1459
+ afterInstall: (action) => Effect.sync(() => {
1460
+ MutableRef.get(cell).after.push(action);
1461
+ }),
1462
+ finish: Effect.sync(() => {
1463
+ const draft = MutableRef.get(cell);
1464
+ if (draft.finished !== undefined)
1465
+ return draft.finished;
1466
+ const inodes = HashMap.mutate(base.inodes, (mutable) => {
1467
+ for (const [ino, node] of draft.pending) {
1468
+ if (node === undefined)
1469
+ HashMap.remove(mutable, ino);
1470
+ else
1471
+ setNode(mutable, node);
1472
+ }
1473
+ });
1474
+ let open = base.open;
1475
+ if (draft.opens !== undefined) {
1476
+ const next = new Map(base.open);
1477
+ for (const [ino, count] of draft.opens) {
1478
+ if (count > 0)
1479
+ next.set(ino, count);
1480
+ else
1481
+ next.delete(ino);
1482
+ }
1483
+ open = next;
1484
+ }
1485
+ const finished = {
1486
+ inodes,
1487
+ open,
1488
+ nextInode: draft.nextInode,
1489
+ revision: draft.changed ? revision : base.revision,
1490
+ entries: draft.entries,
1491
+ usedBytes: draft.usedBytes
1492
+ };
1493
+ MutableRef.update(cell, (current) => ({ ...current, finished }));
1494
+ return finished;
1495
+ }),
1496
+ actions: Effect.sync(() => {
1497
+ const { events, after, removed, renames } = MutableRef.get(cell);
1498
+ return { events, after, removed, renames };
1499
+ })
1500
+ };
1501
+ })
1502
+ }) {
1503
+ }
1504
+ const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
1505
+ const withEntries = (directory, entries) => ({ ...directory, entries });
1506
+ const withoutLink = (links, parent, name) => {
1507
+ const index = links.findIndex((link) => link.parent === parent && link.name === name);
1508
+ return index < 0 ? links : [...links.slice(0, index), ...links.slice(index + 1)];
1509
+ };
1510
+ /** @internal */
1511
+ export const makeEngine = Effect.fnUntraced(function* (options = {}) {
1512
+ const services = yield* Effect.context();
1513
+ const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
1514
+ const limits = yield* Effect.fromResult(readLimits(options.limits));
1515
+ const commitProvider = options.commitProvider ?? memoryCommitProvider;
1516
+ const execution = options.execution ?? "cooperative";
1517
+ if ((execution !== "cooperative" && execution !== "sync") ||
1518
+ (execution === "sync" && commitProvider !== memoryCommitProvider)) {
1519
+ return yield* argumentFailure("make", "execution");
1520
+ }
1521
+ const synchronous = execution === "sync";
1522
+ const clock = yield* Clock.clockWith(Effect.succeed);
1523
+ const initialTime = options.initialTime ?? clock.currentTimeNanosUnsafe();
1524
+ if (!isTimestamp(initialTime)) {
1525
+ return yield* argumentFailure("make", "clock.currentTimeNanos");
1526
+ }
1527
+ const timestamp = (op) => Effect.suspend(() => {
1528
+ const now = clock.currentTimeNanosUnsafe();
1529
+ return isTimestamp(now)
1530
+ ? Effect.succeed(now)
1531
+ : Effect.fail(op.fail("InvalidArgument"));
1532
+ });
1533
+ const volumeIdentity = Symbol();
1534
+ const directoryHolds = new Map();
1535
+ const maxPendingOperations = limits.maxPendingOperations;
1536
+ const permits = maxPendingOperations + 1;
1537
+ // The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
1538
+ // overtaken by each later observation. A change holds a turnstile while it gathers the permits, and an
1539
+ // observation passes the turnstile before it takes one, so observations that arrive after a change wait
1540
+ // behind it. The turnstile goes to its waiters in arrival order, even to one that arrives while a finished
1541
+ // change is still releasing. Both waits stay interruptible, and every permit is taken and released by
1542
+ // `withPermits`.
1543
+ const cooperative = synchronous ? undefined : {
1544
+ gate: Semaphore.makeUnsafe(permits),
1545
+ turnstile: makeTurnstile()
1546
+ };
1547
+ let active = false;
1548
+ const pending = new Map();
1549
+ const preflight = (op) => synchronous
1550
+ ? Effect.suspend(() => {
1551
+ if (active)
1552
+ return Effect.fail(op.fail("VolumeBusy"));
1553
+ return Effect.flatMap(VolumeTestSeams, (seams) => Object.values(seams).some((effect) => effect !== Effect.void)
1554
+ ? Effect.fail(op.fail("InvalidArgument", { field: "testSeams" }))
1555
+ : Effect.void);
1556
+ })
1557
+ : Effect.void;
1558
+ const drain = Effect.gen(function* () {
1559
+ let failure = Cause.empty;
1560
+ while (pending.size > 0) {
1561
+ const releases = Array.from(pending.values());
1562
+ pending.clear();
1563
+ for (const release of releases) {
1564
+ const exit = yield* Effect.exit(release);
1565
+ if (Exit.isFailure(exit))
1566
+ failure = Cause.combine(failure, exit.cause);
1567
+ }
1568
+ }
1569
+ if (failure.reasons.length > 0)
1570
+ return yield* Effect.failCause(failure);
1571
+ });
1572
+ const inSyncSection = (effect) => Effect.provideService(Effect.provideService(Effect.uninterruptible(Effect.suspend(() => {
1573
+ if (active)
1574
+ return Effect.fail(fsFailure("VolumeBusy", "coordinate"));
1575
+ active = true;
1576
+ return Effect.ensuring(effect, Effect.ensuring(drain, Effect.sync(() => {
1577
+ active = false;
1578
+ })));
1579
+ })), Scheduler.PreventSchedulerYield, true), SyncSection, true);
1580
+ const observing = (effect) => cooperative === undefined
1581
+ ? inSyncSection(effect)
1582
+ : Effect.andThen(cooperative.turnstile.withTurn(Effect.void), cooperative.gate.withPermits(1)(effect));
1583
+ const changing = (effect) => cooperative === undefined
1584
+ ? inSyncSection(effect)
1585
+ : cooperative.turnstile.withTurn(cooperative.gate.withPermits(permits)(effect));
1586
+ const admission = yield* Semaphore.make(permits);
1587
+ const admit = (op, effect) => Effect.andThen(preflight(op), admission.withPermitsIfAvailable(1)(effect).pipe(Effect.flatMap(Option.match({
1588
+ onNone: () => op.fail("VolumeBusy"),
1589
+ onSome: Effect.succeed
1590
+ }))));
1591
+ const finalizeOwned = (reference, release, ordinary) => synchronous
1592
+ ? Effect.uninterruptible(Effect.suspend(() => {
1593
+ if (!active)
1594
+ return ordinary;
1595
+ reference.lifecycle = { kind: "releasedPendingCommit", ino: TokenRegistry.inode(reference) };
1596
+ pending.set(reference, Effect.orDie(Effect.suspend(release)));
1597
+ return Effect.void;
1598
+ }))
1599
+ : ordinary;
1600
+ let state = emptyState(initialTime);
1601
+ const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
1602
+ if (options.initialState !== undefined)
1603
+ state = options.initialState;
1604
+ const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
1605
+ // Select the operation's namespace at execution time; never retain it across operations.
1606
+ const authorityView = Effect.map(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.lookup : (ino) => getNode(state, ino));
1607
+ const registry = TokenRegistry.make(volumeIdentity, view);
1608
+ const referenceFor = registry.referenceFor;
1609
+ const makeFileReference = registry.file;
1610
+ const makeDirectoryReference = registry.directory;
1611
+ let available = true;
1612
+ const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
1613
+ const install = Effect.fnUntraced(function* (finished) {
1614
+ const before = state;
1615
+ state = yield* finished.finish;
1616
+ const actions = yield* finished.actions;
1617
+ for (const ino of actions.removed)
1618
+ registry.forget(ino);
1619
+ for (const apply of actions.after)
1620
+ apply();
1621
+ const after = state;
1622
+ options.installed?.(() => actions.events.flatMap((events) => Array.from(events(after))), {
1623
+ before,
1624
+ after,
1625
+ renames: actions.renames
1626
+ });
1627
+ });
1628
+ const transition = Effect.fnUntraced(function* (change) {
1629
+ const current = yield* Draft.make(state);
1630
+ const value = yield* Effect.provideService(change, Draft, current);
1631
+ return [value, current];
1632
+ });
1633
+ // Cleanup releases local ownership even when storage refuses; that divergence disables the volume.
1634
+ const committed = Effect.fnUntraced(function* (op, finished, cleanup) {
1635
+ if ((yield* finished.unchanged) || (cleanup && !available))
1636
+ return yield* install(finished);
1637
+ const next = yield* finished.finish;
1638
+ const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
1639
+ if (!answer.available)
1640
+ available = false;
1641
+ if (answer.cause !== undefined) {
1642
+ if (cleanup)
1643
+ yield* install(finished);
1644
+ return yield* Effect.failCause(answer.cause);
1645
+ }
1646
+ if (answer.failure !== undefined) {
1647
+ if (cleanup)
1648
+ yield* install(finished);
1649
+ return yield* answer.failure;
1650
+ }
1651
+ yield* install(finished);
1652
+ });
1653
+ const applyCleanup = Effect.fnUntraced(function* (change) {
1654
+ const current = yield* Draft.make(state);
1655
+ yield* Effect.provideService(change(current), Draft, current);
1656
+ yield* committed(OpContext.make("close"), current, true);
1657
+ });
1658
+ const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
1659
+ // Permit waits stay interruptible. Memory transitions are masked; durable transitions remain
1660
+ // interruptible until the candidate is ready. Every provider commit and installation is masked.
1661
+ const coordinated = (op, effect, cleanup = false) => admit(op, changing(Effect.uninterruptibleMask((restore) => {
1662
+ const staged = transition(Effect.andThen(checkAvailable(op.operation), effect));
1663
+ return Effect.flatMap(commitProvider.mode === "memory" ? staged : restore(staged), ([value, finished]) => Effect.as(committed(op, finished, cleanup), value));
1664
+ }))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
1665
+ const coordinatedRead = (op, effect) => admit(op, observing(Effect.andThen(checkAvailable(op.operation), effect))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
1666
+ const refreshAccess = Effect.fnUntraced(function* (read) {
1667
+ if (refreshDue(read)) {
1668
+ const { node, now } = read.access;
1669
+ const draft = yield* Draft;
1670
+ yield* draft.putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
1671
+ }
1672
+ return read.value;
1673
+ });
1674
+ // A read that may refresh an access time. It observes under one permit, so reads run beside each other, and
1675
+ // only when the access time is due does it run again as a change. The change repeats every check and the rule,
1676
+ // since another read may have refreshed the time meanwhile, and its result is the one returned.
1677
+ const accessing = (op, read) => Effect.flatMap(coordinatedRead(op, read), (observed) => refreshDue(observed)
1678
+ ? coordinated(op, Effect.flatMap(read, refreshAccess))
1679
+ : Effect.succeed(observed.value));
1680
+ const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
1681
+ const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
1682
+ available = false;
1683
+ }), commitProvider.shutdown))).pipe(Effect.orDie);
1684
+ const pathOfCurrent = Effect.fnUntraced(function* (ino) {
1685
+ const names = [];
1686
+ const first = yield* view(ino);
1687
+ if (first?.kind !== "directory" || first.metadata.nlink === 0)
1688
+ return undefined;
1689
+ let node = first;
1690
+ while (node.ino !== ROOT_INO) {
1691
+ names.push(node.name);
1692
+ const parent = yield* view(node.parent);
1693
+ if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
1694
+ return undefined;
1695
+ node = parent;
1696
+ }
1697
+ return SLASH_HEX + names.reverse().join(SLASH_HEX);
1698
+ });
1699
+ const publishEntry = Effect.fnUntraced(function* (_tag, parent, name, ino) {
1700
+ if (options.installed === undefined)
1701
+ return;
1702
+ const before = state;
1703
+ const draft = yield* Draft;
1704
+ yield* draft.publish((installed) => {
1705
+ const prefix = pathOf((at) => getNode(installed, at), parent) ?? pathOf((at) => getNode(before, at), parent);
1706
+ return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino, name }];
1707
+ });
1708
+ });
1709
+ const publishNode = Effect.fnUntraced(function* (ino) {
1710
+ if (options.installed === undefined)
1711
+ return;
1712
+ const draft = yield* Draft;
1713
+ const target = yield* draft.get(ino);
1714
+ if (target === undefined)
1715
+ return;
1716
+ if (target.kind === "directory") {
1717
+ yield* draft.publish((installed) => {
1718
+ const directory = getNode(installed, ino);
1719
+ const path = pathOf((at) => getNode(installed, at), ino);
1720
+ return path === undefined || directory?.kind !== "directory"
1721
+ ? []
1722
+ : [{ change: { _tag: "Update", path: ownedPath(nameBytes(path)) }, parent: directory.parent, ino }];
1723
+ });
1724
+ return;
1725
+ }
1726
+ const links = target.links;
1727
+ yield* draft.publish((installed) => {
1728
+ const events = [];
1729
+ for (const link of links) {
1730
+ const prefix = pathOf((at) => getNode(installed, at), link.parent);
1731
+ if (prefix !== undefined) {
1732
+ events.push({
1733
+ change: { _tag: "Update", path: entryPath(prefix, link.name) },
1734
+ parent: link.parent,
1735
+ ino
1736
+ });
1737
+ }
1738
+ }
1739
+ return events;
1740
+ });
1741
+ });
1742
+ const holdDirectory = (ino) => {
1743
+ directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
1744
+ };
1745
+ const unholdDirectory = Effect.fnUntraced(function* (ino) {
1746
+ const count = (directoryHolds.get(ino) ?? 1) - 1;
1747
+ if (count > 0) {
1748
+ directoryHolds.set(ino, count);
1749
+ return;
1750
+ }
1751
+ directoryHolds.delete(ino);
1752
+ const node = getNode(state, ino);
1753
+ if (node?.kind === "directory" && node.metadata.nlink === 0) {
1754
+ yield* applyCleanup((d) => d.remove(ino));
1755
+ }
1756
+ });
1757
+ const releaseDirectory = (reference) => TokenRegistry.release(reference, unholdDirectory, (action) => Effect.sync(action));
1758
+ const finalizeDirectory = (reference) => {
1759
+ const close = coordinatedCleanup(releaseDirectory(reference));
1760
+ return finalizeOwned(reference, () => releaseDirectory(reference), TokenRegistry.finalize(reference, close, Effect.ignore(close)));
1761
+ };
1762
+ const closeReleasedScope = TokenRegistry.closeReleasedScope;
1763
+ const permitted = Resolution.permitted;
1764
+ const authorize = Resolution.authorize;
1765
+ const reclaim = Effect.fnUntraced(function* (d, ino) {
1766
+ const node = yield* d.get(ino);
1767
+ if (node === undefined)
1768
+ return;
1769
+ if (node.kind === "file") {
1770
+ if (node.links.length > 0 || (yield* d.openCount(ino)) > 0)
1771
+ return;
1772
+ yield* d.addUsedBytes(-BigInt(node.data.length));
1773
+ }
1774
+ else if (node.kind === "symlink") {
1775
+ if (node.links.length > 0)
1776
+ return;
1777
+ yield* d.addUsedBytes(-BigInt(node.target.length));
1778
+ }
1779
+ else if (node.metadata.nlink > 0 || directoryHolds.has(ino))
1780
+ return;
1781
+ yield* d.remove(ino);
1782
+ });
1783
+ const atEntryLimit = Effect.gen(function* () {
1784
+ const draft = yield* Draft;
1785
+ return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
1786
+ });
1787
+ const reserveEntry = Effect.fnUntraced(function* (op) {
1788
+ const draft = yield* Draft;
1789
+ if ((yield* atEntryLimit) || !(yield* draft.canAllocate))
1790
+ return yield* op.fail("NoSpace");
1791
+ });
1792
+ const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
1793
+ const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
1794
+ if (limits.maxBytes === undefined)
1795
+ return;
1796
+ const draft = yield* Draft;
1797
+ if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
1798
+ return yield* op.fail("NoSpace");
1799
+ }
1800
+ });
1801
+ const attach = Effect.fnUntraced(function* (parent, name, child, now) {
1802
+ const d = yield* Draft;
1803
+ yield* d.put(withEntries({
1804
+ ...parent,
1805
+ metadata: {
1806
+ ...parent.metadata,
1807
+ nlink: parent.metadata.nlink + (child.kind === "directory" ? 1 : 0),
1808
+ mtimeNs: now,
1809
+ ctimeNs: now
1810
+ }
1811
+ }, DirectoryEntries.set(parent.entries, name, child.ino)));
1812
+ if (child.kind === "directory")
1813
+ yield* d.put({ ...child, parent: parent.ino, name });
1814
+ else {
1815
+ yield* d.put({
1816
+ ...child,
1817
+ links: [...child.links, { parent: parent.ino, name }],
1818
+ metadata: { ...child.metadata, nlink: child.links.length + 1, ctimeNs: now }
1819
+ });
1820
+ }
1821
+ });
1822
+ const detach = Effect.fnUntraced(function* (child, parent, name, now) {
1823
+ const d = yield* Draft;
1824
+ if (child.kind === "directory") {
1825
+ yield* d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
1826
+ }
1827
+ else {
1828
+ const links = withoutLink(child.links, parent, name);
1829
+ yield* d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
1830
+ }
1831
+ yield* reclaim(d, child.ino);
1832
+ });
1833
+ const releaseFileInode = Effect.fnUntraced(function* (ino) {
1834
+ const d = yield* Draft;
1835
+ yield* d.release(ino);
1836
+ yield* reclaim(d, ino);
1837
+ });
1838
+ const releaseFile = (ref) => TokenRegistry.release(ref, releaseFileInode, (action) => Effect.sync(action));
1839
+ const releaseOpenFile = (ref) => Effect.ignore(coordinatedCleanup(Effect.suspend(() => TokenRegistry.inode(ref) === undefined ? Effect.void : applyCleanup(() => releaseFile(ref)))));
1840
+ // Admission refusal leaves the handle open. Every admitted close shares the registry release,
1841
+ // and a storage failure still completes cleanup while only explicit close reports the failure.
1842
+ const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, TokenRegistry.release(ref, releaseFileInode, (action) => Effect.flatMap(Draft, (draft) => draft.afterInstall(action)))), true).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
1843
+ const finalizeFile = (ref) => finalizeOwned(ref, () => applyCleanup(() => releaseFile(ref)), TokenRegistry.finalize(ref, closeFile(ref, OpContext.make("close"), Effect.void), releaseOpenFile(ref)));
1844
+ const acquireHandle = TokenRegistry.acquire;
1845
+ const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => TokenRegistry.publish(ref, ino)));
1846
+ const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
1847
+ const d = yield* Draft;
1848
+ yield* d.addUsedBytes(BigInt(data.length - file.data.length));
1849
+ yield* d.put({
1850
+ ...file,
1851
+ data,
1852
+ metadata: {
1853
+ ...file.metadata,
1854
+ size: BigInt(data.length),
1855
+ mode: file.metadata.mode & ~SET_ID_BITS,
1856
+ mtimeNs: now,
1857
+ ctimeNs: now
1858
+ }
1859
+ });
1860
+ if (publish)
1861
+ yield* publishNode(file.ino);
1862
+ });
1863
+ const resizeAt = Effect.fnUntraced(function* (file, length, now, op) {
1864
+ if (length > BigInt(maxFileBytes))
1865
+ return yield* op.fail("FileTooLarge");
1866
+ const size = Number(length);
1867
+ yield* reserveBytes(op, BigInt(size - file.data.length));
1868
+ const data = new Uint8Array(size);
1869
+ data.set(file.data.subarray(0, size));
1870
+ yield* replaceContent(file, data, now, false);
1871
+ });
1872
+ const resize = Effect.fnUntraced(function* (file, length, op) {
1873
+ if (!isLength(length))
1874
+ return yield* op.fail("InvalidArgument");
1875
+ yield* resizeAt(file, length, yield* timestamp(op), op);
1876
+ yield* publishNode(file.ino);
1877
+ });
1878
+ const fileHandle = (ref) => {
1879
+ const get = Effect.fnUntraced(function* (op, access, cleanup = false) {
1880
+ const ino = TokenRegistry.inode(ref);
1881
+ const node = ino === undefined ? undefined : yield* view(ino);
1882
+ if ((!cleanup && !TokenRegistry.isOpen(ref)) || node?.kind !== "file" ||
1883
+ (access === "read" && ref.access === "write") ||
1884
+ (access === "write" && ref.access === "read"))
1885
+ return yield* op.fail("InvalidHandle");
1886
+ if (!cleanup && ref.authority !== undefined)
1887
+ yield* ref.authority(node, op);
1888
+ return node;
1889
+ });
1890
+ const read = (maximum, position) => {
1891
+ const op = OpContext.make(position === undefined ? "read" : "pread");
1892
+ const readOp = OpContext.make("read");
1893
+ const body = Effect.gen(function* () {
1894
+ const file = yield* get(op, "read");
1895
+ if (!isNatural(maximum))
1896
+ return yield* readOp.fail("InvalidArgument");
1897
+ const offset = position ?? ref.offset;
1898
+ if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
1899
+ return yield* readOp.fail("InvalidArgument");
1900
+ }
1901
+ const start = Number(offset > file.metadata.size ? file.metadata.size : offset);
1902
+ const data = file.data.slice(start, start + Math.min(maximum, file.data.length - start));
1903
+ const eof = start + data.length >= file.data.length;
1904
+ const access = maximum > 0 ? { node: file, now: yield* timestamp(readOp) } : undefined;
1905
+ return { value: { bytes: data, eof, next: offset + BigInt(data.length) }, access };
1906
+ });
1907
+ // A positioned read runs beside other reads. A cursor read stays a change, since two reads beside each
1908
+ // other would start at the same offset.
1909
+ if (position !== undefined)
1910
+ return Effect.map(accessing(op, body), ({ bytes, eof }) => ({ bytes, eof }));
1911
+ return coordinated(op, Effect.gen(function* () {
1912
+ const { bytes, eof, next } = yield* refreshAccess(yield* body);
1913
+ const draft = yield* Draft;
1914
+ yield* draft.afterInstall(() => {
1915
+ ref.offset = next;
1916
+ });
1917
+ return { bytes, eof };
1918
+ }));
1919
+ };
1920
+ const write = Effect.fnUntraced(function* (input, position) {
1921
+ const op = OpContext.make(position === undefined ? "write" : "pwrite");
1922
+ const writeOp = OpContext.make("write");
1923
+ if (!isAttachedBytes(input))
1924
+ return yield* writeOp.fail("InvalidArgument");
1925
+ const bytes = new Uint8Array(input);
1926
+ return yield* coordinated(op, Effect.gen(function* () {
1927
+ const file = yield* get(op, "write");
1928
+ const offset = position ?? (ref.append ? file.metadata.size : ref.offset);
1929
+ if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
1930
+ return yield* writeOp.fail("InvalidArgument");
1931
+ }
1932
+ if (bytes.length === 0) {
1933
+ return 0;
1934
+ }
1935
+ if (offset >= BigInt(maxFileBytes))
1936
+ return yield* writeOp.fail("FileTooLarge");
1937
+ const start = Number(offset);
1938
+ const draft = yield* Draft;
1939
+ const free = limits.maxBytes === undefined
1940
+ ? BigInt(maxFileBytes)
1941
+ : ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
1942
+ const maximumEnd = BigInt(file.data.length) + free;
1943
+ const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
1944
+ const count = Math.min(bytes.length, Math.max(0, end - start));
1945
+ if (count === 0)
1946
+ return yield* writeOp.fail("NoSpace");
1947
+ const size = Math.max(file.data.length, start + count);
1948
+ // Always detach before mutation. A same-sized write is the critical
1949
+ // case: the current payload may belong to the base or a prior capture.
1950
+ const data = new Uint8Array(size);
1951
+ data.set(file.data);
1952
+ const now = yield* timestamp(writeOp);
1953
+ data.set(bytes.subarray(0, count), start);
1954
+ yield* replaceContent(file, data, now);
1955
+ if (position === undefined) {
1956
+ const next = offset + BigInt(count);
1957
+ yield* draft.afterInstall(() => {
1958
+ ref.offset = next;
1959
+ });
1960
+ }
1961
+ return count;
1962
+ }));
1963
+ });
1964
+ const statOp = OpContext.make("stat");
1965
+ const syncOp = OpContext.make("sync");
1966
+ const closeOp = OpContext.make("close");
1967
+ const handle = Object.freeze({
1968
+ [FileHandleId]: true,
1969
+ read: Effect.fnUntraced(function* (maximum) {
1970
+ return (yield* read(maximum)).bytes;
1971
+ }),
1972
+ pread: Effect.fnUntraced(function* (maximum, offset) {
1973
+ return yield* read(maximum, offset);
1974
+ }),
1975
+ write: Effect.fnUntraced(function* (bytes) {
1976
+ return yield* write(bytes);
1977
+ }),
1978
+ pwrite: dual(2, Effect.fnUntraced(function* (bytes, offset) {
1979
+ return yield* write(bytes, offset);
1980
+ })),
1981
+ seek: dual(2, Effect.fnUntraced(function* (offset, mode) {
1982
+ const op = OpContext.make("seek");
1983
+ return yield* coordinatedRead(op, Effect.uninterruptible(Effect.gen(function* () {
1984
+ const file = yield* get(op);
1985
+ if (!Predicate.isBigInt(offset) || !isSeekMode(mode)) {
1986
+ return yield* op.fail("InvalidArgument");
1987
+ }
1988
+ let next = mode === "current"
1989
+ ? ref.offset + offset
1990
+ : mode === "end"
1991
+ ? file.metadata.size + offset
1992
+ : offset;
1993
+ if (next < 0n || next > MAX_FILE_OFFSET) {
1994
+ return yield* op.fail("InvalidArgument");
1995
+ }
1996
+ if (mode === "data" || mode === "hole") {
1997
+ if (offset >= file.metadata.size)
1998
+ return yield* op.fail("NoData");
1999
+ if (mode === "hole")
2000
+ next = file.metadata.size;
2001
+ }
2002
+ ref.offset = next;
2003
+ return next;
2004
+ })));
2005
+ })),
2006
+ truncate: Effect.fnUntraced(function* (length) {
2007
+ const op = OpContext.make("truncate");
2008
+ return yield* coordinated(op, Effect.gen(function* () {
2009
+ yield* resize(yield* get(op, "write"), length, op);
2010
+ }));
2011
+ }),
2012
+ stat: coordinatedRead(statOp, Effect.map(Effect.suspend(() => get(statOp)), withMetadata)),
2013
+ sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))),
2014
+ close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp, undefined, true))).pipe(Effect.ensuring(closeReleasedScope(ref)))
2015
+ });
2016
+ registry.registerFile(handle, ref);
2017
+ return handle;
2018
+ };
2019
+ const createContext = (reference, identity, umask, roots = [], inherited = []) => {
2020
+ const boundary = roots.length === 0 ? undefined : Confinement.make(authorityView, roots);
2021
+ inherited = inherited.filter((chain, index) => !((chain.length <= roots.length && chain.every((ino, at) => roots[at] === ino)) ||
2022
+ inherited.slice(0, index).some((other) => other.length === chain.length && chain.every((ino, at) => other[at] === ino))));
2023
+ const additional = inherited.map((chain) => Confinement.make(authorityView, chain));
2024
+ const checkAuthority = Effect.fnUntraced(function* (node, op) {
2025
+ if (boundary !== undefined)
2026
+ yield* boundary.check(node, op);
2027
+ for (const authority of additional)
2028
+ yield* authority.check(node, op);
2029
+ });
2030
+ const inheritedFor = (target) => Target.$is("Handle")(target)
2031
+ ? registry.handleBoundaries(target.handle)
2032
+ : Target.$is("Path")(target) && target.relativeTo !== undefined
2033
+ ? registry.handleBoundaries(target.relativeTo)
2034
+ : [];
2035
+ const boundariesFor = (target) => [...(roots.length > 0 ? [roots] : []), ...inherited, ...(target === undefined ? [] : inheritedFor(target))];
2036
+ const assignAuthority = (ref, target) => {
2037
+ const chains = boundariesFor(target).filter((chain, index, all) => !all.some((other, at) => at !== index && other.length >= chain.length && chain.every((ino, offset) => other[offset] === ino) &&
2038
+ (other.length > chain.length || at < index)));
2039
+ if (chains.length === 0)
2040
+ return;
2041
+ ref.boundaries = chains;
2042
+ const authorities = chains.map((chain) => Confinement.make(authorityView, chain));
2043
+ ref.authority = Effect.fnUntraced(function* (node, op) {
2044
+ for (const authority of authorities)
2045
+ yield* authority.check(node, op, true);
2046
+ });
2047
+ };
2048
+ const callerPath = boundary?.path ?? pathOfCurrent;
2049
+ const resolver = Resolution.make({
2050
+ caller: reference,
2051
+ confinement: boundary,
2052
+ additional: additional.length === 0 ? undefined : additional,
2053
+ get: view,
2054
+ authorityView,
2055
+ identity,
2056
+ maxPathBytes: limits.maxPathBytes,
2057
+ registry
2058
+ });
2059
+ const prepare = resolver.prepare;
2060
+ const prepareEntry = resolver.prepareEntry;
2061
+ const entryName = Resolution.entryName;
2062
+ const resolveTarget = (target, op, options) => resolver.resolve(target, { kind: "Node", ...options }, op);
2063
+ const resolveDirectory = resolver.directory;
2064
+ const resolveEntry = (prepared, op) => resolver.resolve(prepared, { kind: "Parent" }, op);
2065
+ const referencedNode = (target, op) => Effect.map(resolveTarget(Target.Reference({ reference: target }), op), (resolved) => resolved.node);
2066
+ const directoryNow = Effect.fnUntraced(function* (ino) {
2067
+ const node = yield* view(ino);
2068
+ if (node?.kind !== "directory") {
2069
+ return dieInvariant("Directory left the inode table during a transition");
2070
+ }
2071
+ return node;
2072
+ });
2073
+ const nodeNow = Effect.fnUntraced(function* (ino) {
2074
+ const node = yield* view(ino);
2075
+ if (node === undefined)
2076
+ return dieInvariant("Inode left the table during a transition");
2077
+ return node;
2078
+ });
2079
+ const creationTimes = (times, now) => ({
2080
+ atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
2081
+ mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
2082
+ });
2083
+ const newDirectory = Effect.fnUntraced(function* (parent, mode, now, times) {
2084
+ const draft = yield* Draft;
2085
+ const ino = yield* draft.allocate;
2086
+ return {
2087
+ kind: "directory",
2088
+ ino,
2089
+ parent: parent.ino,
2090
+ name: "",
2091
+ entries: new Map(),
2092
+ metadata: {
2093
+ ...directoryMetadata(BigInt(ino), identity.uid, parent.metadata.gid, mode, now),
2094
+ ...creationTimes(times, now)
2095
+ },
2096
+ revision: 0n
2097
+ };
2098
+ });
2099
+ const newFile = Effect.fnUntraced(function* (parent, data, mode, now, owner, times) {
2100
+ const draft = yield* Draft;
2101
+ const ino = yield* draft.allocate;
2102
+ return {
2103
+ kind: "file",
2104
+ ino,
2105
+ data,
2106
+ links: [],
2107
+ metadata: {
2108
+ ...directoryMetadata(BigInt(ino), owner?.uid ?? identity.uid, owner?.gid ?? parent.metadata.gid, mode, now),
2109
+ ...creationTimes(times, now),
2110
+ kind: "file",
2111
+ size: BigInt(data.length),
2112
+ nlink: 0
2113
+ },
2114
+ revision: 0n
2115
+ };
2116
+ });
2117
+ const newSymlink = Effect.fnUntraced(function* (parent, target, now, times) {
2118
+ const draft = yield* Draft;
2119
+ const ino = yield* draft.allocate;
2120
+ return {
2121
+ kind: "symlink",
2122
+ ino,
2123
+ target,
2124
+ links: [],
2125
+ metadata: {
2126
+ ...directoryMetadata(BigInt(ino), identity.uid, parent.metadata.gid, 0o777, now),
2127
+ ...creationTimes(times, now),
2128
+ kind: "symlink",
2129
+ nlink: 0,
2130
+ size: BigInt(target.length)
2131
+ },
2132
+ revision: 0n
2133
+ };
2134
+ });
2135
+ const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
2136
+ const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
2137
+ ? Effect.fail(op.fail("NotPermitted"))
2138
+ : Effect.succeed(grantedMode(metadata, mode));
2139
+ // Changes the attributes one change at a time would, as one change: one draft, one revision, one event.
2140
+ // Every check runs against the node before any attribute applies, ownership (NotPermitted) before
2141
+ // permission (AccessDenied), so the first failure leaves everything unchanged. The attributes apply as
2142
+ // size, owner, mode, then times, the POSIX composition of chown then chmod, so a requested mode wins over
2143
+ // the set-ID clearing that a resize or an owner change triggers; an adapter owns any protocol-specific
2144
+ // sanitising of the mode, as NFS SETATTR does after knfsd. The attributes arrive validated; an owner with
2145
+ // neither id still checks ownership, and times that omit both are no change and check nothing. An expected
2146
+ // revision is checked first, against the target as the change finds it, so a caller that decided the
2147
+ // attributes from an earlier observation learns the target moved instead of applying them to a newer state.
2148
+ const changeAttributes = (resolve, attributes, op) => {
2149
+ const { size, mode } = attributes;
2150
+ const expected = attributes.expected?.revision;
2151
+ const owner = attributes.owner === undefined ? undefined : { ...attributes.owner };
2152
+ const times = attributes.times === undefined
2153
+ ? undefined
2154
+ : { access: { ...attributes.times.access }, modification: { ...attributes.times.modification } };
2155
+ const timed = times !== undefined && (times.access.kind !== "omit" || times.modification.kind !== "omit");
2156
+ const bothNow = times?.access.kind === "now" && times.modification.kind === "now";
2157
+ const resolving = resolve();
2158
+ return coordinated(op, Effect.gen(function* () {
2159
+ const resolved = yield* resolving;
2160
+ const node = yield* nodeNow(resolved.ino);
2161
+ if (expected !== undefined && node.revision !== expected) {
2162
+ return yield* resolved.op.fail("StaleReference", { field: "expected" });
2163
+ }
2164
+ if (size !== undefined && node.kind === "symlink")
2165
+ return yield* resolved.op.fail("SymlinkLoop");
2166
+ if (size !== undefined && node.kind !== "file")
2167
+ return yield* resolved.op.fail("IsDirectory");
2168
+ const owns = identity.privileged || identity.uid === node.metadata.uid;
2169
+ if ((mode !== undefined && !owns) ||
2170
+ (owner !== undefined && !identity.privileged && (!owns ||
2171
+ (owner.uid !== undefined && owner.uid !== node.metadata.uid) ||
2172
+ (owner.gid !== undefined && !inGroup(identity, owner.gid)))) ||
2173
+ // POSIX grants write access only when both times are UTIME_NOW; every other combination that
2174
+ // changes a time, mixed ones included, needs ownership.
2175
+ (timed && !owns && !bothNow)) {
2176
+ return yield* resolved.op.fail("NotPermitted");
2177
+ }
2178
+ if (size !== undefined || (timed && !owns))
2179
+ yield* authorize(node, identity, WRITE, resolved.op);
2180
+ const chowned = owner?.uid !== undefined || owner?.gid !== undefined;
2181
+ if (size === undefined && !chowned && mode === undefined && !timed)
2182
+ return;
2183
+ const now = yield* timestamp(op);
2184
+ if (size !== undefined && node.kind === "file")
2185
+ yield* resizeAt(node, size, now, op);
2186
+ const sized = yield* nodeNow(resolved.ino);
2187
+ const gid = owner?.gid ?? sized.metadata.gid;
2188
+ const cleared = chowned && sized.kind === "file" ? sized.metadata.mode & ~SET_ID_BITS : sized.metadata.mode;
2189
+ if (chowned || mode !== undefined || timed) {
2190
+ yield* (yield* Draft).put({
2191
+ ...sized,
2192
+ metadata: {
2193
+ ...sized.metadata,
2194
+ uid: owner?.uid ?? sized.metadata.uid,
2195
+ gid,
2196
+ mode: mode === undefined ? cleared : grantedMode({ kind: sized.kind, gid }, mode),
2197
+ atimeNs: timeAt(times?.access, sized.metadata.atimeNs, now),
2198
+ mtimeNs: timeAt(times?.modification, sized.metadata.mtimeNs, now),
2199
+ ctimeNs: now
2200
+ }
2201
+ });
2202
+ }
2203
+ yield* publishNode(node.ino);
2204
+ }));
2205
+ };
2206
+ const authorizeRemoval = (parent, child, op) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
2207
+ identity.uid !== parent.metadata.uid && identity.uid !== child.metadata.uid
2208
+ ? Effect.fail(op.fail("NotPermitted"))
2209
+ : Effect.void;
2210
+ const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
2211
+ const parent = yield* directoryNow(entry.parent);
2212
+ if (boundary !== undefined && parent.ino === boundary.root &&
2213
+ (entry.name === undefined || isDotComponent(entry.name)))
2214
+ return yield* entry.op.fail("NotPermitted");
2215
+ if (isDotComponent(entry.name)) {
2216
+ return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
2217
+ }
2218
+ yield* authorize(parent, identity, EXECUTE, entry.op);
2219
+ if (parent.entries.has(entry.name))
2220
+ return yield* entry.op.fail("AlreadyExists");
2221
+ if (trailingSlash === "rejected" && entry.trailingSlash)
2222
+ return yield* entry.op.fail("NotFound");
2223
+ yield* authorize(parent, identity, WRITE, entry.op);
2224
+ return entry.name;
2225
+ });
2226
+ const makeDirectory = Effect.fnUntraced(function* (entry, request, op) {
2227
+ const name = yield* claimName(entry);
2228
+ yield* reserveEntry(entry.op);
2229
+ const parent = yield* directoryNow(entry.parent);
2230
+ const before = parent.revision;
2231
+ const now = yield* timestamp(op);
2232
+ const mode = request.exactMode
2233
+ ? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, request.mode, op)
2234
+ : (request.mode & 0o777 & ~umask) | (request.mode & STICKY_BIT);
2235
+ const child = yield* newDirectory(parent, mode, now, request.times);
2236
+ yield* attach(parent, name, child, now);
2237
+ const draft = yield* Draft;
2238
+ yield* draft.addEntries(1);
2239
+ yield* publishEntry("Create", parent.ino, name, child.ino);
2240
+ return { child: child.ino, directory: { before, after: draft.revision } };
2241
+ });
2242
+ // Creates every missing directory a path names in the running transition, so a failure anywhere discards
2243
+ // them all. The mode applies to each directory created and the times to the one in the final position. An
2244
+ // entry names one child, created unless it is already a directory. The result names the directory the path
2245
+ // ends on and its parent's revision before and after the call, so a final directory that already exists is
2246
+ // no change, while a path that leaves a directory it created, such as `new/..`, reports the creation.
2247
+ const makeDirectories = Effect.fnUntraced(function* (prepared, request, op) {
2248
+ let node;
2249
+ let entryOp = op;
2250
+ const revisionsBefore = new Map();
2251
+ if (prepared.kind === "entry") {
2252
+ const entry = yield* resolveEntry(prepared, op);
2253
+ const made = yield* Effect.result(makeDirectory(entry, request, op));
2254
+ if (Result.isSuccess(made))
2255
+ return made.success;
2256
+ const existingIno = entry.name === undefined
2257
+ ? undefined
2258
+ : (yield* directoryNow(entry.parent)).entries.get(entry.name);
2259
+ node = existingIno === undefined ? undefined : (yield* view(existingIno));
2260
+ if (made.failure.code !== "AlreadyExists" || node?.kind !== "directory")
2261
+ return yield* made.failure;
2262
+ }
2263
+ else {
2264
+ entryOp = op.at(prepared.path.input);
2265
+ const resolved = yield* resolver.resolve(Target.Path(prepared.base === undefined
2266
+ ? { path: prepared.path.input }
2267
+ : { path: prepared.path.input, relativeTo: prepared.base }), {
2268
+ kind: "Node",
2269
+ createMissing: (parent, name, final) => Effect.flatMap(makeDirectory({ parent: parent.ino, name, trailingSlash: false, addressing: "path", op: entryOp }, final ? request : { mode: request.mode, exactMode: request.exactMode }, op), (made) => {
2270
+ if (!revisionsBefore.has(parent.ino))
2271
+ revisionsBefore.set(parent.ino, made.directory.before);
2272
+ return directoryNow(made.child);
2273
+ })
2274
+ }, op);
2275
+ node = resolved.node;
2276
+ }
2277
+ if (node?.kind !== "directory")
2278
+ return yield* entryOp.fail("AlreadyExists");
2279
+ const after = (yield* directoryNow(node.parent)).revision;
2280
+ return { child: node.ino, directory: { before: revisionsBefore.get(node.parent) ?? after, after } };
2281
+ });
2282
+ const linkNode = Effect.fnUntraced(function* (node, entry, op) {
2283
+ const name = yield* claimName(entry, "rejected");
2284
+ yield* reserveEntry(entry.op);
2285
+ const parent = yield* directoryNow(entry.parent);
2286
+ const before = parent.revision;
2287
+ const now = yield* timestamp(op);
2288
+ yield* attach(parent, name, node, now);
2289
+ const draft = yield* Draft;
2290
+ yield* draft.addEntries(1);
2291
+ yield* publishEntry("Create", parent.ino, name, node.ino);
2292
+ return { before, after: draft.revision };
2293
+ });
2294
+ const makeSymlink = Effect.fnUntraced(function* (entry, target, times, op) {
2295
+ const name = yield* claimName(entry, "rejected");
2296
+ yield* reserveEntry(entry.op);
2297
+ yield* reserveBytes(entry.op, BigInt(target.length));
2298
+ const parent = yield* directoryNow(entry.parent);
2299
+ const before = parent.revision;
2300
+ const now = yield* timestamp(op);
2301
+ const child = yield* newSymlink(parent, target, now, times);
2302
+ yield* attach(parent, name, child, now);
2303
+ const draft = yield* Draft;
2304
+ yield* draft.addEntries(1);
2305
+ yield* draft.addUsedBytes(BigInt(target.length));
2306
+ yield* publishEntry("Create", parent.ino, name, child.ino);
2307
+ return { child: child.ino, directory: { before, after: draft.revision } };
2308
+ });
2309
+ const removeChild = Effect.fnUntraced(function* (parent, name, child, op) {
2310
+ const before = parent.revision;
2311
+ const now = yield* timestamp(op);
2312
+ const d = yield* Draft;
2313
+ yield* d.put(withEntries({
2314
+ ...parent,
2315
+ metadata: {
2316
+ ...parent.metadata,
2317
+ nlink: parent.metadata.nlink - (child.kind === "directory" ? 1 : 0),
2318
+ mtimeNs: now,
2319
+ ctimeNs: now
2320
+ }
2321
+ }, DirectoryEntries.remove(parent.entries, name)));
2322
+ yield* publishEntry("Remove", parent.ino, name, child.ino);
2323
+ yield* detach(child, parent.ino, name, now);
2324
+ yield* d.addEntries(-1);
2325
+ return { before, after: d.revision };
2326
+ });
2327
+ // Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
2328
+ // entry, and each verb reports it with its own code. Search permission comes before the lookup and write
2329
+ // permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
2330
+ const protectsRoot = (parent, name) => boundary !== undefined &&
2331
+ ((parent.ino === boundary.root && (name === undefined || isDotComponent(name))) ||
2332
+ (name === "2e2e" && parent.parent === boundary.root));
2333
+ const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
2334
+ const parent = yield* directoryNow(entry.parent);
2335
+ if (protectsRoot(parent, entry.name))
2336
+ return yield* entry.op.fail("NotPermitted");
2337
+ if (isDotComponent(entry.name)) {
2338
+ return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
2339
+ }
2340
+ yield* authorize(parent, identity, EXECUTE, entry.op);
2341
+ const childIno = parent.entries.get(entry.name);
2342
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
2343
+ if (child === undefined)
2344
+ return yield* entry.op.fail("NotFound");
2345
+ if (child.ino === boundary?.root)
2346
+ return yield* entry.op.fail("NotPermitted");
2347
+ const rejected = beforeWrite?.(child);
2348
+ if (rejected !== undefined)
2349
+ return yield* rejected;
2350
+ yield* authorize(parent, identity, WRITE, entry.op);
2351
+ return { parent, name: entry.name, child };
2352
+ });
2353
+ const removeEntry = Effect.fnUntraced(function* (entry, op) {
2354
+ const { child, name, parent } = yield* removalTarget(entry, "InvalidArgument", (found) => entry.trailingSlash && found.kind !== "directory" ? entry.op.fail("NotDirectory") : undefined);
2355
+ yield* authorizeRemoval(parent, child, entry.op);
2356
+ if (child.kind === "directory" && child.entries.size > 0)
2357
+ return yield* entry.op.fail("NotEmpty");
2358
+ return yield* removeChild(parent, name, child, op);
2359
+ });
2360
+ const unlinkEntry = Effect.fnUntraced(function* (entry, op) {
2361
+ const { child, name, parent } = yield* removalTarget(entry, "IsDirectory", (found) => !entry.trailingSlash
2362
+ ? undefined
2363
+ : entry.op.fail(found.kind === "directory" ? "IsDirectory" : "NotDirectory"));
2364
+ if (child.kind === "directory")
2365
+ return yield* entry.op.fail("IsDirectory");
2366
+ yield* authorizeRemoval(parent, child, entry.op);
2367
+ return yield* removeChild(parent, name, child, op);
2368
+ });
2369
+ const rmdirEntry = Effect.fnUntraced(function* (entry, op) {
2370
+ const { child, name, parent } = yield* removalTarget(entry, "InvalidArgument");
2371
+ yield* authorizeRemoval(parent, child, entry.op);
2372
+ if (child.kind !== "directory")
2373
+ return yield* entry.op.fail("NotDirectory");
2374
+ if (child.entries.size > 0)
2375
+ return yield* entry.op.fail("NotEmpty");
2376
+ return yield* removeChild(parent, name, child, op);
2377
+ });
2378
+ const renameEntry = Effect.fnUntraced(function* (source, destination, op) {
2379
+ const sourceDirectory = yield* directoryNow(source.parent);
2380
+ const destinationDirectory = yield* directoryNow(destination.parent);
2381
+ const sameDirectory = source.parent === destination.parent;
2382
+ const sourceName = source.name;
2383
+ const destinationName = destination.name;
2384
+ if (protectsRoot(sourceDirectory, sourceName) || protectsRoot(destinationDirectory, destinationName))
2385
+ return yield* source.op.fail("NotPermitted");
2386
+ if (isDotComponent(sourceName) || isDotComponent(destinationName)) {
2387
+ return yield* source.op.fail("InvalidArgument");
2388
+ }
2389
+ // Linux's order: search permission on both directories before either name is looked up; the names,
2390
+ // trailing slashes, the subtree rule and the same-object no-op; then write permission and the rest.
2391
+ yield* authorize(sourceDirectory, identity, EXECUTE, source.op);
2392
+ yield* authorize(destinationDirectory, identity, EXECUTE, destination.op);
2393
+ const sourceBefore = sourceDirectory.revision;
2394
+ const destinationBefore = destinationDirectory.revision;
2395
+ const childIno = sourceDirectory.entries.get(sourceName);
2396
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
2397
+ if (child === undefined)
2398
+ return yield* source.op.fail("NotFound");
2399
+ const replacedIno = destinationDirectory.entries.get(destinationName);
2400
+ const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
2401
+ if (boundary !== undefined && (child.ino === boundary.root || replaced?.ino === boundary.root)) {
2402
+ return yield* source.op.fail("NotPermitted");
2403
+ }
2404
+ // A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
2405
+ // source is one, as Linux allows.
2406
+ if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
2407
+ return yield* (source.trailingSlash ? source.op : destination.op).fail("NotDirectory");
2408
+ }
2409
+ if (destination.trailingSlash && replaced !== undefined && replaced.kind !== "directory") {
2410
+ return yield* destination.op.fail("NotDirectory");
2411
+ }
2412
+ const result = Effect.fnUntraced(function* () {
2413
+ return sameDirectory
2414
+ ? {
2415
+ _tag: "SameDirectory",
2416
+ directory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision }
2417
+ }
2418
+ : {
2419
+ _tag: "DifferentDirectories",
2420
+ sourceDirectory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision },
2421
+ destinationDirectory: {
2422
+ before: destinationBefore,
2423
+ after: (yield* directoryNow(destination.parent)).revision
2424
+ }
2425
+ };
2426
+ });
2427
+ for (let ancestor = destinationDirectory;; ancestor = yield* directoryNow(ancestor.parent)) {
2428
+ if (ancestor.ino === child.ino)
2429
+ return yield* destination.op.fail("InvalidArgument");
2430
+ if (ancestor.ino === ROOT_INO)
2431
+ break;
2432
+ }
2433
+ if (child.ino === replaced?.ino)
2434
+ return yield* result();
2435
+ yield* authorize(sourceDirectory, identity, WRITE, source.op);
2436
+ yield* authorize(destinationDirectory, identity, WRITE, destination.op);
2437
+ yield* authorizeRemoval(sourceDirectory, child, source.op);
2438
+ if (replaced !== undefined) {
2439
+ yield* authorizeRemoval(destinationDirectory, replaced, destination.op);
2440
+ if (child.kind === "directory" && replaced.kind !== "directory") {
2441
+ return yield* destination.op.fail("NotDirectory");
2442
+ }
2443
+ if (child.kind !== "directory" && replaced.kind === "directory") {
2444
+ return yield* destination.op.fail("IsDirectory");
2445
+ }
2446
+ if (replaced.kind === "directory" && replaced.entries.size > 0) {
2447
+ return yield* destination.op.fail("NotEmpty");
2448
+ }
2449
+ }
2450
+ const now = yield* timestamp(op);
2451
+ const d = yield* Draft;
2452
+ yield* d.rename({
2453
+ ino: child.ino,
2454
+ from: { parent: source.parent, name: sourceName },
2455
+ to: { parent: destination.parent, name: destinationName }
2456
+ });
2457
+ yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
2458
+ yield* d.put(withEntries({
2459
+ ...sourceDirectory,
2460
+ metadata: {
2461
+ ...sourceDirectory.metadata,
2462
+ nlink: sourceDirectory.metadata.nlink - (child.kind === "directory" ? 1 : 0),
2463
+ mtimeNs: now,
2464
+ ctimeNs: now
2465
+ }
2466
+ }, DirectoryEntries.remove(sourceDirectory.entries, sourceName)));
2467
+ const destinationNow = yield* directoryNow(destination.parent);
2468
+ yield* d.put(withEntries({
2469
+ ...destinationNow,
2470
+ metadata: {
2471
+ ...destinationNow.metadata,
2472
+ nlink: destinationNow.metadata.nlink + (child.kind === "directory" && replaced === undefined ? 1 : 0),
2473
+ mtimeNs: now,
2474
+ ctimeNs: now
2475
+ }
2476
+ }, DirectoryEntries.set(destinationNow.entries, destinationName, child.ino)));
2477
+ const moved = yield* nodeNow(child.ino);
2478
+ if (moved.kind === "directory") {
2479
+ yield* d.put({
2480
+ ...moved,
2481
+ parent: destination.parent,
2482
+ name: destinationName,
2483
+ metadata: { ...moved.metadata, ctimeNs: now }
2484
+ });
2485
+ }
2486
+ else {
2487
+ yield* d.put({
2488
+ ...moved,
2489
+ links: [
2490
+ ...withoutLink(moved.links, source.parent, sourceName),
2491
+ { parent: destination.parent, name: destinationName }
2492
+ ],
2493
+ metadata: { ...moved.metadata, ctimeNs: now }
2494
+ });
2495
+ }
2496
+ if (replaced !== undefined) {
2497
+ yield* detach(yield* nodeNow(replaced.ino), destination.parent, destinationName, now);
2498
+ yield* d.addEntries(-1);
2499
+ }
2500
+ yield* publishEntry("Create", destination.parent, destinationName, child.ino);
2501
+ return yield* result();
2502
+ });
2503
+ const openExisting = Effect.fnUntraced(function* (file, request, at, op) {
2504
+ yield* authorize(file, identity, request.access === "read" ? READ : request.access === "write" ? WRITE : READ | WRITE, at);
2505
+ if (request.truncate)
2506
+ yield* resize(file, 0n, op);
2507
+ });
2508
+ const openFile = Effect.fnUntraced(function* (entry, found, request, acquired, op) {
2509
+ const { name } = entry;
2510
+ const parent = yield* directoryNow(entry.parent);
2511
+ const before = parent.revision;
2512
+ let file = found;
2513
+ let created = false;
2514
+ if (file === undefined) {
2515
+ if (request.create === undefined || request.create === "never")
2516
+ return yield* entry.op.fail("NotFound");
2517
+ yield* authorize(parent, identity, WRITE | EXECUTE, entry.op);
2518
+ yield* reserveEntry(entry.op);
2519
+ const size = request.initialSize ?? 0n;
2520
+ if (request.initialSize !== undefined) {
2521
+ if (size < 0n)
2522
+ return yield* entry.op.fail("InvalidArgument");
2523
+ if (size > BigInt(maxFileBytes))
2524
+ return yield* entry.op.fail("FileTooLarge");
2525
+ yield* reserveBytes(entry.op, size);
2526
+ }
2527
+ const owner = request.owner;
2528
+ if (!identity.privileged &&
2529
+ ((owner?.uid !== undefined && owner.uid !== identity.uid) ||
2530
+ (owner?.gid !== undefined && !inGroup(identity, owner.gid)))) {
2531
+ return yield* entry.op.fail("NotPermitted");
2532
+ }
2533
+ const now = yield* timestamp(op);
2534
+ const mode = request.exactMode
2535
+ ? yield* permittedMode({ kind: "file", uid: owner?.uid ?? identity.uid, gid: owner?.gid ?? parent.metadata.gid }, request.mode, op)
2536
+ : (request.mode ?? 0o666) & 0o777 & ~umask;
2537
+ file = yield* newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
2538
+ yield* attach(parent, name, file, now);
2539
+ const draft = yield* Draft;
2540
+ yield* draft.addEntries(1);
2541
+ yield* draft.addUsedBytes(size);
2542
+ created = true;
2543
+ yield* publishEntry("Create", parent.ino, name, file.ino);
2544
+ }
2545
+ else {
2546
+ if (file.kind === "symlink")
2547
+ return yield* entry.op.fail("SymlinkLoop");
2548
+ if (file.kind !== "file")
2549
+ return yield* entry.op.fail("IsDirectory");
2550
+ yield* openExisting(file, request, entry.op, op);
2551
+ }
2552
+ yield* (yield* Draft).retain(file.ino);
2553
+ yield* bindFile(acquired, file.ino);
2554
+ return { ino: file.ino, created, directory: { before, after: (yield* directoryNow(entry.parent)).revision } };
2555
+ });
2556
+ const releasePending = (opened) => () => Effect.gen(function* () {
2557
+ const ino = opened();
2558
+ if (ino === undefined)
2559
+ return;
2560
+ yield* releaseFileInode(ino);
2561
+ });
2562
+ const acquireOpenedFile = (ref, op, acquire) => {
2563
+ let opened;
2564
+ return Effect.andThen(preflight(op), acquireHandle(ref, (effect) => coordinated(op, effect), acquire((ino) => {
2565
+ opened = ino;
2566
+ }), releasePending(() => opened), finalizeFile(ref)));
2567
+ };
2568
+ const asResolvedNode = (target, op, options) => Effect.map(resolveTarget(target, op, options), (resolved) => ({ ino: resolved.node.ino, op: resolved.op }));
2569
+ const asTarget = (input) => Target.of(input);
2570
+ const acquireDirectory = Effect.fnUntraced(function* (input, op) {
2571
+ yield* preflight(op);
2572
+ const target = asTarget(input);
2573
+ const acquired = makeDirectoryReference();
2574
+ return yield* acquireHandle(acquired, (acquire) => coordinatedRead(op, Effect.uninterruptible(acquire)), Effect.gen(function* () {
2575
+ const directory = yield* resolveDirectory(target, op);
2576
+ yield* authorize(directory.node, identity, EXECUTE, directory.op);
2577
+ assignAuthority(acquired, target);
2578
+ const candidates = acquired.boundaries ?? [];
2579
+ if (candidates.length === 1)
2580
+ acquired.namespaceRoots = candidates[0];
2581
+ let current = directory.node;
2582
+ while (candidates.length > 1) {
2583
+ const nearest = candidates.find((chain) => chain.at(-1) === current.ino);
2584
+ if (nearest !== undefined) {
2585
+ acquired.namespaceRoots = nearest;
2586
+ break;
2587
+ }
2588
+ if (current.ino === ROOT_INO)
2589
+ break;
2590
+ current = yield* directoryNow(current.parent);
2591
+ }
2592
+ holdDirectory(directory.node.ino);
2593
+ TokenRegistry.publish(acquired, directory.node.ino);
2594
+ return acquired;
2595
+ }),
2596
+ // Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
2597
+ // every permit, where a detached directory may leave the table.
2598
+ () => Effect.void, finalizeDirectory(acquired));
2599
+ });
2600
+ const fail = (op, cause, field) => op.fail("InvalidArgument", field === undefined ? { cause } : { cause, field });
2601
+ const preparedOp = (prepared, op) => prepared.kind === "path" ? op.at(prepared.path.input) : op;
2602
+ const openPath = Effect.fnUntraced(function* (target, options, op) {
2603
+ const pathOp = op.at(target.path);
2604
+ const prepared = prepare(target.path, op);
2605
+ const chosen = yield* decodeOpenOptions(options).pipe(Effect.mapError((cause) => fail(pathOp, cause)));
2606
+ if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
2607
+ return yield* pathOp.fail("InvalidArgument");
2608
+ }
2609
+ if (chosen.mode !== undefined && (chosen.create === undefined || chosen.create === "never")) {
2610
+ return yield* pathOp.fail("InvalidArgument");
2611
+ }
2612
+ const acquired = makeFileReference(chosen.access, chosen.append ?? false);
2613
+ const follow = target.followFinalSymlink ?? true;
2614
+ return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
2615
+ const path = yield* Effect.fromResult(prepared);
2616
+ const resolved = yield* resolver.resolve({ kind: "path", path, base: target.relativeTo }, {
2617
+ kind: "OrCreate",
2618
+ create: chosen.create ?? "never",
2619
+ finalSymlink: follow ? "follow" : "preserve"
2620
+ }, op);
2621
+ const { parent, name } = resolved;
2622
+ if (resolved.kind === "missing" && path.trailingSlash)
2623
+ return yield* pathOp.fail("IsDirectory");
2624
+ yield* authorize(parent, identity, EXECUTE, pathOp);
2625
+ const file = resolved.kind === "existing" ? resolved.node : undefined;
2626
+ const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
2627
+ opened(result.ino);
2628
+ assignAuthority(acquired, target);
2629
+ return fileHandle(acquired);
2630
+ }));
2631
+ });
2632
+ const openNode = Effect.fnUntraced(function* (target, options, op) {
2633
+ const chosen = yield* decodeOpenOptions(options).pipe(Effect.mapError((cause) => fail(op, cause)));
2634
+ if (chosen.access === "read" && (chosen.append || chosen.truncate))
2635
+ return yield* op.fail("InvalidArgument");
2636
+ if ((chosen.create !== undefined && chosen.create !== "never") || chosen.mode !== undefined) {
2637
+ return yield* op.fail("InvalidArgument");
2638
+ }
2639
+ const acquired = makeFileReference(chosen.access, chosen.append ?? false);
2640
+ return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
2641
+ const resolved = yield* resolveTarget(target, op);
2642
+ const node = resolved.node;
2643
+ if (node.kind !== "file")
2644
+ return yield* resolved.op.fail("IsDirectory");
2645
+ if (node.metadata.nlink === 0)
2646
+ return yield* resolved.op.fail("StaleReference");
2647
+ yield* openExisting(node, chosen, resolved.op, op);
2648
+ const draft = yield* Draft;
2649
+ yield* draft.retain(node.ino);
2650
+ yield* bindFile(acquired, node.ino);
2651
+ opened(node.ino);
2652
+ assignAuthority(acquired, target);
2653
+ return fileHandle(acquired);
2654
+ }));
2655
+ });
2656
+ const openEntry = Effect.fnUntraced(function* (entry, options, op) {
2657
+ const name = yield* Effect.fromResult(entryName(entry.name, op));
2658
+ const decoded = yield* decodeOpenEntryOptions(options).pipe(Effect.mapError((cause) => fail(op, cause)));
2659
+ const chosen = { ...decoded };
2660
+ if (chosen.access === "read" && (chosen.append || chosen.truncate))
2661
+ return yield* op.fail("InvalidArgument");
2662
+ if ((chosen.mode !== undefined || chosen.times !== undefined || chosen.initialSize !== undefined ||
2663
+ chosen.exactMode !== undefined || chosen.owner !== undefined) &&
2664
+ (chosen.create === undefined || chosen.create === "never")) {
2665
+ return yield* op.fail("InvalidArgument");
2666
+ }
2667
+ if (chosen.exactMode && chosen.mode === undefined)
2668
+ return yield* op.fail("InvalidArgument");
2669
+ const acquired = makeFileReference(chosen.access, chosen.append ?? false);
2670
+ const expected = chosen.expected;
2671
+ return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
2672
+ const resolved = yield* resolver.resolve({ kind: "entry", directory: entry.directory, name }, {
2673
+ kind: "OrCreate",
2674
+ create: chosen.create ?? "never",
2675
+ finalSymlink: chosen.followFinalSymlink !== false ? "follow" : "preserve",
2676
+ beforeFollow: Effect.fnUntraced(function* (_, direct) {
2677
+ if (expected !== undefined) {
2678
+ let expectedIno;
2679
+ if (expected !== null) {
2680
+ const observed = yield* Effect.result(referencedNode(expected, op));
2681
+ if (Result.isFailure(observed)) {
2682
+ return yield* op.fail("VolumeBusy", { cause: observed.failure });
2683
+ }
2684
+ expectedIno = observed.success.ino;
2685
+ }
2686
+ if (direct?.ino !== expectedIno)
2687
+ return yield* op.fail("VolumeBusy");
2688
+ }
2689
+ if (chosen.expectedChild === null) {
2690
+ if (direct !== undefined)
2691
+ return yield* op.fail("StaleReference");
2692
+ }
2693
+ else if (chosen.expectedChild !== undefined) {
2694
+ const expectedChild = chosen.expectedChild;
2695
+ const observed = yield* referencedNode(expectedChild.reference, op);
2696
+ if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
2697
+ observed.metadata.atimeNs !== expectedChild.atimeNs ||
2698
+ observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
2699
+ return yield* op.fail("StaleReference");
2700
+ }
2701
+ }
2702
+ })
2703
+ }, op);
2704
+ const file = resolved.kind === "existing" ? resolved.node : undefined;
2705
+ const mutationParent = resolved.kind === "missing" ? resolved.parent.ino : resolved.origin.parent.ino;
2706
+ const mutationName = resolved.kind === "missing" ? resolved.name : resolved.origin.name;
2707
+ const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
2708
+ opened(result.ino);
2709
+ return {
2710
+ handle: (() => {
2711
+ assignAuthority(acquired, entry.directory);
2712
+ return fileHandle(acquired);
2713
+ })(),
2714
+ reference: referenceFor(result.ino),
2715
+ created: result.created,
2716
+ directory: result.directory
2717
+ };
2718
+ }));
2719
+ });
2720
+ const walkChildren = Effect.fnUntraced(function* (directory, path, depth, up) {
2721
+ const listed = referenceFor(directory.ino);
2722
+ const frames = [];
2723
+ for (const [name, childIno] of Arr.sort(directory.entries, byEntryName)) {
2724
+ const child = yield* view(childIno);
2725
+ if (child === undefined)
2726
+ continue;
2727
+ const bytes = nameBytes(name);
2728
+ frames.push({
2729
+ ino: child.ino,
2730
+ kind: child.kind,
2731
+ name: bytes,
2732
+ key: name,
2733
+ path: joinPath(path, bytes),
2734
+ parent: directory.ino,
2735
+ up,
2736
+ depth: depth + 1,
2737
+ bytes: child.kind === "directory" ? 0n : child.metadata.size,
2738
+ reference: referenceFor(child.ino),
2739
+ directory: listed,
2740
+ listed: false
2741
+ });
2742
+ }
2743
+ return frames;
2744
+ });
2745
+ const anchorFrame = (parent, key, root) => ({
2746
+ ino: root.ino,
2747
+ kind: root.kind,
2748
+ name: nameBytes(key),
2749
+ key,
2750
+ path: new Uint8Array(0),
2751
+ parent: parent.ino,
2752
+ up: undefined,
2753
+ depth: 0,
2754
+ bytes: 0n,
2755
+ reference: referenceFor(root.ino),
2756
+ directory: referenceFor(parent.ino),
2757
+ listed: false
2758
+ });
2759
+ const pathAnchor = (target, root, op) => {
2760
+ if (!Target.$is("Path")(target))
2761
+ return Effect.undefined;
2762
+ const prepared = prepare(target.path, op);
2763
+ if (Result.isFailure(prepared))
2764
+ return Effect.undefined;
2765
+ return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
2766
+ return entry.name !== undefined && parent?.kind === "directory" &&
2767
+ parent.entries.get(entry.name) === root.ino
2768
+ ? anchorFrame(parent, entry.name, root)
2769
+ : undefined;
2770
+ })), Effect.catch(() => Effect.undefined));
2771
+ };
2772
+ const holds = (parent, frame) => parent?.kind === "directory" && parent.metadata.nlink > 0 && parent.entries.get(frame.key) === frame.ino;
2773
+ // The node a frame names, reached by name from the walk's root as a path lookup reaches it: every directory on
2774
+ // the way must be searchable and must still hold the name the walk listed for the object it listed. A frame
2775
+ // whose chain no longer holds it, such as a directory renamed out of the tree, reaches nothing. A root the
2776
+ // walk reached by name is anchored by that name, so a root moved away reaches nothing too; a root reached
2777
+ // through a reference or a handle is held by the object it resolved to, as a descriptor holds it.
2778
+ // It steps down the chain in a loop, so a tree of any depth reaches its frames without deepening the stack.
2779
+ const reachFrame = (frame, at) => Effect.gen(function* () {
2780
+ if (boundary !== undefined)
2781
+ yield* boundary.alive(at);
2782
+ const chain = [];
2783
+ for (let link = frame; link !== undefined; link = link.up)
2784
+ chain.push(link);
2785
+ let node = yield* view((chain.at(-1) ?? frame).parent);
2786
+ if (node !== undefined)
2787
+ yield* checkAuthority(node, at);
2788
+ for (let index = chain.length - 1; index >= 0; index--) {
2789
+ const link = chain[index];
2790
+ if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
2791
+ return undefined;
2792
+ yield* authorize(node, identity, EXECUTE, at);
2793
+ node = node.entries.get(link.key) === link.ino ? (yield* view(link.ino)) : undefined;
2794
+ }
2795
+ return node;
2796
+ });
2797
+ // Walks the tree below the directory `first` lists, depth first. Every later directory is read in its own
2798
+ // observation, so a walk holds one permit at a time and never a handle; it writes nothing, so it refreshes no
2799
+ // access time. Each directory is reached by name, so it needs search permission on the directories above it,
2800
+ // and one that left the tree after it was listed has nothing to walk. A directory past `maxDepth` is never
2801
+ // read. `locate` names an entry's path in a failure, and `listable` authorizes reading a directory. Entries
2802
+ // gathered before a failure are handed on before the failure is.
2803
+ const walkFrames = (op, first, plan, locate, listable) => Stream.suspend(() => {
2804
+ let pending;
2805
+ let entries = 0;
2806
+ let bytes = 0n;
2807
+ let failure;
2808
+ const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
2809
+ if (TokenRegistry.inode(reference) === undefined)
2810
+ return Effect.fail(op.fail("ClosedCaller"));
2811
+ const at = op.at(locate(frame.path));
2812
+ return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
2813
+ ? Effect.succeed([])
2814
+ : Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
2815
+ }));
2816
+ const admit = (frame) => {
2817
+ entries++;
2818
+ bytes += frame.bytes;
2819
+ return plan.limit(frame, { entries, bytes }, "entry");
2820
+ };
2821
+ const step = Effect.gen(function* () {
2822
+ // Preserve the generic limit error E; yielding the error directly widens it to WalkFailure.
2823
+ // oxlint-disable-next-line effecttsgo/unnecessary-fail-yieldable-error -- E must stay narrow for unbounded removal.
2824
+ if (failure !== undefined)
2825
+ return yield* Effect.fail(failure);
2826
+ if (pending === undefined)
2827
+ pending = (yield* first).reverse();
2828
+ const out = [];
2829
+ const stop = (error) => {
2830
+ if (out.length === 0)
2831
+ return Effect.fail(error);
2832
+ failure = error;
2833
+ return Effect.succeed([out, Option.some(undefined)]);
2834
+ };
2835
+ while (out.length < WALK_CHUNK_ENTRIES) {
2836
+ const frame = pending.pop();
2837
+ if (frame === undefined)
2838
+ return [out, Option.none()];
2839
+ const unlisted = frame.kind === "directory" && !frame.listed;
2840
+ if (!unlisted || plan.order === "pre") {
2841
+ const rejected = admit(frame);
2842
+ if (rejected !== undefined)
2843
+ return yield* stop(rejected);
2844
+ out.push(frame);
2845
+ }
2846
+ if (unlisted) {
2847
+ const rejected = plan.limit(frame, { entries, bytes }, "directory");
2848
+ if (rejected !== undefined)
2849
+ return yield* stop(rejected);
2850
+ const listed = yield* Effect.result(list(frame));
2851
+ if (Result.isFailure(listed))
2852
+ return yield* stop(listed.failure);
2853
+ if (plan.order === "post")
2854
+ pending.push({ ...frame, listed: true });
2855
+ for (let index = listed.success.length - 1; index >= 0; index--) {
2856
+ const child = listed.success[index];
2857
+ if (child !== undefined)
2858
+ pending.push(child);
2859
+ }
2860
+ return [out, Option.some(undefined)];
2861
+ }
2862
+ }
2863
+ return [out, Option.some(undefined)];
2864
+ });
2865
+ return Stream.paginate(undefined, () => step);
2866
+ });
2867
+ // Empties the directory an entry names, entries before their directories, each removal its own change. Each
2868
+ // entry is removed by its name in the directory the walk reached by name from the target's name, and only
2869
+ // while that name still holds the object the walk listed, so a subtree renamed out of the target, the target
2870
+ // renamed away, and a replacement created under a listed name are all left alone. It stops at the first
2871
+ // failure, which names the entry's path under the one the caller gave. Removing an empty directory reveals nothing, so only a directory with entries must be readable, and
2872
+ // no permission is changed to make one so. With `force`, the entry itself going missing leaves nothing to
2873
+ // empty.
2874
+ const emptyDirectory = Effect.fnUntraced(function* (prepared, op, force, guard) {
2875
+ const prefix = prepared.kind === "path" ? prepared.path.bytes : nameBytes(prepared.name);
2876
+ const locate = (path) => ownedPath(joinPath(prefix, path));
2877
+ const targetGuard = guard.pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.void));
2878
+ const listable = (directory, at) => Effect.andThen(targetGuard, directory.entries.size === 0 ? Effect.void : authorize(directory, identity, READ, at));
2879
+ let anchor;
2880
+ let targetAt = preparedOp(prepared, op);
2881
+ const first = coordinatedRead(op, Effect.gen(function* () {
2882
+ yield* guard;
2883
+ const entry = yield* resolveEntry(prepared, op);
2884
+ const parent = yield* directoryNow(entry.parent);
2885
+ yield* authorize(parent, identity, EXECUTE, entry.op);
2886
+ const childIno = entry.name === undefined ? undefined : parent.entries.get(entry.name);
2887
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
2888
+ if (entry.name === undefined || child === undefined)
2889
+ return yield* entry.op.fail("NotFound");
2890
+ if (child.kind !== "directory")
2891
+ return [];
2892
+ yield* listable(child, entry.op);
2893
+ anchor = anchorFrame(parent, entry.name, child);
2894
+ targetAt = entry.op;
2895
+ return yield* walkChildren(child, new Uint8Array(0), 0, anchor);
2896
+ })).pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.succeed([])));
2897
+ const seams = yield* VolumeTestSeams;
2898
+ const removeFrame = (frame) => {
2899
+ const at = op.at(locate(frame.path));
2900
+ return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.gen(function* () {
2901
+ yield* targetGuard;
2902
+ if (anchor !== undefined && !holds(yield* view(anchor.parent), anchor)) {
2903
+ return yield* (force ? Effect.void : Effect.fail(targetAt.fail("NotFound")));
2904
+ }
2905
+ return yield* Effect.flatMap(frame.up === undefined ? view(frame.parent) : reachFrame(frame.up, at), (parent) => !holds(parent, frame)
2906
+ ? Effect.fail(at.fail("NotFound"))
2907
+ : removeEntry({
2908
+ parent: parent.ino,
2909
+ name: frame.key,
2910
+ trailingSlash: false,
2911
+ addressing: "entry",
2912
+ op: at
2913
+ }, op));
2914
+ })));
2915
+ };
2916
+ const plan = { order: "post", limit: () => undefined };
2917
+ return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
2918
+ });
2919
+ const rootOp = OpContext.make("root");
2920
+ const entryVerb = Effect.fnUntraced(function* (operation, input, body) {
2921
+ const op = OpContext.make(operation);
2922
+ const prepared = yield* Effect.fromResult(prepareEntry(input, op));
2923
+ return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
2924
+ });
2925
+ const openDispatch = Effect.fnUntraced(function* (input, options) {
2926
+ const op = OpContext.make("open");
2927
+ if (isEntry(input))
2928
+ return yield* openEntry(input, options, op);
2929
+ const target = asTarget(input);
2930
+ if (Target.$is("Path")(target))
2931
+ return yield* openPath(target, options, op);
2932
+ return yield* openNode(target, options, op);
2933
+ });
2934
+ function open(input, options) {
2935
+ return openDispatch(input, options);
2936
+ }
2937
+ const removeDispatch = Effect.fnUntraced(function* (input, options) {
2938
+ const op = OpContext.make("remove");
2939
+ const prepared = yield* Effect.fromResult(prepareEntry(input, op));
2940
+ const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
2941
+ const imported = prepared.kind === "entry"
2942
+ ? inheritedFor(prepared.directory)
2943
+ : prepared.base === undefined
2944
+ ? []
2945
+ : registry.handleBoundaries(prepared.base);
2946
+ const guard = chosen.expected === undefined && imported.length === 0 ? Effect.void : Effect.gen(function* () {
2947
+ const entry = yield* resolveEntry(prepared, op);
2948
+ const parent = yield* directoryNow(entry.parent);
2949
+ if (protectsRoot(parent, entry.name))
2950
+ return yield* entry.op.fail("NotPermitted");
2951
+ if (chosen.expected === undefined)
2952
+ return;
2953
+ const expected = yield* registry.referenceIdentity(chosen.expected, op);
2954
+ const expectedNode = yield* view(expected);
2955
+ if (expectedNode !== undefined && expectedNode.metadata.nlink > 0)
2956
+ yield* checkAuthority(expectedNode, op);
2957
+ yield* authorize(parent, identity, EXECUTE, entry.op);
2958
+ const found = entry.name === undefined ? parent.ino : parent.entries.get(entry.name);
2959
+ if (found === undefined)
2960
+ return yield* entry.op.fail("NotFound");
2961
+ if (found !== expected)
2962
+ return yield* entry.op.fail("VolumeBusy", { field: "expected" });
2963
+ });
2964
+ const once = coordinated(op, Effect.andThen(guard, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op))));
2965
+ const target = (effect) => chosen.force === true
2966
+ ? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
2967
+ : effect;
2968
+ const removed = yield* Effect.result(target(once));
2969
+ if (Result.isSuccess(removed))
2970
+ return removed.success;
2971
+ if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
2972
+ return yield* removed.failure;
2973
+ yield* emptyDirectory(prepared, op, chosen.force === true, guard);
2974
+ return yield* target(once);
2975
+ });
2976
+ function remove(input, options) {
2977
+ return removeDispatch(input, options);
2978
+ }
2979
+ const selectWatch = (input, decoded) => {
2980
+ const op = OpContext.make("watch");
2981
+ const target = asTarget(input);
2982
+ return Effect.gen(function* () {
2983
+ yield* checkAvailable("watch");
2984
+ const resolved = yield* resolveTarget(target, op);
2985
+ yield* authorize(resolved.node, identity, READ, resolved.op);
2986
+ if (resolved.node.metadata.nlink === 0)
2987
+ return yield* resolved.op.fail("NotFound");
2988
+ if (decoded.alias === "resolved" && resolved.kind === "node" && resolved.node.kind !== "directory") {
2989
+ return yield* op.fail("InvalidArgument");
2990
+ }
2991
+ const selection = {
2992
+ roots,
2993
+ additionalRoots: [...inherited, ...inheritedFor(target)],
2994
+ target: resolved.node.ino,
2995
+ recursive: decoded.recursive ?? true
2996
+ };
2997
+ return (decoded.alias === "resolved" && resolved.kind === "entry"
2998
+ ? { ...selection, selected: { parent: resolved.parent.ino, name: resolved.name } }
2999
+ : selection);
3000
+ });
3001
+ };
3002
+ const operations = Object.freeze({
3003
+ limits,
3004
+ root: coordinatedRead(rootOp, Effect.suspend(() => !TokenRegistry.isOpen(reference)
3005
+ ? Effect.fail(rootOp.fail("ClosedCaller"))
3006
+ : Effect.gen(function* () {
3007
+ const node = yield* view(boundary?.root ?? ROOT_INO);
3008
+ if (node === undefined)
3009
+ return yield* rootOp.fail("ClosedCaller");
3010
+ yield* checkAuthority(node, rootOp);
3011
+ return referenceFor(node.ino);
3012
+ }))),
3013
+ lookup: Effect.fnUntraced(function* (input) {
3014
+ const op = OpContext.make("lookup");
3015
+ const prepared = yield* Effect.fromResult(prepareEntry(input, op));
3016
+ return yield* coordinatedRead(op, Effect.gen(function* () {
3017
+ const entry = yield* resolveEntry(prepared, op);
3018
+ const directory = yield* directoryNow(entry.parent);
3019
+ if (isDotComponent(entry.name)) {
3020
+ return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "NotFound");
3021
+ }
3022
+ yield* authorize(directory, identity, EXECUTE, entry.op);
3023
+ const child = directory.entries.get(entry.name);
3024
+ if (child === undefined)
3025
+ return yield* entry.op.fail("NotFound");
3026
+ return referenceFor(child);
3027
+ }));
3028
+ }),
3029
+ parent: Effect.fnUntraced(function* (input) {
3030
+ const op = OpContext.make("parent");
3031
+ const target = asTarget(input);
3032
+ return yield* coordinatedRead(op, Effect.gen(function* () {
3033
+ const directory = yield* resolveDirectory(target, op);
3034
+ yield* authorize(directory.node, identity, EXECUTE, directory.op);
3035
+ return referenceFor(directory.node.ino === boundary?.root ||
3036
+ inheritedFor(target).some((chain) => chain.at(-1) === directory.node.ino)
3037
+ ? directory.node.ino
3038
+ : directory.node.parent);
3039
+ }));
3040
+ }),
3041
+ stat: Effect.fnUntraced(function* (input) {
3042
+ const op = OpContext.make("stat");
3043
+ const target = asTarget(input);
3044
+ return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => withMetadata(resolved.node)));
3045
+ }),
3046
+ readDirectory: Effect.fnUntraced(function* (input) {
3047
+ const op = OpContext.make("readDirectory");
3048
+ const target = asTarget(input);
3049
+ return yield* accessing(op, Effect.gen(function* () {
3050
+ const directory = yield* resolveDirectory(target, op);
3051
+ yield* authorize(directory.node, identity, READ, directory.op);
3052
+ const value = Object.freeze(DirectoryEntries.toArray(directory.node.entries).map(([name, child]) => Object.freeze({ name: nameBytes(name), reference: referenceFor(child) })));
3053
+ const access = { node: directory.node, now: yield* timestamp(op) };
3054
+ return { value: Object.freeze({ value, revision: directory.node.revision }), access };
3055
+ }));
3056
+ }),
3057
+ walk: (input, options) => {
3058
+ const op = OpContext.make("walk");
3059
+ const target = asTarget(input);
3060
+ const prefix = Target.$is("Path")(target) ? Result.getOrUndefined(inputBytes(target.path)) : undefined;
3061
+ const locate = (path) => ownedPath(prefix === undefined ? path : joinPath(prefix, path));
3062
+ const authorities = inheritedFor(target).map((chain) => Confinement.make(authorityView, chain));
3063
+ const readable = Effect.fnUntraced(function* (directory, at) {
3064
+ for (const authority of authorities)
3065
+ yield* authority.check(directory, at, true);
3066
+ yield* authorize(directory, identity, READ, at);
3067
+ });
3068
+ return Stream.unwrap(Effect.gen(function* () {
3069
+ const chosen = yield* decodeWalkOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(Target.$is("Path")(target) ? op.at(target.path) : op, cause)));
3070
+ const first = coordinatedRead(op, Effect.gen(function* () {
3071
+ const directory = yield* resolveDirectory(target, op);
3072
+ yield* authorize(directory.node, identity, READ, directory.op);
3073
+ return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
3074
+ }));
3075
+ const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
3076
+ const maxBytes = chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes);
3077
+ const plan = {
3078
+ order: chosen.order ?? "pre",
3079
+ limit: (frame, progress, phase) => {
3080
+ if (chosen.maxDepth !== undefined && frame.depth > chosen.maxDepth)
3081
+ return exceeded(frame, "maxDepth");
3082
+ if (phase === "directory")
3083
+ return undefined;
3084
+ if (chosen.maxEntries !== undefined && progress.entries > chosen.maxEntries) {
3085
+ return exceeded(frame, "maxEntries");
3086
+ }
3087
+ return maxBytes !== undefined && progress.bytes > maxBytes ? exceeded(frame, "maxBytes") : undefined;
3088
+ }
3089
+ };
3090
+ return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
3091
+ path: ownedPath(frame.path),
3092
+ name: frame.name,
3093
+ reference: frame.reference,
3094
+ directory: frame.directory,
3095
+ kind: frame.kind,
3096
+ depth: frame.depth
3097
+ }));
3098
+ }));
3099
+ },
3100
+ readLink: Effect.fnUntraced(function* (input) {
3101
+ const op = OpContext.make("readLink");
3102
+ const target = asTarget(input);
3103
+ return yield* coordinatedRead(op, Effect.gen(function* () {
3104
+ const resolved = yield* resolveTarget(target, op, { followFinalSymlink: false, final: true });
3105
+ if (resolved.node.kind !== "symlink")
3106
+ return yield* resolved.op.fail("InvalidArgument");
3107
+ return new Uint8Array(resolved.node.target);
3108
+ }));
3109
+ }),
3110
+ realPath: Effect.fnUntraced(function* (input) {
3111
+ const op = OpContext.make("realPath");
3112
+ const target = asTarget(input);
3113
+ return yield* coordinatedRead(op, Effect.gen(function* () {
3114
+ if (Target.$is("Path")(target)) {
3115
+ const pathOp = op.at(target.path);
3116
+ const result = yield* resolveTarget(target, op, { final: true });
3117
+ const directory = result.node.kind === "directory"
3118
+ ? result.node
3119
+ : result.kind === "entry"
3120
+ ? result.parent
3121
+ : undefined;
3122
+ const prefix = directory === undefined ? SLASH_HEX : yield* callerPath(directory.ino);
3123
+ if (prefix === undefined)
3124
+ return yield* pathOp.fail("NotFound");
3125
+ if (result.node.kind === "directory" || result.kind === "node")
3126
+ return ownedPath(nameBytes(prefix));
3127
+ return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
3128
+ }
3129
+ const resolved = yield* resolveTarget(target, op);
3130
+ const path = yield* callerPath(resolved.node.ino);
3131
+ if (path === undefined)
3132
+ return yield* resolved.op.fail("NotFound");
3133
+ return ownedPath(nameBytes(path));
3134
+ }));
3135
+ }),
3136
+ access: Effect.fnUntraced(function* (input, bits = 0) {
3137
+ const op = OpContext.make("access");
3138
+ const target = asTarget(input);
3139
+ if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
3140
+ return yield* (Target.$is("Path")(target) ? op.at(target.path) : op).fail("InvalidArgument");
3141
+ }
3142
+ return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => {
3143
+ const node = resolved.node;
3144
+ let granted = 0;
3145
+ for (const bit of [READ, WRITE, EXECUTE]) {
3146
+ if ((bits & bit) === 0)
3147
+ continue;
3148
+ if (bit === EXECUTE && node.kind === "file" && (node.metadata.mode & ANY_EXECUTE) === 0)
3149
+ continue;
3150
+ if (identity.privileged || (permitted(node, identity) & bit) !== 0)
3151
+ granted |= bit;
3152
+ }
3153
+ return granted;
3154
+ }));
3155
+ }),
3156
+ readFile: Effect.fnUntraced(function* (input) {
3157
+ const op = OpContext.make("readFile");
3158
+ const target = asTarget(input);
3159
+ return yield* accessing(op, Effect.gen(function* () {
3160
+ const resolved = yield* resolveTarget(target, op);
3161
+ const node = resolved.node;
3162
+ if (node.kind === "symlink")
3163
+ return yield* resolved.op.fail("SymlinkLoop");
3164
+ if (node.kind !== "file")
3165
+ return yield* resolved.op.fail("IsDirectory");
3166
+ yield* authorize(node, identity, READ, resolved.op);
3167
+ const data = new Uint8Array(node.data);
3168
+ return { value: data, access: { node, now: yield* timestamp(op) } };
3169
+ }));
3170
+ }),
3171
+ writeFile: dual(3, Effect.fnUntraced(function* (input, bytes, options) {
3172
+ const op = OpContext.make("writeFile");
3173
+ const prepared = yield* Effect.fromResult(prepareEntry(input, op));
3174
+ const optionsOp = preparedOp(prepared, op);
3175
+ if (!isAttachedBytes(bytes))
3176
+ return yield* optionsOp.fail("InvalidArgument");
3177
+ const captured = new Uint8Array(bytes);
3178
+ const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
3179
+ return yield* coordinated(op, Effect.gen(function* () {
3180
+ const resolved = yield* resolver.resolve(prepared, {
3181
+ kind: "OrCreate",
3182
+ create: chosen.create ?? "never",
3183
+ finalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false
3184
+ ? "follow"
3185
+ : "preserve",
3186
+ entryExclusive: "afterSymlink"
3187
+ }, op);
3188
+ const { parent, name, trailingSlash, op: entryOp } = resolved;
3189
+ const found = resolved.kind === "existing" ? resolved.node : undefined;
3190
+ if (found?.kind === "directory" || (found === undefined && trailingSlash)) {
3191
+ return yield* entryOp.fail("IsDirectory");
3192
+ }
3193
+ const replaced = found?.kind === "symlink" ? found : undefined;
3194
+ if (replaced !== undefined && !chosen.replaceFinalSymlink)
3195
+ return yield* entryOp.fail("SymlinkLoop");
3196
+ if (chosen.access === "read")
3197
+ return yield* entryOp.fail("InvalidHandle");
3198
+ const file = found?.kind === "file" ? found : undefined;
3199
+ if (file === undefined) {
3200
+ yield* authorize(parent, identity, WRITE | EXECUTE, entryOp);
3201
+ if (replaced !== undefined)
3202
+ yield* authorizeRemoval(parent, replaced, entryOp);
3203
+ yield* (replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp));
3204
+ }
3205
+ else {
3206
+ yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, entryOp);
3207
+ }
3208
+ const finalMode = chosen.finalMode === undefined ? undefined : yield* permittedMode(file?.metadata ?? { kind: "file", uid: identity.uid, gid: parent.metadata.gid }, chosen.finalMode, entryOp);
3209
+ const previous = file?.data.length ?? 0;
3210
+ const initial = chosen.truncate ? 0 : previous;
3211
+ const position = chosen.append ? initial : 0;
3212
+ const size = Math.max(initial, position + captured.length);
3213
+ if (size > maxFileBytes)
3214
+ return yield* entryOp.fail("FileTooLarge");
3215
+ const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
3216
+ yield* reserveBytes(entryOp, BigInt(size - previous) - BigInt(reclaimed));
3217
+ if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
3218
+ return referenceFor(file.ino);
3219
+ }
3220
+ let data = captured;
3221
+ if (position !== 0 || size !== captured.length) {
3222
+ data = new Uint8Array(size);
3223
+ if (file !== undefined && !chosen.truncate)
3224
+ data.set(file.data);
3225
+ data.set(captured, position);
3226
+ }
3227
+ const now = yield* timestamp(op);
3228
+ const d = yield* Draft;
3229
+ const node = file ??
3230
+ (yield* newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now));
3231
+ const written = {
3232
+ ...node,
3233
+ data,
3234
+ metadata: {
3235
+ ...node.metadata,
3236
+ mode: finalMode ?? node.metadata.mode & ~SET_ID_BITS,
3237
+ size: BigInt(size),
3238
+ mtimeNs: now,
3239
+ ctimeNs: now
3240
+ }
3241
+ };
3242
+ yield* d.addUsedBytes(BigInt(size - previous));
3243
+ if (file === undefined) {
3244
+ if (replaced !== undefined)
3245
+ yield* detach(replaced, parent.ino, name, now);
3246
+ yield* attach(yield* directoryNow(parent.ino), name, written, now);
3247
+ if (replaced === undefined)
3248
+ yield* d.addEntries(1);
3249
+ yield* publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
3250
+ }
3251
+ else {
3252
+ yield* d.put(written);
3253
+ yield* publishNode(written.ino);
3254
+ }
3255
+ return referenceFor(written.ino);
3256
+ }));
3257
+ })),
3258
+ open,
3259
+ mkdir: Effect.fnUntraced(function* (input, options = {}) {
3260
+ const op = OpContext.make("mkdir");
3261
+ const prepared = yield* Effect.fromResult(prepareEntry(input, op));
3262
+ const optionsOp = preparedOp(prepared, op);
3263
+ const decoded = yield* decodeMkdirOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
3264
+ const chosen = { ...decoded };
3265
+ if (chosen.exactMode && chosen.mode === undefined)
3266
+ return yield* optionsOp.fail("InvalidArgument");
3267
+ return yield* coordinated(op, Effect.gen(function* () {
3268
+ const request = { mode: chosen.mode ?? 0o777, exactMode: chosen.exactMode, times: chosen.times };
3269
+ const { child, directory } = chosen.recursive === true
3270
+ ? yield* makeDirectories(prepared, request, op)
3271
+ : yield* makeDirectory(yield* resolveEntry(prepared, op), request, op);
3272
+ return { reference: referenceFor(child), directory };
3273
+ }));
3274
+ }),
3275
+ symlink: Effect.fnUntraced(function* (target, input, options = {}) {
3276
+ const op = OpContext.make("symlink");
3277
+ const targetOp = op.at(target);
3278
+ const prepared = yield* Effect.fromResult(prepareEntry(input, op));
3279
+ const optionsOp = preparedOp(prepared, op);
3280
+ const decoded = yield* decodeSymlinkOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
3281
+ const rawTarget = inputBytes(target);
3282
+ if (Result.isFailure(rawTarget))
3283
+ return yield* targetOp.fail(rawTarget.failure);
3284
+ if (rawTarget.success.includes(0))
3285
+ return yield* targetOp.fail("InvalidArgument");
3286
+ const targetBytes = new Uint8Array(rawTarget.success);
3287
+ const chosen = { ...decoded };
3288
+ return yield* coordinated(op, Effect.gen(function* () {
3289
+ const entry = yield* resolveEntry(prepared, op);
3290
+ const { child, directory } = yield* makeSymlink(entry, targetBytes, chosen.times, op);
3291
+ return { reference: referenceFor(child), directory };
3292
+ }));
3293
+ }),
3294
+ link: dual(2, Effect.fnUntraced(function* (sourceInput, input) {
3295
+ const op = OpContext.make("link");
3296
+ const source = asTarget(sourceInput);
3297
+ const prepared = yield* Effect.fromResult(prepareEntry(input, op));
3298
+ return yield* coordinated(op, Effect.gen(function* () {
3299
+ const resolved = yield* resolveTarget(source, op, { followFinalSymlink: false });
3300
+ const node = resolved.node;
3301
+ if (node.kind === "directory")
3302
+ return yield* resolved.op.fail("IsDirectory");
3303
+ if (node.metadata.nlink === 0)
3304
+ return yield* resolved.op.fail("StaleReference");
3305
+ const entry = yield* resolveEntry(prepared, op);
3306
+ const directory = yield* linkNode(node, entry, op);
3307
+ return { reference: referenceFor(node.ino), directory };
3308
+ }));
3309
+ })),
3310
+ unlink: Effect.fnUntraced(function* (input) {
3311
+ return yield* entryVerb("unlink", input, unlinkEntry);
3312
+ }),
3313
+ rmdir: Effect.fnUntraced(function* (input) {
3314
+ return yield* entryVerb("rmdir", input, rmdirEntry);
3315
+ }),
3316
+ remove,
3317
+ rename: dual(2, Effect.fnUntraced(function* (fromInput, toInput) {
3318
+ const op = OpContext.make("rename");
3319
+ const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
3320
+ const to = yield* Effect.fromResult(prepareEntry(toInput, op));
3321
+ return yield* coordinated(op, Effect.gen(function* () {
3322
+ const source = yield* resolveEntry(from, op);
3323
+ const destination = yield* resolveEntry(to, op);
3324
+ return yield* renameEntry(source, destination, op);
3325
+ }));
3326
+ })),
3327
+ chmod: dual(2, Effect.fnUntraced(function* (input, mode) {
3328
+ const op = OpContext.make("chmod");
3329
+ const target = asTarget(input);
3330
+ if (!isMode(mode))
3331
+ return yield* op.fail("InvalidArgument");
3332
+ yield* changeAttributes(() => asResolvedNode(target, op), { mode }, op);
3333
+ })),
3334
+ chown: dual(2, Effect.fnUntraced(function* (input, owner) {
3335
+ const op = OpContext.make("chown");
3336
+ const target = asTarget(input);
3337
+ const decoded = yield* decodeOwnerUpdate(owner).pipe(Effect.mapError((cause) => fail(op, cause)));
3338
+ yield* changeAttributes(() => asResolvedNode(target, op), { owner: decoded }, op);
3339
+ })),
3340
+ utimes: dual(2, Effect.fnUntraced(function* (input, times) {
3341
+ const op = OpContext.make("utimes");
3342
+ const target = asTarget(input);
3343
+ const decoded = yield* decodeTimes(times).pipe(Effect.mapError((cause) => fail(op, cause)));
3344
+ yield* changeAttributes(() => asResolvedNode(target, op), { times: decoded }, op);
3345
+ })),
3346
+ // A negative length fails before the target resolves, as truncate(2) rejects it before the lookup.
3347
+ truncate: dual(2, Effect.fnUntraced(function* (input, length) {
3348
+ const op = OpContext.make("truncate");
3349
+ const target = asTarget(input);
3350
+ if (!isLength(length))
3351
+ return yield* op.fail("InvalidArgument");
3352
+ yield* changeAttributes(() => asResolvedNode(target, op), { size: length }, op);
3353
+ })),
3354
+ setattr: dual(2, Effect.fnUntraced(function* (input, attributes) {
3355
+ const op = OpContext.make("setattr");
3356
+ const target = asTarget(input);
3357
+ // A caller outside TypeScript can pass anything, so a non-object fails typed rather than as a defect;
3358
+ // the check reads a widened copy so the attributes keep their type below.
3359
+ const raw = attributes;
3360
+ if (!Predicate.isObject(raw))
3361
+ return yield* fail(op);
3362
+ const unknown = Object.keys(attributes).find((key) => !SETATTR_FIELDS.includes(key));
3363
+ if (unknown !== undefined)
3364
+ return yield* fail(op, undefined, unknown);
3365
+ if (attributes.size !== undefined && !isLength(attributes.size))
3366
+ return yield* fail(op, undefined, "size");
3367
+ if (attributes.mode !== undefined && !isMode(attributes.mode))
3368
+ return yield* fail(op, undefined, "mode");
3369
+ const owner = attributes.owner === undefined
3370
+ ? undefined
3371
+ : yield* decodeOwnerUpdate(attributes.owner).pipe(Effect.mapError((cause) => fail(op, cause, "owner")));
3372
+ const times = attributes.times === undefined
3373
+ ? undefined
3374
+ : yield* decodeTimes(attributes.times).pipe(Effect.mapError((cause) => fail(op, cause, "times")));
3375
+ const expected = attributes.expected === undefined
3376
+ ? undefined
3377
+ : yield* decodeExpected(attributes.expected).pipe(Effect.mapError((cause) => fail(op, cause, "expected")));
3378
+ yield* changeAttributes(() => asResolvedNode(target, op), { size: attributes.size, mode: attributes.mode, owner, times, expected }, op);
3379
+ })),
3380
+ openDirectory: Effect.fnUntraced(function* (input) {
3381
+ const acquired = yield* acquireDirectory(input, OpContext.make("openDirectory"));
3382
+ const statOp = OpContext.make("stat");
3383
+ const handle = Object.freeze({
3384
+ [DirectoryHandleId]: true,
3385
+ stat: coordinatedRead(statOp, Effect.gen(function* () {
3386
+ const ino = TokenRegistry.inode(acquired);
3387
+ const node = !TokenRegistry.isOpen(acquired) || ino === undefined ? undefined : yield* view(ino);
3388
+ if (node !== undefined && acquired.authority !== undefined)
3389
+ yield* acquired.authority(node, statOp);
3390
+ return yield* (node === undefined
3391
+ ? Effect.fail(statOp.fail("InvalidHandle"))
3392
+ : Effect.succeed(withMetadata(node)));
3393
+ })),
3394
+ close: coordinatedCleanup(Effect.suspend(() => {
3395
+ if (TokenRegistry.inode(acquired) === undefined) {
3396
+ return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
3397
+ }
3398
+ return releaseDirectory(acquired);
3399
+ })).pipe(Effect.mapError((error) => error.code === "VolumeBusy" ? OpContext.make("close").fail("VolumeBusy") : error), Effect.ensuring(closeReleasedScope(acquired)))
3400
+ });
3401
+ registry.registerDirectory(handle, acquired);
3402
+ return handle;
3403
+ })
3404
+ });
3405
+ return {
3406
+ operations,
3407
+ selectWatch,
3408
+ withRoot: Effect.fnUntraced(function* (input) {
3409
+ const acquired = yield* acquireDirectory(input, OpContext.make("withRoot"));
3410
+ return createContext(acquired, identity, umask, [...roots, TokenRegistry.inode(acquired)], acquired.boundaries ?? []);
3411
+ }),
3412
+ withDirectory: Effect.fnUntraced(function* (input) {
3413
+ const acquired = yield* acquireDirectory(input, OpContext.make("withDirectory"));
3414
+ return createContext(acquired, identity, umask, acquired.namespaceRoots ?? roots, acquired.boundaries ?? []);
3415
+ })
3416
+ };
3417
+ };
3418
+ return Object.freeze({
3419
+ limits,
3420
+ shutdown,
3421
+ usage: coordinatedRead(OpContext.make("usage"), Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries }))),
3422
+ context: Effect.fnUntraced(function* (options) {
3423
+ const decoded = yield* Effect.fromResult(decodeConfiguration(RootCallerOptions, options === undefined ? {} : options, "caller"));
3424
+ const chosen = decoded.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
3425
+ const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
3426
+ return yield* coordinatedRead(OpContext.make("caller"), Effect.sync(() => createContext(makeDirectoryReference(ROOT_INO), identity, decoded.umask ?? 0o022)));
3427
+ }),
3428
+ coordinate: changing,
3429
+ admit,
3430
+ checkAvailable,
3431
+ execution,
3432
+ syncBinding: synchronous ?
3433
+ Object.freeze({
3434
+ check: (operation) => active
3435
+ ? Result.fail(fsFailure("VolumeBusy", operation))
3436
+ : Result.succeed(undefined)
3437
+ }) :
3438
+ undefined,
3439
+ preflight,
3440
+ capture: (operation) => coordinatedRead(OpContext.make(operation), Effect.sync(() => state)),
3441
+ cooperativeCapture: synchronous ?
3442
+ undefined :
3443
+ (operation, read) => coordinatedRead(OpContext.make(operation), Effect.suspend(() => read(state))),
3444
+ referenceInode: (reference, op) => coordinatedRead(op, Effect.gen(function* () {
3445
+ return (yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op))
3446
+ .ino;
3447
+ })),
3448
+ objectFor: (resolveInode, op) => coordinatedRead(op, Effect.gen(function* () {
3449
+ const ino = yield* resolveInode;
3450
+ yield* TokenRegistry.nodeOrFail(yield* registry.resolveInode(ino), "reference", op);
3451
+ return referenceFor(ino);
3452
+ })),
3453
+ watchRoot: Effect.fnUntraced(function* (reference, op) {
3454
+ const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op);
3455
+ return node.metadata.nlink === 0 ? yield* op.fail("StaleReference") : node.ino;
3456
+ })
3457
+ });
3458
+ });