@bash0816/claude-code 2.1.241 → 2.1.245
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 +10 -2
- package/config/claude-native-audited-versions.json +8 -0
- package/config/claude-termux-release-manifest.json +1 -1
- 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 +74 -0
- package/lib/bunfs-esm-loader.test.js +329 -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/prepare-native.js +30 -0
- package/lib/termux-run-claude-native.sh +132 -4
- package/lib/termux-run-claude-native.test.js +3 -3
- package/package.json +2 -2
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
|
|
9
|
+
const {
|
|
10
|
+
discoverModuleGraph,
|
|
11
|
+
extractToProcessOwnedDir,
|
|
12
|
+
cleanupStaleOwnedDirs,
|
|
13
|
+
prepareProcessOwnedDir,
|
|
14
|
+
readEntryContentPrefix,
|
|
15
|
+
} = require('./bunfs-extract.js');
|
|
16
|
+
|
|
17
|
+
const TRAILER = '\n---- Bun! ----\n';
|
|
18
|
+
|
|
19
|
+
// StandaloneModuleGraphの最小合成バイナリを構築する。
|
|
20
|
+
// レイアウト: [preamble padding][module contents][module table][Offsets(32byte)][trailer]
|
|
21
|
+
function buildSyntheticBinary({ modules, entryPointId, corruptTrailer = false, preamblePadding = 64 }) {
|
|
22
|
+
const nameBuffers = modules.map((m) => Buffer.from(m.name, 'utf8'));
|
|
23
|
+
const contentBuffers = modules.map((m) => Buffer.from(m.content ?? '', 'utf8'));
|
|
24
|
+
|
|
25
|
+
const dataParts = [];
|
|
26
|
+
const nameOffsets = [];
|
|
27
|
+
const contOffsets = [];
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
for (let i = 0; i < modules.length; i += 1) {
|
|
30
|
+
nameOffsets.push(cursor);
|
|
31
|
+
dataParts.push(nameBuffers[i]);
|
|
32
|
+
cursor += nameBuffers[i].length;
|
|
33
|
+
}
|
|
34
|
+
for (let i = 0; i < modules.length; i += 1) {
|
|
35
|
+
contOffsets.push(cursor);
|
|
36
|
+
dataParts.push(contentBuffers[i]);
|
|
37
|
+
cursor += contentBuffers[i].length;
|
|
38
|
+
}
|
|
39
|
+
const byteCountBeforeTable = cursor;
|
|
40
|
+
|
|
41
|
+
const MODULE_TABLE_ENTRY_SIZE = 52;
|
|
42
|
+
const modTable = Buffer.alloc(MODULE_TABLE_ENTRY_SIZE * modules.length);
|
|
43
|
+
for (let i = 0; i < modules.length; i += 1) {
|
|
44
|
+
const base = i * MODULE_TABLE_ENTRY_SIZE;
|
|
45
|
+
modTable.writeUInt32LE(nameOffsets[i], base);
|
|
46
|
+
modTable.writeUInt32LE(nameBuffers[i].length, base + 4);
|
|
47
|
+
modTable.writeUInt32LE(contOffsets[i], base + 8);
|
|
48
|
+
modTable.writeUInt32LE(contentBuffers[i].length, base + 12);
|
|
49
|
+
modTable[base + 49] = modules[i].loader ?? 1; // 1 = js
|
|
50
|
+
}
|
|
51
|
+
const modulesOffset = byteCountBeforeTable;
|
|
52
|
+
const modulesLength = modTable.length;
|
|
53
|
+
const byteCount = byteCountBeforeTable + modulesLength;
|
|
54
|
+
|
|
55
|
+
const offsetsBuf = Buffer.alloc(32);
|
|
56
|
+
offsetsBuf.writeBigUInt64LE(BigInt(byteCount), 0);
|
|
57
|
+
offsetsBuf.writeUInt32LE(modulesOffset, 8);
|
|
58
|
+
offsetsBuf.writeUInt32LE(modulesLength, 12);
|
|
59
|
+
offsetsBuf.writeUInt32LE(entryPointId, 16);
|
|
60
|
+
|
|
61
|
+
const trailerBuf = Buffer.from(corruptTrailer ? '\n---- NOT BUN ----\n' : TRAILER, 'utf8');
|
|
62
|
+
|
|
63
|
+
return Buffer.concat([
|
|
64
|
+
Buffer.alloc(preamblePadding),
|
|
65
|
+
...dataParts,
|
|
66
|
+
modTable,
|
|
67
|
+
offsetsBuf,
|
|
68
|
+
trailerBuf,
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeTempBinary(buf) {
|
|
73
|
+
const file = path.join(os.tmpdir(), `bunfs-extract-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.bin`);
|
|
74
|
+
fs.writeFileSync(file, buf);
|
|
75
|
+
return file;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test('discoverModuleGraph parses a well-formed synthetic StandaloneModuleGraph', () => {
|
|
79
|
+
const buf = buildSyntheticBinary({
|
|
80
|
+
modules: [
|
|
81
|
+
{ name: '/$bunfs/root/cli', content: 'console.log("entry")' },
|
|
82
|
+
{ name: '/$bunfs/root/chunk-a.js', content: 'export const a = 1;' },
|
|
83
|
+
],
|
|
84
|
+
entryPointId: 0,
|
|
85
|
+
});
|
|
86
|
+
const file = writeTempBinary(buf);
|
|
87
|
+
try {
|
|
88
|
+
const graph = discoverModuleGraph(file);
|
|
89
|
+
try {
|
|
90
|
+
assert.equal(graph.numModules, 2);
|
|
91
|
+
assert.equal(graph.entryName, '/$bunfs/root/cli');
|
|
92
|
+
assert.equal(graph.modules.length, 2);
|
|
93
|
+
const prefix = readEntryContentPrefix(graph.fd, graph.entryModule, 256).toString('utf8');
|
|
94
|
+
assert.equal(prefix, 'console.log("entry")');
|
|
95
|
+
} finally {
|
|
96
|
+
fs.closeSync(graph.fd);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
fs.rmSync(file, { force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('discoverModuleGraph rejects a binary with a corrupted trailer', () => {
|
|
104
|
+
const buf = buildSyntheticBinary({
|
|
105
|
+
modules: [{ name: '/$bunfs/root/cli', content: 'x' }],
|
|
106
|
+
entryPointId: 0,
|
|
107
|
+
corruptTrailer: true,
|
|
108
|
+
});
|
|
109
|
+
const file = writeTempBinary(buf);
|
|
110
|
+
try {
|
|
111
|
+
assert.throws(() => discoverModuleGraph(file), /trailer not found/);
|
|
112
|
+
} finally {
|
|
113
|
+
fs.rmSync(file, { force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('discoverModuleGraph rejects entry_point_id out of range', () => {
|
|
118
|
+
const buf = buildSyntheticBinary({
|
|
119
|
+
modules: [{ name: '/$bunfs/root/cli', content: 'x' }],
|
|
120
|
+
entryPointId: 5, // 存在しないインデックス
|
|
121
|
+
});
|
|
122
|
+
const file = writeTempBinary(buf);
|
|
123
|
+
try {
|
|
124
|
+
assert.throws(() => discoverModuleGraph(file), /entry_point_id.*out of range/);
|
|
125
|
+
} finally {
|
|
126
|
+
fs.rmSync(file, { force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('discoverModuleGraph rejects duplicate module names', () => {
|
|
131
|
+
const buf = buildSyntheticBinary({
|
|
132
|
+
modules: [
|
|
133
|
+
{ name: '/$bunfs/root/cli', content: 'a' },
|
|
134
|
+
{ name: '/$bunfs/root/cli', content: 'b' },
|
|
135
|
+
],
|
|
136
|
+
entryPointId: 0,
|
|
137
|
+
});
|
|
138
|
+
const file = writeTempBinary(buf);
|
|
139
|
+
try {
|
|
140
|
+
assert.throws(() => discoverModuleGraph(file), /duplicate module name/);
|
|
141
|
+
} finally {
|
|
142
|
+
fs.rmSync(file, { force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('discoverModuleGraph skips NAPI loader modules from extraction list', () => {
|
|
147
|
+
const buf = buildSyntheticBinary({
|
|
148
|
+
modules: [
|
|
149
|
+
{ name: '/$bunfs/root/cli', content: 'x' },
|
|
150
|
+
{ name: '/$bunfs/root/native.node', content: 'BINARY', loader: 10 },
|
|
151
|
+
],
|
|
152
|
+
entryPointId: 0,
|
|
153
|
+
});
|
|
154
|
+
const file = writeTempBinary(buf);
|
|
155
|
+
try {
|
|
156
|
+
const graph = discoverModuleGraph(file);
|
|
157
|
+
try {
|
|
158
|
+
assert.equal(graph.modules.length, 1);
|
|
159
|
+
assert.equal(graph.modules[0].name, '/$bunfs/root/cli');
|
|
160
|
+
} finally {
|
|
161
|
+
fs.closeSync(graph.fd);
|
|
162
|
+
}
|
|
163
|
+
} finally {
|
|
164
|
+
fs.rmSync(file, { force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('extractToProcessOwnedDir rejects path traversal via module name', () => {
|
|
169
|
+
const buf = buildSyntheticBinary({
|
|
170
|
+
modules: [{ name: '/$bunfs/root/../../etc/passwd', content: 'evil' }],
|
|
171
|
+
entryPointId: 0,
|
|
172
|
+
});
|
|
173
|
+
const file = writeTempBinary(buf);
|
|
174
|
+
const ownedDir = path.join(os.tmpdir(), `bunfs-extract-owned-${process.pid}-${Date.now()}`);
|
|
175
|
+
try {
|
|
176
|
+
assert.throws(() => extractToProcessOwnedDir(file, ownedDir), /rejected unsafe module name|escapes owned dir/);
|
|
177
|
+
} finally {
|
|
178
|
+
fs.rmSync(file, { force: true });
|
|
179
|
+
fs.rmSync(ownedDir, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('extractToProcessOwnedDir writes module contents to the owned directory', () => {
|
|
184
|
+
const buf = buildSyntheticBinary({
|
|
185
|
+
modules: [
|
|
186
|
+
{ name: '/$bunfs/root/cli', content: 'entry-content' },
|
|
187
|
+
{ name: '/$bunfs/root/chunk-a.js', content: 'chunk-content' },
|
|
188
|
+
],
|
|
189
|
+
entryPointId: 0,
|
|
190
|
+
});
|
|
191
|
+
const file = writeTempBinary(buf);
|
|
192
|
+
const ownedDir = path.join(os.tmpdir(), `bunfs-extract-owned-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
193
|
+
try {
|
|
194
|
+
const { entryRelPath } = extractToProcessOwnedDir(file, ownedDir);
|
|
195
|
+
assert.equal(entryRelPath, 'cli');
|
|
196
|
+
assert.equal(fs.readFileSync(path.join(ownedDir, 'cli'), 'utf8'), 'entry-content');
|
|
197
|
+
assert.equal(fs.readFileSync(path.join(ownedDir, 'chunk-a.js'), 'utf8'), 'chunk-content');
|
|
198
|
+
} finally {
|
|
199
|
+
fs.rmSync(file, { force: true });
|
|
200
|
+
fs.rmSync(ownedDir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('cleanupStaleOwnedDirs keeps directories whose PID is still alive', () => {
|
|
205
|
+
const workdir = path.join(os.tmpdir(), `bunfs-cleanup-test-${process.pid}-${Date.now()}`);
|
|
206
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
207
|
+
const aliveDir = path.join(workdir, `esm.${process.pid}.old.marker.bare-dir`);
|
|
208
|
+
fs.mkdirSync(aliveDir);
|
|
209
|
+
const oldTime = new Date(Date.now() - 48 * 60 * 60 * 1000);
|
|
210
|
+
fs.utimesSync(aliveDir, oldTime, oldTime);
|
|
211
|
+
try {
|
|
212
|
+
cleanupStaleOwnedDirs(workdir, 'esm.');
|
|
213
|
+
assert.ok(fs.existsSync(aliveDir), 'directory owned by a live PID must not be removed even if old');
|
|
214
|
+
} finally {
|
|
215
|
+
fs.rmSync(workdir, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('cleanupStaleOwnedDirs removes old directories whose PID is dead', () => {
|
|
220
|
+
const workdir = path.join(os.tmpdir(), `bunfs-cleanup-test-${process.pid}-${Date.now()}-dead`);
|
|
221
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
222
|
+
// 実在しない可能性が極めて高い巨大なPID番号を使う
|
|
223
|
+
const deadDir = path.join(workdir, `esm.999999999.old.marker.bare-dir`);
|
|
224
|
+
fs.mkdirSync(deadDir);
|
|
225
|
+
const oldTime = new Date(Date.now() - 48 * 60 * 60 * 1000);
|
|
226
|
+
fs.utimesSync(deadDir, oldTime, oldTime);
|
|
227
|
+
try {
|
|
228
|
+
cleanupStaleOwnedDirs(workdir, 'esm.');
|
|
229
|
+
assert.ok(!fs.existsSync(deadDir), 'stale directory owned by a dead PID should be removed');
|
|
230
|
+
} finally {
|
|
231
|
+
fs.rmSync(workdir, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('cleanupStaleOwnedDirs keeps recently modified directories regardless of PID', () => {
|
|
236
|
+
const workdir = path.join(os.tmpdir(), `bunfs-cleanup-test-${process.pid}-${Date.now()}-recent`);
|
|
237
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
238
|
+
const recentDir = path.join(workdir, `esm.999999998.recent.marker.bare-dir`);
|
|
239
|
+
fs.mkdirSync(recentDir);
|
|
240
|
+
try {
|
|
241
|
+
cleanupStaleOwnedDirs(workdir, 'esm.');
|
|
242
|
+
assert.ok(fs.existsSync(recentDir), 'recently created directory must not be removed regardless of PID liveness');
|
|
243
|
+
} finally {
|
|
244
|
+
fs.rmSync(workdir, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('prepareProcessOwnedDir extracts into a unique directory and returns entry path', () => {
|
|
249
|
+
const buf = buildSyntheticBinary({
|
|
250
|
+
modules: [{ name: '/$bunfs/root/cli', content: 'hello' }],
|
|
251
|
+
entryPointId: 0,
|
|
252
|
+
});
|
|
253
|
+
const file = writeTempBinary(buf);
|
|
254
|
+
const workdir = path.join(os.tmpdir(), `bunfs-prepare-test-${process.pid}-${Date.now()}`);
|
|
255
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
256
|
+
try {
|
|
257
|
+
const { ownedDir, entryRelPath } = prepareProcessOwnedDir(file, workdir);
|
|
258
|
+
assert.ok(ownedDir.startsWith(workdir));
|
|
259
|
+
assert.equal(entryRelPath, 'cli');
|
|
260
|
+
assert.equal(fs.readFileSync(path.join(ownedDir, 'cli'), 'utf8'), 'hello');
|
|
261
|
+
} finally {
|
|
262
|
+
fs.rmSync(file, { force: true });
|
|
263
|
+
fs.rmSync(workdir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const realVm = require('node:vm');
|
|
5
|
+
|
|
6
|
+
function injectBunIntoContext(context) {
|
|
7
|
+
if (!context || typeof context !== 'object') return context;
|
|
8
|
+
try {
|
|
9
|
+
if (!Object.prototype.hasOwnProperty.call(context, '__claudeYaml')) {
|
|
10
|
+
Object.defineProperty(context, '__claudeYaml', {
|
|
11
|
+
value: globalThis.__claudeYaml,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (!Object.prototype.hasOwnProperty.call(context, '__claudeBunShim')) {
|
|
17
|
+
Object.defineProperty(context, '__claudeBunShim', {
|
|
18
|
+
value: globalThis.__claudeBunShim,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (!Object.prototype.hasOwnProperty.call(context, '__claudeBun')) {
|
|
24
|
+
Object.defineProperty(context, '__claudeBun', {
|
|
25
|
+
value: globalThis.__claudeBunShim,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (Object.prototype.hasOwnProperty.call(context, 'Bun')) {
|
|
31
|
+
if (context.Bun && typeof context.Bun === 'object' && context.Bun !== globalThis.Bun) {
|
|
32
|
+
try {
|
|
33
|
+
context.Bun = globalThis.Bun;
|
|
34
|
+
} catch {
|
|
35
|
+
Object.defineProperty(context, 'Bun', {
|
|
36
|
+
value: globalThis.Bun,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!context.Bun || typeof context.Bun !== 'object') {
|
|
43
|
+
Object.defineProperty(context, 'Bun', {
|
|
44
|
+
value: globalThis.Bun,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
Object.defineProperty(context, 'Bun', {
|
|
51
|
+
value: globalThis.Bun,
|
|
52
|
+
configurable: true,
|
|
53
|
+
writable: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (context.Bun && globalThis.__claudeYaml) {
|
|
57
|
+
context.Bun.YAML = globalThis.__claudeYaml;
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
return context;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!realVm.__claudeBunShimPatched) {
|
|
64
|
+
const originalCreateContext = realVm.createContext.bind(realVm);
|
|
65
|
+
const originalRunInNewContext = realVm.runInNewContext.bind(realVm);
|
|
66
|
+
const originalRunInContext = realVm.runInContext.bind(realVm);
|
|
67
|
+
const originalRunInThisContext = realVm.runInThisContext && realVm.runInThisContext.bind(realVm);
|
|
68
|
+
const scriptProto = realVm.Script && realVm.Script.prototype;
|
|
69
|
+
|
|
70
|
+
realVm.createContext = (contextObject, ...rest) =>
|
|
71
|
+
originalCreateContext(injectBunIntoContext(contextObject), ...rest);
|
|
72
|
+
realVm.runInNewContext = (code, contextObject, ...rest) =>
|
|
73
|
+
originalRunInNewContext(code, injectBunIntoContext(contextObject), ...rest);
|
|
74
|
+
realVm.runInContext = (code, contextObject, ...rest) =>
|
|
75
|
+
originalRunInContext(code, injectBunIntoContext(contextObject), ...rest);
|
|
76
|
+
if (originalRunInThisContext) {
|
|
77
|
+
realVm.runInThisContext = (code, ...rest) => originalRunInThisContext(code, ...rest);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (scriptProto && !scriptProto.__claudeBunShimPatched) {
|
|
81
|
+
const originalScriptRunInContext = scriptProto.runInContext;
|
|
82
|
+
const originalScriptRunInNewContext = scriptProto.runInNewContext;
|
|
83
|
+
const originalScriptRunInThisContext = scriptProto.runInThisContext;
|
|
84
|
+
|
|
85
|
+
scriptProto.runInContext = function (contextObject, ...rest) {
|
|
86
|
+
return originalScriptRunInContext.call(this, injectBunIntoContext(contextObject), ...rest);
|
|
87
|
+
};
|
|
88
|
+
scriptProto.runInNewContext = function (contextObject, ...rest) {
|
|
89
|
+
return originalScriptRunInNewContext.call(this, injectBunIntoContext(contextObject), ...rest);
|
|
90
|
+
};
|
|
91
|
+
if (originalScriptRunInThisContext) {
|
|
92
|
+
scriptProto.runInThisContext = function (...rest) {
|
|
93
|
+
return originalScriptRunInThisContext.call(this, ...rest);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Object.defineProperty(scriptProto, '__claudeBunShimPatched', { value: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Object.defineProperty(realVm, '__claudeBunShimPatched', { value: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const createContext = realVm.createContext;
|
|
104
|
+
export const isContext = realVm.isContext;
|
|
105
|
+
export const runInContext = realVm.runInContext;
|
|
106
|
+
export const runInNewContext = realVm.runInNewContext;
|
|
107
|
+
export const runInThisContext = realVm.runInThisContext;
|
|
108
|
+
export const createScript = realVm.createScript;
|
|
109
|
+
export const compileFunction = realVm.compileFunction;
|
|
110
|
+
export const measureMemory = realVm.measureMemory;
|
|
111
|
+
export const Script = realVm.Script;
|
|
112
|
+
export const SourceTextModule = realVm.SourceTextModule;
|
|
113
|
+
export const SyntheticModule = realVm.SyntheticModule;
|
|
114
|
+
export const constants = realVm.constants;
|
|
115
|
+
|
|
116
|
+
export default realVm;
|