@bash0816/claude-code 2.1.241 → 2.1.248
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 +1 -1
- package/bin/claude +20 -2
- package/config/claude-native-audited-versions.json +30 -0
- package/config/claude-termux-release-manifest.json +3 -3
- package/lib/bunfs-child-process-guard.mjs +18 -0
- package/lib/bunfs-child-process-guard.test.js +113 -0
- package/lib/bunfs-esm-loader.mjs +167 -0
- package/lib/bunfs-esm-loader.test.js +488 -0
- package/lib/bunfs-extract.js +205 -0
- package/lib/bunfs-extract.test.js +265 -0
- package/lib/bunfs-vm-guard.mjs +116 -0
- package/lib/bunfs-vm-guard.test.js +142 -0
- package/lib/bunfs-ws-stub.mjs +11 -0
- package/lib/bunfs-yaml-shim.mjs +91 -0
- package/lib/native-validators.js +90 -0
- package/lib/native-validators.test.js +160 -0
- package/lib/prepare-native.js +5 -41
- package/lib/termux-run-claude-native.sh +154 -4
- package/lib/termux-run-claude-native.test.js +3 -3
- package/package.json +2 -2
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { pathToFileURL } = require('node:url');
|
|
9
|
+
|
|
10
|
+
// ESM test ファイルから CommonJS で import できないため、
|
|
11
|
+
// ここでは基本的な構造をテストする
|
|
12
|
+
test('bunfs-esm-loader module exports initialize, resolve, load functions', async () => {
|
|
13
|
+
// ESM モジュールを動的 import でテストする
|
|
14
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
15
|
+
assert.equal(typeof loader.initialize, 'function');
|
|
16
|
+
assert.equal(typeof loader.resolve, 'function');
|
|
17
|
+
assert.equal(typeof loader.load, 'function');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('resolve() handles child_process and node:child_process specifiers', async () => {
|
|
21
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
22
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
23
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
24
|
+
const guardPath = path.join(tempDir, 'guard.mjs');
|
|
25
|
+
fs.writeFileSync(guardPath, 'export default {};');
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
loader.initialize({
|
|
29
|
+
processOwnedDir: tempDir,
|
|
30
|
+
sourceBin: '/dummy/bin',
|
|
31
|
+
childProcessGuardPath: guardPath,
|
|
32
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
33
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
37
|
+
|
|
38
|
+
// child_process should resolve to childProcessGuardPath
|
|
39
|
+
const result1 = loader.resolve('child_process', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
40
|
+
assert.ok(result1.url.includes(guardPath));
|
|
41
|
+
assert.equal(result1.shortCircuit, true);
|
|
42
|
+
|
|
43
|
+
// node:child_process should also resolve to childProcessGuardPath
|
|
44
|
+
const result2 = loader.resolve('node:child_process', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
45
|
+
assert.ok(result2.url.includes(guardPath));
|
|
46
|
+
assert.equal(result2.shortCircuit, true);
|
|
47
|
+
} finally {
|
|
48
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('resolve() handles vm and node:vm specifiers', async () => {
|
|
53
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
54
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
55
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
56
|
+
const vmGuardPath = path.join(tempDir, 'vm-guard.mjs');
|
|
57
|
+
fs.writeFileSync(vmGuardPath, 'export default {};');
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
loader.initialize({
|
|
61
|
+
processOwnedDir: tempDir,
|
|
62
|
+
sourceBin: '/dummy/bin',
|
|
63
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
64
|
+
vmGuardPath: vmGuardPath,
|
|
65
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
69
|
+
|
|
70
|
+
// vm should resolve to vmGuardPath
|
|
71
|
+
const result1 = loader.resolve('vm', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
72
|
+
assert.ok(result1.url.includes(vmGuardPath));
|
|
73
|
+
assert.equal(result1.shortCircuit, true);
|
|
74
|
+
|
|
75
|
+
// node:vm should also resolve to vmGuardPath
|
|
76
|
+
const result2 = loader.resolve('node:vm', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
77
|
+
assert.ok(result2.url.includes(vmGuardPath));
|
|
78
|
+
assert.equal(result2.shortCircuit, true);
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('resolve() handles ws specifier', async () => {
|
|
85
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
86
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
87
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
88
|
+
const wsStubPath = path.join(tempDir, 'ws-stub.mjs');
|
|
89
|
+
fs.writeFileSync(wsStubPath, 'export default {};');
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
loader.initialize({
|
|
93
|
+
processOwnedDir: tempDir,
|
|
94
|
+
sourceBin: '/dummy/bin',
|
|
95
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
96
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
97
|
+
wsStubPath: wsStubPath,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
101
|
+
|
|
102
|
+
const result = loader.resolve('ws', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
103
|
+
assert.ok(result.url.includes(wsStubPath));
|
|
104
|
+
assert.equal(result.shortCircuit, true);
|
|
105
|
+
} finally {
|
|
106
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('resolve() resolves /$bunfs/root/ specifiers to real files in processOwnedDir', async () => {
|
|
111
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
112
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
113
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
114
|
+
|
|
115
|
+
// Create a dummy file in processOwnedDir
|
|
116
|
+
const dummyFile = path.join(tempDir, 'foo.js');
|
|
117
|
+
fs.writeFileSync(dummyFile, 'export const foo = 1;');
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
loader.initialize({
|
|
121
|
+
processOwnedDir: tempDir,
|
|
122
|
+
sourceBin: '/dummy/bin',
|
|
123
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
124
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
125
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const nextResolve = async (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
129
|
+
|
|
130
|
+
const result = await loader.resolve('/$bunfs/root/foo.js', {}, nextResolve);
|
|
131
|
+
assert.ok(result.url.includes('foo.js'));
|
|
132
|
+
assert.equal(result.shortCircuit, true);
|
|
133
|
+
assert.equal(result.format, 'module');
|
|
134
|
+
} finally {
|
|
135
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('resolve() rejects path traversal with ..', async () => {
|
|
140
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
141
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
142
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
loader.initialize({
|
|
146
|
+
processOwnedDir: tempDir,
|
|
147
|
+
sourceBin: '/dummy/bin',
|
|
148
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
149
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
150
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
154
|
+
|
|
155
|
+
assert.throws(
|
|
156
|
+
() => loader.resolve('/$bunfs/root/../../etc/passwd', {}, nextResolve),
|
|
157
|
+
/rejected specifier|escapes/,
|
|
158
|
+
);
|
|
159
|
+
} finally {
|
|
160
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('resolve() rejects absolute paths', async () => {
|
|
165
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
166
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
167
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
loader.initialize({
|
|
171
|
+
processOwnedDir: tempDir,
|
|
172
|
+
sourceBin: '/dummy/bin',
|
|
173
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
174
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
175
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
179
|
+
|
|
180
|
+
assert.throws(
|
|
181
|
+
() => loader.resolve('/$bunfs/root//etc/passwd', {}, nextResolve),
|
|
182
|
+
/rejected specifier|escapes/,
|
|
183
|
+
);
|
|
184
|
+
} finally {
|
|
185
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('resolve() throws error for missing extracted module', async () => {
|
|
190
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
191
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
192
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
loader.initialize({
|
|
196
|
+
processOwnedDir: tempDir,
|
|
197
|
+
sourceBin: '/dummy/bin',
|
|
198
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
199
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
200
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
204
|
+
|
|
205
|
+
assert.throws(
|
|
206
|
+
() => loader.resolve('/$bunfs/root/nonexistent.js', {}, nextResolve),
|
|
207
|
+
/missing extracted module/,
|
|
208
|
+
);
|
|
209
|
+
} finally {
|
|
210
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('resolve() calls nextResolve for unknown specifiers', async () => {
|
|
215
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
216
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
217
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
loader.initialize({
|
|
221
|
+
processOwnedDir: tempDir,
|
|
222
|
+
sourceBin: '/dummy/bin',
|
|
223
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
224
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
225
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
let nextResolveCalled = false;
|
|
229
|
+
const nextResolve = async (spec, ctx) => {
|
|
230
|
+
nextResolveCalled = true;
|
|
231
|
+
return { url: `unresolved:${spec}` };
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
await loader.resolve('some-unknown-package', {}, nextResolve);
|
|
235
|
+
assert.equal(nextResolveCalled, true);
|
|
236
|
+
} finally {
|
|
237
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('load() returns source as-is when import.meta.require is not present', async () => {
|
|
242
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
243
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
244
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
245
|
+
|
|
246
|
+
const testFile = path.join(tempDir, 'test.js');
|
|
247
|
+
const sourceCode = 'export const x = 1;';
|
|
248
|
+
fs.writeFileSync(testFile, sourceCode);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
loader.initialize({
|
|
252
|
+
processOwnedDir: tempDir,
|
|
253
|
+
sourceBin: '/dummy/bin',
|
|
254
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
255
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
256
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const fileUrl = pathToFileURL(testFile).href;
|
|
260
|
+
const result = await loader.load(fileUrl, {}, async () => ({ source: 'fallback' }));
|
|
261
|
+
|
|
262
|
+
assert.equal(result.format, 'module');
|
|
263
|
+
assert.equal(result.source, sourceCode);
|
|
264
|
+
assert.equal(result.shortCircuit, true);
|
|
265
|
+
} finally {
|
|
266
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('load() injects polyfill prelude when import.meta.require is present', async () => {
|
|
271
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
272
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
273
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
274
|
+
|
|
275
|
+
const testFile = path.join(tempDir, 'test.js');
|
|
276
|
+
const sourceCode = 'const cp = import.meta.require("child_process");';
|
|
277
|
+
fs.writeFileSync(testFile, sourceCode);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
loader.initialize({
|
|
281
|
+
processOwnedDir: tempDir,
|
|
282
|
+
sourceBin: '/dummy/bin',
|
|
283
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
284
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
285
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const fileUrl = pathToFileURL(testFile).href;
|
|
289
|
+
const result = await loader.load(fileUrl, {}, async () => ({ source: 'fallback' }));
|
|
290
|
+
|
|
291
|
+
assert.equal(result.format, 'module');
|
|
292
|
+
assert.ok(result.source.includes('__bunfsMetaRequire'));
|
|
293
|
+
assert.ok(result.source.includes('import __bunfsGuardedChildProcess'));
|
|
294
|
+
assert.ok(result.source.includes('import __bunfsGuardedVm'));
|
|
295
|
+
// Check that import.meta.require was replaced with __bunfsMetaRequire
|
|
296
|
+
assert.ok(result.source.includes('__bunfsMetaRequire("child_process")'));
|
|
297
|
+
assert.ok(!result.source.includes('import.meta.require("child_process")'));
|
|
298
|
+
assert.equal(result.shortCircuit, true);
|
|
299
|
+
} finally {
|
|
300
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('load() calls nextLoad for URLs outside processOwnedDir', async () => {
|
|
305
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
306
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-test-${process.pid}-${Date.now()}`);
|
|
307
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
loader.initialize({
|
|
311
|
+
processOwnedDir: tempDir,
|
|
312
|
+
sourceBin: '/dummy/bin',
|
|
313
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
314
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
315
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
let nextLoadCalled = false;
|
|
319
|
+
const nextLoad = async (url, ctx) => {
|
|
320
|
+
nextLoadCalled = true;
|
|
321
|
+
return { source: 'fallback', format: 'module' };
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
await loader.load('file:///some/other/path/module.js', {}, nextLoad);
|
|
325
|
+
assert.equal(nextLoadCalled, true);
|
|
326
|
+
} finally {
|
|
327
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('load() hoists the cycle-breaking import.meta.require call in chunk-vmw9kxhv.js', async () => {
|
|
332
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
333
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-hoist-test-${process.pid}-${Date.now()}`);
|
|
334
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
335
|
+
|
|
336
|
+
fs.writeFileSync(path.join(tempDir, 'chunk-y0jj307t.js'), 'export const daemonColdStartGbDefault = () => "fixture";\n');
|
|
337
|
+
const targetFile = path.join(tempDir, 'chunk-vmw9kxhv.js');
|
|
338
|
+
const sourceCode = 'var O9=import.meta.require("/$bunfs/root/chunk-y0jj307t.js");\nexport const value = O9.daemonColdStartGbDefault();\n';
|
|
339
|
+
fs.writeFileSync(targetFile, sourceCode);
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
loader.initialize({
|
|
343
|
+
processOwnedDir: tempDir,
|
|
344
|
+
sourceBin: '/dummy/bin',
|
|
345
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
346
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
347
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
348
|
+
cycleHoists: [{ file: 'chunk-vmw9kxhv.js', targetModule: 'chunk-y0jj307t.js', expectedOccurrences: 1, assertProperties: [] }],
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const fileUrl = pathToFileURL(targetFile).href;
|
|
352
|
+
const result = await loader.load(fileUrl, {}, async () => ({ source: 'fallback' }));
|
|
353
|
+
|
|
354
|
+
assert.equal(result.format, 'module');
|
|
355
|
+
assert.ok(result.source.includes('import * as __bunfsHoisted_0 from'));
|
|
356
|
+
assert.ok(!result.source.includes('var O9=import.meta.require('));
|
|
357
|
+
assert.ok(result.source.includes('var O9=__bunfsHoisted_0'));
|
|
358
|
+
assert.equal(result.shortCircuit, true);
|
|
359
|
+
} finally {
|
|
360
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('import.meta.require resolves /$bunfs/root/ specifiers via loader integration', async () => {
|
|
365
|
+
const { registerHooks } = await import('node:module');
|
|
366
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
367
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-integration-${process.pid}-${Date.now()}`);
|
|
368
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
// Create guard files
|
|
372
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
373
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
374
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
375
|
+
|
|
376
|
+
// Create ESM fixture that will be required via import.meta.require (normal case).
|
|
377
|
+
// PROCESS_OWNED_DIR only ever contains genuine ESM chunk files extracted from the
|
|
378
|
+
// Bun esm-chunked bundle (verified against a real 2.1.248 extraction: 0 of 1768
|
|
379
|
+
// chunk files are CommonJS), so load() always returns format: 'module' for this dir.
|
|
380
|
+
fs.writeFileSync(path.join(tempDir, 'foo.js'), 'export const value = 42;');
|
|
381
|
+
|
|
382
|
+
// Create ESM files for each test scenario
|
|
383
|
+
const okCallerPath = path.join(tempDir, 'ok-caller.mjs');
|
|
384
|
+
fs.writeFileSync(okCallerPath, 'export const result = import.meta.require("/$bunfs/root/foo.js").value;\n');
|
|
385
|
+
|
|
386
|
+
const traversalCallerPath = path.join(tempDir, 'traversal-caller.mjs');
|
|
387
|
+
fs.writeFileSync(traversalCallerPath, 'import.meta.require("/$bunfs/root/../../etc/passwd");\n');
|
|
388
|
+
|
|
389
|
+
const missingCallerPath = path.join(tempDir, 'missing-caller.mjs');
|
|
390
|
+
fs.writeFileSync(missingCallerPath, 'import.meta.require("/$bunfs/root/nonexistent.js");\n');
|
|
391
|
+
|
|
392
|
+
// Register loader (only once) with data
|
|
393
|
+
const sourceBin = path.join(tempDir, 'dummy-bin');
|
|
394
|
+
fs.writeFileSync(sourceBin, '#!/bin/false');
|
|
395
|
+
|
|
396
|
+
loader.initialize({
|
|
397
|
+
processOwnedDir: tempDir,
|
|
398
|
+
sourceBin: sourceBin,
|
|
399
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
400
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
401
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
402
|
+
cycleHoists: [
|
|
403
|
+
{ file: 'chunk-vmw9kxhv.js', targetModule: 'chunk-y0jj307t.js', expectedOccurrences: 1, assertProperties: [] },
|
|
404
|
+
],
|
|
405
|
+
});
|
|
406
|
+
registerHooks({ resolve: loader.resolve, load: loader.load });
|
|
407
|
+
|
|
408
|
+
// Test 1: Normal case - should load and resolve correctly
|
|
409
|
+
const okModule = await import(pathToFileURL(okCallerPath).href);
|
|
410
|
+
assert.equal(okModule.result, 42);
|
|
411
|
+
|
|
412
|
+
// Test 2: Path traversal rejection - should throw error
|
|
413
|
+
await assert.rejects(
|
|
414
|
+
() => import(pathToFileURL(traversalCallerPath).href),
|
|
415
|
+
/rejected specifier|escapes/,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Test 3: Missing module rejection - should throw error
|
|
419
|
+
await assert.rejects(
|
|
420
|
+
() => import(pathToFileURL(missingCallerPath).href),
|
|
421
|
+
/missing extracted module/,
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// Cycle regression proof: a generic sync-require-into-in-flight-static-import cycle
|
|
425
|
+
// must throw ERR_REQUIRE_CYCLE_MODULE when NOT hoisted (proves our understanding of
|
|
426
|
+
// the bug mechanism is correct, independent of the real chunk-y0jj307t.js file).
|
|
427
|
+
fs.writeFileSync(
|
|
428
|
+
path.join(tempDir, 'chunk-cycle-demo-target.js'),
|
|
429
|
+
'import "/$bunfs/root/chunk-vmw9kxhv-a.js";\nexport const daemonColdStartGbDefault = () => "fixture";\n',
|
|
430
|
+
);
|
|
431
|
+
fs.writeFileSync(
|
|
432
|
+
path.join(tempDir, 'chunk-vmw9kxhv-a.js'),
|
|
433
|
+
'var O9X=import.meta.require("/$bunfs/root/chunk-cycle-demo-target.js");\nexport const value = O9X;\n',
|
|
434
|
+
);
|
|
435
|
+
await assert.rejects(
|
|
436
|
+
() => import(pathToFileURL(path.join(tempDir, 'chunk-vmw9kxhv-a.js')).href),
|
|
437
|
+
(err) => {
|
|
438
|
+
assert.equal(err.code, 'ERR_REQUIRE_CYCLE_MODULE');
|
|
439
|
+
return true;
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// Cycle fix proof: the real chunk-vmw9kxhv.js / chunk-y0jj307t.js pair (exact filenames
|
|
444
|
+
// and declaration text that tryHoistCycleBreakingImport() targets) must resolve cleanly
|
|
445
|
+
// once hoisting is applied, and the hoisted namespace's property access must work.
|
|
446
|
+
fs.writeFileSync(
|
|
447
|
+
path.join(tempDir, 'chunk-y0jj307t.js'),
|
|
448
|
+
'import "/$bunfs/root/chunk-vmw9kxhv.js";\nexport const daemonColdStartGbDefault = () => "fixture";\n',
|
|
449
|
+
);
|
|
450
|
+
fs.writeFileSync(
|
|
451
|
+
path.join(tempDir, 'chunk-vmw9kxhv.js'),
|
|
452
|
+
'var O9=import.meta.require("/$bunfs/root/chunk-y0jj307t.js");\nexport const value = O9.daemonColdStartGbDefault();\n',
|
|
453
|
+
);
|
|
454
|
+
const hoistedModule = await import(pathToFileURL(path.join(tempDir, 'chunk-vmw9kxhv.js')).href);
|
|
455
|
+
assert.equal(hoistedModule.value, 'fixture');
|
|
456
|
+
|
|
457
|
+
fs.writeFileSync(path.join(tempDir, 'doc.md'), '# Hello\nSome markdown text.\n');
|
|
458
|
+
fs.writeFileSync(
|
|
459
|
+
path.join(tempDir, 'md-caller.mjs'),
|
|
460
|
+
'export const result = import.meta.require("/$bunfs/root/doc.md");\n',
|
|
461
|
+
);
|
|
462
|
+
const mdModule = await import(pathToFileURL(path.join(tempDir, 'md-caller.mjs')).href);
|
|
463
|
+
assert.equal(typeof mdModule.result, 'string');
|
|
464
|
+
assert.equal(mdModule.result, '# Hello\nSome markdown text.\n');
|
|
465
|
+
|
|
466
|
+
fs.writeFileSync(path.join(tempDir, 'note.txt'), 'plain text content');
|
|
467
|
+
fs.writeFileSync(
|
|
468
|
+
path.join(tempDir, 'txt-caller.mjs'),
|
|
469
|
+
'export const result = import.meta.require("/$bunfs/root/note.txt");\n',
|
|
470
|
+
);
|
|
471
|
+
const txtModule = await import(pathToFileURL(path.join(tempDir, 'txt-caller.mjs')).href);
|
|
472
|
+
assert.equal(typeof txtModule.result, 'string');
|
|
473
|
+
assert.equal(txtModule.result, 'plain text content');
|
|
474
|
+
|
|
475
|
+
fs.writeFileSync(
|
|
476
|
+
path.join(tempDir, 'chunk-alias.js'),
|
|
477
|
+
'export const ee = import.meta.require;\n',
|
|
478
|
+
);
|
|
479
|
+
fs.writeFileSync(
|
|
480
|
+
path.join(tempDir, 'alias-caller.mjs'),
|
|
481
|
+
'import { ee } from "/$bunfs/root/chunk-alias.js";\nexport const result = ee("/$bunfs/root/doc.md");\n',
|
|
482
|
+
);
|
|
483
|
+
const aliasModule = await import(pathToFileURL(path.join(tempDir, 'alias-caller.mjs')).href);
|
|
484
|
+
assert.equal(aliasModule.result, '# Hello\nSome markdown text.\n');
|
|
485
|
+
} finally {
|
|
486
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { openSync, readSync, closeSync, fstatSync, mkdirSync, writeFileSync, readdirSync, statSync, rmSync } = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const TRAILER = Buffer.from('\n---- Bun! ----\n');
|
|
7
|
+
const OFFSETS_STRUCT_SIZE = 32;
|
|
8
|
+
const MODULE_TABLE_ENTRY_SIZE = 52;
|
|
9
|
+
const SCAN_CHUNK_SIZE = 1024 * 1024;
|
|
10
|
+
const NAPI_LOADER = 10;
|
|
11
|
+
|
|
12
|
+
function readRange(fd, offset, length) {
|
|
13
|
+
const buf = Buffer.alloc(length);
|
|
14
|
+
const bytesRead = readSync(fd, buf, 0, length, offset);
|
|
15
|
+
if (bytesRead !== length) {
|
|
16
|
+
throw new Error(`bunfs-extract: short read at offset ${offset} (expected ${length}, got ${bytesRead})`);
|
|
17
|
+
}
|
|
18
|
+
return buf;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findTrailerOffset(fd, fileSize) {
|
|
22
|
+
for (let end = fileSize; end > 0; end -= SCAN_CHUNK_SIZE) {
|
|
23
|
+
const start = Math.max(0, end - SCAN_CHUNK_SIZE - TRAILER.length);
|
|
24
|
+
const len = end - start;
|
|
25
|
+
const buf = readRange(fd, start, len);
|
|
26
|
+
const idx = buf.lastIndexOf(TRAILER);
|
|
27
|
+
if (idx >= 0) return start + idx;
|
|
28
|
+
}
|
|
29
|
+
throw new Error('bunfs-extract: StandaloneModuleGraph trailer not found');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isSafeUint(value) {
|
|
33
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function discoverModuleGraph(sourceBin) {
|
|
37
|
+
const fd = openSync(sourceBin, 'r');
|
|
38
|
+
try {
|
|
39
|
+
const fileSize = fstatSync(fd).size;
|
|
40
|
+
const trailerOffset = findTrailerOffset(fd, fileSize);
|
|
41
|
+
|
|
42
|
+
const offsetsStructStart = trailerOffset - OFFSETS_STRUCT_SIZE;
|
|
43
|
+
if (offsetsStructStart < 0) throw new Error('bunfs-extract: invalid trailer position');
|
|
44
|
+
const offsetsBuf = readRange(fd, offsetsStructStart, OFFSETS_STRUCT_SIZE);
|
|
45
|
+
const byteCount = Number(offsetsBuf.readBigUInt64LE(0));
|
|
46
|
+
const modulesOffset = offsetsBuf.readUInt32LE(8);
|
|
47
|
+
const modulesLength = offsetsBuf.readUInt32LE(12);
|
|
48
|
+
const entryPointId = offsetsBuf.readUInt32LE(16);
|
|
49
|
+
|
|
50
|
+
if (!isSafeUint(byteCount) || !isSafeUint(modulesOffset) || !isSafeUint(modulesLength)) {
|
|
51
|
+
throw new Error('bunfs-extract: unsafe integer in Offsets struct');
|
|
52
|
+
}
|
|
53
|
+
if (modulesLength % MODULE_TABLE_ENTRY_SIZE !== 0) {
|
|
54
|
+
throw new Error('bunfs-extract: module table length is not a multiple of entry size');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const dataStart = offsetsStructStart - byteCount;
|
|
58
|
+
if (dataStart < 0) throw new Error('bunfs-extract: computed dataStart is negative');
|
|
59
|
+
|
|
60
|
+
const numModules = modulesLength / MODULE_TABLE_ENTRY_SIZE;
|
|
61
|
+
if (numModules <= 0) throw new Error('bunfs-extract: module table is empty');
|
|
62
|
+
if (!(entryPointId >= 0 && entryPointId < numModules)) {
|
|
63
|
+
throw new Error(`bunfs-extract: entry_point_id ${entryPointId} out of range (numModules=${numModules})`);
|
|
64
|
+
}
|
|
65
|
+
if (modulesOffset + modulesLength > byteCount) {
|
|
66
|
+
throw new Error('bunfs-extract: module table extends beyond byte_count');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const modTableBuf = readRange(fd, dataStart + modulesOffset, modulesLength);
|
|
70
|
+
|
|
71
|
+
const modules = [];
|
|
72
|
+
const seenNames = new Set();
|
|
73
|
+
let entryName = null;
|
|
74
|
+
let entryModule = null;
|
|
75
|
+
for (let i = 0; i < numModules; i += 1) {
|
|
76
|
+
const base = i * MODULE_TABLE_ENTRY_SIZE;
|
|
77
|
+
const nameOff = modTableBuf.readUInt32LE(base);
|
|
78
|
+
const nameLen = modTableBuf.readUInt32LE(base + 4);
|
|
79
|
+
const contOff = modTableBuf.readUInt32LE(base + 8);
|
|
80
|
+
const contLen = modTableBuf.readUInt32LE(base + 12);
|
|
81
|
+
const loader = modTableBuf[base + 49];
|
|
82
|
+
|
|
83
|
+
if (!isSafeUint(nameOff) || !isSafeUint(nameLen) || !isSafeUint(contOff) || !isSafeUint(contLen)) {
|
|
84
|
+
throw new Error(`bunfs-extract: unsafe integer in module table entry ${i}`);
|
|
85
|
+
}
|
|
86
|
+
if (nameOff + nameLen > byteCount) {
|
|
87
|
+
throw new Error(`bunfs-extract: module ${i} name range out of bounds`);
|
|
88
|
+
}
|
|
89
|
+
if (contOff + contLen > byteCount) {
|
|
90
|
+
throw new Error(`bunfs-extract: module ${i} content range out of bounds`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const name = readRange(fd, dataStart + nameOff, nameLen).toString('utf-8');
|
|
94
|
+
if (seenNames.has(name)) {
|
|
95
|
+
throw new Error(`bunfs-extract: duplicate module name ${name}`);
|
|
96
|
+
}
|
|
97
|
+
seenNames.add(name);
|
|
98
|
+
|
|
99
|
+
const absContOff = dataStart + contOff;
|
|
100
|
+
if (i === entryPointId) {
|
|
101
|
+
entryName = name;
|
|
102
|
+
entryModule = { name, contOff: absContOff, contLen };
|
|
103
|
+
}
|
|
104
|
+
if (loader === NAPI_LOADER) continue; // ネイティブ.nodeバイナリは未使用、展開しない
|
|
105
|
+
if (contLen === 0) continue;
|
|
106
|
+
|
|
107
|
+
modules.push({ name, contOff: absContOff, contLen });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (entryName === null) throw new Error('bunfs-extract: entry module not found');
|
|
111
|
+
|
|
112
|
+
return { fd, modules, entryName, entryModule, numModules, byteCount };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
closeSync(fd);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function relPathFromModuleName(name) {
|
|
120
|
+
const rel = name.replace(/^\/\$bunfs\/root\//, '');
|
|
121
|
+
if (rel.includes('..') || path.isAbsolute(rel)) {
|
|
122
|
+
throw new Error(`bunfs-extract: rejected unsafe module name ${name}`);
|
|
123
|
+
}
|
|
124
|
+
return rel;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function extractToProcessOwnedDir(sourceBin, ownedDir) {
|
|
128
|
+
const graph = discoverModuleGraph(sourceBin);
|
|
129
|
+
const { fd, modules, entryName } = graph;
|
|
130
|
+
try {
|
|
131
|
+
mkdirSync(ownedDir, { recursive: true });
|
|
132
|
+
for (const mod of modules) {
|
|
133
|
+
const rel = relPathFromModuleName(mod.name);
|
|
134
|
+
const outPath = path.resolve(ownedDir, rel);
|
|
135
|
+
if (path.relative(ownedDir, outPath).startsWith('..')) {
|
|
136
|
+
throw new Error(`bunfs-extract: path escapes owned dir: ${mod.name}`);
|
|
137
|
+
}
|
|
138
|
+
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
139
|
+
const content = readRange(fd, mod.contOff, mod.contLen);
|
|
140
|
+
writeFileSync(outPath, content);
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
closeSync(fd);
|
|
144
|
+
}
|
|
145
|
+
return { entryRelPath: relPathFromModuleName(entryName) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function cleanupStaleOwnedDirs(workdir, prefix, now = Date.now()) {
|
|
149
|
+
const maxAgeMs = 24 * 60 * 60 * 1000;
|
|
150
|
+
let entries;
|
|
151
|
+
try {
|
|
152
|
+
entries = readdirSync(workdir, { withFileTypes: true });
|
|
153
|
+
} catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (!entry.isDirectory()) continue;
|
|
158
|
+
if (!entry.name.startsWith(prefix)) continue;
|
|
159
|
+
const dirPath = path.join(workdir, entry.name);
|
|
160
|
+
let stats;
|
|
161
|
+
try {
|
|
162
|
+
stats = statSync(dirPath);
|
|
163
|
+
} catch {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (Number.isFinite(stats.mtimeMs) && now - stats.mtimeMs < maxAgeMs) continue;
|
|
167
|
+
|
|
168
|
+
const pidMatch = entry.name.match(/^esm\.(\d+)\./);
|
|
169
|
+
if (pidMatch) {
|
|
170
|
+
const pid = Number(pidMatch[1]);
|
|
171
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
172
|
+
try {
|
|
173
|
+
process.kill(pid, 0);
|
|
174
|
+
continue; // ESRCH以外(プロセス生存中、またはEPERM等)は削除対象から除外
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error && error.code !== 'ESRCH') continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function prepareProcessOwnedDir(sourceBin, workdir) {
|
|
187
|
+
const dirName = `esm.${process.pid}.${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}.bare-dir`;
|
|
188
|
+
cleanupStaleOwnedDirs(workdir, 'esm.');
|
|
189
|
+
const ownedDir = path.join(workdir, dirName);
|
|
190
|
+
const { entryRelPath } = extractToProcessOwnedDir(sourceBin, ownedDir);
|
|
191
|
+
return { ownedDir, entryRelPath };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readEntryContentPrefix(fd, entryModule, maxLength = 256) {
|
|
195
|
+
const length = Math.min(entryModule.contLen, maxLength);
|
|
196
|
+
return readRange(fd, entryModule.contOff, length);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
discoverModuleGraph,
|
|
201
|
+
extractToProcessOwnedDir,
|
|
202
|
+
cleanupStaleOwnedDirs,
|
|
203
|
+
prepareProcessOwnedDir,
|
|
204
|
+
readEntryContentPrefix,
|
|
205
|
+
};
|