@secure-exec/core 0.1.1-rc.3 → 0.2.0-rc.2
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 +2868 -494
- 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 +78 -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 +122 -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 +1393 -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 +312 -0
- package/dist/kernel/socket-table.js +1188 -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 +349 -22
- package/dist/shared/bridge-contract.js +62 -5
- package/dist/shared/console-formatter.js +8 -4
- package/dist/shared/global-exposure.js +364 -19
- package/dist/shared/in-memory-fs.d.ts +33 -11
- package/dist/shared/in-memory-fs.js +439 -130
- 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
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
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
|
+
});
|
|
50
124
|
}
|
|
51
125
|
}
|
|
52
|
-
for (const
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
126
|
+
for (const [linkPath, link] of this.symlinks.entries()) {
|
|
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: link.ino,
|
|
136
|
+
});
|
|
58
137
|
}
|
|
59
138
|
}
|
|
60
|
-
return Array.from(entries.
|
|
61
|
-
name,
|
|
62
|
-
isDirectory,
|
|
63
|
-
}));
|
|
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,54 @@ 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
|
+
this.ensureDirectory(current);
|
|
193
|
+
}
|
|
194
|
+
const inode = this.allocateFileInode();
|
|
195
|
+
this.files.set(resolved, inode.ino);
|
|
196
|
+
this.fileContents.set(inode.ino, new Uint8Array(0));
|
|
197
|
+
this.updateFileMetadata(inode.ino, 0);
|
|
198
|
+
created = true;
|
|
199
|
+
}
|
|
200
|
+
if (hasTrunc) {
|
|
201
|
+
if (this.dirs.has(resolved)) {
|
|
202
|
+
throw new KernelError("EISDIR", `illegal operation on a directory, open '${normalized}'`);
|
|
203
|
+
}
|
|
204
|
+
const truncateIno = this.files.get(resolved);
|
|
205
|
+
if (truncateIno === undefined) {
|
|
206
|
+
throw new KernelError("ENOENT", `no such file or directory, open '${normalized}'`);
|
|
207
|
+
}
|
|
208
|
+
this.fileContents.set(truncateIno, new Uint8Array(0));
|
|
209
|
+
this.updateFileMetadata(truncateIno, 0);
|
|
210
|
+
}
|
|
211
|
+
return created;
|
|
88
212
|
}
|
|
89
213
|
async createDir(path) {
|
|
90
214
|
const normalized = normalizePath(path);
|
|
@@ -92,59 +216,57 @@ export class InMemoryFileSystem {
|
|
|
92
216
|
if (!this.dirs.has(parent)) {
|
|
93
217
|
throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
|
|
94
218
|
}
|
|
95
|
-
this.
|
|
219
|
+
this.ensureDirectory(normalized);
|
|
96
220
|
}
|
|
97
|
-
async mkdir(path) {
|
|
221
|
+
async mkdir(path, _options) {
|
|
98
222
|
const parts = splitPath(path);
|
|
99
223
|
let current = "";
|
|
100
224
|
for (const part of parts) {
|
|
101
225
|
current += `/${part}`;
|
|
102
|
-
|
|
103
|
-
this.dirs.add(current);
|
|
104
|
-
}
|
|
226
|
+
this.ensureDirectory(current);
|
|
105
227
|
}
|
|
106
228
|
}
|
|
229
|
+
resolveIfSymlink(normalized) {
|
|
230
|
+
return this.symlinks.has(normalized) ? this.resolveSymlink(normalized) : null;
|
|
231
|
+
}
|
|
107
232
|
resolveSymlink(normalized, maxDepth = 16) {
|
|
108
233
|
let current = normalized;
|
|
109
234
|
for (let i = 0; i < maxDepth; i++) {
|
|
110
|
-
const
|
|
111
|
-
if (!
|
|
235
|
+
const link = this.symlinks.get(current);
|
|
236
|
+
if (!link)
|
|
112
237
|
return current;
|
|
113
|
-
current = target.startsWith("/")
|
|
238
|
+
current = link.target.startsWith("/")
|
|
239
|
+
? normalizePath(link.target)
|
|
240
|
+
: normalizePath(`${dirname(current)}/${link.target}`);
|
|
114
241
|
}
|
|
115
242
|
throw new Error(`ELOOP: too many levels of symbolic links, stat '${normalized}'`);
|
|
116
243
|
}
|
|
244
|
+
statForInode(inode) {
|
|
245
|
+
const isDirectory = (inode.mode & 0o170000) === S_IFDIR;
|
|
246
|
+
const isSymbolicLink = (inode.mode & 0o170000) === S_IFLNK;
|
|
247
|
+
return {
|
|
248
|
+
mode: inode.mode,
|
|
249
|
+
size: isDirectory ? 4096 : inode.size,
|
|
250
|
+
isDirectory,
|
|
251
|
+
isSymbolicLink,
|
|
252
|
+
atimeMs: inode.atime.getTime(),
|
|
253
|
+
mtimeMs: inode.mtime.getTime(),
|
|
254
|
+
ctimeMs: inode.ctime.getTime(),
|
|
255
|
+
birthtimeMs: inode.birthtime.getTime(),
|
|
256
|
+
ino: inode.ino,
|
|
257
|
+
nlink: inode.nlink,
|
|
258
|
+
uid: inode.uid,
|
|
259
|
+
gid: inode.gid,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
117
262
|
statEntry(normalized) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (this.dirs.has(normalized)) {
|
|
138
|
-
return {
|
|
139
|
-
mode: customMode ?? (S_IFDIR | 0o755),
|
|
140
|
-
size: 4096,
|
|
141
|
-
isDirectory: true,
|
|
142
|
-
isSymbolicLink: false,
|
|
143
|
-
atimeMs,
|
|
144
|
-
mtimeMs,
|
|
145
|
-
ctimeMs: now,
|
|
146
|
-
birthtimeMs: now,
|
|
147
|
-
};
|
|
263
|
+
const fileIno = this.files.get(normalized);
|
|
264
|
+
if (fileIno !== undefined) {
|
|
265
|
+
return this.statByInode(fileIno);
|
|
266
|
+
}
|
|
267
|
+
const dirIno = this.dirs.get(normalized);
|
|
268
|
+
if (dirIno !== undefined) {
|
|
269
|
+
return this.statByInode(dirIno);
|
|
148
270
|
}
|
|
149
271
|
throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
|
|
150
272
|
}
|
|
@@ -152,8 +274,8 @@ export class InMemoryFileSystem {
|
|
|
152
274
|
const normalized = normalizePath(path);
|
|
153
275
|
if (this.symlinks.has(normalized)) {
|
|
154
276
|
try {
|
|
155
|
-
this.resolveSymlink(normalized);
|
|
156
|
-
return
|
|
277
|
+
const resolved = this.resolveSymlink(normalized);
|
|
278
|
+
return this.files.has(resolved) || this.dirs.has(resolved);
|
|
157
279
|
}
|
|
158
280
|
catch {
|
|
159
281
|
return false;
|
|
@@ -168,9 +290,26 @@ export class InMemoryFileSystem {
|
|
|
168
290
|
}
|
|
169
291
|
async removeFile(path) {
|
|
170
292
|
const normalized = normalizePath(path);
|
|
171
|
-
|
|
293
|
+
const symlink = this.symlinks.get(normalized);
|
|
294
|
+
if (symlink) {
|
|
295
|
+
this.symlinks.delete(normalized);
|
|
296
|
+
this.inodeTable.decrementLinks(symlink.ino);
|
|
297
|
+
if (this.inodeTable.shouldDelete(symlink.ino)) {
|
|
298
|
+
this.inodeTable.delete(symlink.ino);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const resolved = this.resolveSymlink(normalized);
|
|
303
|
+
const ino = this.files.get(resolved);
|
|
304
|
+
if (ino === undefined) {
|
|
172
305
|
throw new Error(`ENOENT: no such file or directory, unlink '${normalized}'`);
|
|
173
306
|
}
|
|
307
|
+
this.files.delete(resolved);
|
|
308
|
+
this.inodeTable.decrementLinks(ino);
|
|
309
|
+
if (this.inodeTable.shouldDelete(ino)) {
|
|
310
|
+
this.deleteInodeData(ino);
|
|
311
|
+
this.inodeTable.delete(ino);
|
|
312
|
+
}
|
|
174
313
|
}
|
|
175
314
|
async removeDir(path) {
|
|
176
315
|
const normalized = normalizePath(path);
|
|
@@ -186,12 +325,24 @@ export class InMemoryFileSystem {
|
|
|
186
325
|
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
187
326
|
}
|
|
188
327
|
}
|
|
189
|
-
for (const dirPath of this.dirs.
|
|
328
|
+
for (const dirPath of this.dirs.keys()) {
|
|
190
329
|
if (dirPath !== normalized && dirPath.startsWith(prefix)) {
|
|
191
330
|
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
192
331
|
}
|
|
193
332
|
}
|
|
333
|
+
for (const linkPath of this.symlinks.keys()) {
|
|
334
|
+
if (linkPath.startsWith(prefix)) {
|
|
335
|
+
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const ino = this.dirs.get(normalized);
|
|
194
339
|
this.dirs.delete(normalized);
|
|
340
|
+
this.inodeTable.decrementLinks(ino);
|
|
341
|
+
this.inodeTable.decrementLinks(ino);
|
|
342
|
+
this.adjustParentDirectoryLinkCount(normalized, -1);
|
|
343
|
+
if (this.inodeTable.shouldDelete(ino)) {
|
|
344
|
+
this.inodeTable.delete(ino);
|
|
345
|
+
}
|
|
195
346
|
}
|
|
196
347
|
async rename(oldPath, newPath) {
|
|
197
348
|
const oldNormalized = normalizePath(oldPath);
|
|
@@ -206,9 +357,23 @@ export class InMemoryFileSystem {
|
|
|
206
357
|
if (this.dirs.has(newNormalized)) {
|
|
207
358
|
throw new Error(`EISDIR: illegal operation on a directory, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
208
359
|
}
|
|
209
|
-
|
|
210
|
-
|
|
360
|
+
if (this.files.has(newNormalized) || this.symlinks.has(newNormalized)) {
|
|
361
|
+
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
362
|
+
}
|
|
363
|
+
const ino = this.files.get(oldNormalized);
|
|
211
364
|
this.files.delete(oldNormalized);
|
|
365
|
+
this.files.set(newNormalized, ino);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (this.symlinks.has(oldNormalized)) {
|
|
369
|
+
if (this.files.has(newNormalized) ||
|
|
370
|
+
this.dirs.has(newNormalized) ||
|
|
371
|
+
this.symlinks.has(newNormalized)) {
|
|
372
|
+
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
373
|
+
}
|
|
374
|
+
const target = this.symlinks.get(oldNormalized);
|
|
375
|
+
this.symlinks.delete(oldNormalized);
|
|
376
|
+
this.symlinks.set(newNormalized, target);
|
|
212
377
|
return;
|
|
213
378
|
}
|
|
214
379
|
if (!this.dirs.has(oldNormalized)) {
|
|
@@ -220,119 +385,263 @@ export class InMemoryFileSystem {
|
|
|
220
385
|
if (newNormalized.startsWith(`${oldNormalized}/`)) {
|
|
221
386
|
throw new Error(`EINVAL: invalid argument, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
222
387
|
}
|
|
223
|
-
if (this.dirs.has(newNormalized) ||
|
|
388
|
+
if (this.dirs.has(newNormalized) ||
|
|
389
|
+
this.files.has(newNormalized) ||
|
|
390
|
+
this.symlinks.has(newNormalized)) {
|
|
224
391
|
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
225
392
|
}
|
|
226
393
|
const sourcePrefix = `${oldNormalized}/`;
|
|
227
394
|
const targetPrefix = `${newNormalized}/`;
|
|
228
|
-
const
|
|
229
|
-
.filter((path) => path === oldNormalized || path.startsWith(sourcePrefix))
|
|
230
|
-
.sort((a, b) => a.length - b.length);
|
|
231
|
-
const
|
|
232
|
-
|
|
395
|
+
const dirEntries = Array.from(this.dirs.entries())
|
|
396
|
+
.filter(([path]) => path === oldNormalized || path.startsWith(sourcePrefix))
|
|
397
|
+
.sort(([a], [b]) => a.length - b.length);
|
|
398
|
+
const fileEntries = Array.from(this.files.entries()).filter(([path]) => path.startsWith(sourcePrefix));
|
|
399
|
+
const symlinkEntries = Array.from(this.symlinks.entries()).filter(([path]) => path.startsWith(sourcePrefix));
|
|
400
|
+
for (const [path] of dirEntries)
|
|
233
401
|
this.dirs.delete(path);
|
|
234
|
-
|
|
235
|
-
for (const path of filePaths) {
|
|
236
|
-
const content = this.files.get(path);
|
|
402
|
+
for (const [path] of fileEntries)
|
|
237
403
|
this.files.delete(path);
|
|
238
|
-
|
|
404
|
+
for (const [path] of symlinkEntries)
|
|
405
|
+
this.symlinks.delete(path);
|
|
406
|
+
for (const [path, ino] of dirEntries) {
|
|
407
|
+
const nextPath = path === oldNormalized
|
|
408
|
+
? newNormalized
|
|
409
|
+
: `${targetPrefix}${path.slice(sourcePrefix.length)}`;
|
|
410
|
+
this.dirs.set(nextPath, ino);
|
|
239
411
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
412
|
+
for (const [path, ino] of fileEntries) {
|
|
413
|
+
this.files.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, ino);
|
|
414
|
+
}
|
|
415
|
+
for (const [path, target] of symlinkEntries) {
|
|
416
|
+
this.symlinks.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, target);
|
|
417
|
+
}
|
|
418
|
+
if (dirname(oldNormalized) !== dirname(newNormalized)) {
|
|
419
|
+
this.adjustParentDirectoryLinkCount(oldNormalized, -1);
|
|
420
|
+
this.adjustParentDirectoryLinkCount(newNormalized, 1);
|
|
246
421
|
}
|
|
247
422
|
}
|
|
248
423
|
async symlink(target, linkPath) {
|
|
249
424
|
const normalized = normalizePath(linkPath);
|
|
250
|
-
if (this.files.has(normalized) ||
|
|
425
|
+
if (this.files.has(normalized) ||
|
|
426
|
+
this.dirs.has(normalized) ||
|
|
427
|
+
this.symlinks.has(normalized)) {
|
|
251
428
|
throw new Error(`EEXIST: file already exists, symlink '${target}' -> '${normalized}'`);
|
|
252
429
|
}
|
|
253
430
|
await this.mkdir(dirname(normalized));
|
|
254
|
-
this.
|
|
431
|
+
const inode = this.allocateSymlinkInode(target);
|
|
432
|
+
this.symlinks.set(normalized, { target, ino: inode.ino });
|
|
255
433
|
}
|
|
256
434
|
async readlink(path) {
|
|
257
435
|
const normalized = normalizePath(path);
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
436
|
+
const link = this.symlinks.get(normalized);
|
|
437
|
+
if (link === undefined) {
|
|
260
438
|
throw new Error(`EINVAL: invalid argument, readlink '${normalized}'`);
|
|
261
439
|
}
|
|
262
|
-
return target;
|
|
440
|
+
return link.target;
|
|
263
441
|
}
|
|
264
442
|
async lstat(path) {
|
|
265
443
|
const normalized = normalizePath(path);
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
mode: S_IFLNK | 0o777,
|
|
271
|
-
size: new TextEncoder().encode(target).byteLength,
|
|
272
|
-
isDirectory: false,
|
|
273
|
-
isSymbolicLink: true,
|
|
274
|
-
atimeMs: now,
|
|
275
|
-
mtimeMs: now,
|
|
276
|
-
ctimeMs: now,
|
|
277
|
-
birthtimeMs: now,
|
|
278
|
-
};
|
|
444
|
+
const link = this.symlinks.get(normalized);
|
|
445
|
+
if (link !== undefined) {
|
|
446
|
+
return this.statForInode(this.requireInode(link.ino));
|
|
279
447
|
}
|
|
280
448
|
return this.statEntry(normalized);
|
|
281
449
|
}
|
|
282
450
|
async link(oldPath, newPath) {
|
|
283
451
|
const oldNormalized = normalizePath(oldPath);
|
|
284
452
|
const newNormalized = normalizePath(newPath);
|
|
285
|
-
const
|
|
286
|
-
|
|
453
|
+
const resolved = this.resolveSymlink(oldNormalized);
|
|
454
|
+
const ino = this.files.get(resolved);
|
|
455
|
+
if (ino === undefined) {
|
|
287
456
|
throw new Error(`ENOENT: no such file or directory, link '${oldNormalized}' -> '${newNormalized}'`);
|
|
288
457
|
}
|
|
289
|
-
if (this.files.has(newNormalized) ||
|
|
458
|
+
if (this.files.has(newNormalized) ||
|
|
459
|
+
this.dirs.has(newNormalized) ||
|
|
460
|
+
this.symlinks.has(newNormalized)) {
|
|
290
461
|
throw new Error(`EEXIST: file already exists, link '${oldNormalized}' -> '${newNormalized}'`);
|
|
291
462
|
}
|
|
292
463
|
await this.mkdir(dirname(newNormalized));
|
|
293
|
-
this.files.set(newNormalized,
|
|
294
|
-
this.
|
|
464
|
+
this.files.set(newNormalized, ino);
|
|
465
|
+
this.inodeTable.incrementLinks(ino);
|
|
295
466
|
}
|
|
296
467
|
async chmod(path, mode) {
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
300
|
-
|
|
468
|
+
const inode = this.requirePathInode(path, "chmod");
|
|
469
|
+
const callerTypeBits = mode & 0o170000;
|
|
470
|
+
if (callerTypeBits !== 0) {
|
|
471
|
+
inode.mode = mode;
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
const existingTypeBits = inode.mode & 0o170000;
|
|
475
|
+
inode.mode = existingTypeBits | (mode & 0o7777);
|
|
301
476
|
}
|
|
302
|
-
|
|
303
|
-
const typeBits = existing ? (existing & 0o170000) : (this.files.has(resolved) ? S_IFREG : S_IFDIR);
|
|
304
|
-
this.modes.set(resolved, typeBits | (mode & 0o7777));
|
|
477
|
+
inode.ctime = new Date();
|
|
305
478
|
}
|
|
306
479
|
async chown(path, uid, gid) {
|
|
480
|
+
const inode = this.requirePathInode(path, "chown");
|
|
481
|
+
inode.uid = uid;
|
|
482
|
+
inode.gid = gid;
|
|
483
|
+
inode.ctime = new Date();
|
|
484
|
+
}
|
|
485
|
+
async utimes(path, atime, mtime) {
|
|
486
|
+
const inode = this.requirePathInode(path, "utimes");
|
|
487
|
+
inode.atime = new Date(atime * 1000);
|
|
488
|
+
inode.mtime = new Date(mtime * 1000);
|
|
489
|
+
inode.ctime = new Date();
|
|
490
|
+
}
|
|
491
|
+
async realpath(path) {
|
|
307
492
|
const normalized = normalizePath(path);
|
|
308
493
|
const resolved = this.resolveSymlink(normalized);
|
|
309
494
|
if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
|
|
310
|
-
throw new Error(`ENOENT: no such file or directory,
|
|
495
|
+
throw new Error(`ENOENT: no such file or directory, realpath '${normalized}'`);
|
|
311
496
|
}
|
|
312
|
-
|
|
497
|
+
return resolved;
|
|
313
498
|
}
|
|
314
|
-
async
|
|
499
|
+
async pread(path, offset, length) {
|
|
315
500
|
const normalized = normalizePath(path);
|
|
316
501
|
const resolved = this.resolveSymlink(normalized);
|
|
317
|
-
|
|
318
|
-
|
|
502
|
+
const ino = this.files.get(resolved);
|
|
503
|
+
if (ino === undefined) {
|
|
504
|
+
throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
|
|
319
505
|
}
|
|
320
|
-
this.
|
|
506
|
+
return this.preadByInode(ino, offset, length);
|
|
321
507
|
}
|
|
322
508
|
async truncate(path, length) {
|
|
323
509
|
const normalized = normalizePath(path);
|
|
324
510
|
const resolved = this.resolveSymlink(normalized);
|
|
325
|
-
const
|
|
326
|
-
if (
|
|
511
|
+
const ino = this.files.get(resolved);
|
|
512
|
+
if (ino === undefined) {
|
|
327
513
|
throw new Error(`ENOENT: no such file or directory, truncate '${normalized}'`);
|
|
328
514
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
515
|
+
const file = this.readFileByInode(ino);
|
|
516
|
+
const next = length >= file.byteLength
|
|
517
|
+
? (() => {
|
|
518
|
+
const padded = new Uint8Array(length);
|
|
519
|
+
padded.set(file);
|
|
520
|
+
return padded;
|
|
521
|
+
})()
|
|
522
|
+
: file.slice(0, length);
|
|
523
|
+
this.fileContents.set(ino, next);
|
|
524
|
+
this.updateFileMetadata(ino, next.byteLength);
|
|
525
|
+
}
|
|
526
|
+
reindexInodes(oldTable) {
|
|
527
|
+
const oldContents = new Map(this.fileContents);
|
|
528
|
+
const oldFiles = new Map(this.files);
|
|
529
|
+
const oldSymlinks = new Map(this.symlinks);
|
|
530
|
+
const oldDirs = Array.from(this.dirs.entries()).sort(([a], [b]) => a.length - b.length);
|
|
531
|
+
const inoMap = new Map();
|
|
532
|
+
this.files = new Map();
|
|
533
|
+
this.fileContents = new Map();
|
|
534
|
+
this.dirs = new Map();
|
|
535
|
+
this.symlinks = new Map();
|
|
536
|
+
for (const [dirPath, oldIno] of oldDirs) {
|
|
537
|
+
const ino = this.cloneInode(oldIno, oldTable, S_IFDIR | 0o755).ino;
|
|
538
|
+
this.dirs.set(dirPath, ino);
|
|
539
|
+
}
|
|
540
|
+
if (!this.dirs.has("/")) {
|
|
541
|
+
this.dirs.set("/", this.allocateDirectoryInode().ino);
|
|
542
|
+
}
|
|
543
|
+
for (const [path, oldIno] of oldFiles) {
|
|
544
|
+
const mapped = inoMap.get(oldIno) ?? (() => {
|
|
545
|
+
const inode = this.cloneInode(oldIno, oldTable, S_IFREG | 0o644);
|
|
546
|
+
inoMap.set(oldIno, inode.ino);
|
|
547
|
+
return inode.ino;
|
|
548
|
+
})();
|
|
549
|
+
this.files.set(path, mapped);
|
|
550
|
+
const content = oldContents.get(oldIno);
|
|
551
|
+
if (content) {
|
|
552
|
+
this.fileContents.set(mapped, content);
|
|
553
|
+
this.requireInode(mapped).size = content.byteLength;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const [path, link] of oldSymlinks) {
|
|
557
|
+
const mapped = this.cloneInode(link.ino, oldTable, S_IFLNK | 0o777).ino;
|
|
558
|
+
this.symlinks.set(path, { target: link.target, ino: mapped });
|
|
559
|
+
this.requireInode(mapped).size = new TextEncoder().encode(link.target).byteLength;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
cloneInode(oldIno, oldTable, fallbackMode) {
|
|
563
|
+
const source = oldTable.get(oldIno);
|
|
564
|
+
const inode = this.inodeTable.allocate(source?.mode ?? fallbackMode, source?.uid ?? 0, source?.gid ?? 0);
|
|
565
|
+
inode.nlink = source?.nlink ?? 1;
|
|
566
|
+
inode.openRefCount = 0;
|
|
567
|
+
inode.size = source?.size ?? 0;
|
|
568
|
+
inode.atime = source?.atime ? new Date(source.atime) : new Date();
|
|
569
|
+
inode.mtime = source?.mtime ? new Date(source.mtime) : new Date();
|
|
570
|
+
inode.ctime = source?.ctime ? new Date(source.ctime) : new Date();
|
|
571
|
+
inode.birthtime = source?.birthtime ? new Date(source.birthtime) : new Date();
|
|
572
|
+
return inode;
|
|
573
|
+
}
|
|
574
|
+
allocateFileInode() {
|
|
575
|
+
return this.inodeTable.allocate(S_IFREG | 0o644, 0, 0);
|
|
576
|
+
}
|
|
577
|
+
allocateDirectoryInode() {
|
|
578
|
+
const inode = this.inodeTable.allocate(S_IFDIR | 0o755, 0, 0);
|
|
579
|
+
inode.nlink = 2;
|
|
580
|
+
inode.size = 4096;
|
|
581
|
+
return inode;
|
|
582
|
+
}
|
|
583
|
+
allocateSymlinkInode(target) {
|
|
584
|
+
const inode = this.inodeTable.allocate(S_IFLNK | 0o777, 0, 0);
|
|
585
|
+
inode.size = new TextEncoder().encode(target).byteLength;
|
|
586
|
+
return inode;
|
|
587
|
+
}
|
|
588
|
+
updateFileMetadata(ino, size) {
|
|
589
|
+
const inode = this.requireFileInode(ino);
|
|
590
|
+
const now = new Date();
|
|
591
|
+
inode.size = size;
|
|
592
|
+
inode.atime = now;
|
|
593
|
+
inode.mtime = now;
|
|
594
|
+
inode.ctime = now;
|
|
595
|
+
}
|
|
596
|
+
requirePathInode(path, op) {
|
|
597
|
+
const normalized = normalizePath(path);
|
|
598
|
+
const resolved = this.resolveSymlink(normalized);
|
|
599
|
+
const ino = this.files.get(resolved) ?? this.dirs.get(resolved);
|
|
600
|
+
if (ino === undefined) {
|
|
601
|
+
throw new Error(`ENOENT: no such file or directory, ${op} '${normalized}'`);
|
|
602
|
+
}
|
|
603
|
+
return this.requireInode(ino);
|
|
604
|
+
}
|
|
605
|
+
requireFileInode(ino) {
|
|
606
|
+
const inode = this.requireInode(ino);
|
|
607
|
+
if ((inode.mode & 0o170000) !== S_IFREG && (inode.mode & 0o170000) !== S_IFSOCK) {
|
|
608
|
+
throw new Error(`EINVAL: inode ${ino} is not a regular file`);
|
|
609
|
+
}
|
|
610
|
+
return inode;
|
|
611
|
+
}
|
|
612
|
+
requireInode(ino) {
|
|
613
|
+
const inode = this.inodeTable.get(ino);
|
|
614
|
+
if (!inode) {
|
|
615
|
+
throw new Error(`ENOENT: inode ${ino} not found`);
|
|
616
|
+
}
|
|
617
|
+
return inode;
|
|
618
|
+
}
|
|
619
|
+
ensureDirectory(path) {
|
|
620
|
+
const normalized = normalizePath(path);
|
|
621
|
+
if (normalized === "/")
|
|
622
|
+
return;
|
|
623
|
+
if (this.dirs.has(normalized))
|
|
624
|
+
return;
|
|
625
|
+
const parent = dirname(normalized);
|
|
626
|
+
if (!this.dirs.has(parent)) {
|
|
627
|
+
throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
|
|
628
|
+
}
|
|
629
|
+
this.dirs.set(normalized, this.allocateDirectoryInode().ino);
|
|
630
|
+
this.adjustParentDirectoryLinkCount(normalized, 1);
|
|
631
|
+
}
|
|
632
|
+
adjustParentDirectoryLinkCount(path, delta) {
|
|
633
|
+
const normalized = normalizePath(path);
|
|
634
|
+
if (normalized === "/")
|
|
635
|
+
return;
|
|
636
|
+
const parent = dirname(normalized);
|
|
637
|
+
const parentIno = this.dirs.get(parent);
|
|
638
|
+
if (parentIno === undefined)
|
|
639
|
+
return;
|
|
640
|
+
if (delta > 0) {
|
|
641
|
+
this.inodeTable.incrementLinks(parentIno);
|
|
333
642
|
}
|
|
334
643
|
else {
|
|
335
|
-
this.
|
|
644
|
+
this.inodeTable.decrementLinks(parentIno);
|
|
336
645
|
}
|
|
337
646
|
}
|
|
338
647
|
}
|