@effect-vfs/core 0.8.0 → 0.9.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/Caller.d.ts +2 -0
- package/dist/Caller.d.ts.map +1 -1
- package/dist/Caller.js +3 -1
- package/dist/LiveVolume.d.ts +69 -1
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +80 -3
- package/dist/Search.d.ts.map +1 -1
- package/dist/Search.js +12 -4
- package/dist/VfsError.d.ts +3 -1
- package/dist/VfsError.d.ts.map +1 -1
- package/dist/VfsError.js +3 -1
- package/dist/VirtualFileSystem.d.ts +25 -1
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +10 -4
- package/dist/Watch.d.ts +18 -0
- package/dist/Watch.d.ts.map +1 -1
- package/dist/Watch.js +12 -0
- package/dist/internal/callerWatch.d.ts +32 -0
- package/dist/internal/callerWatch.d.ts.map +1 -0
- package/dist/internal/callerWatch.js +184 -0
- package/dist/internal/confinement.d.ts +16 -0
- package/dist/internal/confinement.d.ts.map +1 -0
- package/dist/internal/confinement.js +79 -0
- package/dist/internal/directoryEntries.d.ts +6 -0
- package/dist/internal/directoryEntries.d.ts.map +1 -0
- package/dist/internal/directoryEntries.js +79 -0
- package/dist/internal/fixture.js +1 -1
- package/dist/internal/path.js +2 -2
- package/dist/internal/resolution.d.ts +8 -4
- package/dist/internal/resolution.d.ts.map +1 -1
- package/dist/internal/resolution.js +68 -13
- package/dist/internal/tokenRegistry.d.ts +6 -0
- package/dist/internal/tokenRegistry.d.ts.map +1 -1
- package/dist/internal/tokenRegistry.js +21 -3
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +245 -80
- package/dist/internal/watchHub.d.ts +6 -2
- package/dist/internal/watchHub.d.ts.map +1 -1
- package/dist/internal/watchHub.js +31 -6
- package/package.json +5 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callerWatch.d.ts","sourceRoot":"","sources":["../../src/internal/callerWatch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAIpC,OAAO,EAAW,KAAK,GAAG,EAAE,KAAK,IAAI,EAAY,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC3F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C,gBAAgB;AAChB,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAA;IACpB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;IACjB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,gBAAgB;AAChB,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;IACjB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;IACnB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAClB;AAED,gBAAgB;AAChB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACzC;AA6ED,iGAAiG;AACjG,eAAO,MAAM,IAAI,YAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,CAAA;IAClC,QAAQ,CAAC,eAAe,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAA;IAC5D,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAA;IACpB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAA;CACzB,KAAG,SAAS,CAAC,UAAU,EAAE,YAAY,EAAE,SAAS,CA6IhD,CAAA"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as Predicate from "effect/Predicate";
|
|
2
|
+
import { Change } from "../Watch.js";
|
|
3
|
+
import { getBytes } from "./bytePath.js";
|
|
4
|
+
import { fsFailure } from "./errors.js";
|
|
5
|
+
import { nameBytes, ownedPath, SLASH_HEX } from "./path.js";
|
|
6
|
+
import { getNode, ROOT_INO } from "./volumeState.js";
|
|
7
|
+
const directoryPath = (state, ino) => {
|
|
8
|
+
const names = [];
|
|
9
|
+
const visited = new Set();
|
|
10
|
+
let at = ino;
|
|
11
|
+
for (;;) {
|
|
12
|
+
if (visited.has(at))
|
|
13
|
+
return undefined;
|
|
14
|
+
visited.add(at);
|
|
15
|
+
const directory = getNode(state, at);
|
|
16
|
+
if (directory?.kind !== "directory" || directory.metadata.nlink === 0)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (at === ROOT_INO)
|
|
19
|
+
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
20
|
+
names.push(directory.name);
|
|
21
|
+
at = directory.parent;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const below = (path, root) => root === SLASH_HEX || path === root || path.startsWith(root + SLASH_HEX);
|
|
25
|
+
const rebase = (path, root) => root === SLASH_HEX ? path : path === root ? SLASH_HEX : path.slice(root.length);
|
|
26
|
+
const entryPath = (state, link) => {
|
|
27
|
+
const parent = directoryPath(state, link.parent);
|
|
28
|
+
return parent === undefined ? undefined : parent + (parent === SLASH_HEX ? "" : SLASH_HEX) + link.name;
|
|
29
|
+
};
|
|
30
|
+
const paths = (state, target) => {
|
|
31
|
+
const node = getNode(state, target);
|
|
32
|
+
if (node === undefined || node.metadata.nlink === 0)
|
|
33
|
+
return [];
|
|
34
|
+
if (node.kind === "directory") {
|
|
35
|
+
const path = directoryPath(state, target);
|
|
36
|
+
return path === undefined ? [] : [path];
|
|
37
|
+
}
|
|
38
|
+
return node.links.flatMap((link) => {
|
|
39
|
+
const directory = getNode(state, link.parent);
|
|
40
|
+
const path = directory?.kind === "directory" && directory.entries.get(link.name) === target
|
|
41
|
+
? entryPath(state, link)
|
|
42
|
+
: undefined;
|
|
43
|
+
return path === undefined ? [] : [path];
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const rootPath = (state, roots) => {
|
|
47
|
+
let previous = SLASH_HEX;
|
|
48
|
+
for (const root of roots) {
|
|
49
|
+
const path = directoryPath(state, root);
|
|
50
|
+
if (path === undefined)
|
|
51
|
+
return fsFailure("ClosedCaller", "watch");
|
|
52
|
+
if (!below(path, previous))
|
|
53
|
+
return fsFailure("AccessDenied", "watch");
|
|
54
|
+
previous = path;
|
|
55
|
+
}
|
|
56
|
+
return previous;
|
|
57
|
+
};
|
|
58
|
+
const eventName = (event) => {
|
|
59
|
+
if (event.name !== undefined)
|
|
60
|
+
return event.name;
|
|
61
|
+
const bytes = getBytes(event.change.path);
|
|
62
|
+
const last = bytes.lastIndexOf(47);
|
|
63
|
+
return Array.from(bytes.subarray(last + 1), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
64
|
+
};
|
|
65
|
+
/** Select and rebase names while publication holds both installed namespace states. @internal */
|
|
66
|
+
export const make = (options) => {
|
|
67
|
+
let selected = options.selected;
|
|
68
|
+
let oldSelected = selected;
|
|
69
|
+
let failure;
|
|
70
|
+
let previousRoot = SLASH_HEX;
|
|
71
|
+
let currentRoot = SLASH_HEX;
|
|
72
|
+
let terminal;
|
|
73
|
+
let rescanPath = SLASH_HEX;
|
|
74
|
+
const visible = (state, root, selection) => {
|
|
75
|
+
const aliases = paths(state, options.target);
|
|
76
|
+
for (const chain of options.additionalRoots ?? []) {
|
|
77
|
+
const boundary = rootPath(state, chain);
|
|
78
|
+
if (!Predicate.isString(boundary) || !aliases.some((path) => below(path, boundary)))
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return aliases.filter((path) => below(path, root) && (selection === undefined || path === entryPath(state, selection)));
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
boundedTerminal: true,
|
|
85
|
+
prepare: ({ before, after, renames }) => {
|
|
86
|
+
terminal = undefined;
|
|
87
|
+
oldSelected = selected;
|
|
88
|
+
const beforeRoot = rootPath(before, options.roots);
|
|
89
|
+
const afterRoot = rootPath(after, options.roots);
|
|
90
|
+
for (const chain of options.additionalRoots ?? []) {
|
|
91
|
+
const boundary = rootPath(after, chain);
|
|
92
|
+
if (!Predicate.isString(boundary)) {
|
|
93
|
+
failure = boundary;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!Predicate.isString(afterRoot)) {
|
|
98
|
+
failure = afterRoot;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!Predicate.isString(beforeRoot))
|
|
102
|
+
return;
|
|
103
|
+
previousRoot = beforeRoot;
|
|
104
|
+
currentRoot = afterRoot;
|
|
105
|
+
for (const rename of renames ?? []) {
|
|
106
|
+
if (rename.ino === options.target && selected !== undefined && selected.parent === rename.from.parent &&
|
|
107
|
+
selected.name === rename.from.name)
|
|
108
|
+
selected = rename.to;
|
|
109
|
+
}
|
|
110
|
+
const oldPaths = visible(before, previousRoot, oldSelected);
|
|
111
|
+
const nextPaths = visible(after, currentRoot, selected);
|
|
112
|
+
rescanPath = rebase(nextPaths[0] ?? oldPaths[0] ?? currentRoot, nextPaths.length > 0 ? currentRoot : previousRoot);
|
|
113
|
+
if (nextPaths.length === 0)
|
|
114
|
+
terminal = oldPaths.map((path) => rebase(path, previousRoot));
|
|
115
|
+
},
|
|
116
|
+
includes: () => failure === undefined,
|
|
117
|
+
project: (event, { before, after }) => {
|
|
118
|
+
if (failure !== undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
if (terminal === undefined && event.ino !== options.target && Predicate.isTagged(event.change, "Create")) {
|
|
121
|
+
const name = eventName(event);
|
|
122
|
+
const parent = getNode(before, event.parent);
|
|
123
|
+
const replacedPath = entryPath(before, { parent: event.parent, name });
|
|
124
|
+
// Rename publishes the incoming object; its replaced alias needs the old target's removal.
|
|
125
|
+
if (parent?.kind === "directory" && parent.entries.get(name) === options.target &&
|
|
126
|
+
replacedPath !== undefined && visible(before, previousRoot, oldSelected).includes(replacedPath)) {
|
|
127
|
+
return {
|
|
128
|
+
...event,
|
|
129
|
+
ino: options.target,
|
|
130
|
+
change: Change.cases.Remove.make({ path: ownedPath(nameBytes(rebase(replacedPath, previousRoot))) })
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const removed = Predicate.isTagged(event.change, "Remove");
|
|
135
|
+
const installed = removed ? before : after;
|
|
136
|
+
const root = removed ? previousRoot : currentRoot;
|
|
137
|
+
const selection = removed ? oldSelected : selected;
|
|
138
|
+
let path;
|
|
139
|
+
const node = getNode(installed, event.ino);
|
|
140
|
+
if (event.ino === options.target) {
|
|
141
|
+
// Settlement emits the terminal removals once, behind already queued changes.
|
|
142
|
+
if (removed && terminal !== undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
path = node?.kind === "directory"
|
|
145
|
+
? directoryPath(installed, event.ino)
|
|
146
|
+
: entryPath(installed, { parent: event.parent, name: eventName(event) });
|
|
147
|
+
if (selection !== undefined && path !== entryPath(installed, selection))
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const scopes = visible(installed, root, selection);
|
|
152
|
+
path = entryPath(installed, { parent: event.parent, name: eventName(event) });
|
|
153
|
+
if (path === undefined || !scopes.some((scope) => {
|
|
154
|
+
if (!below(path, scope))
|
|
155
|
+
return false;
|
|
156
|
+
if (options.recursive)
|
|
157
|
+
return true;
|
|
158
|
+
return directoryPath(installed, event.parent) === scope;
|
|
159
|
+
}))
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
if (path === undefined || !below(path, root))
|
|
163
|
+
return undefined;
|
|
164
|
+
// A confinement root's outside parent is never reported as a rename.
|
|
165
|
+
if (event.ino === options.roots.at(-1) && !Predicate.isTagged(event.change, "Update"))
|
|
166
|
+
return undefined;
|
|
167
|
+
return {
|
|
168
|
+
...event,
|
|
169
|
+
change: Change.cases[event.change._tag].make({ path: ownedPath(nameBytes(rebase(path, root))) })
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
rescan: () => ({
|
|
173
|
+
change: Change.cases.Rescan.make({ path: ownedPath(nameBytes(rescanPath)) }),
|
|
174
|
+
parent: options.target,
|
|
175
|
+
ino: options.target
|
|
176
|
+
}),
|
|
177
|
+
failure: () => failure,
|
|
178
|
+
settle: () => terminal?.map((path) => ({
|
|
179
|
+
change: Change.cases.Remove.make({ path: ownedPath(nameBytes(path)) }),
|
|
180
|
+
parent: options.target,
|
|
181
|
+
ino: options.target
|
|
182
|
+
}))
|
|
183
|
+
};
|
|
184
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type { FsFailure } from "../VfsError.js";
|
|
3
|
+
import type { OpContext } from "./errors.js";
|
|
4
|
+
import { type Ino, type Node } from "./volumeState.js";
|
|
5
|
+
/** @internal */
|
|
6
|
+
export declare const make: (view: Effect.Effect<(ino: Ino) => Node | undefined>, roots: ReadonlyArray<Ino>) => {
|
|
7
|
+
roots: readonly Ino[];
|
|
8
|
+
root: Ino;
|
|
9
|
+
alive: (op: OpContext, handle?: boolean) => Effect.Effect<undefined, FsFailure, never>;
|
|
10
|
+
contains: (node: Node) => Effect.Effect<boolean, never, never>;
|
|
11
|
+
check: (node: Node, op: OpContext, handle?: boolean | undefined) => Effect.Effect<void, FsFailure, never>;
|
|
12
|
+
path: (ino: Ino) => Effect.Effect<string | undefined, never, never>;
|
|
13
|
+
};
|
|
14
|
+
/** @internal */
|
|
15
|
+
export type Confinement = ReturnType<typeof make>;
|
|
16
|
+
//# sourceMappingURL=confinement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confinement.d.ts","sourceRoot":"","sources":["../../src/internal/confinement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI,EAAiC,MAAM,kBAAkB,CAAA;AAErF,gBAAgB;AAChB,eAAO,MAAM,IAAI,SAAU,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC,SAAS,aAAa,CAAC,GAAG,CAAC;;;gBAoD9E,SAAS,WAAU,OAAO;qBAErB,IAAI;;;CAuC7B,CAAA;AAED,gBAAgB;AAChB,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAA"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { ROOT_INO, WALK_YIELD_INTERVAL } from "./volumeState.js";
|
|
3
|
+
/** @internal */
|
|
4
|
+
export const make = (view, roots) => {
|
|
5
|
+
const descends = Effect.fnUntraced(function* (get, ino, root) {
|
|
6
|
+
let node = get(ino);
|
|
7
|
+
let steps = 0;
|
|
8
|
+
while (node?.kind === "directory" && node.metadata.nlink > 0) {
|
|
9
|
+
if (node.ino === root)
|
|
10
|
+
return true;
|
|
11
|
+
if (node.ino === ROOT_INO)
|
|
12
|
+
return false;
|
|
13
|
+
const parent = get(node.parent);
|
|
14
|
+
if (parent?.kind !== "directory" || parent.entries.get(node.name) !== node.ino)
|
|
15
|
+
return false;
|
|
16
|
+
node = parent;
|
|
17
|
+
if (++steps % WALK_YIELD_INTERVAL === 0)
|
|
18
|
+
yield* Effect.yieldNow;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
});
|
|
22
|
+
const aliveIn = Effect.fnUntraced(function* (get, op, handle) {
|
|
23
|
+
for (let index = 0; index < roots.length; index++) {
|
|
24
|
+
const root = roots[index];
|
|
25
|
+
const node = get(root);
|
|
26
|
+
if (node?.kind !== "directory" || node.metadata.nlink === 0) {
|
|
27
|
+
return yield* op.fail(handle ? "InvalidHandle" : "ClosedCaller");
|
|
28
|
+
}
|
|
29
|
+
if (index > 0 && !(yield* descends(get, root, roots[index - 1])))
|
|
30
|
+
return yield* op.fail("AccessDenied");
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const containsIn = Effect.fnUntraced(function* (get, node) {
|
|
34
|
+
const root = roots.at(-1);
|
|
35
|
+
if (root === undefined)
|
|
36
|
+
return true;
|
|
37
|
+
if (node.kind === "directory")
|
|
38
|
+
return yield* descends(get, node.ino, root);
|
|
39
|
+
for (const link of node.links) {
|
|
40
|
+
const parent = get(link.parent);
|
|
41
|
+
if (parent?.kind === "directory" && parent.entries.get(link.name) === node.ino &&
|
|
42
|
+
(yield* descends(get, parent.ino, root)))
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
});
|
|
47
|
+
const alive = (op, handle = false) => Effect.flatMap(view, (get) => aliveIn(get, op, handle));
|
|
48
|
+
const contains = (node) => Effect.flatMap(view, (get) => containsIn(get, node));
|
|
49
|
+
const check = Effect.fnUntraced(function* (node, op, handle = false) {
|
|
50
|
+
// Share the current lookup within authorization, never its result between operations.
|
|
51
|
+
const get = yield* view;
|
|
52
|
+
yield* aliveIn(get, op, handle);
|
|
53
|
+
if (!(yield* containsIn(get, node)))
|
|
54
|
+
return yield* op.fail("AccessDenied");
|
|
55
|
+
});
|
|
56
|
+
const path = Effect.fnUntraced(function* (ino) {
|
|
57
|
+
const names = [];
|
|
58
|
+
const get = yield* view;
|
|
59
|
+
let node = get(ino);
|
|
60
|
+
let steps = 0;
|
|
61
|
+
if (node?.kind !== "directory" || node.metadata.nlink === 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
const root = roots.at(-1) ?? ROOT_INO;
|
|
64
|
+
while (node.ino !== root) {
|
|
65
|
+
if (node.ino === ROOT_INO)
|
|
66
|
+
return undefined;
|
|
67
|
+
names.push(node.name);
|
|
68
|
+
const parent = get(node.parent);
|
|
69
|
+
if (parent?.kind !== "directory" || parent.metadata.nlink === 0 || parent.entries.get(node.name) !== node.ino) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
node = parent;
|
|
73
|
+
if (++steps % WALK_YIELD_INTERVAL === 0)
|
|
74
|
+
yield* Effect.yieldNow;
|
|
75
|
+
}
|
|
76
|
+
return "2f" + names.reverse().join("2f");
|
|
77
|
+
});
|
|
78
|
+
return { roots, root: roots.at(-1) ?? ROOT_INO, alive, contains, check, path };
|
|
79
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Ino } from "./volumeState.js";
|
|
2
|
+
/** @internal */
|
|
3
|
+
export declare const set: (entries: ReadonlyMap<string, Ino>, name: string, ino: Ino) => ReadonlyMap<string, Ino>;
|
|
4
|
+
/** @internal */
|
|
5
|
+
export declare const remove: (entries: ReadonlyMap<string, Ino>, name: string) => ReadonlyMap<string, Ino>;
|
|
6
|
+
//# sourceMappingURL=directoryEntries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directoryEntries.d.ts","sourceRoot":"","sources":["../../src/internal/directoryEntries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAA;AA2E3C,gBAAgB;AAChB,eAAO,MAAM,GAAG,YAAa,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,MAAM,OAAO,GAAG,KAAG,WAAW,CAAC,MAAM,EAAE,GAAG,CAYtG,CAAA;AAED,gBAAgB;AAChB,eAAO,MAAM,MAAM,YAAa,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,MAAM,KAAG,WAAW,CAAC,MAAM,EAAE,GAAG,CAa/F,CAAA"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as HashMap from "effect/HashMap";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as InodeTable from "./inodeTable.js";
|
|
4
|
+
// Name lookup and insertion order share immutable trees. Deleted slots are compacted once they outnumber live entries.
|
|
5
|
+
class DirectoryEntries {
|
|
6
|
+
names;
|
|
7
|
+
slots;
|
|
8
|
+
length;
|
|
9
|
+
[Symbol.toStringTag] = "Map";
|
|
10
|
+
constructor(names, slots, length) {
|
|
11
|
+
this.names = names;
|
|
12
|
+
this.slots = slots;
|
|
13
|
+
this.length = length;
|
|
14
|
+
}
|
|
15
|
+
get size() {
|
|
16
|
+
return HashMap.size(this.names);
|
|
17
|
+
}
|
|
18
|
+
get(name) {
|
|
19
|
+
const slot = HashMap.get(this.names, name);
|
|
20
|
+
return Option.isSome(slot) ? slot.value.ino : undefined;
|
|
21
|
+
}
|
|
22
|
+
has(name) {
|
|
23
|
+
return HashMap.has(this.names, name);
|
|
24
|
+
}
|
|
25
|
+
*entries() {
|
|
26
|
+
for (let index = 0; index < this.length; index++) {
|
|
27
|
+
const entry = InodeTable.get(this.slots, index);
|
|
28
|
+
if (entry !== undefined)
|
|
29
|
+
yield [entry.name, entry.ino];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
*keys() {
|
|
33
|
+
for (const [name] of this)
|
|
34
|
+
yield name;
|
|
35
|
+
}
|
|
36
|
+
*values() {
|
|
37
|
+
for (const [, ino] of this)
|
|
38
|
+
yield ino;
|
|
39
|
+
}
|
|
40
|
+
[Symbol.iterator]() {
|
|
41
|
+
return this.entries();
|
|
42
|
+
}
|
|
43
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- ReadonlyMap forwards the opaque callback receiver without inspecting it.
|
|
44
|
+
forEach(callback, thisArg) {
|
|
45
|
+
for (const [name, ino] of this)
|
|
46
|
+
callback.call(thisArg, ino, name, this);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const fromEntries = (entries) => {
|
|
50
|
+
let slots = InodeTable.empty();
|
|
51
|
+
let length = 0;
|
|
52
|
+
const owner = Symbol();
|
|
53
|
+
const names = HashMap.mutate(HashMap.empty(), (mutable) => {
|
|
54
|
+
for (const [name, ino] of entries) {
|
|
55
|
+
const entry = { name, ino, index: length };
|
|
56
|
+
HashMap.set(mutable, name, entry);
|
|
57
|
+
slots = InodeTable.set(slots, length, entry, owner);
|
|
58
|
+
length++;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return new DirectoryEntries(names, slots, length);
|
|
62
|
+
};
|
|
63
|
+
/** @internal */
|
|
64
|
+
export const set = (entries, name, ino) => {
|
|
65
|
+
const current = entries instanceof DirectoryEntries ? entries : fromEntries(entries);
|
|
66
|
+
const existing = HashMap.get(current.names, name);
|
|
67
|
+
const index = Option.isSome(existing) ? existing.value.index : current.length;
|
|
68
|
+
const entry = { name, ino, index };
|
|
69
|
+
return new DirectoryEntries(HashMap.set(current.names, name, entry), InodeTable.set(current.slots, index, entry), Option.isSome(existing) ? current.length : current.length + 1);
|
|
70
|
+
};
|
|
71
|
+
/** @internal */
|
|
72
|
+
export const remove = (entries, name) => {
|
|
73
|
+
const current = entries instanceof DirectoryEntries ? entries : fromEntries(entries);
|
|
74
|
+
const existing = HashMap.get(current.names, name);
|
|
75
|
+
if (Option.isNone(existing))
|
|
76
|
+
return current;
|
|
77
|
+
const removed = new DirectoryEntries(HashMap.remove(current.names, name), InodeTable.set(current.slots, existing.value.index, undefined), current.length);
|
|
78
|
+
return removed.length > removed.size * 2 ? fromEntries(removed) : removed;
|
|
79
|
+
};
|
package/dist/internal/fixture.js
CHANGED
|
@@ -10,7 +10,7 @@ import { assemble, Ino, ROOT_INO } from "./volumeState.js";
|
|
|
10
10
|
const DEFAULT_MODE = { directory: 0o755, file: 0o644, symlink: 0o777 };
|
|
11
11
|
const EPOCH_NS = 0n;
|
|
12
12
|
/** @internal */
|
|
13
|
-
export const fromFixture = Effect.
|
|
13
|
+
export const fromFixture = Effect.fnUntraced(function* (fixture, options) {
|
|
14
14
|
const config = yield* Effect.fromResult(decodeConfiguration(VolumeOptionsSchema, options ?? {}, "fromFixture"));
|
|
15
15
|
const source = yield* Schema.decodeEffect(FixtureSchema, { onExcessProperty: "error" })(fixture).pipe(Effect.mapError((cause) => imageFailure("fromFixture", "InvalidStructure", { field: "fixture", cause })));
|
|
16
16
|
if (source.entries.some((entry) => FixtureEntry.match(entry, {
|
package/dist/internal/path.js
CHANGED
|
@@ -63,7 +63,7 @@ const attachedBuffer = (bytes) => {
|
|
|
63
63
|
/** @internal */
|
|
64
64
|
export const isAttachedBytes = (bytes) => Predicate.isUint8Array(bytes) && bytes.buffer instanceof ArrayBuffer && attachedBuffer(bytes);
|
|
65
65
|
/** @internal */
|
|
66
|
-
export const pathFromBytes = Effect.
|
|
66
|
+
export const pathFromBytes = Effect.fnUntraced(function* (bytes) {
|
|
67
67
|
if (!isAttachedBytes(bytes))
|
|
68
68
|
return yield* fsFailure("InvalidArgument", "pathFromBytes");
|
|
69
69
|
const owned = new Uint8Array(bytes);
|
|
@@ -73,7 +73,7 @@ export const pathFromBytes = Effect.fn("VirtualFileSystem.pathFromBytes")(functi
|
|
|
73
73
|
return makeBytePath(owned);
|
|
74
74
|
});
|
|
75
75
|
/** @internal */
|
|
76
|
-
export const pathToBytes = Effect.
|
|
76
|
+
export const pathToBytes = Effect.fnUntraced(function* (path) {
|
|
77
77
|
const bytes = getBytePathBytes(path);
|
|
78
78
|
if (bytes === undefined)
|
|
79
79
|
return yield* fsFailure("InvalidArgument", "pathToBytes");
|
|
@@ -4,7 +4,8 @@ import * as Result from "effect/Result";
|
|
|
4
4
|
import type { Identity } from "../Caller.js";
|
|
5
5
|
import { type EntryInput, type NameInput, type Target } from "../Target.js";
|
|
6
6
|
import type { FsFailure } from "../VfsError.js";
|
|
7
|
-
import type { DirectoryHandle, PathInput } from "../VirtualFileSystem.js";
|
|
7
|
+
import type { DirectoryHandle, FileHandle, PathInput } from "../VirtualFileSystem.js";
|
|
8
|
+
import { type Confinement } from "./confinement.js";
|
|
8
9
|
import type { OpContext } from "./errors.js";
|
|
9
10
|
import { type PreparedPath } from "./path.js";
|
|
10
11
|
import * as TokenRegistry from "./tokenRegistry.js";
|
|
@@ -86,7 +87,10 @@ export declare const authorize: (node: Node, identity: Identity, bits: number, o
|
|
|
86
87
|
/** @internal */
|
|
87
88
|
export declare const entryName: (input: NameInput, op: OpContext) => Result.Result<string, FsFailure>;
|
|
88
89
|
/** @internal */
|
|
89
|
-
export declare const make: ({ caller, get, identity, maxPathBytes, registry }: {
|
|
90
|
+
export declare const make: ({ caller, get, identity, maxPathBytes, registry, confinement, additional, authorityView }: {
|
|
91
|
+
readonly additional?: ReadonlyArray<Confinement> | undefined;
|
|
92
|
+
readonly authorityView: Effect.Effect<(ino: Ino) => Node | undefined>;
|
|
93
|
+
readonly confinement?: Confinement | undefined;
|
|
90
94
|
readonly caller: TokenRegistry.DirectoryReference;
|
|
91
95
|
readonly get: (ino: Ino) => Effect.Effect<Node | undefined>;
|
|
92
96
|
readonly identity: Identity;
|
|
@@ -102,7 +106,7 @@ export declare const make: ({ caller, get, identity, maxPathBytes, registry }: {
|
|
|
102
106
|
};
|
|
103
107
|
directory: (target: {
|
|
104
108
|
readonly _tag: "Handle";
|
|
105
|
-
readonly handle:
|
|
109
|
+
readonly handle: FileHandle | DirectoryHandle;
|
|
106
110
|
} | {
|
|
107
111
|
readonly _tag: "Path";
|
|
108
112
|
readonly path: PathInput;
|
|
@@ -117,7 +121,7 @@ export declare const make: ({ caller, get, identity, maxPathBytes, registry }: {
|
|
|
117
121
|
}, FsFailure, never>;
|
|
118
122
|
entryDirectory: (target: {
|
|
119
123
|
readonly _tag: "Handle";
|
|
120
|
-
readonly handle:
|
|
124
|
+
readonly handle: FileHandle | DirectoryHandle;
|
|
121
125
|
} | {
|
|
122
126
|
readonly _tag: "Path";
|
|
123
127
|
readonly path: PathInput;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolution.d.ts","sourceRoot":"","sources":["../../src/internal/resolution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,KAAK,UAAU,EAAqB,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAA;AAC9F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"resolution.d.ts","sourceRoot":"","sources":["../../src/internal/resolution.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,10 +4,12 @@ import * as Hex from "effect/encoding/Hex";
|
|
|
4
4
|
import * as Predicate from "effect/Predicate";
|
|
5
5
|
import * as Result from "effect/Result";
|
|
6
6
|
import { isEntry, isTarget } from "../Target.js";
|
|
7
|
+
import { make as makeConfinement } from "./confinement.js";
|
|
7
8
|
import { DOT_DOT_HEX, DOT_HEX, isAttachedBytes, isDotComponent, isWellFormed, MAX_NAME_BYTES, MAX_SYMLINK_TRAVERSALS, nameBytes, ownedPath, preparePath, SLASH_BYTE } from "./path.js";
|
|
8
9
|
import * as TokenRegistry from "./tokenRegistry.js";
|
|
9
10
|
import { ROOT_INO } from "./volumeState.js";
|
|
10
11
|
const encoder = new TextEncoder();
|
|
12
|
+
const EMPTY_AUTHORITIES = [];
|
|
11
13
|
/** @internal */
|
|
12
14
|
export const inGroup = (identity, gid) => identity.gid === gid || identity.groups.includes(gid);
|
|
13
15
|
/** @internal */
|
|
@@ -30,28 +32,37 @@ export const entryName = (input, op) => {
|
|
|
30
32
|
return Result.succeed(Hex.encode(new Uint8Array(bytes)));
|
|
31
33
|
};
|
|
32
34
|
/** @internal */
|
|
33
|
-
export const make = ({ caller, get, identity, maxPathBytes, registry }) => {
|
|
35
|
+
export const make = ({ caller, get, identity, maxPathBytes, registry, confinement, additional, authorityView }) => {
|
|
36
|
+
const visibleRoot = confinement?.root ?? ROOT_INO;
|
|
34
37
|
const prepare = (input, op) => preparePath(input, op.operation, maxPathBytes);
|
|
35
38
|
const prepareEntry = (input, op) => isEntry(input)
|
|
36
39
|
? Result.map(entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
|
|
37
40
|
: isTarget(input)
|
|
38
41
|
? Result.map(prepare(input.path, op), (path) => ({ kind: "path", path, base: input.relativeTo }))
|
|
39
42
|
: Result.map(prepare(input, op), (path) => ({ kind: "path", path, base: undefined }));
|
|
40
|
-
const nodeAt = Effect.fnUntraced(function* (ino) {
|
|
41
|
-
const node =
|
|
43
|
+
const nodeAt = Effect.fnUntraced(function* (lookup, ino) {
|
|
44
|
+
const node = lookup(ino);
|
|
42
45
|
return node === undefined ? yield* Effect.die("Inode left the table during resolution") : node;
|
|
43
46
|
});
|
|
44
|
-
const directoryAt = Effect.fnUntraced(function* (ino) {
|
|
45
|
-
const node = yield* nodeAt(ino);
|
|
47
|
+
const directoryAt = Effect.fnUntraced(function* (lookup, ino) {
|
|
48
|
+
const node = yield* nodeAt(lookup, ino);
|
|
46
49
|
return node.kind !== "directory" ? yield* Effect.die("Directory left the table during resolution") : node;
|
|
47
50
|
});
|
|
48
|
-
const callerDirectory = Effect.fnUntraced(function* (op) {
|
|
51
|
+
const callerDirectory = Effect.fnUntraced(function* (lookup, op) {
|
|
49
52
|
const ino = TokenRegistry.inode(caller);
|
|
50
|
-
const node = ino === undefined ? undefined :
|
|
53
|
+
const node = ino === undefined ? undefined : lookup(ino);
|
|
51
54
|
return node?.kind !== "directory" ? yield* op.fail("ClosedCaller") : node;
|
|
52
55
|
});
|
|
53
56
|
const walk = Effect.fnUntraced(function* (path, base, op, mode, referencedBase) {
|
|
54
57
|
const pathOp = op.at(path.input);
|
|
58
|
+
const baseAuthorities = base === undefined
|
|
59
|
+
? EMPTY_AUTHORITIES
|
|
60
|
+
: registry.handleBoundaries(base).map((roots) => makeConfinement(authorityView, roots));
|
|
61
|
+
const authorizeDirectory = baseAuthorities.length === 0 ? authorize : Effect.fnUntraced(function* (node, callerIdentity, bits, context) {
|
|
62
|
+
for (const authority of baseAuthorities)
|
|
63
|
+
yield* authority.check(node, context, true);
|
|
64
|
+
yield* authorize(node, callerIdentity, bits, context);
|
|
65
|
+
});
|
|
55
66
|
const parentOnly = mode.kind === "Parent";
|
|
56
67
|
const allowMissing = mode.kind === "OrCreate" && mode.create !== "never";
|
|
57
68
|
const followFinalSymlink = mode.kind === "Node"
|
|
@@ -61,7 +72,26 @@ export const make = ({ caller, get, identity, maxPathBytes, registry }) => {
|
|
|
61
72
|
if (!TokenRegistry.isOpen(caller)) {
|
|
62
73
|
return yield* pathOp.fail("ClosedCaller");
|
|
63
74
|
}
|
|
64
|
-
|
|
75
|
+
if (additional !== undefined && additional.length > 0) {
|
|
76
|
+
for (const authority of additional)
|
|
77
|
+
yield* authority.alive(pathOp);
|
|
78
|
+
}
|
|
79
|
+
if (confinement !== undefined)
|
|
80
|
+
yield* confinement.alive(pathOp);
|
|
81
|
+
if (path.absolute && base !== undefined &&
|
|
82
|
+
(confinement !== undefined || (additional?.length ?? 0) > 0 || baseAuthorities.length > 0)) {
|
|
83
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(base, "directory"), "handle", pathOp);
|
|
84
|
+
yield* registry.authorizeHandle(base, node, pathOp);
|
|
85
|
+
if (confinement !== undefined)
|
|
86
|
+
yield* confinement.check(node, pathOp);
|
|
87
|
+
if (additional !== undefined && additional.length > 0) {
|
|
88
|
+
for (const authority of additional)
|
|
89
|
+
yield* authority.check(node, pathOp);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Reads remain live so recursive creation can revisit nodes added earlier in this walk.
|
|
93
|
+
const lookup = yield* authorityView;
|
|
94
|
+
let current = path.absolute ? (yield* nodeAt(lookup, visibleRoot)) : yield* callerDirectory(lookup, pathOp);
|
|
65
95
|
if (!path.absolute && referencedBase !== undefined) {
|
|
66
96
|
current = referencedBase;
|
|
67
97
|
yield* authorize(current, identity, 0o1, op);
|
|
@@ -70,9 +100,16 @@ export const make = ({ caller, get, identity, maxPathBytes, registry }) => {
|
|
|
70
100
|
const directory = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(base, "directory"), "handle", pathOp);
|
|
71
101
|
if (directory.kind !== "directory")
|
|
72
102
|
return yield* pathOp.fail("InvalidHandle");
|
|
103
|
+
yield* registry.authorizeHandle(base, directory, pathOp);
|
|
73
104
|
current = directory;
|
|
74
105
|
yield* authorize(current, identity, 0o1, pathOp);
|
|
75
106
|
}
|
|
107
|
+
if (confinement !== undefined)
|
|
108
|
+
yield* confinement.check(current, pathOp);
|
|
109
|
+
if (additional !== undefined && additional.length > 0) {
|
|
110
|
+
for (const authority of additional)
|
|
111
|
+
yield* authority.check(current, pathOp);
|
|
112
|
+
}
|
|
76
113
|
if (current.metadata.nlink === 0) {
|
|
77
114
|
return yield* pathOp.fail("NotFound");
|
|
78
115
|
}
|
|
@@ -87,14 +124,14 @@ export const make = ({ caller, get, identity, maxPathBytes, registry }) => {
|
|
|
87
124
|
if (current.kind !== "directory") {
|
|
88
125
|
return yield* pathOp.fail("NotDirectory");
|
|
89
126
|
}
|
|
90
|
-
yield*
|
|
127
|
+
yield* authorizeDirectory(current, identity, 0o1, pathOp);
|
|
91
128
|
const component = work.components[index];
|
|
92
129
|
if (component === undefined)
|
|
93
130
|
break;
|
|
94
131
|
if (component === DOT_HEX)
|
|
95
132
|
continue;
|
|
96
133
|
if (component === DOT_DOT_HEX) {
|
|
97
|
-
current = yield* directoryAt(current.parent);
|
|
134
|
+
current = yield* directoryAt(lookup, confinement !== undefined && current.ino === visibleRoot ? current.ino : current.parent);
|
|
98
135
|
parent = undefined;
|
|
99
136
|
name = undefined;
|
|
100
137
|
continue;
|
|
@@ -102,7 +139,7 @@ export const make = ({ caller, get, identity, maxPathBytes, registry }) => {
|
|
|
102
139
|
parent = current;
|
|
103
140
|
name = component;
|
|
104
141
|
const childIno = current.entries.get(component);
|
|
105
|
-
const child = childIno === undefined ? undefined : (
|
|
142
|
+
const child = childIno === undefined ? undefined : lookup(childIno);
|
|
106
143
|
if (child === undefined) {
|
|
107
144
|
if (allowMissing && index === work.components.length - 1) {
|
|
108
145
|
return { kind: "missing", parent, name, op: pathOp };
|
|
@@ -139,12 +176,16 @@ export const make = ({ caller, get, identity, maxPathBytes, registry }) => {
|
|
|
139
176
|
work = expanded.success;
|
|
140
177
|
linked = work.components.length - remaining + Math.max(0, linked - index - 1);
|
|
141
178
|
if (work.absolute)
|
|
142
|
-
current = yield* nodeAt(
|
|
179
|
+
current = yield* nodeAt(lookup, visibleRoot);
|
|
143
180
|
index = -1;
|
|
144
181
|
}
|
|
145
182
|
else
|
|
146
183
|
current = child;
|
|
147
184
|
}
|
|
185
|
+
if (baseAuthorities.length > 0) {
|
|
186
|
+
for (const authority of baseAuthorities)
|
|
187
|
+
yield* authority.check(current, pathOp, true);
|
|
188
|
+
}
|
|
148
189
|
if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
|
|
149
190
|
return yield* pathOp.fail("NotDirectory");
|
|
150
191
|
}
|
|
@@ -168,6 +209,14 @@ export const make = ({ caller, get, identity, maxPathBytes, registry }) => {
|
|
|
168
209
|
const node = yield* TokenRegistry.nodeOrFail(yield* (Predicate.isTagged(target, "Reference")
|
|
169
210
|
? registry.resolve(target.reference, "reference")
|
|
170
211
|
: registry.resolve(target.handle, "handle")), Predicate.isTagged(target, "Reference") ? "reference" : "handle", op);
|
|
212
|
+
if (!Predicate.isTagged(target, "Reference"))
|
|
213
|
+
yield* registry.authorizeHandle(target.handle, node, op);
|
|
214
|
+
if (confinement !== undefined)
|
|
215
|
+
yield* confinement.check(node, op);
|
|
216
|
+
if (additional !== undefined && additional.length > 0) {
|
|
217
|
+
for (const authority of additional)
|
|
218
|
+
yield* authority.check(node, op);
|
|
219
|
+
}
|
|
171
220
|
return { kind: "node", node, op };
|
|
172
221
|
});
|
|
173
222
|
const directory = Effect.fnUntraced(function* (target, op) {
|
|
@@ -249,7 +298,13 @@ export const make = ({ caller, get, identity, maxPathBytes, registry }) => {
|
|
|
249
298
|
: { kind: "entry", node: direct, parent, name: prepared.name, op };
|
|
250
299
|
if (direct?.kind === "symlink" && mode.finalSymlink === "follow") {
|
|
251
300
|
const path = yield* Effect.fromResult(preparePath(ownedPath(nameBytes(prepared.name)), op.operation, undefined));
|
|
252
|
-
|
|
301
|
+
// Following an entry link must retain the directory capability's boundaries.
|
|
302
|
+
const base = Predicate.isTagged(prepared.directory, "Handle")
|
|
303
|
+
? prepared.directory.handle
|
|
304
|
+
: Predicate.isTagged(prepared.directory, "Path")
|
|
305
|
+
? prepared.directory.relativeTo
|
|
306
|
+
: undefined;
|
|
307
|
+
result = yield* walk(path, base, op, mode, parent);
|
|
253
308
|
// Entry writeFile rejects a followed directory before checking exclusive creation.
|
|
254
309
|
if (mode.create === "exclusive" && mode.entryExclusive === "afterSymlink" &&
|
|
255
310
|
(result.kind === "node" || (result.kind === "entry" && result.node.kind === "directory"))) {
|