@@ -2984,10 +2984,12 @@ def __exit__(self, *exc):
2984
2984
self .bio = None
2985
2985
2986
2986
def add (self , name , * , type = None , symlink_to = None , hardlink_to = None ,
2987
- mode = None , ** kwargs ):
2987
+ mode = None , size = None , ** kwargs ):
2988
2988
"""Add a member to the test archive. Call within `with`."""
2989
2989
name = str (name )
2990
2990
tarinfo = tarfile .TarInfo (name ).replace (** kwargs )
2991
+ if size is not None :
2992
+ tarinfo .size = size
2991
2993
if mode :
2992
2994
tarinfo .mode = _filemode_to_int (mode )
2993
2995
if symlink_to is not None :
@@ -3051,7 +3053,8 @@ def check_context(self, tar, filter):
3051
3053
raise self .raised_exception
3052
3054
self .assertEqual (self .expected_paths , set ())
3053
3055
3054
- def expect_file (self , name , type = None , symlink_to = None , mode = None ):
3056
+ def expect_file (self , name , type = None , symlink_to = None , mode = None ,
3057
+ size = None ):
3055
3058
"""Check a single file. See check_context."""
3056
3059
if self .raised_exception :
3057
3060
raise self .raised_exception
@@ -3085,6 +3088,8 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None):
3085
3088
self .assertTrue (path .is_fifo ())
3086
3089
else :
3087
3090
raise NotImplementedError (type )
3091
+ if size is not None :
3092
+ self .assertEqual (path .stat ().st_size , size )
3088
3093
for parent in path .parents :
3089
3094
self .expected_paths .discard (parent )
3090
3095
@@ -3130,8 +3135,15 @@ def test_parent_symlink(self):
3130
3135
# Test interplaying symlinks
3131
3136
# Inspired by 'dirsymlink2a' in jwilk/traversal-archives
3132
3137
with ArchiveMaker () as arc :
3138
+
3139
+ # `current` links to `.` which is both:
3140
+ # - the destination directory
3141
+ # - `current` itself
3133
3142
arc .add ('current' , symlink_to = '.' )
3143
+
3144
+ # effectively points to ./../
3134
3145
arc .add ('parent' , symlink_to = 'current/..' )
3146
+
3135
3147
arc .add ('parent/evil' )
3136
3148
3137
3149
if support .can_symlink ():
@@ -3172,9 +3184,46 @@ def test_parent_symlink(self):
3172
3184
def test_parent_symlink2 (self ):
3173
3185
# Test interplaying symlinks
3174
3186
# Inspired by 'dirsymlink2b' in jwilk/traversal-archives
3187
+
3188
+ # Posix and Windows have different pathname resolution:
3189
+ # either symlink or a '..' component resolve first.
3190
+ # Let's see which we are on.
3191
+ if support .can_symlink ():
3192
+ testpath = os .path .join (TEMPDIR , 'resolution_test' )
3193
+ os .mkdir (testpath )
3194
+
3195
+ # testpath/current links to `.` which is all of:
3196
+ # - `testpath`
3197
+ # - `testpath/current`
3198
+ # - `testpath/current/current`
3199
+ # - etc.
3200
+ os .symlink ('.' , os .path .join (testpath , 'current' ))
3201
+
3202
+ # we'll test where `testpath/current/../file` ends up
3203
+ with open (os .path .join (testpath , 'current' , '..' , 'file' ), 'w' ):
3204
+ pass
3205
+
3206
+ if os .path .exists (os .path .join (testpath , 'file' )):
3207
+ # Windows collapses 'current\..' to '.' first, leaving
3208
+ # 'testpath\file'
3209
+ dotdot_resolves_early = True
3210
+ elif os .path .exists (os .path .join (testpath , '..' , 'file' )):
3211
+ # Posix resolves 'current' to '.' first, leaving
3212
+ # 'testpath/../file'
3213
+ dotdot_resolves_early = False
3214
+ else :
3215
+ raise AssertionError ('Could not determine link resolution' )
3216
+
3175
3217
with ArchiveMaker () as arc :
3218
+
3219
+ # `current` links to `.` which is both the destination directory
3220
+ # and `current` itself
3176
3221
arc .add ('current' , symlink_to = '.' )
3222
+
3223
+ # `current/parent` is also available as `./parent`,
3224
+ # and effectively points to `./../`
3177
3225
arc .add ('current/parent' , symlink_to = '..' )
3226
+
3178
3227
arc .add ('parent/evil' )
3179
3228
3180
3229
with self .check_context (arc .open (), 'fully_trusted' ):
@@ -3188,6 +3237,7 @@ def test_parent_symlink2(self):
3188
3237
3189
3238
with self .check_context (arc .open (), 'tar' ):
3190
3239
if support .can_symlink ():
3240
+ # Fail when extracting a file outside destination
3191
3241
self .expect_exception (
3192
3242
tarfile .OutsideDestinationError ,
3193
3243
"'parent/evil' would be extracted to "
@@ -3198,10 +3248,24 @@ def test_parent_symlink2(self):
3198
3248
self .expect_file ('parent/evil' )
3199
3249
3200
3250
with self .check_context (arc .open (), 'data' ):
3201
- self .expect_exception (
3202
- tarfile .LinkOutsideDestinationError ,
3203
- """'current/parent' would link to ['"].*['"], """
3204
- + "which is outside the destination" )
3251
+ if support .can_symlink ():
3252
+ if dotdot_resolves_early :
3253
+ # Fail when extracting a file outside destination
3254
+ self .expect_exception (
3255
+ tarfile .OutsideDestinationError ,
3256
+ "'parent/evil' would be extracted to "
3257
+ + """['"].*evil['"], which is outside """
3258
+ + "the destination" )
3259
+ else :
3260
+ # Fail as soon as we have a symlink outside the destination
3261
+ self .expect_exception (
3262
+ tarfile .LinkOutsideDestinationError ,
3263
+ "'current/parent' would link to "
3264
+ + """['"].*outerdir['"], which is outside """
3265
+ + "the destination" )
3266
+ else :
3267
+ self .expect_file ('current/' )
3268
+ self .expect_file ('parent/evil' )
3205
3269
3206
3270
def test_absolute_symlink (self ):
3207
3271
# Test symlink to an absolute path
@@ -3230,11 +3294,29 @@ def test_absolute_symlink(self):
3230
3294
with self .check_context (arc .open (), 'data' ):
3231
3295
self .expect_exception (
3232
3296
tarfile .AbsoluteLinkError ,
3233
- "'parent' is a symlink to an absolute path" )
3297
+ "'parent' is a link to an absolute path" )
3298
+
3299
+ def test_absolute_hardlink (self ):
3300
+ # Test hardlink to an absolute path
3301
+ # Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
3302
+ with ArchiveMaker () as arc :
3303
+ arc .add ('parent' , hardlink_to = self .outerdir / 'foo' )
3304
+
3305
+ with self .check_context (arc .open (), 'fully_trusted' ):
3306
+ self .expect_exception (KeyError , ".*foo. not found" )
3307
+
3308
+ with self .check_context (arc .open (), 'tar' ):
3309
+ self .expect_exception (KeyError , ".*foo. not found" )
3310
+
3311
+ with self .check_context (arc .open (), 'data' ):
3312
+ self .expect_exception (
3313
+ tarfile .AbsoluteLinkError ,
3314
+ "'parent' is a link to an absolute path" )
3234
3315
3235
3316
def test_sly_relative0 (self ):
3236
3317
# Inspired by 'relative0' in jwilk/traversal-archives
3237
3318
with ArchiveMaker () as arc :
3319
+ # points to `../../tmp/moo`
3238
3320
arc .add ('../moo' , symlink_to = '..//tmp/moo' )
3239
3321
3240
3322
try :
@@ -3284,6 +3366,54 @@ def test_sly_relative2(self):
3284
3366
+ """['"].*moo['"], which is outside the """
3285
3367
+ "destination" )
3286
3368
3369
+ def test_deep_symlink (self ):
3370
+ # Test that symlinks and hardlinks inside a directory
3371
+ # point to the correct file (`target` of size 3).
3372
+ # If links aren't supported we get a copy of the file.
3373
+ with ArchiveMaker () as arc :
3374
+ arc .add ('targetdir/target' , size = 3 )
3375
+ # a hardlink's linkname is relative to the archive
3376
+ arc .add ('linkdir/hardlink' , hardlink_to = os .path .join (
3377
+ 'targetdir' , 'target' ))
3378
+ # a symlink's linkname is relative to the link's directory
3379
+ arc .add ('linkdir/symlink' , symlink_to = os .path .join (
3380
+ '..' , 'targetdir' , 'target' ))
3381
+
3382
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3383
+ with self .check_context (arc .open (), filter ):
3384
+ self .expect_file ('targetdir/target' , size = 3 )
3385
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3386
+ if support .can_symlink ():
3387
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3388
+ symlink_to = '../targetdir/target' )
3389
+ else :
3390
+ self .expect_file ('linkdir/symlink' , size = 3 )
3391
+
3392
+ def test_chains (self ):
3393
+ # Test chaining of symlinks/hardlinks.
3394
+ # Symlinks are created before the files they point to.
3395
+ with ArchiveMaker () as arc :
3396
+ arc .add ('linkdir/symlink' , symlink_to = 'hardlink' )
3397
+ arc .add ('symlink2' , symlink_to = os .path .join (
3398
+ 'linkdir' , 'hardlink2' ))
3399
+ arc .add ('targetdir/target' , size = 3 )
3400
+ arc .add ('linkdir/hardlink' , hardlink_to = 'targetdir/target' )
3401
+ arc .add ('linkdir/hardlink2' , hardlink_to = 'linkdir/symlink' )
3402
+
3403
+ for filter in 'tar' , 'data' , 'fully_trusted' :
3404
+ with self .check_context (arc .open (), filter ):
3405
+ self .expect_file ('targetdir/target' , size = 3 )
3406
+ self .expect_file ('linkdir/hardlink' , size = 3 )
3407
+ self .expect_file ('linkdir/hardlink2' , size = 3 )
3408
+ if support .can_symlink ():
3409
+ self .expect_file ('linkdir/symlink' , size = 3 ,
3410
+ symlink_to = 'hardlink' )
3411
+ self .expect_file ('symlink2' , size = 3 ,
3412
+ symlink_to = 'linkdir/hardlink2' )
3413
+ else :
3414
+ self .expect_file ('linkdir/symlink' , size = 3 )
3415
+ self .expect_file ('symlink2' , size = 3 )
3416
+
3287
3417
def test_modes (self ):
3288
3418
# Test how file modes are extracted
3289
3419
# (Note that the modes are ignored on platforms without working chmod)
0 commit comments