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