@@ -3256,10 +3256,12 @@ def __exit__(self, *exc):
3256
3256
self .bio = None
3257
3257
3258
3258
def add (self , name , * , type = None , symlink_to = None , hardlink_to = None ,
3259
- mode = None , ** kwargs ):
3259
+ mode = None , size = None , ** kwargs ):
3260
3260
"""Add a member to the test archive. Call within `with`."""
3261
3261
name = str (name )
3262
3262
tarinfo = tarfile .TarInfo (name ).replace (** kwargs )
3263
+ if size is not None :
3264
+ tarinfo .size = size
3263
3265
if mode :
3264
3266
tarinfo .mode = _filemode_to_int (mode )
3265
3267
if symlink_to is not None :
@@ -3335,7 +3337,8 @@ def check_context(self, tar, filter):
3335
3337
raise self .raised_exception
3336
3338
self .assertEqual (self .expected_paths , set ())
3337
3339
3338
- def expect_file (self , name , type = None , symlink_to = None , mode = None ):
3340
+ def expect_file (self , name , type = None , symlink_to = None , mode = None ,
3341
+ size = None ):
3339
3342
"""Check a single file. See check_context."""
3340
3343
if self .raised_exception :
3341
3344
raise self .raised_exception
@@ -3364,6 +3367,8 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None):
3364
3367
self .assertTrue (path .is_fifo ())
3365
3368
else :
3366
3369
raise NotImplementedError (type )
3370
+ if size is not None :
3371
+ self .assertEqual (path .stat ().st_size , size )
3367
3372
for parent in path .parents :
3368
3373
self .expected_paths .discard (parent )
3369
3374
@@ -3410,8 +3415,15 @@ def test_parent_symlink(self):
3410
3415
# Test interplaying symlinks
3411
3416
# Inspired by 'dirsymlink2a' in jwilk/traversal-archives
3412
3417
with ArchiveMaker () as arc :
3418
+
3419
+ # `current` links to `.` which is both:
3420
+ # - the destination directory
3421
+ # - `current` itself
3413
3422
arc .add ('current' , symlink_to = '.' )
3423
+
3424
+ # effectively points to ./../
3414
3425
arc .add ('parent' , symlink_to = 'current/..' )
3426
+
3415
3427
arc .add ('parent/evil' )
3416
3428
3417
3429
if os_helper .can_symlink ():
@@ -3453,9 +3465,46 @@ def test_parent_symlink(self):
3453
3465
def test_parent_symlink2 (self ):
3454
3466
# Test interplaying symlinks
3455
3467
# Inspired by 'dirsymlink2b' in jwilk/traversal-archives
3468
+
3469
+ # Posix and Windows have different pathname resolution:
3470
+ # either symlink or a '..' component resolve first.
3471
+ # Let's see which we are on.
3472
+ if os_helper .can_symlink ():
3473
+ testpath = os .path .join (TEMPDIR , 'resolution_test' )
3474
+ os .mkdir (testpath )
3475
+
3476
+ # testpath/current links to `.` which is all of:
3477
+ # - `testpath`
3478
+ # - `testpath/current`
3479
+ # - `testpath/current/current`
3480
+ # - etc.
3481
+ os .symlink ('.' , os .path .join (testpath , 'current' ))
3482
+
3483
+ # we'll test where `testpath/current/../file` ends up
3484
+ with open (os .path .join (testpath , 'current' , '..' , 'file' ), 'w' ):
3485
+ pass
3486
+
3487
+ if os .path .exists (os .path .join (testpath , 'file' )):
3488
+ # Windows collapses 'current\..' to '.' first, leaving
3489
+ # 'testpath\file'
3490
+ dotdot_resolves_early = True
3491
+ elif os .path .exists (os .path .join (testpath , '..' , 'file' )):
3492
+ # Posix resolves 'current' to '.' first, leaving
3493
+ # 'testpath/../file'
3494
+ dotdot_resolves_early = False
3495
+ else :
3496
+ raise AssertionError ('Could not determine link resolution' )
3497
+
3456
3498
with ArchiveMaker () as arc :
3499
+
3500
+ # `current` links to `.` which is both the destination directory
3501
+ # and `current` itself
3457
3502
arc .add ('current' , symlink_to = '.' )
3503
+
3504
+ # `current/parent` is also available as `./parent`,
3505
+ # and effectively points to `./../`
3458
3506
arc .add ('current/parent' , symlink_to = '..' )
3507
+
3459
3508
arc .add ('parent/evil' )
3460
3509
3461
3510
with self .check_context (arc .open (), 'fully_trusted' ):
@@ -3469,6 +3518,7 @@ def test_parent_symlink2(self):
3469
3518
3470
3519
with self .check_context (arc .open (), 'tar' ):
3471
3520
if os_helper .can_symlink ():
3521
+ # Fail when extracting a file outside destination
3472
3522
self .expect_exception (
3473
3523
tarfile .OutsideDestinationError ,
3474
3524
"'parent/evil' would be extracted to "
@@ -3479,10 +3529,24 @@ def test_parent_symlink2(self):
3479
3529
self .expect_file ('parent/evil' )
3480
3530
3481
3531
with self .check_context (arc .open (), 'data' ):
3482
- self .expect_exception (
3483
- tarfile .LinkOutsideDestinationError ,
3484
- """'current/parent' would link to ['"].*['"], """
3485
- + "which is outside the destination" )
3532
+ if os_helper .can_symlink ():
3533
+ if dotdot_resolves_early :
3534
+ # Fail when extracting a file outside destination
3535
+ self .expect_exception (
3536
+ tarfile .OutsideDestinationError ,
3537
+ "'parent/evil' would be extracted to "
3538
+ + """['"].*evil['"], which is outside """
3539
+ + "the destination" )
3540
+ else :
3541
+ # Fail as soon as we have a symlink outside the destination
3542
+ self .expect_exception (
3543
+ tarfile .LinkOutsideDestinationError ,
3544
+ "'current/parent' would link to "
3545
+ + """['"].*outerdir['"], which is outside """
3546
+ + "the destination" )
3547
+ else :
3548
+ self .expect_file ('current/' )
3549
+ self .expect_file ('parent/evil' )
3486
3550
3487
3551
@symlink_test
3488
3552
def test_absolute_symlink (self ):
@@ -3512,12 +3576,30 @@ def test_absolute_symlink(self):
3512
3576
with self .check_context (arc .open (), 'data' ):
3513
3577
self .expect_exception (
3514
3578
tarfile .AbsoluteLinkError ,
3515
- "'parent' is a symlink to an absolute path" )
3579
+ "'parent' is a link to an absolute path" )
3580
+
3581
+ def test_absolute_hardlink (self ):
3582
+ # Test hardlink to an absolute path
3583
+ # Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
3584
+ with ArchiveMaker () as arc :
3585
+ arc .add ('parent' , hardlink_to = self .outerdir / 'foo' )
3586
+
3587
+ with self .check_context (arc .open (), 'fully_trusted' ):
3588
+ self .expect_exception (KeyError , ".*foo. not found" )
3589
+
3590
+ with self .check_context (arc .open (), 'tar' ):
3591
+ self .expect_exception (KeyError , ".*foo. not found" )
3592
+
3593
+ with self .check_context (arc .open (), 'data' ):
3594
+ self .expect_exception (
3595
+ tarfile .AbsoluteLinkError ,
3596
+ "'parent' is a link to an absolute path" )
3516
3597
3517
3598
@symlink_test
3518
3599
def test_sly_relative0 (self ):
3519
3600
# Inspired by 'relative0' in jwilk/traversal-archives
3520
3601
with ArchiveMaker () as arc :
3602
+ # points to `../../tmp/moo`
3521
3603
arc .add ('../moo' , symlink_to = '..//tmp/moo' )
3522
3604
3523
3605
try :
@@ -3568,6 +3650,56 @@ def test_sly_relative2(self):
3568
3650
+ """['"].*moo['"], which is outside the """
3569
3651
+ "destination" )
3570
3652
3653
+ @symlink_test
3654
+ def test_deep_symlink (self ):
3655
+ # Test that symlinks and hardlinks inside a directory
3656
+ # point to the correct file (`target` of size 3).
3657
+ # If links aren't supported we get a copy of the file.
3658
+ with ArchiveMaker () as arc :
3659
+ arc .add ('targetdir/target' , size = 3 )
3660
+ # a hardlink's linkname is relative to the archive
3661
+ arc .add ('linkdir/hardlink' , hardlink_to = os .path .join (
3662
+ 'targetdir' , 'target' ))
3663
+ # a symlink's linkname is relative to the link's directory
3664
+ arc .add ('linkdir/symlink' , symlink_to = os .path .join (
3665
+ '..' , 'targetdir' , 'target' ))
3666
+
3667
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3668
+ with self .check_context (arc .open (), filter ):
3669
+ self .expect_file ('targetdir/target' , size = 3 )
3670
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3671
+ if os_helper .can_symlink ():
3672
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3673
+ symlink_to = '../targetdir/target' )
3674
+ else :
3675
+ self .expect_file ('linkdir/symlink' , size = 3 )
3676
+
3677
+ @symlink_test
3678
+ def test_chains (self ):
3679
+ # Test chaining of symlinks/hardlinks.
3680
+ # Symlinks are created before the files they point to.
3681
+ with ArchiveMaker () as arc :
3682
+ arc .add ('linkdir/symlink' , symlink_to = 'hardlink' )
3683
+ arc .add ('symlink2' , symlink_to = os .path .join (
3684
+ 'linkdir' , 'hardlink2' ))
3685
+ arc .add ('targetdir/target' , size = 3 )
3686
+ arc .add ('linkdir/hardlink' , hardlink_to = 'targetdir/target' )
3687
+ arc .add ('linkdir/hardlink2' , hardlink_to = 'linkdir/symlink' )
3688
+
3689
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3690
+ with self .check_context (arc .open (), filter ):
3691
+ self .expect_file ('targetdir/target' , size = 3 )
3692
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3693
+ self .expect_file ('linkdir/hardlink2' , size = 3 )
3694
+ if os_helper .can_symlink ():
3695
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3696
+ symlink_to = 'hardlink' )
3697
+ self .expect_file ('symlink2' , size = 3 ,
3698
+ symlink_to = 'linkdir/hardlink2' )
3699
+ else :
3700
+ self .expect_file ('linkdir/symlink' , size = 3 )
3701
+ self .expect_file ('symlink2' , size = 3 )
3702
+
3571
3703
def test_modes (self ):
3572
3704
# Test how file modes are extracted
3573
3705
# (Note that the modes are ignored on platforms without working chmod)
0 commit comments