@@ -3337,10 +3337,12 @@ def __exit__(self, *exc):
3337
3337
self .bio = None
3338
3338
3339
3339
def add (self , name , * , type = None , symlink_to = None , hardlink_to = None ,
3340
- mode = None , ** kwargs ):
3340
+ mode = None , size = None , ** kwargs ):
3341
3341
"""Add a member to the test archive. Call within `with`."""
3342
3342
name = str (name )
3343
3343
tarinfo = tarfile .TarInfo (name ).replace (** kwargs )
3344
+ if size is not None :
3345
+ tarinfo .size = size
3344
3346
if mode :
3345
3347
tarinfo .mode = _filemode_to_int (mode )
3346
3348
if symlink_to is not None :
@@ -3416,7 +3418,8 @@ def check_context(self, tar, filter):
3416
3418
raise self .raised_exception
3417
3419
self .assertEqual (self .expected_paths , set ())
3418
3420
3419
- def expect_file (self , name , type = None , symlink_to = None , mode = None ):
3421
+ def expect_file (self , name , type = None , symlink_to = None , mode = None ,
3422
+ size = None ):
3420
3423
"""Check a single file. See check_context."""
3421
3424
if self .raised_exception :
3422
3425
raise self .raised_exception
@@ -3445,6 +3448,8 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None):
3445
3448
self .assertTrue (path .is_fifo ())
3446
3449
else :
3447
3450
raise NotImplementedError (type )
3451
+ if size is not None :
3452
+ self .assertEqual (path .stat ().st_size , size )
3448
3453
for parent in path .parents :
3449
3454
self .expected_paths .discard (parent )
3450
3455
@@ -3491,8 +3496,15 @@ def test_parent_symlink(self):
3491
3496
# Test interplaying symlinks
3492
3497
# Inspired by 'dirsymlink2a' in jwilk/traversal-archives
3493
3498
with ArchiveMaker () as arc :
3499
+
3500
+ # `current` links to `.` which is both:
3501
+ # - the destination directory
3502
+ # - `current` itself
3494
3503
arc .add ('current' , symlink_to = '.' )
3504
+
3505
+ # effectively points to ./../
3495
3506
arc .add ('parent' , symlink_to = 'current/..' )
3507
+
3496
3508
arc .add ('parent/evil' )
3497
3509
3498
3510
if os_helper .can_symlink ():
@@ -3534,9 +3546,46 @@ def test_parent_symlink(self):
3534
3546
def test_parent_symlink2 (self ):
3535
3547
# Test interplaying symlinks
3536
3548
# Inspired by 'dirsymlink2b' in jwilk/traversal-archives
3549
+
3550
+ # Posix and Windows have different pathname resolution:
3551
+ # either symlink or a '..' component resolve first.
3552
+ # Let's see which we are on.
3553
+ if os_helper .can_symlink ():
3554
+ testpath = os .path .join (TEMPDIR , 'resolution_test' )
3555
+ os .mkdir (testpath )
3556
+
3557
+ # testpath/current links to `.` which is all of:
3558
+ # - `testpath`
3559
+ # - `testpath/current`
3560
+ # - `testpath/current/current`
3561
+ # - etc.
3562
+ os .symlink ('.' , os .path .join (testpath , 'current' ))
3563
+
3564
+ # we'll test where `testpath/current/../file` ends up
3565
+ with open (os .path .join (testpath , 'current' , '..' , 'file' ), 'w' ):
3566
+ pass
3567
+
3568
+ if os .path .exists (os .path .join (testpath , 'file' )):
3569
+ # Windows collapses 'current\..' to '.' first, leaving
3570
+ # 'testpath\file'
3571
+ dotdot_resolves_early = True
3572
+ elif os .path .exists (os .path .join (testpath , '..' , 'file' )):
3573
+ # Posix resolves 'current' to '.' first, leaving
3574
+ # 'testpath/../file'
3575
+ dotdot_resolves_early = False
3576
+ else :
3577
+ raise AssertionError ('Could not determine link resolution' )
3578
+
3537
3579
with ArchiveMaker () as arc :
3580
+
3581
+ # `current` links to `.` which is both the destination directory
3582
+ # and `current` itself
3538
3583
arc .add ('current' , symlink_to = '.' )
3584
+
3585
+ # `current/parent` is also available as `./parent`,
3586
+ # and effectively points to `./../`
3539
3587
arc .add ('current/parent' , symlink_to = '..' )
3588
+
3540
3589
arc .add ('parent/evil' )
3541
3590
3542
3591
with self .check_context (arc .open (), 'fully_trusted' ):
@@ -3550,6 +3599,7 @@ def test_parent_symlink2(self):
3550
3599
3551
3600
with self .check_context (arc .open (), 'tar' ):
3552
3601
if os_helper .can_symlink ():
3602
+ # Fail when extracting a file outside destination
3553
3603
self .expect_exception (
3554
3604
tarfile .OutsideDestinationError ,
3555
3605
"'parent/evil' would be extracted to "
@@ -3560,10 +3610,24 @@ def test_parent_symlink2(self):
3560
3610
self .expect_file ('parent/evil' )
3561
3611
3562
3612
with self .check_context (arc .open (), 'data' ):
3563
- self .expect_exception (
3564
- tarfile .LinkOutsideDestinationError ,
3565
- """'current/parent' would link to ['"].*['"], """
3566
- + "which is outside the destination" )
3613
+ if os_helper .can_symlink ():
3614
+ if dotdot_resolves_early :
3615
+ # Fail when extracting a file outside destination
3616
+ self .expect_exception (
3617
+ tarfile .OutsideDestinationError ,
3618
+ "'parent/evil' would be extracted to "
3619
+ + """['"].*evil['"], which is outside """
3620
+ + "the destination" )
3621
+ else :
3622
+ # Fail as soon as we have a symlink outside the destination
3623
+ self .expect_exception (
3624
+ tarfile .LinkOutsideDestinationError ,
3625
+ "'current/parent' would link to "
3626
+ + """['"].*outerdir['"], which is outside """
3627
+ + "the destination" )
3628
+ else :
3629
+ self .expect_file ('current/' )
3630
+ self .expect_file ('parent/evil' )
3567
3631
3568
3632
@symlink_test
3569
3633
def test_absolute_symlink (self ):
@@ -3593,12 +3657,30 @@ def test_absolute_symlink(self):
3593
3657
with self .check_context (arc .open (), 'data' ):
3594
3658
self .expect_exception (
3595
3659
tarfile .AbsoluteLinkError ,
3596
- "'parent' is a symlink to an absolute path" )
3660
+ "'parent' is a link to an absolute path" )
3661
+
3662
+ def test_absolute_hardlink (self ):
3663
+ # Test hardlink to an absolute path
3664
+ # Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
3665
+ with ArchiveMaker () as arc :
3666
+ arc .add ('parent' , hardlink_to = self .outerdir / 'foo' )
3667
+
3668
+ with self .check_context (arc .open (), 'fully_trusted' ):
3669
+ self .expect_exception (KeyError , ".*foo. not found" )
3670
+
3671
+ with self .check_context (arc .open (), 'tar' ):
3672
+ self .expect_exception (KeyError , ".*foo. not found" )
3673
+
3674
+ with self .check_context (arc .open (), 'data' ):
3675
+ self .expect_exception (
3676
+ tarfile .AbsoluteLinkError ,
3677
+ "'parent' is a link to an absolute path" )
3597
3678
3598
3679
@symlink_test
3599
3680
def test_sly_relative0 (self ):
3600
3681
# Inspired by 'relative0' in jwilk/traversal-archives
3601
3682
with ArchiveMaker () as arc :
3683
+ # points to `../../tmp/moo`
3602
3684
arc .add ('../moo' , symlink_to = '..//tmp/moo' )
3603
3685
3604
3686
try :
@@ -3649,6 +3731,56 @@ def test_sly_relative2(self):
3649
3731
+ """['"].*moo['"], which is outside the """
3650
3732
+ "destination" )
3651
3733
3734
+ @symlink_test
3735
+ def test_deep_symlink (self ):
3736
+ # Test that symlinks and hardlinks inside a directory
3737
+ # point to the correct file (`target` of size 3).
3738
+ # If links aren't supported we get a copy of the file.
3739
+ with ArchiveMaker () as arc :
3740
+ arc .add ('targetdir/target' , size = 3 )
3741
+ # a hardlink's linkname is relative to the archive
3742
+ arc .add ('linkdir/hardlink' , hardlink_to = os .path .join (
3743
+ 'targetdir' , 'target' ))
3744
+ # a symlink's linkname is relative to the link's directory
3745
+ arc .add ('linkdir/symlink' , symlink_to = os .path .join (
3746
+ '..' , 'targetdir' , 'target' ))
3747
+
3748
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3749
+ with self .check_context (arc .open (), filter ):
3750
+ self .expect_file ('targetdir/target' , size = 3 )
3751
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3752
+ if os_helper .can_symlink ():
3753
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3754
+ symlink_to = '../targetdir/target' )
3755
+ else :
3756
+ self .expect_file ('linkdir/symlink' , size = 3 )
3757
+
3758
+ @symlink_test
3759
+ def test_chains (self ):
3760
+ # Test chaining of symlinks/hardlinks.
3761
+ # Symlinks are created before the files they point to.
3762
+ with ArchiveMaker () as arc :
3763
+ arc .add ('linkdir/symlink' , symlink_to = 'hardlink' )
3764
+ arc .add ('symlink2' , symlink_to = os .path .join (
3765
+ 'linkdir' , 'hardlink2' ))
3766
+ arc .add ('targetdir/target' , size = 3 )
3767
+ arc .add ('linkdir/hardlink' , hardlink_to = 'targetdir/target' )
3768
+ arc .add ('linkdir/hardlink2' , hardlink_to = 'linkdir/symlink' )
3769
+
3770
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3771
+ with self .check_context (arc .open (), filter ):
3772
+ self .expect_file ('targetdir/target' , size = 3 )
3773
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3774
+ self .expect_file ('linkdir/hardlink2' , size = 3 )
3775
+ if os_helper .can_symlink ():
3776
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3777
+ symlink_to = 'hardlink' )
3778
+ self .expect_file ('symlink2' , size = 3 ,
3779
+ symlink_to = 'linkdir/hardlink2' )
3780
+ else :
3781
+ self .expect_file ('linkdir/symlink' , size = 3 )
3782
+ self .expect_file ('symlink2' , size = 3 )
3783
+
3652
3784
def test_modes (self ):
3653
3785
# Test how file modes are extracted
3654
3786
# (Note that the modes are ignored on platforms without working chmod)
0 commit comments