@effect-vfs/core 0.0.1

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.
@@ -0,0 +1,1768 @@
1
+ /**
2
+ * Runtime-neutral virtual filesystem contracts and constructors.
3
+ *
4
+ * **Details**
5
+ *
6
+ * Volumes own isolated namespaces. Callers carry path context and credentials,
7
+ * while file and directory handles use `Scope` for deterministic release.
8
+ *
9
+ * @since 0.1.0
10
+ */
11
+ import * as Clock from "effect/Clock";
12
+ import * as Context from "effect/Context";
13
+ import { DecodeLimits, ImageError } from "./Snapshot.js";
14
+ export { DecodeLimits, ImageError, SnapshotTypeId } from "./Snapshot.js";
15
+ import * as Data from "effect/Data";
16
+ import * as Effect from "effect/Effect";
17
+ import * as Encoding from "effect/Encoding";
18
+ import * as PubSub from "effect/PubSub";
19
+ import * as Result from "effect/Result";
20
+ import * as Schema from "effect/Schema";
21
+ import * as Scope from "effect/Scope";
22
+ import * as Semaphore from "effect/Semaphore";
23
+ import * as Stream from "effect/Stream";
24
+ import * as Image from "./internal/image.js";
25
+ const BytePathId = Symbol("@effect-vfs/core/BytePath");
26
+ const VolumeId = Symbol("@effect-vfs/core/Volume");
27
+ const CallerId = Symbol("@effect-vfs/core/Caller");
28
+ const FileHandleId = Symbol("@effect-vfs/core/FileHandle");
29
+ const DirectoryHandleId = Symbol("@effect-vfs/core/DirectoryHandle");
30
+ /**
31
+ * Schema for portable virtual filesystem error codes.
32
+ *
33
+ * @category schemas
34
+ * @since 0.1.0
35
+ */
36
+ export const FsCode = Schema.Literals([
37
+ "NotFound",
38
+ "AlreadyExists",
39
+ "NotEmpty",
40
+ "NotDirectory",
41
+ "AccessDenied",
42
+ "InvalidHandle",
43
+ "ForeignHandle",
44
+ "ClosedCaller",
45
+ "InvalidArgument",
46
+ "InvalidPathEncoding",
47
+ "PathTooLong",
48
+ "NoSpace",
49
+ "IsDirectory",
50
+ "FileTooLarge",
51
+ "NoData",
52
+ "SymlinkLoop",
53
+ "UnrepresentableName"
54
+ ]);
55
+ /**
56
+ * Describes an expected filesystem operation failure.
57
+ *
58
+ * @category errors
59
+ * @since 0.1.0
60
+ */
61
+ export class FsError extends Data.TaggedError("FsError") {
62
+ }
63
+ /**
64
+ * Describes an invalid volume or caller option and names the rejected field.
65
+ *
66
+ * @category errors
67
+ * @since 0.1.0
68
+ */
69
+ export class ConfigurationError extends Data.TaggedError("ConfigurationError") {
70
+ }
71
+ const Natural = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER));
72
+ const Mode = Natural.check(Schema.isLessThanOrEqualTo(0o7777));
73
+ /**
74
+ * Schema for a caller's numeric identity, supplementary groups, and explicit privilege.
75
+ *
76
+ * @category schemas
77
+ * @since 0.1.0
78
+ */
79
+ export const Identity = Schema.Struct({
80
+ /** Numeric user identifier used by ownership and permission checks. */
81
+ uid: Natural,
82
+ /** Primary numeric group identifier. */
83
+ gid: Natural,
84
+ /** Supplementary group identifiers used by group permission checks. */
85
+ groups: Schema.Array(Natural),
86
+ /** Grants root-style permission bypasses independently of `uid`. */
87
+ privileged: Schema.Boolean
88
+ });
89
+ /**
90
+ * Schema for root caller credentials and creation mask.
91
+ *
92
+ * @category schemas
93
+ * @since 0.1.0
94
+ */
95
+ export const RootCallerOptions = Schema.Struct({
96
+ /** Caller identity. Defaults to privileged uid and gid `0`. */
97
+ identity: Schema.optionalKey(Identity),
98
+ /** Creation mask applied to requested modes. Defaults to `0o022`. */
99
+ umask: Schema.optionalKey(Natural.check(Schema.isLessThanOrEqualTo(0o777)))
100
+ });
101
+ /**
102
+ * Schema for optional volume capacity and path limits.
103
+ *
104
+ * @category schemas
105
+ * @since 0.1.0
106
+ */
107
+ export const VolumeOptions = Schema.Struct({
108
+ /** Maximum number of filesystem nodes. Omission leaves the count unbounded. */
109
+ maxEntries: Schema.optionalKey(Natural),
110
+ /** Maximum combined regular-file content in bytes. */
111
+ maxBytes: Schema.optionalKey(Natural),
112
+ /** Maximum content size of one regular file in bytes. */
113
+ maxFileBytes: Schema.optionalKey(Natural.check(Schema.isLessThanOrEqualTo(0xffffffff))),
114
+ /** Maximum encoded byte length of an absolute or relative path. */
115
+ maxPathBytes: Schema.optionalKey(Natural.check(Schema.isGreaterThanOrEqualTo(1)))
116
+ });
117
+ // Match snapshot v1's canonical signed decimal timestamp domain.
118
+ const timestampLimit = 10n ** 128n - 1n;
119
+ const Timestamp = Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(-timestampLimit), Schema.isLessThanOrEqualToBigInt(timestampLimit));
120
+ /**
121
+ * Schema for filesystem node metadata with bigint inode, size, and nanosecond fields.
122
+ *
123
+ * @category schemas
124
+ * @since 0.1.0
125
+ */
126
+ export const Metadata = Schema.Struct({
127
+ kind: Schema.Literals(["directory", "file", "symlink"]),
128
+ ino: Schema.BigInt,
129
+ nlink: Natural,
130
+ size: Schema.BigInt,
131
+ uid: Natural,
132
+ gid: Natural,
133
+ mode: Mode,
134
+ atimeNs: Timestamp,
135
+ mtimeNs: Timestamp,
136
+ ctimeNs: Timestamp,
137
+ birthtimeNs: Timestamp
138
+ });
139
+ /**
140
+ * Schema for an owner update. Omitted fields retain their existing values.
141
+ *
142
+ * @category schemas
143
+ * @since 0.1.0
144
+ */
145
+ export const OwnerUpdate = Schema.Struct({ uid: Schema.optionalKey(Natural), gid: Schema.optionalKey(Natural) });
146
+ /**
147
+ * Schema for setting a timestamp to the clock, retaining it, or supplying nanoseconds.
148
+ *
149
+ * @category schemas
150
+ * @since 0.1.0
151
+ */
152
+ export const TimeUpdate = Schema.Union([
153
+ Schema.Struct({ kind: Schema.Literal("now") }),
154
+ Schema.Struct({ kind: Schema.Literal("omit") }),
155
+ Schema.Struct({ kind: Schema.Literal("value"), nanoseconds: Timestamp })
156
+ ]);
157
+ /**
158
+ * Schema for independent access and modification time updates.
159
+ *
160
+ * @category schemas
161
+ * @since 0.1.0
162
+ */
163
+ export const Times = Schema.Struct({ access: TimeUpdate, modification: TimeUpdate });
164
+ /**
165
+ * Schema for file seek origins, including dense-file data and hole queries.
166
+ *
167
+ * @category schemas
168
+ * @since 0.1.0
169
+ */
170
+ export const SeekMode = Schema.Literals(["start", "current", "end", "data", "hole"]);
171
+ /**
172
+ * Schema for file access, creation, append, truncate, and symlink behavior.
173
+ *
174
+ * @category schemas
175
+ * @since 0.1.0
176
+ */
177
+ export const OpenSettings = Schema.Struct({
178
+ /** Permitted operations on the returned handle. */
179
+ access: Schema.Literals(["read", "write", "readWrite"]),
180
+ /** Creation policy. Defaults to `"never"`. */
181
+ create: Schema.optionalKey(Schema.Literals(["never", "ifMissing", "exclusive"])),
182
+ /** Requested mode for a new file, before applying the caller's umask. */
183
+ mode: Schema.optionalKey(Mode),
184
+ /** Write at the current end of file regardless of the handle cursor. */
185
+ append: Schema.optionalKey(Schema.Boolean),
186
+ /** Truncate an existing regular file to zero bytes during open. */
187
+ truncate: Schema.optionalKey(Schema.Boolean),
188
+ /** Follow the final symbolic link. Defaults to `true`. */
189
+ followFinalSymlink: Schema.optionalKey(Schema.Boolean)
190
+ });
191
+ const WriteFileSettings = Schema.Struct({
192
+ ...OpenSettings.fields,
193
+ /** Replace the final symbolic link itself instead of its target. */
194
+ replaceFinalSymlink: Schema.optionalKey(Schema.Boolean),
195
+ /** Mode to apply after replacing an existing file. */
196
+ finalMode: Schema.optionalKey(Mode)
197
+ });
198
+ /**
199
+ * Optional Effect service for providing an existing filesystem caller.
200
+ *
201
+ * @category services
202
+ * @since 0.1.0
203
+ */
204
+ export class CurrentFileSystem extends Context.Service()("@effect-vfs/core/CurrentFileSystem") {
205
+ }
206
+ /**
207
+ * Encodes a snapshot as owned UTF-8 JSON bytes using the version 1 snapshot format.
208
+ *
209
+ * @category serialization
210
+ * @since 0.1.0
211
+ */
212
+ export const encodeSnapshot = Image.encodeSnapshot;
213
+ /**
214
+ * Decodes version 1 snapshot bytes while enforcing explicit input and payload limits.
215
+ *
216
+ * @category serialization
217
+ * @since 0.1.0
218
+ */
219
+ export const decodeSnapshot = Image.decodeSnapshot;
220
+ const bytePaths = new WeakMap();
221
+ const failure = (code, operation, path) => new FsError({ code, operation, ...(path === undefined ? {} : { path }) });
222
+ const ownedPath = (bytes) => {
223
+ const path = Object.freeze({ [BytePathId]: true });
224
+ bytePaths.set(path, bytes);
225
+ return path;
226
+ };
227
+ const strictString = (bytes, operation) => Effect.try({
228
+ try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes),
229
+ catch: () => failure("UnrepresentableName", operation)
230
+ });
231
+ const nameBytes = (name) => {
232
+ const bytes = new Uint8Array(name.length / 2);
233
+ for (let i = 0; i < bytes.length; i++)
234
+ bytes[i] = Number.parseInt(name.slice(i * 2, i * 2 + 2), 16);
235
+ return bytes;
236
+ };
237
+ // A zero-length view distinguishes a detached buffer from a valid empty buffer.
238
+ const attachedBuffer = (bytes) => {
239
+ try {
240
+ new Uint8Array(bytes.buffer, bytes.byteOffset, 0);
241
+ return true;
242
+ }
243
+ catch (error) {
244
+ if (error instanceof TypeError)
245
+ return false;
246
+ throw error;
247
+ }
248
+ };
249
+ /**
250
+ * Creates an opaque byte path by copying the input when the Effect executes.
251
+ *
252
+ * **Gotchas**
253
+ *
254
+ * Shared-memory-backed and detached views fail with `InvalidArgument`.
255
+ *
256
+ * @category constructors
257
+ * @since 0.1.0
258
+ */
259
+ export const pathFromBytes = Effect.fn("VirtualFileSystem.pathFromBytes")(function* (bytes) {
260
+ if (!(bytes instanceof Uint8Array) || !(bytes.buffer instanceof ArrayBuffer)) {
261
+ return yield* failure("InvalidArgument", "pathFromBytes");
262
+ }
263
+ if (!attachedBuffer(bytes))
264
+ return yield* failure("InvalidArgument", "pathFromBytes");
265
+ const owned = new Uint8Array(bytes);
266
+ if (owned.length === 0 || owned.includes(0))
267
+ return yield* failure("InvalidArgument", "pathFromBytes");
268
+ const path = Object.freeze({ [BytePathId]: true });
269
+ bytePaths.set(path, owned);
270
+ return path;
271
+ });
272
+ /**
273
+ * Copies the bytes held by an opaque byte path.
274
+ *
275
+ * @category getters
276
+ * @since 0.1.0
277
+ */
278
+ export const pathToBytes = Effect.fn("VirtualFileSystem.pathToBytes")(function* (path) {
279
+ const bytes = bytePaths.get(path);
280
+ if (bytes === undefined)
281
+ return yield* failure("InvalidArgument", "pathToBytes");
282
+ return new Uint8Array(bytes);
283
+ });
284
+ const files = new WeakMap();
285
+ const handles = new WeakMap();
286
+ const wellFormed = (value) => {
287
+ for (let index = 0; index < value.length; index++) {
288
+ const code = value.charCodeAt(index);
289
+ if (code >= 0xd800 && code <= 0xdbff) {
290
+ const next = value.charCodeAt(++index);
291
+ if (!(next >= 0xdc00 && next <= 0xdfff))
292
+ return false;
293
+ }
294
+ else if (code >= 0xdc00 && code <= 0xdfff)
295
+ return false;
296
+ }
297
+ return true;
298
+ };
299
+ const preparePath = (input, operation, maxPathBytes) => {
300
+ let bytes;
301
+ if (typeof input === "string") {
302
+ if (!wellFormed(input))
303
+ return Result.fail(failure("InvalidPathEncoding", operation, input));
304
+ bytes = new TextEncoder().encode(input);
305
+ }
306
+ else if (typeof input === "object" && input !== null) {
307
+ bytes = bytePaths.get(input);
308
+ }
309
+ if (bytes === undefined)
310
+ return Result.fail(failure("InvalidArgument", operation));
311
+ if (bytes.length === 0)
312
+ return Result.fail(failure("NotFound", operation, input));
313
+ if (bytes.includes(0))
314
+ return Result.fail(failure("InvalidArgument", operation, input));
315
+ if (maxPathBytes !== undefined && bytes.length > maxPathBytes) {
316
+ return Result.fail(failure("PathTooLong", operation, input));
317
+ }
318
+ const components = [];
319
+ const suffixes = [];
320
+ let start = 0;
321
+ for (let index = 0; index <= bytes.length; index++) {
322
+ if (index !== bytes.length && bytes[index] !== 47)
323
+ continue;
324
+ if (index > start) {
325
+ // Provisional component bound from decision 0019; names are compared as bytes.
326
+ if (index - start > 255)
327
+ return Result.fail(failure("PathTooLong", operation, input));
328
+ components.push(Encoding.encodeHex(bytes.subarray(start, index)));
329
+ suffixes.push(bytes.subarray(index));
330
+ }
331
+ start = index + 1;
332
+ }
333
+ return Result.succeed({
334
+ input,
335
+ absolute: bytes[0] === 47,
336
+ trailingSlash: bytes.at(-1) === 47,
337
+ bytes,
338
+ suffixes,
339
+ components
340
+ });
341
+ };
342
+ const configurationField = (issue) => {
343
+ if (issue._tag === "Pointer")
344
+ return issue.path.map(String).join(".");
345
+ if (issue._tag === "Composite")
346
+ return configurationField(issue.issues[0]);
347
+ return "options";
348
+ };
349
+ const decodeConfiguration = (schema, value) => Schema.decodeUnknownResult(schema, { onExcessProperty: "error" })(value).pipe(Result.mapError((error) => new ConfigurationError({ field: configurationField(error.issue) })));
350
+ const directoryMetadata = (ino, uid, gid, mode, now) => ({
351
+ kind: "directory",
352
+ ino,
353
+ uid,
354
+ gid,
355
+ mode,
356
+ nlink: 2,
357
+ size: 0n,
358
+ atimeNs: now,
359
+ mtimeNs: now,
360
+ ctimeNs: now,
361
+ birthtimeNs: now
362
+ });
363
+ const storedMetadata = (metadata) => ({
364
+ uid: metadata.uid,
365
+ gid: metadata.gid,
366
+ mode: metadata.mode,
367
+ atimeNs: String(metadata.atimeNs),
368
+ mtimeNs: String(metadata.mtimeNs),
369
+ ctimeNs: String(metadata.ctimeNs),
370
+ birthtimeNs: String(metadata.birthtimeNs)
371
+ });
372
+ /** Each execution constructs a fresh volume and captures its Clock. */
373
+ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options, image) {
374
+ const decoded = decodeConfiguration(VolumeOptions, options === undefined ? {} : options);
375
+ if (Result.isFailure(decoded))
376
+ return yield* decoded.failure;
377
+ const settings = { ...decoded.success };
378
+ const clock = yield* Clock.clockWith(Effect.succeed);
379
+ const initialTime = clock.currentTimeNanosUnsafe();
380
+ if (!Schema.is(Timestamp)(initialTime)) {
381
+ return yield* new ConfigurationError({ field: "clock.currentTimeNanos" });
382
+ }
383
+ const timestamp = (operation) => Effect.suspend(() => {
384
+ const now = clock.currentTimeNanosUnsafe();
385
+ return Schema.is(Timestamp)(now) ? Effect.succeed(now) : Effect.fail(failure("InvalidArgument", operation));
386
+ });
387
+ const volumeIdentity = Symbol();
388
+ const gate = Semaphore.makeUnsafe(1);
389
+ const root = {
390
+ kind: "directory",
391
+ parent: undefined,
392
+ entries: new Map(),
393
+ metadata: directoryMetadata(1n, 0, 0, 0o755, initialTime)
394
+ };
395
+ let nextInode = 2n;
396
+ let entries = 0;
397
+ let usedBytes = 0;
398
+ const maxFileBytes = settings.maxFileBytes ?? 0xffffffff;
399
+ if (image !== undefined) {
400
+ const incoming = new Map();
401
+ let content = 0;
402
+ let count = 0;
403
+ for (const record of image.records) {
404
+ if (record.kind === "directory")
405
+ count += record.entries.length;
406
+ else {
407
+ const length = Image.decodedLength(record.kind === "file" ? record.data : record.target);
408
+ if (record.kind === "file" && length > maxFileBytes) {
409
+ return yield* new ImageError({ code: "LimitExceeded", field: "maxFileBytes" });
410
+ }
411
+ content += length;
412
+ }
413
+ }
414
+ if ((settings.maxEntries !== undefined && count > settings.maxEntries) ||
415
+ (settings.maxBytes !== undefined && content > settings.maxBytes)) {
416
+ return yield* new ImageError({ code: "LimitExceeded", field: "volume" });
417
+ }
418
+ for (const record of image.records) {
419
+ const metadata = {
420
+ ...record.metadata,
421
+ kind: record.kind,
422
+ ino: record.id === image.root ? 1n : nextInode++,
423
+ nlink: record.kind === "directory" ? 2 : 0,
424
+ size: 0n,
425
+ atimeNs: BigInt(record.metadata.atimeNs),
426
+ mtimeNs: BigInt(record.metadata.mtimeNs),
427
+ ctimeNs: BigInt(record.metadata.ctimeNs),
428
+ birthtimeNs: BigInt(record.metadata.birthtimeNs)
429
+ };
430
+ if (record.kind === "directory") {
431
+ const node = record.id === image.root
432
+ ? root
433
+ : { kind: "directory", parent: undefined, entries: new Map(), metadata };
434
+ node.metadata = metadata;
435
+ incoming.set(record.id, node);
436
+ }
437
+ else if (record.kind === "file") {
438
+ const data = Image.bytes(record.data);
439
+ incoming.set(record.id, {
440
+ kind: "file",
441
+ data,
442
+ openCount: 0,
443
+ metadata: { ...metadata, size: BigInt(data.length) }
444
+ });
445
+ }
446
+ else {
447
+ const target = Image.bytes(record.target);
448
+ incoming.set(record.id, { kind: "symlink", target, metadata: { ...metadata, size: BigInt(target.length) } });
449
+ }
450
+ }
451
+ for (const record of image.records) {
452
+ if (record.kind !== "directory")
453
+ continue;
454
+ const parent = incoming.get(record.id);
455
+ if (parent?.kind !== "directory")
456
+ return yield* new ImageError({ code: "InvalidStructure" });
457
+ for (const entry of record.entries) {
458
+ const node = incoming.get(entry.target);
459
+ if (node === undefined)
460
+ return yield* new ImageError({ code: "InvalidStructure" });
461
+ parent.entries.set(Encoding.encodeHex(Image.bytes(entry.name)), node);
462
+ if (node.kind === "directory") {
463
+ node.parent = parent;
464
+ parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink + 1 };
465
+ }
466
+ else
467
+ node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1 };
468
+ }
469
+ }
470
+ entries = count;
471
+ usedBytes = content;
472
+ }
473
+ const events = yield* PubSub.unbounded();
474
+ let subscribers = 0;
475
+ const directoryHex = (directory) => {
476
+ const names = [];
477
+ let current = directory;
478
+ while (current.parent !== undefined) {
479
+ const parent = current.parent;
480
+ const found = [...parent.entries].find(([, node]) => node === current);
481
+ if (found === undefined)
482
+ break;
483
+ names.push(found[0]);
484
+ current = parent;
485
+ }
486
+ return "2f" + names.reverse().join("2f");
487
+ };
488
+ const publishEntry = (_tag, parent, name) => {
489
+ if (subscribers === 0)
490
+ return;
491
+ const prefix = directoryHex(parent);
492
+ PubSub.publishUnsafe(events, { _tag, path: ownedPath(nameBytes(prefix + (prefix === "2f" ? "" : "2f") + name)) });
493
+ };
494
+ const publishNode = (target) => {
495
+ if (subscribers === 0)
496
+ return;
497
+ if (target === root) {
498
+ PubSub.publishUnsafe(events, { _tag: "Update", path: ownedPath(new Uint8Array([47])) });
499
+ }
500
+ const pending = [[root, "2f"]];
501
+ while (pending.length > 0) {
502
+ const next = pending.pop();
503
+ if (next === undefined)
504
+ break;
505
+ const [directory, prefix] = next;
506
+ for (const [name, node] of directory.entries) {
507
+ const path = prefix + name;
508
+ if (node === target) {
509
+ PubSub.publishUnsafe(events, { _tag: "Update", path: ownedPath(nameBytes(path)) });
510
+ }
511
+ if (node.kind === "directory")
512
+ pending.push([node, path + "2f"]);
513
+ }
514
+ }
515
+ };
516
+ // Permit waits stay interruptible. State transitions and resource registration do not.
517
+ const coordinated = (effect) => gate.withPermit(Effect.uninterruptible(effect));
518
+ const release = (reference) => coordinated(Effect.sync(() => {
519
+ reference.directory = undefined;
520
+ reference.closed = true;
521
+ }));
522
+ const authorize = (directory, identity, bits, operation, path) => {
523
+ if (identity.privileged)
524
+ return Effect.void;
525
+ const metadata = directory.metadata;
526
+ const shift = metadata.uid === identity.uid ?
527
+ 6
528
+ : metadata.gid === identity.gid || identity.groups.includes(metadata.gid)
529
+ ? 3
530
+ : 0;
531
+ return ((metadata.mode >> shift) & bits) === bits
532
+ ? Effect.void
533
+ : Effect.fail(failure("AccessDenied", operation, path));
534
+ };
535
+ const reclaim = (file) => {
536
+ if (file.metadata.nlink === 0 && file.openCount === 0) {
537
+ usedBytes -= file.data.length;
538
+ file.data = new Uint8Array(0);
539
+ }
540
+ };
541
+ const detach = (node, now) => {
542
+ if (node.kind === "directory") {
543
+ node.parent = undefined;
544
+ node.metadata = { ...node.metadata, nlink: 0, ctimeNs: now };
545
+ }
546
+ else {
547
+ node.metadata = { ...node.metadata, nlink: node.metadata.nlink - 1, ctimeNs: now };
548
+ if (node.kind === "file")
549
+ reclaim(node);
550
+ else if (node.metadata.nlink === 0)
551
+ usedBytes -= node.target.length;
552
+ }
553
+ };
554
+ const releaseFile = (ref) => {
555
+ if (ref.file !== undefined) {
556
+ ref.file.openCount -= 1;
557
+ reclaim(ref.file);
558
+ ref.file = undefined;
559
+ }
560
+ ref.closed = true;
561
+ };
562
+ const resize = Effect.fnUntraced(function* (file, length, operation) {
563
+ if (typeof length !== "bigint" || length < 0n)
564
+ return yield* failure("InvalidArgument", operation);
565
+ if (length > BigInt(maxFileBytes))
566
+ return yield* failure("FileTooLarge", operation);
567
+ const size = Number(length);
568
+ if (size - file.data.length > (settings.maxBytes ?? Number.MAX_SAFE_INTEGER) - usedBytes) {
569
+ return yield* failure("NoSpace", operation);
570
+ }
571
+ const data = new Uint8Array(size);
572
+ data.set(file.data.subarray(0, size));
573
+ const now = yield* timestamp(operation);
574
+ usedBytes += size - file.data.length;
575
+ file.data = data;
576
+ file.metadata = {
577
+ ...file.metadata,
578
+ size: length,
579
+ mode: file.metadata.mode & ~0o6000,
580
+ mtimeNs: now,
581
+ ctimeNs: now
582
+ };
583
+ publishNode(file);
584
+ });
585
+ const fileHandle = (ref) => {
586
+ const get = (operation, access) => ref.file === undefined || (access === "read" && ref.access === "write") ||
587
+ (access === "write" && ref.access === "read")
588
+ ? Effect.fail(failure("InvalidHandle", operation))
589
+ : Effect.succeed(ref.file);
590
+ const read = Effect.fnUntraced(function* (maximum, position) {
591
+ const file = yield* get(position === undefined ? "read" : "pread", "read");
592
+ if (!Schema.is(Natural)(maximum))
593
+ return yield* failure("InvalidArgument", "read");
594
+ const offset = position ?? ref.offset;
595
+ if (typeof offset !== "bigint" || offset < 0n || offset > 0x7fffffffffffffffn) {
596
+ return yield* failure("InvalidArgument", "read");
597
+ }
598
+ const start = Number(offset > file.metadata.size ? file.metadata.size : offset);
599
+ const data = file.data.slice(start, start + Math.min(maximum, file.data.length - start));
600
+ if (maximum > 0) {
601
+ file.metadata = { ...file.metadata, atimeNs: (yield* timestamp("read")) };
602
+ }
603
+ if (position === undefined)
604
+ ref.offset += BigInt(data.length);
605
+ return data;
606
+ }, coordinated);
607
+ const write = Effect.fnUntraced(function* (input, position) {
608
+ if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer) || !attachedBuffer(input)) {
609
+ return yield* failure("InvalidArgument", "write");
610
+ }
611
+ const bytes = new Uint8Array(input);
612
+ return yield* coordinated(Effect.gen(function* () {
613
+ const file = yield* get(position === undefined ? "write" : "pwrite", "write");
614
+ const offset = position ?? (ref.append ? file.metadata.size : ref.offset);
615
+ if (typeof offset !== "bigint" || offset < 0n || offset > 0x7fffffffffffffffn) {
616
+ return yield* failure("InvalidArgument", "write");
617
+ }
618
+ if (bytes.length === 0) {
619
+ return 0;
620
+ }
621
+ if (offset >= BigInt(maxFileBytes))
622
+ return yield* failure("FileTooLarge", "write");
623
+ const start = Number(offset);
624
+ const free = (settings.maxBytes ?? Number.MAX_SAFE_INTEGER) - usedBytes;
625
+ const end = Math.min(maxFileBytes, file.data.length + free);
626
+ const count = Math.min(bytes.length, Math.max(0, end - start));
627
+ if (count === 0)
628
+ return yield* failure("NoSpace", "write");
629
+ const size = Math.max(file.data.length, start + count);
630
+ const data = size === file.data.length ? file.data : new Uint8Array(size);
631
+ if (data !== file.data)
632
+ data.set(file.data);
633
+ const now = yield* timestamp("write");
634
+ data.set(bytes.subarray(0, count), start);
635
+ usedBytes += size - file.data.length;
636
+ file.data = data;
637
+ file.metadata = {
638
+ ...file.metadata,
639
+ size: BigInt(size),
640
+ mode: file.metadata.mode & ~0o6000,
641
+ mtimeNs: now,
642
+ ctimeNs: now
643
+ };
644
+ publishNode(file);
645
+ if (position === undefined)
646
+ ref.offset = offset + BigInt(count);
647
+ return count;
648
+ }));
649
+ });
650
+ const handle = Object.freeze({
651
+ [FileHandleId]: true,
652
+ read: Effect.fn("FileHandle.read")(function* (maximum) {
653
+ return yield* read(maximum);
654
+ }),
655
+ pread: Effect.fn("FileHandle.pread")(function* (maximum, offset) {
656
+ return yield* read(maximum, offset);
657
+ }),
658
+ write: Effect.fn("FileHandle.write")(function* (bytes) {
659
+ return yield* write(bytes);
660
+ }),
661
+ pwrite: Effect.fn("FileHandle.pwrite")(function* (bytes, offset) {
662
+ return yield* write(bytes, offset);
663
+ }),
664
+ seek: Effect.fn("FileHandle.seek")(function* (offset, mode) {
665
+ return yield* coordinated(Effect.gen(function* () {
666
+ const file = yield* get("seek");
667
+ if (typeof offset !== "bigint" || !Schema.is(SeekMode)(mode)) {
668
+ return yield* failure("InvalidArgument", "seek");
669
+ }
670
+ let next = mode === "current" ? ref.offset + offset : mode === "end" ? file.metadata.size + offset : offset;
671
+ if (next < 0n || next > 0x7fffffffffffffffn)
672
+ return yield* failure("InvalidArgument", "seek");
673
+ if (mode === "data" || mode === "hole") {
674
+ if (offset >= file.metadata.size)
675
+ return yield* failure("NoData", "seek");
676
+ if (mode === "hole")
677
+ next = file.metadata.size;
678
+ }
679
+ ref.offset = next;
680
+ return next;
681
+ }));
682
+ }),
683
+ truncate: Effect.fn("FileHandle.truncate")(function* (length) {
684
+ return yield* coordinated(Effect.gen(function* () {
685
+ yield* resize(yield* get("truncate", "write"), length, "truncate");
686
+ }));
687
+ }),
688
+ stat: coordinated(Effect.gen(function* () {
689
+ return { ...(yield* get("stat")).metadata };
690
+ })).pipe(Effect.withSpan("FileHandle.stat")),
691
+ sync: coordinated(Effect.suspend(() => Effect.asVoid(get("sync")))).pipe(Effect.withSpan("FileHandle.sync")),
692
+ close: coordinated(Effect.gen(function* () {
693
+ yield* get("close");
694
+ releaseFile(ref);
695
+ })).pipe(Effect.withSpan("FileHandle.close"))
696
+ });
697
+ files.set(handle, ref);
698
+ return handle;
699
+ };
700
+ const createCaller = (reference, identity, umask) => {
701
+ const lookup = Effect.fnUntraced(function* (path, base, operation, options = {}) {
702
+ const { followFinalSymlink = true, allowMissing = false, parentOnly = false } = options;
703
+ if (reference.directory === undefined)
704
+ return yield* failure("ClosedCaller", operation, path.input);
705
+ let current = path.absolute ? root : reference.directory;
706
+ if (!path.absolute && base !== undefined) {
707
+ const target = handles.get(base);
708
+ if (target === undefined)
709
+ return yield* failure("InvalidHandle", operation, path.input);
710
+ if (target.volume !== volumeIdentity)
711
+ return yield* failure("ForeignHandle", operation, path.input);
712
+ if (target.directory === undefined)
713
+ return yield* failure("InvalidHandle", operation, path.input);
714
+ current = target.directory;
715
+ yield* authorize(current, identity, 1, operation, path.input);
716
+ }
717
+ if (current.metadata.nlink === 0)
718
+ return yield* failure("NotFound", operation, path.input);
719
+ let work = path;
720
+ let parent;
721
+ let name;
722
+ let traversals = 0;
723
+ for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
724
+ if (current.kind !== "directory")
725
+ return yield* failure("NotDirectory", operation, path.input);
726
+ yield* authorize(current, identity, 1, operation, path.input);
727
+ const component = work.components[index];
728
+ if (component === undefined)
729
+ break;
730
+ if (component === "2e")
731
+ continue;
732
+ if (component === "2e2e") {
733
+ current = current.parent ?? current;
734
+ parent = undefined;
735
+ name = undefined;
736
+ continue;
737
+ }
738
+ parent = current;
739
+ name = component;
740
+ const child = current.entries.get(component);
741
+ if (child === undefined) {
742
+ if (allowMissing && index === work.components.length - 1 && !work.trailingSlash) {
743
+ return { node: undefined, parent, name };
744
+ }
745
+ return yield* failure("NotFound", operation, path.input);
746
+ }
747
+ if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
748
+ if (child.target.length === 0)
749
+ return yield* failure("NotFound", operation, path.input);
750
+ if (++traversals > 40)
751
+ return yield* failure("SymlinkLoop", operation, path.input);
752
+ const suffix = work.suffixes[index] ?? new Uint8Array(0);
753
+ if (settings.maxPathBytes !== undefined && child.target.length + suffix.length > settings.maxPathBytes) {
754
+ return yield* failure("PathTooLong", operation, path.input);
755
+ }
756
+ const expansion = new Uint8Array(child.target.length + suffix.length);
757
+ expansion.set(child.target);
758
+ expansion.set(suffix, child.target.length);
759
+ work = yield* Effect.fromResult(preparePath(ownedPath(expansion), operation, settings.maxPathBytes));
760
+ if (work.absolute)
761
+ current = root;
762
+ index = -1;
763
+ }
764
+ else
765
+ current = child;
766
+ }
767
+ if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
768
+ return yield* failure("NotDirectory", operation, path.input);
769
+ }
770
+ return { node: current, parent, name };
771
+ });
772
+ const resolveNode = Effect.fnUntraced(function* (path, base, operation, options) {
773
+ const result = yield* lookup(path, base, operation, options);
774
+ if (result.node === undefined)
775
+ return yield* failure("NotFound", operation, path.input);
776
+ return result.node;
777
+ });
778
+ const locate = Effect.fnUntraced(function* (path, base, operation, options) {
779
+ const result = yield* lookup(path, base, operation, options);
780
+ const node = result.node;
781
+ if (node === undefined)
782
+ return yield* failure("NotFound", operation, path.input);
783
+ if (node.kind !== "directory")
784
+ return yield* failure("NotDirectory", operation, path.input);
785
+ return node;
786
+ });
787
+ const acquireDirectory = Effect.fnUntraced(function* (input, options, operation) {
788
+ const prepared = preparePath(input, operation, settings.maxPathBytes);
789
+ const base = options?.relativeTo;
790
+ const acquired = { volume: volumeIdentity, directory: undefined, closed: false };
791
+ // Register before retaining a directory. Closed scopes can run this immediately,
792
+ // so registration must not happen while holding the volume permit.
793
+ yield* Effect.addFinalizer(() => release(acquired));
794
+ return yield* coordinated(Effect.gen(function* () {
795
+ if (acquired.closed)
796
+ return yield* Effect.interrupt;
797
+ const path = yield* Effect.fromResult(prepared);
798
+ const directory = yield* locate(path, base, operation);
799
+ yield* authorize(directory, identity, 1, operation, input);
800
+ acquired.directory = directory;
801
+ return acquired;
802
+ }));
803
+ });
804
+ const list = Effect.fnUntraced(function* (input, options) {
805
+ const prepared = preparePath(input, "readDirectory", settings.maxPathBytes);
806
+ const base = options?.relativeTo;
807
+ return yield* coordinated(Effect.gen(function* () {
808
+ const directory = yield* locate(yield* Effect.fromResult(prepared), base, "readDirectory");
809
+ yield* authorize(directory, identity, 4, "readDirectory", input);
810
+ const result = [...directory.entries.keys()].map(nameBytes);
811
+ directory.metadata = { ...directory.metadata, atimeNs: (yield* timestamp("readDirectory")) };
812
+ return result;
813
+ }));
814
+ });
815
+ const readTarget = Effect.fnUntraced(function* (input, options) {
816
+ const prepared = preparePath(input, "readLink", settings.maxPathBytes);
817
+ const base = options?.relativeTo;
818
+ return yield* coordinated(Effect.gen(function* () {
819
+ const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readLink", {
820
+ followFinalSymlink: false
821
+ });
822
+ if (node.kind !== "symlink")
823
+ return yield* failure("InvalidArgument", "readLink", input);
824
+ return new Uint8Array(node.target);
825
+ }));
826
+ });
827
+ const canonical = Effect.fnUntraced(function* (input, options) {
828
+ const prepared = preparePath(input, "realPath", settings.maxPathBytes);
829
+ const base = options?.relativeTo;
830
+ return yield* coordinated(Effect.gen(function* () {
831
+ const result = yield* lookup(yield* Effect.fromResult(prepared), base, "realPath");
832
+ const components = [];
833
+ if (result.node?.kind !== "directory" && result.name !== undefined)
834
+ components.push(result.name);
835
+ let directory = result.node?.kind === "directory" ? result.node : result.parent;
836
+ while (directory !== undefined && directory.parent !== undefined) {
837
+ const parent = directory.parent;
838
+ const entry = [...parent.entries].find(([, child]) => child === directory);
839
+ if (entry === undefined)
840
+ return yield* failure("NotFound", "realPath", input);
841
+ components.push(entry[0]);
842
+ directory = parent;
843
+ }
844
+ return nameBytes("2f" + components.reverse().join("2f"));
845
+ }));
846
+ });
847
+ const metadataNode = Effect.fnUntraced(function* (target, options, operation) {
848
+ if (reference.directory === undefined)
849
+ return yield* failure("ClosedCaller", operation);
850
+ if (typeof target === "object" && target !== null && (FileHandleId in target || DirectoryHandleId in target)) {
851
+ const ref = FileHandleId in target ? files.get(target) : handles.get(target);
852
+ if (ref === undefined)
853
+ return yield* failure("InvalidHandle", operation);
854
+ if (ref.volume !== volumeIdentity)
855
+ return yield* failure("ForeignHandle", operation);
856
+ const node = "file" in ref ? ref.file : ref.directory;
857
+ if (node === undefined)
858
+ return yield* failure("InvalidHandle", operation);
859
+ return node;
860
+ }
861
+ const path = yield* Effect.fromResult(preparePath(target, operation, settings.maxPathBytes));
862
+ return yield* resolveNode(path, options?.relativeTo, operation, {
863
+ followFinalSymlink: options?.followFinalSymlink !== false
864
+ });
865
+ });
866
+ const permittedMode = (metadata, mode, operation, path) => {
867
+ if (!identity.privileged && identity.uid !== metadata.uid) {
868
+ return Effect.fail(failure("AccessDenied", operation, path));
869
+ }
870
+ const group = identity.gid === metadata.gid || identity.groups.includes(metadata.gid);
871
+ return Effect.succeed(!identity.privileged && metadata.kind === "file" && !group ? mode & ~0o2000 : mode);
872
+ };
873
+ const changeMode = Effect.fnUntraced(function* (target, mode, options) {
874
+ if (!Schema.is(Mode)(mode))
875
+ return yield* failure("InvalidArgument", "chmod");
876
+ const chosen = options === undefined ? undefined : { ...options };
877
+ return yield* coordinated(Effect.gen(function* () {
878
+ const node = yield* metadataNode(target, chosen, "chmod");
879
+ const permitted = yield* permittedMode(node.metadata, mode, "chmod");
880
+ node.metadata = {
881
+ ...node.metadata,
882
+ mode: permitted,
883
+ ctimeNs: (yield* timestamp("chmod"))
884
+ };
885
+ publishNode(node);
886
+ }));
887
+ });
888
+ const changeOwner = Effect.fnUntraced(function* (target, owner, options) {
889
+ const decoded = Schema.decodeResult(OwnerUpdate, { onExcessProperty: "error" })(owner);
890
+ if (Result.isFailure(decoded))
891
+ return yield* failure("InvalidArgument", "chown");
892
+ const update = { ...decoded.success };
893
+ const chosen = options === undefined ? undefined : { ...options };
894
+ return yield* coordinated(Effect.gen(function* () {
895
+ const node = yield* metadataNode(target, chosen, "chown");
896
+ if (!identity.privileged && (identity.uid !== node.metadata.uid ||
897
+ (update.uid !== undefined && update.uid !== node.metadata.uid) ||
898
+ (update.gid !== undefined && update.gid !== identity.gid && !identity.groups.includes(update.gid)))) {
899
+ return yield* failure("AccessDenied", "chown");
900
+ }
901
+ if (update.uid === undefined && update.gid === undefined)
902
+ return;
903
+ node.metadata = {
904
+ ...node.metadata,
905
+ uid: update.uid ?? node.metadata.uid,
906
+ gid: update.gid ?? node.metadata.gid,
907
+ mode: node.kind === "file" ? node.metadata.mode & ~0o6000 : node.metadata.mode,
908
+ ctimeNs: (yield* timestamp("chown"))
909
+ };
910
+ publishNode(node);
911
+ }));
912
+ });
913
+ const changeTimes = Effect.fnUntraced(function* (target, times, options) {
914
+ const decoded = Schema.decodeResult(Times, { onExcessProperty: "error" })(times);
915
+ if (Result.isFailure(decoded))
916
+ return yield* failure("InvalidArgument", "utimes");
917
+ const access = { ...decoded.success.access };
918
+ const modification = { ...decoded.success.modification };
919
+ const chosen = options === undefined ? undefined : { ...options };
920
+ return yield* coordinated(Effect.gen(function* () {
921
+ const node = yield* metadataNode(target, chosen, "utimes");
922
+ if (access.kind === "omit" && modification.kind === "omit")
923
+ return;
924
+ if (!identity.privileged && identity.uid !== node.metadata.uid) {
925
+ if (access.kind !== "now" || modification.kind !== "now")
926
+ return yield* failure("AccessDenied", "utimes");
927
+ yield* authorize(node, identity, 2, "utimes", "/");
928
+ }
929
+ const now = yield* timestamp("utimes");
930
+ node.metadata = {
931
+ ...node.metadata,
932
+ atimeNs: access.kind === "omit"
933
+ ? node.metadata.atimeNs
934
+ : access.kind === "now"
935
+ ? now
936
+ : access.nanoseconds,
937
+ mtimeNs: modification.kind === "omit"
938
+ ? node.metadata.mtimeNs
939
+ : modification.kind === "now"
940
+ ? now
941
+ : modification.nanoseconds,
942
+ ctimeNs: now
943
+ };
944
+ publishNode(node);
945
+ }));
946
+ });
947
+ const authorizeRemoval = (parent, child, operation, input) => (parent.metadata.mode & 0o1000) !== 0 && !identity.privileged &&
948
+ identity.uid !== parent.metadata.uid && identity.uid !== child.metadata.uid
949
+ ? Effect.fail(failure("AccessDenied", operation, input))
950
+ : Effect.void;
951
+ return Object.freeze({
952
+ [CallerId]: true,
953
+ readFile: Effect.fn("Caller.readFile")(function* (input, options) {
954
+ const prepared = preparePath(input, "readFile", settings.maxPathBytes);
955
+ const base = options?.relativeTo;
956
+ return yield* coordinated(Effect.gen(function* () {
957
+ const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readFile");
958
+ if (node.kind !== "file")
959
+ return yield* failure("IsDirectory", "readFile", input);
960
+ yield* authorize(node, identity, 4, "readFile", input);
961
+ const data = new Uint8Array(node.data);
962
+ node.metadata = { ...node.metadata, atimeNs: (yield* timestamp("readFile")) };
963
+ return data;
964
+ }));
965
+ }),
966
+ writeFile: Effect.fn("Caller.writeFile")(function* (input, bytes, options) {
967
+ const prepared = preparePath(input, "writeFile", settings.maxPathBytes);
968
+ if (!(bytes instanceof Uint8Array) || !(bytes.buffer instanceof ArrayBuffer) || !attachedBuffer(bytes)) {
969
+ return yield* failure("InvalidArgument", "writeFile", input);
970
+ }
971
+ const captured = new Uint8Array(bytes);
972
+ const { relativeTo: base, ...raw } = options;
973
+ const decoded = Schema.decodeResult(WriteFileSettings, { onExcessProperty: "error" })(raw);
974
+ if (Result.isFailure(decoded))
975
+ return yield* failure("InvalidArgument", "writeFile", input);
976
+ const chosen = decoded.success;
977
+ return yield* coordinated(Effect.gen(function* () {
978
+ const path = yield* Effect.fromResult(prepared);
979
+ if (chosen.create === "exclusive") {
980
+ const exists = yield* Effect.result(lookup(path, base, "writeFile", { followFinalSymlink: false }));
981
+ if (Result.isSuccess(exists))
982
+ return yield* failure("AlreadyExists", "writeFile", input);
983
+ if (exists.failure.code !== "NotFound")
984
+ return yield* exists.failure;
985
+ }
986
+ const resolved = yield* lookup(path, base, "writeFile", {
987
+ followFinalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false,
988
+ allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
989
+ });
990
+ const { name, parent } = resolved;
991
+ if (parent === undefined || name === undefined || resolved.node?.kind === "directory") {
992
+ return yield* failure("IsDirectory", "writeFile", input);
993
+ }
994
+ const replaced = resolved.node?.kind === "symlink" ? resolved.node : undefined;
995
+ if (replaced !== undefined && !chosen.replaceFinalSymlink) {
996
+ return yield* failure("SymlinkLoop", "writeFile", input);
997
+ }
998
+ if (chosen.access === "read")
999
+ return yield* failure("InvalidHandle", "writeFile", input);
1000
+ const file = resolved.node?.kind === "file" ? resolved.node : undefined;
1001
+ if (file === undefined) {
1002
+ yield* authorize(parent, identity, 3, "writeFile", input);
1003
+ if (replaced !== undefined)
1004
+ yield* authorizeRemoval(parent, replaced, "writeFile", input);
1005
+ if (replaced === undefined && settings.maxEntries !== undefined && entries >= settings.maxEntries) {
1006
+ return yield* failure("NoSpace", "writeFile", input);
1007
+ }
1008
+ }
1009
+ else
1010
+ yield* authorize(file, identity, chosen.access === "readWrite" ? 6 : 2, "writeFile", input);
1011
+ const finalMode = chosen.finalMode === undefined ? undefined : yield* permittedMode(file?.metadata ?? { kind: "file", uid: identity.uid, gid: parent.metadata.gid }, chosen.finalMode, "writeFile", input);
1012
+ const previous = file?.data.length ?? 0;
1013
+ const initial = chosen.truncate ? 0 : previous;
1014
+ const position = chosen.append ? initial : 0;
1015
+ const size = Math.max(initial, position + captured.length);
1016
+ if (size > maxFileBytes)
1017
+ return yield* failure("FileTooLarge", "writeFile", input);
1018
+ const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
1019
+ if (size - previous > (settings.maxBytes ?? Number.MAX_SAFE_INTEGER) - usedBytes + reclaimed) {
1020
+ return yield* failure("NoSpace", "writeFile", input);
1021
+ }
1022
+ if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
1023
+ return;
1024
+ }
1025
+ let data = captured;
1026
+ if (position !== 0 || size !== captured.length) {
1027
+ data = new Uint8Array(size);
1028
+ if (file !== undefined && !chosen.truncate)
1029
+ data.set(file.data);
1030
+ data.set(captured, position);
1031
+ }
1032
+ const now = yield* timestamp("writeFile");
1033
+ const node = file ??
1034
+ {
1035
+ kind: "file",
1036
+ data,
1037
+ openCount: 0,
1038
+ metadata: {
1039
+ ...directoryMetadata(nextInode++, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
1040
+ kind: "file",
1041
+ nlink: 1
1042
+ }
1043
+ };
1044
+ node.data = data;
1045
+ node.metadata = {
1046
+ ...node.metadata,
1047
+ mode: finalMode ?? node.metadata.mode & ~0o6000,
1048
+ size: BigInt(size),
1049
+ mtimeNs: now,
1050
+ ctimeNs: now
1051
+ };
1052
+ usedBytes += size - previous;
1053
+ if (file === undefined) {
1054
+ if (replaced !== undefined)
1055
+ detach(replaced, now);
1056
+ parent.entries.set(name, node);
1057
+ parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1058
+ if (replaced === undefined)
1059
+ entries += 1;
1060
+ publishEntry(replaced === undefined ? "Create" : "Update", parent, name);
1061
+ }
1062
+ else
1063
+ publishNode(node);
1064
+ }));
1065
+ }),
1066
+ chmod: Effect.fn("Caller.chmod")(function* (path, mode, options) {
1067
+ yield* changeMode(path, mode, options);
1068
+ }),
1069
+ chmodHandle: Effect.fn("Caller.chmodHandle")(function* (handle, mode) {
1070
+ yield* changeMode(handle, mode);
1071
+ }),
1072
+ chown: Effect.fn("Caller.chown")(function* (path, owner, options) {
1073
+ yield* changeOwner(path, owner, options);
1074
+ }),
1075
+ chownHandle: Effect.fn("Caller.chownHandle")(function* (handle, owner) {
1076
+ yield* changeOwner(handle, owner);
1077
+ }),
1078
+ utimes: Effect.fn("Caller.utimes")(function* (path, times, options) {
1079
+ yield* changeTimes(path, times, options);
1080
+ }),
1081
+ utimesHandle: Effect.fn("Caller.utimesHandle")(function* (handle, times) {
1082
+ yield* changeTimes(handle, times);
1083
+ }),
1084
+ access: Effect.fn("Caller.access")(function* (input, bits = 0, options) {
1085
+ const prepared = preparePath(input, "access", settings.maxPathBytes);
1086
+ const base = options?.relativeTo;
1087
+ if (!Number.isInteger(bits) || bits < 0 || bits > 7)
1088
+ return yield* failure("InvalidArgument", "access", input);
1089
+ return yield* coordinated(Effect.gen(function* () {
1090
+ const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "access");
1091
+ if (node.kind === "file" && (bits & 1) !== 0 && (node.metadata.mode & 0o111) === 0) {
1092
+ return yield* failure("AccessDenied", "access", input);
1093
+ }
1094
+ yield* authorize(node, identity, bits, "access", input);
1095
+ }));
1096
+ }),
1097
+ truncate: Effect.fn("Caller.truncate")(function* (input, length, options) {
1098
+ const prepared = preparePath(input, "truncate", settings.maxPathBytes);
1099
+ const base = options?.relativeTo;
1100
+ return yield* coordinated(Effect.gen(function* () {
1101
+ const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "truncate");
1102
+ if (node.kind !== "file")
1103
+ return yield* failure("IsDirectory", "truncate", input);
1104
+ yield* authorize(node, identity, 2, "truncate", input);
1105
+ yield* resize(node, length, "truncate");
1106
+ }));
1107
+ }),
1108
+ lstat: Effect.fn("Caller.lstat")(function* (input, options) {
1109
+ const prepared = preparePath(input, "lstat", settings.maxPathBytes);
1110
+ const base = options?.relativeTo;
1111
+ return yield* coordinated(Effect.gen(function* () {
1112
+ const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "lstat", {
1113
+ followFinalSymlink: false
1114
+ });
1115
+ return { ...node.metadata };
1116
+ }));
1117
+ }),
1118
+ link: Effect.fn("Caller.link")(function* (source, destination, options) {
1119
+ const a = preparePath(source, "link", settings.maxPathBytes);
1120
+ const b = preparePath(destination, "link", settings.maxPathBytes);
1121
+ const sourceBase = options?.sourceRelativeTo;
1122
+ const destinationBase = options?.destinationRelativeTo;
1123
+ const follow = options?.followSourceSymlink ?? false;
1124
+ return yield* coordinated(Effect.gen(function* () {
1125
+ const node = yield* resolveNode(yield* Effect.fromResult(a), sourceBase, "link", {
1126
+ followFinalSymlink: follow
1127
+ });
1128
+ if (node.kind === "directory")
1129
+ return yield* failure("IsDirectory", "link", source);
1130
+ const path = yield* Effect.fromResult(b);
1131
+ const parent = yield* locate(path, destinationBase, "link", { parentOnly: true });
1132
+ yield* authorize(parent, identity, 3, "link", destination);
1133
+ const name = path.components.at(-1);
1134
+ if (name === undefined || name === "2e" || name === "2e2e" || parent.entries.has(name)) {
1135
+ return yield* failure("AlreadyExists", "link", destination);
1136
+ }
1137
+ if (path.trailingSlash)
1138
+ return yield* failure("NotDirectory", "link", destination);
1139
+ if (settings.maxEntries !== undefined && entries >= settings.maxEntries) {
1140
+ return yield* failure("NoSpace", "link", destination);
1141
+ }
1142
+ const now = yield* timestamp("link");
1143
+ parent.entries.set(name, node);
1144
+ parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1145
+ node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1, ctimeNs: now };
1146
+ entries += 1;
1147
+ publishEntry("Create", parent, name);
1148
+ }));
1149
+ }),
1150
+ symlink: Effect.fn("Caller.symlink")(function* (target, input, options) {
1151
+ const prepared = preparePath(input, "symlink", settings.maxPathBytes);
1152
+ if (typeof target === "string" && !wellFormed(target)) {
1153
+ return yield* failure("InvalidPathEncoding", "symlink", target);
1154
+ }
1155
+ const rawTarget = typeof target === "string" ? new TextEncoder().encode(target) : bytePaths.get(target);
1156
+ if (rawTarget === undefined || rawTarget.includes(0)) {
1157
+ return yield* failure("InvalidArgument", "symlink", target);
1158
+ }
1159
+ const targetBytes = new Uint8Array(rawTarget);
1160
+ const base = options?.relativeTo;
1161
+ return yield* coordinated(Effect.gen(function* () {
1162
+ const path = yield* Effect.fromResult(prepared);
1163
+ const bytes = targetBytes;
1164
+ const parent = yield* locate(path, base, "symlink", { parentOnly: true });
1165
+ yield* authorize(parent, identity, 3, "symlink", input);
1166
+ const name = path.components.at(-1);
1167
+ if (name === undefined || name === "2e" || name === "2e2e" || parent.entries.has(name)) {
1168
+ return yield* failure("AlreadyExists", "symlink", input);
1169
+ }
1170
+ if (path.trailingSlash)
1171
+ return yield* failure("NotDirectory", "symlink", input);
1172
+ if ((settings.maxEntries !== undefined && entries >= settings.maxEntries) ||
1173
+ bytes.length > (settings.maxBytes ?? Number.MAX_SAFE_INTEGER) - usedBytes)
1174
+ return yield* failure("NoSpace", "symlink", input);
1175
+ const now = yield* timestamp("symlink");
1176
+ const node = {
1177
+ kind: "symlink",
1178
+ target: new Uint8Array(bytes),
1179
+ metadata: {
1180
+ ...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, 0o777, now),
1181
+ kind: "symlink",
1182
+ nlink: 1,
1183
+ size: BigInt(bytes.length)
1184
+ }
1185
+ };
1186
+ parent.entries.set(name, node);
1187
+ parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1188
+ nextInode += 1n;
1189
+ entries += 1;
1190
+ usedBytes += bytes.length;
1191
+ publishEntry("Create", parent, name);
1192
+ }));
1193
+ }),
1194
+ readDirectoryBytes: Effect.fn("Caller.readDirectoryBytes")(function* (input, options) {
1195
+ return yield* list(input, options);
1196
+ }),
1197
+ readDirectory: Effect.fn("Caller.readDirectory")(function* (input, options) {
1198
+ return yield* Effect.forEach(yield* list(input, options), (bytes) => strictString(bytes, "readDirectory"));
1199
+ }),
1200
+ readLinkBytes: Effect.fn("Caller.readLinkBytes")(function* (input, options) {
1201
+ return yield* readTarget(input, options);
1202
+ }),
1203
+ readLink: Effect.fn("Caller.readLink")(function* (input, options) {
1204
+ return yield* strictString(yield* readTarget(input, options), "readLink");
1205
+ }),
1206
+ realPathBytes: Effect.fn("Caller.realPathBytes")(function* (input, options) {
1207
+ return ownedPath(yield* canonical(input, options));
1208
+ }),
1209
+ realPath: Effect.fn("Caller.realPath")(function* (input, options) {
1210
+ return yield* strictString(yield* canonical(input, options), "realPath");
1211
+ }),
1212
+ open: Effect.fn("Caller.open")(function* (input, options) {
1213
+ const prepared = preparePath(input, "open", settings.maxPathBytes);
1214
+ const { relativeTo: base, ...raw } = options;
1215
+ const decoded = Schema.decodeResult(OpenSettings, { onExcessProperty: "error" })(raw);
1216
+ if (Result.isFailure(decoded))
1217
+ return yield* failure("InvalidArgument", "open", input);
1218
+ const chosen = { ...decoded.success };
1219
+ if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
1220
+ return yield* failure("InvalidArgument", "open", input);
1221
+ }
1222
+ if (chosen.mode !== undefined && (chosen.create === undefined || chosen.create === "never")) {
1223
+ return yield* failure("InvalidArgument", "open", input);
1224
+ }
1225
+ const acquired = {
1226
+ volume: volumeIdentity,
1227
+ file: undefined,
1228
+ closed: false,
1229
+ offset: 0n,
1230
+ access: chosen.access,
1231
+ append: chosen.append ?? false
1232
+ };
1233
+ yield* Effect.addFinalizer(() => coordinated(Effect.sync(() => releaseFile(acquired))));
1234
+ return yield* coordinated(Effect.gen(function* () {
1235
+ if (acquired.closed)
1236
+ return yield* Effect.interrupt;
1237
+ const path = yield* Effect.fromResult(prepared);
1238
+ if (chosen.create === "exclusive") {
1239
+ const existing = yield* Effect.result(lookup(path, base, "open", { followFinalSymlink: false }));
1240
+ if (Result.isSuccess(existing))
1241
+ return yield* failure("AlreadyExists", "open", input);
1242
+ if (existing.failure.code !== "NotFound")
1243
+ return yield* existing.failure;
1244
+ }
1245
+ const resolved = yield* lookup(path, base, "open", {
1246
+ followFinalSymlink: chosen.followFinalSymlink !== false,
1247
+ allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
1248
+ });
1249
+ const parent = resolved.parent;
1250
+ if (parent === undefined)
1251
+ return yield* failure("IsDirectory", "open", input);
1252
+ const name = resolved.name;
1253
+ if (name === undefined || name === "2e" || name === "2e2e") {
1254
+ return yield* failure("IsDirectory", "open", input);
1255
+ }
1256
+ yield* authorize(parent, identity, 1, "open", input);
1257
+ let file = resolved.node;
1258
+ if (file !== undefined && chosen.create === "exclusive") {
1259
+ return yield* failure("AlreadyExists", "open", input);
1260
+ }
1261
+ if (file === undefined) {
1262
+ if (chosen.create === undefined || chosen.create === "never" || path.trailingSlash) {
1263
+ return yield* failure("NotFound", "open", input);
1264
+ }
1265
+ yield* authorize(parent, identity, 3, "open", input);
1266
+ if (settings.maxEntries !== undefined && entries >= settings.maxEntries) {
1267
+ return yield* failure("NoSpace", "open", input);
1268
+ }
1269
+ const now = yield* timestamp("open");
1270
+ file = {
1271
+ kind: "file",
1272
+ data: new Uint8Array(0),
1273
+ openCount: 0,
1274
+ metadata: {
1275
+ ...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
1276
+ kind: "file",
1277
+ nlink: 1
1278
+ }
1279
+ };
1280
+ parent.entries.set(name, file);
1281
+ parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1282
+ entries += 1;
1283
+ nextInode += 1n;
1284
+ publishEntry("Create", parent, name);
1285
+ }
1286
+ else {
1287
+ if (file.kind === "symlink")
1288
+ return yield* failure("SymlinkLoop", "open", input);
1289
+ if (file.kind !== "file")
1290
+ return yield* failure("IsDirectory", "open", input);
1291
+ if (path.trailingSlash)
1292
+ return yield* failure("NotDirectory", "open", input);
1293
+ yield* authorize(file, identity, chosen.access === "read" ? 4 : chosen.access === "write" ? 2 : 6, "open", input);
1294
+ if (chosen.truncate)
1295
+ yield* resize(file, 0n, "open");
1296
+ }
1297
+ file.openCount += 1;
1298
+ acquired.file = file;
1299
+ return fileHandle(acquired);
1300
+ }));
1301
+ }),
1302
+ unlink: Effect.fn("Caller.unlink")(function* (input, options) {
1303
+ const prepared = preparePath(input, "unlink", settings.maxPathBytes);
1304
+ const base = options?.relativeTo;
1305
+ return yield* coordinated(Effect.gen(function* () {
1306
+ const path = yield* Effect.fromResult(prepared);
1307
+ const parent = yield* locate(path, base, "unlink", { parentOnly: true });
1308
+ yield* authorize(parent, identity, 3, "unlink", input);
1309
+ const name = path.components.at(-1);
1310
+ if (name === undefined || name === "2e" || name === "2e2e") {
1311
+ return yield* failure("IsDirectory", "unlink", input);
1312
+ }
1313
+ const child = parent.entries.get(name);
1314
+ if (child === undefined)
1315
+ return yield* failure("NotFound", "unlink", input);
1316
+ if (child.kind === "directory")
1317
+ return yield* failure("IsDirectory", "unlink", input);
1318
+ if (path.trailingSlash)
1319
+ return yield* failure("NotDirectory", "unlink", input);
1320
+ yield* authorizeRemoval(parent, child, "unlink", input);
1321
+ const now = yield* timestamp("unlink");
1322
+ parent.entries.delete(name);
1323
+ parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1324
+ detach(child, now);
1325
+ entries -= 1;
1326
+ publishEntry("Remove", parent, name);
1327
+ }));
1328
+ }),
1329
+ rename: Effect.fn("Caller.rename")(function* (source, destination, options) {
1330
+ const oldPrepared = preparePath(source, "rename", settings.maxPathBytes);
1331
+ const newPrepared = preparePath(destination, "rename", settings.maxPathBytes);
1332
+ const oldBase = options?.sourceRelativeTo;
1333
+ const newBase = options?.destinationRelativeTo;
1334
+ return yield* coordinated(Effect.gen(function* () {
1335
+ const oldPath = yield* Effect.fromResult(oldPrepared);
1336
+ const newPath = yield* Effect.fromResult(newPrepared);
1337
+ const oldParent = yield* locate(oldPath, oldBase, "rename", { parentOnly: true });
1338
+ const newParent = yield* locate(newPath, newBase, "rename", { parentOnly: true });
1339
+ yield* authorize(oldParent, identity, 3, "rename", source);
1340
+ yield* authorize(newParent, identity, 3, "rename", destination);
1341
+ const oldName = oldPath.components.at(-1);
1342
+ const newName = newPath.components.at(-1);
1343
+ if (oldName === undefined || newName === undefined || oldName === "2e" || oldName === "2e2e" ||
1344
+ newName === "2e" || newName === "2e2e") {
1345
+ return yield* failure("InvalidArgument", "rename", source);
1346
+ }
1347
+ const child = oldParent.entries.get(oldName);
1348
+ if (child === undefined)
1349
+ return yield* failure("NotFound", "rename", source);
1350
+ const replaced = newParent.entries.get(newName);
1351
+ if (newPath.trailingSlash && replaced === undefined) {
1352
+ return yield* failure("NotFound", "rename", destination);
1353
+ }
1354
+ if (oldPath.trailingSlash && child.kind !== "directory") {
1355
+ return yield* failure("NotDirectory", "rename", source);
1356
+ }
1357
+ if (newPath.trailingSlash && replaced?.kind !== "directory") {
1358
+ return yield* failure("NotDirectory", "rename", destination);
1359
+ }
1360
+ if (child === replaced)
1361
+ return;
1362
+ yield* authorizeRemoval(oldParent, child, "rename", source);
1363
+ if (replaced !== undefined) {
1364
+ yield* authorizeRemoval(newParent, replaced, "rename", destination);
1365
+ if (child.kind === "directory" && replaced.kind !== "directory") {
1366
+ return yield* failure("NotDirectory", "rename", destination);
1367
+ }
1368
+ if (child.kind !== "directory" && replaced.kind === "directory") {
1369
+ return yield* failure("IsDirectory", "rename", destination);
1370
+ }
1371
+ if (replaced.kind === "directory" && replaced.entries.size > 0) {
1372
+ return yield* failure("NotEmpty", "rename", destination);
1373
+ }
1374
+ }
1375
+ for (let ancestor = newParent; ancestor !== undefined; ancestor = ancestor.parent) {
1376
+ if (ancestor === child)
1377
+ return yield* failure("InvalidArgument", "rename", destination);
1378
+ }
1379
+ const now = yield* timestamp("rename");
1380
+ // All rejection checks precede namespace, ancestry, quota, and metadata publication.
1381
+ const oldEvent = subscribers > 0
1382
+ ? ownedPath(nameBytes(directoryHex(oldParent) + (oldParent === root ? "" : "2f") + oldName))
1383
+ : undefined;
1384
+ oldParent.entries.delete(oldName);
1385
+ newParent.entries.set(newName, child);
1386
+ if (child.kind === "directory")
1387
+ child.parent = newParent;
1388
+ oldParent.metadata = {
1389
+ ...oldParent.metadata,
1390
+ nlink: oldParent.metadata.nlink - (child.kind === "directory" ? 1 : 0),
1391
+ mtimeNs: now,
1392
+ ctimeNs: now
1393
+ };
1394
+ newParent.metadata = {
1395
+ ...newParent.metadata,
1396
+ nlink: newParent.metadata.nlink + (child.kind === "directory" && replaced === undefined ? 1 : 0),
1397
+ mtimeNs: now,
1398
+ ctimeNs: now
1399
+ };
1400
+ child.metadata = { ...child.metadata, ctimeNs: now };
1401
+ if (replaced !== undefined) {
1402
+ detach(replaced, now);
1403
+ entries -= 1;
1404
+ }
1405
+ if (oldEvent !== undefined)
1406
+ PubSub.publishUnsafe(events, { _tag: "Remove", path: oldEvent });
1407
+ publishEntry("Create", newParent, newName);
1408
+ }));
1409
+ }),
1410
+ rmdir: Effect.fn("Caller.rmdir")(function* (input, options) {
1411
+ const prepared = preparePath(input, "rmdir", settings.maxPathBytes);
1412
+ const base = options?.relativeTo;
1413
+ return yield* coordinated(Effect.gen(function* () {
1414
+ const path = yield* Effect.fromResult(prepared);
1415
+ const parent = yield* locate(path, base, "rmdir", { parentOnly: true });
1416
+ yield* authorize(parent, identity, 3, "rmdir", input);
1417
+ const name = path.components.at(-1);
1418
+ if (name === undefined || name === "2e" || name === "2e2e") {
1419
+ return yield* failure("InvalidArgument", "rmdir", input);
1420
+ }
1421
+ const child = parent.entries.get(name);
1422
+ if (child === undefined)
1423
+ return yield* failure("NotFound", "rmdir", input);
1424
+ yield* authorizeRemoval(parent, child, "rmdir", input);
1425
+ if (child.kind !== "directory")
1426
+ return yield* failure("NotDirectory", "rmdir", input);
1427
+ if (child.entries.size > 0)
1428
+ return yield* failure("NotEmpty", "rmdir", input);
1429
+ const now = yield* timestamp("rmdir");
1430
+ parent.entries.delete(name);
1431
+ parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink - 1, mtimeNs: now, ctimeNs: now };
1432
+ child.parent = undefined;
1433
+ child.metadata = { ...child.metadata, nlink: 0, ctimeNs: now };
1434
+ entries -= 1;
1435
+ publishEntry("Remove", parent, name);
1436
+ }));
1437
+ }),
1438
+ stat: Effect.fn("Caller.stat")(function* (input, options) {
1439
+ const prepared = preparePath(input, "stat", settings.maxPathBytes);
1440
+ const base = options?.relativeTo;
1441
+ return yield* coordinated(Effect.gen(function* () {
1442
+ const path = yield* Effect.fromResult(prepared);
1443
+ const directory = yield* resolveNode(path, base, "stat");
1444
+ return { ...directory.metadata };
1445
+ }));
1446
+ }),
1447
+ mkdir: Effect.fn("Caller.mkdir")(function* (input, options) {
1448
+ const prepared = preparePath(input, "mkdir", settings.maxPathBytes);
1449
+ const base = options?.relativeTo;
1450
+ const mode = options?.mode === undefined ? 0o777 : options.mode;
1451
+ if (!Schema.is(Mode)(mode))
1452
+ return yield* failure("InvalidArgument", "mkdir", input);
1453
+ return yield* coordinated(Effect.gen(function* () {
1454
+ const path = yield* Effect.fromResult(prepared);
1455
+ const parent = yield* locate(path, base, "mkdir", { parentOnly: true });
1456
+ yield* authorize(parent, identity, 3, "mkdir", input);
1457
+ const name = path.components.at(-1);
1458
+ if (name === undefined || name === "2e" || name === "2e2e" || parent.entries.has(name)) {
1459
+ return yield* failure("AlreadyExists", "mkdir", input);
1460
+ }
1461
+ if (settings.maxEntries !== undefined && entries >= settings.maxEntries) {
1462
+ return yield* failure("NoSpace", "mkdir", input);
1463
+ }
1464
+ const now = yield* timestamp("mkdir");
1465
+ const child = {
1466
+ kind: "directory",
1467
+ parent,
1468
+ entries: new Map(),
1469
+ metadata: directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (mode & 0o777 & ~umask) | (mode & 0o1000), now)
1470
+ };
1471
+ const parentMetadata = {
1472
+ ...parent.metadata,
1473
+ nlink: parent.metadata.nlink + 1,
1474
+ mtimeNs: now,
1475
+ ctimeNs: now
1476
+ };
1477
+ // No Effect yield or expected failure between these publication writes.
1478
+ parent.entries.set(name, child);
1479
+ parent.metadata = parentMetadata;
1480
+ nextInode += 1n;
1481
+ entries += 1;
1482
+ publishEntry("Create", parent, name);
1483
+ }));
1484
+ }),
1485
+ withDirectory: Effect.fn("Caller.withDirectory")(function* (input, options) {
1486
+ const acquired = yield* acquireDirectory(input, options, "withDirectory");
1487
+ return createCaller(acquired, identity, umask);
1488
+ }),
1489
+ openDirectory: Effect.fn("Caller.openDirectory")(function* (input, options) {
1490
+ const acquired = yield* acquireDirectory(input, options, "openDirectory");
1491
+ const handle = Object.freeze({
1492
+ [DirectoryHandleId]: true,
1493
+ stat: coordinated(Effect.suspend(() => acquired.directory === undefined
1494
+ ? Effect.fail(failure("InvalidHandle", "stat"))
1495
+ : Effect.succeed({ ...acquired.directory.metadata }))).pipe(Effect.withSpan("DirectoryHandle.stat")),
1496
+ close: coordinated(Effect.suspend(() => {
1497
+ if (acquired.directory === undefined)
1498
+ return Effect.fail(failure("InvalidHandle", "close"));
1499
+ acquired.directory = undefined;
1500
+ acquired.closed = true;
1501
+ return Effect.void;
1502
+ })).pipe(Effect.withSpan("DirectoryHandle.close"))
1503
+ });
1504
+ handles.set(handle, acquired);
1505
+ return handle;
1506
+ })
1507
+ });
1508
+ };
1509
+ const volume = Object.freeze({
1510
+ [VolumeId]: true,
1511
+ watch: Effect.gen(function* () {
1512
+ const subscription = yield* PubSub.subscribe(events);
1513
+ subscribers += 1;
1514
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
1515
+ subscribers -= 1;
1516
+ }));
1517
+ return Stream.fromEffectRepeat(PubSub.take(subscription));
1518
+ }).pipe(Effect.withSpan("Volume.watch")),
1519
+ snapshot: coordinated(Effect.gen(function* () {
1520
+ const ids = new Map([[root, "0"]]);
1521
+ const pending = [root];
1522
+ const records = [];
1523
+ for (let index = 0; index < pending.length; index++) {
1524
+ const node = pending[index];
1525
+ if (node === undefined)
1526
+ continue;
1527
+ const id = ids.get(node);
1528
+ if (id === undefined)
1529
+ return yield* new ImageError({ code: "InvalidStructure" });
1530
+ const metadata = storedMetadata(node.metadata);
1531
+ if (node.kind === "directory") {
1532
+ const children = [];
1533
+ for (const [name, child] of node.entries) {
1534
+ let target = ids.get(child);
1535
+ if (target === undefined) {
1536
+ target = String(ids.size);
1537
+ ids.set(child, target);
1538
+ pending.push(child);
1539
+ }
1540
+ children.push({ name: Image.base64(nameBytes(name)), target });
1541
+ }
1542
+ records.push({ id, kind: "directory", metadata, entries: children });
1543
+ }
1544
+ else if (node.kind === "file") {
1545
+ records.push({ id, kind: "file", metadata, data: Image.base64(node.data) });
1546
+ }
1547
+ else
1548
+ records.push({ id, kind: "symlink", metadata, target: Image.base64(node.target) });
1549
+ }
1550
+ return yield* Image.capture({ format: "effect-vfs", version: 1, root: "0", records });
1551
+ })).pipe(Effect.withSpan("Volume.snapshot")),
1552
+ caller: Effect.fn("Volume.caller")(function* (options) {
1553
+ const decoded = decodeConfiguration(RootCallerOptions, options === undefined ? {} : options);
1554
+ if (Result.isFailure(decoded))
1555
+ return yield* decoded.failure;
1556
+ const chosen = decoded.success.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
1557
+ const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
1558
+ return createCaller({ volume: volumeIdentity, directory: root, closed: false }, identity, decoded.success.umask ?? 0o022);
1559
+ })
1560
+ });
1561
+ return volume;
1562
+ });
1563
+ /**
1564
+ * Creates a fresh empty volume and captures the current Effect `Clock`.
1565
+ *
1566
+ * **Details**
1567
+ *
1568
+ * Each execution creates independent storage. Snapshot image failures cannot
1569
+ * arise because this constructor does not accept persisted input.
1570
+ *
1571
+ * @category constructors
1572
+ * @since 0.1.0
1573
+ */
1574
+ export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
1575
+ return yield* makeVolume(options).pipe(Effect.catchTag("ImageError", Effect.die));
1576
+ });
1577
+ /**
1578
+ * Restores a fresh volume from an opaque snapshot under the supplied destination limits.
1579
+ *
1580
+ * @category constructors
1581
+ * @since 0.1.0
1582
+ */
1583
+ export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
1584
+ const image = yield* Image.inspect(snapshot);
1585
+ return yield* makeVolume(options, image);
1586
+ });
1587
+ const FixtureMetadata = Schema.Struct({
1588
+ uid: Schema.optionalKey(Natural),
1589
+ gid: Schema.optionalKey(Natural),
1590
+ mode: Schema.optionalKey(Mode),
1591
+ atimeNs: Schema.optionalKey(Timestamp),
1592
+ mtimeNs: Schema.optionalKey(Timestamp),
1593
+ ctimeNs: Schema.optionalKey(Timestamp),
1594
+ birthtimeNs: Schema.optionalKey(Timestamp)
1595
+ });
1596
+ const FixturePath = Schema.Union([
1597
+ Schema.String,
1598
+ Schema.declare((value) => typeof value === "object" && value !== null && BytePathId in value && value[BytePathId] === true)
1599
+ ]);
1600
+ /**
1601
+ * Schema for a complete fixture namespace with optional metadata and forward hard links.
1602
+ *
1603
+ * **Details**
1604
+ *
1605
+ * Fixture paths must be absolute, unique, and explicitly include their parent directories.
1606
+ *
1607
+ * @category schemas
1608
+ * @since 0.1.0
1609
+ */
1610
+ export const Fixture = Schema.Struct({
1611
+ rootMetadata: Schema.optionalKey(FixtureMetadata),
1612
+ entries: Schema.Array(Schema.Union([
1613
+ Schema.Struct({
1614
+ kind: Schema.Literal("directory"),
1615
+ path: FixturePath,
1616
+ metadata: Schema.optionalKey(FixtureMetadata)
1617
+ }),
1618
+ Schema.Struct({
1619
+ kind: Schema.Literal("file"),
1620
+ path: FixturePath,
1621
+ bytes: Schema.Uint8Array,
1622
+ metadata: Schema.optionalKey(FixtureMetadata)
1623
+ }),
1624
+ Schema.Struct({
1625
+ kind: Schema.Literal("symlink"),
1626
+ path: FixturePath,
1627
+ target: FixturePath,
1628
+ metadata: Schema.optionalKey(FixtureMetadata)
1629
+ }),
1630
+ Schema.Struct({ kind: Schema.Literal("hardLink"), path: FixturePath, target: FixturePath })
1631
+ ]))
1632
+ });
1633
+ /**
1634
+ * Builds a fresh volume from a validated final-state fixture.
1635
+ *
1636
+ * @category constructors
1637
+ * @since 0.1.0
1638
+ */
1639
+ export const fromFixture = Effect.fn("VirtualFileSystem.fromFixture")(function* (fixture, options) {
1640
+ const config = decodeConfiguration(VolumeOptions, options ?? {});
1641
+ if (Result.isFailure(config))
1642
+ return yield* config.failure;
1643
+ const decoded = Schema.decodeResult(Fixture, { onExcessProperty: "error" })(fixture);
1644
+ if (Result.isFailure(decoded))
1645
+ return yield* new ImageError({ code: "InvalidStructure", field: "fixture" });
1646
+ const source = decoded.success;
1647
+ for (const entry of source.entries) {
1648
+ if (entry.kind === "file" && (!(entry.bytes.buffer instanceof ArrayBuffer) || !attachedBuffer(entry.bytes)))
1649
+ return yield* new ImageError({ code: "InvalidEncoding", field: "bytes" });
1650
+ }
1651
+ const metadata = (kind, overrides) => ({
1652
+ uid: overrides?.uid ?? 0,
1653
+ gid: overrides?.gid ?? 0,
1654
+ mode: overrides?.mode ?? (kind === "directory" ? 0o755 : kind === "file" ? 0o644 : 0o777),
1655
+ atimeNs: String(overrides?.atimeNs ?? 0n),
1656
+ mtimeNs: String(overrides?.mtimeNs ?? 0n),
1657
+ ctimeNs: String(overrides?.ctimeNs ?? 0n),
1658
+ birthtimeNs: String(overrides?.birthtimeNs ?? 0n)
1659
+ });
1660
+ const declarations = new Map();
1661
+ const aliases = new Map();
1662
+ const paths = new Map();
1663
+ const root = {
1664
+ id: "root",
1665
+ kind: "directory",
1666
+ metadata: metadata("directory", source.rootMetadata),
1667
+ entries: []
1668
+ };
1669
+ declarations.set("", root);
1670
+ const fixturePath = (input) => preparePath(input, "fixture", config.success.maxPathBytes).pipe(Result.flatMap((path) => !path.absolute || path.components.length === 0 ||
1671
+ path.components.some((name) => name === "2e" || name === "2e2e")
1672
+ ? Result.fail(failure("InvalidArgument", "fixture", input)) :
1673
+ Result.succeed(path.components)));
1674
+ // All byte inputs become immutable strings before the first successful suspension.
1675
+ for (const entry of source.entries) {
1676
+ const parsed = fixturePath(entry.path);
1677
+ if (Result.isFailure(parsed)) {
1678
+ return yield* new ImageError({ code: "InvalidStructure", field: "path" });
1679
+ }
1680
+ const components = parsed.success;
1681
+ const key = components.join("/");
1682
+ if (paths.has(key))
1683
+ return yield* new ImageError({ code: "InvalidStructure", field: "duplicate" });
1684
+ paths.set(key, components);
1685
+ if (entry.kind === "hardLink") {
1686
+ const target = fixturePath(entry.target);
1687
+ if (Result.isFailure(target))
1688
+ return yield* new ImageError({ code: "InvalidStructure", field: "target" });
1689
+ aliases.set(key, target.success.join("/"));
1690
+ }
1691
+ else if (entry.kind === "directory") {
1692
+ declarations.set(key, {
1693
+ id: String(paths.size),
1694
+ kind: "directory",
1695
+ metadata: metadata("directory", entry.metadata),
1696
+ entries: []
1697
+ });
1698
+ }
1699
+ else if (entry.kind === "file") {
1700
+ declarations.set(key, {
1701
+ id: String(paths.size),
1702
+ kind: "file",
1703
+ metadata: metadata("file", entry.metadata),
1704
+ data: Image.base64(entry.bytes)
1705
+ });
1706
+ }
1707
+ else {
1708
+ if (typeof entry.target === "string" && !wellFormed(entry.target)) {
1709
+ return yield* new ImageError({
1710
+ code: "InvalidEncoding",
1711
+ field: "target"
1712
+ });
1713
+ }
1714
+ const target = typeof entry.target === "string"
1715
+ ? new TextEncoder().encode(entry.target)
1716
+ : bytePaths.get(entry.target);
1717
+ if (target === undefined || target.includes(0)) {
1718
+ return yield* new ImageError({
1719
+ code: "InvalidStructure",
1720
+ field: "target"
1721
+ });
1722
+ }
1723
+ declarations.set(key, {
1724
+ id: String(paths.size),
1725
+ kind: "symlink",
1726
+ metadata: metadata("symlink", entry.metadata),
1727
+ target: Image.base64(target)
1728
+ });
1729
+ }
1730
+ }
1731
+ for (const [key] of aliases) {
1732
+ let target = key;
1733
+ const seen = new Set();
1734
+ while (!declarations.has(target)) {
1735
+ if (seen.has(target))
1736
+ return yield* new ImageError({ code: "InvalidStructure", field: "hardLink" });
1737
+ seen.add(target);
1738
+ const next = aliases.get(target);
1739
+ if (next === undefined)
1740
+ return yield* new ImageError({ code: "InvalidStructure", field: "hardLink" });
1741
+ target = next;
1742
+ }
1743
+ const node = declarations.get(target);
1744
+ if (node === undefined || node.kind === "directory") {
1745
+ return yield* new ImageError({
1746
+ code: "InvalidStructure",
1747
+ field: "hardLink"
1748
+ });
1749
+ }
1750
+ for (const alias of seen)
1751
+ declarations.set(alias, node);
1752
+ }
1753
+ const children = new Map();
1754
+ for (const [key, components] of paths) {
1755
+ const parent = declarations.get(components.slice(0, -1).join("/"));
1756
+ const child = declarations.get(key);
1757
+ const name = components.at(-1);
1758
+ if (parent?.kind !== "directory" || child === undefined || name === undefined) {
1759
+ return yield* new ImageError({ code: "InvalidStructure", field: "parent" });
1760
+ }
1761
+ const entries = children.get(parent.id) ?? [];
1762
+ entries.push({ name: Image.base64(nameBytes(name)), target: child.id });
1763
+ children.set(parent.id, entries);
1764
+ }
1765
+ const records = [...new Set(declarations.values())].map((record) => record.kind === "directory" ? { ...record, entries: children.get(record.id) ?? [] } : record);
1766
+ const snapshot = yield* Image.capture({ format: "effect-vfs", version: 1, root: "root", records });
1767
+ return yield* makeVolume(config.success, yield* Image.inspect(snapshot));
1768
+ });