#!/usr/bin/env python3

import binascii
import inspect
import io
import os
import re
import shutil
import struct
import subprocess
import tempfile
import unittest
import zipfile

# Debug Flag:
# When set to `False` the `tearDown` methods of the TestCases will not delete
# the temporary test directories.
CleanUp = True


def custom_zip_info(local_encoding: str, central_encoding: str | None = None):
    """
    Wrapper around zipfile.ZipInfo that saves files with custom metadata
    encoding and no unicode flag.  Needed for mocking PKZip behavior.
    """

    if central_encoding is None:
        central_encoding = local_encoding

    class CustomEncodingZipInfo(zipfile.ZipInfo):
        def _encodeFilenameFlags(self):
            if inspect.stack()[1][3] == "_write_end_record":
                encoding = central_encoding
            else:
                encoding = local_encoding
            return self.filename.encode(encoding), self.flag_bits

    return CustomEncodingZipInfo


def diff(expected: str, actual: str) -> str:
    """
    Wrapper function for the shell command 'diff' to compare and show the\
    difference of two strings.

    Args:
        expected: The string value that was expected.
        actual: The string that was actually produced and that should be\
                compared against expected.

    Returns:
        str: A string representation in a diff format that shows the\
             difference between `expected` and `actual`.
    """

    bash_command = f'''/usr/bin/env bash -c "printf -- 'diff EXPECTED ACTUAL\\n---EXPECTED\\n+++ACTUAL\\n'; diff --minimal --unified <(cat << 'EOF'
{expected}
EOF
) <(cat << 'EOF'
{actual}
EOF
) | tail -n +3"'''

    try:
        output = subprocess.check_output(bash_command,
                                         shell=True, stderr=subprocess.STDOUT)
        return output.decode()
    except subprocess.CalledProcessError as error:
        # Error can safely be ignored, because diff returns with failure
        # exit code if expected and actual have no difference.
        return error.output.decode()


def mktemp(createDirectory: bool = False,
           basePath: str | None = None,
           template: str = "tmp.XXXXXXXXXX") -> str:
    """
    Create a temporary file or directory and returns its path.

    Remarks:
        Files are created u+rw, and directories u+rwx, minus umask\
        restrictions.

        See `$ man mktemp` for more details.

    Args:
        createDirectory: If True create a directory, otherwise a file.\
                         (default: `False`)
        basePath: Base path of the provided `template`; if `None`, use current\
                  working directory. (default: `None`)
        template: Template of the created temporary file/directory. `X`s will\
                  be replaced by a random alpha-numeric character.\
                  `template` must contain at least 3 consecutive 'X's in last\
                  component. (default: `"tmp.XXXXXXXXXX"`)

    Returns:
        str: Absolute path of the created  temporary  file  or directory.
    """

    command = "mktemp "

    if createDirectory:
        command += "--directory "

    if basePath is None:
        command += "--tmpdir=\"$PWD\" "
    else:
        command += f"--tmpdir='{str(basePath)}' "

    command += f"'{str(template)}'"

    return subprocess.check_output(command,
                                   shell=True, encoding="utf-8").strip()


def deleteRecursiveWithForce(path: str) -> None:
    """
    Deletes a directory or file for a given path recursively and forcefully.

    Args:
        path: Path to the file/directory to delete.

    Returns:
        `None`
    """

    subprocess.check_call(f"rm -rf '{path}'", shell=True)


def dd(inputFile: str,
       outputFile: str,
       blockSize: str | int,
       blockCount: str | int,
       cwd: str | None = None) -> None:
    """
    Convert and copy a file; Wrapper function of the shell command `dd`.

    Args:
        inputFile: The path of the file where `dd` should read the data from.
        outputFile: The path of the file where `dd` should write the data to.
        blockSize: The size of a block limits the amount of reads or writes\
                   at a time in byte.
        blockCount: Copy the specified amount of blocks to the output file.
        cwd: Path of the working directory where `dd` will be executed.\
             If `None` is provided the working directory of the test script\
             process will be used. (default: `None`)

    Returns:
        `None`
    """

    command = ("dd"
               f" if='{inputFile}'"
               f" of='{outputFile}'"
               f" bs='{str(blockSize)}'"
               f" count='{str(blockCount)}'"
               " status='none'")

    subprocess.check_call(command, cwd=cwd, shell=True)


def ddRandom(outputFile: str,
             outputFileSizeInMebibyte: int,
             cwd: str | None = None) -> None:
    """
    Writes random data to a file using the shell command `dd`.

    Args:
        outputFile: The path of the file where the random data should be\
                    written to.
        outputFileSizeInMebibyte: The amount of random data in MiB (Mebibyte)\
                                  that should be copied into the output file.
        cwd: Path of the working directory where `dd` will be executed.\
             If `None` is provided the working directory of the test script\
             process will be used. (default: `None`)

    Returns:
        `None`
    """

    dd(inputFile='/dev/random',
       outputFile=outputFile,
       blockSize=1024,
       blockCount=1024 * outputFileSizeInMebibyte,
       cwd=cwd)


def zip(zipArchive: str,
        fileToAdd: str,
        password: str | None = None,
        cwd: str | None = None) -> None:
    """
    Adds a file to a zip archive (and creates it if it does not exist).\
    Wrapper function of the shell command `zip`.

    Args:
        zipArchive: Path of the zip archive, the file should be added to.\
                    The file will be created if it does not exists.
        fileToAdd: Path of the file to add to the zip archive.
        password: Used to encrypt zipfile entries. If `None` is provided, the\
                  zip archive will not be encrypted. (default: `None`)
        cwd: Path of the working directory where `zip` will be executed.\
             If `None` is provided the working directory of the test script\
             process will be used. (default: `None`)

    Returns:
        `None`
    """

    command = "zip -q"
    if password is not None:
        command += f" --password '{password}'"
    command += f" '{zipArchive}' '{fileToAdd}'"

    subprocess.check_call(command, cwd=cwd, shell=True)


def prependUnZipSFX(zipArchive: str,
                    outputFile: str,
                    cwd: str | None = None) -> None:
    """
    Prepends UnZipSFX in fornt of a zip archive and configures it to be\
    ready to run.

    Args:
        zipArchive: Path of the zip archive, UnZipSFX should be prepended to.
        outputFile: Path of the resulting file (UnZipSFX + zip archive).
        cwd: Path of the working directory where the operation will be\
             executed. If `None` is provided the working directory of the\
             test script process will be used. (default: `None`)

    Returns:
        `None`
    """

    subprocess.check_call(f"cat $(which unzipsfx) '{zipArchive}'"
                          f" > '{outputFile}'", cwd=cwd, shell=True)
    subprocess.check_call(f"chmod u+x '{outputFile}'", cwd=cwd, shell=True)
    subprocess.check_call(f"zip -Aq '{outputFile}'", cwd=cwd, shell=True)


class ShellCommandResult(object):
    """
    Holds the result of a shell command and provides convenience functions\
    to run assertions on them.
    """

    def __init__(self,
                 command: str,
                 exitcode: int,
                 stdout: str,
                 stderr: str) -> None:
        self.command = command
        self.exitcode = exitcode
        self.stdout = stdout
        self.stderr = stderr

    def __CombinedOutput(self) -> str:
        result = ""

        if self.stdout != "":
            result += "\n\nSTDOUT:\n" + self.stdout

        if self.stderr != "":
            result += "\n\nSTDERR:\n" + self.stderr

        return result

    def __AssertOutputStartsWith(self,
                                 output: str,
                                 outputName: str,
                                 expectedStart: str) -> None:
        difference = diff(expectedStart, output[:len(expectedStart)])

        assert len(output) >= len(expectedStart), \
               (f"Length of {outputName} output is smaller than expected!"
                f"{self.__CombinedOutput()}\n\n{difference}")

        for i in range(len(expectedStart)):
            assert output[i] == expectedStart[i], \
                   (f"{outputName} output does not start with expected output!"
                    f"\n\nAt postion {i} character '{expectedStart[i]}' "
                    f"({hex(ord(expectedStart[i]))}) was expected, "
                    f"got '{output[i]}' "
                    f"({hex(ord(output[i]))}).\n\n{difference}")

    def AssertSuccessExitCode(self) -> None:
        """
        Fail the test unless the exit code of the shell command was `0`.
        """

        assert self.exitcode == 0, \
               ("Expected Success Exit Code (== 0), "
                f"got {self.exitcode}!{self.__CombinedOutput()}")

    def AssertFailureExitCode(self) -> None:
        """
        Fail the test unless the exit code of the shell command was NOT `0`.
        """

        assert self.exitcode != 0, \
               ("Expected Failure Exit Code (!= 0), "
                f"got {self.exitcode}!{self.__CombinedOutput()}")

    def AssertStdoutIsEmpty(self) -> None:
        """
        Fail the test unless the standard output of the shell command\
        was empty.
        """

        assert self.stdout == "", \
               ("STDOUT output is not empty! "
                f"Expected was an empty output.{self.__CombinedOutput()}")

    def AssertStdoutIsNotEmpty(self) -> None:
        """
        Fail the test unless the standard output of the shell command\
        was NOT empty.
        """

        assert self.stdout != "", \
               ("STDOUT output is empty! "
                f"Expected was a non-empty output.{self.__CombinedOutput()}")

    def AssertStdoutEquals(self, expectedOutput) -> None:
        """
        Fail the test unless the standard output of the shell command\
        equals an expected value.
        """

        assert self.stdout == expectedOutput, \
               ("Unexpected STDOUT output!"
                f"{self.__CombinedOutput()}\n\n"
                f"{diff(expectedOutput, self.stdout)}")

    def AssertStdoutStartsWith(self, expectedStart: str) -> None:
        """
        Fail the test unless the standard output of the shell command\
        starts with an expected value.
        """

        self.__AssertOutputStartsWith(self.stdout, "STDOUT", expectedStart)

    def AssertStderrIsEmpty(self) -> None:
        """
        Fail the test unless the standard error output of the shell command\
        was empty.
        """

        assert self.stderr == "", \
            ("STDERR output is not empty! "
             f"Expected was an empty output.{self.__CombinedOutput()}")

    def AssertStderrIsNotEmpty(self) -> None:
        """
        Fail the test unless the standard error output of the shell command\
        was NOT empty.
        """

        assert self.stderr != "", \
            ("STDERR output is empty! "
             f"Expected was a non-empty output.{self.__CombinedOutput()}")

    def AssertStderrEquals(self, expectedOutput) -> None:
        """
        Fail the test unless the standard error output of the shell command\
        equals an expected value.
        """

        assert self.stderr == expectedOutput, \
            ("Unexpected STDERR output!"
             f"{self.__CombinedOutput()}\n\n"
             f"{diff(expectedOutput, self.stderr)}")

    def AssertStderrStartsWith(self, expectedStart: str) -> None:
        """
        Fail the test unless the standard error output of the shell command\
        starts with an expected value.
        """

        self.__AssertOutputStartsWith(self.stderr, "STDERR", expectedStart)


def RunShellCommand(command: str,
                    cwd: str | None = None) -> ShellCommandResult:
    """
    Runs a shell command and wraps the result it in a `ShellCommandResult`\
    instance.

    Args:
        command: The `/bin/sh` command that should be executed.
        cwd: The path of the working directory where the shell command\
             should be executed. If `None` is provided the working directory\
             of the test script process will be used. (default: `None`)

    Returns:
        ShellCommandResult: Instance that holds the result of the executed\
                            shell command.
    """

    result = subprocess.run(
        args=command,
        shell=True,
        capture_output=True,
        text=True,
        encoding='utf-8',
        cwd=cwd,
        env={
            "LANG": "C",
            "LANGUAGE": "C",
            "LC_ADDRESS": "C",
            "LC_IDENTIFICATION": "C",
            "LC_MEASUREMENT": "C",
            "LC_MONETARY": "C",
            "LC_NAME": "C",
            "LC_NUMERIC": "C",
            "LC_PAPER": "C",
            "LC_TELEPHONE": "C",
            "LC_TIME": "C",
        })

    return ShellCommandResult(command,
                              result.returncode, result.stdout, result.stderr)


class ManPageTestCases(unittest.TestCase):
    def AssertManPageContainsContent(self, command: str) -> None:
        """
        Fails unless a manpage for the command `command` exists, is not-empty\
        and starts with the name of the command in upper case.
        """

        result = RunShellCommand("man -P cat " + command)
        result.AssertSuccessExitCode()
        result.AssertStderrIsEmpty()
        result.AssertStdoutIsNotEmpty()
        result.AssertStdoutStartsWith(command.upper())

    def test_When_RunningFUnZipManpage_Expect_NonEmptyManPage(self):
        """
        Executing the `man funzip` command should print existing (non-empty)\
        usage/help information to STDOUT.
        """

        self.AssertManPageContainsContent("funzip")

    def test_When_RunningUnZipManpage_Expect_NonEmptyManPage(self):
        """
        Executing the `man unzip` command should print existing (non-empty)\
        usage/help information to STDOUT.
        """

        self.AssertManPageContainsContent("unzip")

    def test_When_RunningUnZipSFXManpage_Expect_NonEmptyManPage(self):
        """
        Executing the `man funzipsfx` command should print existing\
        (non-empty) usage/help information to STDOUT.
        """

        self.AssertManPageContainsContent("unzipsfx")

    def test_When_RunningZipGrepManpage_Expect_NonEmptyManPage(self):
        """
        Executing the `man zipgrep` command should print existing (non-empty)\
        usage/help information to STDOUT.
        """

        self.AssertManPageContainsContent("zipgrep")

    def test_When_RunningZipInfoManpage_Expect_NonEmptyManPage(self):
        """
        Executing the `man zipinfo` command should print existing (non-empty)\
        usage/help information to STDOUT.
        """

        self.AssertManPageContainsContent("zipinfo")


class UsageInfoTestCases(unittest.TestCase):
    """
    The tests of this class test if the commands provided by the unzip\
    source packages show basic usage informations.
    """

    def test_When_RunningFUnZipCommandWithoutParameters_Expect_NonEmptyOutput(self) -> None:  # noqa: E501
        """
        Executing the `funzip` command without any additional parameters\
        should print short usage/help information to STDERR.
        """

        # Arrange
        # coyprightHeader = "fUnZip (filter UnZip), version 3.95 of 20 January 2009\n"  # noqa: E501

        # Act
        result = RunShellCommand("funzip")

        # Assert
        result.AssertFailureExitCode()
        result.AssertStdoutIsEmpty()
        result.AssertStderrIsNotEmpty()

        # If unzip command is not called in tty mode it will not print the
        # coypright header. When your run this script locally it would pass,
        # but the autopkgtest runner has no tty attached.
        # I could not figure out how to emulate this.
        # result.AssertStderrStartsWith(coyprightHeader)

    def test_When_RunningUnZipCommandWithoutParameters_Expect_NonEmptyOutput(self) -> None:  # noqa: E501
        """
        Executing the `unzip` command without any additional parameters\
        should print short usage/help information to STDOUT.
        """

        # Arrange
        coyprightHeader = "UnZip 6.00 of 20 April 2009, by Debian. Original by Info-ZIP.\n"  # noqa: E501

        # Act
        result = RunShellCommand("unzip")

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStderrIsEmpty()
        result.AssertStdoutIsNotEmpty()
        result.AssertStdoutStartsWith(coyprightHeader)

    def test_When_RunningUnZipCommandWithHH_Expect_DisplayExtendedHelp(self) -> None:  # noqa: E501
        """
        Executing the `unzip` command without any additional parameters should\
        print short usage/help information to STDOUT.
        """

        # Arrange
        header = ("\nExtended Help for UnZip\n\n"
                  "See the UnZip Manual for more detailed help\n\n\n")

        # Act
        result = RunShellCommand("unzip -hh")

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStderrIsEmpty()
        result.AssertStdoutIsNotEmpty()
        result.AssertStdoutStartsWith(header)

    def test_When_RunningUnZipSFXCommandWithoutParameters_Expect_NonEmptyOutput(self) -> None:  # noqa: E501
        """
        Executing the `unzipsfx` command without any additional parameters\
        should print short usage/help information to STDERR.
        """

        # Arrange
        coyprightHeader = "UnZipSFX 6.00 of 20 April 2009, by Info-ZIP (http://www.info-zip.org).\n"  # noqa: E501

        # Act
        result = RunShellCommand("unzipsfx")

        # Assert
        result.AssertFailureExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStderrIsNotEmpty()
        result.AssertStdoutStartsWith(coyprightHeader)

    def test_When_RunningZipGrepCommandWithoutParameters_Expect_NonEmptyOutput(self) -> None:  # noqa: E501
        """
        Executing the `zipgrep` command without any additional parameters\
        should print short usage/help information to STDOUT.
        """

        # Arrange
        expectedOutput = ("usage: zipgrep [egrep_options] pattern zipfile [members...]\n"   # noqa: E501
                          "Uses unzip and egrep to search the zip members for a string or pattern.\n")  # noqa: E501

        # Act
        result = RunShellCommand("zipgrep")

        # Assert
        result.AssertFailureExitCode()
        result.AssertStderrIsEmpty()
        result.AssertStdoutIsNotEmpty()
        result.AssertStdoutEquals(expectedOutput)

    def test_When_RunningZipInfoCommandWithoutParameters_Expect_NonEmptyOutput(self) -> None:  # noqa: E501
        """
        Executing the `zipinfo` command without any additional parameters\
        should print short usage/help information to STDOUT.
        """

        # Arrange
        coyprightHeader = "ZipInfo 3.00 of 20 April 2009, by Greg Roelofs and the Info-ZIP group.\n"  # noqa: E501

        # Act
        result = RunShellCommand("zipinfo")

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStderrIsEmpty()
        result.AssertStdoutIsNotEmpty()
        result.AssertStdoutStartsWith(coyprightHeader)


class SingleFileZipTestCases(unittest.TestCase):
    """
    The tests of this class test if the commands provided by the unzip\
    source packages can handle a zip archive with a simple single file.
    """

    ExampleFileName = "example.txt"
    ExampleFileContent = "Hello Zip!"
    SingleFileZipName = "single-file.zip"

    @property
    def ExampleFilePath(self) -> str:
        return self.WorkingDirectory + "/" + self.ExampleFileName

    def CreateTemporaryWorkingDirectory(self) -> None:
        self.WorkingDirectory = mktemp(createDirectory=True)

    def CreateExampleFile(self):
        with open(self.ExampleFilePath, "w") as file:
            file.write(self.ExampleFileContent)

    def DeleteExampleFile(self):
        os.remove(self.ExampleFilePath)

    def CreateSingleFileZipArchive(self):
        self.CreateExampleFile()

        zip(zipArchive=self.SingleFileZipName,
            fileToAdd=self.ExampleFileName,
            cwd=self.WorkingDirectory)

    def setUp(self) -> None:
        self.CreateTemporaryWorkingDirectory()
        self.CreateSingleFileZipArchive()

    def tearDown(self) -> None:
        if CleanUp:
            deleteRecursiveWithForce(self.WorkingDirectory)

    def AssertExampleFileExists(self) -> None:
        exampleFilePath = self.ExampleFilePath

        self.assertTrue(os.path.exists(exampleFilePath),
                        msg="Example file does not exist!")

        with open(exampleFilePath, 'r') as file:
            fileContent = file.read()
            self.assertEqual(fileContent, self.ExampleFileContent,
                             msg="Example file has unexpected content!")

    def test_When_FUnZipIsCalledWithAZipArchiveThatContainsOneFile_Expect_PrintsContentOfSingleFile(self) -> None:  # noqa: E501
        """
        When the `funzip` is called with a zip archive that contains one file.\
        `funzip` should print the content of the file to STDOUT.
        """

        # Arrange
        pass

        # Act
        result = RunShellCommand(f"funzip ./{self.SingleFileZipName}",
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStderrIsEmpty()
        result.AssertStdoutEquals(self.ExampleFileContent)

    def test_When_UnZipIsCalledWithAZipArchiveThatContainsOneFile_Expect_ExtractsSingleFile(self) -> None:  # noqa: E501
        """
        When the `unzip` is called with a zip archive that contains one file.\
        `unzip` should extract the single file.
        """

        # Arrange
        self.DeleteExampleFile()

        # Act
        result = RunShellCommand(f"unzip ./{self.SingleFileZipName}",
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        self.AssertExampleFileExists()

    def test_When_UnZipSFXIsPrependedToAZipArchiveThatContainsOneFileAndCalled_Expect_ExtractsSingleFile(self) -> None:  # noqa: E501
        """
        When the `unzipsfx` is prepended in front of a zip archive that\
        contains one file, it should extract the single file.
        """

        # Arrange
        self.DeleteExampleFile()
        prependUnZipSFX(zipArchive=self.SingleFileZipName,
                        outputFile="single-file-zip",
                        cwd=self.WorkingDirectory)
        # Act
        result = RunShellCommand("./single-file-zip",
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        self.AssertExampleFileExists()

    def test_When_ZipInfoIsCalledWithAZipArchiveThatContainsOneFileAndNoAdditionalParameters_Expect_PrintUnixStyleFileListThatContainsSingleFile(self) -> None:  # noqa: E501
        """
        When the shell command `zipinfo` is called with a zip archive that\
        contains only one file and no additional parameters. It is expected\
        that `unzip` will print a unix style file list\
        (like `ls -l` containing only the single file.)
        """

        # Arrange
        zipFileNameAsRegex = re.escape(self.SingleFileZipName)
        exampleFileNameAsRegex = re.escape(self.ExampleFileName)
        pattern = re.compile(rf"^Archive:  ./{zipFileNameAsRegex}\nZip file size: \d+ bytes, number of entries: 1\n-rw-rw-r--.*{exampleFileNameAsRegex}\n1 file, \d+ bytes uncompressed, \d+ bytes compressed: .*%$")  # noqa: E501

        # Act
        result = RunShellCommand(f"zipinfo ./{self.SingleFileZipName}",
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStderrIsEmpty()
        self.assertRegex(result.stdout, pattern,
                         msg="ZipInfo did not show the expected output.")

    def test_When_ZipInfoIsCalledWithAZipArchiveThatContainsOneFileAndDash1_Expect_PrintSingleFileName(self) -> None:  # noqa: E501
        """
        When the shell command `zipinfo` is called with a zip archive that\
        contains only one file and the parameter `-1`. It is expected that\
        `unzip` will print the name of the single file.)
        """

        # Arrange
        pass

        # Act
        result = RunShellCommand(f"zipinfo -1 ./{self.SingleFileZipName}",
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStderrIsEmpty()
        result.AssertStdoutEquals(f"{self.ExampleFileName}\n")

    def test_When_ZipGrepIsCalledWithAZipArchiveThatContainsOneFileAndAPatternThatMatchesTheContentOfTheSingleFile_Expect_ReturnSingleFile(self) -> None:  # noqa: E501
        """
        When the shell command `zipgrep` is called with a zip archive that\
        contains only one file and an egrep pattern that matches the single\
        file content, it should return a reference to the single file.
        """

        # Arrange
        pass

        # Act
        result = RunShellCommand(f"zipgrep '{self.ExampleFileContent}' "
                                 f"./{self.SingleFileZipName} "
                                 f"{self.ExampleFileName}",
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStderrIsEmpty()
        result.AssertStdoutEquals(f"{self.ExampleFileName}:"
                                  f"{self.ExampleFileContent}\n")


class MultiFileZipTestCases(unittest.TestCase):
    """
    The tests of this class test if the commands provided by the unzip\
    source packages can handle a zip archive with a multiple files.
    """

    FileAmount = 10
    MultiFileZipFileName = "multi-file.zip"

    def CreateTemporaryWorkingDirectory(self) -> None:
        self.WorkingDirectory = mktemp(createDirectory=True)

    def TestFilePath(self, suffix, absolutePath: bool = False) -> str:
        path = "test-file_" + str(suffix) + ".txt"

        if absolutePath:
            path = self.WorkingDirectory + "/" + path

        return path

    def CreateMultiFileZipArchive(self) -> None:
        for i in range(self.FileAmount):
            testFilePath = self.TestFilePath(i, absolutePath=False)

            ddRandom(outputFile=testFilePath,
                     outputFileSizeInMebibyte=1,
                     cwd=self.WorkingDirectory)

            zip(zipArchive=self.MultiFileZipFileName,
                fileToAdd=testFilePath,
                cwd=self.WorkingDirectory)

            shutil.move(src=f"{self.WorkingDirectory}/{testFilePath}",
                        dst=f"{self.WorkingDirectory}/{testFilePath}.backup")

    def AssertExtractedFileEqualsOriginal(self, i) -> None:
        testFileName = self.TestFilePath(i, absolutePath=False)
        testFilePath = self.TestFilePath(i, absolutePath=True)
        originalTestFilePath = testFilePath + ".backup"

        self.assertTrue(os.path.exists(testFilePath),
                        msg=f"'{testFileName}' was not extracted.")

        self.assertEqual(
            os.path.getsize(testFilePath),
            os.path.getsize(originalTestFilePath),
            msg="Extracted File {testFileName} does not match "
                "the original file size.")

        # compare file content:
        with open(testFilePath, 'rb') as originalFile, \
             open(originalTestFilePath, 'rb') as extractedFile:
            while True:
                block1 = originalFile.read(1024)
                block2 = extractedFile.read(1024)

                self.assertTrue(block1 == block2,
                                msg=f"Extracted File {testFileName} does not "
                                    f"match the original file content.")

                # Reached the end of both files without finding any differences
                if not block1:
                    break

    def AssertAllFilesExtractedFromMultiFileZipArchive(self):
        for i in range(self.FileAmount):
            self.AssertExtractedFileEqualsOriginal(i)

    def setUp(self) -> None:
        self.CreateTemporaryWorkingDirectory()
        self.CreateMultiFileZipArchive()

    def tearDown(self) -> None:
        if CleanUp:
            deleteRecursiveWithForce(self.WorkingDirectory)

    def test_When_FUnZipIsCalledWithMultiFileArchive_Expect_PrintContentOfFirstFile(self) -> None:  # noqa: E501
        """
        When the shell command `funzip` is called with a multi-file\
        zip archive, it is expected that it prints the content of the\
        first file contained in the multi-file archive to STDOUT.
        """

        # Arrange
        testFilePath = self.TestFilePath(0, absolutePath=False)

        # Act
        result = RunShellCommand(f"funzip '{self.MultiFileZipFileName}' > "
                                 f"'{testFilePath}'",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertSuccessExitCode()
        self.AssertExtractedFileEqualsOriginal(0)

    def test_When_UnZipIsCalledWithMultiFileArchive_Expect_ExtractsFilesContainedInMultifileArchive(self) -> None:  # noqa: E501
        """
        When the shell command `unzip` is called with a multi-file\
        zip archive, it is expected that it extracts the files contained\
        in the multi-file archive.
        """

        # Arrange
        pass

        # Act
        result = RunShellCommand(f"unzip '{self.MultiFileZipFileName}'",
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        self.AssertAllFilesExtractedFromMultiFileZipArchive()

    def test_When_UnZipSFXIsPrependedToMultiFileArchive_Expect_(self) -> None:
        """
        When the `unzipsfx` is prepended in front of a multi-file\
        zip archive, it is expected that it extracts the files contained\
        in the multi-file archive when it is called.
        """

        # Arrange
        prependUnZipSFX(zipArchive=self.MultiFileZipFileName,
                        outputFile="multi-file-zip",
                        cwd=self.WorkingDirectory)

        # Act
        result = RunShellCommand("./multi-file-zip", cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        self.AssertAllFilesExtractedFromMultiFileZipArchive()

    def test_When_ZipGrepIsCalledWithMultiFileArchiveAndPattern_Expect_FindsFilesContainedInTheMultiFileArchive(self) -> None:  # noqa: E501
        """
        When the shell command `zipgrep` is called with a multi-file\
        zip archive and a pattern, it is expected that it find the files\
        with the pattern contained in the multi-file archive and prints\
        it as a list to STDOUT.
        """

        # Arrange
        fileNameAsRegex = re.escape(self.TestFilePath("\\d+", absolutePath=False))  # noqa: E501

        pattern = re.compile(f"^({fileNameAsRegex}:X\n)*$")

        # Act
        result = RunShellCommand(f"zipgrep -o 'X' '{self.MultiFileZipFileName}'",  # noqa: E501
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertSuccessExitCode()
        self.assertRegex(result.stdout, pattern)

    def test_When_UsingZipInfoWithoutAdditionalParametersForMultiFileZipArchive_Expect_UnixLsDashLListOfContainedFiles(self) -> None:  # noqa: E501
        """
        When the shell command `zipinfo` is called with a multi-file\
        zip archive and no additional parameters it is expected that it\
        will list the contained files as a unix style file list\
        (like `ls -l`) to STDOUT.
        """

        # Arrange
        regex = rf"^Archive:  multi-file.zip\nZip file size: \d+ bytes, number of entries: {str(self.FileAmount)}\n"  # noqa: E501
        for i in range(self.FileAmount):
            filenameAsRegex = re.escape(self.TestFilePath(i))
            regex += rf"-rw-rw-r--.*{filenameAsRegex}\n"
        regex += rf"{str(self.FileAmount)} files, \d+ bytes uncompressed, \d+ bytes compressed: .*%$"  # noqa: E501

        pattern = re.compile(regex)

        # Act
        result = RunShellCommand("zipinfo " + self.MultiFileZipFileName,
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStderrIsEmpty()
        result.AssertStdoutIsNotEmpty()

        self.assertRegex(result.stdout, pattern,
                         "STDOUT of unzip for a multi-file zip archive "
                         "does not match the expectations!")

    def test_When_UsingZipInfoDash1ForMultiFileZipArchive_Expect_ListOfContainedFiles(self) -> None:  # noqa: E501
        """
        When the shell command `zipinfo` is called with a multi-file\
        zip archive and the additional parameter `-1` it is expected that\
        it will list the contained files to STDOUT split by a `\\n` char.
        """

        # Arrange
        expectedOutput = ""
        for i in range(self.FileAmount):
            expectedOutput += f"{self.TestFilePath(i)}\n"

        # Act
        result = RunShellCommand("zipinfo -1 " + self.MultiFileZipFileName,
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStderrIsEmpty()
        result.AssertStdoutIsNotEmpty()
        result.AssertStdoutEquals(expectedOutput)

    def test_When_UsingZipInfoDash2htForMultiFileZipArchive_Expect_ListOfContainedFilesWithMetadata(self) -> None:  # noqa: E501
        """
        When the shell command `zipinfo` is called with a multi-file\
        zip archive and the additional parameter `-2ht` it is expected\
        that it will list the contained files to STDOUT split by a `\\n`\
        char with metadata about the zip archive.
        """

        # Arrange
        regex = rf"^Archive:  multi-file.zip\nZip file size: \d+ bytes, number of entries: {str(self.FileAmount)}\n"  # noqa: E501
        for i in range(self.FileAmount):
            filenameAsRegex = re.escape(self.TestFilePath(i))
            regex += filenameAsRegex + "\\n"
        regex += rf"{str(self.FileAmount)} files, \d+ bytes uncompressed, \d+ bytes compressed: .*%$"  # noqa: E501

        pattern = re.compile(regex)

        # Act
        result = RunShellCommand("zipinfo -2ht " + self.MultiFileZipFileName,
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStderrIsEmpty()
        result.AssertStdoutIsNotEmpty()

        self.assertRegex(result.stdout, pattern,
                         "STDOUT of unzip for a multi-file zip archive"
                         "does not match the expectations!")


class LargeZipArchiveTestCases(unittest.TestCase):
    """
    The tests of this class test if the commands provided by the\
    unzip source packages can handle a zip archive with a large file.
    """

    NoLargeFile = False  # Debug Flag

    LargeFileName = "large-file.bin"
    LargeFileSizeInMebibyte = 500  # 1 MiB = 1024 * 1024 byte
    LargeZipArchiveFileName = "large.zip"

    def CreateTemporaryWorkingDirectory(self) -> None:
        self.WorkingDirectory = mktemp(createDirectory=True)

    def CreateLargeZipArchive(self) -> None:
        largeFilePath = self.WorkingDirectory + "/" + self.LargeFileName

        print("creating large file (this may take a few seconds)")
        ddRandom(outputFile=self.LargeFileName,
                 outputFileSizeInMebibyte=self.LargeFileSizeInMebibyte,
                 cwd=self.WorkingDirectory)

        zip(zipArchive=self.LargeZipArchiveFileName,
            fileToAdd=self.LargeFileName,
            cwd=self.WorkingDirectory)

        shutil.move(src=largeFilePath,
                    dst=largeFilePath + ".backup")

    def setUp(self) -> None:
        if self.NoLargeFile:
            self.LargeFileSizeInMebibyte = 1

        self.CreateTemporaryWorkingDirectory()
        self.CreateLargeZipArchive()

    def tearDown(self) -> None:
        if CleanUp:
            deleteRecursiveWithForce(self.WorkingDirectory)

    def AssertBackedUpLargeFileEqualsExtracted(self):
        largeFilePath = f"{self.WorkingDirectory}/{self.LargeFileName}"
        backupFilePath = largeFilePath + ".backup"

        self.assertTrue(os.path.exists(largeFilePath),
                        msg="Large file was not extracted from "
                            "large file zip.")

        originalFileSize = os.path.getsize(backupFilePath)
        extractedFileSize = os.path.getsize(largeFilePath)

        self.assertEqual(extractedFileSize, originalFileSize,
                         msg="Extracted File does not have the same size "
                             "as the original file.")

        # compare file content:
        with open(backupFilePath, 'rb') as originalFile, \
             open(largeFilePath, 'rb') as extractedFile:
            while True:
                block1 = originalFile.read(1024)
                block2 = extractedFile.read(1024)

                self.assertTrue(block1 == block2,
                                msg="Extracted File does not match "
                                    "the original file content.")

                # Reached the end of both files without finding any differences
                if not block1:
                    break

    def test_When_FUnZipIsCalledWithLargeZipArchive_Expect_PrintContentOfFirstFile(self) -> None:  # noqa: E501
        """
        When the shell command `funzip` is called with a large zip archive,\
        it is expected that it will print the content of the first contained\
        file to STDOUT.
        """

        # Arrange
        pass

        # Act
        result = RunShellCommand(f"funzip '{self.LargeZipArchiveFileName}' > "
                                 f"'{self.LargeFileName}'",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertSuccessExitCode()
        self.AssertBackedUpLargeFileEqualsExtracted()

    def test_When_UnZipIsCalledWithLargeZipArchive_Expect_ExtractsContainedFiles(self) -> None:  # noqa: E501
        """
        When the shell command `unzip` is called with a large zip archive,\
        it is expected that it will extract the contained files.
        """

        # Arrange
        pass

        # Act
        result = RunShellCommand(f"unzip '{self.LargeZipArchiveFileName}'",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertSuccessExitCode()
        self.AssertBackedUpLargeFileEqualsExtracted()

    def test_When_UnZipSFXIsPrependedToLargeArchiveAndCalled_Expect_ExtractsContainedFiles(self) -> None:  # noqa: E501
        """
        When `unzipsfx` is prepended in front of a large zip archive and\
        called, it is expected that it will extract the contained files.
        """

        # Arrange
        prependUnZipSFX(self.LargeZipArchiveFileName,
                        outputFile="large-zip",
                        cwd=self.WorkingDirectory)
        # Act
        result = RunShellCommand("./large-zip",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertSuccessExitCode()
        self.AssertBackedUpLargeFileEqualsExtracted()

    def test_When_ZipGrepIsCalledWithLargeArchiveAndPattern_Expect_FindsContainedFilesWithPattern(self) -> None:  # noqa: E501
        """
        When the shell command `zipgrep` is called with a large zip archive\
        and an egrep pattern it is expected that it will find and list\
        refrences to the found matches.
        """

        # Arrange
        fileNameAsRegex = self.LargeFileName.replace('.', '\\.')
        pattern = re.compile(f"^({fileNameAsRegex}:X\n)*$")

        # Act
        result = RunShellCommand(f"zipgrep -o 'X' "
                                 f"'{self.LargeZipArchiveFileName}'",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertSuccessExitCode()
        self.assertRegex(result.stdout, pattern)

    def test_When_ZipInfoIsCalledWithLargeArchive_Expect_ListsContainedFiles(self) -> None:  # noqa: E501
        """
        If the shell command `zipinfo` is called with a large zip archive\
        it is expected that it will list the name of the contained files.
        """

        # Arrange
        expectedOutput = f"{self.LargeFileName}\n"

        # Act
        result = RunShellCommand("zipinfo -1 " + self.LargeZipArchiveFileName,
                                 cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStderrIsEmpty()
        result.AssertStdoutIsNotEmpty()
        result.AssertStdoutEquals(expectedOutput)


class PasswordProtectedZipTestCases(unittest.TestCase):
    """
    The tests of this class test if the commands provided by the unzip\
    source packages can handle a simple password protected zip archive.
    """

    SecretFileName = "secret.txt"
    SecretFileContent = ("Top Secret: Project Penguin Power - "
                         "Objective: Unleash Ubuntu-powered penguins - "
                         "Phase 1: Tux Training - "
                         "Phase 2: Fishy Integration - "
                         "Phase 3: Linux March - "
                         "Caution: Expect adorable waddling and occasional "
                         "Linux commands quacking.")
    SecretFileContentFragment = "Top Secret"
    PasswordProtectedZipFileName = "password-protected.zip"
    Password = "WaddleFishLinux123"

    @property
    def SecretFilePath(self) -> str:
        return f"{self.WorkingDirectory}/{self.SecretFileName}"

    def CreateTemporaryWorkingDirectory(self) -> None:
        self.WorkingDirectory = mktemp(createDirectory=True)

    def CreatePasswordProtectedZipArchive(self) -> None:
        secretFilePath = self.SecretFilePath

        with open(secretFilePath, 'w') as secretFile:
            secretFile.write(self.SecretFileContent)

        zip(zipArchive=self.PasswordProtectedZipFileName,
            fileToAdd=self.SecretFileName,
            password=self.Password,
            cwd=self.WorkingDirectory)

        shutil.move(src=secretFilePath,
                    dst=secretFilePath + ".backup")

    def AssertSecretFileExists(self) -> None:
        secretFilePath = self.SecretFilePath

        self.assertTrue(os.path.exists(secretFilePath),
                        msg="Secret file does not exist!")

        with open(secretFilePath, 'r') as file:
            fileContent = file.read()
            self.assertEqual(fileContent, self.SecretFileContent,
                             msg="Secret file has unexpected content!")

    def setUp(self) -> None:
        self.CreateTemporaryWorkingDirectory()
        self.CreatePasswordProtectedZipArchive()

    def tearDown(self) -> None:
        if CleanUp:
            deleteRecursiveWithForce(self.WorkingDirectory)

    def test_When_FUnZipIsCalledWithEncrypedZipArchiveAndPassword_Expect_DecryptZipArchiveAndPrintContentOfFirstFile(self) -> None:  # noqa: E501
        """
        When the shell command `funzip` is called with an encryped\
        zip archive and the corresponding password, it is expected that\
        it will decrypt the archive and print the content of the first\
        contained file to stdout.
        """

        # Arange
        pass

        # Act
        result = RunShellCommand(f"funzip '-{self.Password}' "
                                 f"'{self.PasswordProtectedZipFileName}'",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertSuccessExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStderrIsEmpty()
        result.AssertStdoutEquals(self.SecretFileContent)

    def test_When_UnZipIsCalledWithEncrypedZipArchiveAndPassword_Expect_DecryptZipArchiveAndExtractFiles(self) -> None:  # noqa: E501
        """
        When the shell command `unzip` is called with an encryped zip archive\
        and the corresponding password, it is expected that it will decrypt\
        the archive extract it's files.
        """

        # Arange
        pattern = re.compile(rf"^Archive:  {self.PasswordProtectedZipFileName}\n\s+inflating: {self.SecretFileName}\s+$")  # noqa: E501

        # Act
        result = RunShellCommand(f"unzip -P '{self.Password}' "
                                 f"'{self.PasswordProtectedZipFileName}'",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertSuccessExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStderrIsEmpty()
        self.assertRegex(result.stdout, pattern,
                         msg="STDOUT does not match expected output.")
        self.AssertSecretFileExists()

    def test_When_UnZipSFXIsPrependedToEncryptedZipArchive_Expect_FailureDueToUnsupportedUseCase(self) -> None:  # noqa: E501
        """
        When the `unzipsfx` is prepended in fron of an encryped zip archive\
        and called, it is expected that it will fail, because decryption is\
        not supported.
        """

        # Arrange
        copyrightHeader = "UnZipSFX 6.00 of 20 April 2009, by Info-ZIP (http://www.info-zip.org).\n"  # noqa: E501
        pattern = re.compile(f"^\\s+skipping: {self.SecretFileName}\\s+encrypted \\(not supported\\)\\s+$")  # noqa: E501
        prependUnZipSFX(zipArchive=self.PasswordProtectedZipFileName,
                        outputFile="password-protected-zip",
                        cwd=self.WorkingDirectory)
        # Act
        result = RunShellCommand("./password-protected-zip",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertFailureExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStdoutEquals(copyrightHeader)
        result.AssertStderrIsNotEmpty()
        self.assertRegex(result.stderr, pattern,
                         msg="STDERR does not match expected output.")
        self.assertFalse(os.path.exists(self.SecretFilePath),
                         msg="Output says, that encrypted files are not "
                             "supported, but encrypted file was extracted.")

    # I can't get it to work with the autopkgtest ubuntu cloud vm image :/
    # Probably the same tty problem as
    # test_When_RunningFUnZipCommandWithoutParameters_Expect_NonEmptyOutput.
    @unittest.skip(reason="flaky test")
    def test_When_ZipGrepIsCalledWithEncryptedZipArchiveAndPattern_Expect_PromtsForPasswordAndFindsMatchesOfContainedFiles(self) -> None:  # noqa: E501
        """
        When the shell command `zipgrep` is called with an encryped\
        zip archive and an egrep pattern, it is expected that it will ask\
        for the password, when given decrypt the archive and print matches\
        of the patttern of the contained files to stdout.
        """

        # Arrange
        expectScript = f"""
log_user 0
spawn zipgrep \"{self.SecretFileContentFragment}\" \"{self.PasswordProtectedZipFileName}\"
expect "password:" {{
    log_user 1
    send "{self.Password}\\r"
}}
expect eof
        """

        with open(self.WorkingDirectory + "/expct", "w") as expectFile:
            expectFile.write(expectScript)

        # Act
        result = RunShellCommand("expect expct", cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStderrIsEmpty()
        result.AssertStdoutEquals(f"\n{self.SecretFileName}:{self.SecretFileContent}\n")  # noqa: E501

    def test_When_ZipInfoIsCalledWithEncryptedZipArchiveAndWithoutPassword_Expect_ListContainedFiles(self) -> None:  # noqa: E501
        """
        When the shell command `zipinfo` is called with a encrypted\
        zip archive, it es expected that it can list the contained files\
        even without the password.
        """

        # Arange
        pass

        # Act
        result = RunShellCommand(
            f"zipinfo -1 '{self.PasswordProtectedZipFileName}'",
            cwd=self.WorkingDirectory)

        # Assert
        result.AssertSuccessExitCode()
        result.AssertStdoutIsNotEmpty()
        result.AssertStderrIsEmpty()
        result.AssertStdoutEquals(f"{self.SecretFileName}\n")


class InvalidZipTestCases(unittest.TestCase):
    """
    The tests of this class test if the commands provided by the unzip\
    source packages fail, when passed an incalid/corruped zip archive.
    """

    InvalidZipFileName = "invalid.zip"

    def CreateTemporaryWorkingDirectory(self) -> None:
        self.WorkingDirectory = mktemp(createDirectory=True)

    def CreateInvalidZipFile(self) -> None:
        ddRandom(outputFile=self.InvalidZipFileName,
                 outputFileSizeInMebibyte=1,
                 cwd=self.WorkingDirectory)

    def setUp(self) -> None:
        self.CreateTemporaryWorkingDirectory()
        self.CreateInvalidZipFile()

    def tearDown(self) -> None:
        if CleanUp:
            deleteRecursiveWithForce(self.WorkingDirectory)

    def test_When_FUnZipIsCalledWithInvalidZipArchive_Expect_Fail(self) -> None:  # noqa: E501
        """
        When the shell command `funzip` is called with an invalid\
        zip archive, expect it to fail.
        """

        # Arange
        pass

        # Act
        result = RunShellCommand(f"funzip '{self.InvalidZipFileName}'",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertFailureExitCode()
        result.AssertStderrIsNotEmpty()

    def test_When_UnZipIsCalledWithInvalidZipArchive_Expect_Fail(self) -> None:  # noqa: E501
        """
        When the shell command `unzip` is called with an invalid\
        zip archive, expect it to fail.
        """

        # Arange
        pass

        # Act
        result = RunShellCommand(f"unzip '{self.InvalidZipFileName}'",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertFailureExitCode()
        result.AssertStderrIsNotEmpty()

    def test_When_ZipGrepIsCalledWithInvalidZipArchive_Expect_Fail(self) -> None:  # noqa: E501
        """
        When the shell command `zipgrep` is called with an invalid\
        zip archive, expect it to fail.
        """

        # Arange
        pass

        # Act
        result = RunShellCommand(
            f"zipgrep Pattern '{self.InvalidZipFileName}'",
            cwd=self.WorkingDirectory)

        # Assert
        result.AssertFailureExitCode()
        result.AssertStderrIsNotEmpty()

    def test_When_ZipInfoIsCalledWithInvalidZipArchive_Expect_Fail(self) -> None:  # noqa: E501
        """
        When the shell command `zipinfo` is called with an invalid\
        zip archive, expect it to fail.
        """

        # Arange
        pass

        # Act
        result = RunShellCommand(f"zipinfo '{self.InvalidZipFileName}'",
                                 cwd=self.WorkingDirectory)
        # Assert
        result.AssertFailureExitCode()
        result.AssertStdoutIsNotEmpty()


class UnicodeNamesTestCases(unittest.TestCase):
    """
    Tests for file names containing non-ASCII (e.g. cyrillic) symbols,
    mimicking output of various real-life ZIP archivers.
    """

    filename = "абвгде"

    def _do_test(self, zipinfo, encoding=None):
        with tempfile.TemporaryFile(suffix=".zip") as fp:
            with zipfile.ZipFile(fp, "w") as zip_file:
                zip_file.writestr(zipinfo, b"")
            encoding_args = ["-I", encoding] if encoding else []
            args = ["zipinfo", "-1", *encoding_args, "/dev/stdin"]
            output = subprocess.check_output(args, stdin=fp)

        self.assertEqual(output, f"{self.filename}\n".encode("utf-8"))

    def test_unicode_field(self):
        """
        When Info-ZIP Unicode Path Extra Field (0x7075) is present, it should
        be used instead of the normal filename header.
        """

        zipinfo = zipfile.ZipInfo(filename="ignore")
        zipinfo.create_system = 0

        filename_encoded = struct.pack("<BL", 1, binascii.crc32(b"ignore"))
        filename_encoded += self.filename.encode("utf-8")
        zipinfo.extra = struct.pack("<HH", 0x7075, len(filename_encoded))
        zipinfo.extra += filename_encoded

        self._do_test(zipinfo)

    def test_bit11(self):
        """
        When general purpose bit 11 is set (Python does this by default),
        the filename field is treated as UTF-8.
        """

        zipinfo = zipfile.ZipInfo(filename=self.filename)
        zipinfo.create_system = 0

        self._do_test(zipinfo)

    def test_dos_encoding(self):
        """
        When create_system == 0 (MS-DOS), the filename field is treated as OEM.

        We cannot set locale in the test (e.g. to ru_RU.UTF-8) because it is
        not necessarily registered, so let's at least test passing the locale
        manually.
        """

        zipinfo = custom_zip_info("cp866")(filename=self.filename)
        zipinfo.create_system = 0
        zipinfo.create_version = 20

        self._do_test(zipinfo, encoding="CP866")

    def test_windows_encoding(self):
        """
        When create_system == 11 (Windows) and create_version >= 20, the
        filename field is treated as ANSI.

        We cannot set locale in the test (e.g. to ru_RU.UTF-8) because it is
        not necessarily registered, so let's at least test passing the locale
        manually.
        """

        zipinfo = custom_zip_info("cp1251")(filename=self.filename)
        zipinfo.create_system = 11
        zipinfo.create_version = 30

        self._do_test(zipinfo, encoding="CP1251")

    def test_unix_encoding(self):
        """
        When create_system == 3 (UNIX), the filename field is treated as UTF-8.
        """

        zipinfo = custom_zip_info("utf-8")(filename=self.filename)
        zipinfo.create_system = 3

        self._do_test(zipinfo)

    def test_pkzip4(self):
        """
        If central header and local header have different encodings, e.g.
        central OEM and local ANSI, the encoding from central header is used.

        Such files were generated by PKZip for Windows 2.5, 2.6, and 4.0.
        """

        zipinfo = custom_zip_info("cp1251", "cp866")(filename=self.filename)
        zipinfo.create_system = 0
        zipinfo.create_version = 40

        # Make sure both encodings are really present in the generated file.
        buffer = io.BytesIO()
        with zipfile.ZipFile(buffer, "w") as zip_file:
            zip_file.writestr(zipinfo, b"")
        self.assertIn(self.filename.encode("cp1251"), buffer.getvalue())
        self.assertIn(self.filename.encode("cp866"), buffer.getvalue())

        self._do_test(zipinfo, encoding="CP866")

    def test_pkzip5(self):
        """
        PKZip version 5 and newer started using OEM encoding again. Let's test
        this configuration too.
        """

        zipinfo = custom_zip_info("cp866")(filename=self.filename)
        zipinfo.create_system = 0
        zipinfo.create_version = 50

        self._do_test(zipinfo, encoding="CP866")


# if script is not imported as library, run all tests
if __name__ == "__main__":
    unittest.main()
