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