forked from Piebald-AI/tweakcc
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnativeInstallation.ts
More file actions
1646 lines (1446 loc) · 50.5 KB
/
Copy pathnativeInstallation.ts
File metadata and controls
1646 lines (1446 loc) · 50.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Utilities for extracting and repacking native installation binaries.
*/
import fs from 'node:fs';
import { execSync } from 'node:child_process';
import LIEF from 'node-lief';
import { isDebug, debug } from './utils';
// ============================================================================
// Nix binary wrapper detection
// ============================================================================
/**
* Maximum file size for a Nix binary wrapper. These are tiny compiled C
* programs (~5-20KB). Anything larger is definitely not a wrapper.
*/
const NIX_WRAPPER_MAX_SIZE = 200_000;
/**
* Detects whether a binary is a Nix `makeBinaryWrapper` output and, if so,
* extracts the path to the real wrapped executable.
*
* Nix's `makeBinaryWrapper` generates a small C program that:
* 1. Manipulates the environment (setenv/unsetenv/putenv)
* 2. Calls `execv("/nix/store/.../real-binary", argv)`
*
* The wrapper always embeds a DOCSTRING in `.rodata` (ELF) or `__cstring`
* (Mach-O) containing the literal `makeCWrapper` invocation, whose first
* argument is the real executable path. This is a contractual part of the
* wrapper format (used by `makeBinaryWrapper.extractCmd`).
*
* Detection strategy:
* 1. Size gate: wrappers are tiny (<200KB), real Bun binaries are multi-MB
* 2. Symbol gate: wrappers import `execv`, real Bun apps do not
* 3. Parse the DOCSTRING: `makeCWrapper '/nix/store/.../real-binary' ...`
* 4. Fallback: find `/nix/store/` paths with `/bin/` in `.rodata`
*
* @returns The path to the real wrapped executable, or null if not a wrapper.
*/
export function resolveNixBinaryWrapper(binaryPath: string): string | null {
try {
// Gate 1: file size — wrappers are tiny
const stat = fs.statSync(binaryPath);
if (stat.size > NIX_WRAPPER_MAX_SIZE) {
return null;
}
LIEF.logging.disable();
const binary = LIEF.parse(binaryPath);
// Gate 2: must import execv — the hallmark of a makeBinaryWrapper
const symbols = binary.symbols();
const hasExecv = symbols.some(sym => {
const name = sym.name;
return name === 'execv' || name === '_execv';
});
if (!hasExecv) {
debug(
'resolveNixBinaryWrapper: no execv import found, not a Nix wrapper'
);
return null;
}
debug(
'resolveNixBinaryWrapper: execv import found, checking for Nix wrapper DOCSTRING'
);
// Extract string data from .rodata (ELF) or __TEXT,__cstring (Mach-O)
let rawBytes: Buffer | null = null;
if (binary.format === 'ELF') {
const rodata = binary.sections().find(s => s.name === '.rodata');
if (rodata) {
rawBytes = rodata.content;
}
} else if (binary.format === 'MachO') {
const machoBinary = binary as LIEF.MachO.Binary;
const textSeg = machoBinary.getSegment('__TEXT');
if (textSeg) {
const cstring = textSeg.getSection('__cstring');
if (cstring) {
rawBytes = cstring.content;
}
}
}
if (!rawBytes || rawBytes.length === 0) {
debug('resolveNixBinaryWrapper: could not read string section');
return null;
}
const text = rawBytes.toString('utf-8');
// Strategy 1: parse the DOCSTRING
// makeBinaryWrapper always embeds: makeCWrapper '/nix/store/.../real' ...
const docstringMatch = text.match(/makeCWrapper\s+'(\/nix\/store\/[^']+)'/);
if (docstringMatch) {
const resolvedPath = docstringMatch[1];
debug(
`resolveNixBinaryWrapper: found wrapped executable via DOCSTRING: ${resolvedPath}`
);
return resolvedPath;
}
// Also handle unquoted (shouldn't happen but defensive)
const unquotedMatch = text.match(/makeCWrapper\s+(\/nix\/store\/\S+)/);
if (unquotedMatch) {
const resolvedPath = unquotedMatch[1];
debug(
`resolveNixBinaryWrapper: found wrapped executable via unquoted DOCSTRING: ${resolvedPath}`
);
return resolvedPath;
}
// Strategy 2: find /nix/store/ paths in the string table
// The execv target is the one that points to an executable (contains /bin/)
// as opposed to env var values (--prefix PATH) which point to directories.
const nixPaths = text.match(/\/nix\/store\/[^\0\n\r]+/g);
if (nixPaths) {
for (const p of nixPaths) {
if (p.includes('/bin/')) {
debug(
`resolveNixBinaryWrapper: found wrapped executable via /bin/ heuristic: ${p}`
);
return p;
}
}
}
debug('resolveNixBinaryWrapper: has execv but no Nix store paths found');
return null;
} catch (error) {
debug('resolveNixBinaryWrapper: error during detection:', error);
return null;
}
}
/**
* Constants for Bun trailer and serialized layout sizes.
*
* Bun data layout (normalized across formats) is:
* [data...][OFFSETS struct][BUN_TRAILER]
*
* Where OFFSETS struct (SIZEOF_OFFSETS bytes) is:
* - byteCount: u64 (total size of [data][OFFSETS][BUN_TRAILER])
* - modulesPtr: { u32 offset, u32 length } into [data...] for modules table
* - entryPointId: u32
* - compileExecArgvPtr: { u32 offset, u32 length }
* - flags: u32
*/
const BUN_TRAILER = Buffer.from('\n---- Bun! ----\n');
// Size constants for binary structures
const SIZEOF_OFFSETS = 32;
const SIZEOF_STRING_POINTER = 8;
// Module struct sizes vary by Bun version:
// - Old format (pre-ESM bytecode, before Bun ~1.3.7): 4 StringPointers + 4 u8s = 36 bytes
// - New format (ESM bytecode, Bun ~1.3.7+): 6 StringPointers + 4 u8s = 52 bytes
const SIZEOF_MODULE_OLD = 4 * SIZEOF_STRING_POINTER + 4;
const SIZEOF_MODULE_NEW = 6 * SIZEOF_STRING_POINTER + 4;
// Types
interface StringPointer {
offset: number;
length: number;
}
interface BunOffsets {
byteCount: bigint | number;
modulesPtr: StringPointer;
entryPointId: number;
compileExecArgvPtr: StringPointer;
flags: number;
}
interface BunModule {
name: StringPointer;
contents: StringPointer;
sourcemap: StringPointer;
bytecode: StringPointer;
moduleInfo: StringPointer;
bytecodeOriginPath: StringPointer;
encoding: number;
loader: number;
moduleFormat: number;
side: number;
}
interface BunData {
bunOffsets: BunOffsets;
bunData: Buffer;
/** Header size used in section format: 4 for old format (Bun < 1.3.4), 8 for new format. Only for Mach-O and PE. */
sectionHeaderSize?: number;
/** Detected module struct size: SIZEOF_MODULE_OLD (36) or SIZEOF_MODULE_NEW (52). */
moduleStructSize: number;
}
/**
* Read a StringPointer slice from given buffer.
*/
function getStringPointerContent(
buffer: Buffer,
stringPointer: StringPointer
): Buffer {
return buffer.subarray(
stringPointer.offset,
stringPointer.offset + stringPointer.length
);
}
function parseStringPointer(buffer: Buffer, offset: number): StringPointer {
return {
offset: buffer.readUInt32LE(offset),
length: buffer.readUInt32LE(offset + 4),
};
}
/**
* True if the module represents the native claude entrypoint.
*/
function isClaudeModule(moduleName: string): boolean {
return (
moduleName.endsWith('/claude') ||
moduleName === 'claude' ||
moduleName.endsWith('/claude.exe') ||
moduleName === 'claude.exe' ||
moduleName.endsWith('/src/entrypoints/cli.js') ||
moduleName === 'src/entrypoints/cli.js'
);
}
/**
* Detects the module struct size from the modules list byte length.
* Returns SIZEOF_MODULE_NEW (52) or SIZEOF_MODULE_OLD (36).
*/
function detectModuleStructSize(modulesListLength: number): number {
const fitsNew = modulesListLength % SIZEOF_MODULE_NEW === 0;
const fitsOld = modulesListLength % SIZEOF_MODULE_OLD === 0;
if (fitsNew && !fitsOld) return SIZEOF_MODULE_NEW;
if (fitsOld && !fitsNew) return SIZEOF_MODULE_OLD;
if (fitsNew && fitsOld) {
// Ambiguous — prefer new format (more likely with recent Bun versions)
debug(
`detectModuleStructSize: Ambiguous module list length ${modulesListLength}, assuming new format`
);
return SIZEOF_MODULE_NEW;
}
// Neither fits cleanly — try new format as default
debug(
`detectModuleStructSize: Module list length ${modulesListLength} doesn't cleanly divide by either struct size, assuming new format`
);
return SIZEOF_MODULE_NEW;
}
/**
* Iterates over modules in the Bun data and calls visitor for each.
* Handles all module parsing and iteration logic in one place.
*/
function mapModules<T>(
bunData: Buffer,
bunOffsets: BunOffsets,
moduleStructSize: number,
visitor: (
module: BunModule,
moduleName: string,
index: number
) => T | undefined
): T | undefined {
const modulesListBytes = getStringPointerContent(
bunData,
bunOffsets.modulesPtr
);
const modulesListCount = Math.floor(
modulesListBytes.length / moduleStructSize
);
for (let i = 0; i < modulesListCount; i++) {
const offset = i * moduleStructSize;
const module = parseCompiledModuleGraphFile(
modulesListBytes,
offset,
moduleStructSize
);
const moduleName = getStringPointerContent(bunData, module.name).toString(
'utf-8'
);
const result = visitor(module, moduleName, i);
if (result !== undefined) {
return result;
}
}
return undefined;
}
function parseOffsets(buffer: Buffer): BunOffsets {
let pos = 0;
const byteCount = buffer.readBigUInt64LE(pos);
pos += 8;
const modulesPtr = parseStringPointer(buffer, pos);
pos += 8;
const entryPointId = buffer.readUInt32LE(pos);
pos += 4;
const compileExecArgvPtr = parseStringPointer(buffer, pos);
pos += 8;
const flags = buffer.readUInt32LE(pos);
return { byteCount, modulesPtr, entryPointId, compileExecArgvPtr, flags };
}
function parseCompiledModuleGraphFile(
buffer: Buffer,
offset: number,
moduleStructSize: number
): BunModule {
let pos = offset;
const name = parseStringPointer(buffer, pos);
pos += 8;
const contents = parseStringPointer(buffer, pos);
pos += 8;
const sourcemap = parseStringPointer(buffer, pos);
pos += 8;
const bytecode = parseStringPointer(buffer, pos);
pos += 8;
let moduleInfo: StringPointer;
let bytecodeOriginPath: StringPointer;
if (moduleStructSize === SIZEOF_MODULE_NEW) {
moduleInfo = parseStringPointer(buffer, pos);
pos += 8;
bytecodeOriginPath = parseStringPointer(buffer, pos);
pos += 8;
} else {
moduleInfo = { offset: 0, length: 0 };
bytecodeOriginPath = { offset: 0, length: 0 };
}
const encoding = buffer.readUInt8(pos);
pos += 1;
const loader = buffer.readUInt8(pos);
pos += 1;
const moduleFormat = buffer.readUInt8(pos);
pos += 1;
const side = buffer.readUInt8(pos);
return {
name,
contents,
sourcemap,
bytecode,
moduleInfo,
bytecodeOriginPath,
encoding,
loader,
moduleFormat,
side,
};
}
/**
* Parses Bun data blob that contains: [data][offsets][trailer]
* This is the common structure across all formats after extraction.
*/
function parseBunDataBlob(bunDataContent: Buffer): {
bunOffsets: BunOffsets;
bunData: Buffer;
moduleStructSize: number;
} {
if (bunDataContent.length < SIZEOF_OFFSETS + BUN_TRAILER.length) {
throw new Error('BUN data is too small to contain trailer and offsets');
}
// Verify trailer
const trailerStart = bunDataContent.length - BUN_TRAILER.length;
const trailerBytes = bunDataContent.subarray(trailerStart);
debug(`parseBunDataBlob: Expected trailer: ${BUN_TRAILER.toString('hex')}`);
debug(`parseBunDataBlob: Got trailer: ${trailerBytes.toString('hex')}`);
if (!trailerBytes.equals(BUN_TRAILER)) {
debug(`Expected: ${BUN_TRAILER.toString('hex')}`);
debug(`Got: ${trailerBytes.toString('hex')}`);
throw new Error('BUN trailer bytes do not match trailer');
}
// Parse Offsets structure
const offsetsStart =
bunDataContent.length - SIZEOF_OFFSETS - BUN_TRAILER.length;
const offsetsBytes = bunDataContent.subarray(
offsetsStart,
offsetsStart + SIZEOF_OFFSETS
);
const bunOffsets = parseOffsets(offsetsBytes);
const moduleStructSize = detectModuleStructSize(bunOffsets.modulesPtr.length);
return {
bunOffsets,
bunData: bunDataContent,
moduleStructSize,
};
}
/**
* Section format helper (for Mach-O and PE):
* Old format (Bun < 1.3.4): [u32 size][size bytes of Bun data blob...]
* New format (Bun >= 1.3.4): [u64 size][size bytes of Bun data blob...]
*
* Size is the length of the Bun blob (which itself is [data][OFFSETS][TRAILER]).
* We detect which format by checking if (headerSize + size) matches the section length.
*/
function extractBunDataFromSection(sectionData: Buffer): BunData {
if (sectionData.length < 4) {
throw new Error('Section data too small');
}
debug(`extractBunDataFromSection: sectionData.length=${sectionData.length}`);
// Try u32 header (old format, Bun < 1.3.4)
const bunDataSizeU32 = sectionData.readUInt32LE(0);
const expectedLengthU32 = 4 + bunDataSizeU32;
// Try u64 header (new format, Bun >= 1.3.4) - only if we have enough bytes
const bunDataSizeU64 =
sectionData.length >= 8 ? Number(sectionData.readBigUInt64LE(0)) : 0;
const expectedLengthU64 = 8 + bunDataSizeU64;
debug(
`extractBunDataFromSection: u32 header would give size=${bunDataSizeU32}, expected total=${expectedLengthU32}`
);
debug(
`extractBunDataFromSection: u64 header would give size=${bunDataSizeU64}, expected total=${expectedLengthU64}`
);
let headerSize: number;
let bunDataSize: number;
// Check which format matches the section length (allowing for padding up to 4KB)
if (
sectionData.length >= 8 &&
expectedLengthU64 <= sectionData.length &&
expectedLengthU64 >= sectionData.length - 4096
) {
// u64 format matches
headerSize = 8;
bunDataSize = bunDataSizeU64;
debug(
`extractBunDataFromSection: detected u64 header format (Bun >= 1.3.4)`
);
} else if (
expectedLengthU32 <= sectionData.length &&
expectedLengthU32 >= sectionData.length - 4096
) {
// u32 format matches
headerSize = 4;
bunDataSize = bunDataSizeU32;
debug(
`extractBunDataFromSection: detected u32 header format (Bun < 1.3.4)`
);
} else {
throw new Error(
`Cannot determine section header format: sectionData.length=${sectionData.length}, ` +
`u64 would expect ${expectedLengthU64}, u32 would expect ${expectedLengthU32}`
);
}
debug(`extractBunDataFromSection: bunDataSize from header=${bunDataSize}`);
const bunDataContent = sectionData.subarray(
headerSize,
headerSize + bunDataSize
);
debug(
`extractBunDataFromSection: bunDataContent.length=${bunDataContent.length}`
);
const { bunOffsets, bunData, moduleStructSize } =
parseBunDataBlob(bunDataContent);
return {
bunOffsets,
bunData,
sectionHeaderSize: headerSize,
moduleStructSize,
};
}
const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
/** Returns true if the file at filePath begins with the ELF magic bytes. */
function isELFFile(filePath: string): boolean {
let fd: number | null = null;
try {
fd = fs.openSync(filePath, 'r');
const buf = Buffer.allocUnsafe(4);
const bytesRead = fs.readSync(fd, buf, 0, 4, 0);
return bytesRead === 4 && buf.equals(ELF_MAGIC);
} catch {
return false;
} finally {
if (fd !== null) fs.closeSync(fd);
}
}
/**
* Extracts Bun data from an ELF binary by reading the file tail directly,
* without using LIEF. Uses BunOffsets.byteCount (from the offsets struct) as
* the authoritative data region size. The trailing u64 footer is not used.
*/
function extractBunDataFromELFRaw(filePath: string): BunData {
const fd = fs.openSync(filePath, 'r');
try {
const { size: fileSize } = fs.fstatSync(fd);
const tailSize = SIZEOF_OFFSETS + BUN_TRAILER.length + 8;
if (fileSize < tailSize) {
throw new Error('File too small to contain Bun data');
}
const tailBuffer = Buffer.allocUnsafe(tailSize);
fs.readSync(fd, tailBuffer, 0, tailSize, fileSize - tailSize);
const trailerStart = tailSize - 8 - BUN_TRAILER.length;
const trailerBytes = tailBuffer.subarray(
trailerStart,
trailerStart + BUN_TRAILER.length
);
if (!trailerBytes.equals(BUN_TRAILER)) {
throw new Error('BUN trailer not found in ELF file');
}
const offsetsBytes = tailBuffer.subarray(0, SIZEOF_OFFSETS);
const bunOffsets = parseOffsets(offsetsBytes);
const byteCount =
typeof bunOffsets.byteCount === 'bigint'
? Number(bunOffsets.byteCount)
: bunOffsets.byteCount;
if (byteCount <= 0 || byteCount >= fileSize) {
throw new Error(`ELF byteCount out of range: ${byteCount}`);
}
const dataStart =
fileSize - 8 - BUN_TRAILER.length - SIZEOF_OFFSETS - byteCount;
if (dataStart < 0) {
throw new Error('ELF data region extends before start of file');
}
const dataBuffer = Buffer.allocUnsafe(byteCount);
fs.readSync(fd, dataBuffer, 0, byteCount, dataStart);
const bunDataBlob = Buffer.concat([dataBuffer, offsetsBytes, trailerBytes]);
const moduleStructSize = detectModuleStructSize(
bunOffsets.modulesPtr.length
);
debug(
`extractBunDataFromELFRaw: byteCount=${byteCount}, moduleStructSize=${moduleStructSize}`
);
return { bunOffsets, bunData: bunDataBlob, moduleStructSize };
} finally {
fs.closeSync(fd);
}
}
/**
* New ELF format (Bun >= 1.3.x, post-PR#26923):
* Bun data is stored in a .bun ELF section, using the same
* [u64 payload_len][payload bytes] format as macOS and PE.
*
* At build time, Bun's writeBunSection() appends the module graph data to
* the end of the ELF, creates a PT_LOAD segment for it, and updates the
* .bun section header to point there. The original BUN_COMPILED location
* (in the RW data segment) stores a vaddr pointing to the appended data.
*
* Returns null if the .bun section doesn't exist or doesn't have valid data.
*/
function extractBunDataFromELFSection(
elfBinary: LIEF.ELF.Binary
): BunData | null {
try {
const bunSection = elfBinary.getSection('.bun');
if (!bunSection) {
debug('extractBunDataFromELFSection: .bun section not found');
return null;
}
const sectionContent = bunSection.content;
if (sectionContent.length < 8) {
debug('extractBunDataFromELFSection: .bun section too small');
return null;
}
debug(
`extractBunDataFromELFSection: .bun section found, size=${sectionContent.length}`
);
// The .bun section uses the same [u64 size][payload] format as macOS/PE
const result = extractBunDataFromSection(sectionContent);
debug('extractBunDataFromELFSection: successfully extracted data');
return result;
} catch (error) {
debug('extractBunDataFromELFSection: failed to extract:', error);
return null;
}
}
/**
* Legacy ELF layout (Bun < 1.3.x, pre-PR#26923):
* [original ELF ...][Bun data...][Bun offsets][Bun trailer][u64 totalByteCount]
*
* Matches bun_unpack.py logic: parse Offsets structure and use its byteCount
* field instead of the trailing totalByteCount (which is unreliable for musl).
*/
function extractBunDataFromELFOverlay(elfBinary: LIEF.ELF.Binary): BunData {
if (!elfBinary.hasOverlay) {
throw new Error('ELF binary has no overlay data');
}
const overlayData = elfBinary.overlay;
debug(
`extractBunDataFromELFOverlay: Overlay size=${overlayData.length} bytes`
);
if (overlayData.length < BUN_TRAILER.length + 8 + SIZEOF_OFFSETS) {
throw new Error('ELF overlay data is too small');
}
// Read totalByteCount from last 8 bytes
const totalByteCount = overlayData.readBigUInt64LE(overlayData.length - 8);
debug(
`extractBunDataFromELFOverlay: Total byte count from tail=${totalByteCount}`
);
if (totalByteCount < 4096n || totalByteCount > 2n ** 32n - 1n) {
throw new Error(`ELF total byte count is out of range: ${totalByteCount}`);
}
// Verify trailer at [len - 8 - trailer_len : len - 8]
const trailerStart = overlayData.length - 8 - BUN_TRAILER.length;
const trailerBytes = overlayData.subarray(
trailerStart,
overlayData.length - 8
);
debug(
`extractBunDataFromELFOverlay: Expected trailer: ${BUN_TRAILER.toString('hex')}`
);
debug(
`extractBunDataFromELFOverlay: Got trailer: ${trailerBytes.toString('hex')}`
);
if (!trailerBytes.equals(BUN_TRAILER)) {
throw new Error('BUN trailer bytes do not match trailer');
}
// Parse Offsets at [len - 8 - trailer_len - sizeof_offsets : len - 8 - trailer_len]
const offsetsStart =
overlayData.length - 8 - BUN_TRAILER.length - SIZEOF_OFFSETS;
const offsetsBytes = overlayData.subarray(
offsetsStart,
overlayData.length - 8 - BUN_TRAILER.length
);
const bunOffsets = parseOffsets(offsetsBytes);
debug(
`extractBunDataFromELFOverlay: Offsets.byteCount=${bunOffsets.byteCount}`
);
// Validate byteCount from Offsets structure
const byteCount =
typeof bunOffsets.byteCount === 'bigint'
? bunOffsets.byteCount
: BigInt(bunOffsets.byteCount);
if (byteCount >= totalByteCount) {
throw new Error('ELF total byte count is out of range');
}
// Extract data region using byteCount from Offsets (not totalByteCount)
const tailDataLen = 8 + BUN_TRAILER.length + SIZEOF_OFFSETS;
const dataStart = overlayData.length - tailDataLen - Number(byteCount);
const dataRegion = overlayData.subarray(
dataStart,
overlayData.length - tailDataLen
);
debug(
`extractBunDataFromELFOverlay: Extracted ${dataRegion.length} bytes of data`
);
// Reconstruct full blob [data][offsets][trailer] to match other formats
const bunDataBlob = Buffer.concat([dataRegion, offsetsBytes, trailerBytes]);
const moduleStructSize = detectModuleStructSize(bunOffsets.modulesPtr.length);
return {
bunOffsets,
bunData: bunDataBlob,
moduleStructSize,
};
}
/**
* Mach-O layout:
* __BUN/__bun section content is:
* [u32 size][size bytes of Bun blob...]
*/
function extractBunDataFromMachO(machoBinary: LIEF.MachO.Binary): BunData {
const bunSegment = machoBinary.getSegment('__BUN');
if (!bunSegment) {
throw new Error('__BUN segment not found');
}
const bunSection = bunSegment.getSection('__bun');
if (!bunSection) {
throw new Error('__bun section not found');
}
return extractBunDataFromSection(bunSection.content);
}
/**
* PE layout:
* .bun section content is:
* [u32 size][size bytes of Bun blob...]
*/
function extractBunDataFromPE(peBinary: LIEF.PE.Binary): BunData {
const bunSection = peBinary.sections().find(s => s.name === '.bun');
if (!bunSection) {
throw new Error('.bun section not found');
}
return extractBunDataFromSection(bunSection.content);
}
function getBunData(
binary: LIEF.ELF.Binary | LIEF.PE.Binary | LIEF.MachO.Binary
): BunData {
debug(`getBunData: Binary format detected as ${binary.format}`);
switch (binary.format) {
case 'MachO':
return extractBunDataFromMachO(binary as LIEF.MachO.Binary);
case 'PE':
return extractBunDataFromPE(binary as LIEF.PE.Binary);
case 'ELF': {
// Try new .bun ELF section format first (Bun >= 1.3.x, post-PR#26923)
const elfBinary = binary as LIEF.ELF.Binary;
const sectionResult = extractBunDataFromELFSection(elfBinary);
if (sectionResult) {
debug('getBunData: Using new ELF .bun section format');
return sectionResult;
}
// Fall back to legacy overlay format
debug('getBunData: Falling back to legacy ELF overlay format');
return extractBunDataFromELFOverlay(elfBinary);
}
default: {
const _exhaustive: never = binary;
throw new Error(
`Unsupported binary format: ${(_exhaustive as LIEF.ELF.Binary | LIEF.PE.Binary | LIEF.MachO.Binary).format}`
);
}
}
}
/**
* Extracts claude.js from a native installation binary.
* Returns the contents as a Buffer, or null if not found.
*
* Note: If the binary might be a Nix `makeBinaryWrapper` wrapper, callers
* should resolve it first using `resolveNixBinaryWrapper()` and pass the
* real binary path here. This is handled at detection time in
* `installationDetection.ts`.
*/
export function extractClaudeJsFromNativeInstallation(
nativeInstallationPath: string
): Buffer | null {
try {
let extracted: BunData | null = null;
if (isELFFile(nativeInstallationPath)) {
try {
extracted = extractBunDataFromELFRaw(nativeInstallationPath);
} catch {
debug(
'extractClaudeJsFromNativeInstallation: raw ELF extraction failed, falling back to LIEF'
);
}
}
if (!extracted) {
LIEF.logging.disable();
const binary = LIEF.parse(nativeInstallationPath);
extracted = getBunData(binary);
}
const { bunOffsets, bunData, moduleStructSize } = extracted;
debug(
`extractClaudeJsFromNativeInstallation: Got bunData, size=${bunData.length} bytes, moduleStructSize=${moduleStructSize}`
);
const result = mapModules(
bunData,
bunOffsets,
moduleStructSize,
(module, moduleName, index) => {
debug(
`extractClaudeJsFromNativeInstallation: Module ${index}: ${moduleName}`
);
// Module name is typically:
// - Unix/macOS: /$bunfs/root/claude
// - Windows: B:/~BUN/root/claude.exe
if (!isClaudeModule(moduleName)) return undefined;
const moduleContents = getStringPointerContent(
bunData,
module.contents
);
debug(
`extractClaudeJsFromNativeInstallation: Found claude module, contents length=${moduleContents.length}`
);
return moduleContents.length > 0 ? moduleContents : undefined;
}
);
if (result) {
return result;
}
debug(
'extractClaudeJsFromNativeInstallation: claude module not found in any module'
);
return null;
} catch (error) {
debug(
'extractClaudeJsFromNativeInstallation: Error during extraction:',
error
);
return null;
}
}
function rebuildBunData(
bunData: Buffer,
bunOffsets: BunOffsets,
modifiedClaudeJs: Buffer | null,
moduleStructSize: number
): Buffer {
// Phase 1: Collect all string data
const stringsData: Buffer[] = [];
const modulesMetadata: Array<{
name: Buffer;
contents: Buffer;
sourcemap: Buffer;
bytecode: Buffer;
moduleInfo: Buffer;
bytecodeOriginPath: Buffer;
encoding: number;
loader: number;
moduleFormat: number;
side: number;
}> = [];
// Use mapModules to iterate and collect module data
mapModules(bunData, bunOffsets, moduleStructSize, (module, moduleName) => {
const nameBytes = getStringPointerContent(bunData, module.name);
// Check if this is claude.js and we have modified contents
let contentsBytes: Buffer;
if (modifiedClaudeJs && isClaudeModule(moduleName)) {
contentsBytes = modifiedClaudeJs;
} else {
contentsBytes = getStringPointerContent(bunData, module.contents);
}
const sourcemapBytes = getStringPointerContent(bunData, module.sourcemap);
const bytecodeBytes = getStringPointerContent(bunData, module.bytecode);
const moduleInfoBytes = getStringPointerContent(bunData, module.moduleInfo);
const bytecodeOriginPathBytes = getStringPointerContent(
bunData,
module.bytecodeOriginPath
);
modulesMetadata.push({
name: nameBytes,
contents: contentsBytes,
sourcemap: sourcemapBytes,
bytecode: bytecodeBytes,
moduleInfo: moduleInfoBytes,
bytecodeOriginPath: bytecodeOriginPathBytes,
encoding: module.encoding,
loader: module.loader,
moduleFormat: module.moduleFormat,
side: module.side,
});
if (moduleStructSize === SIZEOF_MODULE_NEW) {
stringsData.push(
nameBytes,
contentsBytes,
sourcemapBytes,
bytecodeBytes,
moduleInfoBytes,
bytecodeOriginPathBytes
);
} else {
stringsData.push(nameBytes, contentsBytes, sourcemapBytes, bytecodeBytes);
}
return undefined;
});
const stringsPerModule = moduleStructSize === SIZEOF_MODULE_NEW ? 6 : 4;
// Phase 2: Calculate buffer layout
let currentOffset = 0;
const stringOffsets: StringPointer[] = [];
// Allocate space for strings with null terminators
for (const stringData of stringsData) {
stringOffsets.push({ offset: currentOffset, length: stringData.length });
currentOffset += stringData.length + 1; // +1 for null terminator
}
// Module structures
const modulesListOffset = currentOffset;
const modulesListSize = modulesMetadata.length * moduleStructSize;
currentOffset += modulesListSize;
// compileExecArgv
const compileExecArgvBytes = getStringPointerContent(
bunData,
bunOffsets.compileExecArgvPtr
);
const compileExecArgvOffset = currentOffset;
const compileExecArgvLength = compileExecArgvBytes.length;
currentOffset += compileExecArgvLength + 1; // +1 for null terminator
// Offsets structure
const offsetsOffset = currentOffset;
currentOffset += SIZEOF_OFFSETS;
// Trailer
const trailerOffset = currentOffset;
currentOffset += BUN_TRAILER.length;
// Phase 3: Build the new buffer
const newBuffer = Buffer.allocUnsafe(currentOffset);
newBuffer.fill(0);
// Write all strings with null terminators
let stringIdx = 0;
for (const { offset, length } of stringOffsets) {
if (length > 0) {
stringsData[stringIdx].copy(newBuffer, offset, 0, length);
}
newBuffer[offset + length] = 0; // null terminator
stringIdx++;
}
// Write compileExecArgv
if (compileExecArgvLength > 0) {
compileExecArgvBytes.copy(
newBuffer,
compileExecArgvOffset,
0,
compileExecArgvLength
);
newBuffer[compileExecArgvOffset + compileExecArgvLength] = 0;
}
// Build and write module structures
for (let i = 0; i < modulesMetadata.length; i++) {
const metadata = modulesMetadata[i];
const baseStringIdx = i * stringsPerModule;
const moduleStruct: BunModule = {
name: stringOffsets[baseStringIdx],
contents: stringOffsets[baseStringIdx + 1],
sourcemap: stringOffsets[baseStringIdx + 2],
bytecode: stringOffsets[baseStringIdx + 3],
moduleInfo:
moduleStructSize === SIZEOF_MODULE_NEW
? stringOffsets[baseStringIdx + 4]
: { offset: 0, length: 0 },
bytecodeOriginPath:
moduleStructSize === SIZEOF_MODULE_NEW
? stringOffsets[baseStringIdx + 5]