1
1
/* eslint-disable no-console */
2
+ import { Buffer } from 'node:buffer'
2
3
import fs from 'node:fs'
3
4
import { arch , platform } from 'node:os'
4
5
import path from 'node:path'
@@ -32,19 +33,84 @@ function validatePath(installPath: string): boolean {
32
33
}
33
34
}
34
35
36
+ /**
37
+ * Validate if a file is a proper zip archive
38
+ */
39
+ function validateZipFile ( filePath : string ) : boolean {
40
+ try {
41
+ if ( ! fs . existsSync ( filePath ) ) {
42
+ return false
43
+ }
44
+
45
+ const stats = fs . statSync ( filePath )
46
+
47
+ // Check if file size is reasonable (bun should be at least 1MB, less than 200MB)
48
+ if ( stats . size < 1024 * 1024 || stats . size > 200 * 1024 * 1024 ) {
49
+ if ( config . verbose ) {
50
+ console . warn ( `Invalid zip file size: ${ stats . size } bytes (expected 1MB-200MB)` )
51
+ }
52
+ return false
53
+ }
54
+
55
+ // Read the first few bytes to check zip signature
56
+ const buffer = Buffer . alloc ( 4 )
57
+ const fd = fs . openSync ( filePath , 'r' )
58
+ try {
59
+ fs . readSync ( fd , buffer , 0 , 4 , 0 )
60
+
61
+ // Check for ZIP signature (PK\x03\x04 or PK\x05\x06 or PK\x07\x08)
62
+ const signature = buffer . readUInt32LE ( 0 )
63
+ // eslint-disable-next-line unicorn/number-literal-case
64
+ const isValidZip = signature === 0x04034b50 // Local file header
65
+ // eslint-disable-next-line unicorn/number-literal-case
66
+ || signature === 0x06054b50 // End of central directory
67
+ // eslint-disable-next-line unicorn/number-literal-case
68
+ || signature === 0x08074b50 // Data descriptor
69
+
70
+ if ( ! isValidZip && config . verbose ) {
71
+ console . warn ( `Invalid zip signature: 0x${ signature . toString ( 16 ) . padStart ( 8 , '0' ) } ` )
72
+ }
73
+
74
+ return isValidZip
75
+ }
76
+ finally {
77
+ fs . closeSync ( fd )
78
+ }
79
+ }
80
+ catch ( error ) {
81
+ if ( config . verbose ) {
82
+ console . warn ( `Error validating zip file: ${ error } ` )
83
+ }
84
+ return false
85
+ }
86
+ }
87
+
35
88
/**
36
89
* Get cached binary path for a specific version
37
90
*/
38
91
function getCachedBinaryPath ( version : string , filename : string ) : string | null {
39
92
const cachedArchivePath = path . join ( BINARY_CACHE_DIR , version , filename )
40
93
41
- if ( fs . existsSync ( cachedArchivePath ) ) {
94
+ if ( fs . existsSync ( cachedArchivePath ) && validateZipFile ( cachedArchivePath ) ) {
42
95
if ( config . verbose ) {
43
96
console . warn ( `Found cached binary: ${ cachedArchivePath } ` )
44
97
}
45
98
return cachedArchivePath
46
99
}
47
100
101
+ // Remove corrupted cache if it exists
102
+ if ( fs . existsSync ( cachedArchivePath ) && ! validateZipFile ( cachedArchivePath ) ) {
103
+ try {
104
+ fs . unlinkSync ( cachedArchivePath )
105
+ if ( config . verbose ) {
106
+ console . warn ( `Cached file is corrupted, removing: ${ cachedArchivePath } ` )
107
+ }
108
+ }
109
+ catch {
110
+ // Ignore errors removing cached file
111
+ }
112
+ }
113
+
48
114
return null
49
115
}
50
116
@@ -290,7 +356,7 @@ export async function install_bun(installPath: string, version?: string): Promis
290
356
let zipPath : string
291
357
292
358
try {
293
- if ( cachedArchivePath ) {
359
+ if ( cachedArchivePath && validateZipFile ( cachedArchivePath ) ) {
294
360
// Use cached version - show success message directly without intermediate loading message
295
361
if ( config . verbose ) {
296
362
console . warn ( `Using cached Bun v${ bunVersion } from: ${ cachedArchivePath } ` )
@@ -309,6 +375,18 @@ export async function install_bun(installPath: string, version?: string): Promis
309
375
}
310
376
}
311
377
else {
378
+ // Remove corrupted cache if it exists
379
+ if ( cachedArchivePath && ! validateZipFile ( cachedArchivePath ) ) {
380
+ if ( config . verbose ) {
381
+ console . warn ( `Cached file is corrupted, removing: ${ cachedArchivePath } ` )
382
+ }
383
+ try {
384
+ fs . unlinkSync ( cachedArchivePath )
385
+ }
386
+ catch {
387
+ // Ignore errors removing corrupted cache
388
+ }
389
+ }
312
390
// Download new version
313
391
if ( config . verbose ) {
314
392
console . warn ( `Downloading from: ${ url } ` )
@@ -419,6 +497,33 @@ export async function install_bun(installPath: string, version?: string): Promis
419
497
if ( config . verbose )
420
498
console . warn ( `Downloaded to ${ zipPath } ` )
421
499
500
+ // Validate the downloaded zip file before caching and extraction
501
+ if ( ! validateZipFile ( zipPath ) ) {
502
+ // Remove corrupted file
503
+ try {
504
+ fs . unlinkSync ( zipPath )
505
+ }
506
+ catch {
507
+ // Ignore errors removing corrupted file
508
+ }
509
+
510
+ // Try to clear any cached corrupted versions
511
+ const cachedPath = getCachedBinaryPath ( bunVersion , filename )
512
+ if ( cachedPath && fs . existsSync ( cachedPath ) ) {
513
+ try {
514
+ fs . unlinkSync ( cachedPath )
515
+ if ( config . verbose ) {
516
+ console . warn ( `Removed corrupted cached file: ${ cachedPath } ` )
517
+ }
518
+ }
519
+ catch {
520
+ // Ignore errors removing cached file
521
+ }
522
+ }
523
+
524
+ throw new Error ( `Downloaded bun archive is corrupted. Try clearing cache with: launchpad cache:clear --force` )
525
+ }
526
+
422
527
// Cache the downloaded file for future use
423
528
saveBinaryToCache ( bunVersion , filename , zipPath )
424
529
}
@@ -445,7 +550,40 @@ export async function install_bun(installPath: string, version?: string): Promis
445
550
const { promisify } = await import ( 'node:util' )
446
551
const execAsync = promisify ( exec )
447
552
448
- await execAsync ( `unzip -o "${ zipPath } " -d "${ tempDir } "` )
553
+ try {
554
+ await execAsync ( `unzip -o "${ zipPath } " -d "${ tempDir } "` )
555
+ }
556
+ catch ( error ) {
557
+ // Remove the corrupted file and any cached versions
558
+ try {
559
+ fs . unlinkSync ( zipPath )
560
+ }
561
+ catch {
562
+ // Ignore cleanup errors
563
+ }
564
+
565
+ const cachedPath = getCachedBinaryPath ( bunVersion , filename )
566
+ if ( cachedPath && fs . existsSync ( cachedPath ) ) {
567
+ try {
568
+ fs . unlinkSync ( cachedPath )
569
+ if ( config . verbose ) {
570
+ console . warn ( `Removed corrupted cached file: ${ cachedPath } ` )
571
+ }
572
+ }
573
+ catch {
574
+ // Ignore cleanup errors
575
+ }
576
+ }
577
+
578
+ const errorMessage = error instanceof Error ? error . message : String ( error )
579
+ if ( errorMessage . includes ( 'End-of-central-directory signature not found' )
580
+ || errorMessage . includes ( 'zipfile' )
581
+ || errorMessage . includes ( 'not a zipfile' ) ) {
582
+ throw new Error ( `Downloaded bun archive is corrupted. The download may have been interrupted or the file is damaged. Try running: launchpad cache:clear --force` )
583
+ }
584
+
585
+ throw new Error ( `Failed to extract bun archive: ${ errorMessage } ` )
586
+ }
449
587
450
588
// Move the bun executable to the bin directory
451
589
const bunExeName = platform ( ) === 'win32' ? 'bun.exe' : 'bun'
@@ -467,6 +605,25 @@ export async function install_bun(installPath: string, version?: string): Promis
467
605
}
468
606
}
469
607
608
+ // Show individual package success message to match other packages
609
+ if ( ! config . verbose ) {
610
+ const message = `✅ bun.sh \x1B[2m\x1B[3m(v${ bunVersion } )\x1B[0m`
611
+ if ( process . env . LAUNCHPAD_SHELL_INTEGRATION === '1' ) {
612
+ process . stderr . write ( `${ message } \n` )
613
+ }
614
+ else {
615
+ console . log ( message )
616
+ }
617
+ }
618
+
619
+ // Create environment readiness marker file for fast shell integration
620
+ try {
621
+ fs . writeFileSync ( path . join ( installPath , '.launchpad_ready' ) , new Date ( ) . toISOString ( ) )
622
+ }
623
+ catch {
624
+ // Ignore errors creating marker file
625
+ }
626
+
470
627
// Clean up
471
628
fs . rmSync ( tempDir , { recursive : true , force : true } )
472
629
0 commit comments