@@ -3239,10 +3239,12 @@ def __exit__(self, *exc):
3239
3239
self .bio = None
3240
3240
3241
3241
def add (self , name , * , type = None , symlink_to = None , hardlink_to = None ,
3242
- mode = None , ** kwargs ):
3242
+ mode = None , size = None , ** kwargs ):
3243
3243
"""Add a member to the test archive. Call within `with`."""
3244
3244
name = str (name )
3245
3245
tarinfo = tarfile .TarInfo (name ).replace (** kwargs )
3246
+ if size is not None :
3247
+ tarinfo .size = size
3246
3248
if mode :
3247
3249
tarinfo .mode = _filemode_to_int (mode )
3248
3250
if symlink_to is not None :
@@ -3318,7 +3320,8 @@ def check_context(self, tar, filter):
3318
3320
raise self .raised_exception
3319
3321
self .assertEqual (self .expected_paths , set ())
3320
3322
3321
- def expect_file (self , name , type = None , symlink_to = None , mode = None ):
3323
+ def expect_file (self , name , type = None , symlink_to = None , mode = None ,
3324
+ size = None ):
3322
3325
"""Check a single file. See check_context."""
3323
3326
if self .raised_exception :
3324
3327
raise self .raised_exception
@@ -3347,6 +3350,8 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None):
3347
3350
self .assertTrue (path .is_fifo ())
3348
3351
else :
3349
3352
raise NotImplementedError (type )
3353
+ if size is not None :
3354
+ self .assertEqual (path .stat ().st_size , size )
3350
3355
for parent in path .parents :
3351
3356
self .expected_paths .discard (parent )
3352
3357
@@ -3393,8 +3398,15 @@ def test_parent_symlink(self):
3393
3398
# Test interplaying symlinks
3394
3399
# Inspired by 'dirsymlink2a' in jwilk/traversal-archives
3395
3400
with ArchiveMaker () as arc :
3401
+
3402
+ # `current` links to `.` which is both:
3403
+ # - the destination directory
3404
+ # - `current` itself
3396
3405
arc .add ('current' , symlink_to = '.' )
3406
+
3407
+ # effectively points to ./../
3397
3408
arc .add ('parent' , symlink_to = 'current/..' )
3409
+
3398
3410
arc .add ('parent/evil' )
3399
3411
3400
3412
if os_helper .can_symlink ():
@@ -3436,9 +3448,46 @@ def test_parent_symlink(self):
3436
3448
def test_parent_symlink2 (self ):
3437
3449
# Test interplaying symlinks
3438
3450
# Inspired by 'dirsymlink2b' in jwilk/traversal-archives
3451
+
3452
+ # Posix and Windows have different pathname resolution:
3453
+ # either symlink or a '..' component resolve first.
3454
+ # Let's see which we are on.
3455
+ if os_helper .can_symlink ():
3456
+ testpath = os .path .join (TEMPDIR , 'resolution_test' )
3457
+ os .mkdir (testpath )
3458
+
3459
+ # testpath/current links to `.` which is all of:
3460
+ # - `testpath`
3461
+ # - `testpath/current`
3462
+ # - `testpath/current/current`
3463
+ # - etc.
3464
+ os .symlink ('.' , os .path .join (testpath , 'current' ))
3465
+
3466
+ # we'll test where `testpath/current/../file` ends up
3467
+ with open (os .path .join (testpath , 'current' , '..' , 'file' ), 'w' ):
3468
+ pass
3469
+
3470
+ if os .path .exists (os .path .join (testpath , 'file' )):
3471
+ # Windows collapses 'current\..' to '.' first, leaving
3472
+ # 'testpath\file'
3473
+ dotdot_resolves_early = True
3474
+ elif os .path .exists (os .path .join (testpath , '..' , 'file' )):
3475
+ # Posix resolves 'current' to '.' first, leaving
3476
+ # 'testpath/../file'
3477
+ dotdot_resolves_early = False
3478
+ else :
3479
+ raise AssertionError ('Could not determine link resolution' )
3480
+
3439
3481
with ArchiveMaker () as arc :
3482
+
3483
+ # `current` links to `.` which is both the destination directory
3484
+ # and `current` itself
3440
3485
arc .add ('current' , symlink_to = '.' )
3486
+
3487
+ # `current/parent` is also available as `./parent`,
3488
+ # and effectively points to `./../`
3441
3489
arc .add ('current/parent' , symlink_to = '..' )
3490
+
3442
3491
arc .add ('parent/evil' )
3443
3492
3444
3493
with self .check_context (arc .open (), 'fully_trusted' ):
@@ -3452,6 +3501,7 @@ def test_parent_symlink2(self):
3452
3501
3453
3502
with self .check_context (arc .open (), 'tar' ):
3454
3503
if os_helper .can_symlink ():
3504
+ # Fail when extracting a file outside destination
3455
3505
self .expect_exception (
3456
3506
tarfile .OutsideDestinationError ,
3457
3507
"'parent/evil' would be extracted to "
@@ -3462,10 +3512,24 @@ def test_parent_symlink2(self):
3462
3512
self .expect_file ('parent/evil' )
3463
3513
3464
3514
with self .check_context (arc .open (), 'data' ):
3465
- self .expect_exception (
3466
- tarfile .LinkOutsideDestinationError ,
3467
- """'current/parent' would link to ['"].*['"], """
3468
- + "which is outside the destination" )
3515
+ if os_helper .can_symlink ():
3516
+ if dotdot_resolves_early :
3517
+ # Fail when extracting a file outside destination
3518
+ self .expect_exception (
3519
+ tarfile .OutsideDestinationError ,
3520
+ "'parent/evil' would be extracted to "
3521
+ + """['"].*evil['"], which is outside """
3522
+ + "the destination" )
3523
+ else :
3524
+ # Fail as soon as we have a symlink outside the destination
3525
+ self .expect_exception (
3526
+ tarfile .LinkOutsideDestinationError ,
3527
+ "'current/parent' would link to "
3528
+ + """['"].*outerdir['"], which is outside """
3529
+ + "the destination" )
3530
+ else :
3531
+ self .expect_file ('current/' )
3532
+ self .expect_file ('parent/evil' )
3469
3533
3470
3534
@symlink_test
3471
3535
def test_absolute_symlink (self ):
@@ -3495,12 +3559,30 @@ def test_absolute_symlink(self):
3495
3559
with self .check_context (arc .open (), 'data' ):
3496
3560
self .expect_exception (
3497
3561
tarfile .AbsoluteLinkError ,
3498
- "'parent' is a symlink to an absolute path" )
3562
+ "'parent' is a link to an absolute path" )
3563
+
3564
+ def test_absolute_hardlink (self ):
3565
+ # Test hardlink to an absolute path
3566
+ # Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
3567
+ with ArchiveMaker () as arc :
3568
+ arc .add ('parent' , hardlink_to = self .outerdir / 'foo' )
3569
+
3570
+ with self .check_context (arc .open (), 'fully_trusted' ):
3571
+ self .expect_exception (KeyError , ".*foo. not found" )
3572
+
3573
+ with self .check_context (arc .open (), 'tar' ):
3574
+ self .expect_exception (KeyError , ".*foo. not found" )
3575
+
3576
+ with self .check_context (arc .open (), 'data' ):
3577
+ self .expect_exception (
3578
+ tarfile .AbsoluteLinkError ,
3579
+ "'parent' is a link to an absolute path" )
3499
3580
3500
3581
@symlink_test
3501
3582
def test_sly_relative0 (self ):
3502
3583
# Inspired by 'relative0' in jwilk/traversal-archives
3503
3584
with ArchiveMaker () as arc :
3585
+ # points to `../../tmp/moo`
3504
3586
arc .add ('../moo' , symlink_to = '..//tmp/moo' )
3505
3587
3506
3588
try :
@@ -3551,6 +3633,56 @@ def test_sly_relative2(self):
3551
3633
+ """['"].*moo['"], which is outside the """
3552
3634
+ "destination" )
3553
3635
3636
+ @symlink_test
3637
+ def test_deep_symlink (self ):
3638
+ # Test that symlinks and hardlinks inside a directory
3639
+ # point to the correct file (`target` of size 3).
3640
+ # If links aren't supported we get a copy of the file.
3641
+ with ArchiveMaker () as arc :
3642
+ arc .add ('targetdir/target' , size = 3 )
3643
+ # a hardlink's linkname is relative to the archive
3644
+ arc .add ('linkdir/hardlink' , hardlink_to = os .path .join (
3645
+ 'targetdir' , 'target' ))
3646
+ # a symlink's linkname is relative to the link's directory
3647
+ arc .add ('linkdir/symlink' , symlink_to = os .path .join (
3648
+ '..' , 'targetdir' , 'target' ))
3649
+
3650
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3651
+ with self .check_context (arc .open (), filter ):
3652
+ self .expect_file ('targetdir/target' , size = 3 )
3653
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3654
+ if os_helper .can_symlink ():
3655
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3656
+ symlink_to = '../targetdir/target' )
3657
+ else :
3658
+ self .expect_file ('linkdir/symlink' , size = 3 )
3659
+
3660
+ @symlink_test
3661
+ def test_chains (self ):
3662
+ # Test chaining of symlinks/hardlinks.
3663
+ # Symlinks are created before the files they point to.
3664
+ with ArchiveMaker () as arc :
3665
+ arc .add ('linkdir/symlink' , symlink_to = 'hardlink' )
3666
+ arc .add ('symlink2' , symlink_to = os .path .join (
3667
+ 'linkdir' , 'hardlink2' ))
3668
+ arc .add ('targetdir/target' , size = 3 )
3669
+ arc .add ('linkdir/hardlink' , hardlink_to = 'targetdir/target' )
3670
+ arc .add ('linkdir/hardlink2' , hardlink_to = 'linkdir/symlink' )
3671
+
3672
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3673
+ with self .check_context (arc .open (), filter ):
3674
+ self .expect_file ('targetdir/target' , size = 3 )
3675
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3676
+ self .expect_file ('linkdir/hardlink2' , size = 3 )
3677
+ if os_helper .can_symlink ():
3678
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3679
+ symlink_to = 'hardlink' )
3680
+ self .expect_file ('symlink2' , size = 3 ,
3681
+ symlink_to = 'linkdir/hardlink2' )
3682
+ else :
3683
+ self .expect_file ('linkdir/symlink' , size = 3 )
3684
+ self .expect_file ('symlink2' , size = 3 )
3685
+
3554
3686
def test_modes (self ):
3555
3687
# Test how file modes are extracted
3556
3688
# (Note that the modes are ignored on platforms without working chmod)
0 commit comments