import os import shutil import stat import sys import tarfile import tempfile import time import zipfile import pytest from pip._internal.exceptions import InstallationError from pip._internal.utils.unpacking import ( is_within_directory, untar_file, unzip_file, ) class TestUnpackArchives(object): """ test_tar.tgz/test_tar.zip have content as follows engineered to confirm 3 things: 1) confirm that reg files, dirs, and symlinks get unpacked 2) permissions are not preserved (and go by the 022 umask) 3) reg files with *any* execute perms, get chmod +x file.txt 600 regular file symlink.txt 777 symlink to file.txt script_owner.sh 700 script where owner can execute script_group.sh 610 script where group can execute script_world.sh 601 script where world can execute dir 744 directory dir/dirfile 622 regular file 4) the file contents are extracted correctly (though the content of each file isn't currently unique) """ def setup(self): self.tempdir = tempfile.mkdtemp() self.old_mask = os.umask(0o022) self.symlink_expected_mode = None def teardown(self): os.umask(self.old_mask) shutil.rmtree(self.tempdir, ignore_errors=True) def mode(self, path): return stat.S_IMODE(os.stat(path).st_mode) def confirm_files(self): # expectations based on 022 umask set above and the unpack logic that # sets execute permissions, not preservation for fname, expected_mode, test, expected_contents in [ ('file.txt', 0o644, os.path.isfile, b'file\n'), # We don't test the "symlink.txt" contents for now. ('symlink.txt', 0o644, os.path.isfile, None), ('script_owner.sh', 0o755, os.path.isfile, b'file\n'), ('script_group.sh', 0o755, os.path.isfile, b'file\n'), ('script_world.sh', 0o755, os.path.isfile, b'file\n'), ('dir', 0o755, os.path.isdir, None), (os.path.join('dir', 'dirfile'), 0o644, os.path.isfile, b''), ]: path = os.path.join(self.tempdir, fname) if path.endswith('symlink.txt') and sys.platform == 'win32': # no symlinks created on windows continue assert test(path), path if expected_contents is not None: with open(path, mode='rb') as f: contents = f.read() assert contents == expected_contents, 'fname: {}'.format(fname) if sys.platform == 'win32': # the permissions tests below don't apply in windows # due to os.chmod being a noop continue mode = self.mode(path) assert mode == expected_mode, ( "mode: {}, expected mode: {}".format(mode, expected_mode) ) def make_zip_file(self, filename, file_list): """ Create a zip file for test case """ test_zip = os.path.join(self.tempdir, filename) with zipfile.ZipFile(test_zip, 'w') as myzip: for item in file_list: myzip.writestr(item, 'file content') return test_zip def make_tar_file(self, filename, file_list): """ Create a tar file for test case """ test_tar = os.path.join(self.tempdir, filename) with tarfile.open(test_tar, 'w') as mytar: for item in file_list: file_tarinfo = tarfile.TarInfo(item) mytar.addfile(file_tarinfo, 'file content') return test_tar def test_unpack_tgz(self, data): """ Test unpacking a *.tgz, and setting execute permissions """ test_file = data.packages.joinpath("test_tar.tgz") untar_file(test_file, self.tempdir) self.confirm_files() # Check the timestamp of an extracted file file_txt_path = os.path.join(self.tempdir, 'file.txt') mtime = time.gmtime(os.stat(file_txt_path).st_mtime) assert mtime[0:6] == (2013, 8, 16, 5, 13, 37), mtime def test_unpack_zip(self, data): """ Test unpacking a *.zip, and setting execute permissions """ test_file = data.packages.joinpath("test_zip.zip") unzip_file(test_file, self.tempdir) self.confirm_files() def test_unpack_zip_failure(self): """ Test unpacking a *.zip with file containing .. path and expect exception """ files = ['regular_file.txt', os.path.join('..', 'outside_file.txt')] test_zip = self.make_zip_file('test_zip.zip', files) with pytest.raises(InstallationError) as e: unzip_file(test_zip, self.tempdir) assert 'trying to install outside target directory' in str(e.value) def test_unpack_zip_success(self): """ Test unpacking a *.zip with regular files, no file will be installed outside target directory after unpack so no exception raised """ files = [ 'regular_file1.txt', os.path.join('dir', 'dir_file1.txt'), os.path.join('dir', '..', 'dir_file2.txt'), ] test_zip = self.make_zip_file('test_zip.zip', files) unzip_file(test_zip, self.tempdir) def test_unpack_tar_failure(self): """ Test unpacking a *.tar with file containing .. path and expect exception """ files = ['regular_file.txt', os.path.join('..', 'outside_file.txt')] test_tar = self.make_tar_file('test_tar.tar', files) with pytest.raises(InstallationError) as e: untar_file(test_tar, self.tempdir) assert 'trying to install outside target directory' in str(e.value) def test_unpack_tar_success(self): """ Test unpacking a *.tar with regular files, no file will be installed outside target directory after unpack so no exception raised """ files = [ 'regular_file1.txt', os.path.join('dir', 'dir_file1.txt'), os.path.join('dir', '..', 'dir_file2.txt'), ] test_tar = self.make_tar_file('test_tar.tar', files) untar_file(test_tar, self.tempdir) @pytest.mark.parametrize('args, expected', [ # Test the second containing the first. (('parent/sub', 'parent/'), False), # Test the first not ending in a trailing slash. (('parent', 'parent/foo'), True), # Test target containing `..` but still inside the parent. (('parent/', 'parent/foo/../bar'), True), # Test target within the parent (('parent/', 'parent/sub'), True), # Test target outside parent (('parent/', 'parent/../sub'), False), ]) def test_is_within_directory(args, expected): result = is_within_directory(*args) assert result == expected