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