@secure-exec/core 0.1.1-rc.3 → 0.2.0-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/esm-compiler.d.ts +5 -1
- package/dist/esm-compiler.js +5 -1
- package/dist/fs-helpers.d.ts +1 -1
- package/dist/generated/isolate-runtime.d.ts +15 -15
- package/dist/generated/isolate-runtime.js +15 -15
- package/dist/index.d.ts +24 -5
- package/dist/index.js +23 -3
- package/dist/isolate-runtime/apply-custom-global-policy.js +3 -3
- package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +2 -2
- package/dist/isolate-runtime/apply-timing-mitigation-off.js +2 -2
- package/dist/isolate-runtime/bridge-attach.js +2 -2
- package/dist/isolate-runtime/bridge-initial-globals.js +145 -6
- package/dist/isolate-runtime/eval-script-result.js +1 -1
- package/dist/isolate-runtime/global-exposure-helpers.js +2 -2
- package/dist/isolate-runtime/init-commonjs-module-globals.js +2 -2
- package/dist/isolate-runtime/override-process-cwd.js +1 -1
- package/dist/isolate-runtime/override-process-env.js +1 -1
- package/dist/isolate-runtime/require-setup.js +1600 -338
- package/dist/isolate-runtime/set-commonjs-file-globals.js +2 -2
- package/dist/isolate-runtime/set-stdin-data.js +1 -1
- package/dist/isolate-runtime/setup-dynamic-import.js +47 -19
- package/dist/isolate-runtime/setup-fs-facade.js +62 -23
- package/dist/kernel/command-registry.d.ts +44 -0
- package/dist/kernel/command-registry.js +114 -0
- package/dist/kernel/device-layer.d.ts +12 -0
- package/dist/kernel/device-layer.js +262 -0
- package/dist/kernel/dns-cache.d.ts +29 -0
- package/dist/kernel/dns-cache.js +52 -0
- package/dist/kernel/fd-table.d.ts +84 -0
- package/dist/kernel/fd-table.js +278 -0
- package/dist/kernel/file-lock.d.ts +34 -0
- package/dist/kernel/file-lock.js +123 -0
- package/dist/kernel/host-adapter.d.ts +50 -0
- package/dist/kernel/host-adapter.js +8 -0
- package/dist/kernel/index.d.ts +36 -0
- package/dist/kernel/index.js +34 -0
- package/dist/kernel/inode-table.d.ts +43 -0
- package/dist/kernel/inode-table.js +85 -0
- package/dist/kernel/kernel.d.ts +9 -0
- package/dist/kernel/kernel.js +1396 -0
- package/dist/kernel/permissions.d.ts +27 -0
- package/dist/kernel/permissions.js +118 -0
- package/dist/kernel/pipe-manager.d.ts +64 -0
- package/dist/kernel/pipe-manager.js +267 -0
- package/dist/kernel/proc-layer.d.ts +11 -0
- package/dist/kernel/proc-layer.js +501 -0
- package/dist/kernel/process-table.d.ts +124 -0
- package/dist/kernel/process-table.js +631 -0
- package/dist/kernel/pty.d.ts +108 -0
- package/dist/kernel/pty.js +541 -0
- package/dist/kernel/socket-table.d.ts +305 -0
- package/dist/kernel/socket-table.js +1124 -0
- package/dist/kernel/timer-table.d.ts +54 -0
- package/dist/kernel/timer-table.js +108 -0
- package/dist/kernel/types.d.ts +500 -0
- package/dist/kernel/types.js +89 -0
- package/dist/kernel/user.d.ts +29 -0
- package/dist/kernel/user.js +35 -0
- package/dist/kernel/vfs.d.ts +54 -0
- package/dist/kernel/vfs.js +8 -0
- package/dist/kernel/wait.d.ts +45 -0
- package/dist/kernel/wait.js +112 -0
- package/dist/kernel/wstatus.d.ts +21 -0
- package/dist/kernel/wstatus.js +33 -0
- package/dist/module-resolver.d.ts +4 -0
- package/dist/module-resolver.js +4 -0
- package/dist/package-bundler.d.ts +6 -1
- package/dist/runtime-driver.d.ts +3 -1
- package/dist/shared/bridge-contract.d.ts +329 -20
- package/dist/shared/bridge-contract.js +60 -5
- package/dist/shared/console-formatter.js +8 -4
- package/dist/shared/global-exposure.js +269 -19
- package/dist/shared/in-memory-fs.d.ts +30 -11
- package/dist/shared/in-memory-fs.js +383 -109
- package/dist/shared/permissions.d.ts +4 -6
- package/dist/shared/permissions.js +19 -39
- package/dist/types.d.ts +8 -159
- package/dist/types.js +5 -0
- package/package.json +12 -22
- package/dist/bridge/active-handles.d.ts +0 -22
- package/dist/bridge/active-handles.js +0 -55
- package/dist/bridge/child-process.d.ts +0 -99
- package/dist/bridge/child-process.js +0 -670
- package/dist/bridge/fs.d.ts +0 -281
- package/dist/bridge/fs.js +0 -2235
- package/dist/bridge/index.d.ts +0 -10
- package/dist/bridge/index.js +0 -41
- package/dist/bridge/module.d.ts +0 -75
- package/dist/bridge/module.js +0 -308
- package/dist/bridge/network.d.ts +0 -350
- package/dist/bridge/network.js +0 -2050
- package/dist/bridge/os.d.ts +0 -13
- package/dist/bridge/os.js +0 -256
- package/dist/bridge/polyfills.d.ts +0 -2
- package/dist/bridge/polyfills.js +0 -11
- package/dist/bridge/process.d.ts +0 -89
- package/dist/bridge/process.js +0 -1015
- package/dist/bridge.js +0 -12496
- package/dist/python-runtime.d.ts +0 -16
- package/dist/python-runtime.js +0 -45
- package/dist/runtime.d.ts +0 -31
- package/dist/runtime.js +0 -69
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { InodeTable } from "../kernel/inode-table.js";
|
|
2
|
+
import { KernelError, O_CREAT, O_EXCL, O_TRUNC } from "../kernel/types.js";
|
|
1
3
|
const S_IFREG = 0o100000;
|
|
2
4
|
const S_IFDIR = 0o040000;
|
|
3
5
|
const S_IFLNK = 0o120000;
|
|
6
|
+
const S_IFSOCK = 0o140000;
|
|
4
7
|
function normalizePath(path) {
|
|
5
8
|
if (!path)
|
|
6
9
|
return "/";
|
|
@@ -22,53 +25,127 @@ function dirname(path) {
|
|
|
22
25
|
return `/${parts.slice(0, -1).join("/")}`;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
|
-
* A fully in-memory VirtualFileSystem backed by Maps.
|
|
28
|
+
* A fully in-memory VirtualFileSystem backed by inode-aware Maps.
|
|
26
29
|
* Used as the default filesystem for the browser sandbox and for tests.
|
|
27
30
|
* Paths are always POSIX-style (forward slashes, rooted at "/").
|
|
28
31
|
*/
|
|
29
32
|
export class InMemoryFileSystem {
|
|
33
|
+
inodeTable;
|
|
30
34
|
files = new Map();
|
|
31
|
-
|
|
35
|
+
fileContents = new Map();
|
|
36
|
+
dirs = new Map();
|
|
32
37
|
symlinks = new Map();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
constructor(inodeTable = new InodeTable()) {
|
|
39
|
+
this.inodeTable = inodeTable;
|
|
40
|
+
this.dirs.set("/", this.allocateDirectoryInode().ino);
|
|
41
|
+
}
|
|
42
|
+
// Rebind the filesystem to the kernel's shared inode table.
|
|
43
|
+
setInodeTable(inodeTable) {
|
|
44
|
+
if (this.inodeTable === inodeTable)
|
|
45
|
+
return;
|
|
46
|
+
const oldTable = this.inodeTable;
|
|
47
|
+
this.inodeTable = inodeTable;
|
|
48
|
+
this.reindexInodes(oldTable);
|
|
49
|
+
}
|
|
50
|
+
getInodeForPath(path) {
|
|
51
|
+
const normalized = normalizePath(path);
|
|
52
|
+
const resolved = this.resolveSymlink(normalized);
|
|
53
|
+
return this.files.get(resolved) ?? this.dirs.get(resolved) ?? null;
|
|
54
|
+
}
|
|
55
|
+
readFileByInode(ino) {
|
|
56
|
+
const data = this.fileContents.get(ino);
|
|
57
|
+
if (!data) {
|
|
58
|
+
throw new Error(`ENOENT: inode ${ino} has no file data`);
|
|
59
|
+
}
|
|
60
|
+
this.requireInode(ino).atime = new Date();
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
writeFileByInode(ino, content) {
|
|
64
|
+
this.requireFileInode(ino);
|
|
65
|
+
this.fileContents.set(ino, content);
|
|
66
|
+
this.updateFileMetadata(ino, content.byteLength);
|
|
67
|
+
}
|
|
68
|
+
preadByInode(ino, offset, length) {
|
|
69
|
+
const data = this.readFileByInode(ino);
|
|
70
|
+
return data.slice(offset, offset + length);
|
|
71
|
+
}
|
|
72
|
+
statByInode(ino) {
|
|
73
|
+
return this.statForInode(this.requireInode(ino));
|
|
74
|
+
}
|
|
75
|
+
deleteInodeData(ino) {
|
|
76
|
+
this.fileContents.delete(ino);
|
|
77
|
+
}
|
|
37
78
|
listDirEntries(path) {
|
|
38
79
|
const normalized = normalizePath(path);
|
|
39
|
-
|
|
80
|
+
const dirIno = this.dirs.get(normalized);
|
|
81
|
+
if (dirIno === undefined) {
|
|
40
82
|
throw new Error(`ENOENT: no such file or directory, scandir '${normalized}'`);
|
|
41
83
|
}
|
|
42
84
|
const prefix = normalized === "/" ? "/" : `${normalized}/`;
|
|
43
85
|
const entries = new Map();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
86
|
+
const parentPath = normalized === "/" ? "/" : dirname(normalized);
|
|
87
|
+
const parentIno = this.dirs.get(parentPath) ?? dirIno;
|
|
88
|
+
entries.set(".", {
|
|
89
|
+
name: ".",
|
|
90
|
+
isDirectory: true,
|
|
91
|
+
isSymbolicLink: false,
|
|
92
|
+
ino: dirIno,
|
|
93
|
+
});
|
|
94
|
+
entries.set("..", {
|
|
95
|
+
name: "..",
|
|
96
|
+
isDirectory: true,
|
|
97
|
+
isSymbolicLink: false,
|
|
98
|
+
ino: parentIno,
|
|
99
|
+
});
|
|
100
|
+
for (const [filePath, ino] of this.files.entries()) {
|
|
101
|
+
if (!filePath.startsWith(prefix))
|
|
102
|
+
continue;
|
|
103
|
+
const rest = filePath.slice(prefix.length);
|
|
104
|
+
if (rest && !rest.includes("/")) {
|
|
105
|
+
entries.set(rest, {
|
|
106
|
+
name: rest,
|
|
107
|
+
isDirectory: false,
|
|
108
|
+
isSymbolicLink: false,
|
|
109
|
+
ino,
|
|
110
|
+
});
|
|
50
111
|
}
|
|
51
112
|
}
|
|
52
|
-
for (const dirPath of this.dirs.
|
|
53
|
-
if (dirPath.startsWith(prefix))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
113
|
+
for (const [dirPath, ino] of this.dirs.entries()) {
|
|
114
|
+
if (!dirPath.startsWith(prefix))
|
|
115
|
+
continue;
|
|
116
|
+
const rest = dirPath.slice(prefix.length);
|
|
117
|
+
if (rest && !rest.includes("/")) {
|
|
118
|
+
entries.set(rest, {
|
|
119
|
+
name: rest,
|
|
120
|
+
isDirectory: true,
|
|
121
|
+
isSymbolicLink: false,
|
|
122
|
+
ino,
|
|
123
|
+
});
|
|
58
124
|
}
|
|
59
125
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
126
|
+
for (const linkPath of this.symlinks.keys()) {
|
|
127
|
+
if (!linkPath.startsWith(prefix))
|
|
128
|
+
continue;
|
|
129
|
+
const rest = linkPath.slice(prefix.length);
|
|
130
|
+
if (rest && !rest.includes("/")) {
|
|
131
|
+
entries.set(rest, {
|
|
132
|
+
name: rest,
|
|
133
|
+
isDirectory: false,
|
|
134
|
+
isSymbolicLink: true,
|
|
135
|
+
ino: 0,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return Array.from(entries.values());
|
|
64
140
|
}
|
|
65
141
|
async readFile(path) {
|
|
66
142
|
const normalized = normalizePath(path);
|
|
67
|
-
const
|
|
68
|
-
|
|
143
|
+
const resolved = this.resolveSymlink(normalized);
|
|
144
|
+
const ino = this.files.get(resolved);
|
|
145
|
+
if (ino === undefined) {
|
|
69
146
|
throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
|
|
70
147
|
}
|
|
71
|
-
return
|
|
148
|
+
return this.readFileByInode(ino);
|
|
72
149
|
}
|
|
73
150
|
async readTextFile(path) {
|
|
74
151
|
const data = await this.readFile(path);
|
|
@@ -84,7 +161,56 @@ export class InMemoryFileSystem {
|
|
|
84
161
|
const normalized = normalizePath(path);
|
|
85
162
|
await this.mkdir(dirname(normalized));
|
|
86
163
|
const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
87
|
-
this.
|
|
164
|
+
const resolved = this.resolveIfSymlink(normalized) ?? normalized;
|
|
165
|
+
const existing = this.files.get(resolved);
|
|
166
|
+
if (existing !== undefined) {
|
|
167
|
+
this.writeFileByInode(existing, data);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const inode = this.allocateFileInode();
|
|
171
|
+
this.files.set(resolved, inode.ino);
|
|
172
|
+
this.fileContents.set(inode.ino, data);
|
|
173
|
+
this.updateFileMetadata(inode.ino, data.byteLength);
|
|
174
|
+
}
|
|
175
|
+
prepareOpenSync(path, flags) {
|
|
176
|
+
const normalized = normalizePath(path);
|
|
177
|
+
const resolved = this.resolveIfSymlink(normalized) ?? normalized;
|
|
178
|
+
const hasCreate = (flags & O_CREAT) !== 0;
|
|
179
|
+
const hasExcl = (flags & O_EXCL) !== 0;
|
|
180
|
+
const hasTrunc = (flags & O_TRUNC) !== 0;
|
|
181
|
+
const fileIno = this.files.get(resolved);
|
|
182
|
+
const exists = fileIno !== undefined || this.dirs.has(resolved) || this.symlinks.has(normalized);
|
|
183
|
+
if (hasCreate && hasExcl && exists) {
|
|
184
|
+
throw new KernelError("EEXIST", `file already exists, open '${normalized}'`);
|
|
185
|
+
}
|
|
186
|
+
let created = false;
|
|
187
|
+
if (fileIno === undefined && hasCreate) {
|
|
188
|
+
const parts = splitPath(dirname(resolved));
|
|
189
|
+
let current = "";
|
|
190
|
+
for (const part of parts) {
|
|
191
|
+
current += `/${part}`;
|
|
192
|
+
if (!this.dirs.has(current)) {
|
|
193
|
+
this.dirs.set(current, this.allocateDirectoryInode().ino);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const inode = this.allocateFileInode();
|
|
197
|
+
this.files.set(resolved, inode.ino);
|
|
198
|
+
this.fileContents.set(inode.ino, new Uint8Array(0));
|
|
199
|
+
this.updateFileMetadata(inode.ino, 0);
|
|
200
|
+
created = true;
|
|
201
|
+
}
|
|
202
|
+
if (hasTrunc) {
|
|
203
|
+
if (this.dirs.has(resolved)) {
|
|
204
|
+
throw new KernelError("EISDIR", `illegal operation on a directory, open '${normalized}'`);
|
|
205
|
+
}
|
|
206
|
+
const truncateIno = this.files.get(resolved);
|
|
207
|
+
if (truncateIno === undefined) {
|
|
208
|
+
throw new KernelError("ENOENT", `no such file or directory, open '${normalized}'`);
|
|
209
|
+
}
|
|
210
|
+
this.fileContents.set(truncateIno, new Uint8Array(0));
|
|
211
|
+
this.updateFileMetadata(truncateIno, 0);
|
|
212
|
+
}
|
|
213
|
+
return created;
|
|
88
214
|
}
|
|
89
215
|
async createDir(path) {
|
|
90
216
|
const normalized = normalizePath(path);
|
|
@@ -92,59 +218,60 @@ export class InMemoryFileSystem {
|
|
|
92
218
|
if (!this.dirs.has(parent)) {
|
|
93
219
|
throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
|
|
94
220
|
}
|
|
95
|
-
this.dirs.
|
|
221
|
+
if (!this.dirs.has(normalized)) {
|
|
222
|
+
this.dirs.set(normalized, this.allocateDirectoryInode().ino);
|
|
223
|
+
}
|
|
96
224
|
}
|
|
97
|
-
async mkdir(path) {
|
|
225
|
+
async mkdir(path, _options) {
|
|
98
226
|
const parts = splitPath(path);
|
|
99
227
|
let current = "";
|
|
100
228
|
for (const part of parts) {
|
|
101
229
|
current += `/${part}`;
|
|
102
230
|
if (!this.dirs.has(current)) {
|
|
103
|
-
this.dirs.
|
|
231
|
+
this.dirs.set(current, this.allocateDirectoryInode().ino);
|
|
104
232
|
}
|
|
105
233
|
}
|
|
106
234
|
}
|
|
235
|
+
resolveIfSymlink(normalized) {
|
|
236
|
+
return this.symlinks.has(normalized) ? this.resolveSymlink(normalized) : null;
|
|
237
|
+
}
|
|
107
238
|
resolveSymlink(normalized, maxDepth = 16) {
|
|
108
239
|
let current = normalized;
|
|
109
240
|
for (let i = 0; i < maxDepth; i++) {
|
|
110
241
|
const target = this.symlinks.get(current);
|
|
111
242
|
if (!target)
|
|
112
243
|
return current;
|
|
113
|
-
current = target.startsWith("/")
|
|
244
|
+
current = target.startsWith("/")
|
|
245
|
+
? normalizePath(target)
|
|
246
|
+
: normalizePath(`${dirname(current)}/${target}`);
|
|
114
247
|
}
|
|
115
248
|
throw new Error(`ELOOP: too many levels of symbolic links, stat '${normalized}'`);
|
|
116
249
|
}
|
|
250
|
+
statForInode(inode) {
|
|
251
|
+
const isDirectory = (inode.mode & 0o170000) === S_IFDIR;
|
|
252
|
+
return {
|
|
253
|
+
mode: inode.mode,
|
|
254
|
+
size: isDirectory ? 4096 : inode.size,
|
|
255
|
+
isDirectory,
|
|
256
|
+
isSymbolicLink: false,
|
|
257
|
+
atimeMs: inode.atime.getTime(),
|
|
258
|
+
mtimeMs: inode.mtime.getTime(),
|
|
259
|
+
ctimeMs: inode.ctime.getTime(),
|
|
260
|
+
birthtimeMs: inode.birthtime.getTime(),
|
|
261
|
+
ino: inode.ino,
|
|
262
|
+
nlink: inode.nlink,
|
|
263
|
+
uid: inode.uid,
|
|
264
|
+
gid: inode.gid,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
117
267
|
statEntry(normalized) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const customMode = this.modes.get(normalized);
|
|
122
|
-
const atimeMs = ts?.atimeMs ?? now;
|
|
123
|
-
const mtimeMs = ts?.mtimeMs ?? now;
|
|
124
|
-
const file = this.files.get(normalized);
|
|
125
|
-
if (file) {
|
|
126
|
-
return {
|
|
127
|
-
mode: customMode ?? (S_IFREG | 0o644),
|
|
128
|
-
size: file.byteLength,
|
|
129
|
-
isDirectory: false,
|
|
130
|
-
isSymbolicLink: false,
|
|
131
|
-
atimeMs,
|
|
132
|
-
mtimeMs,
|
|
133
|
-
ctimeMs: now,
|
|
134
|
-
birthtimeMs: now,
|
|
135
|
-
};
|
|
268
|
+
const fileIno = this.files.get(normalized);
|
|
269
|
+
if (fileIno !== undefined) {
|
|
270
|
+
return this.statByInode(fileIno);
|
|
136
271
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
size: 4096,
|
|
141
|
-
isDirectory: true,
|
|
142
|
-
isSymbolicLink: false,
|
|
143
|
-
atimeMs,
|
|
144
|
-
mtimeMs,
|
|
145
|
-
ctimeMs: now,
|
|
146
|
-
birthtimeMs: now,
|
|
147
|
-
};
|
|
272
|
+
const dirIno = this.dirs.get(normalized);
|
|
273
|
+
if (dirIno !== undefined) {
|
|
274
|
+
return this.statByInode(dirIno);
|
|
148
275
|
}
|
|
149
276
|
throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
|
|
150
277
|
}
|
|
@@ -152,8 +279,8 @@ export class InMemoryFileSystem {
|
|
|
152
279
|
const normalized = normalizePath(path);
|
|
153
280
|
if (this.symlinks.has(normalized)) {
|
|
154
281
|
try {
|
|
155
|
-
this.resolveSymlink(normalized);
|
|
156
|
-
return
|
|
282
|
+
const resolved = this.resolveSymlink(normalized);
|
|
283
|
+
return this.files.has(resolved) || this.dirs.has(resolved);
|
|
157
284
|
}
|
|
158
285
|
catch {
|
|
159
286
|
return false;
|
|
@@ -168,9 +295,20 @@ export class InMemoryFileSystem {
|
|
|
168
295
|
}
|
|
169
296
|
async removeFile(path) {
|
|
170
297
|
const normalized = normalizePath(path);
|
|
171
|
-
if (
|
|
298
|
+
if (this.symlinks.delete(normalized)) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const resolved = this.resolveSymlink(normalized);
|
|
302
|
+
const ino = this.files.get(resolved);
|
|
303
|
+
if (ino === undefined) {
|
|
172
304
|
throw new Error(`ENOENT: no such file or directory, unlink '${normalized}'`);
|
|
173
305
|
}
|
|
306
|
+
this.files.delete(resolved);
|
|
307
|
+
this.inodeTable.decrementLinks(ino);
|
|
308
|
+
if (this.inodeTable.shouldDelete(ino)) {
|
|
309
|
+
this.deleteInodeData(ino);
|
|
310
|
+
this.inodeTable.delete(ino);
|
|
311
|
+
}
|
|
174
312
|
}
|
|
175
313
|
async removeDir(path) {
|
|
176
314
|
const normalized = normalizePath(path);
|
|
@@ -186,12 +324,22 @@ export class InMemoryFileSystem {
|
|
|
186
324
|
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
187
325
|
}
|
|
188
326
|
}
|
|
189
|
-
for (const dirPath of this.dirs.
|
|
327
|
+
for (const dirPath of this.dirs.keys()) {
|
|
190
328
|
if (dirPath !== normalized && dirPath.startsWith(prefix)) {
|
|
191
329
|
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
192
330
|
}
|
|
193
331
|
}
|
|
332
|
+
for (const linkPath of this.symlinks.keys()) {
|
|
333
|
+
if (linkPath.startsWith(prefix)) {
|
|
334
|
+
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const ino = this.dirs.get(normalized);
|
|
194
338
|
this.dirs.delete(normalized);
|
|
339
|
+
this.inodeTable.decrementLinks(ino);
|
|
340
|
+
if (this.inodeTable.shouldDelete(ino)) {
|
|
341
|
+
this.inodeTable.delete(ino);
|
|
342
|
+
}
|
|
195
343
|
}
|
|
196
344
|
async rename(oldPath, newPath) {
|
|
197
345
|
const oldNormalized = normalizePath(oldPath);
|
|
@@ -206,9 +354,23 @@ export class InMemoryFileSystem {
|
|
|
206
354
|
if (this.dirs.has(newNormalized)) {
|
|
207
355
|
throw new Error(`EISDIR: illegal operation on a directory, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
208
356
|
}
|
|
209
|
-
|
|
210
|
-
|
|
357
|
+
if (this.files.has(newNormalized) || this.symlinks.has(newNormalized)) {
|
|
358
|
+
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
359
|
+
}
|
|
360
|
+
const ino = this.files.get(oldNormalized);
|
|
211
361
|
this.files.delete(oldNormalized);
|
|
362
|
+
this.files.set(newNormalized, ino);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (this.symlinks.has(oldNormalized)) {
|
|
366
|
+
if (this.files.has(newNormalized) ||
|
|
367
|
+
this.dirs.has(newNormalized) ||
|
|
368
|
+
this.symlinks.has(newNormalized)) {
|
|
369
|
+
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
370
|
+
}
|
|
371
|
+
const target = this.symlinks.get(oldNormalized);
|
|
372
|
+
this.symlinks.delete(oldNormalized);
|
|
373
|
+
this.symlinks.set(newNormalized, target);
|
|
212
374
|
return;
|
|
213
375
|
}
|
|
214
376
|
if (!this.dirs.has(oldNormalized)) {
|
|
@@ -220,34 +382,42 @@ export class InMemoryFileSystem {
|
|
|
220
382
|
if (newNormalized.startsWith(`${oldNormalized}/`)) {
|
|
221
383
|
throw new Error(`EINVAL: invalid argument, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
222
384
|
}
|
|
223
|
-
if (this.dirs.has(newNormalized) ||
|
|
385
|
+
if (this.dirs.has(newNormalized) ||
|
|
386
|
+
this.files.has(newNormalized) ||
|
|
387
|
+
this.symlinks.has(newNormalized)) {
|
|
224
388
|
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
225
389
|
}
|
|
226
390
|
const sourcePrefix = `${oldNormalized}/`;
|
|
227
391
|
const targetPrefix = `${newNormalized}/`;
|
|
228
|
-
const
|
|
229
|
-
.filter((path) => path === oldNormalized || path.startsWith(sourcePrefix))
|
|
230
|
-
.sort((a, b) => a.length - b.length);
|
|
231
|
-
const
|
|
232
|
-
|
|
392
|
+
const dirEntries = Array.from(this.dirs.entries())
|
|
393
|
+
.filter(([path]) => path === oldNormalized || path.startsWith(sourcePrefix))
|
|
394
|
+
.sort(([a], [b]) => a.length - b.length);
|
|
395
|
+
const fileEntries = Array.from(this.files.entries()).filter(([path]) => path.startsWith(sourcePrefix));
|
|
396
|
+
const symlinkEntries = Array.from(this.symlinks.entries()).filter(([path]) => path.startsWith(sourcePrefix));
|
|
397
|
+
for (const [path] of dirEntries)
|
|
233
398
|
this.dirs.delete(path);
|
|
234
|
-
|
|
235
|
-
for (const path of filePaths) {
|
|
236
|
-
const content = this.files.get(path);
|
|
399
|
+
for (const [path] of fileEntries)
|
|
237
400
|
this.files.delete(path);
|
|
238
|
-
|
|
401
|
+
for (const [path] of symlinkEntries)
|
|
402
|
+
this.symlinks.delete(path);
|
|
403
|
+
for (const [path, ino] of dirEntries) {
|
|
404
|
+
const nextPath = path === oldNormalized
|
|
405
|
+
? newNormalized
|
|
406
|
+
: `${targetPrefix}${path.slice(sourcePrefix.length)}`;
|
|
407
|
+
this.dirs.set(nextPath, ino);
|
|
239
408
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
this.dirs.add(`${targetPrefix}${path.slice(sourcePrefix.length)}`);
|
|
409
|
+
for (const [path, ino] of fileEntries) {
|
|
410
|
+
this.files.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, ino);
|
|
411
|
+
}
|
|
412
|
+
for (const [path, target] of symlinkEntries) {
|
|
413
|
+
this.symlinks.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, target);
|
|
246
414
|
}
|
|
247
415
|
}
|
|
248
416
|
async symlink(target, linkPath) {
|
|
249
417
|
const normalized = normalizePath(linkPath);
|
|
250
|
-
if (this.files.has(normalized) ||
|
|
418
|
+
if (this.files.has(normalized) ||
|
|
419
|
+
this.dirs.has(normalized) ||
|
|
420
|
+
this.symlinks.has(normalized)) {
|
|
251
421
|
throw new Error(`EEXIST: file already exists, symlink '${target}' -> '${normalized}'`);
|
|
252
422
|
}
|
|
253
423
|
await this.mkdir(dirname(normalized));
|
|
@@ -275,6 +445,10 @@ export class InMemoryFileSystem {
|
|
|
275
445
|
mtimeMs: now,
|
|
276
446
|
ctimeMs: now,
|
|
277
447
|
birthtimeMs: now,
|
|
448
|
+
ino: 0,
|
|
449
|
+
nlink: 1,
|
|
450
|
+
uid: 0,
|
|
451
|
+
gid: 0,
|
|
278
452
|
};
|
|
279
453
|
}
|
|
280
454
|
return this.statEntry(normalized);
|
|
@@ -282,58 +456,158 @@ export class InMemoryFileSystem {
|
|
|
282
456
|
async link(oldPath, newPath) {
|
|
283
457
|
const oldNormalized = normalizePath(oldPath);
|
|
284
458
|
const newNormalized = normalizePath(newPath);
|
|
285
|
-
const
|
|
286
|
-
|
|
459
|
+
const resolved = this.resolveSymlink(oldNormalized);
|
|
460
|
+
const ino = this.files.get(resolved);
|
|
461
|
+
if (ino === undefined) {
|
|
287
462
|
throw new Error(`ENOENT: no such file or directory, link '${oldNormalized}' -> '${newNormalized}'`);
|
|
288
463
|
}
|
|
289
|
-
if (this.files.has(newNormalized) ||
|
|
464
|
+
if (this.files.has(newNormalized) ||
|
|
465
|
+
this.dirs.has(newNormalized) ||
|
|
466
|
+
this.symlinks.has(newNormalized)) {
|
|
290
467
|
throw new Error(`EEXIST: file already exists, link '${oldNormalized}' -> '${newNormalized}'`);
|
|
291
468
|
}
|
|
292
469
|
await this.mkdir(dirname(newNormalized));
|
|
293
|
-
this.files.set(newNormalized,
|
|
294
|
-
this.
|
|
470
|
+
this.files.set(newNormalized, ino);
|
|
471
|
+
this.inodeTable.incrementLinks(ino);
|
|
295
472
|
}
|
|
296
473
|
async chmod(path, mode) {
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
300
|
-
|
|
474
|
+
const inode = this.requirePathInode(path, "chmod");
|
|
475
|
+
const callerTypeBits = mode & 0o170000;
|
|
476
|
+
if (callerTypeBits !== 0) {
|
|
477
|
+
inode.mode = mode;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const existingTypeBits = inode.mode & 0o170000;
|
|
481
|
+
inode.mode = existingTypeBits | (mode & 0o7777);
|
|
301
482
|
}
|
|
302
|
-
|
|
303
|
-
const typeBits = existing ? (existing & 0o170000) : (this.files.has(resolved) ? S_IFREG : S_IFDIR);
|
|
304
|
-
this.modes.set(resolved, typeBits | (mode & 0o7777));
|
|
483
|
+
inode.ctime = new Date();
|
|
305
484
|
}
|
|
306
485
|
async chown(path, uid, gid) {
|
|
486
|
+
const inode = this.requirePathInode(path, "chown");
|
|
487
|
+
inode.uid = uid;
|
|
488
|
+
inode.gid = gid;
|
|
489
|
+
inode.ctime = new Date();
|
|
490
|
+
}
|
|
491
|
+
async utimes(path, atime, mtime) {
|
|
492
|
+
const inode = this.requirePathInode(path, "utimes");
|
|
493
|
+
inode.atime = new Date(atime * 1000);
|
|
494
|
+
inode.mtime = new Date(mtime * 1000);
|
|
495
|
+
inode.ctime = new Date();
|
|
496
|
+
}
|
|
497
|
+
async realpath(path) {
|
|
307
498
|
const normalized = normalizePath(path);
|
|
308
499
|
const resolved = this.resolveSymlink(normalized);
|
|
309
500
|
if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
|
|
310
|
-
throw new Error(`ENOENT: no such file or directory,
|
|
501
|
+
throw new Error(`ENOENT: no such file or directory, realpath '${normalized}'`);
|
|
311
502
|
}
|
|
312
|
-
|
|
503
|
+
return resolved;
|
|
313
504
|
}
|
|
314
|
-
async
|
|
505
|
+
async pread(path, offset, length) {
|
|
315
506
|
const normalized = normalizePath(path);
|
|
316
507
|
const resolved = this.resolveSymlink(normalized);
|
|
317
|
-
|
|
318
|
-
|
|
508
|
+
const ino = this.files.get(resolved);
|
|
509
|
+
if (ino === undefined) {
|
|
510
|
+
throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
|
|
319
511
|
}
|
|
320
|
-
this.
|
|
512
|
+
return this.preadByInode(ino, offset, length);
|
|
321
513
|
}
|
|
322
514
|
async truncate(path, length) {
|
|
323
515
|
const normalized = normalizePath(path);
|
|
324
516
|
const resolved = this.resolveSymlink(normalized);
|
|
325
|
-
const
|
|
326
|
-
if (
|
|
517
|
+
const ino = this.files.get(resolved);
|
|
518
|
+
if (ino === undefined) {
|
|
327
519
|
throw new Error(`ENOENT: no such file or directory, truncate '${normalized}'`);
|
|
328
520
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
521
|
+
const file = this.readFileByInode(ino);
|
|
522
|
+
const next = length >= file.byteLength
|
|
523
|
+
? (() => {
|
|
524
|
+
const padded = new Uint8Array(length);
|
|
525
|
+
padded.set(file);
|
|
526
|
+
return padded;
|
|
527
|
+
})()
|
|
528
|
+
: file.slice(0, length);
|
|
529
|
+
this.fileContents.set(ino, next);
|
|
530
|
+
this.updateFileMetadata(ino, next.byteLength);
|
|
531
|
+
}
|
|
532
|
+
reindexInodes(oldTable) {
|
|
533
|
+
const oldContents = new Map(this.fileContents);
|
|
534
|
+
const oldFiles = new Map(this.files);
|
|
535
|
+
const oldDirs = Array.from(this.dirs.entries()).sort(([a], [b]) => a.length - b.length);
|
|
536
|
+
const inoMap = new Map();
|
|
537
|
+
this.files = new Map();
|
|
538
|
+
this.fileContents = new Map();
|
|
539
|
+
this.dirs = new Map();
|
|
540
|
+
for (const [dirPath, oldIno] of oldDirs) {
|
|
541
|
+
const ino = this.cloneInode(oldIno, oldTable, S_IFDIR | 0o755).ino;
|
|
542
|
+
this.dirs.set(dirPath, ino);
|
|
543
|
+
}
|
|
544
|
+
if (!this.dirs.has("/")) {
|
|
545
|
+
this.dirs.set("/", this.allocateDirectoryInode().ino);
|
|
546
|
+
}
|
|
547
|
+
for (const [path, oldIno] of oldFiles) {
|
|
548
|
+
const mapped = inoMap.get(oldIno) ?? (() => {
|
|
549
|
+
const inode = this.cloneInode(oldIno, oldTable, S_IFREG | 0o644);
|
|
550
|
+
inoMap.set(oldIno, inode.ino);
|
|
551
|
+
return inode.ino;
|
|
552
|
+
})();
|
|
553
|
+
this.files.set(path, mapped);
|
|
554
|
+
const content = oldContents.get(oldIno);
|
|
555
|
+
if (content) {
|
|
556
|
+
this.fileContents.set(mapped, content);
|
|
557
|
+
this.requireInode(mapped).size = content.byteLength;
|
|
558
|
+
}
|
|
333
559
|
}
|
|
334
|
-
|
|
335
|
-
|
|
560
|
+
}
|
|
561
|
+
cloneInode(oldIno, oldTable, fallbackMode) {
|
|
562
|
+
const source = oldTable.get(oldIno);
|
|
563
|
+
const inode = this.inodeTable.allocate(source?.mode ?? fallbackMode, source?.uid ?? 0, source?.gid ?? 0);
|
|
564
|
+
inode.nlink = source?.nlink ?? 1;
|
|
565
|
+
inode.openRefCount = 0;
|
|
566
|
+
inode.size = source?.size ?? 0;
|
|
567
|
+
inode.atime = source?.atime ? new Date(source.atime) : new Date();
|
|
568
|
+
inode.mtime = source?.mtime ? new Date(source.mtime) : new Date();
|
|
569
|
+
inode.ctime = source?.ctime ? new Date(source.ctime) : new Date();
|
|
570
|
+
inode.birthtime = source?.birthtime ? new Date(source.birthtime) : new Date();
|
|
571
|
+
return inode;
|
|
572
|
+
}
|
|
573
|
+
allocateFileInode() {
|
|
574
|
+
return this.inodeTable.allocate(S_IFREG | 0o644, 0, 0);
|
|
575
|
+
}
|
|
576
|
+
allocateDirectoryInode() {
|
|
577
|
+
const inode = this.inodeTable.allocate(S_IFDIR | 0o755, 0, 0);
|
|
578
|
+
inode.size = 4096;
|
|
579
|
+
return inode;
|
|
580
|
+
}
|
|
581
|
+
updateFileMetadata(ino, size) {
|
|
582
|
+
const inode = this.requireFileInode(ino);
|
|
583
|
+
const now = new Date();
|
|
584
|
+
inode.size = size;
|
|
585
|
+
inode.atime = now;
|
|
586
|
+
inode.mtime = now;
|
|
587
|
+
inode.ctime = now;
|
|
588
|
+
}
|
|
589
|
+
requirePathInode(path, op) {
|
|
590
|
+
const normalized = normalizePath(path);
|
|
591
|
+
const resolved = this.resolveSymlink(normalized);
|
|
592
|
+
const ino = this.files.get(resolved) ?? this.dirs.get(resolved);
|
|
593
|
+
if (ino === undefined) {
|
|
594
|
+
throw new Error(`ENOENT: no such file or directory, ${op} '${normalized}'`);
|
|
595
|
+
}
|
|
596
|
+
return this.requireInode(ino);
|
|
597
|
+
}
|
|
598
|
+
requireFileInode(ino) {
|
|
599
|
+
const inode = this.requireInode(ino);
|
|
600
|
+
if ((inode.mode & 0o170000) !== S_IFREG && (inode.mode & 0o170000) !== S_IFSOCK) {
|
|
601
|
+
throw new Error(`EINVAL: inode ${ino} is not a regular file`);
|
|
602
|
+
}
|
|
603
|
+
return inode;
|
|
604
|
+
}
|
|
605
|
+
requireInode(ino) {
|
|
606
|
+
const inode = this.inodeTable.get(ino);
|
|
607
|
+
if (!inode) {
|
|
608
|
+
throw new Error(`ENOENT: inode ${ino} not found`);
|
|
336
609
|
}
|
|
610
|
+
return inode;
|
|
337
611
|
}
|
|
338
612
|
}
|
|
339
613
|
export function createInMemoryFileSystem() {
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* checks that throw EACCES on denial. When no permission callback is provided
|
|
6
6
|
* for a category, guarded operations in that category are denied by default.
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
8
|
+
import type { EnvAccessRequest, Permissions } from "../kernel/types.js";
|
|
9
|
+
import type { VirtualFileSystem } from "../kernel/vfs.js";
|
|
10
|
+
import type { CommandExecutor, NetworkAdapter } from "../types.js";
|
|
9
11
|
export declare const allowAllFs: Pick<Permissions, "fs">;
|
|
10
12
|
export declare const allowAllNetwork: Pick<Permissions, "network">;
|
|
11
13
|
export declare const allowAllChildProcess: Pick<Permissions, "childProcess">;
|
|
@@ -16,11 +18,7 @@ export declare const allowAll: Permissions;
|
|
|
16
18
|
* Throws EACCES if the permission callback denies or is absent.
|
|
17
19
|
*/
|
|
18
20
|
export declare function wrapFileSystem(fs: VirtualFileSystem, permissions?: Permissions): VirtualFileSystem;
|
|
19
|
-
/**
|
|
20
|
-
* Wrap a NetworkAdapter so externally-originating operations (`listen`, `fetch`,
|
|
21
|
-
* `dns`, `http`) pass through the network permission check.
|
|
22
|
-
* `httpServerClose` is forwarded as-is.
|
|
23
|
-
*/
|
|
21
|
+
/** Wrap a NetworkAdapter so external client operations pass through the network permission check. */
|
|
24
22
|
export declare function wrapNetworkAdapter(adapter: NetworkAdapter, permissions?: Permissions): NetworkAdapter;
|
|
25
23
|
/** Wrap a CommandExecutor so spawn passes through the childProcess permission check. */
|
|
26
24
|
export declare function wrapCommandExecutor(executor: CommandExecutor, permissions?: Permissions): CommandExecutor;
|