@@ -3209,10 +3209,12 @@ def __exit__(self, *exc):
3209
3209
self .bio = None
3210
3210
3211
3211
def add (self , name , * , type = None , symlink_to = None , hardlink_to = None ,
3212
- mode = None , ** kwargs ):
3212
+ mode = None , size = None , ** kwargs ):
3213
3213
"""Add a member to the test archive. Call within `with`."""
3214
3214
name = str (name )
3215
3215
tarinfo = tarfile .TarInfo (name ).replace (** kwargs )
3216
+ if size is not None :
3217
+ tarinfo .size = size
3216
3218
if mode :
3217
3219
tarinfo .mode = _filemode_to_int (mode )
3218
3220
if symlink_to is not None :
@@ -3276,7 +3278,8 @@ def check_context(self, tar, filter):
3276
3278
raise self .raised_exception
3277
3279
self .assertEqual (self .expected_paths , set ())
3278
3280
3279
- def expect_file (self , name , type = None , symlink_to = None , mode = None ):
3281
+ def expect_file (self , name , type = None , symlink_to = None , mode = None ,
3282
+ size = None ):
3280
3283
"""Check a single file. See check_context."""
3281
3284
if self .raised_exception :
3282
3285
raise self .raised_exception
@@ -3310,6 +3313,8 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None):
3310
3313
self .assertTrue (path .is_fifo ())
3311
3314
else :
3312
3315
raise NotImplementedError (type )
3316
+ if size is not None :
3317
+ self .assertEqual (path .stat ().st_size , size )
3313
3318
for parent in path .parents :
3314
3319
self .expected_paths .discard (parent )
3315
3320
@@ -3355,8 +3360,15 @@ def test_parent_symlink(self):
3355
3360
# Test interplaying symlinks
3356
3361
# Inspired by 'dirsymlink2a' in jwilk/traversal-archives
3357
3362
with ArchiveMaker () as arc :
3363
+
3364
+ # `current` links to `.` which is both:
3365
+ # - the destination directory
3366
+ # - `current` itself
3358
3367
arc .add ('current' , symlink_to = '.' )
3368
+
3369
+ # effectively points to ./../
3359
3370
arc .add ('parent' , symlink_to = 'current/..' )
3371
+
3360
3372
arc .add ('parent/evil' )
3361
3373
3362
3374
if os_helper .can_symlink ():
@@ -3397,9 +3409,46 @@ def test_parent_symlink(self):
3397
3409
def test_parent_symlink2 (self ):
3398
3410
# Test interplaying symlinks
3399
3411
# Inspired by 'dirsymlink2b' in jwilk/traversal-archives
3412
+
3413
+ # Posix and Windows have different pathname resolution:
3414
+ # either symlink or a '..' component resolve first.
3415
+ # Let's see which we are on.
3416
+ if os_helper .can_symlink ():
3417
+ testpath = os .path .join (TEMPDIR , 'resolution_test' )
3418
+ os .mkdir (testpath )
3419
+
3420
+ # testpath/current links to `.` which is all of:
3421
+ # - `testpath`
3422
+ # - `testpath/current`
3423
+ # - `testpath/current/current`
3424
+ # - etc.
3425
+ os .symlink ('.' , os .path .join (testpath , 'current' ))
3426
+
3427
+ # we'll test where `testpath/current/../file` ends up
3428
+ with open (os .path .join (testpath , 'current' , '..' , 'file' ), 'w' ):
3429
+ pass
3430
+
3431
+ if os .path .exists (os .path .join (testpath , 'file' )):
3432
+ # Windows collapses 'current\..' to '.' first, leaving
3433
+ # 'testpath\file'
3434
+ dotdot_resolves_early = True
3435
+ elif os .path .exists (os .path .join (testpath , '..' , 'file' )):
3436
+ # Posix resolves 'current' to '.' first, leaving
3437
+ # 'testpath/../file'
3438
+ dotdot_resolves_early = False
3439
+ else :
3440
+ raise AssertionError ('Could not determine link resolution' )
3441
+
3400
3442
with ArchiveMaker () as arc :
3443
+
3444
+ # `current` links to `.` which is both the destination directory
3445
+ # and `current` itself
3401
3446
arc .add ('current' , symlink_to = '.' )
3447
+
3448
+ # `current/parent` is also available as `./parent`,
3449
+ # and effectively points to `./../`
3402
3450
arc .add ('current/parent' , symlink_to = '..' )
3451
+
3403
3452
arc .add ('parent/evil' )
3404
3453
3405
3454
with self .check_context (arc .open (), 'fully_trusted' ):
@@ -3413,6 +3462,7 @@ def test_parent_symlink2(self):
3413
3462
3414
3463
with self .check_context (arc .open (), 'tar' ):
3415
3464
if os_helper .can_symlink ():
3465
+ # Fail when extracting a file outside destination
3416
3466
self .expect_exception (
3417
3467
tarfile .OutsideDestinationError ,
3418
3468
"'parent/evil' would be extracted to "
@@ -3423,10 +3473,24 @@ def test_parent_symlink2(self):
3423
3473
self .expect_file ('parent/evil' )
3424
3474
3425
3475
with self .check_context (arc .open (), 'data' ):
3426
- self .expect_exception (
3427
- tarfile .LinkOutsideDestinationError ,
3428
- """'current/parent' would link to ['"].*['"], """
3429
- + "which is outside the destination" )
3476
+ if os_helper .can_symlink ():
3477
+ if dotdot_resolves_early :
3478
+ # Fail when extracting a file outside destination
3479
+ self .expect_exception (
3480
+ tarfile .OutsideDestinationError ,
3481
+ "'parent/evil' would be extracted to "
3482
+ + """['"].*evil['"], which is outside """
3483
+ + "the destination" )
3484
+ else :
3485
+ # Fail as soon as we have a symlink outside the destination
3486
+ self .expect_exception (
3487
+ tarfile .LinkOutsideDestinationError ,
3488
+ "'current/parent' would link to "
3489
+ + """['"].*outerdir['"], which is outside """
3490
+ + "the destination" )
3491
+ else :
3492
+ self .expect_file ('current/' )
3493
+ self .expect_file ('parent/evil' )
3430
3494
3431
3495
def test_absolute_symlink (self ):
3432
3496
# Test symlink to an absolute path
@@ -3455,11 +3519,29 @@ def test_absolute_symlink(self):
3455
3519
with self .check_context (arc .open (), 'data' ):
3456
3520
self .expect_exception (
3457
3521
tarfile .AbsoluteLinkError ,
3458
- "'parent' is a symlink to an absolute path" )
3522
+ "'parent' is a link to an absolute path" )
3523
+
3524
+ def test_absolute_hardlink (self ):
3525
+ # Test hardlink to an absolute path
3526
+ # Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
3527
+ with ArchiveMaker () as arc :
3528
+ arc .add ('parent' , hardlink_to = self .outerdir / 'foo' )
3529
+
3530
+ with self .check_context (arc .open (), 'fully_trusted' ):
3531
+ self .expect_exception (KeyError , ".*foo. not found" )
3532
+
3533
+ with self .check_context (arc .open (), 'tar' ):
3534
+ self .expect_exception (KeyError , ".*foo. not found" )
3535
+
3536
+ with self .check_context (arc .open (), 'data' ):
3537
+ self .expect_exception (
3538
+ tarfile .AbsoluteLinkError ,
3539
+ "'parent' is a link to an absolute path" )
3459
3540
3460
3541
def test_sly_relative0 (self ):
3461
3542
# Inspired by 'relative0' in jwilk/traversal-archives
3462
3543
with ArchiveMaker () as arc :
3544
+ # points to `../../tmp/moo`
3463
3545
arc .add ('../moo' , symlink_to = '..//tmp/moo' )
3464
3546
3465
3547
try :
@@ -3509,6 +3591,54 @@ def test_sly_relative2(self):
3509
3591
+ """['"].*moo['"], which is outside the """
3510
3592
+ "destination" )
3511
3593
3594
+ def test_deep_symlink (self ):
3595
+ # Test that symlinks and hardlinks inside a directory
3596
+ # point to the correct file (`target` of size 3).
3597
+ # If links aren't supported we get a copy of the file.
3598
+ with ArchiveMaker () as arc :
3599
+ arc .add ('targetdir/target' , size = 3 )
3600
+ # a hardlink's linkname is relative to the archive
3601
+ arc .add ('linkdir/hardlink' , hardlink_to = os .path .join (
3602
+ 'targetdir' , 'target' ))
3603
+ # a symlink's linkname is relative to the link's directory
3604
+ arc .add ('linkdir/symlink' , symlink_to = os .path .join (
3605
+ '..' , 'targetdir' , 'target' ))
3606
+
3607
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3608
+ with self .check_context (arc .open (), filter ):
3609
+ self .expect_file ('targetdir/target' , size = 3 )
3610
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3611
+ if os_helper .can_symlink ():
3612
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3613
+ symlink_to = '../targetdir/target' )
3614
+ else :
3615
+ self .expect_file ('linkdir/symlink' , size = 3 )
3616
+
3617
+ def test_chains (self ):
3618
+ # Test chaining of symlinks/hardlinks.
3619
+ # Symlinks are created before the files they point to.
3620
+ with ArchiveMaker () as arc :
3621
+ arc .add ('linkdir/symlink' , symlink_to = 'hardlink' )
3622
+ arc .add ('symlink2' , symlink_to = os .path .join (
3623
+ 'linkdir' , 'hardlink2' ))
3624
+ arc .add ('targetdir/target' , size = 3 )
3625
+ arc .add ('linkdir/hardlink' , hardlink_to = 'targetdir/target' )
3626
+ arc .add ('linkdir/hardlink2' , hardlink_to = 'linkdir/symlink' )
3627
+
3628
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3629
+ with self .check_context (arc .open (), filter ):
3630
+ self .expect_file ('targetdir/target' , size = 3 )
3631
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3632
+ self .expect_file ('linkdir/hardlink2' , size = 3 )
3633
+ if os_helper .can_symlink ():
3634
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3635
+ symlink_to = 'hardlink' )
3636
+ self .expect_file ('symlink2' , size = 3 ,
3637
+ symlink_to = 'linkdir/hardlink2' )
3638
+ else :
3639
+ self .expect_file ('linkdir/symlink' , size = 3 )
3640
+ self .expect_file ('symlink2' , size = 3 )
3641
+
3512
3642
def test_modes (self ):
3513
3643
# Test how file modes are extracted
3514
3644
# (Note that the modes are ignored on platforms without working chmod)
0 commit comments