@@ -330,59 +330,65 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b
330
330
Debug . Assert ( ! string . IsNullOrEmpty ( destinationDirectoryPath ) ) ;
331
331
Debug . Assert ( Path . IsPathFullyQualified ( destinationDirectoryPath ) ) ;
332
332
333
- destinationDirectoryPath = Path . TrimEndingDirectorySeparator ( destinationDirectoryPath ) ;
334
-
335
- string ? fileDestinationPath = GetSanitizedFullPath ( destinationDirectoryPath , Name ) ;
333
+ string name = ArchivingUtils . SanitizeEntryFilePath ( Name , preserveDriveRoot : true ) ;
334
+ string ? fileDestinationPath = GetFullDestinationPath (
335
+ destinationDirectoryPath ,
336
+ Path . IsPathFullyQualified ( name ) ? name : Path . Join ( Path . GetDirectoryName ( destinationDirectoryPath ) , name ) ) ;
336
337
if ( fileDestinationPath == null )
337
338
{
338
- throw new IOException ( SR . Format ( SR . TarExtractingResultsFileOutside , Name , destinationDirectoryPath ) ) ;
339
+ throw new IOException ( SR . Format ( SR . TarExtractingResultsFileOutside , name , destinationDirectoryPath ) ) ;
339
340
}
340
341
341
342
string ? linkTargetPath = null ;
342
- if ( EntryType is TarEntryType . SymbolicLink or TarEntryType . HardLink )
343
- {
344
- if ( string . IsNullOrEmpty ( LinkName ) )
343
+ if ( EntryType is TarEntryType . SymbolicLink )
344
+ {
345
+ // LinkName is an absolute path, or path relative to the fileDestinationPath directory.
346
+ // We don't check if the LinkName is empty. In that case, creation of the link will fail because link targets can't be empty.
347
+ string linkName = ArchivingUtils . SanitizeEntryFilePath ( LinkName , preserveDriveRoot : true ) ;
348
+ string ? linkDestination = GetFullDestinationPath (
349
+ destinationDirectoryPath ,
350
+ Path . IsPathFullyQualified ( linkName ) ? linkName : Path . Join ( Path . GetDirectoryName ( fileDestinationPath ) , linkName ) ) ;
351
+ if ( linkDestination is null )
345
352
{
346
- throw new InvalidDataException ( SR . TarEntryHardLinkOrSymlinkLinkNameEmpty ) ;
353
+ throw new IOException ( SR . Format ( SR . TarExtractingResultsLinkOutside , linkName , destinationDirectoryPath ) ) ;
347
354
}
348
-
349
- linkTargetPath = GetSanitizedFullPath ( destinationDirectoryPath ,
350
- Path . IsPathFullyQualified ( LinkName ) ? LinkName : Path . Join ( Path . GetDirectoryName ( fileDestinationPath ) , LinkName ) ) ;
351
-
352
- if ( linkTargetPath == null )
355
+ // Use the linkName for creating the symbolic link.
356
+ linkTargetPath = linkName ;
357
+ }
358
+ else if ( EntryType is TarEntryType . HardLink )
359
+ {
360
+ // LinkName is path relative to the destinationDirectoryPath.
361
+ // We don't check if the LinkName is empty. In that case, creation of the link will fail because a hard link can't target a directory.
362
+ string linkName = ArchivingUtils . SanitizeEntryFilePath ( LinkName , preserveDriveRoot : false ) ;
363
+ string ? linkDestination = GetFullDestinationPath (
364
+ destinationDirectoryPath ,
365
+ Path . Join ( destinationDirectoryPath , linkName ) ) ;
366
+ if ( linkDestination is null )
353
367
{
354
- throw new IOException ( SR . Format ( SR . TarExtractingResultsLinkOutside , LinkName , destinationDirectoryPath ) ) ;
368
+ throw new IOException ( SR . Format ( SR . TarExtractingResultsLinkOutside , linkName , destinationDirectoryPath ) ) ;
355
369
}
356
-
357
- // after TarExtractingResultsLinkOutside validation, preserve the original
358
- // symlink target path (to match behavior of other utilities).
359
- linkTargetPath = LinkName ;
370
+ // Use the target path for creating the hard link.
371
+ linkTargetPath = linkDestination ;
360
372
}
361
373
362
374
return ( fileDestinationPath , linkTargetPath ) ;
363
375
}
364
376
365
- // If the path can be extracted in the specified destination directory, returns the full path with sanitized file name . Otherwise, returns null.
366
- private static string ? GetSanitizedFullPath ( string destinationDirectoryFullPath , string path )
377
+ // Returns the full destination path if the path is the destinationDirectory or a subpath . Otherwise, returns null.
378
+ private static string ? GetFullDestinationPath ( string destinationDirectoryFullPath , string qualifiedPath )
367
379
{
368
- destinationDirectoryFullPath = PathInternal . EnsureTrailingSeparator ( destinationDirectoryFullPath ) ;
380
+ Debug . Assert ( Path . IsPathFullyQualified ( qualifiedPath ) , $ "{ qualifiedPath } is not qualified") ;
381
+ Debug . Assert ( PathInternal . EndsInDirectorySeparator ( destinationDirectoryFullPath ) , "caller must ensure the path ends with a separator." ) ;
369
382
370
- string fullyQualifiedPath = Path . IsPathFullyQualified ( path ) ? path : Path . Combine ( destinationDirectoryFullPath , path ) ;
371
- string normalizedPath = Path . GetFullPath ( fullyQualifiedPath ) ; // Removes relative segments
372
- string ? fileName = Path . GetFileName ( normalizedPath ) ;
373
- if ( string . IsNullOrEmpty ( fileName ) ) // It's a directory
374
- {
375
- fileName = PathInternal . DirectorySeparatorCharAsString ;
376
- }
383
+ string fullPath = Path . GetFullPath ( qualifiedPath ) ; // Removes relative segments
377
384
378
- string sanitizedPath = Path . Join ( Path . GetDirectoryName ( normalizedPath ) , ArchivingUtils . SanitizeEntryFilePath ( fileName ) ) ;
379
- return sanitizedPath . StartsWith ( destinationDirectoryFullPath , PathInternal . StringComparison ) ? sanitizedPath : null ;
385
+ return fullPath . StartsWith ( destinationDirectoryFullPath , PathInternal . StringComparison ) ? fullPath : null ;
380
386
}
381
387
382
388
// Extracts the current entry into the filesystem, regardless of the entry type.
383
389
private void ExtractToFileInternal ( string filePath , string ? linkTargetPath , bool overwrite )
384
390
{
385
- VerifyPathsForEntryType ( filePath , linkTargetPath , overwrite ) ;
391
+ VerifyDestinationPath ( filePath , overwrite ) ;
386
392
387
393
if ( EntryType is TarEntryType . RegularFile or TarEntryType . V7RegularFile or TarEntryType . ContiguousFile )
388
394
{
@@ -401,7 +407,7 @@ private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath,
401
407
{
402
408
return Task . FromCanceled ( cancellationToken ) ;
403
409
}
404
- VerifyPathsForEntryType ( filePath , linkTargetPath , overwrite ) ;
410
+ VerifyDestinationPath ( filePath , overwrite ) ;
405
411
406
412
if ( EntryType is TarEntryType . RegularFile or TarEntryType . V7RegularFile or TarEntryType . ContiguousFile )
407
413
{
@@ -423,7 +429,7 @@ private void CreateNonRegularFile(string filePath, string? linkTargetPath)
423
429
case TarEntryType . Directory :
424
430
case TarEntryType . DirectoryList :
425
431
// Mode must only be used for the leaf directory.
426
- // VerifyPathsForEntryType ensures we're only creating a leaf.
432
+ // VerifyDestinationPath ensures we're only creating a leaf.
427
433
Debug . Assert ( Directory . Exists ( Path . GetDirectoryName ( filePath ) ) ) ;
428
434
Debug . Assert ( ! Directory . Exists ( filePath ) ) ;
429
435
@@ -476,8 +482,8 @@ private void CreateNonRegularFile(string filePath, string? linkTargetPath)
476
482
}
477
483
}
478
484
479
- // Verifies if the specified paths make sense for the current type of entry .
480
- private void VerifyPathsForEntryType ( string filePath , string ? linkTargetPath , bool overwrite )
485
+ // Verifies there's a writable destination .
486
+ private static void VerifyDestinationPath ( string filePath , bool overwrite )
481
487
{
482
488
string ? directoryPath = Path . GetDirectoryName ( filePath ) ;
483
489
// If the destination contains a directory segment, need to check that it exists
@@ -503,35 +509,6 @@ private void VerifyPathsForEntryType(string filePath, string? linkTargetPath, bo
503
509
throw new IOException ( SR . Format ( SR . IO_AlreadyExists_Name , filePath ) ) ;
504
510
}
505
511
File . Delete ( filePath ) ;
506
-
507
- if ( EntryType is TarEntryType . SymbolicLink or TarEntryType . HardLink )
508
- {
509
- if ( ! string . IsNullOrEmpty ( linkTargetPath ) )
510
- {
511
- string ? targetDirectoryPath = Path . GetDirectoryName ( linkTargetPath ) ;
512
- // If the destination target contains a directory segment, need to check that it exists
513
- if ( ! string . IsNullOrEmpty ( targetDirectoryPath ) && ! Path . Exists ( targetDirectoryPath ) )
514
- {
515
- throw new IOException ( SR . Format ( SR . TarSymbolicLinkTargetNotExists , filePath , linkTargetPath ) ) ;
516
- }
517
-
518
- if ( EntryType is TarEntryType . HardLink )
519
- {
520
- if ( ! Path . Exists ( linkTargetPath ) )
521
- {
522
- throw new IOException ( SR . Format ( SR . TarHardLinkTargetNotExists , filePath , linkTargetPath ) ) ;
523
- }
524
- else if ( Directory . Exists ( linkTargetPath ) )
525
- {
526
- throw new IOException ( SR . Format ( SR . TarHardLinkToDirectoryNotAllowed , filePath , linkTargetPath ) ) ;
527
- }
528
- }
529
- }
530
- else
531
- {
532
- throw new InvalidDataException ( SR . TarEntryHardLinkOrSymlinkLinkNameEmpty ) ;
533
- }
534
- }
535
512
}
536
513
537
514
// Extracts the current entry as a regular file into the specified destination.
0 commit comments