@effect-vfs/core 0.8.0 → 0.10.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 +9 -0
- package/dist/BytePath.d.ts +4 -1
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/BytePath.js +6 -4
- package/dist/Caller.d.ts +107 -66
- package/dist/Caller.d.ts.map +1 -1
- package/dist/Caller.js +16 -86
- package/dist/LiveVolume.d.ts +75 -2
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +87 -3
- package/dist/Search.d.ts +32 -8
- package/dist/Search.d.ts.map +1 -1
- package/dist/Search.js +19 -10
- package/dist/SynchronousAdapter.d.ts +58 -0
- package/dist/SynchronousAdapter.d.ts.map +1 -0
- package/dist/SynchronousAdapter.js +116 -0
- package/dist/Target.d.ts +7 -91
- package/dist/Target.d.ts.map +1 -1
- package/dist/Target.js +7 -47
- package/dist/Testing.d.ts +5 -2
- package/dist/Testing.d.ts.map +1 -1
- package/dist/Testing.js +2 -1
- package/dist/VfsError.d.ts +4 -2
- package/dist/VfsError.d.ts.map +1 -1
- package/dist/VfsError.js +3 -1
- package/dist/VirtualFileSystem.d.ts +151 -44
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +31 -21
- package/dist/Volume.d.ts +6 -1
- package/dist/Volume.d.ts.map +1 -1
- package/dist/Volume.js +6 -2
- package/dist/Watch.d.ts +18 -0
- package/dist/Watch.d.ts.map +1 -1
- package/dist/Watch.js +12 -0
- package/dist/internal/adapterBinding.d.ts +13 -0
- package/dist/internal/adapterBinding.d.ts.map +1 -0
- package/dist/internal/adapterBinding.js +13 -0
- package/dist/internal/bytePath.d.ts +6 -1
- package/dist/internal/bytePath.d.ts.map +1 -1
- package/dist/internal/callerWatch.d.ts +36 -0
- package/dist/internal/callerWatch.d.ts.map +1 -0
- package/dist/internal/callerWatch.js +185 -0
- package/dist/internal/engine.d.ts +1305 -0
- package/dist/internal/engine.d.ts.map +1 -0
- package/dist/internal/engine.js +3458 -0
- package/dist/internal/errors.d.ts +1 -33
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +1 -45
- package/dist/internal/fixture.d.ts +2 -1
- package/dist/internal/fixture.d.ts.map +1 -1
- package/dist/internal/fixture.js +8 -4
- package/dist/internal/image.d.ts +4 -1
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +6 -3
- package/dist/internal/lines.d.ts.map +1 -1
- package/dist/internal/lines.js +3 -2
- package/dist/internal/liveImage.d.ts +4 -2
- package/dist/internal/liveImage.d.ts.map +1 -1
- package/dist/internal/liveImage.js +3 -1
- package/dist/internal/merkle.d.ts +7 -4
- package/dist/internal/merkle.d.ts.map +1 -1
- package/dist/internal/merkle.js +6 -4
- package/dist/internal/search.d.ts +3 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +4 -3
- package/dist/internal/snapshotDelta.d.ts +11 -7
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +15 -9
- package/dist/internal/snapshotMerge.d.ts +1 -1
- package/dist/internal/snapshotMerge.d.ts.map +1 -1
- package/dist/internal/snapshotMerge.js +5 -2
- package/dist/internal/tree.d.ts +4 -2
- package/dist/internal/tree.d.ts.map +1 -1
- package/dist/internal/tree.js +4 -2
- package/dist/internal/virtualFileSystem.d.ts +20 -49
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +85 -1927
- package/dist/internal/volumeLimits.d.ts +1 -0
- package/dist/internal/volumeLimits.d.ts.map +1 -1
- package/dist/internal/volumeLimits.js +3 -2
- package/dist/internal/volumeState.d.ts +10 -79
- package/dist/internal/volumeState.d.ts.map +1 -1
- package/dist/internal/volumeState.js +9 -79
- package/dist/internal/watchHub.d.ts +9 -5
- package/dist/internal/watchHub.d.ts.map +1 -1
- package/dist/internal/watchHub.js +35 -8
- package/package.json +16 -9
- package/dist/internal/inodeTable.d.ts +0 -20
- package/dist/internal/inodeTable.d.ts.map +0 -1
- package/dist/internal/inodeTable.js +0 -48
- package/dist/internal/path.d.ts +0 -56
- package/dist/internal/path.d.ts.map +0 -1
- package/dist/internal/path.js +0 -141
- package/dist/internal/resolution.d.ts +0 -135
- package/dist/internal/resolution.d.ts.map +0 -1
- package/dist/internal/resolution.js +0 -277
- package/dist/internal/testSeams.d.ts +0 -17
- package/dist/internal/testSeams.d.ts.map +0 -1
- package/dist/internal/testSeams.js +0 -20
- package/dist/internal/tokenRegistry.d.ts +0 -77
- package/dist/internal/tokenRegistry.d.ts.map +0 -1
- package/dist/internal/tokenRegistry.js +0 -128
- package/dist/internal/turnstile.d.ts +0 -8
- package/dist/internal/turnstile.d.ts.map +0 -1
- package/dist/internal/turnstile.js +0 -43
|
@@ -0,0 +1,3458 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
2
|
+
import * as Brand from "effect/Brand";
|
|
3
|
+
import * as ByteSize from "effect/ByteSize";
|
|
4
|
+
import * as Cause from "effect/Cause";
|
|
5
|
+
import * as Clock from "effect/Clock";
|
|
6
|
+
import * as Context from "effect/Context";
|
|
7
|
+
import * as Data from "effect/Data";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Hex from "effect/encoding/Hex";
|
|
10
|
+
import * as Equal from "effect/Equal";
|
|
11
|
+
import * as Exit from "effect/Exit";
|
|
12
|
+
import { dual } from "effect/Function";
|
|
13
|
+
import * as HashMap from "effect/HashMap";
|
|
14
|
+
import * as Match from "effect/Match";
|
|
15
|
+
import * as MutableRef from "effect/MutableRef";
|
|
16
|
+
import * as Option from "effect/Option";
|
|
17
|
+
import * as Order from "effect/Order";
|
|
18
|
+
import * as Predicate from "effect/Predicate";
|
|
19
|
+
import * as Result from "effect/Result";
|
|
20
|
+
import * as Scheduler from "effect/Scheduler";
|
|
21
|
+
import * as Schema from "effect/Schema";
|
|
22
|
+
import * as Scope from "effect/Scope";
|
|
23
|
+
import * as Semaphore from "effect/Semaphore";
|
|
24
|
+
import * as Stream from "effect/Stream";
|
|
25
|
+
import { DirectoryHandleId, FileHandleId, SeekMode } from "../FileHandle.js";
|
|
26
|
+
import * as BytePathStorage from "./bytePath.js";
|
|
27
|
+
import { Mode, OwnerUpdate, Times, Timestamp } from "../Metadata.js";
|
|
28
|
+
import { make as makeError, VfsError } from "../VfsError.js";
|
|
29
|
+
/** Filesystem error helpers; image/codec helpers remain community-owned. @internal */
|
|
30
|
+
export var Errors;
|
|
31
|
+
(function (Errors) {
|
|
32
|
+
const encoder = new TextEncoder();
|
|
33
|
+
// Omit invalid paths rather than attach a BytePath that breaks its invariant.
|
|
34
|
+
/** @internal */
|
|
35
|
+
Errors.errorPath = (input) => {
|
|
36
|
+
const bytes = Predicate.isString(input)
|
|
37
|
+
? encoder.encode(input)
|
|
38
|
+
: BytePathStorage.isBytePath(input)
|
|
39
|
+
? BytePathStorage.getBytes(input)
|
|
40
|
+
: undefined;
|
|
41
|
+
if (bytes === undefined || !BytePathStorage.isPathBytes(bytes))
|
|
42
|
+
return undefined;
|
|
43
|
+
return BytePathStorage.isBytePath(input) ? input : BytePathStorage.make(bytes);
|
|
44
|
+
};
|
|
45
|
+
const configurationField = (issue) => {
|
|
46
|
+
if (Predicate.isTagged("Pointer")(issue))
|
|
47
|
+
return issue.path.map(String).join(".");
|
|
48
|
+
if (Predicate.isTagged("Composite")(issue))
|
|
49
|
+
return configurationField(issue.issues[0]);
|
|
50
|
+
if (Predicate.isTagged("Encoding")(issue))
|
|
51
|
+
return configurationField(issue.issue);
|
|
52
|
+
return "options";
|
|
53
|
+
};
|
|
54
|
+
const build = (code, operation, details) => makeError({
|
|
55
|
+
code,
|
|
56
|
+
operation,
|
|
57
|
+
field: details?.field,
|
|
58
|
+
path: details?.path === undefined ? undefined : Errors.errorPath(details.path),
|
|
59
|
+
cause: details?.cause
|
|
60
|
+
});
|
|
61
|
+
/** @internal */
|
|
62
|
+
Errors.argumentFailure = (operation, field, cause) => build("InvalidArgument", operation, { field, cause });
|
|
63
|
+
/** @internal */
|
|
64
|
+
Errors.decodeConfiguration = (schema, value, operation) => Schema.decodeUnknownResult(schema, { onExcessProperty: "error" })(value).pipe(Result.mapError((error) => Errors.argumentFailure(operation, configurationField(error.issue), error)));
|
|
65
|
+
/** @internal */
|
|
66
|
+
Errors.fsFailure = (code, operation, details) => build(code, operation, details);
|
|
67
|
+
const makeOpContext = (operation, located) => ({
|
|
68
|
+
operation,
|
|
69
|
+
fail: (code, details) => Errors.fsFailure(code, operation, located === undefined ? details : { ...details, path: located }),
|
|
70
|
+
at: (path) => makeOpContext(operation, Predicate.isString(path) || BytePathStorage.isBytePath(path) ? path : undefined)
|
|
71
|
+
});
|
|
72
|
+
/** @internal */
|
|
73
|
+
Errors.OpContext = {
|
|
74
|
+
make: (operation) => makeOpContext(operation)
|
|
75
|
+
};
|
|
76
|
+
})(Errors || (Errors = {}));
|
|
77
|
+
const { argumentFailure, decodeConfiguration, errorPath, fsFailure, OpContext } = Errors;
|
|
78
|
+
/** Byte-name rules and filesystem lookup preparation. @internal */
|
|
79
|
+
export var Paths;
|
|
80
|
+
(function (Paths) {
|
|
81
|
+
/** @internal */
|
|
82
|
+
Paths.SLASH_HEX = "2f";
|
|
83
|
+
/** @internal */
|
|
84
|
+
Paths.DOT_HEX = "2e";
|
|
85
|
+
/** @internal */
|
|
86
|
+
Paths.DOT_DOT_HEX = "2e2e";
|
|
87
|
+
/** @internal */
|
|
88
|
+
Paths.SLASH_BYTE = 47;
|
|
89
|
+
const DOT_BYTE = 46;
|
|
90
|
+
/** @internal */
|
|
91
|
+
Paths.NUL_BYTE = 0;
|
|
92
|
+
/** @internal */
|
|
93
|
+
Paths.MAX_SYMLINK_TRAVERSALS = 40;
|
|
94
|
+
/** @internal */
|
|
95
|
+
Paths.MAX_NAME_BYTES = 255;
|
|
96
|
+
/** @internal */
|
|
97
|
+
Paths.isNameBytes = (name) => name.length >= 1 && name.length <= Paths.MAX_NAME_BYTES && !name.includes(Paths.NUL_BYTE) && !name.includes(Paths.SLASH_BYTE) &&
|
|
98
|
+
!(name.length === 1 && name[0] === DOT_BYTE) && !(name.length === 2 && name[0] === DOT_BYTE && name[1] === DOT_BYTE);
|
|
99
|
+
/** @internal */
|
|
100
|
+
Paths.ROOT_PATH = new Uint8Array([Paths.SLASH_BYTE]);
|
|
101
|
+
/** @internal */
|
|
102
|
+
Paths.joinPath = (prefix, name) => {
|
|
103
|
+
const separator = prefix.length === 0 || prefix[prefix.length - 1] === Paths.SLASH_BYTE ? 0 : 1;
|
|
104
|
+
const joined = new Uint8Array(prefix.length + separator + name.length);
|
|
105
|
+
joined.set(prefix);
|
|
106
|
+
if (separator === 1)
|
|
107
|
+
joined[prefix.length] = Paths.SLASH_BYTE;
|
|
108
|
+
joined.set(name, prefix.length + separator);
|
|
109
|
+
return joined;
|
|
110
|
+
};
|
|
111
|
+
// A missing final component follows the same rejection path as "." and ".."; callers choose the POSIX error code.
|
|
112
|
+
/** @internal */
|
|
113
|
+
Paths.isDotComponent = (name) => name === undefined || name === Paths.DOT_HEX || name === Paths.DOT_DOT_HEX;
|
|
114
|
+
/** @internal */
|
|
115
|
+
Paths.ownedPath = (bytes) => BytePathStorage.make(bytes);
|
|
116
|
+
/** @internal */
|
|
117
|
+
Paths.strictString = (bytes, operation) => Effect.try({
|
|
118
|
+
try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes),
|
|
119
|
+
catch: () => fsFailure("UnrepresentableName", operation)
|
|
120
|
+
});
|
|
121
|
+
/** @internal */
|
|
122
|
+
Paths.nameBytes = (name) => Result.getOrThrow(Hex.decode(name));
|
|
123
|
+
// Constructing a view detects detached buffers, including empty ones.
|
|
124
|
+
const attachedBuffer = (bytes) => {
|
|
125
|
+
try {
|
|
126
|
+
const probe = new Uint8Array(bytes.buffer, bytes.byteOffset, 0);
|
|
127
|
+
return probe.byteLength === 0;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error instanceof TypeError)
|
|
131
|
+
return false;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
// Reject shared or detached buffers before copying; Buffer subclasses remain valid.
|
|
136
|
+
/** @internal */
|
|
137
|
+
Paths.isAttachedBytes = (bytes) => Predicate.isUint8Array(bytes) && bytes.buffer instanceof ArrayBuffer && attachedBuffer(bytes);
|
|
138
|
+
/** @internal */
|
|
139
|
+
Paths.pathFromBytes = Effect.fnUntraced(function* (bytes) {
|
|
140
|
+
if (!Paths.isAttachedBytes(bytes))
|
|
141
|
+
return yield* fsFailure("InvalidArgument", "pathFromBytes");
|
|
142
|
+
const owned = new Uint8Array(bytes);
|
|
143
|
+
if (owned.length === 0 || owned.includes(0)) {
|
|
144
|
+
return yield* fsFailure("InvalidArgument", "pathFromBytes");
|
|
145
|
+
}
|
|
146
|
+
return BytePathStorage.make(owned);
|
|
147
|
+
});
|
|
148
|
+
/** @internal */
|
|
149
|
+
Paths.pathToBytes = Effect.fnUntraced(function* (path) {
|
|
150
|
+
const bytes = BytePathStorage.getBytes(path);
|
|
151
|
+
if (bytes === undefined)
|
|
152
|
+
return yield* fsFailure("InvalidArgument", "pathToBytes");
|
|
153
|
+
return new Uint8Array(bytes);
|
|
154
|
+
});
|
|
155
|
+
// String.prototype.isWellFormed requires ES2024; this package targets ES2023.
|
|
156
|
+
/** @internal */
|
|
157
|
+
Paths.isWellFormed = (value) => {
|
|
158
|
+
for (let index = 0; index < value.length; index++) {
|
|
159
|
+
const code = value.charCodeAt(index);
|
|
160
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
161
|
+
const next = value.charCodeAt(++index);
|
|
162
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
};
|
|
170
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
171
|
+
/** @internal */
|
|
172
|
+
Paths.inputBytes = (input) => Predicate.isString(input)
|
|
173
|
+
? Paths.isWellFormed(input) ? Result.succeed(UTF8_ENCODER.encode(input)) : Result.fail("InvalidPathEncoding")
|
|
174
|
+
: Result.fromNullishOr(BytePathStorage.getBytes(input), () => "InvalidArgument");
|
|
175
|
+
/** @internal */
|
|
176
|
+
Paths.preparePath = (input, operation, maxPathBytes) => {
|
|
177
|
+
const encoded = Paths.inputBytes(input);
|
|
178
|
+
if (Result.isFailure(encoded)) {
|
|
179
|
+
return Result.fail(encoded.failure === "InvalidPathEncoding"
|
|
180
|
+
? fsFailure("InvalidPathEncoding", operation, { path: input })
|
|
181
|
+
: fsFailure("InvalidArgument", operation));
|
|
182
|
+
}
|
|
183
|
+
const bytes = encoded.success;
|
|
184
|
+
if (bytes.length === 0)
|
|
185
|
+
return Result.fail(fsFailure("NotFound", operation, { path: input }));
|
|
186
|
+
if (bytes.includes(0))
|
|
187
|
+
return Result.fail(fsFailure("InvalidArgument", operation, { path: input }));
|
|
188
|
+
if (maxPathBytes !== undefined && ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxPathBytes)) {
|
|
189
|
+
return Result.fail(fsFailure("PathTooLong", operation, { path: input }));
|
|
190
|
+
}
|
|
191
|
+
const components = [];
|
|
192
|
+
const suffixes = [];
|
|
193
|
+
let start = 0;
|
|
194
|
+
for (let index = 0; index <= bytes.length; index++) {
|
|
195
|
+
if (index !== bytes.length && bytes[index] !== Paths.SLASH_BYTE)
|
|
196
|
+
continue;
|
|
197
|
+
if (index > start) {
|
|
198
|
+
if (index - start > Paths.MAX_NAME_BYTES) {
|
|
199
|
+
return Result.fail(fsFailure("PathTooLong", operation, { path: input }));
|
|
200
|
+
}
|
|
201
|
+
components.push(Hex.encode(bytes.subarray(start, index)));
|
|
202
|
+
suffixes.push(bytes.subarray(index));
|
|
203
|
+
}
|
|
204
|
+
start = index + 1;
|
|
205
|
+
}
|
|
206
|
+
return Result.succeed({
|
|
207
|
+
input,
|
|
208
|
+
absolute: bytes[0] === Paths.SLASH_BYTE,
|
|
209
|
+
trailingSlash: bytes.at(-1) === Paths.SLASH_BYTE,
|
|
210
|
+
bytes,
|
|
211
|
+
suffixes,
|
|
212
|
+
components
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
})(Paths || (Paths = {}));
|
|
216
|
+
const { DOT_DOT_HEX, DOT_HEX, inputBytes, isAttachedBytes, isDotComponent, isWellFormed, joinPath, MAX_NAME_BYTES, MAX_SYMLINK_TRAVERSALS, nameBytes, ownedPath, preparePath, SLASH_BYTE, SLASH_HEX } = Paths;
|
|
217
|
+
/**
|
|
218
|
+
* Read in the caller's fiber, so a test supplies it with `withVolumeTestSeams` on the call under test.
|
|
219
|
+
*
|
|
220
|
+
* @internal
|
|
221
|
+
*/
|
|
222
|
+
export const VolumeTestSeams = Context.Reference("@effect-vfs/core/VolumeTestSeams", {
|
|
223
|
+
defaultValue: () => ({
|
|
224
|
+
afterSubscribe: Effect.void,
|
|
225
|
+
betweenSnapshotAndSummary: Effect.void,
|
|
226
|
+
beforeTreeRemoval: Effect.void
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
/** @internal */
|
|
230
|
+
export const withVolumeTestSeams = (seams) => Effect.updateService(VolumeTestSeams, (current) => ({
|
|
231
|
+
afterSubscribe: seams.afterSubscribe ?? current.afterSubscribe,
|
|
232
|
+
betweenSnapshotAndSummary: seams.betweenSnapshotAndSummary ?? current.betweenSnapshotAndSummary,
|
|
233
|
+
beforeTreeRemoval: seams.beforeTreeRemoval ?? current.beforeTreeRemoval
|
|
234
|
+
}));
|
|
235
|
+
// A Semaphore can let a newcomer overtake a scheduled waiter. Transfer ownership before scheduling the oldest waiter.
|
|
236
|
+
/** @internal */
|
|
237
|
+
const makeTurnstile = () => {
|
|
238
|
+
let held = false;
|
|
239
|
+
const waiting = [];
|
|
240
|
+
const release = () => {
|
|
241
|
+
const next = waiting.shift();
|
|
242
|
+
if (next === undefined) {
|
|
243
|
+
held = false;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
next.granted = true;
|
|
247
|
+
next.dispatcher.scheduleTask(() => next.wake?.(), 0);
|
|
248
|
+
};
|
|
249
|
+
const waitFor = (ticket) => Effect.callback((resume) => {
|
|
250
|
+
if (ticket.granted)
|
|
251
|
+
return resume(Effect.void);
|
|
252
|
+
ticket.wake = () => resume(Effect.void);
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
// Check ticket ownership after interruption so a granted turn is released rather than stranded.
|
|
256
|
+
withTurn: (effect) => Effect.uninterruptibleMask((restore) => Effect.withFiber((fiber) => {
|
|
257
|
+
const run = Effect.ensuring(restore(effect), Effect.sync(release));
|
|
258
|
+
if (!held) {
|
|
259
|
+
held = true;
|
|
260
|
+
return run;
|
|
261
|
+
}
|
|
262
|
+
const ticket = { dispatcher: fiber.currentDispatcher, granted: false, wake: undefined };
|
|
263
|
+
waiting.push(ticket);
|
|
264
|
+
return Effect.flatMap(Effect.exit(restore(waitFor(ticket))), (waited) => {
|
|
265
|
+
if (Exit.isSuccess(waited))
|
|
266
|
+
return run;
|
|
267
|
+
if (ticket.granted)
|
|
268
|
+
release();
|
|
269
|
+
else
|
|
270
|
+
waiting.splice(waiting.indexOf(ticket), 1);
|
|
271
|
+
return Effect.failCause(waited.cause);
|
|
272
|
+
});
|
|
273
|
+
}))
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
/** Private addressing; the public Target module is a compatibility view. @internal */
|
|
277
|
+
export var Address;
|
|
278
|
+
(function (Address) {
|
|
279
|
+
/**
|
|
280
|
+
* How a caller names what it operates on. A `Target` is a path, an object
|
|
281
|
+
* reference, or an open handle; an `Entry` is a child of a directory target
|
|
282
|
+
* called `name`. Every verb takes one or the other, so the three addressing
|
|
283
|
+
* modes share one body and one option vocabulary.
|
|
284
|
+
*
|
|
285
|
+
* @since 0.6.0
|
|
286
|
+
*/
|
|
287
|
+
const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value) && value[FileHandleId] === true;
|
|
288
|
+
const isDirectoryHandle = (value) => Predicate.hasProperty(DirectoryHandleId)(value) && value[DirectoryHandleId] === true;
|
|
289
|
+
const enumeration = Data.taggedEnum();
|
|
290
|
+
/**
|
|
291
|
+
* Whether a value is a `Target`.
|
|
292
|
+
*
|
|
293
|
+
* @category predicates
|
|
294
|
+
* @since 0.6.0
|
|
295
|
+
*/
|
|
296
|
+
Address.isTarget = (value) => Predicate.isTagged("Path")(value) || Predicate.isTagged("Reference")(value) || Predicate.isTagged("Handle")(value);
|
|
297
|
+
/**
|
|
298
|
+
* Constructors and matchers for `Target`, plus `of`, which wraps a bare path,
|
|
299
|
+
* reference, or handle.
|
|
300
|
+
*
|
|
301
|
+
* @category constructors
|
|
302
|
+
* @since 0.6.0
|
|
303
|
+
*/
|
|
304
|
+
Address.Target = {
|
|
305
|
+
Path: enumeration.Path,
|
|
306
|
+
Reference: enumeration.Reference,
|
|
307
|
+
Handle: enumeration.Handle,
|
|
308
|
+
$is: enumeration.$is,
|
|
309
|
+
$match: enumeration.$match,
|
|
310
|
+
// A string or byte path is a path and a handle is a handle; anything else is taken for a reference, so a forged
|
|
311
|
+
// token reaches the registry and is reported as one.
|
|
312
|
+
of: (input) => {
|
|
313
|
+
if (Address.isTarget(input))
|
|
314
|
+
return input;
|
|
315
|
+
if (isFileHandle(input) || isDirectoryHandle(input))
|
|
316
|
+
return enumeration.Handle({ handle: input });
|
|
317
|
+
if (Predicate.isString(input) || BytePathStorage.isBytePath(input))
|
|
318
|
+
return enumeration.Path({ path: input });
|
|
319
|
+
return enumeration.Reference({ reference: input });
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
/**
|
|
323
|
+
* Schema for a well-formed entry name, as bytes.
|
|
324
|
+
*
|
|
325
|
+
* @category schemas
|
|
326
|
+
* @since 0.6.0
|
|
327
|
+
*/
|
|
328
|
+
Address.Name = Schema.Uint8Array.check(Schema.makeFilter((bytes) => bytes.length === 0
|
|
329
|
+
? "must not be empty"
|
|
330
|
+
: bytes.length > 255
|
|
331
|
+
? "must be at most 255 bytes"
|
|
332
|
+
: bytes.includes(0)
|
|
333
|
+
? "must not contain NUL"
|
|
334
|
+
: bytes.includes(0x2f)
|
|
335
|
+
? "must not contain a slash"
|
|
336
|
+
: undefined));
|
|
337
|
+
/**
|
|
338
|
+
* Whether a value is an `Entry`.
|
|
339
|
+
*
|
|
340
|
+
* @category predicates
|
|
341
|
+
* @since 0.6.0
|
|
342
|
+
*/
|
|
343
|
+
Address.isEntry = (value) => Predicate.hasProperty(value, "directory") && Predicate.hasProperty(value, "name") && Address.isTarget(value.directory);
|
|
344
|
+
/**
|
|
345
|
+
* Names a child of a directory. The directory may be given bare.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* import { Entry, Target } from "@effect-vfs/core/Target"
|
|
350
|
+
*
|
|
351
|
+
* const explicit = Entry(Target.Path({ path: "/var" }), "log")
|
|
352
|
+
* const bare = Entry("/var", "log")
|
|
353
|
+
* ```
|
|
354
|
+
*
|
|
355
|
+
* @category constructors
|
|
356
|
+
* @since 0.6.0
|
|
357
|
+
*/
|
|
358
|
+
Address.Entry = dual(2, (directory, name) => ({ directory: Address.Target.of(directory), name }));
|
|
359
|
+
/**
|
|
360
|
+
* Whether a path input is a byte path rather than a string.
|
|
361
|
+
*
|
|
362
|
+
* @category predicates
|
|
363
|
+
* @since 0.6.0
|
|
364
|
+
*/
|
|
365
|
+
Address.isBytePathInput = (input) => BytePathStorage.isBytePath(input);
|
|
366
|
+
})(Address || (Address = {}));
|
|
367
|
+
const isTarget = Address.isTarget;
|
|
368
|
+
const Target = Address.Target;
|
|
369
|
+
const isEntry = Address.isEntry;
|
|
370
|
+
/**
|
|
371
|
+
* Brand key that marks an object reference.
|
|
372
|
+
*
|
|
373
|
+
* @category type IDs
|
|
374
|
+
* @since 0.6.0
|
|
375
|
+
*/
|
|
376
|
+
export const ObjectReferenceId = Symbol.for("@effect-vfs/core/ObjectReference");
|
|
377
|
+
/**
|
|
378
|
+
* Schema for a caller's credentials. `privileged` is explicit; uid zero alone
|
|
379
|
+
* grants nothing.
|
|
380
|
+
*
|
|
381
|
+
* @category schemas
|
|
382
|
+
* @since 0.6.0
|
|
383
|
+
*/
|
|
384
|
+
export const Identity = Schema.Struct({
|
|
385
|
+
uid: Schema.Natural,
|
|
386
|
+
gid: Schema.Natural,
|
|
387
|
+
groups: Schema.Array(Schema.Natural),
|
|
388
|
+
privileged: Schema.Boolean
|
|
389
|
+
});
|
|
390
|
+
/**
|
|
391
|
+
* Schema for the options of a root caller.
|
|
392
|
+
*
|
|
393
|
+
* @category schemas
|
|
394
|
+
* @since 0.6.0
|
|
395
|
+
*/
|
|
396
|
+
export const RootCallerOptions = Schema.Struct({
|
|
397
|
+
identity: Schema.optionalKey(Identity),
|
|
398
|
+
umask: Schema.optionalKey(Schema.Natural.check(Schema.isLessThanOrEqualTo(0o777)))
|
|
399
|
+
});
|
|
400
|
+
/**
|
|
401
|
+
* Schema for the options of `open` on a target: the access mode, whether a
|
|
402
|
+
* missing file is created, the creation mode, and append and truncate flags.
|
|
403
|
+
*
|
|
404
|
+
* @category schemas
|
|
405
|
+
* @since 0.6.0
|
|
406
|
+
*/
|
|
407
|
+
export const OpenOptions = Schema.Struct({
|
|
408
|
+
access: Schema.Literals(["read", "write", "readWrite"]),
|
|
409
|
+
create: Schema.optionalKey(Schema.Literals(["never", "ifMissing", "exclusive"])),
|
|
410
|
+
mode: Schema.optionalKey(Mode),
|
|
411
|
+
append: Schema.optionalKey(Schema.Boolean),
|
|
412
|
+
truncate: Schema.optionalKey(Schema.Boolean)
|
|
413
|
+
});
|
|
414
|
+
/**
|
|
415
|
+
* Schema for the options of `writeFile`: the open options plus whether a
|
|
416
|
+
* final symbolic link is replaced or followed and the mode the file ends with.
|
|
417
|
+
*
|
|
418
|
+
* @category schemas
|
|
419
|
+
* @since 0.6.0
|
|
420
|
+
*/
|
|
421
|
+
export const WriteFileOptions = Schema.Struct({
|
|
422
|
+
...OpenOptions.fields,
|
|
423
|
+
followFinalSymlink: Schema.optionalKey(Schema.Boolean),
|
|
424
|
+
replaceFinalSymlink: Schema.optionalKey(Schema.Boolean),
|
|
425
|
+
finalMode: Schema.optionalKey(Mode)
|
|
426
|
+
});
|
|
427
|
+
/**
|
|
428
|
+
* Schema for a directory's revision before and after a change.
|
|
429
|
+
*
|
|
430
|
+
* @category schemas
|
|
431
|
+
* @since 0.6.0
|
|
432
|
+
*/
|
|
433
|
+
export const DirectoryChange = Schema.Struct({
|
|
434
|
+
before: Schema.BigInt,
|
|
435
|
+
after: Schema.BigInt
|
|
436
|
+
});
|
|
437
|
+
/**
|
|
438
|
+
* Schema for the result of `rename`.
|
|
439
|
+
*
|
|
440
|
+
* @category schemas
|
|
441
|
+
* @since 0.6.0
|
|
442
|
+
*/
|
|
443
|
+
export const RenameReferenceResult = Schema.TaggedUnion({
|
|
444
|
+
SameDirectory: { directory: DirectoryChange },
|
|
445
|
+
DifferentDirectories: {
|
|
446
|
+
sourceDirectory: DirectoryChange,
|
|
447
|
+
destinationDirectory: DirectoryChange
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
/**
|
|
451
|
+
* Schema for the options of `mkdir`. `exactMode` skips the caller's umask for
|
|
452
|
+
* an explicit creation mode; permission policy still applies. `recursive`
|
|
453
|
+
* creates every missing directory on the path in one change, as `mkdir -p`
|
|
454
|
+
* does, and succeeds when the final directory already exists; the mode
|
|
455
|
+
* applies to every directory it creates and the times to the final one.
|
|
456
|
+
*
|
|
457
|
+
* @category schemas
|
|
458
|
+
* @since 0.6.0
|
|
459
|
+
*/
|
|
460
|
+
export const MkdirOptions = Schema.Struct({
|
|
461
|
+
mode: Schema.optionalKey(Mode),
|
|
462
|
+
exactMode: Schema.optionalKey(Schema.Boolean),
|
|
463
|
+
times: Schema.optionalKey(Times),
|
|
464
|
+
recursive: Schema.optionalKey(Schema.Boolean)
|
|
465
|
+
});
|
|
466
|
+
/**
|
|
467
|
+
* Schema for the options of `walk`: the order entries arrive in and the
|
|
468
|
+
* optional bounds on how deep, how many, and how many bytes the walk may
|
|
469
|
+
* reach. `order` defaults to `"pre"`, each directory before its entries; a
|
|
470
|
+
* `"post"` walk reports each directory after them. Every bound defaults to
|
|
471
|
+
* unbounded, and a walk that would pass one fails with `LimitExceeded` naming
|
|
472
|
+
* the bound in `field`.
|
|
473
|
+
*
|
|
474
|
+
* @category schemas
|
|
475
|
+
* @since 0.6.0
|
|
476
|
+
*/
|
|
477
|
+
export const WalkOptions = Schema.Struct({
|
|
478
|
+
order: Schema.optionalKey(Schema.Literals(["pre", "post"])),
|
|
479
|
+
maxDepth: Schema.optionalKey(Schema.Natural),
|
|
480
|
+
maxEntries: Schema.optionalKey(Schema.Natural),
|
|
481
|
+
maxBytes: Schema.optionalKey(Schema.ByteSize)
|
|
482
|
+
});
|
|
483
|
+
/**
|
|
484
|
+
* Schema for the options of `remove`. `recursive` removes a directory and
|
|
485
|
+
* everything under it; `force` succeeds when the target itself is missing.
|
|
486
|
+
* `expected` pins object identity at each coordinated removal stage. A replacement fails `VolumeBusy`.
|
|
487
|
+
*
|
|
488
|
+
* @category schemas
|
|
489
|
+
* @since 0.6.0
|
|
490
|
+
*/
|
|
491
|
+
export const RemoveOptions = Schema.Struct({
|
|
492
|
+
recursive: Schema.optionalKey(Schema.Boolean),
|
|
493
|
+
force: Schema.optionalKey(Schema.Boolean),
|
|
494
|
+
expected: Schema.optionalKey(Schema.suspend(() => ObjectReferenceSchema))
|
|
495
|
+
});
|
|
496
|
+
/**
|
|
497
|
+
* Schema for the options of `symlink`.
|
|
498
|
+
*
|
|
499
|
+
* @category schemas
|
|
500
|
+
* @since 0.6.0
|
|
501
|
+
*/
|
|
502
|
+
export const SymlinkOptions = Schema.Struct({
|
|
503
|
+
times: Schema.optionalKey(Times)
|
|
504
|
+
});
|
|
505
|
+
/**
|
|
506
|
+
* Schema for the attributes `setattr` changes together: a regular file's
|
|
507
|
+
* size, the permission mode, the owner, and the access and modification
|
|
508
|
+
* times. An omitted attribute keeps its current value. `expected` pins the
|
|
509
|
+
* target's revision the caller last observed, checked inside the same change,
|
|
510
|
+
* and fails as `StaleReference` naming `expected` when the target has moved
|
|
511
|
+
* on, so a change decided from that observation never applies to a newer one.
|
|
512
|
+
*
|
|
513
|
+
* @category schemas
|
|
514
|
+
* @since 0.6.0
|
|
515
|
+
*/
|
|
516
|
+
export const SetattrOptions = Schema.Struct({
|
|
517
|
+
size: Schema.optionalKey(Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n))),
|
|
518
|
+
mode: Schema.optionalKey(Mode),
|
|
519
|
+
owner: Schema.optionalKey(OwnerUpdate),
|
|
520
|
+
times: Schema.optionalKey(Times),
|
|
521
|
+
expected: Schema.optionalKey(Schema.Struct({ revision: Schema.BigInt }))
|
|
522
|
+
});
|
|
523
|
+
/**
|
|
524
|
+
* Schema for an object reference: any value carrying the reference brand. Whether the volume issued it is checked
|
|
525
|
+
* where it is used.
|
|
526
|
+
*
|
|
527
|
+
* @internal
|
|
528
|
+
*/
|
|
529
|
+
export const ObjectReferenceSchema = Schema.declare((input) => Predicate.hasProperty(ObjectReferenceId)(input) && input[ObjectReferenceId] === true);
|
|
530
|
+
/**
|
|
531
|
+
* Schema for the options of `open` on an entry, a lookup-or-create-and-open in
|
|
532
|
+
* one gate hold. `expected` pins the child the caller last observed under the
|
|
533
|
+
* name (or `null` for none) and fails as `VolumeBusy` when another has
|
|
534
|
+
* appeared; `expectedChild` pins its revision and times as well and fails as
|
|
535
|
+
* `StaleReference` when they moved.
|
|
536
|
+
*
|
|
537
|
+
* @category schemas
|
|
538
|
+
* @since 0.6.0
|
|
539
|
+
*/
|
|
540
|
+
export const OpenEntryOptions = Schema.Struct({
|
|
541
|
+
...OpenOptions.fields,
|
|
542
|
+
followFinalSymlink: Schema.optionalKey(Schema.Boolean),
|
|
543
|
+
times: Schema.optionalKey(Times),
|
|
544
|
+
initialSize: Schema.optionalKey(Schema.BigInt),
|
|
545
|
+
exactMode: Schema.optionalKey(Schema.Boolean),
|
|
546
|
+
owner: Schema.optionalKey(OwnerUpdate),
|
|
547
|
+
expected: Schema.optionalKey(Schema.NullOr(ObjectReferenceSchema)),
|
|
548
|
+
expectedChild: Schema.optionalKey(Schema.NullOr(Schema.Struct({
|
|
549
|
+
reference: ObjectReferenceSchema,
|
|
550
|
+
revision: Schema.BigInt,
|
|
551
|
+
atimeNs: Timestamp,
|
|
552
|
+
mtimeNs: Timestamp
|
|
553
|
+
})))
|
|
554
|
+
});
|
|
555
|
+
/** State shared with community codecs through the legacy volumeState view. @internal */
|
|
556
|
+
export var State;
|
|
557
|
+
(function (State) {
|
|
558
|
+
/** @internal */
|
|
559
|
+
State.Ino = Brand.nominal();
|
|
560
|
+
/** @internal */
|
|
561
|
+
State.ROOT_INO = State.Ino(1);
|
|
562
|
+
// The largest inode number a value holds. The allocator resumes one above it and must stay exactly representable,
|
|
563
|
+
// since the inode table is keyed by a number.
|
|
564
|
+
/** @internal */
|
|
565
|
+
State.MAX_INO = Number.MAX_SAFE_INTEGER - 1;
|
|
566
|
+
// File sizes fit an unsigned 32-bit count in the volume and its public options.
|
|
567
|
+
/** @internal */
|
|
568
|
+
State.MAX_FILE_BYTES = 0xffffffff;
|
|
569
|
+
// Nodes walked between yields. Whole-tree reads are one synchronous tick otherwise, which
|
|
570
|
+
// starves the event loop and leaves nothing for interruption to act on.
|
|
571
|
+
/** @internal */
|
|
572
|
+
State.WALK_YIELD_INTERVAL = 128;
|
|
573
|
+
/** @internal */
|
|
574
|
+
State.getNode = (state, ino) => Option.getOrUndefined(HashMap.get(state.inodes, ino));
|
|
575
|
+
// Only filesystem-owned records enter this boundary. Identity equality keeps a metadata update from hashing
|
|
576
|
+
// shared file bytes or directory contents; nested payloads and public metadata retain their value semantics.
|
|
577
|
+
/** @internal */
|
|
578
|
+
State.setNode = (inodes, node) => HashMap.set(inodes, node.ino, Equal.byReferenceUnsafe(node));
|
|
579
|
+
/** @internal */
|
|
580
|
+
State.byIno = Order.mapInput(Order.Number, (node) => node.ino);
|
|
581
|
+
/** @internal */
|
|
582
|
+
State.byEntryName = Order.mapInput(Order.String, ([name]) => name);
|
|
583
|
+
const inNameOrder = (entries) => {
|
|
584
|
+
let previous;
|
|
585
|
+
for (const name of entries.keys()) {
|
|
586
|
+
if (previous !== undefined && previous > name)
|
|
587
|
+
return false;
|
|
588
|
+
previous = name;
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
};
|
|
592
|
+
State.inEntryOrder = (directory) => inNameOrder(directory.entries)
|
|
593
|
+
? directory
|
|
594
|
+
: { ...directory, entries: new Map([...directory.entries].sort(State.byEntryName)) };
|
|
595
|
+
/** @internal */
|
|
596
|
+
State.payloadOf = (node) => node.kind === "file" ? node.data : node.kind === "symlink" ? node.target : undefined;
|
|
597
|
+
/** @internal */
|
|
598
|
+
State.directoryMetadata = (ino, uid, gid, mode, now) => ({
|
|
599
|
+
kind: "directory",
|
|
600
|
+
ino,
|
|
601
|
+
uid,
|
|
602
|
+
gid,
|
|
603
|
+
mode,
|
|
604
|
+
nlink: 2,
|
|
605
|
+
size: 0n,
|
|
606
|
+
atimeNs: now,
|
|
607
|
+
mtimeNs: now,
|
|
608
|
+
ctimeNs: now,
|
|
609
|
+
birthtimeNs: now
|
|
610
|
+
});
|
|
611
|
+
/** @internal */
|
|
612
|
+
State.pathOf = (get, ino) => {
|
|
613
|
+
const names = [];
|
|
614
|
+
let node = get(ino);
|
|
615
|
+
if (node?.kind !== "directory" || node.metadata.nlink === 0)
|
|
616
|
+
return undefined;
|
|
617
|
+
while (node.ino !== State.ROOT_INO) {
|
|
618
|
+
names.push(node.name);
|
|
619
|
+
const parent = get(node.parent);
|
|
620
|
+
if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
|
|
621
|
+
return undefined;
|
|
622
|
+
node = parent;
|
|
623
|
+
}
|
|
624
|
+
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
625
|
+
};
|
|
626
|
+
/** @internal */
|
|
627
|
+
State.entryPath = (prefix, name) => ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name));
|
|
628
|
+
/** @internal */
|
|
629
|
+
State.emptyRoot = (now) => ({
|
|
630
|
+
kind: "directory",
|
|
631
|
+
ino: State.ROOT_INO,
|
|
632
|
+
parent: State.ROOT_INO,
|
|
633
|
+
name: "",
|
|
634
|
+
entries: new Map(),
|
|
635
|
+
metadata: State.directoryMetadata(BigInt(State.ROOT_INO), 0, 0, 0o755, now),
|
|
636
|
+
revision: 1n
|
|
637
|
+
});
|
|
638
|
+
/** @internal */
|
|
639
|
+
State.emptyState = (now) => ({
|
|
640
|
+
inodes: State.setNode(HashMap.empty(), State.emptyRoot(now)),
|
|
641
|
+
open: new Map(),
|
|
642
|
+
nextInode: State.Ino(2),
|
|
643
|
+
revision: 1n,
|
|
644
|
+
entries: 0,
|
|
645
|
+
usedBytes: 0n
|
|
646
|
+
});
|
|
647
|
+
})(State || (State = {}));
|
|
648
|
+
const Ino = State.Ino;
|
|
649
|
+
const ROOT_INO = State.ROOT_INO;
|
|
650
|
+
const MAX_FILE_BYTES = State.MAX_FILE_BYTES;
|
|
651
|
+
const WALK_YIELD_INTERVAL = State.WALK_YIELD_INTERVAL;
|
|
652
|
+
const { entryPath, pathOf } = State;
|
|
653
|
+
const getNode = State.getNode;
|
|
654
|
+
const setNode = State.setNode;
|
|
655
|
+
const byEntryName = State.byEntryName;
|
|
656
|
+
const directoryMetadata = State.directoryMetadata;
|
|
657
|
+
const emptyState = State.emptyState;
|
|
658
|
+
/** Persistent name/slot representation belongs to this engine. @internal */
|
|
659
|
+
var DirectoryEntries;
|
|
660
|
+
(function (DirectoryEntries_1) {
|
|
661
|
+
// Name lookup and insertion order share immutable trees. Deleted slots are compacted once they outnumber live entries.
|
|
662
|
+
class DirectoryEntries {
|
|
663
|
+
names;
|
|
664
|
+
slots;
|
|
665
|
+
length;
|
|
666
|
+
[Symbol.toStringTag] = "Map";
|
|
667
|
+
constructor(names, slots, length) {
|
|
668
|
+
this.names = names;
|
|
669
|
+
this.slots = slots;
|
|
670
|
+
this.length = length;
|
|
671
|
+
}
|
|
672
|
+
get size() {
|
|
673
|
+
return HashMap.size(this.names);
|
|
674
|
+
}
|
|
675
|
+
get(name) {
|
|
676
|
+
const slot = HashMap.get(this.names, name);
|
|
677
|
+
return Option.isSome(slot) ? slot.value.ino : undefined;
|
|
678
|
+
}
|
|
679
|
+
has(name) {
|
|
680
|
+
return HashMap.has(this.names, name);
|
|
681
|
+
}
|
|
682
|
+
*entries() {
|
|
683
|
+
for (let index = 0; index < this.length; index++) {
|
|
684
|
+
const entry = Option.getOrUndefined(HashMap.get(this.slots, index));
|
|
685
|
+
if (entry !== undefined)
|
|
686
|
+
yield [entry.name, entry.ino];
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
*keys() {
|
|
690
|
+
for (const [name] of this)
|
|
691
|
+
yield name;
|
|
692
|
+
}
|
|
693
|
+
*values() {
|
|
694
|
+
for (const [, ino] of this)
|
|
695
|
+
yield ino;
|
|
696
|
+
}
|
|
697
|
+
[Symbol.iterator]() {
|
|
698
|
+
return this.entries();
|
|
699
|
+
}
|
|
700
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- ReadonlyMap forwards the opaque callback receiver without inspecting it.
|
|
701
|
+
forEach(callback, thisArg) {
|
|
702
|
+
for (const [name, ino] of this)
|
|
703
|
+
callback.call(thisArg, ino, name, this);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const fromEntries = (entries) => {
|
|
707
|
+
const slots = HashMap.beginMutation(HashMap.empty());
|
|
708
|
+
let length = 0;
|
|
709
|
+
const names = HashMap.mutate(HashMap.empty(), (mutable) => {
|
|
710
|
+
for (const [name, ino] of entries) {
|
|
711
|
+
const entry = { name, ino, index: length };
|
|
712
|
+
HashMap.set(mutable, name, entry);
|
|
713
|
+
HashMap.set(slots, length, entry);
|
|
714
|
+
length++;
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
return new DirectoryEntries(names, HashMap.endMutation(slots), length);
|
|
718
|
+
};
|
|
719
|
+
/** @internal */
|
|
720
|
+
DirectoryEntries_1.set = (entries, name, ino) => {
|
|
721
|
+
const current = entries instanceof DirectoryEntries ? entries : fromEntries(entries);
|
|
722
|
+
const existing = HashMap.get(current.names, name);
|
|
723
|
+
const index = Option.isSome(existing) ? existing.value.index : current.length;
|
|
724
|
+
const entry = { name, ino, index };
|
|
725
|
+
return new DirectoryEntries(HashMap.set(current.names, name, entry), HashMap.set(current.slots, index, entry), Option.isSome(existing) ? current.length : current.length + 1);
|
|
726
|
+
};
|
|
727
|
+
/** @internal */
|
|
728
|
+
DirectoryEntries_1.remove = (entries, name) => {
|
|
729
|
+
const current = entries instanceof DirectoryEntries ? entries : fromEntries(entries);
|
|
730
|
+
const existing = HashMap.get(current.names, name);
|
|
731
|
+
if (Option.isNone(existing))
|
|
732
|
+
return current;
|
|
733
|
+
const removed = new DirectoryEntries(HashMap.remove(current.names, name), HashMap.remove(current.slots, existing.value.index), current.length);
|
|
734
|
+
return removed.length > removed.size * 2 ? fromEntries(removed) : removed;
|
|
735
|
+
};
|
|
736
|
+
// Full directory observations already materialize every entry. Populate indices once instead of hashing every
|
|
737
|
+
// slot lookup; lazy iterators above keep their incremental traversal for bounded snapshot/search consumers.
|
|
738
|
+
/** @internal */
|
|
739
|
+
DirectoryEntries_1.toArray = (entries) => {
|
|
740
|
+
// Larger logical indices are not array elements; retain the incremental fallback for that domain.
|
|
741
|
+
if (!(entries instanceof DirectoryEntries) || entries.length > 0xffffffff)
|
|
742
|
+
return Array.from(entries);
|
|
743
|
+
const ordered = [];
|
|
744
|
+
HashMap.forEach(entries.slots, (entry) => {
|
|
745
|
+
ordered[entry.index] = [entry.name, entry.ino];
|
|
746
|
+
});
|
|
747
|
+
return ordered.filter((entry) => entry !== undefined);
|
|
748
|
+
};
|
|
749
|
+
})(DirectoryEntries || (DirectoryEntries = {}));
|
|
750
|
+
/** @internal */
|
|
751
|
+
export var TokenRegistry;
|
|
752
|
+
(function (TokenRegistry) {
|
|
753
|
+
// Shared across volumes so a genuine token from another registry is distinguishable from a forgery.
|
|
754
|
+
const references = new WeakMap();
|
|
755
|
+
const files = new WeakMap();
|
|
756
|
+
const directories = new WeakMap();
|
|
757
|
+
/** @internal */
|
|
758
|
+
TokenRegistry.inode = (reference) => reference.lifecycle.kind === "open" || reference.lifecycle.kind === "releasedPendingCommit"
|
|
759
|
+
? reference.lifecycle.ino
|
|
760
|
+
: undefined;
|
|
761
|
+
/** @internal */
|
|
762
|
+
TokenRegistry.isOpen = (reference) => reference.lifecycle.kind === "open";
|
|
763
|
+
/** @internal */
|
|
764
|
+
TokenRegistry.publish = (reference, ino) => {
|
|
765
|
+
reference.lifecycle = reference.lifecycle.kind === "releasedPendingCommit" || reference.lifecycle.kind === "closed"
|
|
766
|
+
? { kind: "releasedPendingCommit", ino }
|
|
767
|
+
: { kind: "open", ino };
|
|
768
|
+
};
|
|
769
|
+
/** @internal */
|
|
770
|
+
TokenRegistry.released = (reference) => {
|
|
771
|
+
reference.lifecycle = reference.lifecycle.kind === "acquiring" ||
|
|
772
|
+
(reference.lifecycle.kind === "releasedPendingCommit" && reference.lifecycle.ino === undefined)
|
|
773
|
+
? { kind: "releasedPendingCommit", ino: undefined }
|
|
774
|
+
: { kind: "closed" };
|
|
775
|
+
};
|
|
776
|
+
// A release callback changes the engine's draft; publication may be immediate cleanup or after installation.
|
|
777
|
+
/** @internal */
|
|
778
|
+
TokenRegistry.release = Effect.fnUntraced(function* (reference, releaseInode, afterRelease) {
|
|
779
|
+
const ino = TokenRegistry.inode(reference);
|
|
780
|
+
yield* afterRelease(() => TokenRegistry.released(reference));
|
|
781
|
+
if (ino !== undefined)
|
|
782
|
+
yield* releaseInode(ino);
|
|
783
|
+
});
|
|
784
|
+
/** @internal */
|
|
785
|
+
TokenRegistry.closeReleasedScope = (reference) => Effect.suspend(() => TokenRegistry.isOpen(reference) || reference.scope === undefined ? Effect.void : Scope.close(reference.scope, Exit.void));
|
|
786
|
+
// The inode, rather than the open tag, decides whether a rerun must release a late publication.
|
|
787
|
+
/** @internal */
|
|
788
|
+
TokenRegistry.finalize = (reference, close, fallback) => Effect.uninterruptible(Effect.suspend(() => {
|
|
789
|
+
if (TokenRegistry.inode(reference) === undefined) {
|
|
790
|
+
// Check and mark together: acquisition can publish between separate Effect steps.
|
|
791
|
+
TokenRegistry.released(reference);
|
|
792
|
+
return Effect.void;
|
|
793
|
+
}
|
|
794
|
+
return Effect.ignore(close).pipe(Effect.andThen(Effect.suspend(() => TokenRegistry.inode(reference) === undefined ? Effect.void : fallback)));
|
|
795
|
+
}));
|
|
796
|
+
// Register before waiting: a closed scope runs its finalizer immediately, and engine permits are not reentrant.
|
|
797
|
+
/** @internal */
|
|
798
|
+
TokenRegistry.acquire = Effect.fnUntraced(function* (reference, coordinate, acquisition, releaseAcquired, finalizer) {
|
|
799
|
+
const scope = yield* Scope.fork(yield* Effect.scope);
|
|
800
|
+
reference.scope = scope;
|
|
801
|
+
yield* Scope.addFinalizer(scope, finalizer);
|
|
802
|
+
const closed = () => Predicate.isTagged(scope.state, "Closed");
|
|
803
|
+
return yield* coordinate(Effect.suspend(() => closed() ? Effect.interrupt : acquisition).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.andThen(releaseAcquired(), Effect.interrupt) : Effect.void)))).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.interrupt : Effect.void)), Effect.onError(() => Effect.andThen(finalizer, Scope.close(scope, Exit.void))), Effect.onError(() => Effect.sync(() => {
|
|
804
|
+
if (reference.lifecycle.kind === "releasedPendingCommit" && reference.lifecycle.ino === undefined) {
|
|
805
|
+
reference.lifecycle = { kind: "closed" };
|
|
806
|
+
}
|
|
807
|
+
})));
|
|
808
|
+
});
|
|
809
|
+
/** @internal */
|
|
810
|
+
TokenRegistry.nodeOrFail = (result, addressing, op) => {
|
|
811
|
+
switch (result.kind) {
|
|
812
|
+
case "live":
|
|
813
|
+
return Effect.succeed(result.node);
|
|
814
|
+
case "unknown":
|
|
815
|
+
return Effect.fail(op.fail(addressing === "reference" ? "InvalidReference" : "InvalidHandle"));
|
|
816
|
+
case "foreign":
|
|
817
|
+
return Effect.fail(op.fail(addressing === "reference" ? "ForeignReference" : "ForeignHandle"));
|
|
818
|
+
case "stale":
|
|
819
|
+
return Effect.fail(op.fail(addressing === "reference" ? "StaleReference" : "InvalidHandle"));
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
/** @internal */
|
|
823
|
+
TokenRegistry.make = (volume, get) => {
|
|
824
|
+
const tokens = new Map();
|
|
825
|
+
const resolveInode = (ino) => Effect.map(get(ino), (node) => node === undefined || (node.kind === "directory" && node.metadata.nlink === 0)
|
|
826
|
+
? { kind: "stale" }
|
|
827
|
+
: { kind: "live", node });
|
|
828
|
+
const resolveReference = (token) => Effect.suspend(() => {
|
|
829
|
+
const known = Predicate.isObject(token) ? references.get(token) : undefined;
|
|
830
|
+
if (known === undefined)
|
|
831
|
+
return Effect.succeed({ kind: "unknown" });
|
|
832
|
+
if (known.volume !== volume)
|
|
833
|
+
return Effect.succeed({ kind: "foreign" });
|
|
834
|
+
return resolveInode(known.ino);
|
|
835
|
+
});
|
|
836
|
+
const resolveHandle = (token, directoryOnly = false) => Effect.suspend(() => {
|
|
837
|
+
const known = Predicate.isObject(token)
|
|
838
|
+
? directories.get(token) ?? (directoryOnly ? undefined : files.get(token))
|
|
839
|
+
: undefined;
|
|
840
|
+
if (known === undefined)
|
|
841
|
+
return Effect.succeed({ kind: "unknown" });
|
|
842
|
+
if (known.volume !== volume)
|
|
843
|
+
return Effect.succeed({ kind: "foreign" });
|
|
844
|
+
if (!TokenRegistry.isOpen(known))
|
|
845
|
+
return Effect.succeed({ kind: "stale" });
|
|
846
|
+
const ino = TokenRegistry.inode(known);
|
|
847
|
+
return ino === undefined
|
|
848
|
+
? Effect.succeed({ kind: "stale" })
|
|
849
|
+
: Effect.map(get(ino), (node) => node === undefined ? { kind: "stale" } : { kind: "live", node });
|
|
850
|
+
});
|
|
851
|
+
return {
|
|
852
|
+
referenceIdentity: (token, op) => {
|
|
853
|
+
const known = Predicate.isObject(token) ? references.get(token) : undefined;
|
|
854
|
+
if (known === undefined)
|
|
855
|
+
return Effect.fail(op.fail("InvalidReference"));
|
|
856
|
+
if (known.volume !== volume)
|
|
857
|
+
return Effect.fail(op.fail("ForeignReference"));
|
|
858
|
+
return Effect.succeed(known.ino);
|
|
859
|
+
},
|
|
860
|
+
handleBoundaries: (token) => (files.get(token) ?? directories.get(token))?.boundaries ?? [],
|
|
861
|
+
authorizeHandle: (token, node, op) => {
|
|
862
|
+
const reference = files.get(token) ?? directories.get(token);
|
|
863
|
+
return reference?.authority?.(node, op) ?? Effect.void;
|
|
864
|
+
},
|
|
865
|
+
resolve: (token, addressing) => addressing === "reference" ? resolveReference(token) : resolveHandle(token, addressing === "directory"),
|
|
866
|
+
resolveInode,
|
|
867
|
+
referenceFor: (ino) => {
|
|
868
|
+
const existing = tokens.get(ino);
|
|
869
|
+
if (existing !== undefined)
|
|
870
|
+
return existing;
|
|
871
|
+
const reference = Object.freeze({ [ObjectReferenceId]: true });
|
|
872
|
+
references.set(reference, { volume, ino });
|
|
873
|
+
tokens.set(ino, reference);
|
|
874
|
+
return reference;
|
|
875
|
+
},
|
|
876
|
+
forget: (ino) => tokens.delete(ino),
|
|
877
|
+
file: (access, append) => ({
|
|
878
|
+
volume,
|
|
879
|
+
lifecycle: { kind: "acquiring" },
|
|
880
|
+
scope: undefined,
|
|
881
|
+
offset: 0n,
|
|
882
|
+
access,
|
|
883
|
+
append
|
|
884
|
+
}),
|
|
885
|
+
directory: (ino) => ({
|
|
886
|
+
volume,
|
|
887
|
+
lifecycle: ino === undefined ? { kind: "acquiring" } : { kind: "open", ino },
|
|
888
|
+
scope: undefined
|
|
889
|
+
}),
|
|
890
|
+
registerFile: (handle, reference) => files.set(handle, reference),
|
|
891
|
+
registerDirectory: (handle, reference) => directories.set(handle, reference)
|
|
892
|
+
};
|
|
893
|
+
};
|
|
894
|
+
})(TokenRegistry || (TokenRegistry = {}));
|
|
895
|
+
// Only engine-owned sections set this reference; cooperative callers keep explicit walk yields.
|
|
896
|
+
const SyncSection = Context.Reference("@effect-vfs/core/SyncSection", { defaultValue: () => false });
|
|
897
|
+
/** @internal */
|
|
898
|
+
var Confinement;
|
|
899
|
+
(function (Confinement) {
|
|
900
|
+
/** @internal */
|
|
901
|
+
Confinement.make = (view, roots) => {
|
|
902
|
+
const descends = Effect.fnUntraced(function* (get, ino, root) {
|
|
903
|
+
let node = get(ino);
|
|
904
|
+
let steps = 0;
|
|
905
|
+
while (node?.kind === "directory" && node.metadata.nlink > 0) {
|
|
906
|
+
if (node.ino === root)
|
|
907
|
+
return true;
|
|
908
|
+
if (node.ino === ROOT_INO)
|
|
909
|
+
return false;
|
|
910
|
+
const parent = get(node.parent);
|
|
911
|
+
if (parent?.kind !== "directory" || parent.entries.get(node.name) !== node.ino)
|
|
912
|
+
return false;
|
|
913
|
+
node = parent;
|
|
914
|
+
if (++steps % WALK_YIELD_INTERVAL === 0 && !(yield* SyncSection))
|
|
915
|
+
yield* Effect.yieldNow;
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
});
|
|
919
|
+
const aliveIn = Effect.fnUntraced(function* (get, op, handle) {
|
|
920
|
+
for (let index = 0; index < roots.length; index++) {
|
|
921
|
+
const root = roots[index];
|
|
922
|
+
const node = get(root);
|
|
923
|
+
if (node?.kind !== "directory" || node.metadata.nlink === 0) {
|
|
924
|
+
return yield* op.fail(handle ? "InvalidHandle" : "ClosedCaller");
|
|
925
|
+
}
|
|
926
|
+
if (index > 0 && !(yield* descends(get, root, roots[index - 1])))
|
|
927
|
+
return yield* op.fail("AccessDenied");
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
const containsIn = Effect.fnUntraced(function* (get, node) {
|
|
931
|
+
const root = roots.at(-1);
|
|
932
|
+
if (root === undefined)
|
|
933
|
+
return true;
|
|
934
|
+
if (node.kind === "directory")
|
|
935
|
+
return yield* descends(get, node.ino, root);
|
|
936
|
+
for (const link of node.links) {
|
|
937
|
+
const parent = get(link.parent);
|
|
938
|
+
if (parent?.kind === "directory" && parent.entries.get(link.name) === node.ino &&
|
|
939
|
+
(yield* descends(get, parent.ino, root)))
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
return false;
|
|
943
|
+
});
|
|
944
|
+
const alive = (op, handle = false) => Effect.flatMap(view, (get) => aliveIn(get, op, handle));
|
|
945
|
+
const contains = (node) => Effect.flatMap(view, (get) => containsIn(get, node));
|
|
946
|
+
const check = Effect.fnUntraced(function* (node, op, handle = false) {
|
|
947
|
+
// Share the current lookup within authorization, never its result between operations.
|
|
948
|
+
const get = yield* view;
|
|
949
|
+
yield* aliveIn(get, op, handle);
|
|
950
|
+
if (!(yield* containsIn(get, node)))
|
|
951
|
+
return yield* op.fail("AccessDenied");
|
|
952
|
+
});
|
|
953
|
+
const path = Effect.fnUntraced(function* (ino) {
|
|
954
|
+
const names = [];
|
|
955
|
+
const get = yield* view;
|
|
956
|
+
let node = get(ino);
|
|
957
|
+
let steps = 0;
|
|
958
|
+
if (node?.kind !== "directory" || node.metadata.nlink === 0)
|
|
959
|
+
return undefined;
|
|
960
|
+
const root = roots.at(-1) ?? ROOT_INO;
|
|
961
|
+
while (node.ino !== root) {
|
|
962
|
+
if (node.ino === ROOT_INO)
|
|
963
|
+
return undefined;
|
|
964
|
+
names.push(node.name);
|
|
965
|
+
const parent = get(node.parent);
|
|
966
|
+
if (parent?.kind !== "directory" || parent.metadata.nlink === 0 || parent.entries.get(node.name) !== node.ino) {
|
|
967
|
+
return undefined;
|
|
968
|
+
}
|
|
969
|
+
node = parent;
|
|
970
|
+
if (++steps % WALK_YIELD_INTERVAL === 0 && !(yield* SyncSection))
|
|
971
|
+
yield* Effect.yieldNow;
|
|
972
|
+
}
|
|
973
|
+
return "2f" + names.reverse().join("2f");
|
|
974
|
+
});
|
|
975
|
+
return { roots, root: roots.at(-1) ?? ROOT_INO, alive, contains, check, path };
|
|
976
|
+
};
|
|
977
|
+
})(Confinement || (Confinement = {}));
|
|
978
|
+
/** @internal */
|
|
979
|
+
export var Resolution;
|
|
980
|
+
(function (Resolution) {
|
|
981
|
+
const encoder = new TextEncoder();
|
|
982
|
+
const EMPTY_AUTHORITIES = [];
|
|
983
|
+
/** @internal */
|
|
984
|
+
Resolution.inGroup = (identity, gid) => identity.gid === gid || identity.groups.includes(gid);
|
|
985
|
+
/** @internal */
|
|
986
|
+
Resolution.permitted = (node, identity) => {
|
|
987
|
+
const shift = identity.uid === node.metadata.uid ? 6 : Resolution.inGroup(identity, node.metadata.gid) ? 3 : 0;
|
|
988
|
+
return (node.metadata.mode >> shift) & 0o7;
|
|
989
|
+
};
|
|
990
|
+
/** @internal */
|
|
991
|
+
Resolution.authorize = (node, identity, bits, op) => identity.privileged || (Resolution.permitted(node, identity) & bits) === bits
|
|
992
|
+
? Effect.void
|
|
993
|
+
: Effect.fail(op.fail("AccessDenied"));
|
|
994
|
+
/** @internal */
|
|
995
|
+
Resolution.entryName = (input, op) => {
|
|
996
|
+
if (Predicate.isString(input) && !isWellFormed(input))
|
|
997
|
+
return Result.fail(op.fail("InvalidPathEncoding"));
|
|
998
|
+
const bytes = Predicate.isString(input) ? encoder.encode(input) : input;
|
|
999
|
+
if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
|
|
1000
|
+
bytes.includes(SLASH_BYTE))
|
|
1001
|
+
return Result.fail(op.fail("InvalidArgument"));
|
|
1002
|
+
return Result.succeed(Hex.encode(new Uint8Array(bytes)));
|
|
1003
|
+
};
|
|
1004
|
+
/** @internal */
|
|
1005
|
+
Resolution.make = ({ caller, get, identity, maxPathBytes, registry, confinement, additional, authorityView }) => {
|
|
1006
|
+
const visibleRoot = confinement?.root ?? ROOT_INO;
|
|
1007
|
+
const prepare = (input, op) => preparePath(input, op.operation, maxPathBytes);
|
|
1008
|
+
const prepareEntry = (input, op) => isEntry(input)
|
|
1009
|
+
? Result.map(Resolution.entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
|
|
1010
|
+
: isTarget(input)
|
|
1011
|
+
? Result.map(prepare(input.path, op), (path) => ({ kind: "path", path, base: input.relativeTo }))
|
|
1012
|
+
: Result.map(prepare(input, op), (path) => ({ kind: "path", path, base: undefined }));
|
|
1013
|
+
const nodeAt = Effect.fnUntraced(function* (lookup, ino) {
|
|
1014
|
+
const node = lookup(ino);
|
|
1015
|
+
return node === undefined ? yield* Effect.die("Inode left the table during resolution") : node;
|
|
1016
|
+
});
|
|
1017
|
+
const directoryAt = Effect.fnUntraced(function* (lookup, ino) {
|
|
1018
|
+
const node = yield* nodeAt(lookup, ino);
|
|
1019
|
+
return node.kind !== "directory" ? yield* Effect.die("Directory left the table during resolution") : node;
|
|
1020
|
+
});
|
|
1021
|
+
const callerDirectory = Effect.fnUntraced(function* (lookup, op) {
|
|
1022
|
+
const ino = TokenRegistry.inode(caller);
|
|
1023
|
+
const node = ino === undefined ? undefined : lookup(ino);
|
|
1024
|
+
return node?.kind !== "directory" ? yield* op.fail("ClosedCaller") : node;
|
|
1025
|
+
});
|
|
1026
|
+
const walk = Effect.fnUntraced(function* (path, base, op, mode, referencedBase) {
|
|
1027
|
+
const pathOp = op.at(path.input);
|
|
1028
|
+
const baseAuthorities = base === undefined
|
|
1029
|
+
? EMPTY_AUTHORITIES
|
|
1030
|
+
: registry.handleBoundaries(base).map((roots) => Confinement.make(authorityView, roots));
|
|
1031
|
+
const authorizeDirectory = baseAuthorities.length === 0 ? Resolution.authorize : Effect.fnUntraced(function* (node, callerIdentity, bits, context) {
|
|
1032
|
+
for (const authority of baseAuthorities)
|
|
1033
|
+
yield* authority.check(node, context, true);
|
|
1034
|
+
yield* Resolution.authorize(node, callerIdentity, bits, context);
|
|
1035
|
+
});
|
|
1036
|
+
const parentOnly = mode.kind === "Parent";
|
|
1037
|
+
const allowMissing = mode.kind === "OrCreate" && mode.create !== "never";
|
|
1038
|
+
const followFinalSymlink = mode.kind === "Node"
|
|
1039
|
+
? mode.followFinalSymlink !== false
|
|
1040
|
+
: mode.kind === "Parent" || mode.finalSymlink === "follow";
|
|
1041
|
+
const createMissing = mode.kind === "Node" ? mode.createMissing : undefined;
|
|
1042
|
+
if (!TokenRegistry.isOpen(caller)) {
|
|
1043
|
+
return yield* pathOp.fail("ClosedCaller");
|
|
1044
|
+
}
|
|
1045
|
+
if (additional !== undefined && additional.length > 0) {
|
|
1046
|
+
for (const authority of additional)
|
|
1047
|
+
yield* authority.alive(pathOp);
|
|
1048
|
+
}
|
|
1049
|
+
if (confinement !== undefined)
|
|
1050
|
+
yield* confinement.alive(pathOp);
|
|
1051
|
+
if (path.absolute && base !== undefined &&
|
|
1052
|
+
(confinement !== undefined || (additional?.length ?? 0) > 0 || baseAuthorities.length > 0)) {
|
|
1053
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(base, "directory"), "handle", pathOp);
|
|
1054
|
+
yield* registry.authorizeHandle(base, node, pathOp);
|
|
1055
|
+
if (confinement !== undefined)
|
|
1056
|
+
yield* confinement.check(node, pathOp);
|
|
1057
|
+
if (additional !== undefined && additional.length > 0) {
|
|
1058
|
+
for (const authority of additional)
|
|
1059
|
+
yield* authority.check(node, pathOp);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
// Reads remain live so recursive creation can revisit nodes added earlier in this walk.
|
|
1063
|
+
const lookup = yield* authorityView;
|
|
1064
|
+
let current = path.absolute ? (yield* nodeAt(lookup, visibleRoot)) : yield* callerDirectory(lookup, pathOp);
|
|
1065
|
+
if (!path.absolute && referencedBase !== undefined) {
|
|
1066
|
+
current = referencedBase;
|
|
1067
|
+
yield* Resolution.authorize(current, identity, 0o1, op);
|
|
1068
|
+
}
|
|
1069
|
+
else if (!path.absolute && base !== undefined) {
|
|
1070
|
+
const directory = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(base, "directory"), "handle", pathOp);
|
|
1071
|
+
if (directory.kind !== "directory")
|
|
1072
|
+
return yield* pathOp.fail("InvalidHandle");
|
|
1073
|
+
yield* registry.authorizeHandle(base, directory, pathOp);
|
|
1074
|
+
current = directory;
|
|
1075
|
+
yield* Resolution.authorize(current, identity, 0o1, pathOp);
|
|
1076
|
+
}
|
|
1077
|
+
if (confinement !== undefined)
|
|
1078
|
+
yield* confinement.check(current, pathOp);
|
|
1079
|
+
if (additional !== undefined && additional.length > 0) {
|
|
1080
|
+
for (const authority of additional)
|
|
1081
|
+
yield* authority.check(current, pathOp);
|
|
1082
|
+
}
|
|
1083
|
+
if (current.metadata.nlink === 0) {
|
|
1084
|
+
return yield* pathOp.fail("NotFound");
|
|
1085
|
+
}
|
|
1086
|
+
let work = path;
|
|
1087
|
+
let parent;
|
|
1088
|
+
let name;
|
|
1089
|
+
let traversals = 0;
|
|
1090
|
+
// Leading components that came from a symbolic link's target. Only the components the caller wrote are
|
|
1091
|
+
// created, so a dangling link stays missing, as it does for mkdir -p.
|
|
1092
|
+
let linked = 0;
|
|
1093
|
+
for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
|
|
1094
|
+
if (current.kind !== "directory") {
|
|
1095
|
+
return yield* pathOp.fail("NotDirectory");
|
|
1096
|
+
}
|
|
1097
|
+
yield* authorizeDirectory(current, identity, 0o1, pathOp);
|
|
1098
|
+
const component = work.components[index];
|
|
1099
|
+
if (component === undefined)
|
|
1100
|
+
break;
|
|
1101
|
+
if (component === DOT_HEX)
|
|
1102
|
+
continue;
|
|
1103
|
+
if (component === DOT_DOT_HEX) {
|
|
1104
|
+
current = yield* directoryAt(lookup, confinement !== undefined && current.ino === visibleRoot ? current.ino : current.parent);
|
|
1105
|
+
parent = undefined;
|
|
1106
|
+
name = undefined;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
parent = current;
|
|
1110
|
+
name = component;
|
|
1111
|
+
const childIno = current.entries.get(component);
|
|
1112
|
+
const child = childIno === undefined ? undefined : lookup(childIno);
|
|
1113
|
+
if (child === undefined) {
|
|
1114
|
+
if (allowMissing && index === work.components.length - 1) {
|
|
1115
|
+
return { kind: "missing", parent, name, op: pathOp };
|
|
1116
|
+
}
|
|
1117
|
+
if (createMissing !== undefined && index >= linked) {
|
|
1118
|
+
current = yield* createMissing(current, component, index === work.components.length - 1);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
return yield* pathOp.fail("NotFound");
|
|
1122
|
+
}
|
|
1123
|
+
if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
|
|
1124
|
+
if (child.target.length === 0) {
|
|
1125
|
+
return yield* pathOp.fail("NotFound");
|
|
1126
|
+
}
|
|
1127
|
+
if (++traversals > MAX_SYMLINK_TRAVERSALS) {
|
|
1128
|
+
return yield* pathOp.fail("SymlinkLoop");
|
|
1129
|
+
}
|
|
1130
|
+
const suffix = work.suffixes[index] ?? new Uint8Array(0);
|
|
1131
|
+
const remaining = work.components.length - index - 1;
|
|
1132
|
+
if (maxPathBytes !== undefined &&
|
|
1133
|
+
ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), maxPathBytes)) {
|
|
1134
|
+
return yield* pathOp.fail("PathTooLong");
|
|
1135
|
+
}
|
|
1136
|
+
const expansion = new Uint8Array(child.target.length + suffix.length);
|
|
1137
|
+
expansion.set(child.target);
|
|
1138
|
+
expansion.set(suffix, child.target.length);
|
|
1139
|
+
const expanded = preparePath(ownedPath(expansion), op.operation, maxPathBytes);
|
|
1140
|
+
// The expansion is synthetic: its per-component limits are the caller's to hear about,
|
|
1141
|
+
// but the path in the error has to be the one the caller passed in, so the
|
|
1142
|
+
// expansion's own failure is not kept as the cause.
|
|
1143
|
+
if (Result.isFailure(expanded)) {
|
|
1144
|
+
return yield* pathOp.fail(expanded.failure.code);
|
|
1145
|
+
}
|
|
1146
|
+
work = expanded.success;
|
|
1147
|
+
linked = work.components.length - remaining + Math.max(0, linked - index - 1);
|
|
1148
|
+
if (work.absolute)
|
|
1149
|
+
current = yield* nodeAt(lookup, visibleRoot);
|
|
1150
|
+
index = -1;
|
|
1151
|
+
}
|
|
1152
|
+
else
|
|
1153
|
+
current = child;
|
|
1154
|
+
}
|
|
1155
|
+
if (baseAuthorities.length > 0) {
|
|
1156
|
+
for (const authority of baseAuthorities)
|
|
1157
|
+
yield* authority.check(current, pathOp, true);
|
|
1158
|
+
}
|
|
1159
|
+
if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
|
|
1160
|
+
return yield* pathOp.fail("NotDirectory");
|
|
1161
|
+
}
|
|
1162
|
+
return parent === undefined || name === undefined
|
|
1163
|
+
? { kind: "node", node: current, op: pathOp }
|
|
1164
|
+
: { kind: "entry", node: current, parent, name, op: pathOp };
|
|
1165
|
+
});
|
|
1166
|
+
const resolveNode = Effect.fnUntraced(function* (target, mode, op) {
|
|
1167
|
+
if (Predicate.isTagged(target, "Path")) {
|
|
1168
|
+
const path = yield* Effect.fromResult(prepare(target.path, op));
|
|
1169
|
+
const result = yield* walk(path, target.relativeTo, op, {
|
|
1170
|
+
...mode,
|
|
1171
|
+
followFinalSymlink: mode.final === true
|
|
1172
|
+
? mode.followFinalSymlink ?? true
|
|
1173
|
+
: target.followFinalSymlink ?? mode.followFinalSymlink ?? true
|
|
1174
|
+
});
|
|
1175
|
+
return result.kind === "missing" ? yield* result.op.fail("NotFound") : result;
|
|
1176
|
+
}
|
|
1177
|
+
if (!TokenRegistry.isOpen(caller))
|
|
1178
|
+
return yield* op.fail("ClosedCaller");
|
|
1179
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* (Predicate.isTagged(target, "Reference")
|
|
1180
|
+
? registry.resolve(target.reference, "reference")
|
|
1181
|
+
: registry.resolve(target.handle, "handle")), Predicate.isTagged(target, "Reference") ? "reference" : "handle", op);
|
|
1182
|
+
if (!Predicate.isTagged(target, "Reference"))
|
|
1183
|
+
yield* registry.authorizeHandle(target.handle, node, op);
|
|
1184
|
+
if (confinement !== undefined)
|
|
1185
|
+
yield* confinement.check(node, op);
|
|
1186
|
+
if (additional !== undefined && additional.length > 0) {
|
|
1187
|
+
for (const authority of additional)
|
|
1188
|
+
yield* authority.check(node, op);
|
|
1189
|
+
}
|
|
1190
|
+
return { kind: "node", node, op };
|
|
1191
|
+
});
|
|
1192
|
+
const directory = Effect.fnUntraced(function* (target, op) {
|
|
1193
|
+
const resolved = yield* resolveNode(target, { kind: "Node" }, op);
|
|
1194
|
+
if (resolved.node.kind !== "directory")
|
|
1195
|
+
return yield* resolved.op.fail("NotDirectory");
|
|
1196
|
+
return { node: resolved.node, op: resolved.op };
|
|
1197
|
+
});
|
|
1198
|
+
const entryDirectory = Effect.fnUntraced(function* (target, op) {
|
|
1199
|
+
const resolved = yield* directory(target, op);
|
|
1200
|
+
if (resolved.node.metadata.nlink === 0)
|
|
1201
|
+
return yield* resolved.op.fail("NotFound");
|
|
1202
|
+
return resolved;
|
|
1203
|
+
});
|
|
1204
|
+
const parent = Effect.fnUntraced(function* (prepared, op) {
|
|
1205
|
+
if (prepared.kind === "entry") {
|
|
1206
|
+
const resolved = yield* entryDirectory(prepared.directory, op);
|
|
1207
|
+
return {
|
|
1208
|
+
kind: "parent",
|
|
1209
|
+
parent: resolved.node.ino,
|
|
1210
|
+
name: prepared.name,
|
|
1211
|
+
trailingSlash: false,
|
|
1212
|
+
addressing: "entry",
|
|
1213
|
+
op
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
const resolved = yield* walk(prepared.path, prepared.base, op, { kind: "Parent" });
|
|
1217
|
+
if (resolved.kind === "missing")
|
|
1218
|
+
return yield* resolved.op.fail("NotFound");
|
|
1219
|
+
if (resolved.node.kind !== "directory")
|
|
1220
|
+
return yield* resolved.op.fail("NotDirectory");
|
|
1221
|
+
return {
|
|
1222
|
+
kind: "parent",
|
|
1223
|
+
parent: resolved.node.ino,
|
|
1224
|
+
name: prepared.path.components.at(-1),
|
|
1225
|
+
trailingSlash: prepared.path.trailingSlash,
|
|
1226
|
+
addressing: "path",
|
|
1227
|
+
op: resolved.op
|
|
1228
|
+
};
|
|
1229
|
+
});
|
|
1230
|
+
const creationResult = (result, trailingSlash, addressing, op, origin) => {
|
|
1231
|
+
if (result.kind === "node")
|
|
1232
|
+
return Effect.fail(op.fail("IsDirectory"));
|
|
1233
|
+
const entry = {
|
|
1234
|
+
parent: result.parent,
|
|
1235
|
+
name: result.name,
|
|
1236
|
+
trailingSlash,
|
|
1237
|
+
addressing,
|
|
1238
|
+
op,
|
|
1239
|
+
origin: origin ?? { parent: result.parent, name: result.name }
|
|
1240
|
+
};
|
|
1241
|
+
return Effect.succeed(result.kind === "missing" ? { ...entry, kind: "missing" } : { ...entry, kind: "existing", node: result.node });
|
|
1242
|
+
};
|
|
1243
|
+
const orCreate = Effect.fnUntraced(function* (prepared, mode, op) {
|
|
1244
|
+
if (prepared.kind === "path") {
|
|
1245
|
+
const pathOp = op.at(prepared.path.input);
|
|
1246
|
+
if (mode.create === "exclusive") {
|
|
1247
|
+
const exists = yield* Effect.result(walk(prepared.path, prepared.base, op, { kind: "Node", followFinalSymlink: false }));
|
|
1248
|
+
if (Result.isSuccess(exists))
|
|
1249
|
+
return yield* pathOp.fail("AlreadyExists");
|
|
1250
|
+
if (exists.failure.code !== "NotFound")
|
|
1251
|
+
return yield* exists.failure;
|
|
1252
|
+
}
|
|
1253
|
+
return yield* creationResult(yield* walk(prepared.path, prepared.base, op, mode), prepared.path.trailingSlash, "path", pathOp);
|
|
1254
|
+
}
|
|
1255
|
+
const { node: parent } = yield* entryDirectory(prepared.directory, op);
|
|
1256
|
+
if (isDotComponent(prepared.name))
|
|
1257
|
+
return yield* op.fail("InvalidArgument");
|
|
1258
|
+
yield* Resolution.authorize(parent, identity, 0o1, op);
|
|
1259
|
+
const directIno = parent.entries.get(prepared.name);
|
|
1260
|
+
const direct = directIno === undefined ? undefined : yield* get(directIno);
|
|
1261
|
+
if (mode.beforeFollow !== undefined)
|
|
1262
|
+
yield* mode.beforeFollow(parent, direct);
|
|
1263
|
+
if (mode.create === "exclusive" && mode.entryExclusive !== "afterSymlink" && direct !== undefined) {
|
|
1264
|
+
return yield* op.fail("AlreadyExists");
|
|
1265
|
+
}
|
|
1266
|
+
let result = direct === undefined
|
|
1267
|
+
? { kind: "missing", parent, name: prepared.name, op }
|
|
1268
|
+
: { kind: "entry", node: direct, parent, name: prepared.name, op };
|
|
1269
|
+
if (direct?.kind === "symlink" && mode.finalSymlink === "follow") {
|
|
1270
|
+
const path = yield* Effect.fromResult(preparePath(ownedPath(nameBytes(prepared.name)), op.operation, undefined));
|
|
1271
|
+
// Following an entry link must retain the directory capability's boundaries.
|
|
1272
|
+
const base = Predicate.isTagged(prepared.directory, "Handle")
|
|
1273
|
+
? prepared.directory.handle
|
|
1274
|
+
: Predicate.isTagged(prepared.directory, "Path")
|
|
1275
|
+
? prepared.directory.relativeTo
|
|
1276
|
+
: undefined;
|
|
1277
|
+
result = yield* walk(path, base, op, mode, parent);
|
|
1278
|
+
// Entry writeFile rejects a followed directory before checking exclusive creation.
|
|
1279
|
+
if (mode.create === "exclusive" && mode.entryExclusive === "afterSymlink" &&
|
|
1280
|
+
(result.kind === "node" || (result.kind === "entry" && result.node.kind === "directory"))) {
|
|
1281
|
+
return yield* op.fail("IsDirectory");
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (result.kind === "missing" && mode.create === "never")
|
|
1285
|
+
return yield* op.fail("NotFound");
|
|
1286
|
+
if (mode.create === "exclusive" && mode.entryExclusive === "afterSymlink" && result.kind !== "missing") {
|
|
1287
|
+
return yield* op.fail("AlreadyExists");
|
|
1288
|
+
}
|
|
1289
|
+
return yield* creationResult(result, false, "entry", op, { parent, name: prepared.name });
|
|
1290
|
+
});
|
|
1291
|
+
function resolve(input, mode, op) {
|
|
1292
|
+
if (isTarget(input)) {
|
|
1293
|
+
return mode.kind === "Node"
|
|
1294
|
+
? resolveNode(input, mode, op)
|
|
1295
|
+
: Effect.die("A node target requires Node resolution");
|
|
1296
|
+
}
|
|
1297
|
+
return mode.kind === "Parent"
|
|
1298
|
+
? parent(input, op)
|
|
1299
|
+
: mode.kind === "OrCreate"
|
|
1300
|
+
? orCreate(input, mode, op)
|
|
1301
|
+
: Effect.die("An entry requires Parent or OrCreate resolution");
|
|
1302
|
+
}
|
|
1303
|
+
return { prepare, prepareEntry, resolve, directory, entryDirectory };
|
|
1304
|
+
};
|
|
1305
|
+
})(Resolution || (Resolution = {}));
|
|
1306
|
+
/** @internal */
|
|
1307
|
+
export const BasicLimits = Schema.Struct({
|
|
1308
|
+
maxEntries: Schema.optional(Schema.Natural),
|
|
1309
|
+
maxBytes: Schema.optional(Schema.ByteSize),
|
|
1310
|
+
maxFileBytes: Schema.optional(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.toBigInt(size) <= BigInt(MAX_FILE_BYTES) ? undefined : "file limit is too large"))),
|
|
1311
|
+
maxPathBytes: Schema.optional(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.toBigInt(size) >= 1n ? undefined : "path limit must be positive"))),
|
|
1312
|
+
maxPendingOperations: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)))
|
|
1313
|
+
});
|
|
1314
|
+
const readLimits = (options) => Result.map(decodeConfiguration(BasicLimits, options ?? {}, "make"), (value) => Object.freeze({
|
|
1315
|
+
...value,
|
|
1316
|
+
maxEntries: value.maxEntries,
|
|
1317
|
+
maxBytes: value.maxBytes,
|
|
1318
|
+
maxPathBytes: value.maxPathBytes,
|
|
1319
|
+
maxFileBytes: value.maxFileBytes ?? ByteSize.bytes(MAX_FILE_BYTES),
|
|
1320
|
+
maxPendingOperations: value.maxPendingOperations ?? 64
|
|
1321
|
+
}));
|
|
1322
|
+
const EXECUTE = 0o1;
|
|
1323
|
+
const WRITE = 0o2;
|
|
1324
|
+
const READ = 0o4;
|
|
1325
|
+
const ANY_EXECUTE = 0o111;
|
|
1326
|
+
const SET_ID_BITS = 0o6000;
|
|
1327
|
+
const STICKY_BIT = 0o1000;
|
|
1328
|
+
const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
|
|
1329
|
+
/** @internal */
|
|
1330
|
+
export const memoryCommitProvider = Object.freeze({
|
|
1331
|
+
mode: "memory",
|
|
1332
|
+
commit: () => Effect.succeed("committed"),
|
|
1333
|
+
shutdown: Effect.void
|
|
1334
|
+
});
|
|
1335
|
+
// An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
|
|
1336
|
+
const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
|
|
1337
|
+
if (Exit.isSuccess(committed) && Predicate.isTagged("Rejected")(committed.value)) {
|
|
1338
|
+
return { available: !cleanup, failure: undefined, cause: committed.value.cause };
|
|
1339
|
+
}
|
|
1340
|
+
// TODO(#186): Preserve the cause of a provider defect or interruption.
|
|
1341
|
+
if (Exit.isFailure(committed)) {
|
|
1342
|
+
if (Cause.hasDies(committed.cause) || Cause.hasInterrupts(committed.cause)) {
|
|
1343
|
+
return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
1344
|
+
}
|
|
1345
|
+
const failure = Cause.findErrorOption(committed.cause);
|
|
1346
|
+
return Option.isSome(failure)
|
|
1347
|
+
? { available: !cleanup, failure: failure.value }
|
|
1348
|
+
: { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
1349
|
+
}
|
|
1350
|
+
if (committed.value === "rejected") {
|
|
1351
|
+
return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
|
|
1352
|
+
}
|
|
1353
|
+
if (committed.value === "unknown") {
|
|
1354
|
+
return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
1355
|
+
}
|
|
1356
|
+
return { available: true, failure: undefined };
|
|
1357
|
+
});
|
|
1358
|
+
const WALK_CHUNK_ENTRIES = 128;
|
|
1359
|
+
const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
|
|
1360
|
+
const isMode = Schema.is(Mode);
|
|
1361
|
+
const isNatural = Schema.is(Schema.Natural);
|
|
1362
|
+
const isLength = Schema.is(SetattrOptions.fields.size.schema);
|
|
1363
|
+
const inGroup = Resolution.inGroup;
|
|
1364
|
+
const isTimestamp = Schema.is(Timestamp);
|
|
1365
|
+
const isSeekMode = Schema.is(SeekMode);
|
|
1366
|
+
const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
|
|
1367
|
+
const decodeTimes = Schema.decodeEffect(Times, { onExcessProperty: "error" });
|
|
1368
|
+
const decodeExpected = Schema.decodeEffect(SetattrOptions.fields.expected.schema, { onExcessProperty: "error" });
|
|
1369
|
+
const SETATTR_FIELDS = Object.keys(SetattrOptions.fields);
|
|
1370
|
+
const timeAt = (update, value, now) => update === undefined ? value : Match.value(update).pipe(Match.discriminator("kind")("omit", () => value), Match.discriminator("kind")("now", () => now), Match.discriminator("kind")("value", ({ nanoseconds }) => nanoseconds), Match.exhaustive);
|
|
1371
|
+
const decodeWriteFileOptions = Schema.decodeEffect(WriteFileOptions, { onExcessProperty: "error" });
|
|
1372
|
+
const decodeOpenOptions = Schema.decodeEffect(OpenOptions, { onExcessProperty: "error" });
|
|
1373
|
+
const decodeMkdirOptions = Schema.decodeEffect(MkdirOptions, { onExcessProperty: "error" });
|
|
1374
|
+
const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessProperty: "error" });
|
|
1375
|
+
const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
|
|
1376
|
+
const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
|
|
1377
|
+
const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
|
|
1378
|
+
// Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
|
|
1379
|
+
// status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
|
|
1380
|
+
// skips it, so reads at one instant (a frozen or coarse clock) store nothing.
|
|
1381
|
+
const accessDue = (metadata, now) => now !== metadata.atimeNs && (metadata.atimeNs <= metadata.mtimeNs || metadata.atimeNs <= metadata.ctimeNs ||
|
|
1382
|
+
now - metadata.atimeNs >= RELATIME_INTERVAL_NS);
|
|
1383
|
+
const refreshDue = (read) => read.access !== undefined && accessDue(read.access.node.metadata, read.access.now);
|
|
1384
|
+
class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSystem/Draft", {
|
|
1385
|
+
make: Effect.fnUntraced(function* (base) {
|
|
1386
|
+
const revision = base.revision + 1n;
|
|
1387
|
+
const cell = MutableRef.make({
|
|
1388
|
+
pending: new Map(),
|
|
1389
|
+
opens: undefined,
|
|
1390
|
+
nextInode: base.nextInode,
|
|
1391
|
+
changed: false,
|
|
1392
|
+
finished: undefined,
|
|
1393
|
+
entries: base.entries,
|
|
1394
|
+
usedBytes: base.usedBytes,
|
|
1395
|
+
renames: [],
|
|
1396
|
+
events: [],
|
|
1397
|
+
after: [],
|
|
1398
|
+
removed: []
|
|
1399
|
+
});
|
|
1400
|
+
const lookup = (ino) => {
|
|
1401
|
+
const draft = MutableRef.get(cell);
|
|
1402
|
+
return draft.pending.has(ino) ? draft.pending.get(ino) : getNode(base, ino);
|
|
1403
|
+
};
|
|
1404
|
+
const get = (ino) => Effect.sync(() => lookup(ino));
|
|
1405
|
+
const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
|
|
1406
|
+
const put = (node, changed) => Effect.sync(() => {
|
|
1407
|
+
MutableRef.update(cell, (draft) => {
|
|
1408
|
+
// This map belongs only to this transition. Copying it on every inode write makes recursive changes quadratic.
|
|
1409
|
+
draft.pending.set(node.ino, changed ? { ...node, revision } : node);
|
|
1410
|
+
return { ...draft, changed: draft.changed || changed };
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
return {
|
|
1414
|
+
revision,
|
|
1415
|
+
lookup,
|
|
1416
|
+
get,
|
|
1417
|
+
put: (node) => put(node, true),
|
|
1418
|
+
putQuiet: (node) => put(node, false),
|
|
1419
|
+
unchanged: Effect.sync(() => {
|
|
1420
|
+
const draft = MutableRef.get(cell);
|
|
1421
|
+
return draft.pending.size === 0 && draft.opens === undefined && draft.nextInode === base.nextInode &&
|
|
1422
|
+
draft.entries === base.entries && draft.usedBytes === base.usedBytes;
|
|
1423
|
+
}),
|
|
1424
|
+
remove: (ino) => Effect.sync(() => {
|
|
1425
|
+
const draft = MutableRef.get(cell);
|
|
1426
|
+
draft.pending.set(ino, undefined);
|
|
1427
|
+
draft.removed.push(ino);
|
|
1428
|
+
}),
|
|
1429
|
+
allocate: Effect.sync(() => {
|
|
1430
|
+
const next = MutableRef.get(cell).nextInode;
|
|
1431
|
+
MutableRef.update(cell, (draft) => ({ ...draft, nextInode: Ino(next + 1) }));
|
|
1432
|
+
return next;
|
|
1433
|
+
}),
|
|
1434
|
+
canAllocate: Effect.sync(() => MutableRef.get(cell).nextInode < Number.MAX_SAFE_INTEGER),
|
|
1435
|
+
openCount,
|
|
1436
|
+
retain: (ino) => Effect.sync(() => {
|
|
1437
|
+
const count = MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0;
|
|
1438
|
+
MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, count + 1) }));
|
|
1439
|
+
}),
|
|
1440
|
+
release: (ino) => Effect.sync(() => {
|
|
1441
|
+
const count = (MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0) - 1;
|
|
1442
|
+
MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, Math.max(0, count)) }));
|
|
1443
|
+
return count;
|
|
1444
|
+
}),
|
|
1445
|
+
entries: Effect.sync(() => MutableRef.get(cell).entries),
|
|
1446
|
+
addEntries: (amount) => Effect.sync(() => {
|
|
1447
|
+
MutableRef.update(cell, (draft) => ({ ...draft, entries: draft.entries + amount }));
|
|
1448
|
+
}),
|
|
1449
|
+
usedBytes: Effect.sync(() => MutableRef.get(cell).usedBytes),
|
|
1450
|
+
addUsedBytes: (amount) => Effect.sync(() => {
|
|
1451
|
+
MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
|
|
1452
|
+
}),
|
|
1453
|
+
rename: (rename) => Effect.sync(() => {
|
|
1454
|
+
MutableRef.get(cell).renames.push(rename);
|
|
1455
|
+
}),
|
|
1456
|
+
publish: (event) => Effect.sync(() => {
|
|
1457
|
+
MutableRef.get(cell).events.push(event);
|
|
1458
|
+
}),
|
|
1459
|
+
afterInstall: (action) => Effect.sync(() => {
|
|
1460
|
+
MutableRef.get(cell).after.push(action);
|
|
1461
|
+
}),
|
|
1462
|
+
finish: Effect.sync(() => {
|
|
1463
|
+
const draft = MutableRef.get(cell);
|
|
1464
|
+
if (draft.finished !== undefined)
|
|
1465
|
+
return draft.finished;
|
|
1466
|
+
const inodes = HashMap.mutate(base.inodes, (mutable) => {
|
|
1467
|
+
for (const [ino, node] of draft.pending) {
|
|
1468
|
+
if (node === undefined)
|
|
1469
|
+
HashMap.remove(mutable, ino);
|
|
1470
|
+
else
|
|
1471
|
+
setNode(mutable, node);
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
let open = base.open;
|
|
1475
|
+
if (draft.opens !== undefined) {
|
|
1476
|
+
const next = new Map(base.open);
|
|
1477
|
+
for (const [ino, count] of draft.opens) {
|
|
1478
|
+
if (count > 0)
|
|
1479
|
+
next.set(ino, count);
|
|
1480
|
+
else
|
|
1481
|
+
next.delete(ino);
|
|
1482
|
+
}
|
|
1483
|
+
open = next;
|
|
1484
|
+
}
|
|
1485
|
+
const finished = {
|
|
1486
|
+
inodes,
|
|
1487
|
+
open,
|
|
1488
|
+
nextInode: draft.nextInode,
|
|
1489
|
+
revision: draft.changed ? revision : base.revision,
|
|
1490
|
+
entries: draft.entries,
|
|
1491
|
+
usedBytes: draft.usedBytes
|
|
1492
|
+
};
|
|
1493
|
+
MutableRef.update(cell, (current) => ({ ...current, finished }));
|
|
1494
|
+
return finished;
|
|
1495
|
+
}),
|
|
1496
|
+
actions: Effect.sync(() => {
|
|
1497
|
+
const { events, after, removed, renames } = MutableRef.get(cell);
|
|
1498
|
+
return { events, after, removed, renames };
|
|
1499
|
+
})
|
|
1500
|
+
};
|
|
1501
|
+
})
|
|
1502
|
+
}) {
|
|
1503
|
+
}
|
|
1504
|
+
const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
|
|
1505
|
+
const withEntries = (directory, entries) => ({ ...directory, entries });
|
|
1506
|
+
const withoutLink = (links, parent, name) => {
|
|
1507
|
+
const index = links.findIndex((link) => link.parent === parent && link.name === name);
|
|
1508
|
+
return index < 0 ? links : [...links.slice(0, index), ...links.slice(index + 1)];
|
|
1509
|
+
};
|
|
1510
|
+
/** @internal */
|
|
1511
|
+
export const makeEngine = Effect.fnUntraced(function* (options = {}) {
|
|
1512
|
+
const services = yield* Effect.context();
|
|
1513
|
+
const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
|
|
1514
|
+
const limits = yield* Effect.fromResult(readLimits(options.limits));
|
|
1515
|
+
const commitProvider = options.commitProvider ?? memoryCommitProvider;
|
|
1516
|
+
const execution = options.execution ?? "cooperative";
|
|
1517
|
+
if ((execution !== "cooperative" && execution !== "sync") ||
|
|
1518
|
+
(execution === "sync" && commitProvider !== memoryCommitProvider)) {
|
|
1519
|
+
return yield* argumentFailure("make", "execution");
|
|
1520
|
+
}
|
|
1521
|
+
const synchronous = execution === "sync";
|
|
1522
|
+
const clock = yield* Clock.clockWith(Effect.succeed);
|
|
1523
|
+
const initialTime = options.initialTime ?? clock.currentTimeNanosUnsafe();
|
|
1524
|
+
if (!isTimestamp(initialTime)) {
|
|
1525
|
+
return yield* argumentFailure("make", "clock.currentTimeNanos");
|
|
1526
|
+
}
|
|
1527
|
+
const timestamp = (op) => Effect.suspend(() => {
|
|
1528
|
+
const now = clock.currentTimeNanosUnsafe();
|
|
1529
|
+
return isTimestamp(now)
|
|
1530
|
+
? Effect.succeed(now)
|
|
1531
|
+
: Effect.fail(op.fail("InvalidArgument"));
|
|
1532
|
+
});
|
|
1533
|
+
const volumeIdentity = Symbol();
|
|
1534
|
+
const directoryHolds = new Map();
|
|
1535
|
+
const maxPendingOperations = limits.maxPendingOperations;
|
|
1536
|
+
const permits = maxPendingOperations + 1;
|
|
1537
|
+
// The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
|
|
1538
|
+
// overtaken by each later observation. A change holds a turnstile while it gathers the permits, and an
|
|
1539
|
+
// observation passes the turnstile before it takes one, so observations that arrive after a change wait
|
|
1540
|
+
// behind it. The turnstile goes to its waiters in arrival order, even to one that arrives while a finished
|
|
1541
|
+
// change is still releasing. Both waits stay interruptible, and every permit is taken and released by
|
|
1542
|
+
// `withPermits`.
|
|
1543
|
+
const cooperative = synchronous ? undefined : {
|
|
1544
|
+
gate: Semaphore.makeUnsafe(permits),
|
|
1545
|
+
turnstile: makeTurnstile()
|
|
1546
|
+
};
|
|
1547
|
+
let active = false;
|
|
1548
|
+
const pending = new Map();
|
|
1549
|
+
const preflight = (op) => synchronous
|
|
1550
|
+
? Effect.suspend(() => {
|
|
1551
|
+
if (active)
|
|
1552
|
+
return Effect.fail(op.fail("VolumeBusy"));
|
|
1553
|
+
return Effect.flatMap(VolumeTestSeams, (seams) => Object.values(seams).some((effect) => effect !== Effect.void)
|
|
1554
|
+
? Effect.fail(op.fail("InvalidArgument", { field: "testSeams" }))
|
|
1555
|
+
: Effect.void);
|
|
1556
|
+
})
|
|
1557
|
+
: Effect.void;
|
|
1558
|
+
const drain = Effect.gen(function* () {
|
|
1559
|
+
let failure = Cause.empty;
|
|
1560
|
+
while (pending.size > 0) {
|
|
1561
|
+
const releases = Array.from(pending.values());
|
|
1562
|
+
pending.clear();
|
|
1563
|
+
for (const release of releases) {
|
|
1564
|
+
const exit = yield* Effect.exit(release);
|
|
1565
|
+
if (Exit.isFailure(exit))
|
|
1566
|
+
failure = Cause.combine(failure, exit.cause);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (failure.reasons.length > 0)
|
|
1570
|
+
return yield* Effect.failCause(failure);
|
|
1571
|
+
});
|
|
1572
|
+
const inSyncSection = (effect) => Effect.provideService(Effect.provideService(Effect.uninterruptible(Effect.suspend(() => {
|
|
1573
|
+
if (active)
|
|
1574
|
+
return Effect.fail(fsFailure("VolumeBusy", "coordinate"));
|
|
1575
|
+
active = true;
|
|
1576
|
+
return Effect.ensuring(effect, Effect.ensuring(drain, Effect.sync(() => {
|
|
1577
|
+
active = false;
|
|
1578
|
+
})));
|
|
1579
|
+
})), Scheduler.PreventSchedulerYield, true), SyncSection, true);
|
|
1580
|
+
const observing = (effect) => cooperative === undefined
|
|
1581
|
+
? inSyncSection(effect)
|
|
1582
|
+
: Effect.andThen(cooperative.turnstile.withTurn(Effect.void), cooperative.gate.withPermits(1)(effect));
|
|
1583
|
+
const changing = (effect) => cooperative === undefined
|
|
1584
|
+
? inSyncSection(effect)
|
|
1585
|
+
: cooperative.turnstile.withTurn(cooperative.gate.withPermits(permits)(effect));
|
|
1586
|
+
const admission = yield* Semaphore.make(permits);
|
|
1587
|
+
const admit = (op, effect) => Effect.andThen(preflight(op), admission.withPermitsIfAvailable(1)(effect).pipe(Effect.flatMap(Option.match({
|
|
1588
|
+
onNone: () => op.fail("VolumeBusy"),
|
|
1589
|
+
onSome: Effect.succeed
|
|
1590
|
+
}))));
|
|
1591
|
+
const finalizeOwned = (reference, release, ordinary) => synchronous
|
|
1592
|
+
? Effect.uninterruptible(Effect.suspend(() => {
|
|
1593
|
+
if (!active)
|
|
1594
|
+
return ordinary;
|
|
1595
|
+
reference.lifecycle = { kind: "releasedPendingCommit", ino: TokenRegistry.inode(reference) };
|
|
1596
|
+
pending.set(reference, Effect.orDie(Effect.suspend(release)));
|
|
1597
|
+
return Effect.void;
|
|
1598
|
+
}))
|
|
1599
|
+
: ordinary;
|
|
1600
|
+
let state = emptyState(initialTime);
|
|
1601
|
+
const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
|
|
1602
|
+
if (options.initialState !== undefined)
|
|
1603
|
+
state = options.initialState;
|
|
1604
|
+
const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
|
|
1605
|
+
// Select the operation's namespace at execution time; never retain it across operations.
|
|
1606
|
+
const authorityView = Effect.map(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.lookup : (ino) => getNode(state, ino));
|
|
1607
|
+
const registry = TokenRegistry.make(volumeIdentity, view);
|
|
1608
|
+
const referenceFor = registry.referenceFor;
|
|
1609
|
+
const makeFileReference = registry.file;
|
|
1610
|
+
const makeDirectoryReference = registry.directory;
|
|
1611
|
+
let available = true;
|
|
1612
|
+
const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
|
|
1613
|
+
const install = Effect.fnUntraced(function* (finished) {
|
|
1614
|
+
const before = state;
|
|
1615
|
+
state = yield* finished.finish;
|
|
1616
|
+
const actions = yield* finished.actions;
|
|
1617
|
+
for (const ino of actions.removed)
|
|
1618
|
+
registry.forget(ino);
|
|
1619
|
+
for (const apply of actions.after)
|
|
1620
|
+
apply();
|
|
1621
|
+
const after = state;
|
|
1622
|
+
options.installed?.(() => actions.events.flatMap((events) => Array.from(events(after))), {
|
|
1623
|
+
before,
|
|
1624
|
+
after,
|
|
1625
|
+
renames: actions.renames
|
|
1626
|
+
});
|
|
1627
|
+
});
|
|
1628
|
+
const transition = Effect.fnUntraced(function* (change) {
|
|
1629
|
+
const current = yield* Draft.make(state);
|
|
1630
|
+
const value = yield* Effect.provideService(change, Draft, current);
|
|
1631
|
+
return [value, current];
|
|
1632
|
+
});
|
|
1633
|
+
// Cleanup releases local ownership even when storage refuses; that divergence disables the volume.
|
|
1634
|
+
const committed = Effect.fnUntraced(function* (op, finished, cleanup) {
|
|
1635
|
+
if ((yield* finished.unchanged) || (cleanup && !available))
|
|
1636
|
+
return yield* install(finished);
|
|
1637
|
+
const next = yield* finished.finish;
|
|
1638
|
+
const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
|
|
1639
|
+
if (!answer.available)
|
|
1640
|
+
available = false;
|
|
1641
|
+
if (answer.cause !== undefined) {
|
|
1642
|
+
if (cleanup)
|
|
1643
|
+
yield* install(finished);
|
|
1644
|
+
return yield* Effect.failCause(answer.cause);
|
|
1645
|
+
}
|
|
1646
|
+
if (answer.failure !== undefined) {
|
|
1647
|
+
if (cleanup)
|
|
1648
|
+
yield* install(finished);
|
|
1649
|
+
return yield* answer.failure;
|
|
1650
|
+
}
|
|
1651
|
+
yield* install(finished);
|
|
1652
|
+
});
|
|
1653
|
+
const applyCleanup = Effect.fnUntraced(function* (change) {
|
|
1654
|
+
const current = yield* Draft.make(state);
|
|
1655
|
+
yield* Effect.provideService(change(current), Draft, current);
|
|
1656
|
+
yield* committed(OpContext.make("close"), current, true);
|
|
1657
|
+
});
|
|
1658
|
+
const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
|
|
1659
|
+
// Permit waits stay interruptible. Memory transitions are masked; durable transitions remain
|
|
1660
|
+
// interruptible until the candidate is ready. Every provider commit and installation is masked.
|
|
1661
|
+
const coordinated = (op, effect, cleanup = false) => admit(op, changing(Effect.uninterruptibleMask((restore) => {
|
|
1662
|
+
const staged = transition(Effect.andThen(checkAvailable(op.operation), effect));
|
|
1663
|
+
return Effect.flatMap(commitProvider.mode === "memory" ? staged : restore(staged), ([value, finished]) => Effect.as(committed(op, finished, cleanup), value));
|
|
1664
|
+
}))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
1665
|
+
const coordinatedRead = (op, effect) => admit(op, observing(Effect.andThen(checkAvailable(op.operation), effect))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
1666
|
+
const refreshAccess = Effect.fnUntraced(function* (read) {
|
|
1667
|
+
if (refreshDue(read)) {
|
|
1668
|
+
const { node, now } = read.access;
|
|
1669
|
+
const draft = yield* Draft;
|
|
1670
|
+
yield* draft.putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
|
|
1671
|
+
}
|
|
1672
|
+
return read.value;
|
|
1673
|
+
});
|
|
1674
|
+
// A read that may refresh an access time. It observes under one permit, so reads run beside each other, and
|
|
1675
|
+
// only when the access time is due does it run again as a change. The change repeats every check and the rule,
|
|
1676
|
+
// since another read may have refreshed the time meanwhile, and its result is the one returned.
|
|
1677
|
+
const accessing = (op, read) => Effect.flatMap(coordinatedRead(op, read), (observed) => refreshDue(observed)
|
|
1678
|
+
? coordinated(op, Effect.flatMap(read, refreshAccess))
|
|
1679
|
+
: Effect.succeed(observed.value));
|
|
1680
|
+
const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
|
|
1681
|
+
const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
|
|
1682
|
+
available = false;
|
|
1683
|
+
}), commitProvider.shutdown))).pipe(Effect.orDie);
|
|
1684
|
+
const pathOfCurrent = Effect.fnUntraced(function* (ino) {
|
|
1685
|
+
const names = [];
|
|
1686
|
+
const first = yield* view(ino);
|
|
1687
|
+
if (first?.kind !== "directory" || first.metadata.nlink === 0)
|
|
1688
|
+
return undefined;
|
|
1689
|
+
let node = first;
|
|
1690
|
+
while (node.ino !== ROOT_INO) {
|
|
1691
|
+
names.push(node.name);
|
|
1692
|
+
const parent = yield* view(node.parent);
|
|
1693
|
+
if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
|
|
1694
|
+
return undefined;
|
|
1695
|
+
node = parent;
|
|
1696
|
+
}
|
|
1697
|
+
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
1698
|
+
});
|
|
1699
|
+
const publishEntry = Effect.fnUntraced(function* (_tag, parent, name, ino) {
|
|
1700
|
+
if (options.installed === undefined)
|
|
1701
|
+
return;
|
|
1702
|
+
const before = state;
|
|
1703
|
+
const draft = yield* Draft;
|
|
1704
|
+
yield* draft.publish((installed) => {
|
|
1705
|
+
const prefix = pathOf((at) => getNode(installed, at), parent) ?? pathOf((at) => getNode(before, at), parent);
|
|
1706
|
+
return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino, name }];
|
|
1707
|
+
});
|
|
1708
|
+
});
|
|
1709
|
+
const publishNode = Effect.fnUntraced(function* (ino) {
|
|
1710
|
+
if (options.installed === undefined)
|
|
1711
|
+
return;
|
|
1712
|
+
const draft = yield* Draft;
|
|
1713
|
+
const target = yield* draft.get(ino);
|
|
1714
|
+
if (target === undefined)
|
|
1715
|
+
return;
|
|
1716
|
+
if (target.kind === "directory") {
|
|
1717
|
+
yield* draft.publish((installed) => {
|
|
1718
|
+
const directory = getNode(installed, ino);
|
|
1719
|
+
const path = pathOf((at) => getNode(installed, at), ino);
|
|
1720
|
+
return path === undefined || directory?.kind !== "directory"
|
|
1721
|
+
? []
|
|
1722
|
+
: [{ change: { _tag: "Update", path: ownedPath(nameBytes(path)) }, parent: directory.parent, ino }];
|
|
1723
|
+
});
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const links = target.links;
|
|
1727
|
+
yield* draft.publish((installed) => {
|
|
1728
|
+
const events = [];
|
|
1729
|
+
for (const link of links) {
|
|
1730
|
+
const prefix = pathOf((at) => getNode(installed, at), link.parent);
|
|
1731
|
+
if (prefix !== undefined) {
|
|
1732
|
+
events.push({
|
|
1733
|
+
change: { _tag: "Update", path: entryPath(prefix, link.name) },
|
|
1734
|
+
parent: link.parent,
|
|
1735
|
+
ino
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return events;
|
|
1740
|
+
});
|
|
1741
|
+
});
|
|
1742
|
+
const holdDirectory = (ino) => {
|
|
1743
|
+
directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
|
|
1744
|
+
};
|
|
1745
|
+
const unholdDirectory = Effect.fnUntraced(function* (ino) {
|
|
1746
|
+
const count = (directoryHolds.get(ino) ?? 1) - 1;
|
|
1747
|
+
if (count > 0) {
|
|
1748
|
+
directoryHolds.set(ino, count);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
directoryHolds.delete(ino);
|
|
1752
|
+
const node = getNode(state, ino);
|
|
1753
|
+
if (node?.kind === "directory" && node.metadata.nlink === 0) {
|
|
1754
|
+
yield* applyCleanup((d) => d.remove(ino));
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
const releaseDirectory = (reference) => TokenRegistry.release(reference, unholdDirectory, (action) => Effect.sync(action));
|
|
1758
|
+
const finalizeDirectory = (reference) => {
|
|
1759
|
+
const close = coordinatedCleanup(releaseDirectory(reference));
|
|
1760
|
+
return finalizeOwned(reference, () => releaseDirectory(reference), TokenRegistry.finalize(reference, close, Effect.ignore(close)));
|
|
1761
|
+
};
|
|
1762
|
+
const closeReleasedScope = TokenRegistry.closeReleasedScope;
|
|
1763
|
+
const permitted = Resolution.permitted;
|
|
1764
|
+
const authorize = Resolution.authorize;
|
|
1765
|
+
const reclaim = Effect.fnUntraced(function* (d, ino) {
|
|
1766
|
+
const node = yield* d.get(ino);
|
|
1767
|
+
if (node === undefined)
|
|
1768
|
+
return;
|
|
1769
|
+
if (node.kind === "file") {
|
|
1770
|
+
if (node.links.length > 0 || (yield* d.openCount(ino)) > 0)
|
|
1771
|
+
return;
|
|
1772
|
+
yield* d.addUsedBytes(-BigInt(node.data.length));
|
|
1773
|
+
}
|
|
1774
|
+
else if (node.kind === "symlink") {
|
|
1775
|
+
if (node.links.length > 0)
|
|
1776
|
+
return;
|
|
1777
|
+
yield* d.addUsedBytes(-BigInt(node.target.length));
|
|
1778
|
+
}
|
|
1779
|
+
else if (node.metadata.nlink > 0 || directoryHolds.has(ino))
|
|
1780
|
+
return;
|
|
1781
|
+
yield* d.remove(ino);
|
|
1782
|
+
});
|
|
1783
|
+
const atEntryLimit = Effect.gen(function* () {
|
|
1784
|
+
const draft = yield* Draft;
|
|
1785
|
+
return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
|
|
1786
|
+
});
|
|
1787
|
+
const reserveEntry = Effect.fnUntraced(function* (op) {
|
|
1788
|
+
const draft = yield* Draft;
|
|
1789
|
+
if ((yield* atEntryLimit) || !(yield* draft.canAllocate))
|
|
1790
|
+
return yield* op.fail("NoSpace");
|
|
1791
|
+
});
|
|
1792
|
+
const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
|
|
1793
|
+
const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
|
|
1794
|
+
if (limits.maxBytes === undefined)
|
|
1795
|
+
return;
|
|
1796
|
+
const draft = yield* Draft;
|
|
1797
|
+
if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
|
|
1798
|
+
return yield* op.fail("NoSpace");
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
const attach = Effect.fnUntraced(function* (parent, name, child, now) {
|
|
1802
|
+
const d = yield* Draft;
|
|
1803
|
+
yield* d.put(withEntries({
|
|
1804
|
+
...parent,
|
|
1805
|
+
metadata: {
|
|
1806
|
+
...parent.metadata,
|
|
1807
|
+
nlink: parent.metadata.nlink + (child.kind === "directory" ? 1 : 0),
|
|
1808
|
+
mtimeNs: now,
|
|
1809
|
+
ctimeNs: now
|
|
1810
|
+
}
|
|
1811
|
+
}, DirectoryEntries.set(parent.entries, name, child.ino)));
|
|
1812
|
+
if (child.kind === "directory")
|
|
1813
|
+
yield* d.put({ ...child, parent: parent.ino, name });
|
|
1814
|
+
else {
|
|
1815
|
+
yield* d.put({
|
|
1816
|
+
...child,
|
|
1817
|
+
links: [...child.links, { parent: parent.ino, name }],
|
|
1818
|
+
metadata: { ...child.metadata, nlink: child.links.length + 1, ctimeNs: now }
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
const detach = Effect.fnUntraced(function* (child, parent, name, now) {
|
|
1823
|
+
const d = yield* Draft;
|
|
1824
|
+
if (child.kind === "directory") {
|
|
1825
|
+
yield* d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
|
|
1826
|
+
}
|
|
1827
|
+
else {
|
|
1828
|
+
const links = withoutLink(child.links, parent, name);
|
|
1829
|
+
yield* d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
|
|
1830
|
+
}
|
|
1831
|
+
yield* reclaim(d, child.ino);
|
|
1832
|
+
});
|
|
1833
|
+
const releaseFileInode = Effect.fnUntraced(function* (ino) {
|
|
1834
|
+
const d = yield* Draft;
|
|
1835
|
+
yield* d.release(ino);
|
|
1836
|
+
yield* reclaim(d, ino);
|
|
1837
|
+
});
|
|
1838
|
+
const releaseFile = (ref) => TokenRegistry.release(ref, releaseFileInode, (action) => Effect.sync(action));
|
|
1839
|
+
const releaseOpenFile = (ref) => Effect.ignore(coordinatedCleanup(Effect.suspend(() => TokenRegistry.inode(ref) === undefined ? Effect.void : applyCleanup(() => releaseFile(ref)))));
|
|
1840
|
+
// Admission refusal leaves the handle open. Every admitted close shares the registry release,
|
|
1841
|
+
// and a storage failure still completes cleanup while only explicit close reports the failure.
|
|
1842
|
+
const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, TokenRegistry.release(ref, releaseFileInode, (action) => Effect.flatMap(Draft, (draft) => draft.afterInstall(action)))), true).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
|
|
1843
|
+
const finalizeFile = (ref) => finalizeOwned(ref, () => applyCleanup(() => releaseFile(ref)), TokenRegistry.finalize(ref, closeFile(ref, OpContext.make("close"), Effect.void), releaseOpenFile(ref)));
|
|
1844
|
+
const acquireHandle = TokenRegistry.acquire;
|
|
1845
|
+
const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => TokenRegistry.publish(ref, ino)));
|
|
1846
|
+
const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
|
|
1847
|
+
const d = yield* Draft;
|
|
1848
|
+
yield* d.addUsedBytes(BigInt(data.length - file.data.length));
|
|
1849
|
+
yield* d.put({
|
|
1850
|
+
...file,
|
|
1851
|
+
data,
|
|
1852
|
+
metadata: {
|
|
1853
|
+
...file.metadata,
|
|
1854
|
+
size: BigInt(data.length),
|
|
1855
|
+
mode: file.metadata.mode & ~SET_ID_BITS,
|
|
1856
|
+
mtimeNs: now,
|
|
1857
|
+
ctimeNs: now
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
if (publish)
|
|
1861
|
+
yield* publishNode(file.ino);
|
|
1862
|
+
});
|
|
1863
|
+
const resizeAt = Effect.fnUntraced(function* (file, length, now, op) {
|
|
1864
|
+
if (length > BigInt(maxFileBytes))
|
|
1865
|
+
return yield* op.fail("FileTooLarge");
|
|
1866
|
+
const size = Number(length);
|
|
1867
|
+
yield* reserveBytes(op, BigInt(size - file.data.length));
|
|
1868
|
+
const data = new Uint8Array(size);
|
|
1869
|
+
data.set(file.data.subarray(0, size));
|
|
1870
|
+
yield* replaceContent(file, data, now, false);
|
|
1871
|
+
});
|
|
1872
|
+
const resize = Effect.fnUntraced(function* (file, length, op) {
|
|
1873
|
+
if (!isLength(length))
|
|
1874
|
+
return yield* op.fail("InvalidArgument");
|
|
1875
|
+
yield* resizeAt(file, length, yield* timestamp(op), op);
|
|
1876
|
+
yield* publishNode(file.ino);
|
|
1877
|
+
});
|
|
1878
|
+
const fileHandle = (ref) => {
|
|
1879
|
+
const get = Effect.fnUntraced(function* (op, access, cleanup = false) {
|
|
1880
|
+
const ino = TokenRegistry.inode(ref);
|
|
1881
|
+
const node = ino === undefined ? undefined : yield* view(ino);
|
|
1882
|
+
if ((!cleanup && !TokenRegistry.isOpen(ref)) || node?.kind !== "file" ||
|
|
1883
|
+
(access === "read" && ref.access === "write") ||
|
|
1884
|
+
(access === "write" && ref.access === "read"))
|
|
1885
|
+
return yield* op.fail("InvalidHandle");
|
|
1886
|
+
if (!cleanup && ref.authority !== undefined)
|
|
1887
|
+
yield* ref.authority(node, op);
|
|
1888
|
+
return node;
|
|
1889
|
+
});
|
|
1890
|
+
const read = (maximum, position) => {
|
|
1891
|
+
const op = OpContext.make(position === undefined ? "read" : "pread");
|
|
1892
|
+
const readOp = OpContext.make("read");
|
|
1893
|
+
const body = Effect.gen(function* () {
|
|
1894
|
+
const file = yield* get(op, "read");
|
|
1895
|
+
if (!isNatural(maximum))
|
|
1896
|
+
return yield* readOp.fail("InvalidArgument");
|
|
1897
|
+
const offset = position ?? ref.offset;
|
|
1898
|
+
if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
|
|
1899
|
+
return yield* readOp.fail("InvalidArgument");
|
|
1900
|
+
}
|
|
1901
|
+
const start = Number(offset > file.metadata.size ? file.metadata.size : offset);
|
|
1902
|
+
const data = file.data.slice(start, start + Math.min(maximum, file.data.length - start));
|
|
1903
|
+
const eof = start + data.length >= file.data.length;
|
|
1904
|
+
const access = maximum > 0 ? { node: file, now: yield* timestamp(readOp) } : undefined;
|
|
1905
|
+
return { value: { bytes: data, eof, next: offset + BigInt(data.length) }, access };
|
|
1906
|
+
});
|
|
1907
|
+
// A positioned read runs beside other reads. A cursor read stays a change, since two reads beside each
|
|
1908
|
+
// other would start at the same offset.
|
|
1909
|
+
if (position !== undefined)
|
|
1910
|
+
return Effect.map(accessing(op, body), ({ bytes, eof }) => ({ bytes, eof }));
|
|
1911
|
+
return coordinated(op, Effect.gen(function* () {
|
|
1912
|
+
const { bytes, eof, next } = yield* refreshAccess(yield* body);
|
|
1913
|
+
const draft = yield* Draft;
|
|
1914
|
+
yield* draft.afterInstall(() => {
|
|
1915
|
+
ref.offset = next;
|
|
1916
|
+
});
|
|
1917
|
+
return { bytes, eof };
|
|
1918
|
+
}));
|
|
1919
|
+
};
|
|
1920
|
+
const write = Effect.fnUntraced(function* (input, position) {
|
|
1921
|
+
const op = OpContext.make(position === undefined ? "write" : "pwrite");
|
|
1922
|
+
const writeOp = OpContext.make("write");
|
|
1923
|
+
if (!isAttachedBytes(input))
|
|
1924
|
+
return yield* writeOp.fail("InvalidArgument");
|
|
1925
|
+
const bytes = new Uint8Array(input);
|
|
1926
|
+
return yield* coordinated(op, Effect.gen(function* () {
|
|
1927
|
+
const file = yield* get(op, "write");
|
|
1928
|
+
const offset = position ?? (ref.append ? file.metadata.size : ref.offset);
|
|
1929
|
+
if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
|
|
1930
|
+
return yield* writeOp.fail("InvalidArgument");
|
|
1931
|
+
}
|
|
1932
|
+
if (bytes.length === 0) {
|
|
1933
|
+
return 0;
|
|
1934
|
+
}
|
|
1935
|
+
if (offset >= BigInt(maxFileBytes))
|
|
1936
|
+
return yield* writeOp.fail("FileTooLarge");
|
|
1937
|
+
const start = Number(offset);
|
|
1938
|
+
const draft = yield* Draft;
|
|
1939
|
+
const free = limits.maxBytes === undefined
|
|
1940
|
+
? BigInt(maxFileBytes)
|
|
1941
|
+
: ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
|
|
1942
|
+
const maximumEnd = BigInt(file.data.length) + free;
|
|
1943
|
+
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
1944
|
+
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
1945
|
+
if (count === 0)
|
|
1946
|
+
return yield* writeOp.fail("NoSpace");
|
|
1947
|
+
const size = Math.max(file.data.length, start + count);
|
|
1948
|
+
// Always detach before mutation. A same-sized write is the critical
|
|
1949
|
+
// case: the current payload may belong to the base or a prior capture.
|
|
1950
|
+
const data = new Uint8Array(size);
|
|
1951
|
+
data.set(file.data);
|
|
1952
|
+
const now = yield* timestamp(writeOp);
|
|
1953
|
+
data.set(bytes.subarray(0, count), start);
|
|
1954
|
+
yield* replaceContent(file, data, now);
|
|
1955
|
+
if (position === undefined) {
|
|
1956
|
+
const next = offset + BigInt(count);
|
|
1957
|
+
yield* draft.afterInstall(() => {
|
|
1958
|
+
ref.offset = next;
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
return count;
|
|
1962
|
+
}));
|
|
1963
|
+
});
|
|
1964
|
+
const statOp = OpContext.make("stat");
|
|
1965
|
+
const syncOp = OpContext.make("sync");
|
|
1966
|
+
const closeOp = OpContext.make("close");
|
|
1967
|
+
const handle = Object.freeze({
|
|
1968
|
+
[FileHandleId]: true,
|
|
1969
|
+
read: Effect.fnUntraced(function* (maximum) {
|
|
1970
|
+
return (yield* read(maximum)).bytes;
|
|
1971
|
+
}),
|
|
1972
|
+
pread: Effect.fnUntraced(function* (maximum, offset) {
|
|
1973
|
+
return yield* read(maximum, offset);
|
|
1974
|
+
}),
|
|
1975
|
+
write: Effect.fnUntraced(function* (bytes) {
|
|
1976
|
+
return yield* write(bytes);
|
|
1977
|
+
}),
|
|
1978
|
+
pwrite: dual(2, Effect.fnUntraced(function* (bytes, offset) {
|
|
1979
|
+
return yield* write(bytes, offset);
|
|
1980
|
+
})),
|
|
1981
|
+
seek: dual(2, Effect.fnUntraced(function* (offset, mode) {
|
|
1982
|
+
const op = OpContext.make("seek");
|
|
1983
|
+
return yield* coordinatedRead(op, Effect.uninterruptible(Effect.gen(function* () {
|
|
1984
|
+
const file = yield* get(op);
|
|
1985
|
+
if (!Predicate.isBigInt(offset) || !isSeekMode(mode)) {
|
|
1986
|
+
return yield* op.fail("InvalidArgument");
|
|
1987
|
+
}
|
|
1988
|
+
let next = mode === "current"
|
|
1989
|
+
? ref.offset + offset
|
|
1990
|
+
: mode === "end"
|
|
1991
|
+
? file.metadata.size + offset
|
|
1992
|
+
: offset;
|
|
1993
|
+
if (next < 0n || next > MAX_FILE_OFFSET) {
|
|
1994
|
+
return yield* op.fail("InvalidArgument");
|
|
1995
|
+
}
|
|
1996
|
+
if (mode === "data" || mode === "hole") {
|
|
1997
|
+
if (offset >= file.metadata.size)
|
|
1998
|
+
return yield* op.fail("NoData");
|
|
1999
|
+
if (mode === "hole")
|
|
2000
|
+
next = file.metadata.size;
|
|
2001
|
+
}
|
|
2002
|
+
ref.offset = next;
|
|
2003
|
+
return next;
|
|
2004
|
+
})));
|
|
2005
|
+
})),
|
|
2006
|
+
truncate: Effect.fnUntraced(function* (length) {
|
|
2007
|
+
const op = OpContext.make("truncate");
|
|
2008
|
+
return yield* coordinated(op, Effect.gen(function* () {
|
|
2009
|
+
yield* resize(yield* get(op, "write"), length, op);
|
|
2010
|
+
}));
|
|
2011
|
+
}),
|
|
2012
|
+
stat: coordinatedRead(statOp, Effect.map(Effect.suspend(() => get(statOp)), withMetadata)),
|
|
2013
|
+
sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))),
|
|
2014
|
+
close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp, undefined, true))).pipe(Effect.ensuring(closeReleasedScope(ref)))
|
|
2015
|
+
});
|
|
2016
|
+
registry.registerFile(handle, ref);
|
|
2017
|
+
return handle;
|
|
2018
|
+
};
|
|
2019
|
+
const createContext = (reference, identity, umask, roots = [], inherited = []) => {
|
|
2020
|
+
const boundary = roots.length === 0 ? undefined : Confinement.make(authorityView, roots);
|
|
2021
|
+
inherited = inherited.filter((chain, index) => !((chain.length <= roots.length && chain.every((ino, at) => roots[at] === ino)) ||
|
|
2022
|
+
inherited.slice(0, index).some((other) => other.length === chain.length && chain.every((ino, at) => other[at] === ino))));
|
|
2023
|
+
const additional = inherited.map((chain) => Confinement.make(authorityView, chain));
|
|
2024
|
+
const checkAuthority = Effect.fnUntraced(function* (node, op) {
|
|
2025
|
+
if (boundary !== undefined)
|
|
2026
|
+
yield* boundary.check(node, op);
|
|
2027
|
+
for (const authority of additional)
|
|
2028
|
+
yield* authority.check(node, op);
|
|
2029
|
+
});
|
|
2030
|
+
const inheritedFor = (target) => Target.$is("Handle")(target)
|
|
2031
|
+
? registry.handleBoundaries(target.handle)
|
|
2032
|
+
: Target.$is("Path")(target) && target.relativeTo !== undefined
|
|
2033
|
+
? registry.handleBoundaries(target.relativeTo)
|
|
2034
|
+
: [];
|
|
2035
|
+
const boundariesFor = (target) => [...(roots.length > 0 ? [roots] : []), ...inherited, ...(target === undefined ? [] : inheritedFor(target))];
|
|
2036
|
+
const assignAuthority = (ref, target) => {
|
|
2037
|
+
const chains = boundariesFor(target).filter((chain, index, all) => !all.some((other, at) => at !== index && other.length >= chain.length && chain.every((ino, offset) => other[offset] === ino) &&
|
|
2038
|
+
(other.length > chain.length || at < index)));
|
|
2039
|
+
if (chains.length === 0)
|
|
2040
|
+
return;
|
|
2041
|
+
ref.boundaries = chains;
|
|
2042
|
+
const authorities = chains.map((chain) => Confinement.make(authorityView, chain));
|
|
2043
|
+
ref.authority = Effect.fnUntraced(function* (node, op) {
|
|
2044
|
+
for (const authority of authorities)
|
|
2045
|
+
yield* authority.check(node, op, true);
|
|
2046
|
+
});
|
|
2047
|
+
};
|
|
2048
|
+
const callerPath = boundary?.path ?? pathOfCurrent;
|
|
2049
|
+
const resolver = Resolution.make({
|
|
2050
|
+
caller: reference,
|
|
2051
|
+
confinement: boundary,
|
|
2052
|
+
additional: additional.length === 0 ? undefined : additional,
|
|
2053
|
+
get: view,
|
|
2054
|
+
authorityView,
|
|
2055
|
+
identity,
|
|
2056
|
+
maxPathBytes: limits.maxPathBytes,
|
|
2057
|
+
registry
|
|
2058
|
+
});
|
|
2059
|
+
const prepare = resolver.prepare;
|
|
2060
|
+
const prepareEntry = resolver.prepareEntry;
|
|
2061
|
+
const entryName = Resolution.entryName;
|
|
2062
|
+
const resolveTarget = (target, op, options) => resolver.resolve(target, { kind: "Node", ...options }, op);
|
|
2063
|
+
const resolveDirectory = resolver.directory;
|
|
2064
|
+
const resolveEntry = (prepared, op) => resolver.resolve(prepared, { kind: "Parent" }, op);
|
|
2065
|
+
const referencedNode = (target, op) => Effect.map(resolveTarget(Target.Reference({ reference: target }), op), (resolved) => resolved.node);
|
|
2066
|
+
const directoryNow = Effect.fnUntraced(function* (ino) {
|
|
2067
|
+
const node = yield* view(ino);
|
|
2068
|
+
if (node?.kind !== "directory") {
|
|
2069
|
+
return dieInvariant("Directory left the inode table during a transition");
|
|
2070
|
+
}
|
|
2071
|
+
return node;
|
|
2072
|
+
});
|
|
2073
|
+
const nodeNow = Effect.fnUntraced(function* (ino) {
|
|
2074
|
+
const node = yield* view(ino);
|
|
2075
|
+
if (node === undefined)
|
|
2076
|
+
return dieInvariant("Inode left the table during a transition");
|
|
2077
|
+
return node;
|
|
2078
|
+
});
|
|
2079
|
+
const creationTimes = (times, now) => ({
|
|
2080
|
+
atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
|
|
2081
|
+
mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
|
|
2082
|
+
});
|
|
2083
|
+
const newDirectory = Effect.fnUntraced(function* (parent, mode, now, times) {
|
|
2084
|
+
const draft = yield* Draft;
|
|
2085
|
+
const ino = yield* draft.allocate;
|
|
2086
|
+
return {
|
|
2087
|
+
kind: "directory",
|
|
2088
|
+
ino,
|
|
2089
|
+
parent: parent.ino,
|
|
2090
|
+
name: "",
|
|
2091
|
+
entries: new Map(),
|
|
2092
|
+
metadata: {
|
|
2093
|
+
...directoryMetadata(BigInt(ino), identity.uid, parent.metadata.gid, mode, now),
|
|
2094
|
+
...creationTimes(times, now)
|
|
2095
|
+
},
|
|
2096
|
+
revision: 0n
|
|
2097
|
+
};
|
|
2098
|
+
});
|
|
2099
|
+
const newFile = Effect.fnUntraced(function* (parent, data, mode, now, owner, times) {
|
|
2100
|
+
const draft = yield* Draft;
|
|
2101
|
+
const ino = yield* draft.allocate;
|
|
2102
|
+
return {
|
|
2103
|
+
kind: "file",
|
|
2104
|
+
ino,
|
|
2105
|
+
data,
|
|
2106
|
+
links: [],
|
|
2107
|
+
metadata: {
|
|
2108
|
+
...directoryMetadata(BigInt(ino), owner?.uid ?? identity.uid, owner?.gid ?? parent.metadata.gid, mode, now),
|
|
2109
|
+
...creationTimes(times, now),
|
|
2110
|
+
kind: "file",
|
|
2111
|
+
size: BigInt(data.length),
|
|
2112
|
+
nlink: 0
|
|
2113
|
+
},
|
|
2114
|
+
revision: 0n
|
|
2115
|
+
};
|
|
2116
|
+
});
|
|
2117
|
+
const newSymlink = Effect.fnUntraced(function* (parent, target, now, times) {
|
|
2118
|
+
const draft = yield* Draft;
|
|
2119
|
+
const ino = yield* draft.allocate;
|
|
2120
|
+
return {
|
|
2121
|
+
kind: "symlink",
|
|
2122
|
+
ino,
|
|
2123
|
+
target,
|
|
2124
|
+
links: [],
|
|
2125
|
+
metadata: {
|
|
2126
|
+
...directoryMetadata(BigInt(ino), identity.uid, parent.metadata.gid, 0o777, now),
|
|
2127
|
+
...creationTimes(times, now),
|
|
2128
|
+
kind: "symlink",
|
|
2129
|
+
nlink: 0,
|
|
2130
|
+
size: BigInt(target.length)
|
|
2131
|
+
},
|
|
2132
|
+
revision: 0n
|
|
2133
|
+
};
|
|
2134
|
+
});
|
|
2135
|
+
const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
|
|
2136
|
+
const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
|
|
2137
|
+
? Effect.fail(op.fail("NotPermitted"))
|
|
2138
|
+
: Effect.succeed(grantedMode(metadata, mode));
|
|
2139
|
+
// Changes the attributes one change at a time would, as one change: one draft, one revision, one event.
|
|
2140
|
+
// Every check runs against the node before any attribute applies, ownership (NotPermitted) before
|
|
2141
|
+
// permission (AccessDenied), so the first failure leaves everything unchanged. The attributes apply as
|
|
2142
|
+
// size, owner, mode, then times, the POSIX composition of chown then chmod, so a requested mode wins over
|
|
2143
|
+
// the set-ID clearing that a resize or an owner change triggers; an adapter owns any protocol-specific
|
|
2144
|
+
// sanitising of the mode, as NFS SETATTR does after knfsd. The attributes arrive validated; an owner with
|
|
2145
|
+
// neither id still checks ownership, and times that omit both are no change and check nothing. An expected
|
|
2146
|
+
// revision is checked first, against the target as the change finds it, so a caller that decided the
|
|
2147
|
+
// attributes from an earlier observation learns the target moved instead of applying them to a newer state.
|
|
2148
|
+
const changeAttributes = (resolve, attributes, op) => {
|
|
2149
|
+
const { size, mode } = attributes;
|
|
2150
|
+
const expected = attributes.expected?.revision;
|
|
2151
|
+
const owner = attributes.owner === undefined ? undefined : { ...attributes.owner };
|
|
2152
|
+
const times = attributes.times === undefined
|
|
2153
|
+
? undefined
|
|
2154
|
+
: { access: { ...attributes.times.access }, modification: { ...attributes.times.modification } };
|
|
2155
|
+
const timed = times !== undefined && (times.access.kind !== "omit" || times.modification.kind !== "omit");
|
|
2156
|
+
const bothNow = times?.access.kind === "now" && times.modification.kind === "now";
|
|
2157
|
+
const resolving = resolve();
|
|
2158
|
+
return coordinated(op, Effect.gen(function* () {
|
|
2159
|
+
const resolved = yield* resolving;
|
|
2160
|
+
const node = yield* nodeNow(resolved.ino);
|
|
2161
|
+
if (expected !== undefined && node.revision !== expected) {
|
|
2162
|
+
return yield* resolved.op.fail("StaleReference", { field: "expected" });
|
|
2163
|
+
}
|
|
2164
|
+
if (size !== undefined && node.kind === "symlink")
|
|
2165
|
+
return yield* resolved.op.fail("SymlinkLoop");
|
|
2166
|
+
if (size !== undefined && node.kind !== "file")
|
|
2167
|
+
return yield* resolved.op.fail("IsDirectory");
|
|
2168
|
+
const owns = identity.privileged || identity.uid === node.metadata.uid;
|
|
2169
|
+
if ((mode !== undefined && !owns) ||
|
|
2170
|
+
(owner !== undefined && !identity.privileged && (!owns ||
|
|
2171
|
+
(owner.uid !== undefined && owner.uid !== node.metadata.uid) ||
|
|
2172
|
+
(owner.gid !== undefined && !inGroup(identity, owner.gid)))) ||
|
|
2173
|
+
// POSIX grants write access only when both times are UTIME_NOW; every other combination that
|
|
2174
|
+
// changes a time, mixed ones included, needs ownership.
|
|
2175
|
+
(timed && !owns && !bothNow)) {
|
|
2176
|
+
return yield* resolved.op.fail("NotPermitted");
|
|
2177
|
+
}
|
|
2178
|
+
if (size !== undefined || (timed && !owns))
|
|
2179
|
+
yield* authorize(node, identity, WRITE, resolved.op);
|
|
2180
|
+
const chowned = owner?.uid !== undefined || owner?.gid !== undefined;
|
|
2181
|
+
if (size === undefined && !chowned && mode === undefined && !timed)
|
|
2182
|
+
return;
|
|
2183
|
+
const now = yield* timestamp(op);
|
|
2184
|
+
if (size !== undefined && node.kind === "file")
|
|
2185
|
+
yield* resizeAt(node, size, now, op);
|
|
2186
|
+
const sized = yield* nodeNow(resolved.ino);
|
|
2187
|
+
const gid = owner?.gid ?? sized.metadata.gid;
|
|
2188
|
+
const cleared = chowned && sized.kind === "file" ? sized.metadata.mode & ~SET_ID_BITS : sized.metadata.mode;
|
|
2189
|
+
if (chowned || mode !== undefined || timed) {
|
|
2190
|
+
yield* (yield* Draft).put({
|
|
2191
|
+
...sized,
|
|
2192
|
+
metadata: {
|
|
2193
|
+
...sized.metadata,
|
|
2194
|
+
uid: owner?.uid ?? sized.metadata.uid,
|
|
2195
|
+
gid,
|
|
2196
|
+
mode: mode === undefined ? cleared : grantedMode({ kind: sized.kind, gid }, mode),
|
|
2197
|
+
atimeNs: timeAt(times?.access, sized.metadata.atimeNs, now),
|
|
2198
|
+
mtimeNs: timeAt(times?.modification, sized.metadata.mtimeNs, now),
|
|
2199
|
+
ctimeNs: now
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
yield* publishNode(node.ino);
|
|
2204
|
+
}));
|
|
2205
|
+
};
|
|
2206
|
+
const authorizeRemoval = (parent, child, op) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
|
|
2207
|
+
identity.uid !== parent.metadata.uid && identity.uid !== child.metadata.uid
|
|
2208
|
+
? Effect.fail(op.fail("NotPermitted"))
|
|
2209
|
+
: Effect.void;
|
|
2210
|
+
const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
|
|
2211
|
+
const parent = yield* directoryNow(entry.parent);
|
|
2212
|
+
if (boundary !== undefined && parent.ino === boundary.root &&
|
|
2213
|
+
(entry.name === undefined || isDotComponent(entry.name)))
|
|
2214
|
+
return yield* entry.op.fail("NotPermitted");
|
|
2215
|
+
if (isDotComponent(entry.name)) {
|
|
2216
|
+
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
|
|
2217
|
+
}
|
|
2218
|
+
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
2219
|
+
if (parent.entries.has(entry.name))
|
|
2220
|
+
return yield* entry.op.fail("AlreadyExists");
|
|
2221
|
+
if (trailingSlash === "rejected" && entry.trailingSlash)
|
|
2222
|
+
return yield* entry.op.fail("NotFound");
|
|
2223
|
+
yield* authorize(parent, identity, WRITE, entry.op);
|
|
2224
|
+
return entry.name;
|
|
2225
|
+
});
|
|
2226
|
+
const makeDirectory = Effect.fnUntraced(function* (entry, request, op) {
|
|
2227
|
+
const name = yield* claimName(entry);
|
|
2228
|
+
yield* reserveEntry(entry.op);
|
|
2229
|
+
const parent = yield* directoryNow(entry.parent);
|
|
2230
|
+
const before = parent.revision;
|
|
2231
|
+
const now = yield* timestamp(op);
|
|
2232
|
+
const mode = request.exactMode
|
|
2233
|
+
? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, request.mode, op)
|
|
2234
|
+
: (request.mode & 0o777 & ~umask) | (request.mode & STICKY_BIT);
|
|
2235
|
+
const child = yield* newDirectory(parent, mode, now, request.times);
|
|
2236
|
+
yield* attach(parent, name, child, now);
|
|
2237
|
+
const draft = yield* Draft;
|
|
2238
|
+
yield* draft.addEntries(1);
|
|
2239
|
+
yield* publishEntry("Create", parent.ino, name, child.ino);
|
|
2240
|
+
return { child: child.ino, directory: { before, after: draft.revision } };
|
|
2241
|
+
});
|
|
2242
|
+
// Creates every missing directory a path names in the running transition, so a failure anywhere discards
|
|
2243
|
+
// them all. The mode applies to each directory created and the times to the one in the final position. An
|
|
2244
|
+
// entry names one child, created unless it is already a directory. The result names the directory the path
|
|
2245
|
+
// ends on and its parent's revision before and after the call, so a final directory that already exists is
|
|
2246
|
+
// no change, while a path that leaves a directory it created, such as `new/..`, reports the creation.
|
|
2247
|
+
const makeDirectories = Effect.fnUntraced(function* (prepared, request, op) {
|
|
2248
|
+
let node;
|
|
2249
|
+
let entryOp = op;
|
|
2250
|
+
const revisionsBefore = new Map();
|
|
2251
|
+
if (prepared.kind === "entry") {
|
|
2252
|
+
const entry = yield* resolveEntry(prepared, op);
|
|
2253
|
+
const made = yield* Effect.result(makeDirectory(entry, request, op));
|
|
2254
|
+
if (Result.isSuccess(made))
|
|
2255
|
+
return made.success;
|
|
2256
|
+
const existingIno = entry.name === undefined
|
|
2257
|
+
? undefined
|
|
2258
|
+
: (yield* directoryNow(entry.parent)).entries.get(entry.name);
|
|
2259
|
+
node = existingIno === undefined ? undefined : (yield* view(existingIno));
|
|
2260
|
+
if (made.failure.code !== "AlreadyExists" || node?.kind !== "directory")
|
|
2261
|
+
return yield* made.failure;
|
|
2262
|
+
}
|
|
2263
|
+
else {
|
|
2264
|
+
entryOp = op.at(prepared.path.input);
|
|
2265
|
+
const resolved = yield* resolver.resolve(Target.Path(prepared.base === undefined
|
|
2266
|
+
? { path: prepared.path.input }
|
|
2267
|
+
: { path: prepared.path.input, relativeTo: prepared.base }), {
|
|
2268
|
+
kind: "Node",
|
|
2269
|
+
createMissing: (parent, name, final) => Effect.flatMap(makeDirectory({ parent: parent.ino, name, trailingSlash: false, addressing: "path", op: entryOp }, final ? request : { mode: request.mode, exactMode: request.exactMode }, op), (made) => {
|
|
2270
|
+
if (!revisionsBefore.has(parent.ino))
|
|
2271
|
+
revisionsBefore.set(parent.ino, made.directory.before);
|
|
2272
|
+
return directoryNow(made.child);
|
|
2273
|
+
})
|
|
2274
|
+
}, op);
|
|
2275
|
+
node = resolved.node;
|
|
2276
|
+
}
|
|
2277
|
+
if (node?.kind !== "directory")
|
|
2278
|
+
return yield* entryOp.fail("AlreadyExists");
|
|
2279
|
+
const after = (yield* directoryNow(node.parent)).revision;
|
|
2280
|
+
return { child: node.ino, directory: { before: revisionsBefore.get(node.parent) ?? after, after } };
|
|
2281
|
+
});
|
|
2282
|
+
const linkNode = Effect.fnUntraced(function* (node, entry, op) {
|
|
2283
|
+
const name = yield* claimName(entry, "rejected");
|
|
2284
|
+
yield* reserveEntry(entry.op);
|
|
2285
|
+
const parent = yield* directoryNow(entry.parent);
|
|
2286
|
+
const before = parent.revision;
|
|
2287
|
+
const now = yield* timestamp(op);
|
|
2288
|
+
yield* attach(parent, name, node, now);
|
|
2289
|
+
const draft = yield* Draft;
|
|
2290
|
+
yield* draft.addEntries(1);
|
|
2291
|
+
yield* publishEntry("Create", parent.ino, name, node.ino);
|
|
2292
|
+
return { before, after: draft.revision };
|
|
2293
|
+
});
|
|
2294
|
+
const makeSymlink = Effect.fnUntraced(function* (entry, target, times, op) {
|
|
2295
|
+
const name = yield* claimName(entry, "rejected");
|
|
2296
|
+
yield* reserveEntry(entry.op);
|
|
2297
|
+
yield* reserveBytes(entry.op, BigInt(target.length));
|
|
2298
|
+
const parent = yield* directoryNow(entry.parent);
|
|
2299
|
+
const before = parent.revision;
|
|
2300
|
+
const now = yield* timestamp(op);
|
|
2301
|
+
const child = yield* newSymlink(parent, target, now, times);
|
|
2302
|
+
yield* attach(parent, name, child, now);
|
|
2303
|
+
const draft = yield* Draft;
|
|
2304
|
+
yield* draft.addEntries(1);
|
|
2305
|
+
yield* draft.addUsedBytes(BigInt(target.length));
|
|
2306
|
+
yield* publishEntry("Create", parent.ino, name, child.ino);
|
|
2307
|
+
return { child: child.ino, directory: { before, after: draft.revision } };
|
|
2308
|
+
});
|
|
2309
|
+
const removeChild = Effect.fnUntraced(function* (parent, name, child, op) {
|
|
2310
|
+
const before = parent.revision;
|
|
2311
|
+
const now = yield* timestamp(op);
|
|
2312
|
+
const d = yield* Draft;
|
|
2313
|
+
yield* d.put(withEntries({
|
|
2314
|
+
...parent,
|
|
2315
|
+
metadata: {
|
|
2316
|
+
...parent.metadata,
|
|
2317
|
+
nlink: parent.metadata.nlink - (child.kind === "directory" ? 1 : 0),
|
|
2318
|
+
mtimeNs: now,
|
|
2319
|
+
ctimeNs: now
|
|
2320
|
+
}
|
|
2321
|
+
}, DirectoryEntries.remove(parent.entries, name)));
|
|
2322
|
+
yield* publishEntry("Remove", parent.ino, name, child.ino);
|
|
2323
|
+
yield* detach(child, parent.ino, name, now);
|
|
2324
|
+
yield* d.addEntries(-1);
|
|
2325
|
+
return { before, after: d.revision };
|
|
2326
|
+
});
|
|
2327
|
+
// Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
|
|
2328
|
+
// entry, and each verb reports it with its own code. Search permission comes before the lookup and write
|
|
2329
|
+
// permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
|
|
2330
|
+
const protectsRoot = (parent, name) => boundary !== undefined &&
|
|
2331
|
+
((parent.ino === boundary.root && (name === undefined || isDotComponent(name))) ||
|
|
2332
|
+
(name === "2e2e" && parent.parent === boundary.root));
|
|
2333
|
+
const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
|
|
2334
|
+
const parent = yield* directoryNow(entry.parent);
|
|
2335
|
+
if (protectsRoot(parent, entry.name))
|
|
2336
|
+
return yield* entry.op.fail("NotPermitted");
|
|
2337
|
+
if (isDotComponent(entry.name)) {
|
|
2338
|
+
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
|
|
2339
|
+
}
|
|
2340
|
+
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
2341
|
+
const childIno = parent.entries.get(entry.name);
|
|
2342
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
2343
|
+
if (child === undefined)
|
|
2344
|
+
return yield* entry.op.fail("NotFound");
|
|
2345
|
+
if (child.ino === boundary?.root)
|
|
2346
|
+
return yield* entry.op.fail("NotPermitted");
|
|
2347
|
+
const rejected = beforeWrite?.(child);
|
|
2348
|
+
if (rejected !== undefined)
|
|
2349
|
+
return yield* rejected;
|
|
2350
|
+
yield* authorize(parent, identity, WRITE, entry.op);
|
|
2351
|
+
return { parent, name: entry.name, child };
|
|
2352
|
+
});
|
|
2353
|
+
const removeEntry = Effect.fnUntraced(function* (entry, op) {
|
|
2354
|
+
const { child, name, parent } = yield* removalTarget(entry, "InvalidArgument", (found) => entry.trailingSlash && found.kind !== "directory" ? entry.op.fail("NotDirectory") : undefined);
|
|
2355
|
+
yield* authorizeRemoval(parent, child, entry.op);
|
|
2356
|
+
if (child.kind === "directory" && child.entries.size > 0)
|
|
2357
|
+
return yield* entry.op.fail("NotEmpty");
|
|
2358
|
+
return yield* removeChild(parent, name, child, op);
|
|
2359
|
+
});
|
|
2360
|
+
const unlinkEntry = Effect.fnUntraced(function* (entry, op) {
|
|
2361
|
+
const { child, name, parent } = yield* removalTarget(entry, "IsDirectory", (found) => !entry.trailingSlash
|
|
2362
|
+
? undefined
|
|
2363
|
+
: entry.op.fail(found.kind === "directory" ? "IsDirectory" : "NotDirectory"));
|
|
2364
|
+
if (child.kind === "directory")
|
|
2365
|
+
return yield* entry.op.fail("IsDirectory");
|
|
2366
|
+
yield* authorizeRemoval(parent, child, entry.op);
|
|
2367
|
+
return yield* removeChild(parent, name, child, op);
|
|
2368
|
+
});
|
|
2369
|
+
const rmdirEntry = Effect.fnUntraced(function* (entry, op) {
|
|
2370
|
+
const { child, name, parent } = yield* removalTarget(entry, "InvalidArgument");
|
|
2371
|
+
yield* authorizeRemoval(parent, child, entry.op);
|
|
2372
|
+
if (child.kind !== "directory")
|
|
2373
|
+
return yield* entry.op.fail("NotDirectory");
|
|
2374
|
+
if (child.entries.size > 0)
|
|
2375
|
+
return yield* entry.op.fail("NotEmpty");
|
|
2376
|
+
return yield* removeChild(parent, name, child, op);
|
|
2377
|
+
});
|
|
2378
|
+
const renameEntry = Effect.fnUntraced(function* (source, destination, op) {
|
|
2379
|
+
const sourceDirectory = yield* directoryNow(source.parent);
|
|
2380
|
+
const destinationDirectory = yield* directoryNow(destination.parent);
|
|
2381
|
+
const sameDirectory = source.parent === destination.parent;
|
|
2382
|
+
const sourceName = source.name;
|
|
2383
|
+
const destinationName = destination.name;
|
|
2384
|
+
if (protectsRoot(sourceDirectory, sourceName) || protectsRoot(destinationDirectory, destinationName))
|
|
2385
|
+
return yield* source.op.fail("NotPermitted");
|
|
2386
|
+
if (isDotComponent(sourceName) || isDotComponent(destinationName)) {
|
|
2387
|
+
return yield* source.op.fail("InvalidArgument");
|
|
2388
|
+
}
|
|
2389
|
+
// Linux's order: search permission on both directories before either name is looked up; the names,
|
|
2390
|
+
// trailing slashes, the subtree rule and the same-object no-op; then write permission and the rest.
|
|
2391
|
+
yield* authorize(sourceDirectory, identity, EXECUTE, source.op);
|
|
2392
|
+
yield* authorize(destinationDirectory, identity, EXECUTE, destination.op);
|
|
2393
|
+
const sourceBefore = sourceDirectory.revision;
|
|
2394
|
+
const destinationBefore = destinationDirectory.revision;
|
|
2395
|
+
const childIno = sourceDirectory.entries.get(sourceName);
|
|
2396
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
2397
|
+
if (child === undefined)
|
|
2398
|
+
return yield* source.op.fail("NotFound");
|
|
2399
|
+
const replacedIno = destinationDirectory.entries.get(destinationName);
|
|
2400
|
+
const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
|
|
2401
|
+
if (boundary !== undefined && (child.ino === boundary.root || replaced?.ino === boundary.root)) {
|
|
2402
|
+
return yield* source.op.fail("NotPermitted");
|
|
2403
|
+
}
|
|
2404
|
+
// A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
|
|
2405
|
+
// source is one, as Linux allows.
|
|
2406
|
+
if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
|
|
2407
|
+
return yield* (source.trailingSlash ? source.op : destination.op).fail("NotDirectory");
|
|
2408
|
+
}
|
|
2409
|
+
if (destination.trailingSlash && replaced !== undefined && replaced.kind !== "directory") {
|
|
2410
|
+
return yield* destination.op.fail("NotDirectory");
|
|
2411
|
+
}
|
|
2412
|
+
const result = Effect.fnUntraced(function* () {
|
|
2413
|
+
return sameDirectory
|
|
2414
|
+
? {
|
|
2415
|
+
_tag: "SameDirectory",
|
|
2416
|
+
directory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision }
|
|
2417
|
+
}
|
|
2418
|
+
: {
|
|
2419
|
+
_tag: "DifferentDirectories",
|
|
2420
|
+
sourceDirectory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision },
|
|
2421
|
+
destinationDirectory: {
|
|
2422
|
+
before: destinationBefore,
|
|
2423
|
+
after: (yield* directoryNow(destination.parent)).revision
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
});
|
|
2427
|
+
for (let ancestor = destinationDirectory;; ancestor = yield* directoryNow(ancestor.parent)) {
|
|
2428
|
+
if (ancestor.ino === child.ino)
|
|
2429
|
+
return yield* destination.op.fail("InvalidArgument");
|
|
2430
|
+
if (ancestor.ino === ROOT_INO)
|
|
2431
|
+
break;
|
|
2432
|
+
}
|
|
2433
|
+
if (child.ino === replaced?.ino)
|
|
2434
|
+
return yield* result();
|
|
2435
|
+
yield* authorize(sourceDirectory, identity, WRITE, source.op);
|
|
2436
|
+
yield* authorize(destinationDirectory, identity, WRITE, destination.op);
|
|
2437
|
+
yield* authorizeRemoval(sourceDirectory, child, source.op);
|
|
2438
|
+
if (replaced !== undefined) {
|
|
2439
|
+
yield* authorizeRemoval(destinationDirectory, replaced, destination.op);
|
|
2440
|
+
if (child.kind === "directory" && replaced.kind !== "directory") {
|
|
2441
|
+
return yield* destination.op.fail("NotDirectory");
|
|
2442
|
+
}
|
|
2443
|
+
if (child.kind !== "directory" && replaced.kind === "directory") {
|
|
2444
|
+
return yield* destination.op.fail("IsDirectory");
|
|
2445
|
+
}
|
|
2446
|
+
if (replaced.kind === "directory" && replaced.entries.size > 0) {
|
|
2447
|
+
return yield* destination.op.fail("NotEmpty");
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
const now = yield* timestamp(op);
|
|
2451
|
+
const d = yield* Draft;
|
|
2452
|
+
yield* d.rename({
|
|
2453
|
+
ino: child.ino,
|
|
2454
|
+
from: { parent: source.parent, name: sourceName },
|
|
2455
|
+
to: { parent: destination.parent, name: destinationName }
|
|
2456
|
+
});
|
|
2457
|
+
yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
|
|
2458
|
+
yield* d.put(withEntries({
|
|
2459
|
+
...sourceDirectory,
|
|
2460
|
+
metadata: {
|
|
2461
|
+
...sourceDirectory.metadata,
|
|
2462
|
+
nlink: sourceDirectory.metadata.nlink - (child.kind === "directory" ? 1 : 0),
|
|
2463
|
+
mtimeNs: now,
|
|
2464
|
+
ctimeNs: now
|
|
2465
|
+
}
|
|
2466
|
+
}, DirectoryEntries.remove(sourceDirectory.entries, sourceName)));
|
|
2467
|
+
const destinationNow = yield* directoryNow(destination.parent);
|
|
2468
|
+
yield* d.put(withEntries({
|
|
2469
|
+
...destinationNow,
|
|
2470
|
+
metadata: {
|
|
2471
|
+
...destinationNow.metadata,
|
|
2472
|
+
nlink: destinationNow.metadata.nlink + (child.kind === "directory" && replaced === undefined ? 1 : 0),
|
|
2473
|
+
mtimeNs: now,
|
|
2474
|
+
ctimeNs: now
|
|
2475
|
+
}
|
|
2476
|
+
}, DirectoryEntries.set(destinationNow.entries, destinationName, child.ino)));
|
|
2477
|
+
const moved = yield* nodeNow(child.ino);
|
|
2478
|
+
if (moved.kind === "directory") {
|
|
2479
|
+
yield* d.put({
|
|
2480
|
+
...moved,
|
|
2481
|
+
parent: destination.parent,
|
|
2482
|
+
name: destinationName,
|
|
2483
|
+
metadata: { ...moved.metadata, ctimeNs: now }
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
else {
|
|
2487
|
+
yield* d.put({
|
|
2488
|
+
...moved,
|
|
2489
|
+
links: [
|
|
2490
|
+
...withoutLink(moved.links, source.parent, sourceName),
|
|
2491
|
+
{ parent: destination.parent, name: destinationName }
|
|
2492
|
+
],
|
|
2493
|
+
metadata: { ...moved.metadata, ctimeNs: now }
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
if (replaced !== undefined) {
|
|
2497
|
+
yield* detach(yield* nodeNow(replaced.ino), destination.parent, destinationName, now);
|
|
2498
|
+
yield* d.addEntries(-1);
|
|
2499
|
+
}
|
|
2500
|
+
yield* publishEntry("Create", destination.parent, destinationName, child.ino);
|
|
2501
|
+
return yield* result();
|
|
2502
|
+
});
|
|
2503
|
+
const openExisting = Effect.fnUntraced(function* (file, request, at, op) {
|
|
2504
|
+
yield* authorize(file, identity, request.access === "read" ? READ : request.access === "write" ? WRITE : READ | WRITE, at);
|
|
2505
|
+
if (request.truncate)
|
|
2506
|
+
yield* resize(file, 0n, op);
|
|
2507
|
+
});
|
|
2508
|
+
const openFile = Effect.fnUntraced(function* (entry, found, request, acquired, op) {
|
|
2509
|
+
const { name } = entry;
|
|
2510
|
+
const parent = yield* directoryNow(entry.parent);
|
|
2511
|
+
const before = parent.revision;
|
|
2512
|
+
let file = found;
|
|
2513
|
+
let created = false;
|
|
2514
|
+
if (file === undefined) {
|
|
2515
|
+
if (request.create === undefined || request.create === "never")
|
|
2516
|
+
return yield* entry.op.fail("NotFound");
|
|
2517
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, entry.op);
|
|
2518
|
+
yield* reserveEntry(entry.op);
|
|
2519
|
+
const size = request.initialSize ?? 0n;
|
|
2520
|
+
if (request.initialSize !== undefined) {
|
|
2521
|
+
if (size < 0n)
|
|
2522
|
+
return yield* entry.op.fail("InvalidArgument");
|
|
2523
|
+
if (size > BigInt(maxFileBytes))
|
|
2524
|
+
return yield* entry.op.fail("FileTooLarge");
|
|
2525
|
+
yield* reserveBytes(entry.op, size);
|
|
2526
|
+
}
|
|
2527
|
+
const owner = request.owner;
|
|
2528
|
+
if (!identity.privileged &&
|
|
2529
|
+
((owner?.uid !== undefined && owner.uid !== identity.uid) ||
|
|
2530
|
+
(owner?.gid !== undefined && !inGroup(identity, owner.gid)))) {
|
|
2531
|
+
return yield* entry.op.fail("NotPermitted");
|
|
2532
|
+
}
|
|
2533
|
+
const now = yield* timestamp(op);
|
|
2534
|
+
const mode = request.exactMode
|
|
2535
|
+
? yield* permittedMode({ kind: "file", uid: owner?.uid ?? identity.uid, gid: owner?.gid ?? parent.metadata.gid }, request.mode, op)
|
|
2536
|
+
: (request.mode ?? 0o666) & 0o777 & ~umask;
|
|
2537
|
+
file = yield* newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
|
|
2538
|
+
yield* attach(parent, name, file, now);
|
|
2539
|
+
const draft = yield* Draft;
|
|
2540
|
+
yield* draft.addEntries(1);
|
|
2541
|
+
yield* draft.addUsedBytes(size);
|
|
2542
|
+
created = true;
|
|
2543
|
+
yield* publishEntry("Create", parent.ino, name, file.ino);
|
|
2544
|
+
}
|
|
2545
|
+
else {
|
|
2546
|
+
if (file.kind === "symlink")
|
|
2547
|
+
return yield* entry.op.fail("SymlinkLoop");
|
|
2548
|
+
if (file.kind !== "file")
|
|
2549
|
+
return yield* entry.op.fail("IsDirectory");
|
|
2550
|
+
yield* openExisting(file, request, entry.op, op);
|
|
2551
|
+
}
|
|
2552
|
+
yield* (yield* Draft).retain(file.ino);
|
|
2553
|
+
yield* bindFile(acquired, file.ino);
|
|
2554
|
+
return { ino: file.ino, created, directory: { before, after: (yield* directoryNow(entry.parent)).revision } };
|
|
2555
|
+
});
|
|
2556
|
+
const releasePending = (opened) => () => Effect.gen(function* () {
|
|
2557
|
+
const ino = opened();
|
|
2558
|
+
if (ino === undefined)
|
|
2559
|
+
return;
|
|
2560
|
+
yield* releaseFileInode(ino);
|
|
2561
|
+
});
|
|
2562
|
+
const acquireOpenedFile = (ref, op, acquire) => {
|
|
2563
|
+
let opened;
|
|
2564
|
+
return Effect.andThen(preflight(op), acquireHandle(ref, (effect) => coordinated(op, effect), acquire((ino) => {
|
|
2565
|
+
opened = ino;
|
|
2566
|
+
}), releasePending(() => opened), finalizeFile(ref)));
|
|
2567
|
+
};
|
|
2568
|
+
const asResolvedNode = (target, op, options) => Effect.map(resolveTarget(target, op, options), (resolved) => ({ ino: resolved.node.ino, op: resolved.op }));
|
|
2569
|
+
const asTarget = (input) => Target.of(input);
|
|
2570
|
+
const acquireDirectory = Effect.fnUntraced(function* (input, op) {
|
|
2571
|
+
yield* preflight(op);
|
|
2572
|
+
const target = asTarget(input);
|
|
2573
|
+
const acquired = makeDirectoryReference();
|
|
2574
|
+
return yield* acquireHandle(acquired, (acquire) => coordinatedRead(op, Effect.uninterruptible(acquire)), Effect.gen(function* () {
|
|
2575
|
+
const directory = yield* resolveDirectory(target, op);
|
|
2576
|
+
yield* authorize(directory.node, identity, EXECUTE, directory.op);
|
|
2577
|
+
assignAuthority(acquired, target);
|
|
2578
|
+
const candidates = acquired.boundaries ?? [];
|
|
2579
|
+
if (candidates.length === 1)
|
|
2580
|
+
acquired.namespaceRoots = candidates[0];
|
|
2581
|
+
let current = directory.node;
|
|
2582
|
+
while (candidates.length > 1) {
|
|
2583
|
+
const nearest = candidates.find((chain) => chain.at(-1) === current.ino);
|
|
2584
|
+
if (nearest !== undefined) {
|
|
2585
|
+
acquired.namespaceRoots = nearest;
|
|
2586
|
+
break;
|
|
2587
|
+
}
|
|
2588
|
+
if (current.ino === ROOT_INO)
|
|
2589
|
+
break;
|
|
2590
|
+
current = yield* directoryNow(current.parent);
|
|
2591
|
+
}
|
|
2592
|
+
holdDirectory(directory.node.ino);
|
|
2593
|
+
TokenRegistry.publish(acquired, directory.node.ino);
|
|
2594
|
+
return acquired;
|
|
2595
|
+
}),
|
|
2596
|
+
// Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
|
|
2597
|
+
// every permit, where a detached directory may leave the table.
|
|
2598
|
+
() => Effect.void, finalizeDirectory(acquired));
|
|
2599
|
+
});
|
|
2600
|
+
const fail = (op, cause, field) => op.fail("InvalidArgument", field === undefined ? { cause } : { cause, field });
|
|
2601
|
+
const preparedOp = (prepared, op) => prepared.kind === "path" ? op.at(prepared.path.input) : op;
|
|
2602
|
+
const openPath = Effect.fnUntraced(function* (target, options, op) {
|
|
2603
|
+
const pathOp = op.at(target.path);
|
|
2604
|
+
const prepared = prepare(target.path, op);
|
|
2605
|
+
const chosen = yield* decodeOpenOptions(options).pipe(Effect.mapError((cause) => fail(pathOp, cause)));
|
|
2606
|
+
if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
|
|
2607
|
+
return yield* pathOp.fail("InvalidArgument");
|
|
2608
|
+
}
|
|
2609
|
+
if (chosen.mode !== undefined && (chosen.create === undefined || chosen.create === "never")) {
|
|
2610
|
+
return yield* pathOp.fail("InvalidArgument");
|
|
2611
|
+
}
|
|
2612
|
+
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
2613
|
+
const follow = target.followFinalSymlink ?? true;
|
|
2614
|
+
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
2615
|
+
const path = yield* Effect.fromResult(prepared);
|
|
2616
|
+
const resolved = yield* resolver.resolve({ kind: "path", path, base: target.relativeTo }, {
|
|
2617
|
+
kind: "OrCreate",
|
|
2618
|
+
create: chosen.create ?? "never",
|
|
2619
|
+
finalSymlink: follow ? "follow" : "preserve"
|
|
2620
|
+
}, op);
|
|
2621
|
+
const { parent, name } = resolved;
|
|
2622
|
+
if (resolved.kind === "missing" && path.trailingSlash)
|
|
2623
|
+
return yield* pathOp.fail("IsDirectory");
|
|
2624
|
+
yield* authorize(parent, identity, EXECUTE, pathOp);
|
|
2625
|
+
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
2626
|
+
const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
|
|
2627
|
+
opened(result.ino);
|
|
2628
|
+
assignAuthority(acquired, target);
|
|
2629
|
+
return fileHandle(acquired);
|
|
2630
|
+
}));
|
|
2631
|
+
});
|
|
2632
|
+
const openNode = Effect.fnUntraced(function* (target, options, op) {
|
|
2633
|
+
const chosen = yield* decodeOpenOptions(options).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
2634
|
+
if (chosen.access === "read" && (chosen.append || chosen.truncate))
|
|
2635
|
+
return yield* op.fail("InvalidArgument");
|
|
2636
|
+
if ((chosen.create !== undefined && chosen.create !== "never") || chosen.mode !== undefined) {
|
|
2637
|
+
return yield* op.fail("InvalidArgument");
|
|
2638
|
+
}
|
|
2639
|
+
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
2640
|
+
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
2641
|
+
const resolved = yield* resolveTarget(target, op);
|
|
2642
|
+
const node = resolved.node;
|
|
2643
|
+
if (node.kind !== "file")
|
|
2644
|
+
return yield* resolved.op.fail("IsDirectory");
|
|
2645
|
+
if (node.metadata.nlink === 0)
|
|
2646
|
+
return yield* resolved.op.fail("StaleReference");
|
|
2647
|
+
yield* openExisting(node, chosen, resolved.op, op);
|
|
2648
|
+
const draft = yield* Draft;
|
|
2649
|
+
yield* draft.retain(node.ino);
|
|
2650
|
+
yield* bindFile(acquired, node.ino);
|
|
2651
|
+
opened(node.ino);
|
|
2652
|
+
assignAuthority(acquired, target);
|
|
2653
|
+
return fileHandle(acquired);
|
|
2654
|
+
}));
|
|
2655
|
+
});
|
|
2656
|
+
const openEntry = Effect.fnUntraced(function* (entry, options, op) {
|
|
2657
|
+
const name = yield* Effect.fromResult(entryName(entry.name, op));
|
|
2658
|
+
const decoded = yield* decodeOpenEntryOptions(options).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
2659
|
+
const chosen = { ...decoded };
|
|
2660
|
+
if (chosen.access === "read" && (chosen.append || chosen.truncate))
|
|
2661
|
+
return yield* op.fail("InvalidArgument");
|
|
2662
|
+
if ((chosen.mode !== undefined || chosen.times !== undefined || chosen.initialSize !== undefined ||
|
|
2663
|
+
chosen.exactMode !== undefined || chosen.owner !== undefined) &&
|
|
2664
|
+
(chosen.create === undefined || chosen.create === "never")) {
|
|
2665
|
+
return yield* op.fail("InvalidArgument");
|
|
2666
|
+
}
|
|
2667
|
+
if (chosen.exactMode && chosen.mode === undefined)
|
|
2668
|
+
return yield* op.fail("InvalidArgument");
|
|
2669
|
+
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
2670
|
+
const expected = chosen.expected;
|
|
2671
|
+
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
2672
|
+
const resolved = yield* resolver.resolve({ kind: "entry", directory: entry.directory, name }, {
|
|
2673
|
+
kind: "OrCreate",
|
|
2674
|
+
create: chosen.create ?? "never",
|
|
2675
|
+
finalSymlink: chosen.followFinalSymlink !== false ? "follow" : "preserve",
|
|
2676
|
+
beforeFollow: Effect.fnUntraced(function* (_, direct) {
|
|
2677
|
+
if (expected !== undefined) {
|
|
2678
|
+
let expectedIno;
|
|
2679
|
+
if (expected !== null) {
|
|
2680
|
+
const observed = yield* Effect.result(referencedNode(expected, op));
|
|
2681
|
+
if (Result.isFailure(observed)) {
|
|
2682
|
+
return yield* op.fail("VolumeBusy", { cause: observed.failure });
|
|
2683
|
+
}
|
|
2684
|
+
expectedIno = observed.success.ino;
|
|
2685
|
+
}
|
|
2686
|
+
if (direct?.ino !== expectedIno)
|
|
2687
|
+
return yield* op.fail("VolumeBusy");
|
|
2688
|
+
}
|
|
2689
|
+
if (chosen.expectedChild === null) {
|
|
2690
|
+
if (direct !== undefined)
|
|
2691
|
+
return yield* op.fail("StaleReference");
|
|
2692
|
+
}
|
|
2693
|
+
else if (chosen.expectedChild !== undefined) {
|
|
2694
|
+
const expectedChild = chosen.expectedChild;
|
|
2695
|
+
const observed = yield* referencedNode(expectedChild.reference, op);
|
|
2696
|
+
if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
|
|
2697
|
+
observed.metadata.atimeNs !== expectedChild.atimeNs ||
|
|
2698
|
+
observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
|
|
2699
|
+
return yield* op.fail("StaleReference");
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
})
|
|
2703
|
+
}, op);
|
|
2704
|
+
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
2705
|
+
const mutationParent = resolved.kind === "missing" ? resolved.parent.ino : resolved.origin.parent.ino;
|
|
2706
|
+
const mutationName = resolved.kind === "missing" ? resolved.name : resolved.origin.name;
|
|
2707
|
+
const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
|
|
2708
|
+
opened(result.ino);
|
|
2709
|
+
return {
|
|
2710
|
+
handle: (() => {
|
|
2711
|
+
assignAuthority(acquired, entry.directory);
|
|
2712
|
+
return fileHandle(acquired);
|
|
2713
|
+
})(),
|
|
2714
|
+
reference: referenceFor(result.ino),
|
|
2715
|
+
created: result.created,
|
|
2716
|
+
directory: result.directory
|
|
2717
|
+
};
|
|
2718
|
+
}));
|
|
2719
|
+
});
|
|
2720
|
+
const walkChildren = Effect.fnUntraced(function* (directory, path, depth, up) {
|
|
2721
|
+
const listed = referenceFor(directory.ino);
|
|
2722
|
+
const frames = [];
|
|
2723
|
+
for (const [name, childIno] of Arr.sort(directory.entries, byEntryName)) {
|
|
2724
|
+
const child = yield* view(childIno);
|
|
2725
|
+
if (child === undefined)
|
|
2726
|
+
continue;
|
|
2727
|
+
const bytes = nameBytes(name);
|
|
2728
|
+
frames.push({
|
|
2729
|
+
ino: child.ino,
|
|
2730
|
+
kind: child.kind,
|
|
2731
|
+
name: bytes,
|
|
2732
|
+
key: name,
|
|
2733
|
+
path: joinPath(path, bytes),
|
|
2734
|
+
parent: directory.ino,
|
|
2735
|
+
up,
|
|
2736
|
+
depth: depth + 1,
|
|
2737
|
+
bytes: child.kind === "directory" ? 0n : child.metadata.size,
|
|
2738
|
+
reference: referenceFor(child.ino),
|
|
2739
|
+
directory: listed,
|
|
2740
|
+
listed: false
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
return frames;
|
|
2744
|
+
});
|
|
2745
|
+
const anchorFrame = (parent, key, root) => ({
|
|
2746
|
+
ino: root.ino,
|
|
2747
|
+
kind: root.kind,
|
|
2748
|
+
name: nameBytes(key),
|
|
2749
|
+
key,
|
|
2750
|
+
path: new Uint8Array(0),
|
|
2751
|
+
parent: parent.ino,
|
|
2752
|
+
up: undefined,
|
|
2753
|
+
depth: 0,
|
|
2754
|
+
bytes: 0n,
|
|
2755
|
+
reference: referenceFor(root.ino),
|
|
2756
|
+
directory: referenceFor(parent.ino),
|
|
2757
|
+
listed: false
|
|
2758
|
+
});
|
|
2759
|
+
const pathAnchor = (target, root, op) => {
|
|
2760
|
+
if (!Target.$is("Path")(target))
|
|
2761
|
+
return Effect.undefined;
|
|
2762
|
+
const prepared = prepare(target.path, op);
|
|
2763
|
+
if (Result.isFailure(prepared))
|
|
2764
|
+
return Effect.undefined;
|
|
2765
|
+
return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
|
|
2766
|
+
return entry.name !== undefined && parent?.kind === "directory" &&
|
|
2767
|
+
parent.entries.get(entry.name) === root.ino
|
|
2768
|
+
? anchorFrame(parent, entry.name, root)
|
|
2769
|
+
: undefined;
|
|
2770
|
+
})), Effect.catch(() => Effect.undefined));
|
|
2771
|
+
};
|
|
2772
|
+
const holds = (parent, frame) => parent?.kind === "directory" && parent.metadata.nlink > 0 && parent.entries.get(frame.key) === frame.ino;
|
|
2773
|
+
// The node a frame names, reached by name from the walk's root as a path lookup reaches it: every directory on
|
|
2774
|
+
// the way must be searchable and must still hold the name the walk listed for the object it listed. A frame
|
|
2775
|
+
// whose chain no longer holds it, such as a directory renamed out of the tree, reaches nothing. A root the
|
|
2776
|
+
// walk reached by name is anchored by that name, so a root moved away reaches nothing too; a root reached
|
|
2777
|
+
// through a reference or a handle is held by the object it resolved to, as a descriptor holds it.
|
|
2778
|
+
// It steps down the chain in a loop, so a tree of any depth reaches its frames without deepening the stack.
|
|
2779
|
+
const reachFrame = (frame, at) => Effect.gen(function* () {
|
|
2780
|
+
if (boundary !== undefined)
|
|
2781
|
+
yield* boundary.alive(at);
|
|
2782
|
+
const chain = [];
|
|
2783
|
+
for (let link = frame; link !== undefined; link = link.up)
|
|
2784
|
+
chain.push(link);
|
|
2785
|
+
let node = yield* view((chain.at(-1) ?? frame).parent);
|
|
2786
|
+
if (node !== undefined)
|
|
2787
|
+
yield* checkAuthority(node, at);
|
|
2788
|
+
for (let index = chain.length - 1; index >= 0; index--) {
|
|
2789
|
+
const link = chain[index];
|
|
2790
|
+
if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
|
|
2791
|
+
return undefined;
|
|
2792
|
+
yield* authorize(node, identity, EXECUTE, at);
|
|
2793
|
+
node = node.entries.get(link.key) === link.ino ? (yield* view(link.ino)) : undefined;
|
|
2794
|
+
}
|
|
2795
|
+
return node;
|
|
2796
|
+
});
|
|
2797
|
+
// Walks the tree below the directory `first` lists, depth first. Every later directory is read in its own
|
|
2798
|
+
// observation, so a walk holds one permit at a time and never a handle; it writes nothing, so it refreshes no
|
|
2799
|
+
// access time. Each directory is reached by name, so it needs search permission on the directories above it,
|
|
2800
|
+
// and one that left the tree after it was listed has nothing to walk. A directory past `maxDepth` is never
|
|
2801
|
+
// read. `locate` names an entry's path in a failure, and `listable` authorizes reading a directory. Entries
|
|
2802
|
+
// gathered before a failure are handed on before the failure is.
|
|
2803
|
+
const walkFrames = (op, first, plan, locate, listable) => Stream.suspend(() => {
|
|
2804
|
+
let pending;
|
|
2805
|
+
let entries = 0;
|
|
2806
|
+
let bytes = 0n;
|
|
2807
|
+
let failure;
|
|
2808
|
+
const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
|
|
2809
|
+
if (TokenRegistry.inode(reference) === undefined)
|
|
2810
|
+
return Effect.fail(op.fail("ClosedCaller"));
|
|
2811
|
+
const at = op.at(locate(frame.path));
|
|
2812
|
+
return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
|
|
2813
|
+
? Effect.succeed([])
|
|
2814
|
+
: Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
|
|
2815
|
+
}));
|
|
2816
|
+
const admit = (frame) => {
|
|
2817
|
+
entries++;
|
|
2818
|
+
bytes += frame.bytes;
|
|
2819
|
+
return plan.limit(frame, { entries, bytes }, "entry");
|
|
2820
|
+
};
|
|
2821
|
+
const step = Effect.gen(function* () {
|
|
2822
|
+
// Preserve the generic limit error E; yielding the error directly widens it to WalkFailure.
|
|
2823
|
+
// oxlint-disable-next-line effecttsgo/unnecessary-fail-yieldable-error -- E must stay narrow for unbounded removal.
|
|
2824
|
+
if (failure !== undefined)
|
|
2825
|
+
return yield* Effect.fail(failure);
|
|
2826
|
+
if (pending === undefined)
|
|
2827
|
+
pending = (yield* first).reverse();
|
|
2828
|
+
const out = [];
|
|
2829
|
+
const stop = (error) => {
|
|
2830
|
+
if (out.length === 0)
|
|
2831
|
+
return Effect.fail(error);
|
|
2832
|
+
failure = error;
|
|
2833
|
+
return Effect.succeed([out, Option.some(undefined)]);
|
|
2834
|
+
};
|
|
2835
|
+
while (out.length < WALK_CHUNK_ENTRIES) {
|
|
2836
|
+
const frame = pending.pop();
|
|
2837
|
+
if (frame === undefined)
|
|
2838
|
+
return [out, Option.none()];
|
|
2839
|
+
const unlisted = frame.kind === "directory" && !frame.listed;
|
|
2840
|
+
if (!unlisted || plan.order === "pre") {
|
|
2841
|
+
const rejected = admit(frame);
|
|
2842
|
+
if (rejected !== undefined)
|
|
2843
|
+
return yield* stop(rejected);
|
|
2844
|
+
out.push(frame);
|
|
2845
|
+
}
|
|
2846
|
+
if (unlisted) {
|
|
2847
|
+
const rejected = plan.limit(frame, { entries, bytes }, "directory");
|
|
2848
|
+
if (rejected !== undefined)
|
|
2849
|
+
return yield* stop(rejected);
|
|
2850
|
+
const listed = yield* Effect.result(list(frame));
|
|
2851
|
+
if (Result.isFailure(listed))
|
|
2852
|
+
return yield* stop(listed.failure);
|
|
2853
|
+
if (plan.order === "post")
|
|
2854
|
+
pending.push({ ...frame, listed: true });
|
|
2855
|
+
for (let index = listed.success.length - 1; index >= 0; index--) {
|
|
2856
|
+
const child = listed.success[index];
|
|
2857
|
+
if (child !== undefined)
|
|
2858
|
+
pending.push(child);
|
|
2859
|
+
}
|
|
2860
|
+
return [out, Option.some(undefined)];
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
return [out, Option.some(undefined)];
|
|
2864
|
+
});
|
|
2865
|
+
return Stream.paginate(undefined, () => step);
|
|
2866
|
+
});
|
|
2867
|
+
// Empties the directory an entry names, entries before their directories, each removal its own change. Each
|
|
2868
|
+
// entry is removed by its name in the directory the walk reached by name from the target's name, and only
|
|
2869
|
+
// while that name still holds the object the walk listed, so a subtree renamed out of the target, the target
|
|
2870
|
+
// renamed away, and a replacement created under a listed name are all left alone. It stops at the first
|
|
2871
|
+
// failure, which names the entry's path under the one the caller gave. Removing an empty directory reveals nothing, so only a directory with entries must be readable, and
|
|
2872
|
+
// no permission is changed to make one so. With `force`, the entry itself going missing leaves nothing to
|
|
2873
|
+
// empty.
|
|
2874
|
+
const emptyDirectory = Effect.fnUntraced(function* (prepared, op, force, guard) {
|
|
2875
|
+
const prefix = prepared.kind === "path" ? prepared.path.bytes : nameBytes(prepared.name);
|
|
2876
|
+
const locate = (path) => ownedPath(joinPath(prefix, path));
|
|
2877
|
+
const targetGuard = guard.pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.void));
|
|
2878
|
+
const listable = (directory, at) => Effect.andThen(targetGuard, directory.entries.size === 0 ? Effect.void : authorize(directory, identity, READ, at));
|
|
2879
|
+
let anchor;
|
|
2880
|
+
let targetAt = preparedOp(prepared, op);
|
|
2881
|
+
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
2882
|
+
yield* guard;
|
|
2883
|
+
const entry = yield* resolveEntry(prepared, op);
|
|
2884
|
+
const parent = yield* directoryNow(entry.parent);
|
|
2885
|
+
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
2886
|
+
const childIno = entry.name === undefined ? undefined : parent.entries.get(entry.name);
|
|
2887
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
2888
|
+
if (entry.name === undefined || child === undefined)
|
|
2889
|
+
return yield* entry.op.fail("NotFound");
|
|
2890
|
+
if (child.kind !== "directory")
|
|
2891
|
+
return [];
|
|
2892
|
+
yield* listable(child, entry.op);
|
|
2893
|
+
anchor = anchorFrame(parent, entry.name, child);
|
|
2894
|
+
targetAt = entry.op;
|
|
2895
|
+
return yield* walkChildren(child, new Uint8Array(0), 0, anchor);
|
|
2896
|
+
})).pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.succeed([])));
|
|
2897
|
+
const seams = yield* VolumeTestSeams;
|
|
2898
|
+
const removeFrame = (frame) => {
|
|
2899
|
+
const at = op.at(locate(frame.path));
|
|
2900
|
+
return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.gen(function* () {
|
|
2901
|
+
yield* targetGuard;
|
|
2902
|
+
if (anchor !== undefined && !holds(yield* view(anchor.parent), anchor)) {
|
|
2903
|
+
return yield* (force ? Effect.void : Effect.fail(targetAt.fail("NotFound")));
|
|
2904
|
+
}
|
|
2905
|
+
return yield* Effect.flatMap(frame.up === undefined ? view(frame.parent) : reachFrame(frame.up, at), (parent) => !holds(parent, frame)
|
|
2906
|
+
? Effect.fail(at.fail("NotFound"))
|
|
2907
|
+
: removeEntry({
|
|
2908
|
+
parent: parent.ino,
|
|
2909
|
+
name: frame.key,
|
|
2910
|
+
trailingSlash: false,
|
|
2911
|
+
addressing: "entry",
|
|
2912
|
+
op: at
|
|
2913
|
+
}, op));
|
|
2914
|
+
})));
|
|
2915
|
+
};
|
|
2916
|
+
const plan = { order: "post", limit: () => undefined };
|
|
2917
|
+
return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
|
|
2918
|
+
});
|
|
2919
|
+
const rootOp = OpContext.make("root");
|
|
2920
|
+
const entryVerb = Effect.fnUntraced(function* (operation, input, body) {
|
|
2921
|
+
const op = OpContext.make(operation);
|
|
2922
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
2923
|
+
return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
|
|
2924
|
+
});
|
|
2925
|
+
const openDispatch = Effect.fnUntraced(function* (input, options) {
|
|
2926
|
+
const op = OpContext.make("open");
|
|
2927
|
+
if (isEntry(input))
|
|
2928
|
+
return yield* openEntry(input, options, op);
|
|
2929
|
+
const target = asTarget(input);
|
|
2930
|
+
if (Target.$is("Path")(target))
|
|
2931
|
+
return yield* openPath(target, options, op);
|
|
2932
|
+
return yield* openNode(target, options, op);
|
|
2933
|
+
});
|
|
2934
|
+
function open(input, options) {
|
|
2935
|
+
return openDispatch(input, options);
|
|
2936
|
+
}
|
|
2937
|
+
const removeDispatch = Effect.fnUntraced(function* (input, options) {
|
|
2938
|
+
const op = OpContext.make("remove");
|
|
2939
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
2940
|
+
const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
|
|
2941
|
+
const imported = prepared.kind === "entry"
|
|
2942
|
+
? inheritedFor(prepared.directory)
|
|
2943
|
+
: prepared.base === undefined
|
|
2944
|
+
? []
|
|
2945
|
+
: registry.handleBoundaries(prepared.base);
|
|
2946
|
+
const guard = chosen.expected === undefined && imported.length === 0 ? Effect.void : Effect.gen(function* () {
|
|
2947
|
+
const entry = yield* resolveEntry(prepared, op);
|
|
2948
|
+
const parent = yield* directoryNow(entry.parent);
|
|
2949
|
+
if (protectsRoot(parent, entry.name))
|
|
2950
|
+
return yield* entry.op.fail("NotPermitted");
|
|
2951
|
+
if (chosen.expected === undefined)
|
|
2952
|
+
return;
|
|
2953
|
+
const expected = yield* registry.referenceIdentity(chosen.expected, op);
|
|
2954
|
+
const expectedNode = yield* view(expected);
|
|
2955
|
+
if (expectedNode !== undefined && expectedNode.metadata.nlink > 0)
|
|
2956
|
+
yield* checkAuthority(expectedNode, op);
|
|
2957
|
+
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
2958
|
+
const found = entry.name === undefined ? parent.ino : parent.entries.get(entry.name);
|
|
2959
|
+
if (found === undefined)
|
|
2960
|
+
return yield* entry.op.fail("NotFound");
|
|
2961
|
+
if (found !== expected)
|
|
2962
|
+
return yield* entry.op.fail("VolumeBusy", { field: "expected" });
|
|
2963
|
+
});
|
|
2964
|
+
const once = coordinated(op, Effect.andThen(guard, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op))));
|
|
2965
|
+
const target = (effect) => chosen.force === true
|
|
2966
|
+
? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
|
|
2967
|
+
: effect;
|
|
2968
|
+
const removed = yield* Effect.result(target(once));
|
|
2969
|
+
if (Result.isSuccess(removed))
|
|
2970
|
+
return removed.success;
|
|
2971
|
+
if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
|
|
2972
|
+
return yield* removed.failure;
|
|
2973
|
+
yield* emptyDirectory(prepared, op, chosen.force === true, guard);
|
|
2974
|
+
return yield* target(once);
|
|
2975
|
+
});
|
|
2976
|
+
function remove(input, options) {
|
|
2977
|
+
return removeDispatch(input, options);
|
|
2978
|
+
}
|
|
2979
|
+
const selectWatch = (input, decoded) => {
|
|
2980
|
+
const op = OpContext.make("watch");
|
|
2981
|
+
const target = asTarget(input);
|
|
2982
|
+
return Effect.gen(function* () {
|
|
2983
|
+
yield* checkAvailable("watch");
|
|
2984
|
+
const resolved = yield* resolveTarget(target, op);
|
|
2985
|
+
yield* authorize(resolved.node, identity, READ, resolved.op);
|
|
2986
|
+
if (resolved.node.metadata.nlink === 0)
|
|
2987
|
+
return yield* resolved.op.fail("NotFound");
|
|
2988
|
+
if (decoded.alias === "resolved" && resolved.kind === "node" && resolved.node.kind !== "directory") {
|
|
2989
|
+
return yield* op.fail("InvalidArgument");
|
|
2990
|
+
}
|
|
2991
|
+
const selection = {
|
|
2992
|
+
roots,
|
|
2993
|
+
additionalRoots: [...inherited, ...inheritedFor(target)],
|
|
2994
|
+
target: resolved.node.ino,
|
|
2995
|
+
recursive: decoded.recursive ?? true
|
|
2996
|
+
};
|
|
2997
|
+
return (decoded.alias === "resolved" && resolved.kind === "entry"
|
|
2998
|
+
? { ...selection, selected: { parent: resolved.parent.ino, name: resolved.name } }
|
|
2999
|
+
: selection);
|
|
3000
|
+
});
|
|
3001
|
+
};
|
|
3002
|
+
const operations = Object.freeze({
|
|
3003
|
+
limits,
|
|
3004
|
+
root: coordinatedRead(rootOp, Effect.suspend(() => !TokenRegistry.isOpen(reference)
|
|
3005
|
+
? Effect.fail(rootOp.fail("ClosedCaller"))
|
|
3006
|
+
: Effect.gen(function* () {
|
|
3007
|
+
const node = yield* view(boundary?.root ?? ROOT_INO);
|
|
3008
|
+
if (node === undefined)
|
|
3009
|
+
return yield* rootOp.fail("ClosedCaller");
|
|
3010
|
+
yield* checkAuthority(node, rootOp);
|
|
3011
|
+
return referenceFor(node.ino);
|
|
3012
|
+
}))),
|
|
3013
|
+
lookup: Effect.fnUntraced(function* (input) {
|
|
3014
|
+
const op = OpContext.make("lookup");
|
|
3015
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
3016
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
3017
|
+
const entry = yield* resolveEntry(prepared, op);
|
|
3018
|
+
const directory = yield* directoryNow(entry.parent);
|
|
3019
|
+
if (isDotComponent(entry.name)) {
|
|
3020
|
+
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "NotFound");
|
|
3021
|
+
}
|
|
3022
|
+
yield* authorize(directory, identity, EXECUTE, entry.op);
|
|
3023
|
+
const child = directory.entries.get(entry.name);
|
|
3024
|
+
if (child === undefined)
|
|
3025
|
+
return yield* entry.op.fail("NotFound");
|
|
3026
|
+
return referenceFor(child);
|
|
3027
|
+
}));
|
|
3028
|
+
}),
|
|
3029
|
+
parent: Effect.fnUntraced(function* (input) {
|
|
3030
|
+
const op = OpContext.make("parent");
|
|
3031
|
+
const target = asTarget(input);
|
|
3032
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
3033
|
+
const directory = yield* resolveDirectory(target, op);
|
|
3034
|
+
yield* authorize(directory.node, identity, EXECUTE, directory.op);
|
|
3035
|
+
return referenceFor(directory.node.ino === boundary?.root ||
|
|
3036
|
+
inheritedFor(target).some((chain) => chain.at(-1) === directory.node.ino)
|
|
3037
|
+
? directory.node.ino
|
|
3038
|
+
: directory.node.parent);
|
|
3039
|
+
}));
|
|
3040
|
+
}),
|
|
3041
|
+
stat: Effect.fnUntraced(function* (input) {
|
|
3042
|
+
const op = OpContext.make("stat");
|
|
3043
|
+
const target = asTarget(input);
|
|
3044
|
+
return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => withMetadata(resolved.node)));
|
|
3045
|
+
}),
|
|
3046
|
+
readDirectory: Effect.fnUntraced(function* (input) {
|
|
3047
|
+
const op = OpContext.make("readDirectory");
|
|
3048
|
+
const target = asTarget(input);
|
|
3049
|
+
return yield* accessing(op, Effect.gen(function* () {
|
|
3050
|
+
const directory = yield* resolveDirectory(target, op);
|
|
3051
|
+
yield* authorize(directory.node, identity, READ, directory.op);
|
|
3052
|
+
const value = Object.freeze(DirectoryEntries.toArray(directory.node.entries).map(([name, child]) => Object.freeze({ name: nameBytes(name), reference: referenceFor(child) })));
|
|
3053
|
+
const access = { node: directory.node, now: yield* timestamp(op) };
|
|
3054
|
+
return { value: Object.freeze({ value, revision: directory.node.revision }), access };
|
|
3055
|
+
}));
|
|
3056
|
+
}),
|
|
3057
|
+
walk: (input, options) => {
|
|
3058
|
+
const op = OpContext.make("walk");
|
|
3059
|
+
const target = asTarget(input);
|
|
3060
|
+
const prefix = Target.$is("Path")(target) ? Result.getOrUndefined(inputBytes(target.path)) : undefined;
|
|
3061
|
+
const locate = (path) => ownedPath(prefix === undefined ? path : joinPath(prefix, path));
|
|
3062
|
+
const authorities = inheritedFor(target).map((chain) => Confinement.make(authorityView, chain));
|
|
3063
|
+
const readable = Effect.fnUntraced(function* (directory, at) {
|
|
3064
|
+
for (const authority of authorities)
|
|
3065
|
+
yield* authority.check(directory, at, true);
|
|
3066
|
+
yield* authorize(directory, identity, READ, at);
|
|
3067
|
+
});
|
|
3068
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
3069
|
+
const chosen = yield* decodeWalkOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(Target.$is("Path")(target) ? op.at(target.path) : op, cause)));
|
|
3070
|
+
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
3071
|
+
const directory = yield* resolveDirectory(target, op);
|
|
3072
|
+
yield* authorize(directory.node, identity, READ, directory.op);
|
|
3073
|
+
return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
|
|
3074
|
+
}));
|
|
3075
|
+
const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
|
|
3076
|
+
const maxBytes = chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes);
|
|
3077
|
+
const plan = {
|
|
3078
|
+
order: chosen.order ?? "pre",
|
|
3079
|
+
limit: (frame, progress, phase) => {
|
|
3080
|
+
if (chosen.maxDepth !== undefined && frame.depth > chosen.maxDepth)
|
|
3081
|
+
return exceeded(frame, "maxDepth");
|
|
3082
|
+
if (phase === "directory")
|
|
3083
|
+
return undefined;
|
|
3084
|
+
if (chosen.maxEntries !== undefined && progress.entries > chosen.maxEntries) {
|
|
3085
|
+
return exceeded(frame, "maxEntries");
|
|
3086
|
+
}
|
|
3087
|
+
return maxBytes !== undefined && progress.bytes > maxBytes ? exceeded(frame, "maxBytes") : undefined;
|
|
3088
|
+
}
|
|
3089
|
+
};
|
|
3090
|
+
return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
|
|
3091
|
+
path: ownedPath(frame.path),
|
|
3092
|
+
name: frame.name,
|
|
3093
|
+
reference: frame.reference,
|
|
3094
|
+
directory: frame.directory,
|
|
3095
|
+
kind: frame.kind,
|
|
3096
|
+
depth: frame.depth
|
|
3097
|
+
}));
|
|
3098
|
+
}));
|
|
3099
|
+
},
|
|
3100
|
+
readLink: Effect.fnUntraced(function* (input) {
|
|
3101
|
+
const op = OpContext.make("readLink");
|
|
3102
|
+
const target = asTarget(input);
|
|
3103
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
3104
|
+
const resolved = yield* resolveTarget(target, op, { followFinalSymlink: false, final: true });
|
|
3105
|
+
if (resolved.node.kind !== "symlink")
|
|
3106
|
+
return yield* resolved.op.fail("InvalidArgument");
|
|
3107
|
+
return new Uint8Array(resolved.node.target);
|
|
3108
|
+
}));
|
|
3109
|
+
}),
|
|
3110
|
+
realPath: Effect.fnUntraced(function* (input) {
|
|
3111
|
+
const op = OpContext.make("realPath");
|
|
3112
|
+
const target = asTarget(input);
|
|
3113
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
3114
|
+
if (Target.$is("Path")(target)) {
|
|
3115
|
+
const pathOp = op.at(target.path);
|
|
3116
|
+
const result = yield* resolveTarget(target, op, { final: true });
|
|
3117
|
+
const directory = result.node.kind === "directory"
|
|
3118
|
+
? result.node
|
|
3119
|
+
: result.kind === "entry"
|
|
3120
|
+
? result.parent
|
|
3121
|
+
: undefined;
|
|
3122
|
+
const prefix = directory === undefined ? SLASH_HEX : yield* callerPath(directory.ino);
|
|
3123
|
+
if (prefix === undefined)
|
|
3124
|
+
return yield* pathOp.fail("NotFound");
|
|
3125
|
+
if (result.node.kind === "directory" || result.kind === "node")
|
|
3126
|
+
return ownedPath(nameBytes(prefix));
|
|
3127
|
+
return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
|
|
3128
|
+
}
|
|
3129
|
+
const resolved = yield* resolveTarget(target, op);
|
|
3130
|
+
const path = yield* callerPath(resolved.node.ino);
|
|
3131
|
+
if (path === undefined)
|
|
3132
|
+
return yield* resolved.op.fail("NotFound");
|
|
3133
|
+
return ownedPath(nameBytes(path));
|
|
3134
|
+
}));
|
|
3135
|
+
}),
|
|
3136
|
+
access: Effect.fnUntraced(function* (input, bits = 0) {
|
|
3137
|
+
const op = OpContext.make("access");
|
|
3138
|
+
const target = asTarget(input);
|
|
3139
|
+
if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
|
|
3140
|
+
return yield* (Target.$is("Path")(target) ? op.at(target.path) : op).fail("InvalidArgument");
|
|
3141
|
+
}
|
|
3142
|
+
return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => {
|
|
3143
|
+
const node = resolved.node;
|
|
3144
|
+
let granted = 0;
|
|
3145
|
+
for (const bit of [READ, WRITE, EXECUTE]) {
|
|
3146
|
+
if ((bits & bit) === 0)
|
|
3147
|
+
continue;
|
|
3148
|
+
if (bit === EXECUTE && node.kind === "file" && (node.metadata.mode & ANY_EXECUTE) === 0)
|
|
3149
|
+
continue;
|
|
3150
|
+
if (identity.privileged || (permitted(node, identity) & bit) !== 0)
|
|
3151
|
+
granted |= bit;
|
|
3152
|
+
}
|
|
3153
|
+
return granted;
|
|
3154
|
+
}));
|
|
3155
|
+
}),
|
|
3156
|
+
readFile: Effect.fnUntraced(function* (input) {
|
|
3157
|
+
const op = OpContext.make("readFile");
|
|
3158
|
+
const target = asTarget(input);
|
|
3159
|
+
return yield* accessing(op, Effect.gen(function* () {
|
|
3160
|
+
const resolved = yield* resolveTarget(target, op);
|
|
3161
|
+
const node = resolved.node;
|
|
3162
|
+
if (node.kind === "symlink")
|
|
3163
|
+
return yield* resolved.op.fail("SymlinkLoop");
|
|
3164
|
+
if (node.kind !== "file")
|
|
3165
|
+
return yield* resolved.op.fail("IsDirectory");
|
|
3166
|
+
yield* authorize(node, identity, READ, resolved.op);
|
|
3167
|
+
const data = new Uint8Array(node.data);
|
|
3168
|
+
return { value: data, access: { node, now: yield* timestamp(op) } };
|
|
3169
|
+
}));
|
|
3170
|
+
}),
|
|
3171
|
+
writeFile: dual(3, Effect.fnUntraced(function* (input, bytes, options) {
|
|
3172
|
+
const op = OpContext.make("writeFile");
|
|
3173
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
3174
|
+
const optionsOp = preparedOp(prepared, op);
|
|
3175
|
+
if (!isAttachedBytes(bytes))
|
|
3176
|
+
return yield* optionsOp.fail("InvalidArgument");
|
|
3177
|
+
const captured = new Uint8Array(bytes);
|
|
3178
|
+
const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
|
|
3179
|
+
return yield* coordinated(op, Effect.gen(function* () {
|
|
3180
|
+
const resolved = yield* resolver.resolve(prepared, {
|
|
3181
|
+
kind: "OrCreate",
|
|
3182
|
+
create: chosen.create ?? "never",
|
|
3183
|
+
finalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false
|
|
3184
|
+
? "follow"
|
|
3185
|
+
: "preserve",
|
|
3186
|
+
entryExclusive: "afterSymlink"
|
|
3187
|
+
}, op);
|
|
3188
|
+
const { parent, name, trailingSlash, op: entryOp } = resolved;
|
|
3189
|
+
const found = resolved.kind === "existing" ? resolved.node : undefined;
|
|
3190
|
+
if (found?.kind === "directory" || (found === undefined && trailingSlash)) {
|
|
3191
|
+
return yield* entryOp.fail("IsDirectory");
|
|
3192
|
+
}
|
|
3193
|
+
const replaced = found?.kind === "symlink" ? found : undefined;
|
|
3194
|
+
if (replaced !== undefined && !chosen.replaceFinalSymlink)
|
|
3195
|
+
return yield* entryOp.fail("SymlinkLoop");
|
|
3196
|
+
if (chosen.access === "read")
|
|
3197
|
+
return yield* entryOp.fail("InvalidHandle");
|
|
3198
|
+
const file = found?.kind === "file" ? found : undefined;
|
|
3199
|
+
if (file === undefined) {
|
|
3200
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, entryOp);
|
|
3201
|
+
if (replaced !== undefined)
|
|
3202
|
+
yield* authorizeRemoval(parent, replaced, entryOp);
|
|
3203
|
+
yield* (replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp));
|
|
3204
|
+
}
|
|
3205
|
+
else {
|
|
3206
|
+
yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, entryOp);
|
|
3207
|
+
}
|
|
3208
|
+
const finalMode = chosen.finalMode === undefined ? undefined : yield* permittedMode(file?.metadata ?? { kind: "file", uid: identity.uid, gid: parent.metadata.gid }, chosen.finalMode, entryOp);
|
|
3209
|
+
const previous = file?.data.length ?? 0;
|
|
3210
|
+
const initial = chosen.truncate ? 0 : previous;
|
|
3211
|
+
const position = chosen.append ? initial : 0;
|
|
3212
|
+
const size = Math.max(initial, position + captured.length);
|
|
3213
|
+
if (size > maxFileBytes)
|
|
3214
|
+
return yield* entryOp.fail("FileTooLarge");
|
|
3215
|
+
const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
|
|
3216
|
+
yield* reserveBytes(entryOp, BigInt(size - previous) - BigInt(reclaimed));
|
|
3217
|
+
if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
|
|
3218
|
+
return referenceFor(file.ino);
|
|
3219
|
+
}
|
|
3220
|
+
let data = captured;
|
|
3221
|
+
if (position !== 0 || size !== captured.length) {
|
|
3222
|
+
data = new Uint8Array(size);
|
|
3223
|
+
if (file !== undefined && !chosen.truncate)
|
|
3224
|
+
data.set(file.data);
|
|
3225
|
+
data.set(captured, position);
|
|
3226
|
+
}
|
|
3227
|
+
const now = yield* timestamp(op);
|
|
3228
|
+
const d = yield* Draft;
|
|
3229
|
+
const node = file ??
|
|
3230
|
+
(yield* newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now));
|
|
3231
|
+
const written = {
|
|
3232
|
+
...node,
|
|
3233
|
+
data,
|
|
3234
|
+
metadata: {
|
|
3235
|
+
...node.metadata,
|
|
3236
|
+
mode: finalMode ?? node.metadata.mode & ~SET_ID_BITS,
|
|
3237
|
+
size: BigInt(size),
|
|
3238
|
+
mtimeNs: now,
|
|
3239
|
+
ctimeNs: now
|
|
3240
|
+
}
|
|
3241
|
+
};
|
|
3242
|
+
yield* d.addUsedBytes(BigInt(size - previous));
|
|
3243
|
+
if (file === undefined) {
|
|
3244
|
+
if (replaced !== undefined)
|
|
3245
|
+
yield* detach(replaced, parent.ino, name, now);
|
|
3246
|
+
yield* attach(yield* directoryNow(parent.ino), name, written, now);
|
|
3247
|
+
if (replaced === undefined)
|
|
3248
|
+
yield* d.addEntries(1);
|
|
3249
|
+
yield* publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
|
|
3250
|
+
}
|
|
3251
|
+
else {
|
|
3252
|
+
yield* d.put(written);
|
|
3253
|
+
yield* publishNode(written.ino);
|
|
3254
|
+
}
|
|
3255
|
+
return referenceFor(written.ino);
|
|
3256
|
+
}));
|
|
3257
|
+
})),
|
|
3258
|
+
open,
|
|
3259
|
+
mkdir: Effect.fnUntraced(function* (input, options = {}) {
|
|
3260
|
+
const op = OpContext.make("mkdir");
|
|
3261
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
3262
|
+
const optionsOp = preparedOp(prepared, op);
|
|
3263
|
+
const decoded = yield* decodeMkdirOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
|
|
3264
|
+
const chosen = { ...decoded };
|
|
3265
|
+
if (chosen.exactMode && chosen.mode === undefined)
|
|
3266
|
+
return yield* optionsOp.fail("InvalidArgument");
|
|
3267
|
+
return yield* coordinated(op, Effect.gen(function* () {
|
|
3268
|
+
const request = { mode: chosen.mode ?? 0o777, exactMode: chosen.exactMode, times: chosen.times };
|
|
3269
|
+
const { child, directory } = chosen.recursive === true
|
|
3270
|
+
? yield* makeDirectories(prepared, request, op)
|
|
3271
|
+
: yield* makeDirectory(yield* resolveEntry(prepared, op), request, op);
|
|
3272
|
+
return { reference: referenceFor(child), directory };
|
|
3273
|
+
}));
|
|
3274
|
+
}),
|
|
3275
|
+
symlink: Effect.fnUntraced(function* (target, input, options = {}) {
|
|
3276
|
+
const op = OpContext.make("symlink");
|
|
3277
|
+
const targetOp = op.at(target);
|
|
3278
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
3279
|
+
const optionsOp = preparedOp(prepared, op);
|
|
3280
|
+
const decoded = yield* decodeSymlinkOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
|
|
3281
|
+
const rawTarget = inputBytes(target);
|
|
3282
|
+
if (Result.isFailure(rawTarget))
|
|
3283
|
+
return yield* targetOp.fail(rawTarget.failure);
|
|
3284
|
+
if (rawTarget.success.includes(0))
|
|
3285
|
+
return yield* targetOp.fail("InvalidArgument");
|
|
3286
|
+
const targetBytes = new Uint8Array(rawTarget.success);
|
|
3287
|
+
const chosen = { ...decoded };
|
|
3288
|
+
return yield* coordinated(op, Effect.gen(function* () {
|
|
3289
|
+
const entry = yield* resolveEntry(prepared, op);
|
|
3290
|
+
const { child, directory } = yield* makeSymlink(entry, targetBytes, chosen.times, op);
|
|
3291
|
+
return { reference: referenceFor(child), directory };
|
|
3292
|
+
}));
|
|
3293
|
+
}),
|
|
3294
|
+
link: dual(2, Effect.fnUntraced(function* (sourceInput, input) {
|
|
3295
|
+
const op = OpContext.make("link");
|
|
3296
|
+
const source = asTarget(sourceInput);
|
|
3297
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
3298
|
+
return yield* coordinated(op, Effect.gen(function* () {
|
|
3299
|
+
const resolved = yield* resolveTarget(source, op, { followFinalSymlink: false });
|
|
3300
|
+
const node = resolved.node;
|
|
3301
|
+
if (node.kind === "directory")
|
|
3302
|
+
return yield* resolved.op.fail("IsDirectory");
|
|
3303
|
+
if (node.metadata.nlink === 0)
|
|
3304
|
+
return yield* resolved.op.fail("StaleReference");
|
|
3305
|
+
const entry = yield* resolveEntry(prepared, op);
|
|
3306
|
+
const directory = yield* linkNode(node, entry, op);
|
|
3307
|
+
return { reference: referenceFor(node.ino), directory };
|
|
3308
|
+
}));
|
|
3309
|
+
})),
|
|
3310
|
+
unlink: Effect.fnUntraced(function* (input) {
|
|
3311
|
+
return yield* entryVerb("unlink", input, unlinkEntry);
|
|
3312
|
+
}),
|
|
3313
|
+
rmdir: Effect.fnUntraced(function* (input) {
|
|
3314
|
+
return yield* entryVerb("rmdir", input, rmdirEntry);
|
|
3315
|
+
}),
|
|
3316
|
+
remove,
|
|
3317
|
+
rename: dual(2, Effect.fnUntraced(function* (fromInput, toInput) {
|
|
3318
|
+
const op = OpContext.make("rename");
|
|
3319
|
+
const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
|
|
3320
|
+
const to = yield* Effect.fromResult(prepareEntry(toInput, op));
|
|
3321
|
+
return yield* coordinated(op, Effect.gen(function* () {
|
|
3322
|
+
const source = yield* resolveEntry(from, op);
|
|
3323
|
+
const destination = yield* resolveEntry(to, op);
|
|
3324
|
+
return yield* renameEntry(source, destination, op);
|
|
3325
|
+
}));
|
|
3326
|
+
})),
|
|
3327
|
+
chmod: dual(2, Effect.fnUntraced(function* (input, mode) {
|
|
3328
|
+
const op = OpContext.make("chmod");
|
|
3329
|
+
const target = asTarget(input);
|
|
3330
|
+
if (!isMode(mode))
|
|
3331
|
+
return yield* op.fail("InvalidArgument");
|
|
3332
|
+
yield* changeAttributes(() => asResolvedNode(target, op), { mode }, op);
|
|
3333
|
+
})),
|
|
3334
|
+
chown: dual(2, Effect.fnUntraced(function* (input, owner) {
|
|
3335
|
+
const op = OpContext.make("chown");
|
|
3336
|
+
const target = asTarget(input);
|
|
3337
|
+
const decoded = yield* decodeOwnerUpdate(owner).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
3338
|
+
yield* changeAttributes(() => asResolvedNode(target, op), { owner: decoded }, op);
|
|
3339
|
+
})),
|
|
3340
|
+
utimes: dual(2, Effect.fnUntraced(function* (input, times) {
|
|
3341
|
+
const op = OpContext.make("utimes");
|
|
3342
|
+
const target = asTarget(input);
|
|
3343
|
+
const decoded = yield* decodeTimes(times).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
3344
|
+
yield* changeAttributes(() => asResolvedNode(target, op), { times: decoded }, op);
|
|
3345
|
+
})),
|
|
3346
|
+
// A negative length fails before the target resolves, as truncate(2) rejects it before the lookup.
|
|
3347
|
+
truncate: dual(2, Effect.fnUntraced(function* (input, length) {
|
|
3348
|
+
const op = OpContext.make("truncate");
|
|
3349
|
+
const target = asTarget(input);
|
|
3350
|
+
if (!isLength(length))
|
|
3351
|
+
return yield* op.fail("InvalidArgument");
|
|
3352
|
+
yield* changeAttributes(() => asResolvedNode(target, op), { size: length }, op);
|
|
3353
|
+
})),
|
|
3354
|
+
setattr: dual(2, Effect.fnUntraced(function* (input, attributes) {
|
|
3355
|
+
const op = OpContext.make("setattr");
|
|
3356
|
+
const target = asTarget(input);
|
|
3357
|
+
// A caller outside TypeScript can pass anything, so a non-object fails typed rather than as a defect;
|
|
3358
|
+
// the check reads a widened copy so the attributes keep their type below.
|
|
3359
|
+
const raw = attributes;
|
|
3360
|
+
if (!Predicate.isObject(raw))
|
|
3361
|
+
return yield* fail(op);
|
|
3362
|
+
const unknown = Object.keys(attributes).find((key) => !SETATTR_FIELDS.includes(key));
|
|
3363
|
+
if (unknown !== undefined)
|
|
3364
|
+
return yield* fail(op, undefined, unknown);
|
|
3365
|
+
if (attributes.size !== undefined && !isLength(attributes.size))
|
|
3366
|
+
return yield* fail(op, undefined, "size");
|
|
3367
|
+
if (attributes.mode !== undefined && !isMode(attributes.mode))
|
|
3368
|
+
return yield* fail(op, undefined, "mode");
|
|
3369
|
+
const owner = attributes.owner === undefined
|
|
3370
|
+
? undefined
|
|
3371
|
+
: yield* decodeOwnerUpdate(attributes.owner).pipe(Effect.mapError((cause) => fail(op, cause, "owner")));
|
|
3372
|
+
const times = attributes.times === undefined
|
|
3373
|
+
? undefined
|
|
3374
|
+
: yield* decodeTimes(attributes.times).pipe(Effect.mapError((cause) => fail(op, cause, "times")));
|
|
3375
|
+
const expected = attributes.expected === undefined
|
|
3376
|
+
? undefined
|
|
3377
|
+
: yield* decodeExpected(attributes.expected).pipe(Effect.mapError((cause) => fail(op, cause, "expected")));
|
|
3378
|
+
yield* changeAttributes(() => asResolvedNode(target, op), { size: attributes.size, mode: attributes.mode, owner, times, expected }, op);
|
|
3379
|
+
})),
|
|
3380
|
+
openDirectory: Effect.fnUntraced(function* (input) {
|
|
3381
|
+
const acquired = yield* acquireDirectory(input, OpContext.make("openDirectory"));
|
|
3382
|
+
const statOp = OpContext.make("stat");
|
|
3383
|
+
const handle = Object.freeze({
|
|
3384
|
+
[DirectoryHandleId]: true,
|
|
3385
|
+
stat: coordinatedRead(statOp, Effect.gen(function* () {
|
|
3386
|
+
const ino = TokenRegistry.inode(acquired);
|
|
3387
|
+
const node = !TokenRegistry.isOpen(acquired) || ino === undefined ? undefined : yield* view(ino);
|
|
3388
|
+
if (node !== undefined && acquired.authority !== undefined)
|
|
3389
|
+
yield* acquired.authority(node, statOp);
|
|
3390
|
+
return yield* (node === undefined
|
|
3391
|
+
? Effect.fail(statOp.fail("InvalidHandle"))
|
|
3392
|
+
: Effect.succeed(withMetadata(node)));
|
|
3393
|
+
})),
|
|
3394
|
+
close: coordinatedCleanup(Effect.suspend(() => {
|
|
3395
|
+
if (TokenRegistry.inode(acquired) === undefined) {
|
|
3396
|
+
return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
|
|
3397
|
+
}
|
|
3398
|
+
return releaseDirectory(acquired);
|
|
3399
|
+
})).pipe(Effect.mapError((error) => error.code === "VolumeBusy" ? OpContext.make("close").fail("VolumeBusy") : error), Effect.ensuring(closeReleasedScope(acquired)))
|
|
3400
|
+
});
|
|
3401
|
+
registry.registerDirectory(handle, acquired);
|
|
3402
|
+
return handle;
|
|
3403
|
+
})
|
|
3404
|
+
});
|
|
3405
|
+
return {
|
|
3406
|
+
operations,
|
|
3407
|
+
selectWatch,
|
|
3408
|
+
withRoot: Effect.fnUntraced(function* (input) {
|
|
3409
|
+
const acquired = yield* acquireDirectory(input, OpContext.make("withRoot"));
|
|
3410
|
+
return createContext(acquired, identity, umask, [...roots, TokenRegistry.inode(acquired)], acquired.boundaries ?? []);
|
|
3411
|
+
}),
|
|
3412
|
+
withDirectory: Effect.fnUntraced(function* (input) {
|
|
3413
|
+
const acquired = yield* acquireDirectory(input, OpContext.make("withDirectory"));
|
|
3414
|
+
return createContext(acquired, identity, umask, acquired.namespaceRoots ?? roots, acquired.boundaries ?? []);
|
|
3415
|
+
})
|
|
3416
|
+
};
|
|
3417
|
+
};
|
|
3418
|
+
return Object.freeze({
|
|
3419
|
+
limits,
|
|
3420
|
+
shutdown,
|
|
3421
|
+
usage: coordinatedRead(OpContext.make("usage"), Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries }))),
|
|
3422
|
+
context: Effect.fnUntraced(function* (options) {
|
|
3423
|
+
const decoded = yield* Effect.fromResult(decodeConfiguration(RootCallerOptions, options === undefined ? {} : options, "caller"));
|
|
3424
|
+
const chosen = decoded.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
|
|
3425
|
+
const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
|
|
3426
|
+
return yield* coordinatedRead(OpContext.make("caller"), Effect.sync(() => createContext(makeDirectoryReference(ROOT_INO), identity, decoded.umask ?? 0o022)));
|
|
3427
|
+
}),
|
|
3428
|
+
coordinate: changing,
|
|
3429
|
+
admit,
|
|
3430
|
+
checkAvailable,
|
|
3431
|
+
execution,
|
|
3432
|
+
syncBinding: synchronous ?
|
|
3433
|
+
Object.freeze({
|
|
3434
|
+
check: (operation) => active
|
|
3435
|
+
? Result.fail(fsFailure("VolumeBusy", operation))
|
|
3436
|
+
: Result.succeed(undefined)
|
|
3437
|
+
}) :
|
|
3438
|
+
undefined,
|
|
3439
|
+
preflight,
|
|
3440
|
+
capture: (operation) => coordinatedRead(OpContext.make(operation), Effect.sync(() => state)),
|
|
3441
|
+
cooperativeCapture: synchronous ?
|
|
3442
|
+
undefined :
|
|
3443
|
+
(operation, read) => coordinatedRead(OpContext.make(operation), Effect.suspend(() => read(state))),
|
|
3444
|
+
referenceInode: (reference, op) => coordinatedRead(op, Effect.gen(function* () {
|
|
3445
|
+
return (yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op))
|
|
3446
|
+
.ino;
|
|
3447
|
+
})),
|
|
3448
|
+
objectFor: (resolveInode, op) => coordinatedRead(op, Effect.gen(function* () {
|
|
3449
|
+
const ino = yield* resolveInode;
|
|
3450
|
+
yield* TokenRegistry.nodeOrFail(yield* registry.resolveInode(ino), "reference", op);
|
|
3451
|
+
return referenceFor(ino);
|
|
3452
|
+
})),
|
|
3453
|
+
watchRoot: Effect.fnUntraced(function* (reference, op) {
|
|
3454
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op);
|
|
3455
|
+
return node.metadata.nlink === 0 ? yield* op.fail("StaleReference") : node.ino;
|
|
3456
|
+
})
|
|
3457
|
+
});
|
|
3458
|
+
});
|