@effect-vfs/core 0.2.0 → 0.3.1

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