Skip to content

Instantly share code, notes, and snippets.

@TheCjw
Created June 5, 2026 03:34
Show Gist options
  • Select an option

  • Save TheCjw/2665020a559c1e980fa10f2a5c2aa621 to your computer and use it in GitHub Desktop.

Select an option

Save TheCjw/2665020a559c1e980fa10f2a5c2aa621 to your computer and use it in GitHub Desktop.
A standalone Bun/JavaScript CLI tool that parses the .bun section from PE, ELF, and Mach-O executables compiled with bun build --compile, listing and extracting the embedded source files, assets, and native libraries.
#!/usr/bin/env bun
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
const TRAILER = Buffer.from("\n---- Bun! ----\n");
const BUN_SECTION_NAME = ".bun";
const OFFSET_STRUCT_SIZE = 32;
const MODULE_RECORD_SIZE = 52;
const ENCODINGS = {
0: "binary",
1: "latin1",
2: "utf8",
};
const MODULE_FORMATS = {
0: "none",
1: "esm",
2: "cjs",
};
const SIDES = {
0: "server",
1: "client",
};
const LOADERS = {
0: "jsx",
1: "js",
2: "ts",
3: "tsx",
4: "css",
5: "file",
6: "json",
7: "jsonc",
8: "toml",
9: "wasm",
10: "napi",
11: "base64",
12: "dataurl",
13: "text",
14: "bunsh",
15: "sqlite",
16: "sqlite_embedded",
17: "html",
18: "yaml",
19: "json5",
20: "md",
};
function die(message) {
throw new Error(`error: ${message}`);
}
function readUInt64LE(buffer, offset, what) {
const value = buffer.readBigUInt64LE(offset);
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
die(`${what} exceeds JavaScript safe integer range: ${value}`);
}
return Number(value);
}
function checkedSlice(buffer, offset, size, what) {
if (offset < 0 || size < 0 || offset + size > buffer.length) {
die(`${what} out of bounds: offset=${offset} size=${size} blob_size=${buffer.length}`);
}
return buffer.subarray(offset, offset + size);
}
function readCString(buffer, offset, size, what) {
const data = checkedSlice(buffer, offset, size, what);
return data.length > 0 && data[data.length - 1] === 0 ? data.subarray(0, -1) : data;
}
function decodeUtf8(buffer) {
return buffer.toString("utf8").replace(/\u0000+$/u, "");
}
const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
// PE field offsets
const PE_OFFSET_PTR = 0x3c; // DOS header: offset of PE signature
const PE_NUM_SECTIONS_OFF = 0x06; // COFF header: NumberOfSections (relative to PE sig)
const PE_OPT_HDR_SIZE_OFF = 0x14; // COFF header: SizeOfOptionalHeader (relative to PE sig)
const PE_COFF_HDR_SIZE = 0x18; // size of COFF header (PE sig + COFF fields)
const PE_OPT_MAGIC_OFF = 0x18; // Optional header Magic (relative to PE sig)
const PE_OPT_MAGIC_PE32P = 0x20b; // PE32+ (64-bit)
const PE_SECTION_ENTRY_SIZE = 0x28; // 40 bytes per section header entry
const PE_SECT_RAW_SIZE_OFF = 0x10; // SizeOfRawData (relative to section entry)
const PE_SECT_RAW_OFF_OFF = 0x14; // PointerToRawData (relative to section entry)
const PE_SECT_NAME_LEN = 0x08; // section name field length
// ELF field offsets
const ELF_EI_CLASS = 0x04; // e_ident[EI_CLASS]: 1=32-bit, 2=64-bit
const ELF_EI_DATA = 0x05; // e_ident[EI_DATA]: 1=LE, 2=BE
const ELF_CLASS_64 = 0x02;
const ELF_DATA_LE = 0x01;
// Elf64_Ehdr field offsets
const ELF64_E_SHOFF = 0x28; // u64
const ELF64_E_SHENTSIZE = 0x3a; // u16
const ELF64_E_SHNUM = 0x3c; // u16
const ELF64_E_SHSTRNDX = 0x3e; // u16
// Elf32_Ehdr field offsets
const ELF32_E_SHOFF = 0x20; // u32
const ELF32_E_SHENTSIZE = 0x2e; // u16
const ELF32_E_SHNUM = 0x30; // u16
const ELF32_E_SHSTRNDX = 0x32; // u16
// Elf64_Shdr field offsets (relative to shdr entry)
const ELF64_SH_NAME = 0x00; // u32
const ELF64_SH_OFFSET = 0x18; // u64
const ELF64_SH_SIZE = 0x20; // u64
// Elf32_Shdr field offsets (relative to shdr entry)
const ELF32_SH_NAME = 0x00; // u32
const ELF32_SH_OFFSET = 0x10; // u32
const ELF32_SH_SIZE = 0x14; // u32
// Mach-O magic values (read as big-endian u32)
const MH_MAGIC = 0xfeedface; // 32-bit LE
const MH_CIGAM = 0xcefaedfe; // 32-bit BE
const MH_MAGIC_64 = 0xfeedfacf; // 64-bit LE
const MH_CIGAM_64 = 0xcffaedfe; // 64-bit BE
const FAT_MAGIC = 0xcafebabe;
const FAT_CIGAM = 0xbebafeca;
const FAT_MAGIC_64 = 0xcafebabf;
const FAT_CIGAM_64 = 0xbfbafeca;
// fat_header field offsets (always BE)
const FAT_NFAT_ARCH_OFF = 0x04; // u32
const FAT_ARCH_ENTRY_SIZE = 0x14; // 20 bytes
const FAT_ARCH_TABLE_OFF = 0x08; // first fat_arch
const FAT_ARCH_OFFSET_OFF = 0x08; // offset field within fat_arch entry
// mach_header field offsets
const MACH_NCMDS_OFF = 0x10; // u32 (relative to thin header base)
const MACH_HDR_SIZE_32 = 0x1c; // 28 bytes
const MACH_HDR_SIZE_64 = 0x20; // 32 bytes
const LC_SEGMENT = 0x01;
const LC_SEGMENT_64 = 0x19;
const LC_CMD_OFF = 0x00; // u32 cmd
const LC_CMDSIZE_OFF = 0x04; // u32 cmdsize
const LC_SEGNAME_OFF = 0x08; // char[16]
const LC_SEGNAME_LEN = 0x10;
// segment_command (32-bit) field offsets
const SEG32_NSECTS_OFF = 0x30; // u32
const SEG32_SECTS_OFF = 0x38; // first section entry
const SECT32_ENTRY_SIZE = 0x44; // 68 bytes
const SECT32_SECTNAME_OFF = 0x00; // char[16]
const SECT32_SIZE_OFF = 0x1c; // u32
const SECT32_OFFSET_OFF = 0x20; // u32
// segment_command_64 field offsets
const SEG64_NSECTS_OFF = 0x40; // u32
const SEG64_SECTS_OFF = 0x48; // first section_64 entry
const SECT64_ENTRY_SIZE = 0x50; // 80 bytes
const SECT64_SECTNAME_OFF = 0x00; // char[16]
const SECT64_SIZE_OFF = 0x28; // u64
const SECT64_OFFSET_OFF = 0x30; // u32
function findBunElfSection(buf) {
if (buf.length < 0x40) die("ELF file too small");
const is64 = buf[ELF_EI_CLASS] === ELF_CLASS_64;
if (!is64) die("ELF: only 64-bit executables are supported");
const le = buf[ELF_EI_DATA] === ELF_DATA_LE;
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
const r16 = (o) => dv.getUint16(o, le);
const r32 = (o) => dv.getUint32(o, le);
const r64 = (o) => {
const lo = le ? dv.getUint32(o, true) : dv.getUint32(o + 4, false);
const hi = le ? dv.getUint32(o + 4, true) : dv.getUint32(o, false);
return hi * 0x100000000 + lo;
};
const shoff = r64(ELF64_E_SHOFF);
const shentsize = r16(ELF64_E_SHENTSIZE);
const shnum = r16(ELF64_E_SHNUM);
const shstrndx = r16(ELF64_E_SHSTRNDX);
checkedSlice(buf, shoff, shnum * shentsize, "ELF section header table");
const shstrOff = shoff + shstrndx * shentsize;
const shstrFileOff = r64(shstrOff + ELF64_SH_OFFSET);
const shstrSize = r64(shstrOff + ELF64_SH_SIZE);
checkedSlice(buf, shstrFileOff, shstrSize, "ELF shstrtab");
const matches = [];
for (let i = 0; i < shnum; i++) {
const shdrOff = shoff + i * shentsize;
const nameIdx = r32(shdrOff + ELF64_SH_NAME);
let nameEnd = shstrFileOff + nameIdx;
while (nameEnd < buf.length && buf[nameEnd] !== 0) nameEnd++;
const name = buf.toString("ascii", shstrFileOff + nameIdx, nameEnd);
if (name === ".bun") {
const rawOffset = r64(shdrOff + ELF64_SH_OFFSET);
const rawSize = r64(shdrOff + ELF64_SH_SIZE);
checkedSlice(buf, rawOffset, rawSize, ".bun section raw data");
matches.push({ format: "ELF", name, rawOffset, rawSize, data: buf.subarray(rawOffset, rawOffset + rawSize) });
}
}
if (matches.length === 0) die("ELF has no .bun section");
if (matches.length > 1) die("ELF has multiple .bun sections");
return matches[0];
}
function parseThinMacho(buf, base) {
if (buf.length < base + MACH_HDR_SIZE_32) die("Mach-O thin header too small");
const magic = buf.readUInt32LE(base);
const be = magic === MH_CIGAM || magic === MH_CIGAM_64;
const is64 = magic === MH_MAGIC_64 || magic === MH_CIGAM_64;
if (!is64) die("Mach-O: only 64-bit executables are supported");
const r32 = (o) => be ? buf.readUInt32BE(o) : buf.readUInt32LE(o);
const ncmds = r32(base + MACH_NCMDS_OFF);
const hdrSize = MACH_HDR_SIZE_64;
const matches = [];
let off = base + hdrSize;
for (let i = 0; i < ncmds; i++) {
const cmd = r32(off + LC_CMD_OFF);
const cmdsize = r32(off + LC_CMDSIZE_OFF);
if (cmdsize < 8) die("Mach-O load command size too small");
if (cmd === LC_SEGMENT_64) {
const segname = buf.toString("ascii", off + LC_SEGNAME_OFF, off + LC_SEGNAME_OFF + LC_SEGNAME_LEN).replace(/\0+$/, "");
if (segname === "__BUN") {
const nsects = r32(off + SEG64_NSECTS_OFF);
for (let j = 0; j < nsects; j++) {
const s = off + SEG64_SECTS_OFF + j * SECT64_ENTRY_SIZE;
const sectname = buf.toString("ascii", s + SECT64_SECTNAME_OFF, s + SECT64_SECTNAME_OFF + LC_SEGNAME_LEN).replace(/\0+$/, "");
if (sectname === "__bun") {
const rawSize = Number(buf.readBigUInt64LE(s + SECT64_SIZE_OFF));
const rawOffset = be ? buf.readUInt32BE(s + SECT64_OFFSET_OFF) : buf.readUInt32LE(s + SECT64_OFFSET_OFF);
matches.push({ rawOffset, rawSize });
}
}
}
}
off += cmdsize;
}
return matches;
}
function findBunMachoSection(buf) {
const magic = buf.readUInt32BE(0);
const FAT_SET = new Set([FAT_MAGIC, FAT_CIGAM, FAT_MAGIC_64, FAT_CIGAM_64]);
const allMatches = [];
if (FAT_SET.has(magic)) {
const nfat = buf.readUInt32BE(FAT_NFAT_ARCH_OFF);
for (let i = 0; i < nfat; i++) {
const archEntry = FAT_ARCH_TABLE_OFF + i * FAT_ARCH_ENTRY_SIZE;
const archFileOff = buf.readUInt32BE(archEntry + FAT_ARCH_OFFSET_OFF);
allMatches.push(...parseThinMacho(buf, archFileOff));
}
} else {
allMatches.push(...parseThinMacho(buf, 0));
}
if (allMatches.length === 0) die("Mach-O has no __BUN,__bun section");
if (allMatches.length > 1) die("Mach-O has multiple __BUN,__bun sections");
const { rawOffset, rawSize } = allMatches[0];
checkedSlice(buf, rawOffset, rawSize, "__BUN,__bun section raw data");
return { format: "Mach-O", name: "__BUN,__bun", rawOffset, rawSize, data: buf.subarray(rawOffset, rawOffset + rawSize) };
}
function findBunPeSection(buf) {
if (buf.length < 0x40 || buf.toString("ascii", 0, 2) !== "MZ") {
die("invalid PE file: missing MZ header");
}
const peOffset = buf.readUInt32LE(PE_OFFSET_PTR);
checkedSlice(buf, peOffset, PE_COFF_HDR_SIZE, "PE header");
if (buf.toString("ascii", peOffset, peOffset + 4) !== "PE\0\0") {
die("invalid PE file: missing PE signature");
}
const optMagic = buf.readUInt16LE(peOffset + PE_OPT_MAGIC_OFF);
if (optMagic !== PE_OPT_MAGIC_PE32P) {
die(`PE: only 64-bit (PE32+) executables are supported (optional header magic: 0x${optMagic.toString(16)})`);
}
const numberOfSections = buf.readUInt16LE(peOffset + PE_NUM_SECTIONS_OFF);
const sizeOfOptHdr = buf.readUInt16LE(peOffset + PE_OPT_HDR_SIZE_OFF);
const sectionTableOffset = peOffset + PE_COFF_HDR_SIZE + sizeOfOptHdr;
checkedSlice(buf, sectionTableOffset, numberOfSections * PE_SECTION_ENTRY_SIZE, "section table");
const matches = [];
for (let index = 0; index < numberOfSections; index++) {
const sectionOffset = sectionTableOffset + index * PE_SECTION_ENTRY_SIZE;
const rawName = buf.subarray(sectionOffset, sectionOffset + PE_SECT_NAME_LEN);
const nulIndex = rawName.indexOf(0);
const name = rawName.subarray(0, nulIndex === -1 ? rawName.length : nulIndex).toString("ascii");
const rawSize = buf.readUInt32LE(sectionOffset + PE_SECT_RAW_SIZE_OFF);
const rawOffset = buf.readUInt32LE(sectionOffset + PE_SECT_RAW_OFF_OFF);
if (name === BUN_SECTION_NAME) {
checkedSlice(buf, rawOffset, rawSize, ".bun section raw data");
matches.push({ format: "PE", name, rawOffset, rawSize, data: buf.subarray(rawOffset, rawOffset + rawSize) });
}
}
if (matches.length === 0) die("PE has no .bun section");
if (matches.length > 1) die("PE has multiple .bun sections");
return matches[0];
}
const MACHO_MAGIC_SET = new Set([
FAT_MAGIC, FAT_CIGAM, FAT_MAGIC_64, FAT_CIGAM_64,
MH_MAGIC, MH_CIGAM, MH_MAGIC_64, MH_CIGAM_64,
]);
function findBunSection(buf) {
if (buf.length < 4) die("file too small to determine format");
const magic = buf.readUInt32BE(0);
if (buf.subarray(0, 4).equals(ELF_MAGIC)) return findBunElfSection(buf);
if (MACHO_MAGIC_SET.has(magic)) return findBunMachoSection(buf);
return findBunPeSection(buf);
}
function parsePayload(sectionData) {
if (sectionData.length < 8) {
die(".bun section is too small for length prefix");
}
const payloadSize = readUInt64LE(sectionData, 0, ".bun payload length");
if (payloadSize + 8 > sectionData.length) {
die(`.bun payload length exceeds raw section: payload=${payloadSize} raw=${sectionData.length}`);
}
const payload = sectionData.subarray(8, 8 + payloadSize);
if (payload.length < OFFSET_STRUCT_SIZE + TRAILER.length) {
die(".bun payload is too small for Offsets + trailer");
}
if (!payload.subarray(payload.length - TRAILER.length).equals(TRAILER)) {
die(".bun payload trailer mismatch");
}
return { payloadSize, payload };
}
function parseOffsets(payload) {
const start = payload.length - TRAILER.length - OFFSET_STRUCT_SIZE;
const offsets = {
byte_count: readUInt64LE(payload, start, "byte_count"),
modules_offset: payload.readUInt32LE(start + 8),
modules_size: payload.readUInt32LE(start + 12),
entry_point_id: payload.readUInt32LE(start + 16),
compile_exec_argv_offset: payload.readUInt32LE(start + 20),
compile_exec_argv_size: payload.readUInt32LE(start + 24),
flags: payload.readUInt32LE(start + 28),
};
if (offsets.byte_count > payload.length) {
die(`byte_count exceeds payload: ${offsets.byte_count} > ${payload.length}`);
}
if (offsets.modules_size % MODULE_RECORD_SIZE !== 0) {
die(`modules table size is not a multiple of ${MODULE_RECORD_SIZE}: ${offsets.modules_size}`);
}
checkedSlice(payload, offsets.modules_offset, offsets.modules_size, "modules table");
checkedSlice(
payload,
offsets.compile_exec_argv_offset,
offsets.compile_exec_argv_size,
"compile_exec_argv",
);
return offsets;
}
function readPointerPairs(record) {
const pairs = [];
for (let offset = 0; offset < 48; offset += 8) {
pairs.push([record.readUInt32LE(offset), record.readUInt32LE(offset + 4)]);
}
return pairs;
}
function parseFiles(payload, offsets, sectionFileOffset) {
const files = [];
const count = offsets.modules_size / MODULE_RECORD_SIZE;
if (offsets.entry_point_id >= count) {
die(`entry_point_id out of range: ${offsets.entry_point_id} >= ${count}`);
}
const table = checkedSlice(payload, offsets.modules_offset, offsets.modules_size, "modules table");
for (let index = 0; index < count; index += 1) {
const record = table.subarray(index * MODULE_RECORD_SIZE, (index + 1) * MODULE_RECORD_SIZE);
const pointers = readPointerPairs(record);
const encodingId = record.readUInt8(48);
const loaderId = record.readUInt8(49);
const moduleFormatId = record.readUInt8(50);
const sideId = record.readUInt8(51);
const [nameOffset, nameSize] = pointers[0];
const [contentOffset, contentSize] = pointers[1];
const [sourcemapOffset, sourcemapSize] = pointers[2];
const [bytecodeOffset, bytecodeSize] = pointers[3];
const [moduleInfoOffset, moduleInfoSize] = pointers[4];
const [bytecodeOriginPathOffset, bytecodeOriginPathSize] = pointers[5];
const name = decodeUtf8(readCString(payload, nameOffset, nameSize, `module[${index}].name`));
checkedSlice(payload, contentOffset, contentSize, `module[${index}].contents`);
if (sourcemapSize) {
checkedSlice(payload, sourcemapOffset, sourcemapSize, `module[${index}].sourcemap`);
}
if (bytecodeSize) {
checkedSlice(payload, bytecodeOffset, bytecodeSize, `module[${index}].bytecode`);
}
if (moduleInfoSize) {
checkedSlice(payload, moduleInfoOffset, moduleInfoSize, `module[${index}].module_info`);
}
let bytecodeOriginPath = "";
if (bytecodeOriginPathSize) {
bytecodeOriginPath = decodeUtf8(
readCString(
payload,
bytecodeOriginPathOffset,
bytecodeOriginPathSize,
`module[${index}].bytecode_origin_path`,
),
);
}
files.push({
index,
entry: index === offsets.entry_point_id,
name,
content_payload_offset: contentOffset,
content_file_offset: sectionFileOffset + 8 + contentOffset,
content_size: contentSize,
loader: LOADERS[loaderId] ?? `unknown(${loaderId})`,
encoding: ENCODINGS[encodingId] ?? `unknown(${encodingId})`,
module_format: MODULE_FORMATS[moduleFormatId] ?? `unknown(${moduleFormatId})`,
side: SIDES[sideId] ?? `unknown(${sideId})`,
bytecode_payload_offset: bytecodeSize ? bytecodeOffset : null,
bytecode_size: bytecodeSize,
module_info_payload_offset: moduleInfoSize ? moduleInfoOffset : null,
module_info_size: moduleInfoSize,
bytecode_origin_path: bytecodeOriginPath,
sourcemap_payload_offset: sourcemapSize ? sourcemapOffset : null,
sourcemap_size: sourcemapSize,
});
}
return files;
}
function iterLimited(files, limit) {
return limit == null ? files : files.slice(0, limit);
}
const BUN_VIRTUAL_ROOTS = new Set(["~BUN", "$bunfs"]);
function outputRelativePath(name) {
const normalized = name.replaceAll("\\", "/");
let parts = normalized.split("/").filter((p) => p !== "" && p !== "." && p !== ".." && !p.endsWith(":"));
if (parts.length > 0 && BUN_VIRTUAL_ROOTS.has(parts[0])) {
parts = parts.slice(1);
if (parts.length > 0 && parts[0] === "root") parts = parts.slice(1);
}
return parts.length === 0 ? "unnamed" : path.join(...parts);
}
function extractFiles(payload, files, outputDir) {
const extracted = [];
mkdirSync(outputDir, { recursive: true });
const resolvedOut = path.resolve(outputDir);
for (const file of files) {
const relativePath = outputRelativePath(file.name);
const destination = path.join(outputDir, relativePath);
const resolved = path.resolve(destination);
if (resolved !== resolvedOut && !resolved.startsWith(resolvedOut + path.sep)) {
die(`refusing to write outside output dir: ${JSON.stringify(file.name)} -> ${destination}`);
}
mkdirSync(path.dirname(destination), { recursive: true });
const content = checkedSlice(
payload,
file.content_payload_offset,
file.content_size,
`module[${file.index}].contents`,
);
writeFileSync(destination, content);
extracted.push({
index: file.index,
entry: file.entry,
original_name: file.name,
saved_path: destination,
content_payload_offset: file.content_payload_offset,
content_file_offset: file.content_file_offset,
content_size: file.content_size,
loader: file.loader,
side: file.side,
bytecode_size: file.bytecode_size,
module_info_size: file.module_info_size,
bytecode_origin_path: file.bytecode_origin_path,
});
}
const manifestPath = path.join(outputDir, "manifest.json");
writeFileSync(manifestPath, `${JSON.stringify(extracted, null, 2)}\n`, "utf8");
return extracted;
}
function printText(exePath, section, payloadSize, offsets, files, limit) {
console.log(`exe: ${exePath}`);
console.log(
`${section.format} ${section.name}: raw_offset=0x${section.rawOffset.toString(16)} raw_size=${section.rawSize} payload_size=${payloadSize}`,
);
console.log(
`graph: byte_count=${offsets.byte_count} modules_offset=${offsets.modules_offset} modules_size=${offsets.modules_size} module_count=${files.length} entry_point_id=${offsets.entry_point_id} flags=0x${offsets.flags.toString(16)}`,
);
console.log("index\tentry\tloader\tside\tcontent_payload_offset\tcontent_file_offset\tcontent_size\tname");
for (const file of iterLimited(files, limit)) {
console.log(
`${file.index}\t${Number(file.entry)}\t${file.loader}\t${file.side}\t${file.content_payload_offset}\t0x${file.content_file_offset.toString(16)}\t${file.content_size}\t${file.name}`,
);
}
if (limit != null && files.length > limit) {
console.log(`... ${files.length - limit} more file(s) omitted; use --limit 0 for all`);
}
}
function parseLimit(value) {
const n = Number.parseInt(value, 10);
if (!Number.isInteger(n) || n < 0) die("--limit must be a non-negative integer");
return n === 0 ? null : n;
}
function parseSubArgs(argv, validFlags) {
// Returns { exe, flags... }; exe is required positional arg
const result = { exe: null };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "-h" || arg === "--help") { result._help = true; return result; }
const flag = validFlags.find((f) => arg === f.flag || (f.short && arg === f.short));
if (flag) {
if (flag.takesValue) {
i++;
if (i >= argv.length) die(`${arg} requires a value`);
result[flag.key] = flag.parse ? flag.parse(argv[i]) : argv[i];
} else {
result[flag.key] = true;
}
} else if (arg.startsWith("-")) {
die(`unknown option: ${arg}`);
} else if (result.exe === null) {
result.exe = arg;
} else {
die(`unexpected positional argument: ${arg}`);
}
}
return result;
}
function printHelp() {
console.log(`Usage: bun extract_bun.js <command> EXE [options]
Parse Bun build --compile embedded files from PE/ELF/Mach-O executables.
Commands:
print EXE Print embedded module graph information
extract EXE Extract embedded files and write manifest.json
Options for print:
--limit N Maximum records to print; 0 means no limit (default: all)
--json Emit JSON instead of TSV-like text
-h, --help Show this help message
Options for extract:
-o, --output DIR Output directory (required)
-h, --help Show this help message`);
}
function printExtractedText(exePath, section, payloadSize, offsets, files, extracted, outputDir) {
console.log(`exe: ${exePath}`);
console.log(
`${section.format} ${section.name}: raw_offset=0x${section.rawOffset.toString(16)} raw_size=${section.rawSize} payload_size=${payloadSize}`,
);
console.log(
`graph: module_count=${files.length} entry_point_id=${offsets.entry_point_id} flags=0x${offsets.flags.toString(16)}`,
);
console.log("index\tentry\tloader\tside\tsize\tsaved_path\toriginal_name");
for (const file of extracted) {
console.log(`${file.index}\t${Number(file.entry)}\t${file.loader}\t${file.side}\t${file.content_size}\t${file.saved_path}\t${file.original_name}`);
}
console.log(`extracted: ${extracted.length} file(s) -> ${outputDir}`);
console.log(`manifest: ${path.join(outputDir, "manifest.json")}`);
}
function main(argv = process.argv.slice(2)) {
const [subcmd, ...rest] = argv;
if (!subcmd || subcmd === "-h" || subcmd === "--help") { printHelp(); process.exit(0); }
if (subcmd === "print") {
const args = parseSubArgs(rest, [
{ flag: "--limit", key: "limit", takesValue: true, parse: parseLimit },
{ flag: "--json", key: "json" },
]);
if (args._help) { console.log(`Usage: bun extract_bun.js print EXE [--limit N] [--json] [-h]`); process.exit(0); }
if (!args.exe) die("print requires an EXE argument");
const buf = readFileSync(args.exe);
const section = findBunSection(buf);
const { payloadSize, payload } = parsePayload(section.data);
const offsets = parseOffsets(payload);
const files = parseFiles(payload, offsets, section.rawOffset);
const limit = args.limit ?? null;
if (args.json) {
console.log(JSON.stringify({
exe: args.exe,
bun_section: { format: section.format, name: section.name, raw_offset: section.rawOffset, raw_size: section.rawSize, payload_size: payloadSize },
graph: { ...offsets, module_count: files.length },
files: iterLimited(files, limit),
omitted: limit == null ? 0 : Math.max(0, files.length - limit),
extracted: [],
}, null, 2));
} else {
printText(args.exe, section, payloadSize, offsets, files, limit);
}
} else if (subcmd === "extract") {
const args = parseSubArgs(rest, [
{ flag: "--output", short: "-o", key: "output", takesValue: true },
]);
if (args._help) { console.log(`Usage: bun extract_bun.js extract EXE -o DIR [-h]`); process.exit(0); }
if (!args.exe) die("extract requires an EXE argument");
if (!args.output) die("extract requires -o/--output DIR");
const buf = readFileSync(args.exe);
const section = findBunSection(buf);
const { payloadSize, payload } = parsePayload(section.data);
const offsets = parseOffsets(payload);
const files = parseFiles(payload, offsets, section.rawOffset);
const extracted = extractFiles(payload, files, args.output);
printExtractedText(args.exe, section, payloadSize, offsets, files, extracted, args.output);
} else {
die(`unknown command: ${subcmd}\nRun 'bun extract_bun.js --help' for usage.`);
}
}
try {
main();
} catch (error) {
console.error(error.message);
process.exit(1);
}
@ljw1004

ljw1004 commented Jun 13, 2026

Copy link
Copy Markdown

Thank you! Exactly what I wanted. I really appreciate this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment