@@ -2777,7 +2777,7 @@ def test_useful_error_message_when_modules_missing(self):
27772777 str (excinfo .exception ),
27782778 )
27792779
2780- @unittest . skipUnless ( os_helper .can_symlink (), 'requires symlink support' )
2780+ @os_helper .skip_unless_symlink
27812781 @unittest .skipUnless (hasattr (os , 'chmod' ), "missing os.chmod" )
27822782 @unittest .mock .patch ('os.chmod' )
27832783 def test_deferred_directory_attributes_update (self , mock_chmod ):
@@ -3663,6 +3663,39 @@ class TestExtractionFilters(unittest.TestCase):
36633663 # The destination for the extraction, within `outerdir`
36643664 destdir = outerdir / 'dest'
36653665
3666+ @classmethod
3667+ def setUpClass (cls ):
3668+ # Posix and Windows have different pathname resolution:
3669+ # either symlink or a '..' component resolve first.
3670+ # Let's see which we are on.
3671+ if os_helper .can_symlink ():
3672+ testpath = os .path .join (TEMPDIR , 'resolution_test' )
3673+ os .mkdir (testpath )
3674+
3675+ # testpath/current links to `.` which is all of:
3676+ # - `testpath`
3677+ # - `testpath/current`
3678+ # - `testpath/current/current`
3679+ # - etc.
3680+ os .symlink ('.' , os .path .join (testpath , 'current' ))
3681+
3682+ # we'll test where `testpath/current/../file` ends up
3683+ with open (os .path .join (testpath , 'current' , '..' , 'file' ), 'w' ):
3684+ pass
3685+
3686+ if os .path .exists (os .path .join (testpath , 'file' )):
3687+ # Windows collapses 'current\..' to '.' first, leaving
3688+ # 'testpath\file'
3689+ cls .dotdot_resolves_early = True
3690+ elif os .path .exists (os .path .join (testpath , '..' , 'file' )):
3691+ # Posix resolves 'current' to '.' first, leaving
3692+ # 'testpath/../file'
3693+ cls .dotdot_resolves_early = False
3694+ else :
3695+ raise AssertionError ('Could not determine link resolution' )
3696+ else :
3697+ cls .dotdot_resolves_early = True
3698+
36663699 @contextmanager
36673700 def check_context (self , tar , filter , * , check_flag = True ):
36683701 """Extracts `tar` to `self.destdir` and allows checking the result
@@ -3809,23 +3842,21 @@ def test_parent_symlink(self):
38093842 arc .add ('current' , symlink_to = '.' )
38103843
38113844 # effectively points to ./../
3812- arc .add ('parent' , symlink_to = 'current/..' )
3845+ if self .dotdot_resolves_early :
3846+ arc .add ('parent' , symlink_to = 'current/../..' )
3847+ else :
3848+ arc .add ('parent' , symlink_to = 'current/..' )
38133849
38143850 arc .add ('parent/evil' )
38153851
38163852 if os_helper .can_symlink ():
38173853 with self .check_context (arc .open (), 'fully_trusted' ):
3818- if self .raised_exception is not None :
3819- # Windows will refuse to create a file that's a symlink to itself
3820- # (and tarfile doesn't swallow that exception)
3821- self .expect_exception (FileExistsError )
3822- # The other cases will fail with this error too.
3823- # Skip the rest of this test.
3824- return
3854+ self .expect_file ('current' , symlink_to = '.' )
3855+ if self .dotdot_resolves_early :
3856+ self .expect_file ('parent' , symlink_to = 'current/../..' )
38253857 else :
3826- self .expect_file ('current' , symlink_to = '.' )
38273858 self .expect_file ('parent' , symlink_to = 'current/..' )
3828- self .expect_file ('../evil' )
3859+ self .expect_file ('../evil' )
38293860
38303861 with self .check_context (arc .open (), 'tar' ):
38313862 self .expect_exception (
@@ -3927,35 +3958,6 @@ def test_parent_symlink2(self):
39273958 # Test interplaying symlinks
39283959 # Inspired by 'dirsymlink2b' in jwilk/traversal-archives
39293960
3930- # Posix and Windows have different pathname resolution:
3931- # either symlink or a '..' component resolve first.
3932- # Let's see which we are on.
3933- if os_helper .can_symlink ():
3934- testpath = os .path .join (TEMPDIR , 'resolution_test' )
3935- os .mkdir (testpath )
3936-
3937- # testpath/current links to `.` which is all of:
3938- # - `testpath`
3939- # - `testpath/current`
3940- # - `testpath/current/current`
3941- # - etc.
3942- os .symlink ('.' , os .path .join (testpath , 'current' ))
3943-
3944- # we'll test where `testpath/current/../file` ends up
3945- with open (os .path .join (testpath , 'current' , '..' , 'file' ), 'w' ):
3946- pass
3947-
3948- if os .path .exists (os .path .join (testpath , 'file' )):
3949- # Windows collapses 'current\..' to '.' first, leaving
3950- # 'testpath\file'
3951- dotdot_resolves_early = True
3952- elif os .path .exists (os .path .join (testpath , '..' , 'file' )):
3953- # Posix resolves 'current' to '.' first, leaving
3954- # 'testpath/../file'
3955- dotdot_resolves_early = False
3956- else :
3957- raise AssertionError ('Could not determine link resolution' )
3958-
39593961 with ArchiveMaker () as arc :
39603962
39613963 # `current` links to `.` which is both the destination directory
@@ -3991,7 +3993,7 @@ def test_parent_symlink2(self):
39913993
39923994 with self .check_context (arc .open (), 'data' ):
39933995 if os_helper .can_symlink ():
3994- if dotdot_resolves_early :
3996+ if self . dotdot_resolves_early :
39953997 # Fail when extracting a file outside destination
39963998 self .expect_exception (
39973999 tarfile .OutsideDestinationError ,
@@ -4039,6 +4041,21 @@ def test_absolute_symlink(self):
40394041 tarfile .AbsoluteLinkError ,
40404042 "'parent' is a link to an absolute path" )
40414043
4044+ @symlink_test
4045+ @os_helper .skip_unless_symlink
4046+ def test_symlink_target_seperator_rewrite_on_windows (self ):
4047+ with ArchiveMaker () as arc :
4048+ arc .add ('link' , symlink_to = "relative/test/path" )
4049+
4050+ with self .check_context (arc .open (), 'fully_trusted' ):
4051+ self .expect_file ('link' , type = tarfile .SYMTYPE )
4052+ link_path = os .path .normpath (self .destdir / "link" )
4053+ link_target = os .readlink (link_path )
4054+ if os .name == "nt" :
4055+ self .assertEqual (link_target , "relative\\ test\\ path" )
4056+ else :
4057+ self .assertEqual (link_target , "relative/test/path" )
4058+
40424059 def test_absolute_hardlink (self ):
40434060 # Test hardlink to an absolute path
40444061 # Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
0 commit comments