@@ -8,7 +8,7 @@ import * as zlib from 'node:zlib';
88import { type IReadonlyPathTrieNode , LookupByPath } from '@rushstack/lookup-by-path/lib/LookupByPath' ;
99import type { ITerminal } from '@rushstack/terminal' ;
1010
11- import { getDisposableFileHandle , unlinkSync , type IDisposableFileHandle } from './fs' ;
11+ import { getDisposableFileHandle , rmdirSync , unlinkSync , type IDisposableFileHandle } from './fs' ;
1212import { type IIncrementalZlib , createIncrementalZlib } from './compress' ;
1313import { markStart , markEnd , getDuration , emitSummary , formatDuration } from './perf' ;
1414import {
@@ -55,6 +55,80 @@ export interface IZipSyncUnpackResult {
5555 otherEntriesDeleted : number ;
5656}
5757
58+ const bufferSize : number = 1 << 25 ; // 32 MiB
59+ const outputBuffer : Buffer < ArrayBuffer > = Buffer . allocUnsafeSlow ( bufferSize ) ;
60+ function extractFileFromZip (
61+ terminal : ITerminal ,
62+ targetPath : string ,
63+ zipBuffer : Buffer ,
64+ entry : ICentralDirectoryHeaderParseResult
65+ ) : void {
66+ terminal . writeDebugLine ( `Extracting file: ${ entry . filename } ` ) ;
67+ const fileZipBuffer : Buffer = getFileFromZip ( zipBuffer , entry ) ;
68+ let fileData : Buffer ;
69+ using fileHandle : IDisposableFileHandle = getDisposableFileHandle ( targetPath , 'w' ) ;
70+ if ( entry . header . compressionMethod === STORE_COMPRESSION ) {
71+ fileData = fileZipBuffer ;
72+ let writeOffset : number = 0 ;
73+ while ( writeOffset < fileData . length && ! isNaN ( fileHandle . fd ) ) {
74+ const written : number = fs . writeSync (
75+ fileHandle . fd ,
76+ fileData ,
77+ writeOffset ,
78+ fileData . length - writeOffset
79+ ) ;
80+ writeOffset += written ;
81+ }
82+ } else if ( entry . header . compressionMethod === DEFLATE_COMPRESSION ) {
83+ using inflateIncremental : IIncrementalZlib = createIncrementalZlib (
84+ outputBuffer ,
85+ ( chunk , lengthBytes ) => {
86+ let writeOffset : number = 0 ;
87+ while ( lengthBytes > 0 && writeOffset < chunk . byteLength ) {
88+ const written : number = fs . writeSync ( fileHandle . fd , chunk , writeOffset , lengthBytes ) ;
89+ lengthBytes -= written ;
90+ writeOffset += written ;
91+ }
92+ } ,
93+ 'inflate'
94+ ) ;
95+ inflateIncremental . update ( fileZipBuffer ) ;
96+ inflateIncremental . update ( Buffer . alloc ( 0 ) ) ;
97+ } else {
98+ throw new Error (
99+ `Unsupported compression method: ${ entry . header . compressionMethod } for ${ entry . filename } `
100+ ) ;
101+ }
102+ }
103+
104+ function shouldExtract (
105+ terminal : ITerminal ,
106+ targetPath : string ,
107+ entry : ICentralDirectoryHeaderParseResult ,
108+ metadata : IMetadata | undefined
109+ ) : boolean {
110+ if ( metadata ) {
111+ const metadataFile : { size : number ; sha1Hash : string } | undefined = metadata . files [ entry . filename ] ;
112+
113+ if ( metadataFile ) {
114+ try {
115+ using existingFile : IDisposableFileHandle = getDisposableFileHandle ( targetPath , 'r' ) ;
116+ const existingHash : string | false = computeFileHash ( existingFile . fd ) ;
117+ if ( existingHash === metadataFile . sha1Hash ) {
118+ return false ;
119+ }
120+ } catch ( e ) {
121+ if ( ( e as NodeJS . ErrnoException ) . code === 'ENOENT' ) {
122+ terminal . writeDebugLine ( `File does not exist, will extract: ${ entry . filename } ` ) ;
123+ } else {
124+ throw e ;
125+ }
126+ }
127+ }
128+ }
129+ return true ;
130+ }
131+
58132export function unpack ( {
59133 archivePath,
60134 targetDirectories : rawTargetDirectories ,
@@ -210,22 +284,15 @@ export function unpack({
210284
211285 for ( const dir of dirsToCleanup ) {
212286 // Try to remove the directory. If it is not empty, this will throw and we can ignore the error.
213- try {
214- fs . rmdirSync ( dir ) ;
215- terminal . writeDebugLine ( `Deleted empty directory: ${ dir } ` ) ;
216- deletedFoldersCount ++ ;
217- } catch ( e ) {
218- // Probably not empty
219- terminal . writeDebugLine ( `Directory not empty, skipping: ${ dir } ` ) ;
220- }
287+ rmdirSync ( dir ) ;
288+ terminal . writeDebugLine ( `Deleted empty directory: ${ dir } ` ) ;
289+ deletedFoldersCount ++ ;
221290 }
222291
223292 terminal . writeDebugLine ( `Existing entries tracked: ${ scanCount } ` ) ;
224293 markEnd ( 'unpack.scan.existing' ) ;
225294
226295 markStart ( 'unpack.extract.loop' ) ;
227- const bufferSize : number = 1 << 25 ; // 32 MiB
228- const outputBuffer : Buffer < ArrayBuffer > = Buffer . allocUnsafeSlow ( bufferSize ) ;
229296
230297 const dirsCreated : Set < string > = new Set < string > ( ) ;
231298
@@ -241,69 +308,12 @@ export function unpack({
241308 dirsCreated . add ( targetDir ) ;
242309 }
243310
244- let shouldExtract : boolean = true ;
245- if ( metadata ) {
246- const metadataFile : { size : number ; sha1Hash : string } | undefined = metadata . files [ entry . filename ] ;
247-
248- if ( metadataFile ) {
249- try {
250- using existingFile : IDisposableFileHandle = getDisposableFileHandle ( targetPath , 'r' ) ;
251- const existingHash : string | false = computeFileHash ( existingFile . fd ) ;
252- if ( existingHash === metadataFile . sha1Hash ) {
253- shouldExtract = false ;
254- skippedCount ++ ;
255- terminal . writeDebugLine ( `Skip unchanged file: ${ entry . filename } ` ) ;
256- }
257- } catch ( e ) {
258- if ( ( e as NodeJS . ErrnoException ) . code === 'ENOENT' ) {
259- // File does not exist, will extract
260- } else {
261- throw e ;
262- }
263- }
264- }
265- }
266-
267- if ( shouldExtract ) {
268- terminal . writeDebugLine ( `Extracting file: ${ entry . filename } ` ) ;
269- const fileZipBuffer : Buffer = getFileFromZip ( zipBuffer , entry ) ;
270- let fileData : Buffer ;
271- using fileHandle : IDisposableFileHandle = getDisposableFileHandle ( targetPath , 'w' ) ;
272- if ( entry . header . compressionMethod === STORE_COMPRESSION ) {
273- fileData = fileZipBuffer ;
274- let writeOffset : number = 0 ;
275- while ( writeOffset < fileData . length && ! isNaN ( fileHandle . fd ) ) {
276- const written : number = fs . writeSync (
277- fileHandle . fd ,
278- fileData ,
279- writeOffset ,
280- fileData . length - writeOffset
281- ) ;
282- writeOffset += written ;
283- }
284- } else if ( entry . header . compressionMethod === DEFLATE_COMPRESSION ) {
285- using inflateIncremental : IIncrementalZlib = createIncrementalZlib (
286- outputBuffer ,
287- ( chunk , lengthBytes ) => {
288- let writeOffset : number = 0 ;
289- while ( lengthBytes > 0 && writeOffset < chunk . byteLength ) {
290- const written : number = fs . writeSync ( fileHandle . fd , chunk , writeOffset , lengthBytes ) ;
291- lengthBytes -= written ;
292- writeOffset += written ;
293- }
294- } ,
295- 'inflate'
296- ) ;
297- inflateIncremental . update ( fileZipBuffer ) ;
298- inflateIncremental . update ( Buffer . alloc ( 0 ) ) ;
299- } else {
300- throw new Error (
301- `Unsupported compression method: ${ entry . header . compressionMethod } for ${ entry . filename } `
302- ) ;
303- }
304-
305- // If data descriptor was used we rely on central directory values already consumed.
311+ if ( shouldExtract ( terminal , targetPath , entry , metadata ) ) {
312+ extractFileFromZip ( terminal , targetPath , zipBuffer , entry ) ;
306313 extractedCount ++ ;
314+ } else {
315+ skippedCount ++ ;
316+ terminal . writeDebugLine ( `Skip unchanged file: ${ entry . filename } ` ) ;
307317 }
308318 }
309319 markEnd ( 'unpack.extract.loop' ) ;
0 commit comments