@effect-vfs/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BytePath.d.ts +18 -5
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/BytePath.js +9 -9
- package/dist/Snapshot.d.ts +3 -3
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +3 -4
- package/dist/SnapshotDelta.d.ts +97 -30
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +51 -24
- package/dist/VirtualFileSystem.d.ts +122 -344
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +53 -1773
- package/dist/internal/bytePath.d.ts +10 -0
- package/dist/internal/bytePath.d.ts.map +1 -0
- package/dist/internal/bytePath.js +46 -0
- package/dist/internal/canonicalBase64.d.ts +3 -3
- package/dist/internal/canonicalBase64.d.ts.map +1 -1
- package/dist/internal/canonicalBase64.js +3 -3
- package/dist/internal/image.d.ts +21 -26
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +4 -9
- package/dist/internal/snapshotDelta.d.ts +0 -1
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +15 -28
- package/dist/internal/snapshotDeltaModel.d.ts +10 -0
- package/dist/internal/snapshotDeltaModel.d.ts.map +1 -0
- package/dist/internal/snapshotDeltaModel.js +13 -0
- package/dist/internal/virtualFileSystem/errors.d.ts +26 -0
- package/dist/internal/virtualFileSystem/errors.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/errors.js +32 -0
- package/dist/internal/virtualFileSystem/fixture.d.ts +63 -0
- package/dist/internal/virtualFileSystem/fixture.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/fixture.js +144 -0
- package/dist/internal/{content.d.ts → virtualFileSystem/overlayContent.d.ts} +4 -14
- package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +1 -0
- package/dist/internal/{content.js → virtualFileSystem/overlayContent.js} +3 -7
- package/dist/internal/{overlayChanges.d.ts → virtualFileSystem/overlayDiff.d.ts} +1 -1
- package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/path.d.ts +43 -0
- package/dist/internal/virtualFileSystem/path.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/path.js +124 -0
- package/dist/internal/virtualFileSystem/testHooks.d.ts +20 -0
- package/dist/internal/virtualFileSystem/testHooks.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/testHooks.js +23 -0
- package/dist/internal/virtualFileSystem/watchHub.d.ts +16 -0
- package/dist/internal/virtualFileSystem/watchHub.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/watchHub.js +33 -0
- package/dist/internal/virtualFileSystem.d.ts +290 -0
- package/dist/internal/virtualFileSystem.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem.js +1677 -0
- package/package.json +1 -1
- package/dist/internal/content.d.ts.map +0 -1
- package/dist/internal/overlayChanges.d.ts.map +0 -1
- package/dist/internal/overlayTesting.d.ts +0 -12
- package/dist/internal/overlayTesting.d.ts.map +0 -1
- package/dist/internal/overlayTesting.js +0 -12
- /package/dist/internal/{overlayChanges.js → virtualFileSystem/overlayDiff.js} +0 -0
|
@@ -8,228 +8,125 @@
|
|
|
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
|
|
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";
|
|
16
|
+
import { BytePath } from "./BytePath.js";
|
|
28
17
|
export { BytePath } from "./BytePath.js";
|
|
29
|
-
import
|
|
18
|
+
import { DecodeLimits, ImageError } from "./Snapshot.js";
|
|
19
|
+
export { DecodeLimits, ImageError, SnapshotTypeId } from "./Snapshot.js";
|
|
30
20
|
import * as Image from "./internal/image.js";
|
|
31
|
-
import { compareOverlay } from "./internal/overlayChanges.js";
|
|
32
|
-
import * as OverlayTesting from "./internal/overlayTesting.js";
|
|
33
21
|
import * as SnapshotDeltaInternal from "./internal/snapshotDelta.js";
|
|
22
|
+
import * as VfsModel from "./internal/virtualFileSystem.js";
|
|
23
|
+
import * as FixtureInternal from "./internal/virtualFileSystem/fixture.js";
|
|
24
|
+
import * as Path from "./internal/virtualFileSystem/path.js";
|
|
34
25
|
import * as SnapshotDeltaModel from "./SnapshotDelta.js";
|
|
35
26
|
export { SnapshotChange, SnapshotChangesOptions, SnapshotDeltaError, SnapshotDeltaLimits, SnapshotDeltaTypeId, SnapshotDifference, SnapshotNodeKind } from "./SnapshotDelta.js";
|
|
36
|
-
const VolumeId =
|
|
37
|
-
const CallerId =
|
|
38
|
-
const FileHandleId =
|
|
39
|
-
const DirectoryHandleId =
|
|
27
|
+
const VolumeId = VfsModel.VolumeId;
|
|
28
|
+
const CallerId = VfsModel.CallerId;
|
|
29
|
+
const FileHandleId = VfsModel.FileHandleId;
|
|
30
|
+
const DirectoryHandleId = VfsModel.DirectoryHandleId;
|
|
31
|
+
const ObjectReferenceId = VfsModel.ObjectReferenceId;
|
|
40
32
|
/**
|
|
41
33
|
* Schema for portable virtual filesystem error codes.
|
|
42
34
|
*
|
|
43
35
|
* @category schemas
|
|
44
36
|
* @since 0.1.0
|
|
45
37
|
*/
|
|
46
|
-
export const FsCode =
|
|
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
|
-
]);
|
|
38
|
+
export const FsCode = VfsModel.FsCode;
|
|
65
39
|
/**
|
|
66
40
|
* Describes an expected filesystem operation failure.
|
|
67
41
|
*
|
|
68
42
|
* @category errors
|
|
69
43
|
* @since 0.1.0
|
|
70
44
|
*/
|
|
71
|
-
export
|
|
72
|
-
}
|
|
45
|
+
export const FsError = VfsModel.FsError;
|
|
73
46
|
/**
|
|
74
47
|
* Describes an invalid volume or caller option and names the rejected field.
|
|
75
48
|
*
|
|
76
49
|
* @category errors
|
|
77
50
|
* @since 0.1.0
|
|
78
51
|
*/
|
|
79
|
-
export
|
|
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));
|
|
52
|
+
export const ConfigurationError = VfsModel.ConfigurationError;
|
|
83
53
|
/**
|
|
84
54
|
* Schema for a caller's numeric identity, supplementary groups, and explicit privilege.
|
|
85
55
|
*
|
|
86
56
|
* @category schemas
|
|
87
57
|
* @since 0.1.0
|
|
88
58
|
*/
|
|
89
|
-
export const Identity =
|
|
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
|
-
});
|
|
59
|
+
export const Identity = VfsModel.Identity;
|
|
99
60
|
/**
|
|
100
61
|
* Schema for root caller credentials and creation mask.
|
|
101
62
|
*
|
|
102
63
|
* @category schemas
|
|
103
64
|
* @since 0.1.0
|
|
104
65
|
*/
|
|
105
|
-
export const RootCallerOptions =
|
|
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
|
-
});
|
|
66
|
+
export const RootCallerOptions = VfsModel.RootCallerOptions;
|
|
111
67
|
/**
|
|
112
68
|
* Schema for optional volume capacity and path limits.
|
|
113
69
|
*
|
|
114
70
|
* @category schemas
|
|
115
71
|
* @since 0.1.0
|
|
116
72
|
*/
|
|
117
|
-
export const VolumeOptions =
|
|
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));
|
|
73
|
+
export const VolumeOptions = VfsModel.VolumeOptions;
|
|
130
74
|
/**
|
|
131
75
|
* Schema for filesystem node metadata with bigint inode, size, and nanosecond fields.
|
|
132
76
|
*
|
|
133
77
|
* @category schemas
|
|
134
78
|
* @since 0.1.0
|
|
135
79
|
*/
|
|
136
|
-
export const Metadata =
|
|
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
|
-
});
|
|
80
|
+
export const Metadata = VfsModel.Metadata;
|
|
149
81
|
/**
|
|
150
82
|
* Schema for an owner update. Omitted fields retain their existing values.
|
|
151
83
|
*
|
|
152
84
|
* @category schemas
|
|
153
85
|
* @since 0.1.0
|
|
154
86
|
*/
|
|
155
|
-
export const OwnerUpdate =
|
|
87
|
+
export const OwnerUpdate = VfsModel.OwnerUpdate;
|
|
156
88
|
/**
|
|
157
89
|
* Schema for setting a timestamp to the clock, retaining it, or supplying nanoseconds.
|
|
158
90
|
*
|
|
159
91
|
* @category schemas
|
|
160
92
|
* @since 0.1.0
|
|
161
93
|
*/
|
|
162
|
-
export const TimeUpdate =
|
|
163
|
-
Schema.Struct({ kind: Schema.Literal("now") }),
|
|
164
|
-
Schema.Struct({ kind: Schema.Literal("omit") }),
|
|
165
|
-
Schema.Struct({ kind: Schema.Literal("value"), nanoseconds: Timestamp })
|
|
166
|
-
]);
|
|
94
|
+
export const TimeUpdate = VfsModel.TimeUpdate;
|
|
167
95
|
/**
|
|
168
96
|
* Schema for independent access and modification time updates.
|
|
169
97
|
*
|
|
170
98
|
* @category schemas
|
|
171
99
|
* @since 0.1.0
|
|
172
100
|
*/
|
|
173
|
-
export const Times =
|
|
101
|
+
export const Times = VfsModel.Times;
|
|
174
102
|
/**
|
|
175
103
|
* Schema for file seek origins, including dense-file data and hole queries.
|
|
176
104
|
*
|
|
177
105
|
* @category schemas
|
|
178
106
|
* @since 0.1.0
|
|
179
107
|
*/
|
|
180
|
-
export const SeekMode =
|
|
108
|
+
export const SeekMode = VfsModel.SeekMode;
|
|
181
109
|
/**
|
|
182
110
|
* Schema for file access, creation, append, truncate, and symlink behavior.
|
|
183
111
|
*
|
|
184
112
|
* @category schemas
|
|
185
113
|
* @since 0.1.0
|
|
186
114
|
*/
|
|
187
|
-
export const OpenSettings =
|
|
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
|
-
});
|
|
115
|
+
export const OpenSettings = VfsModel.OpenSettings;
|
|
208
116
|
/**
|
|
209
117
|
* Schema for the filesystem entry kinds reported by overlay summaries.
|
|
210
118
|
*
|
|
211
119
|
* @category schemas
|
|
212
120
|
* @since 0.1.0
|
|
213
121
|
*/
|
|
214
|
-
export const OverlayNodeKind =
|
|
122
|
+
export const OverlayNodeKind = VfsModel.OverlayNodeKind;
|
|
215
123
|
/**
|
|
216
124
|
* Schema for observable fields that can differ from an overlay's immutable base.
|
|
217
125
|
*
|
|
218
126
|
* @category schemas
|
|
219
127
|
* @since 0.1.0
|
|
220
128
|
*/
|
|
221
|
-
export const OverlayDifference =
|
|
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));
|
|
129
|
+
export const OverlayDifference = VfsModel.OverlayDifference;
|
|
233
130
|
/**
|
|
234
131
|
* Schema for a final-state overlay difference.
|
|
235
132
|
*
|
|
@@ -241,41 +138,21 @@ const NonEmptyOverlayDifferences = OverlayDifferences.check(Schema.isMinLength(1
|
|
|
241
138
|
* @category schemas
|
|
242
139
|
* @since 0.1.0
|
|
243
140
|
*/
|
|
244
|
-
export const OverlayChange =
|
|
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
|
-
]);
|
|
141
|
+
export const OverlayChange = VfsModel.OverlayChange;
|
|
261
142
|
/**
|
|
262
143
|
* Schema for overlay summary filtering.
|
|
263
144
|
*
|
|
264
145
|
* @category schemas
|
|
265
146
|
* @since 0.1.0
|
|
266
147
|
*/
|
|
267
|
-
export const OverlayChangesOptions =
|
|
268
|
-
/** Include access, modification, change, and birth-time differences. Defaults to `false`. */
|
|
269
|
-
includeTimestamps: Schema.optionalKey(Schema.Boolean)
|
|
270
|
-
});
|
|
148
|
+
export const OverlayChangesOptions = VfsModel.OverlayChangesOptions;
|
|
271
149
|
/**
|
|
272
150
|
* Optional Effect service for providing an existing filesystem caller.
|
|
273
151
|
*
|
|
274
152
|
* @category services
|
|
275
153
|
* @since 0.1.0
|
|
276
154
|
*/
|
|
277
|
-
export
|
|
278
|
-
}
|
|
155
|
+
export const CurrentFileSystem = VfsModel.CurrentFileSystem;
|
|
279
156
|
/**
|
|
280
157
|
* Encodes a snapshot as owned UTF-8 JSON bytes using the version 1 snapshot format.
|
|
281
158
|
*
|
|
@@ -291,7 +168,7 @@ export const encodeSnapshot = Image.encodeSnapshot;
|
|
|
291
168
|
*/
|
|
292
169
|
export const decodeSnapshot = Image.decodeSnapshot;
|
|
293
170
|
const deltaLimits = (limits) => {
|
|
294
|
-
const decoded = decodeConfiguration(SnapshotDeltaModel.SnapshotDeltaLimits, limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
|
|
171
|
+
const decoded = Path.decodeConfiguration(SnapshotDeltaModel.SnapshotDeltaLimits, limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
|
|
295
172
|
return Result.isFailure(decoded) ? Effect.fail(decoded.failure) : Effect.succeed(decoded.success);
|
|
296
173
|
};
|
|
297
174
|
/**
|
|
@@ -307,9 +184,12 @@ export const diffSnapshots = Effect.fn("VirtualFileSystem.diffSnapshots")(functi
|
|
|
307
184
|
/**
|
|
308
185
|
* Verifies an exact snapshot delta against its base and derives an owned path-oriented summary.
|
|
309
186
|
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
187
|
+
*
|
|
188
|
+
* @category snapshots
|
|
189
|
+
* @since 0.1.0
|
|
310
190
|
*/
|
|
311
191
|
export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshotDelta")(function* (base, delta, options, limits) {
|
|
312
|
-
const decoded = decodeConfiguration(SnapshotDeltaModel.SnapshotChangesOptions, options ?? {});
|
|
192
|
+
const decoded = Path.decodeConfiguration(SnapshotDeltaModel.SnapshotChangesOptions, options ?? {});
|
|
313
193
|
if (Result.isFailure(decoded))
|
|
314
194
|
return yield* decoded.failure;
|
|
315
195
|
return yield* SnapshotDeltaInternal.inspectSnapshotDelta(base, delta, decoded.success, yield* deltaLimits(limits));
|
|
@@ -328,6 +208,9 @@ const deltaSchemaIssue = (cause, input, options) => new SchemaIssue.InvalidValue
|
|
|
328
208
|
/**
|
|
329
209
|
* Creates an Effect Schema codec between owned bytes and opaque snapshot deltas.
|
|
330
210
|
* Omission uses `SnapshotDeltaLimits.default`.
|
|
211
|
+
*
|
|
212
|
+
* @category schemas
|
|
213
|
+
* @since 0.1.0
|
|
331
214
|
*/
|
|
332
215
|
export const SnapshotDeltaFromBytes = (limits) => {
|
|
333
216
|
const selected = Schema.decodeResult(SnapshotDeltaModel.SnapshotDeltaLimits, { onExcessProperty: "error" })(limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
|
|
@@ -340,29 +223,6 @@ export const SnapshotDeltaFromBytes = (limits) => {
|
|
|
340
223
|
: SnapshotDeltaInternal.encodeSnapshotDelta(delta, selected.success).pipe(Effect.mapError((cause) => deltaSchemaIssue(cause, delta, options)))
|
|
341
224
|
})));
|
|
342
225
|
};
|
|
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
226
|
/**
|
|
367
227
|
* Creates an opaque byte path by copying the input when the Effect executes.
|
|
368
228
|
*
|
|
@@ -373,1409 +233,25 @@ const attachedBuffer = (bytes) => {
|
|
|
373
233
|
* @category constructors
|
|
374
234
|
* @since 0.1.0
|
|
375
235
|
*/
|
|
376
|
-
export const pathFromBytes =
|
|
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
|
-
});
|
|
236
|
+
export const pathFromBytes = Path.pathFromBytes;
|
|
387
237
|
/**
|
|
388
238
|
* Copies the bytes held by an opaque byte path.
|
|
389
239
|
*
|
|
390
240
|
* @category getters
|
|
391
241
|
* @since 0.1.0
|
|
392
242
|
*/
|
|
393
|
-
export const pathToBytes =
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
});
|
|
243
|
+
export const pathToBytes = Path.pathToBytes;
|
|
244
|
+
/**
|
|
245
|
+
* Schema for a complete fixture namespace with optional metadata and forward hard links.
|
|
246
|
+
*
|
|
247
|
+
* **Details**
|
|
248
|
+
*
|
|
249
|
+
* Fixture paths must be absolute, unique, and explicitly include their parent directories.
|
|
250
|
+
*
|
|
251
|
+
* @category schemas
|
|
252
|
+
* @since 0.1.0
|
|
253
|
+
*/
|
|
254
|
+
export const Fixture = VfsModel.Fixture;
|
|
1779
255
|
/**
|
|
1780
256
|
* Creates a fresh empty volume and captures the current Effect `Clock`.
|
|
1781
257
|
*
|
|
@@ -1787,25 +263,14 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (source,
|
|
|
1787
263
|
* @category constructors
|
|
1788
264
|
* @since 0.1.0
|
|
1789
265
|
*/
|
|
1790
|
-
export const make =
|
|
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
|
-
});
|
|
266
|
+
export const make = VfsModel.make;
|
|
1796
267
|
/**
|
|
1797
268
|
* Restores a fresh volume from an opaque snapshot under the supplied destination limits.
|
|
1798
269
|
*
|
|
1799
270
|
* @category constructors
|
|
1800
271
|
* @since 0.1.0
|
|
1801
272
|
*/
|
|
1802
|
-
export const fromSnapshot =
|
|
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
|
-
});
|
|
273
|
+
export const fromSnapshot = VfsModel.fromSnapshot;
|
|
1809
274
|
/**
|
|
1810
275
|
* Creates an isolated writable volume relative to one immutable snapshot base.
|
|
1811
276
|
*
|
|
@@ -1822,196 +287,11 @@ export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function
|
|
|
1822
287
|
* @category constructors
|
|
1823
288
|
* @since 0.1.0
|
|
1824
289
|
*/
|
|
1825
|
-
export const makeOverlay =
|
|
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.
|
|
1847
|
-
*
|
|
1848
|
-
* **Details**
|
|
1849
|
-
*
|
|
1850
|
-
* Fixture paths must be absolute, unique, and explicitly include their parent directories.
|
|
1851
|
-
*
|
|
1852
|
-
* @category schemas
|
|
1853
|
-
* @since 0.1.0
|
|
1854
|
-
*/
|
|
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
|
-
});
|
|
290
|
+
export const makeOverlay = VfsModel.makeOverlay;
|
|
1878
291
|
/**
|
|
1879
292
|
* Builds a fresh volume from a validated final-state fixture.
|
|
1880
293
|
*
|
|
1881
294
|
* @category constructors
|
|
1882
295
|
* @since 0.1.0
|
|
1883
296
|
*/
|
|
1884
|
-
export const fromFixture =
|
|
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
|
-
});
|
|
297
|
+
export const fromFixture = FixtureInternal.fromFixture;
|