@@ -7,9 +7,8 @@ import { pipeline } from "node:stream/promises";
77import { transformHeader } from "../tar/options" ;
88import type { TarHeader } from "../tar/types" ;
99import { createTarUnpacker } from "../tar/unpacker" ;
10- import { normalizeUnicode , validateBounds } from "./path" ;
10+ import { normalizeHeaderName , normalizeUnicode , validateBounds } from "./path" ;
1111import type { UnpackOptionsFS } from "./types" ;
12- import { normalizeWindowsPath } from "./win-path" ;
1312
1413/**
1514 * Extract a tar archive to a directory.
@@ -169,12 +168,16 @@ function createFSHandler(directoryPath: string, options: UnpackOptionsFS) {
169168
170169 // If the directory is the destination directory, it already exists.
171170 promise = ( async ( ) : Promise < TarHeader [ "type" ] > => {
171+ if ( signal . aborted ) throw signal . reason ;
172+
172173 const destDir = await destDirPromise ;
173174 if ( dirPath === destDir . symbolic ) return "directory" ;
174175
175176 // Ensure parent directory exists first.
176177 await ensureDirectoryExists ( path . dirname ( dirPath ) ) ;
177178
179+ if ( signal . aborted ) throw signal . reason ;
180+
178181 // Check if the directory exists.
179182 try {
180183 await fs . mkdir ( dirPath , { mode : dmode } ) ;
@@ -204,130 +207,6 @@ function createFSHandler(directoryPath: string, options: UnpackOptionsFS) {
204207 return promise ;
205208 } ;
206209
207- const processHeader = async (
208- header : TarHeader ,
209- entryStream : PassThrough ,
210- ) : Promise < TarHeader [ "type" ] > => {
211- try {
212- // Await the destination directory to ensure it's created first.
213- const destDir = await destDirPromise ;
214- const normalizedName = normalizeUnicode (
215- normalizeWindowsPath ( header . name ) ,
216- ) ;
217-
218- // Prevent ReDOS via deep paths.
219- if ( maxDepth !== Infinity && normalizedName . split ( "/" ) . length > maxDepth )
220- throw new Error ( "Tar exceeds max specified depth." ) ;
221-
222- // Prevent absolute paths and ensure within destDir.
223- if ( path . isAbsolute ( normalizedName ) )
224- throw new Error ( `Absolute path found in "${ header . name } ".` ) ;
225-
226- const outPath = path . join ( destDir . symbolic , normalizedName ) ;
227- validateBounds (
228- outPath ,
229- destDir . symbolic ,
230- `Entry "${ header . name } " points outside the extraction directory.` ,
231- ) ;
232-
233- // Ensure parent directory exists.
234- const parentDir = path . dirname ( outPath ) ;
235- await ensureDirectoryExists ( parentDir ) ;
236-
237- switch ( header . type ) {
238- case "directory" :
239- await fs . mkdir ( outPath , {
240- recursive : true ,
241- mode : dmode ?? header . mode ,
242- } ) ;
243- break ;
244-
245- case "file" : {
246- const fileStream = createWriteStream ( outPath , {
247- mode : fmode ?? header . mode ,
248- // Use 512KB buffer for files > 1MB.
249- highWaterMark : header . size > 1048576 ? 524288 : undefined ,
250- } ) ;
251- await pipeline ( entryStream , fileStream ) ;
252- break ;
253- }
254-
255- case "symlink" : {
256- const { linkname } = header ;
257- if ( ! linkname ) return header . type ;
258- const target = path . resolve ( parentDir , linkname ) ;
259- validateBounds (
260- target ,
261- destDir . symbolic ,
262- `Symlink "${ linkname } " points outside the extraction directory.` ,
263- ) ;
264- await fs . symlink ( linkname , outPath ) ;
265- break ;
266- }
267-
268- case "link" : {
269- const { linkname } = header ;
270- if ( ! linkname ) return header . type ;
271-
272- // Resolve the hardlink target path and ensure it's within destDir.
273- const normalizedLink = normalizeUnicode ( linkname ) ;
274- if ( path . isAbsolute ( normalizedLink ) ) {
275- throw new Error (
276- `Hardlink "${ linkname } " points outside the extraction directory.` ,
277- ) ;
278- }
279-
280- // This is the symbolic path to the link's target inside the extraction dir.
281- const linkTarget = path . join ( destDir . symbolic , normalizedLink ) ;
282- validateBounds (
283- linkTarget ,
284- destDir . symbolic ,
285- `Hardlink "${ linkname } " points outside the extraction directory.` ,
286- ) ;
287- await ensureDirectoryExists ( path . dirname ( linkTarget ) ) ;
288-
289- // Resolve the real path of the parent directory which follows symlinks.
290- const realTargetParent = await fs . realpath ( path . dirname ( linkTarget ) ) ;
291- const realLinkTarget = path . join (
292- realTargetParent ,
293- path . basename ( linkTarget ) ,
294- ) ;
295-
296- // Check that the real path is within the destination directory.
297- validateBounds (
298- realLinkTarget ,
299- destDir . real ,
300- `Hardlink "${ linkname } " points outside the extraction directory.` ,
301- ) ;
302-
303- // A self-referential hardlink should be a noop.
304- if ( linkTarget === outPath ) return header . type ;
305-
306- // Wait for the target to be created if it is in the map.
307- const targetPromise = pathPromises . get ( linkTarget ) ;
308- if ( targetPromise ) await targetPromise ;
309-
310- await fs . link ( linkTarget , outPath ) ;
311- break ;
312- }
313-
314- default :
315- return header . type ; // Unsupported type
316- }
317-
318- // Set modification time if available.
319- if ( header . mtime ) {
320- const utimes = header . type === "symlink" ? fs . lutimes : fs . utimes ;
321- await utimes ( outPath , header . mtime , header . mtime ) . catch ( ( ) => { } ) ;
322- }
323-
324- return header . type ;
325- } finally {
326- // Ensure the entry stream is drained to avoid blocking.
327- if ( ! entryStream . readableEnded ) entryStream . resume ( ) ;
328- }
329- } ;
330-
331210 const handler = {
332211 onHeader ( header : TarHeader ) {
333212 if ( signal . aborted ) return ;
@@ -350,21 +229,13 @@ function createFSHandler(directoryPath: string, options: UnpackOptionsFS) {
350229 return ;
351230 }
352231
353- const destDir = path . resolve ( directoryPath ) ;
354-
355- // Ensure that "path" and "path/" are treated as the same key on all platforms.
356- const keyPath = path . join (
357- destDir ,
358- normalizeUnicode ( transformed . name ) ,
359- ) ;
360- const normalizedTarget =
361- keyPath . endsWith ( "/" ) || keyPath . endsWith ( "\\" )
362- ? keyPath . slice ( 0 , - 1 )
363- : keyPath ;
232+ // Normalize and resolve the target path.
233+ const name = normalizeHeaderName ( transformed . name ) ;
234+ const target = path . join ( path . resolve ( directoryPath ) , name ) ;
364235
365236 // Chain onto any prior operation for this path.
366237 const priorOpPromise =
367- pathPromises . get ( normalizedTarget ) || Promise . resolve ( undefined ) ;
238+ pathPromises . get ( target ) || Promise . resolve ( undefined ) ;
368239
369240 // Start the operation promise chain.
370241 opPromise = priorOpPromise . then ( async ( priorOp ) => {
@@ -374,20 +245,145 @@ function createFSHandler(directoryPath: string, options: UnpackOptionsFS) {
374245 ( priorOp === "directory" && transformed . type !== "directory" ) ||
375246 ( priorOp !== "directory" && transformed . type === "directory" ) ;
376247
377- if ( isConflict ) {
248+ if ( isConflict )
378249 throw new Error (
379- `Path conflict: cannot create ${ transformed . type } over existing ${ priorOp } at "${ transformed . name } "` ,
250+ `Path conflict ${ transformed . type } over existing ${ priorOp } at "${ transformed . name } "` ,
380251 ) ;
381- }
382252 }
383253
384- return await processHeader ( transformed , entryStream ) ;
254+ try {
255+ const destDir = await destDirPromise ;
256+
257+ // Prevent ReDOS via deep paths.
258+ if ( maxDepth !== Infinity && name . split ( "/" ) . length > maxDepth )
259+ throw new Error ( "Tar exceeds max specified depth." ) ;
260+
261+ // Prevent absolute paths and ensure within destDir.
262+ if ( path . isAbsolute ( name ) )
263+ throw new Error (
264+ `Absolute path found in "${ transformed . name } ".` ,
265+ ) ;
266+
267+ const outPath = path . join ( destDir . symbolic , name ) ;
268+ validateBounds (
269+ outPath ,
270+ destDir . symbolic ,
271+ `Entry "${ transformed . name } " points outside the extraction directory.` ,
272+ ) ;
273+
274+ // Ensure parent directory exists.
275+ const parentDir = path . dirname ( outPath ) ;
276+ await ensureDirectoryExists ( parentDir ) ;
277+
278+ switch ( transformed . type ) {
279+ case "directory" :
280+ await fs . mkdir ( outPath , {
281+ recursive : true ,
282+ mode : dmode ?? transformed . mode ,
283+ } ) ;
284+ break ;
285+
286+ case "file" : {
287+ const fileStream = createWriteStream ( outPath , {
288+ mode : fmode ?? transformed . mode ,
289+ // Use 512KB buffer for files > 1MB.
290+ highWaterMark :
291+ transformed . size > 1048576 ? 524288 : undefined ,
292+ } ) ;
293+ await pipeline ( entryStream , fileStream ) ;
294+ break ;
295+ }
296+
297+ case "symlink" : {
298+ const { linkname } = transformed ;
299+ if ( ! linkname ) return transformed . type ;
300+ const target = path . resolve ( parentDir , linkname ) ;
301+ validateBounds (
302+ target ,
303+ destDir . symbolic ,
304+ `Symlink "${ linkname } " points outside the extraction directory.` ,
305+ ) ;
306+ await fs . symlink ( linkname , outPath ) ;
307+ break ;
308+ }
309+
310+ case "link" : {
311+ const { linkname } = transformed ;
312+ if ( ! linkname ) return transformed . type ;
313+
314+ // Resolve the hardlink target path and ensure it's within destDir.
315+ const normalizedLink = normalizeUnicode ( linkname ) ;
316+ if ( path . isAbsolute ( normalizedLink ) ) {
317+ throw new Error (
318+ `Hardlink "${ linkname } " points outside the extraction directory.` ,
319+ ) ;
320+ }
321+
322+ // This is the symbolic path to the link's target inside the extraction dir.
323+ const linkTarget = path . join (
324+ destDir . symbolic ,
325+ normalizedLink ,
326+ ) ;
327+ validateBounds (
328+ linkTarget ,
329+ destDir . symbolic ,
330+ `Hardlink "${ linkname } " points outside the extraction directory.` ,
331+ ) ;
332+ await ensureDirectoryExists ( path . dirname ( linkTarget ) ) ;
333+
334+ // Resolve the real path of the parent directory which follows symlinks.
335+ const realTargetParent = await fs . realpath (
336+ path . dirname ( linkTarget ) ,
337+ ) ;
338+ const realLinkTarget = path . join (
339+ realTargetParent ,
340+ path . basename ( linkTarget ) ,
341+ ) ;
342+
343+ // Check that the real path is within the destination directory.
344+ validateBounds (
345+ realLinkTarget ,
346+ destDir . real ,
347+ `Hardlink "${ linkname } " points outside the extraction directory.` ,
348+ ) ;
349+
350+ // A self-referential hardlink should be a noop.
351+ if ( linkTarget === outPath ) return transformed . type ;
352+
353+ // Wait for the target to be created if it is in the map.
354+ const targetPromise = pathPromises . get ( linkTarget ) ;
355+ if ( targetPromise ) await targetPromise ;
356+
357+ await fs . link ( linkTarget , outPath ) ;
358+ break ;
359+ }
360+
361+ default :
362+ return transformed . type ; // Unsupported type
363+ }
364+
365+ // Set modification time if available.
366+ if ( transformed . mtime ) {
367+ const utimes =
368+ transformed . type === "symlink" ? fs . lutimes : fs . utimes ;
369+ await utimes (
370+ outPath ,
371+ transformed . mtime ,
372+ transformed . mtime ,
373+ ) . catch ( ( ) => { } ) ;
374+ }
375+
376+ return transformed . type ;
377+ } finally {
378+ // Ensure the entry stream is drained to avoid blocking.
379+ if ( ! entryStream . readableEnded ) entryStream . resume ( ) ;
380+ }
385381 } ) ;
386- pathPromises . set ( normalizedTarget , opPromise ) ;
382+
383+ pathPromises . set ( target , opPromise ) ;
387384 } catch ( err ) {
388385 opPromise = Promise . reject ( err ) ;
389386 abortController . abort ( err as Error ) ;
390- entryStream ?. destroy ( err as Error ) ;
391387 }
392388
393389 opPromise
@@ -413,7 +409,6 @@ function createFSHandler(directoryPath: string, options: UnpackOptionsFS) {
413409
414410 onError ( error : Error ) {
415411 abortController . abort ( error ) ;
416- activeEntryStream ?. destroy ( error ) ;
417412 } ,
418413
419414 async process ( ) {
0 commit comments