@@ -3169,10 +3169,12 @@ def __exit__(self, *exc):
3169
3169
self .bio = None
3170
3170
3171
3171
def add (self , name , * , type = None , symlink_to = None , hardlink_to = None ,
3172
- mode = None , ** kwargs ):
3172
+ mode = None , size = None , ** kwargs ):
3173
3173
"""Add a member to the test archive. Call within `with`."""
3174
3174
name = str (name )
3175
3175
tarinfo = tarfile .TarInfo (name ).replace (** kwargs )
3176
+ if size is not None :
3177
+ tarinfo .size = size
3176
3178
if mode :
3177
3179
tarinfo .mode = _filemode_to_int (mode )
3178
3180
if symlink_to is not None :
@@ -3236,7 +3238,8 @@ def check_context(self, tar, filter):
3236
3238
raise self .raised_exception
3237
3239
self .assertEqual (self .expected_paths , set ())
3238
3240
3239
- def expect_file (self , name , type = None , symlink_to = None , mode = None ):
3241
+ def expect_file (self , name , type = None , symlink_to = None , mode = None ,
3242
+ size = None ):
3240
3243
"""Check a single file. See check_context."""
3241
3244
if self .raised_exception :
3242
3245
raise self .raised_exception
@@ -3270,6 +3273,8 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None):
3270
3273
self .assertTrue (path .is_fifo ())
3271
3274
else :
3272
3275
raise NotImplementedError (type )
3276
+ if size is not None :
3277
+ self .assertEqual (path .stat ().st_size , size )
3273
3278
for parent in path .parents :
3274
3279
self .expected_paths .discard (parent )
3275
3280
@@ -3315,8 +3320,15 @@ def test_parent_symlink(self):
3315
3320
# Test interplaying symlinks
3316
3321
# Inspired by 'dirsymlink2a' in jwilk/traversal-archives
3317
3322
with ArchiveMaker () as arc :
3323
+
3324
+ # `current` links to `.` which is both:
3325
+ # - the destination directory
3326
+ # - `current` itself
3318
3327
arc .add ('current' , symlink_to = '.' )
3328
+
3329
+ # effectively points to ./../
3319
3330
arc .add ('parent' , symlink_to = 'current/..' )
3331
+
3320
3332
arc .add ('parent/evil' )
3321
3333
3322
3334
if support .can_symlink ():
@@ -3357,9 +3369,46 @@ def test_parent_symlink(self):
3357
3369
def test_parent_symlink2 (self ):
3358
3370
# Test interplaying symlinks
3359
3371
# Inspired by 'dirsymlink2b' in jwilk/traversal-archives
3372
+
3373
+ # Posix and Windows have different pathname resolution:
3374
+ # either symlink or a '..' component resolve first.
3375
+ # Let's see which we are on.
3376
+ if support .can_symlink ():
3377
+ testpath = os .path .join (TEMPDIR , 'resolution_test' )
3378
+ os .mkdir (testpath )
3379
+
3380
+ # testpath/current links to `.` which is all of:
3381
+ # - `testpath`
3382
+ # - `testpath/current`
3383
+ # - `testpath/current/current`
3384
+ # - etc.
3385
+ os .symlink ('.' , os .path .join (testpath , 'current' ))
3386
+
3387
+ # we'll test where `testpath/current/../file` ends up
3388
+ with open (os .path .join (testpath , 'current' , '..' , 'file' ), 'w' ):
3389
+ pass
3390
+
3391
+ if os .path .exists (os .path .join (testpath , 'file' )):
3392
+ # Windows collapses 'current\..' to '.' first, leaving
3393
+ # 'testpath\file'
3394
+ dotdot_resolves_early = True
3395
+ elif os .path .exists (os .path .join (testpath , '..' , 'file' )):
3396
+ # Posix resolves 'current' to '.' first, leaving
3397
+ # 'testpath/../file'
3398
+ dotdot_resolves_early = False
3399
+ else :
3400
+ raise AssertionError ('Could not determine link resolution' )
3401
+
3360
3402
with ArchiveMaker () as arc :
3403
+
3404
+ # `current` links to `.` which is both the destination directory
3405
+ # and `current` itself
3361
3406
arc .add ('current' , symlink_to = '.' )
3407
+
3408
+ # `current/parent` is also available as `./parent`,
3409
+ # and effectively points to `./../`
3362
3410
arc .add ('current/parent' , symlink_to = '..' )
3411
+
3363
3412
arc .add ('parent/evil' )
3364
3413
3365
3414
with self .check_context (arc .open (), 'fully_trusted' ):
@@ -3373,6 +3422,7 @@ def test_parent_symlink2(self):
3373
3422
3374
3423
with self .check_context (arc .open (), 'tar' ):
3375
3424
if support .can_symlink ():
3425
+ # Fail when extracting a file outside destination
3376
3426
self .expect_exception (
3377
3427
tarfile .OutsideDestinationError ,
3378
3428
"'parent/evil' would be extracted to "
@@ -3383,10 +3433,24 @@ def test_parent_symlink2(self):
3383
3433
self .expect_file ('parent/evil' )
3384
3434
3385
3435
with self .check_context (arc .open (), 'data' ):
3386
- self .expect_exception (
3387
- tarfile .LinkOutsideDestinationError ,
3388
- """'current/parent' would link to ['"].*['"], """
3389
- + "which is outside the destination" )
3436
+ if support .can_symlink ():
3437
+ if dotdot_resolves_early :
3438
+ # Fail when extracting a file outside destination
3439
+ self .expect_exception (
3440
+ tarfile .OutsideDestinationError ,
3441
+ "'parent/evil' would be extracted to "
3442
+ + """['"].*evil['"], which is outside """
3443
+ + "the destination" )
3444
+ else :
3445
+ # Fail as soon as we have a symlink outside the destination
3446
+ self .expect_exception (
3447
+ tarfile .LinkOutsideDestinationError ,
3448
+ "'current/parent' would link to "
3449
+ + """['"].*outerdir['"], which is outside """
3450
+ + "the destination" )
3451
+ else :
3452
+ self .expect_file ('current/' )
3453
+ self .expect_file ('parent/evil' )
3390
3454
3391
3455
def test_absolute_symlink (self ):
3392
3456
# Test symlink to an absolute path
@@ -3415,11 +3479,29 @@ def test_absolute_symlink(self):
3415
3479
with self .check_context (arc .open (), 'data' ):
3416
3480
self .expect_exception (
3417
3481
tarfile .AbsoluteLinkError ,
3418
- "'parent' is a symlink to an absolute path" )
3482
+ "'parent' is a link to an absolute path" )
3483
+
3484
+ def test_absolute_hardlink (self ):
3485
+ # Test hardlink to an absolute path
3486
+ # Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
3487
+ with ArchiveMaker () as arc :
3488
+ arc .add ('parent' , hardlink_to = self .outerdir / 'foo' )
3489
+
3490
+ with self .check_context (arc .open (), 'fully_trusted' ):
3491
+ self .expect_exception (KeyError , ".*foo. not found" )
3492
+
3493
+ with self .check_context (arc .open (), 'tar' ):
3494
+ self .expect_exception (KeyError , ".*foo. not found" )
3495
+
3496
+ with self .check_context (arc .open (), 'data' ):
3497
+ self .expect_exception (
3498
+ tarfile .AbsoluteLinkError ,
3499
+ "'parent' is a link to an absolute path" )
3419
3500
3420
3501
def test_sly_relative0 (self ):
3421
3502
# Inspired by 'relative0' in jwilk/traversal-archives
3422
3503
with ArchiveMaker () as arc :
3504
+ # points to `../../tmp/moo`
3423
3505
arc .add ('../moo' , symlink_to = '..//tmp/moo' )
3424
3506
3425
3507
try :
@@ -3469,6 +3551,54 @@ def test_sly_relative2(self):
3469
3551
+ """['"].*moo['"], which is outside the """
3470
3552
+ "destination" )
3471
3553
3554
+ def test_deep_symlink (self ):
3555
+ # Test that symlinks and hardlinks inside a directory
3556
+ # point to the correct file (`target` of size 3).
3557
+ # If links aren't supported we get a copy of the file.
3558
+ with ArchiveMaker () as arc :
3559
+ arc .add ('targetdir/target' , size = 3 )
3560
+ # a hardlink's linkname is relative to the archive
3561
+ arc .add ('linkdir/hardlink' , hardlink_to = os .path .join (
3562
+ 'targetdir' , 'target' ))
3563
+ # a symlink's linkname is relative to the link's directory
3564
+ arc .add ('linkdir/symlink' , symlink_to = os .path .join (
3565
+ '..' , 'targetdir' , 'target' ))
3566
+
3567
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3568
+ with self .check_context (arc .open (), filter ):
3569
+ self .expect_file ('targetdir/target' , size = 3 )
3570
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3571
+ if support .can_symlink ():
3572
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3573
+ symlink_to = '../targetdir/target' )
3574
+ else :
3575
+ self .expect_file ('linkdir/symlink' , size = 3 )
3576
+
3577
+ def test_chains (self ):
3578
+ # Test chaining of symlinks/hardlinks.
3579
+ # Symlinks are created before the files they point to.
3580
+ with ArchiveMaker () as arc :
3581
+ arc .add ('linkdir/symlink' , symlink_to = 'hardlink' )
3582
+ arc .add ('symlink2' , symlink_to = os .path .join (
3583
+ 'linkdir' , 'hardlink2' ))
3584
+ arc .add ('targetdir/target' , size = 3 )
3585
+ arc .add ('linkdir/hardlink' , hardlink_to = 'targetdir/target' )
3586
+ arc .add ('linkdir/hardlink2' , hardlink_to = 'linkdir/symlink' )
3587
+
3588
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3589
+ with self .check_context (arc .open (), filter ):
3590
+ self .expect_file ('targetdir/target' , size = 3 )
3591
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3592
+ self .expect_file ('linkdir/hardlink2' , size = 3 )
3593
+ if support .can_symlink ():
3594
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3595
+ symlink_to = 'hardlink' )
3596
+ self .expect_file ('symlink2' , size = 3 ,
3597
+ symlink_to = 'linkdir/hardlink2' )
3598
+ else :
3599
+ self .expect_file ('linkdir/symlink' , size = 3 )
3600
+ self .expect_file ('symlink2' , size = 3 )
3601
+
3472
3602
def test_modes (self ):
3473
3603
# Test how file modes are extracted
3474
3604
# (Note that the modes are ignored on platforms without working chmod)
0 commit comments