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
