@secure-exec/core 0.2.0-rc.2 → 0.2.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/isolate-runtime.d.ts +1 -1
- package/dist/generated/isolate-runtime.js +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +10 -2
- package/dist/isolate-runtime/require-setup.js +145 -7
- package/dist/kernel/device-backend.d.ts +14 -0
- package/dist/kernel/device-backend.js +251 -0
- package/dist/kernel/device-layer.js +9 -0
- package/dist/kernel/index.d.ts +4 -4
- package/dist/kernel/index.js +3 -3
- package/dist/kernel/kernel.js +141 -119
- package/dist/kernel/mount-table.d.ts +75 -0
- package/dist/kernel/mount-table.js +353 -0
- package/dist/kernel/permissions.d.ts +9 -0
- package/dist/kernel/permissions.js +33 -1
- package/dist/kernel/proc-backend.d.ts +30 -0
- package/dist/kernel/proc-backend.js +428 -0
- package/dist/kernel/proc-layer.js +6 -0
- package/dist/kernel/process-table.d.ts +3 -1
- package/dist/kernel/process-table.js +23 -3
- package/dist/kernel/pty.d.ts +3 -2
- package/dist/kernel/pty.js +13 -2
- package/dist/kernel/types.d.ts +45 -4
- package/dist/kernel/types.js +9 -0
- package/dist/kernel/vfs.d.ts +30 -2
- package/dist/kernel/vfs.js +19 -2
- package/dist/shared/api-types.d.ts +6 -0
- package/dist/shared/console-formatter.js +8 -8
- package/dist/shared/in-memory-fs.d.ts +14 -62
- package/dist/shared/in-memory-fs.js +101 -636
- package/dist/shared/permissions.js +5 -0
- package/dist/test/block-store-conformance.d.ts +34 -0
- package/dist/test/block-store-conformance.js +251 -0
- package/dist/test/metadata-store-conformance.d.ts +37 -0
- package/dist/test/metadata-store-conformance.js +646 -0
- package/dist/test/vfs-conformance.d.ts +65 -0
- package/dist/test/vfs-conformance.js +842 -0
- package/dist/types.d.ts +1 -0
- package/dist/vfs/chunked-vfs.d.ts +66 -0
- package/dist/vfs/chunked-vfs.js +1290 -0
- package/dist/vfs/host-block-store.d.ts +19 -0
- package/dist/vfs/host-block-store.js +97 -0
- package/dist/vfs/memory-block-store.d.ts +16 -0
- package/dist/vfs/memory-block-store.js +45 -0
- package/dist/vfs/memory-metadata.d.ts +75 -0
- package/dist/vfs/memory-metadata.js +528 -0
- package/dist/vfs/sqlite-metadata.d.ts +91 -0
- package/dist/vfs/sqlite-metadata.js +582 -0
- package/dist/vfs/types.d.ts +210 -0
- package/dist/vfs/types.js +8 -0
- package/package.json +20 -1
- package/dist/kernel/inode-table.d.ts +0 -43
- package/dist/kernel/inode-table.js +0 -85
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount Table.
|
|
3
|
+
*
|
|
4
|
+
* Linux-style VFS mount table that routes paths to mounted filesystem backends
|
|
5
|
+
* by longest-prefix matching. Replaces the hardcoded layer composition
|
|
6
|
+
* (DeviceLayer wraps ProcLayer wraps base FS) with a unified routing table.
|
|
7
|
+
*/
|
|
8
|
+
import { KernelError } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a path: collapse //, ., .., strip trailing /.
|
|
11
|
+
*/
|
|
12
|
+
function normalizePath(path) {
|
|
13
|
+
if (!path || path === "/")
|
|
14
|
+
return "/";
|
|
15
|
+
const parts = path.split("/");
|
|
16
|
+
const stack = [];
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
if (part === "" || part === ".")
|
|
19
|
+
continue;
|
|
20
|
+
if (part === "..") {
|
|
21
|
+
stack.pop();
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
stack.push(part);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return `/${stack.join("/")}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get parent directory path.
|
|
31
|
+
*/
|
|
32
|
+
function parentPath(p) {
|
|
33
|
+
if (p === "/")
|
|
34
|
+
return "/";
|
|
35
|
+
const idx = p.lastIndexOf("/");
|
|
36
|
+
if (idx <= 0)
|
|
37
|
+
return "/";
|
|
38
|
+
return p.slice(0, idx);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get basename of a path.
|
|
42
|
+
*/
|
|
43
|
+
function basename(p) {
|
|
44
|
+
const idx = p.lastIndexOf("/");
|
|
45
|
+
return p.slice(idx + 1);
|
|
46
|
+
}
|
|
47
|
+
export class MountTable {
|
|
48
|
+
/**
|
|
49
|
+
* Mounts sorted by path length descending so longest-prefix match is first hit.
|
|
50
|
+
*/
|
|
51
|
+
mounts;
|
|
52
|
+
constructor(rootFs) {
|
|
53
|
+
this.mounts = [{ path: "/", fs: rootFs, readOnly: false }];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Mount a filesystem at the given path.
|
|
57
|
+
* Auto-creates the mount point directory in the parent filesystem if needed.
|
|
58
|
+
*/
|
|
59
|
+
mount(path, fs, options) {
|
|
60
|
+
const normalized = normalizePath(path);
|
|
61
|
+
if (normalized === "/") {
|
|
62
|
+
throw new KernelError("EINVAL", "cannot mount over root");
|
|
63
|
+
}
|
|
64
|
+
// Check if already mounted
|
|
65
|
+
if (this.mounts.some((m) => m.path === normalized)) {
|
|
66
|
+
throw new KernelError("EEXIST", `already mounted at ${normalized}`);
|
|
67
|
+
}
|
|
68
|
+
// Auto-create mount point directory in the parent filesystem.
|
|
69
|
+
// Resolve *before* inserting the new mount so the path goes to the current owner.
|
|
70
|
+
const { mount: parentMount, relativePath } = this.resolve(normalized);
|
|
71
|
+
void parentMount.fs
|
|
72
|
+
.mkdir(relativePath || "/", { recursive: true })
|
|
73
|
+
.catch(() => {
|
|
74
|
+
/* directory may already exist */
|
|
75
|
+
});
|
|
76
|
+
const entry = {
|
|
77
|
+
path: normalized,
|
|
78
|
+
fs,
|
|
79
|
+
readOnly: options?.readOnly ?? false,
|
|
80
|
+
};
|
|
81
|
+
this.mounts.push(entry);
|
|
82
|
+
// Sort by path length descending for longest-prefix-first matching
|
|
83
|
+
this.mounts.sort((a, b) => b.path.length - a.path.length);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Unmount the filesystem at the given path.
|
|
87
|
+
*/
|
|
88
|
+
unmount(path) {
|
|
89
|
+
const normalized = normalizePath(path);
|
|
90
|
+
if (normalized === "/") {
|
|
91
|
+
throw new KernelError("EINVAL", "cannot unmount root");
|
|
92
|
+
}
|
|
93
|
+
const idx = this.mounts.findIndex((m) => m.path === normalized);
|
|
94
|
+
if (idx === -1) {
|
|
95
|
+
throw new KernelError("EINVAL", `not a mount point: ${normalized}`);
|
|
96
|
+
}
|
|
97
|
+
this.mounts.splice(idx, 1);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* List all current mounts.
|
|
101
|
+
*/
|
|
102
|
+
getMounts() {
|
|
103
|
+
return this.mounts.map((m) => ({
|
|
104
|
+
path: m.path,
|
|
105
|
+
readOnly: m.readOnly,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
// -----------------------------------------------------------------------
|
|
109
|
+
// Path resolution
|
|
110
|
+
// -----------------------------------------------------------------------
|
|
111
|
+
resolve(fullPath) {
|
|
112
|
+
const normalized = normalizePath(fullPath);
|
|
113
|
+
for (const mount of this.mounts) {
|
|
114
|
+
if (mount.path === "/") {
|
|
115
|
+
// Root mount: forward path as-is
|
|
116
|
+
return { mount, relativePath: normalized };
|
|
117
|
+
}
|
|
118
|
+
if (normalized === mount.path ||
|
|
119
|
+
normalized.startsWith(`${mount.path}/`)) {
|
|
120
|
+
const rel = normalized === mount.path
|
|
121
|
+
? ""
|
|
122
|
+
: normalized.slice(mount.path.length + 1);
|
|
123
|
+
return { mount, relativePath: rel };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Should never happen since root mount always matches
|
|
127
|
+
throw new KernelError("ENOENT", `no mount for path: ${fullPath}`);
|
|
128
|
+
}
|
|
129
|
+
assertWritable(mount, path) {
|
|
130
|
+
if (mount.readOnly) {
|
|
131
|
+
throw new KernelError("EROFS", `read-only filesystem: ${path}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// -----------------------------------------------------------------------
|
|
135
|
+
// Read operations
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
async readFile(path) {
|
|
138
|
+
const { mount, relativePath } = this.resolve(path);
|
|
139
|
+
return mount.fs.readFile(relativePath);
|
|
140
|
+
}
|
|
141
|
+
async readTextFile(path) {
|
|
142
|
+
const { mount, relativePath } = this.resolve(path);
|
|
143
|
+
return mount.fs.readTextFile(relativePath);
|
|
144
|
+
}
|
|
145
|
+
async readDir(path) {
|
|
146
|
+
const { mount, relativePath } = this.resolve(path);
|
|
147
|
+
const entries = await mount.fs.readDir(relativePath);
|
|
148
|
+
// Merge mount point basenames for child mounts
|
|
149
|
+
const normalized = normalizePath(path);
|
|
150
|
+
const mountBasenames = this.getChildMountBasenames(normalized);
|
|
151
|
+
if (mountBasenames.length === 0)
|
|
152
|
+
return entries;
|
|
153
|
+
const entrySet = new Set(entries);
|
|
154
|
+
for (const name of mountBasenames) {
|
|
155
|
+
entrySet.add(name);
|
|
156
|
+
}
|
|
157
|
+
return [...entrySet];
|
|
158
|
+
}
|
|
159
|
+
async readDirWithTypes(path) {
|
|
160
|
+
const { mount, relativePath } = this.resolve(path);
|
|
161
|
+
const entries = await mount.fs.readDirWithTypes(relativePath);
|
|
162
|
+
// Merge mount point basenames as directory entries
|
|
163
|
+
const normalized = normalizePath(path);
|
|
164
|
+
const mountBasenames = this.getChildMountBasenames(normalized);
|
|
165
|
+
if (mountBasenames.length === 0)
|
|
166
|
+
return entries;
|
|
167
|
+
const nameSet = new Set(entries.map((e) => e.name));
|
|
168
|
+
for (const name of mountBasenames) {
|
|
169
|
+
if (!nameSet.has(name)) {
|
|
170
|
+
entries.push({
|
|
171
|
+
name,
|
|
172
|
+
isDirectory: true,
|
|
173
|
+
isSymbolicLink: false,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return entries;
|
|
178
|
+
}
|
|
179
|
+
async exists(path) {
|
|
180
|
+
const { mount, relativePath } = this.resolve(path);
|
|
181
|
+
return mount.fs.exists(relativePath);
|
|
182
|
+
}
|
|
183
|
+
async stat(path) {
|
|
184
|
+
const { mount, relativePath } = this.resolve(path);
|
|
185
|
+
return mount.fs.stat(relativePath);
|
|
186
|
+
}
|
|
187
|
+
async lstat(path) {
|
|
188
|
+
const { mount, relativePath } = this.resolve(path);
|
|
189
|
+
return mount.fs.lstat(relativePath);
|
|
190
|
+
}
|
|
191
|
+
async realpath(path) {
|
|
192
|
+
const { mount, relativePath } = this.resolve(path);
|
|
193
|
+
const resolved = await mount.fs.realpath(relativePath);
|
|
194
|
+
if (mount.path === "/")
|
|
195
|
+
return resolved;
|
|
196
|
+
// Re-prefix the mount path for non-root mounts
|
|
197
|
+
return `${mount.path}/${resolved}`;
|
|
198
|
+
}
|
|
199
|
+
async readlink(path) {
|
|
200
|
+
const { mount, relativePath } = this.resolve(path);
|
|
201
|
+
return mount.fs.readlink(relativePath);
|
|
202
|
+
}
|
|
203
|
+
async pread(path, offset, length) {
|
|
204
|
+
const { mount, relativePath } = this.resolve(path);
|
|
205
|
+
return mount.fs.pread(relativePath, offset, length);
|
|
206
|
+
}
|
|
207
|
+
async pwrite(path, offset, data) {
|
|
208
|
+
const { mount, relativePath } = this.resolve(path);
|
|
209
|
+
this.assertWritable(mount, path);
|
|
210
|
+
return mount.fs.pwrite(relativePath, offset, data);
|
|
211
|
+
}
|
|
212
|
+
// -----------------------------------------------------------------------
|
|
213
|
+
// Write operations (check readOnly before forwarding)
|
|
214
|
+
// -----------------------------------------------------------------------
|
|
215
|
+
async writeFile(path, content) {
|
|
216
|
+
const { mount, relativePath } = this.resolve(path);
|
|
217
|
+
this.assertWritable(mount, path);
|
|
218
|
+
return mount.fs.writeFile(relativePath, content);
|
|
219
|
+
}
|
|
220
|
+
async createDir(path) {
|
|
221
|
+
const { mount, relativePath } = this.resolve(path);
|
|
222
|
+
this.assertWritable(mount, path);
|
|
223
|
+
return mount.fs.createDir(relativePath);
|
|
224
|
+
}
|
|
225
|
+
async mkdir(path, options) {
|
|
226
|
+
const { mount, relativePath } = this.resolve(path);
|
|
227
|
+
this.assertWritable(mount, path);
|
|
228
|
+
return mount.fs.mkdir(relativePath, options);
|
|
229
|
+
}
|
|
230
|
+
async removeFile(path) {
|
|
231
|
+
const { mount, relativePath } = this.resolve(path);
|
|
232
|
+
this.assertWritable(mount, path);
|
|
233
|
+
return mount.fs.removeFile(relativePath);
|
|
234
|
+
}
|
|
235
|
+
async removeDir(path) {
|
|
236
|
+
const { mount, relativePath } = this.resolve(path);
|
|
237
|
+
this.assertWritable(mount, path);
|
|
238
|
+
return mount.fs.removeDir(relativePath);
|
|
239
|
+
}
|
|
240
|
+
async symlink(target, linkPath) {
|
|
241
|
+
const { mount, relativePath } = this.resolve(linkPath);
|
|
242
|
+
this.assertWritable(mount, linkPath);
|
|
243
|
+
return mount.fs.symlink(target, relativePath);
|
|
244
|
+
}
|
|
245
|
+
async link(oldPath, newPath) {
|
|
246
|
+
const oldResolved = this.resolve(oldPath);
|
|
247
|
+
const newResolved = this.resolve(newPath);
|
|
248
|
+
if (oldResolved.mount !== newResolved.mount) {
|
|
249
|
+
throw new KernelError("EXDEV", `link across mounts: ${oldPath} -> ${newPath}`);
|
|
250
|
+
}
|
|
251
|
+
this.assertWritable(oldResolved.mount, oldPath);
|
|
252
|
+
return oldResolved.mount.fs.link(oldResolved.relativePath, newResolved.relativePath);
|
|
253
|
+
}
|
|
254
|
+
async rename(oldPath, newPath) {
|
|
255
|
+
const oldResolved = this.resolve(oldPath);
|
|
256
|
+
const newResolved = this.resolve(newPath);
|
|
257
|
+
if (oldResolved.mount !== newResolved.mount) {
|
|
258
|
+
throw new KernelError("EXDEV", `rename across mounts: ${oldPath} -> ${newPath}`);
|
|
259
|
+
}
|
|
260
|
+
this.assertWritable(oldResolved.mount, oldPath);
|
|
261
|
+
return oldResolved.mount.fs.rename(oldResolved.relativePath, newResolved.relativePath);
|
|
262
|
+
}
|
|
263
|
+
async chmod(path, mode) {
|
|
264
|
+
const { mount, relativePath } = this.resolve(path);
|
|
265
|
+
this.assertWritable(mount, path);
|
|
266
|
+
return mount.fs.chmod(relativePath, mode);
|
|
267
|
+
}
|
|
268
|
+
async chown(path, uid, gid) {
|
|
269
|
+
const { mount, relativePath } = this.resolve(path);
|
|
270
|
+
this.assertWritable(mount, path);
|
|
271
|
+
return mount.fs.chown(relativePath, uid, gid);
|
|
272
|
+
}
|
|
273
|
+
async utimes(path, atime, mtime) {
|
|
274
|
+
const { mount, relativePath } = this.resolve(path);
|
|
275
|
+
this.assertWritable(mount, path);
|
|
276
|
+
return mount.fs.utimes(relativePath, atime, mtime);
|
|
277
|
+
}
|
|
278
|
+
async truncate(path, length) {
|
|
279
|
+
const { mount, relativePath } = this.resolve(path);
|
|
280
|
+
this.assertWritable(mount, path);
|
|
281
|
+
return mount.fs.truncate(relativePath, length);
|
|
282
|
+
}
|
|
283
|
+
async fsync(path) {
|
|
284
|
+
const { mount, relativePath } = this.resolve(path);
|
|
285
|
+
await mount.fs.fsync?.(relativePath);
|
|
286
|
+
}
|
|
287
|
+
async copy(srcPath, dstPath) {
|
|
288
|
+
const srcResolved = this.resolve(srcPath);
|
|
289
|
+
const dstResolved = this.resolve(dstPath);
|
|
290
|
+
if (srcResolved.mount !== dstResolved.mount) {
|
|
291
|
+
throw new KernelError("EXDEV", `copy across mounts: ${srcPath} -> ${dstPath}`);
|
|
292
|
+
}
|
|
293
|
+
this.assertWritable(srcResolved.mount, dstPath);
|
|
294
|
+
if (srcResolved.mount.fs.copy) {
|
|
295
|
+
return srcResolved.mount.fs.copy(srcResolved.relativePath, dstResolved.relativePath);
|
|
296
|
+
}
|
|
297
|
+
// Fallback: readFile + writeFile.
|
|
298
|
+
const content = await srcResolved.mount.fs.readFile(srcResolved.relativePath);
|
|
299
|
+
await srcResolved.mount.fs.writeFile(dstResolved.relativePath, content);
|
|
300
|
+
}
|
|
301
|
+
async readDirStat(path) {
|
|
302
|
+
const { mount, relativePath } = this.resolve(path);
|
|
303
|
+
if (mount.fs.readDirStat) {
|
|
304
|
+
return mount.fs.readDirStat(relativePath);
|
|
305
|
+
}
|
|
306
|
+
// Fallback: readDirWithTypes + stat for each entry.
|
|
307
|
+
const entries = await mount.fs.readDirWithTypes(relativePath);
|
|
308
|
+
const normalized = normalizePath(path);
|
|
309
|
+
const results = [];
|
|
310
|
+
for (const entry of entries) {
|
|
311
|
+
const entryPath = normalized === "/" ? `/${entry.name}` : `${normalized}/${entry.name}`;
|
|
312
|
+
const stat = await mount.fs.stat(entryPath);
|
|
313
|
+
results.push({ ...entry, stat });
|
|
314
|
+
}
|
|
315
|
+
return results;
|
|
316
|
+
}
|
|
317
|
+
// -----------------------------------------------------------------------
|
|
318
|
+
// Helpers
|
|
319
|
+
// -----------------------------------------------------------------------
|
|
320
|
+
/**
|
|
321
|
+
* Get basenames of child mount points under a directory.
|
|
322
|
+
* For example, if mounts exist at /dev and /proc, calling with "/" returns ["dev", "proc"].
|
|
323
|
+
*/
|
|
324
|
+
getChildMountBasenames(dirPath) {
|
|
325
|
+
const names = [];
|
|
326
|
+
for (const mount of this.mounts) {
|
|
327
|
+
if (mount.path === "/")
|
|
328
|
+
continue;
|
|
329
|
+
const mountParent = parentPath(mount.path);
|
|
330
|
+
if (mountParent === dirPath) {
|
|
331
|
+
names.push(basename(mount.path));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return names;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Synchronous open preparation (O_CREAT, O_EXCL, O_TRUNC).
|
|
338
|
+
* Delegates to the underlying backend's prepareOpenSync if it exists.
|
|
339
|
+
*/
|
|
340
|
+
prepareOpenSync(path, flags) {
|
|
341
|
+
const { mount, relativePath } = this.resolve(path);
|
|
342
|
+
if (flags & ~0 && mount.readOnly) {
|
|
343
|
+
// Check for write flags (O_CREAT=0o100, O_TRUNC=0o1000)
|
|
344
|
+
const O_CREAT = 0o100;
|
|
345
|
+
const O_TRUNC = 0o1000;
|
|
346
|
+
if ((flags & O_CREAT) || (flags & O_TRUNC)) {
|
|
347
|
+
this.assertWritable(mount, path);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const backend = mount.fs;
|
|
351
|
+
return backend.prepareOpenSync?.(relativePath, flags) ?? false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Permissions } from "./types.js";
|
|
8
8
|
import type { VirtualFileSystem } from "./vfs.js";
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a filesystem path for permission checks.
|
|
11
|
+
*
|
|
12
|
+
* Resolves `.` and `..` components and collapses repeated slashes so that
|
|
13
|
+
* permission callbacks always see the canonical path. Without this,
|
|
14
|
+
* `/home/user/project/../../../etc/passwd` would pass a naive
|
|
15
|
+
* `startsWith('/home/user/project')` check.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeFsPath(p: string): string;
|
|
9
18
|
/**
|
|
10
19
|
* Wrap a VFS with permission checks on every operation.
|
|
11
20
|
*/
|
|
@@ -18,12 +18,43 @@ function fsError(op, path, reason) {
|
|
|
18
18
|
: `permission denied, ${op} '${path ?? ""}'`;
|
|
19
19
|
return new KernelError("EACCES", msg);
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a filesystem path for permission checks.
|
|
23
|
+
*
|
|
24
|
+
* Resolves `.` and `..` components and collapses repeated slashes so that
|
|
25
|
+
* permission callbacks always see the canonical path. Without this,
|
|
26
|
+
* `/home/user/project/../../../etc/passwd` would pass a naive
|
|
27
|
+
* `startsWith('/home/user/project')` check.
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeFsPath(p) {
|
|
30
|
+
// Collapse repeated slashes
|
|
31
|
+
let cleaned = p.replace(/\/+/g, "/");
|
|
32
|
+
if (cleaned.length > 1 && cleaned.endsWith("/")) {
|
|
33
|
+
cleaned = cleaned.slice(0, -1);
|
|
34
|
+
}
|
|
35
|
+
const isAbsolute = cleaned.startsWith("/");
|
|
36
|
+
const parts = cleaned.split("/");
|
|
37
|
+
const resolved = [];
|
|
38
|
+
for (const seg of parts) {
|
|
39
|
+
if (seg === "" || seg === ".")
|
|
40
|
+
continue;
|
|
41
|
+
if (seg === "..") {
|
|
42
|
+
if (resolved.length > 0)
|
|
43
|
+
resolved.pop();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
resolved.push(seg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const result = (isAbsolute ? "/" : "") + resolved.join("/");
|
|
50
|
+
return result || (isAbsolute ? "/" : ".");
|
|
51
|
+
}
|
|
21
52
|
/**
|
|
22
53
|
* Wrap a VFS with permission checks on every operation.
|
|
23
54
|
*/
|
|
24
55
|
export function wrapFileSystem(fs, permissions) {
|
|
25
56
|
const check = (op, path) => {
|
|
26
|
-
checkPermission(permissions?.fs, { op, path }, (req, reason) => fsError(op, req.path, reason));
|
|
57
|
+
checkPermission(permissions?.fs, { op, path: normalizeFsPath(path) }, (req, reason) => fsError(op, req.path, reason));
|
|
27
58
|
};
|
|
28
59
|
const wrapped = {
|
|
29
60
|
prepareOpenSync: (path, flags) => {
|
|
@@ -59,6 +90,7 @@ export function wrapFileSystem(fs, permissions) {
|
|
|
59
90
|
utimes: async (path, atime, mtime) => { check("utimes", path); return fs.utimes(path, atime, mtime); },
|
|
60
91
|
truncate: async (path, length) => { check("truncate", path); return fs.truncate(path, length); },
|
|
61
92
|
pread: async (path, offset, length) => { check("read", path); return fs.pread(path, offset, length); },
|
|
93
|
+
pwrite: async (path, offset, data) => { check("write", path); return fs.pwrite(path, offset, data); },
|
|
62
94
|
};
|
|
63
95
|
return wrapped;
|
|
64
96
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proc backend.
|
|
3
|
+
*
|
|
4
|
+
* Standalone VirtualFileSystem that handles /proc paths.
|
|
5
|
+
* Receives relative paths (e.g. "self/fd" not "/proc/self/fd").
|
|
6
|
+
* Designed to be mounted at /proc via MountTable.
|
|
7
|
+
*/
|
|
8
|
+
import type { FDTableManager } from "./fd-table.js";
|
|
9
|
+
import type { MountEntry } from "./mount-table.js";
|
|
10
|
+
import type { ProcessTable } from "./process-table.js";
|
|
11
|
+
import type { VirtualFileSystem } from "./vfs.js";
|
|
12
|
+
export interface ProcBackendOptions {
|
|
13
|
+
processTable: ProcessTable;
|
|
14
|
+
fdTableManager: FDTableManager;
|
|
15
|
+
hostname?: string;
|
|
16
|
+
mountTable?: {
|
|
17
|
+
getMounts(): ReadonlyArray<MountEntry>;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve /proc/self references to the given PID.
|
|
22
|
+
* Paths are relative (no /proc prefix).
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveProcSelfPath(path: string, pid: number): string;
|
|
25
|
+
/**
|
|
26
|
+
* Create a standalone proc backend VFS.
|
|
27
|
+
* All paths are relative to /proc (e.g. "self/fd", "1/environ", "mounts").
|
|
28
|
+
* Mount at /proc via MountTable.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createProcBackend(options: ProcBackendOptions): VirtualFileSystem;
|