from filetypes.base import *
from filetypes.IFPS import PascalScriptAnalyzer
import malcat
import struct
import re


# Official source: https://github.com/jrsoftware/issrc/
# Inspired by the excellent innoextract: https://github.com/dscharrer/innoextract/ Copyright (C) 2011-2020 Daniel Scharrer <daniel@constexpr.org>
# MIT license: https://github.com/dscharrer/innoextract/blob/master/LICENSE


MAX_FLAGS_LENGTH = 8

class InnoHeader(Struct):

    def parse(self):
        yield String(64, zero_terminated=True, name="Identifier")


class StreamHeader(Struct):

    def parse(self):
        yield UInt32(name="HeaderCrc")
        yield UInt32(name="CompressedSize")
        yield UInt8(name="Compressed")        


class StreamHeader64(Struct):

    def parse(self):
        yield UInt32(name="HeaderCrc")
        yield UInt64(name="CompressedSize")
        yield UInt8(name="Compressed")        


class SetupEncryptionNonce(Struct):

    def parse(self):
        yield UInt64(name="RandomXorStartOffset")
        yield UInt32(name="RandomXorFirstSlice")
        yield Array(3, UInt32(), name="RemainingRandom")

class SetupEncryptionHeader(Struct):

    def parse(self):
        yield UInt32(name="EncryptionHeaderCrc")
        yield UInt8(name="EncryptionUsed", values=[
            ("None", 0),
            ("Files", 1),
            ("Full", 2)
            ])
        yield Bytes(16, name="Salt")
        yield UInt32(name="Iterations")
        yield SetupEncryptionNonce()
        yield UInt32(name="PasswordTest")

class CrcCompressedBlock(Struct):

    def __init__(self, size, **kwargs):
        Struct.__init__(self, **kwargs)
        self._size = size

    def parse(self):
        yield UInt32(name="BlockCrc")
        yield Bytes(self._size, name="BlockData")



class TSetupOffsets:

    def __init__(self, id, version, total_size, exe_offset, exe_uncompressed_size, exe_crc, setup0_offset, setup1_offset, offsets_crc):
        self.id = id
        self.version = version
        self.total_size = total_size
        self.exe_offset = exe_offset
        self.exe_uncompressed_size = exe_uncompressed_size
        self.exe_crc = exe_crc
        self.setup0_offset = setup0_offset
        self.setup1_offset = setup1_offset

    @staticmethod
    def deserialize(data):
        version, = struct.unpack("<I", data[12:16])
        if version == 1:
            return TSetupOffsets(*struct.unpack("<12s8I", data))
        elif version == 2:
            return TSetupOffsets(*struct.unpack("<12sIQQIIQQQ", data))
            

class LanguageIdShort(UInt16):

    def __init__(self, *args, **kwargs):
        kwargs["values"] = LanguageId.ALL
        UInt16.__init__(self, *args, **kwargs)
        

class InnoFile:

    def __init__(self):
        self.path = ""
        self.offset = None
        self.size = 0
        self.chunk_offset = None
        self.chunk_size = 0
        self.encrypted = False
        self.compressed = False
        self.filtered = False
        self.checksum = b""

class InnoStream:

    def __init__(self, compressed=True, blocks=None):
        self.compressed = compressed
        self.blocks = blocks or []




def filter_new(data, flip_high_byte=False):
    block_size = 0x10000
    res = bytearray(data)
    i = 0
    while len(data) - i >= 5:
        c = res[i]
        block_size_left = block_size - (i % block_size)
        i += 1
        if (c == 0xe8 or c == 0xe9) and block_size_left >= 5:
            address = res[i:i+4]
            i += 4

            if address[3] == 0 or address[3] == 0xff:
                rel = address[0] | address[1] << 8 | address[2] << 16
                rel -= i & 0xffffff

                res[i-4] = rel & 0xff
                res[i-3] = (rel >> 8) & 0xff
                res[i-2] = (rel >> 16) & 0xff

                if flip_high_byte and (rel & 0x800000) != 0:
                    res[i-1] = (~res[i-1]) & 0xff

    return bytes(res)


def filter_old(data):
    if type(data) == bytes:
        data = bytearray(data)
    addr_bytes_left = 0
    addr_offset = 5
    addr = 0
    for i, c in enumerate(data):
        if addr_bytes_left == 0:
            if c == 0xe8 or c == 0xe9:
                addr = (~addr_offset + 1) & 0xffffffff
                addr_bytes_left = 4
        else:
            addr = (addr + c) & 0xffffffff
            c = addr & 0xff
            addr = addr >> 8
            addr_bytes_left -= 1
        data[i] = c
    return bytes(data)


class InnoSetupAnalyzer(FileTypeAnalyzer):
    category = malcat.FileType.ARCHIVE
    name = "InnoSetup"
    regexp = r"Inno Setup Setup Data \("

    STREAM_FILES = [
            ("InnoSetup.TSetup", "#Header"), 
            ("InnoSetup.TData", "#InfoFiles"),
            ("", "#Uninstaller")
    ]


    @classmethod
    def locate(cls, curfile, offset_magic, parent_parser):
        if parent_parser is not None and parent_parser.name == "PE":
            for langid in ("unk", "en-us"):
                rsrc_data = f"Resources.RCDATA.11111.{langid}.Data"
                if rsrc_data in parent_parser:
                    try:
                        d = parent_parser[rsrc_data]
                        offsets = TSetupOffsets.deserialize(d)
                        base = min(filter(None, [offsets.exe_offset, offsets.setup0_offset, offsets.setup1_offset]))
                        if offset_magic < base:
                            return None
                        #if offset_magic != offsets.setup1_offset:
                        #    return None
                        return base, d.hex()
                    except:
                        return None
        return None


    def __init__(self):
        FileTypeAnalyzer.__init__(self)
        self.streams = []
        self.filesystem = {}
        self.compression = None
        self.files_start = 0
        self.files_end = 0
        self.guessed_password = None
        self.version = None
        self.magic = ""
        self.encryption_header = None


    def open_stream(self, vfile, password=None):
        for i, el in enumerate(InnoSetupAnalyzer.STREAM_FILES):
            if vfile.path == el[1]:
                stream = self.streams[i]
                return self.read_stream(stream)
        else:
            raise KeyError(vfile.path)


    def open_file(self, vfile, password=None):
        inno_file = self.filesystem[vfile.path]
        if password is None:
            password = self.guessed_password
        return self.read_file(inno_file, password)


    def read_stream(self, stream):
        if self.version < (4, 0, 9):
            raise NotImplementedError("Unsupported Inno version: too old!")
        ba = b"".join([b["BlockData"] for b in stream.blocks])
        if not stream.compressed:
            return ba
        if self.version >= (4, 1, 6):
            import lzma
            props = lzma._decode_filter_properties(lzma.FILTER_LZMA1, ba[0:5])
            ba = memoryview(ba)[5:]
            dec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[props])
        else:
            import zlib
            dec = zlib.decompressobj()
        return dec.decompress(ba)


    def read_chunk(self, offset, size, password=None, compressed=True):
        offset += self.files_start
        if offset + size > self.files_end: 
            raise ValueError("Chunk not in file")

        header = self.read(offset, 4)
        offset += 4
        if header != b"zlb\x1a":
            raise ValueError("Invalid chunk magic")

        if password and self.version < (6,4,0):
            salt = self.read(offset, 8)
            offset += 8

        data = self.read(offset, size)
        
        if password:
            if type(password) == str:
                if self.is_unicode:
                    password = password.encode("utf-16-le")
                else:
                    password = password.encode("latin1")
            import hashlib
            from transforms.stream import RC4
            if self.version >= (6,4,0):
                raise ValueError("XChaCha20 encryption not (yet) supported")
            else:
                if self.version >= (5, 3, 9):
                    key = hashlib.sha1(salt + password).digest()
                elif self.version < (6, 4, 0):
                    key = hashlib.md5(salt + password).digest()
                data = RC4().run(data, key, init_update_rounds=1000)

        if self.compression == "Stored" or not compressed:
            pass
        elif self.compression == "Lzma1":
            import lzma
            props = lzma._decode_filter_properties(lzma.FILTER_LZMA1, data[0:5])
            dec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[props])
            data = dec.decompress(memoryview(data)[5:])
        elif self.compression == "Lzma2":
            import lzma
            props = lzma._decode_filter_properties(lzma.FILTER_LZMA2, data[0:1])
            dec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[props])
            data = dec.decompress(memoryview(data)[1:])
        elif self.compression == "Bzip2":
            import bz2
            data = bz2.decompress(data)
        elif self.compression == "Zlib":
            import zlib
            data = zlib.decompress(data)
        else:
            raise NotImplementedError(f"Unsupported compression scheme {self.compression}")

        return data

    def read_file(self, inno_file, password="", ignore_filters=False):
        if inno_file.encrypted and not password:
            if self.guessed_password:
                password = self.guessed_password
            else:
                raise InvalidPassword("No password provided")
        try:
            chunk_data = self.read_chunk(inno_file.chunk_offset, inno_file.chunk_size, password, compressed=inno_file.compressed)
        except Exception as e:
            if inno_file.encrypted:
                raise InvalidPassword(e)
            else:
                raise

        data = chunk_data[inno_file.offset:inno_file.offset + inno_file.size]

        if inno_file.filtered and not ignore_filters:
            if self.version >= (5, 2, 0):
                data = filter_new(data, flip_high_byte = self.version >= (5, 3, 9))
            else:
                data = filter_old(data)
        if not data:
            raise ValueError("File content is empty")
        return data


    def open_script(self, vfile, password=None):
        data = self.read_stream(self.streams[0])
        fake_file = malcat.FileBuffer(bytearray(data), "stream0")
        parser = InnoSetupTSetupAnalyzer()
        parser.run(fake_file, hint=self.magic)
        setup_header = parser["SetupHeader"]
        return setup_header.CompiledCode.String.bytes


    def parse(self, hint):
        offsets = TSetupOffsets.deserialize(bytes.fromhex(hint))
        base = min(filter(None, [offsets.exe_offset, offsets.setup0_offset, offsets.setup1_offset]))
        self.set_eof(offsets.total_size - base)
        #parse setup0
        self.files_start = offsets.setup1_offset - base

        self.jump(offsets.setup0_offset - base)
        streams_start = self.tell()
        self.files_end = streams_start
        self.add_section( ".files", self.files_start, streams_start - self.files_start)
        ih = yield InnoHeader()
        identifier = ih["Identifier"].replace("\x00", "").strip()
        m = re.match(r"Inno Setup Setup Data \(([^)]*)\)(?: \((u)\))?$", identifier)
        if not m:
            print("No version found")
            self.magic = ""
        else:
            self.magic = m.group(1) + (m.group(2) and "u" or "a")
            self.version = tuple(map(int, m.group(1).split(".")))
        self.add_metadata("Version", self.magic)
        self.confirm()

        if self.version >= (6, 5, 0):
            self.encryption_header = yield SetupEncryptionHeader()

        for stream_type, name in InnoSetupAnalyzer.STREAM_FILES:
            if self.version < (6, 7, 0):
                sh = yield StreamHeader(name=f"StreamHeader.{name}")
            else:
                sh = yield StreamHeader64(name=f"StreamHeader.{name}")
            self.streams.append(InnoStream(sh["Compressed"]))
            to_read = sh["CompressedSize"]
            while to_read > 4:
                b = yield CrcCompressedBlock(min(to_read - 4, 0x1000), name="StreamBlock", parent=sh)
                self.streams[-1].blocks.append(b)
                to_read -= len(b)

            self.add_file(name, sh["CompressedSize"], "open_stream", stream_type, self.magic)


        self.add_section(".streams", streams_start, self.tell() - streams_start, r=True, discardable=True)

        encrypted_file = None
        files = []

        # parse stream 1
        data = self.read_stream(self.streams[1])
        fake_file = malcat.FileBuffer(bytearray(data), "stream1")
        parser2 = InnoSetupTDataAnalyzer()
        parser2.run(fake_file, hint=self.magic)
        for i, entry in enumerate(parser2):
            inno_file = InnoFile()
            inno_file.offset = entry["Offset"]
            inno_file.size = entry["FileSize"]
            inno_file.chunk_offset = entry["ChunkOffset"]
            inno_file.chunk_size = entry["ChunkSize"]
            inno_file.checksum = entry["Checksum"]
            if "ChunkCompressed" in entry["Flags1"]:
                inno_file.compressed = entry["Flags1"]["ChunkCompressed"]
            if "ChunkEncrypted" in entry["Flags1"]:
                inno_file.encrypted = entry["Flags1"]["ChunkEncrypted"]
            if "CallInstructionOptimized" in entry["Flags1"]:
                inno_file.filtered = entry["Flags1"]["CallInstructionOptimized"]
            if inno_file.encrypted and inno_file.size and (encrypted_file is None or (encrypted_file.filtered and not inno_file.filtered) or inno_file.size < encrypted_file.size):
                encrypted_file = inno_file  # we will try to guess the password on this file
            files.append(inno_file)

        # parse stream 0
        data = self.read_stream(self.streams[0])
        fake_file = malcat.FileBuffer(bytearray(data), "stream0")
        parser = InnoSetupTSetupAnalyzer()
        parser.run(fake_file, hint=self.magic)
        self.is_unicode = parser.is_unicode
        for k, v in parser.apps.items():
            self.add_metadata(k, v, category="Application")
        setup_header = parser["SetupHeader"]
        if "FilesArray" in parser:
            for fe in parser["FilesArray"]:
                location = fe["Location"]
                if fe.Type.enum != "UserFile" or fe["Source"]["Size"] > 0 or location == 0xffffffff:
                    continue
                path = fe["Destination"]["String"].replace("\\", "/")
                if location >= len(files):
                    print(f"Cannot locate data entry for {path}")
                else:
                    files[location].path = path
        self.compression = setup_header.CompressionMethod.enum

        if "String" in setup_header.CompiledCode:
            pascal_script = setup_header.CompiledCode.String.bytes
            self.add_file("#Script", len(pascal_script), "open_script", "PascalScript")
        
       

        for inno_file in files:
            self.filesystem[inno_file.path] = inno_file
            self.add_file(inno_file.path, inno_file.size, "open_file")

        if encrypted_file is not None:
            print(f"Found encrypted file(s) (e.g. {encrypted_file.path})... trying to guess password by scanning the inno script")
            try:
                # extract pascal script code
                fake_file = malcat.FileBuffer(bytearray(pascal_script), "IFPS")
                parser = PascalScriptAnalyzer()
                parser.run(fake_file)
                code = fake_file.read(parser.code_start, parser.code_size)
                
                # extract everything looking like a string
                candidates = []
                for m in re.finditer(rb"([\x01-\x80]\x00\x00\x00)([\x20-\x7f]{5,})", code):
                    size, = struct.unpack("<I", m.group(1))
                    try:
                        s = m.group(2)[:size].decode("ascii")
                        if len(s) == size:
                            candidates.append(s)
                    except BaseException as e:
                        print(e)
                for m in re.finditer(rb"([\x01-\x80]\x00\x00\x00)((?:[\x20-\x7f]\x00){5,})", code):
                    size, = struct.unpack("<I", m.group(1))
                    try:
                        s = m.group(2)[:size * 2].decode("utf-16-le")
                        if len(s) == size:
                            candidates.append(s)
                    except BaseException as e:
                        print(e)

                for c in candidates:
                    print(f"Trying {c} ... ", end="")

                    try:
                        decrypted_file = self.read_file(encrypted_file, password=c)
                        if len(encrypted_file.checksum) == 4:
                            pass
                        print("Found!")
                        self.guessed_password = c
                        break
                    except Exception as e:
                        print(f"KO: {e}")
                else:
                    print("No suitable password found: you won't be able to open some files :(")

                
            except Exception as e:
                print(f"Aborting: {e}")

##################################################################################################################################################


AUTO_NO_YES = [
        ("Auto", 0),
        ("No", 1),
        ("Yes", 2)
]


class SetupHeader(Struct):

    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield StringType(name="AppName")
        yield StringType(name="AppVersionedName")
        yield StringType(name="AppId")
        yield StringType(name="AppCopyright")
        yield StringType(name="AppPublisher")
        yield StringType(name="AppPublisherUrl")
        if self.version >= (5, 1, 13):
            yield StringType(name="AppSupportPhone")
        yield StringType(name="AppSupportUrl")
        yield StringType(name="AppUpdatesUrl")
        yield StringType(name="AppVersion")
        yield StringType(name="DefaultDirName")
        yield StringType(name="DefaultGroupName")
        yield StringType(name="BaseFilename")
        if self.version < (5, 2, 5):
            yield PascalString(name="LicenseText")
            yield PascalString(name="InfoBefore")
            yield PascalString(name="InfoAfter")
        yield StringType(name="UninstallFilesDir")
        yield StringType(name="UninstallName")
        yield StringType(name="UninstallIcon")
        yield StringType(name="AppMutex")
        if self.version >= (3, 0, 0):
            yield StringType(name="DefaultUsername")
            yield StringType(name="DefaultOrganisation")
        yield StringType(name="DefaultSerial")
        if self.version < (5, 2, 5):
            yield StringType(name="CompiledCode")
        if self.version >= (4, 2, 4):
            yield StringType(name="AppReadmeFile")
            yield StringType(name="AppContact")
            yield StringType(name="AppComment")
            yield StringType(name="AppModifyPatch")
        if self.version >= (5, 3, 8):
            yield StringType(name="CreateUninstallRegistryKey")
        if self.version >= (5, 3, 10):
            yield StringType(name="Uninstallable")
        if self.version >= (5, 5, 0):
            yield StringType(name="CloseApplicationFilter")
        if self.version >= (5, 5, 6):
            yield StringType(name="SetupMutex")
        if self.version >= (5, 6, 1):
            yield StringType(name="ChangesEnvironment")
            yield StringType(name="ChangesAssociation")
        if self.version >= (6, 3, 0):
            yield StringType(name="ArchitecturesAllowed")
            yield StringType(name="ArchitecturesAllowedIn64BitsMode")
        if self.version >= (6, 4, 0):
            yield StringType(name="CloseApplicationFilterExclude")
        if self.version >= (6, 5, 0):
            yield StringType(name="SevenZipLibraryName")
        if self.version >= (6, 7, 0):
            yield StringType(name="UsePreviousAppDir")
            yield StringType(name="UsePreviousGroup")
            yield StringType(name="UsePreviousSetupType")
            yield StringType(name="UsePreviousTasks")
            yield StringType(name="UsePreviousUserInfo")
        if self.version >= (5, 2, 5):
            yield PascalString(name="LicenseText")
            yield PascalString(name="InfoBefore")
            yield PascalString(name="InfoAfter")
        if self.version >= (5, 2, 1) and self.version < (5, 3, 10):
            yield PascalString(name="UninstallerSignature")
        if self.version >= (5, 2, 5):
            yield PascalString(name="CompiledCode")
        if not self.unicode:
            yield Bytes(256//8, name="Charset")
        yield UInt32(name="LanguageCount")
        if self.version >= (4, 2, 1):
            yield UInt32(name="MessageCount")
        if self.version >= (4, 1, 0):
            yield UInt32(name="PermissionCount")
        yield UInt32(name="TypeCount")
        yield UInt32(name="ComponentCount")
        yield UInt32(name="TaskCount")
        yield UInt32(name="DirectoryCount")
        if self.version >= (6, 5, 0):
            yield UInt32(name="ISSigKeyCount")
        yield UInt32(name="FileCount")
        yield UInt32(name="DataEntryCount")
        yield UInt32(name="IconCount")
        yield UInt32(name="IniEntryCount")
        yield UInt32(name="RegistryEntryCount")
        yield UInt32(name="DeleteEntryCount")
        yield UInt32(name="UninstallEntryCount")
        yield UInt32(name="RunEntryCount")
        yield UInt32(name="UninstallRunEntryCount")
        yield WindowsVersion(name="MinimumWindowsVersion")
        yield WindowsVersion(name="MaximumWindowsVersion")

        if self.version < (6, 4, 0, 1):
            yield UInt32(name="BackColor")
            yield UInt32(name="BackColor2")
        if self.version < (5, 5, 7):
            yield UInt32(name="ImageBackColor")
        if self.version < (5, 0, 4):
            yield UInt32(name="SmallImageBackColor")
        if self.version >= (6, 0, 0) and self.version < (6, 6, 0):
            yield UInt8(name="WizardStyle", values=[
                ("Classic", 0),
                ("Modern", 1)
                ])
        if self.version >= (6, 0, 0):
            yield UInt32(name="WizardResizePercentX")
            yield UInt32(name="WizardResizePercentY")
        if self.version >= (6, 6, 0):
            yield UInt8(name="WizardDarkStyle", values=[("Light", 0), ("Dark", 1), ("Auto", 2)])
        if self.version >= (5, 5, 7):
            yield UInt8(name="WizardImageAlphaFormat", values=[
                ("AlphaIgnored", 0),
                ("AlphaDefined", 1),
                ("AlphaPremultiplied", 2)
                ])
        if self.version >= (6, 5, 2):
            yield UInt32(name="WizardImageBackColor")
            yield UInt32(name="WizardSmallImageBackColor")
        if self.version >= (6, 7, 0):
            yield UInt32(name="WizardBackColor")
        if self.version >= (6, 6, 0):
            yield UInt32(name="WizardImageBackColorDynamicDark")
            yield UInt32(name="WizardSmallImageBackColorDynamicDark")
        if self.version >= (6, 7, 0):
            yield UInt32(name="WizardBackColorDynamicDark")
        if self.version >= (6, 6, 1):
            yield UInt8(name="WizardImageOpacity")
        if self.version >= (6, 7, 0):
            yield UInt8(name="WizardBackImageOpacity")
            yield UInt8(name="WizardLightControlStyling", values=[("All", 0), ("AllButButtons", 1), ("OnlyRequired", 2)])
        if self.version < (6, 5, 0):
            if self.version < (4, 2, 0):
                yield UInt32(name="PasswordCrc32")
            elif self.version < (5, 3, 9):
                pwd = yield Bytes(16, name="PasswordMd5")
                yield Bytes(8, name="PasswordSalt")
            elif self.version < (6, 4, 0):
                pwd = yield Bytes(20, name="PasswordSha1")
                yield Bytes(8, name="PasswordSalt")
            else:
                yield UInt32(name="PasswordTest")
                yield Bytes(16, name="PasswordSalt")
                yield UInt32(name="Iterations")
                yield SetupEncryptionNonce()
        yield Int64(name="ExtraDiskSpace")
        yield UInt32(name="SlicesPerDisk")
        if self.version < (5, 0, 0):
            yield UInt8(name="InstallVerbosity")
        yield UInt8(name="UninstallLogMode", values=[
            ("Append", 0),
            ("New", 1),
            ("Overwrite", 2)
            ])
        if self.version < (5, 0, 0):
            yield UInt8(name="SetupStyle", values=[
                ("Classic", 0),
                ("Modern", 1)
                ])
        yield UInt8(name="DirExistsWarning", values=AUTO_NO_YES)
        yield UInt8(name="PrivilegesRequired", values=[
                ("NoPrivileges", 0),
                ("PowerUserPrivileges", 1),
                ("AdminPriviliges", 2),
                ("LowestPrivileges", 3),
            ])
        if self.version >= (5, 7, 0):
            yield UInt8(name="PrivilegesRequiredOverrideAllowed", values=[
                ("CommandLine", 0),
                ("Dialog", 1)
                ])
        if self.version >= (4, 0, 10):
            yield UInt8(name="ShowLanguageDialog", values=AUTO_NO_YES)
            yield UInt8(name="LanguageDetection", values=[
                ("UI", 0),
                ("Locale", 1),
                ("None", 2),
            ])
        yield UInt8(name="CompressionMethod", values=[
                ("Stored", 0),
                ("Zlib", 1),
                ("Bzip2", 2),
                ("Lzma1", 3),
                ("Lzma2", 4),
            ])
        if self.version >= (5, 1, 0) and self.version < (6,3,0):
            yield UInt8(name="ArchitecturesAllowed", values=[
                ("Unknown", 0),
                ("X86", 1),
                ("AMD64", 2),
                ("IA64", 3),
                ("ARM64", 4),
                ])
            yield UInt8(name="ArchitecturesInstalled64", values=[
                ("Unknown", 0),
                ("X86", 1),
                ("AMD64", 2),
                ("IA64", 3),
                ("ARM64", 4),
                ])
        if self.version >= (5, 2, 1) and self.version < (5, 3, 10):
            yield UInt32(name="UninstallerOriginalSize")
            yield UInt32(name="UninstallheaderCrc")
        if self.version >= (5, 3, 3):
            yield UInt8(name="DisableDirPage", values=AUTO_NO_YES)
            yield UInt8(name="DisableProgramGroupPage", values=AUTO_NO_YES)
        if self.version >= (5, 5, 0):
            yield UInt64(name="UninstallDisplaySize")
        elif self.version >= (5, 3, 6):
            yield UInt32(name="UninstallDisplaySize")
        
        # handle blackbox version
        if self.unicode and self.version in ((5, 5, 0), (5, 4, 2), (5, 3, 10)):
            if self.look_ahead(1) == b"\x00":
                yield Unused(1)

        flags = ["DisableStartupPrompt"]
        if self.version < (5, 3, 10):
            flags.append("Uninstallable")
        flags.append("CreateAppDir")
        if self.version < (5, 3, 3):
            flags.append("DisableDirPage")
            flags.append("DisableProgramGroupPage")
        flags.append("AllowNoIcons")
        flags.append("AlwaysRestart")
        flags.append("AlwaysUsePersonalGroup")
        if self.version < (6, 4, 0, 1):
            flags.append("WindowVisible")
            flags.append("WindowShowCaption")
            flags.append("WindowResizable")
            flags.append("WindowStartMaximized")
        flags.append("EnableDirDoesntExistWarning")
        if self.version < (4, 1, 2):
            flags.append("DisableAppendDir")
        flags.append("Password")
        flags.append("AllowRootDirectory")
        flags.append("DisableFinishedPage")
        if self.version < (5, 6, 1):
            flags.append("ChangesAssociations")
        if self.version < (5, 3, 8):
            flags.append("CreateUninstallRegKey")
        if self.version < (6, 7, 0):
            flags.append("UsePreviousAppDir")
        if self.version < (6, 4, 0, 1):
            flags.append("BackColorHorizontal")
        if self.version < (6, 7, 0):
            flags.append("UsePreviousGroup")
        flags.append("UpdateUninstallLogAppName")
        if self.version < (6, 7, 0):
            flags.append("UsePreviousSetupType")
        flags.append("DisableReadyMemo")
        flags.append("AlwaysShowComponentsList")
        flags.append("FlatComponentsList")
        flags.append("ShowComponentSizes")
        if self.version < (6, 7, 0):
            flags.append("UsePreviousTasks")
        flags.append("DisableReadyPage")
        flags.append("AlwaysShowDirOnReadyPage")
        flags.append("AlwaysShowGroupOnReadyPage")
        if self.version < (4, 1, 5):
            flags.append("BzipUser")
        flags.append("AllowUNCPath")
        flags.append("UserInfoPage")
        if self.version < (6, 7, 0):
            flags.append("UsePreviousUserInfo")
        flags.append("UninstallRestartComputer")
        flags.append("RestartIfNeededByRun")
        flags.append("ShowTasksTreeLines")
        if self.version < (4, 0, 10):
            flags.append("ShowLanguageDialog")
        if self.version >= (4, 0, 1) and self.version < (4, 0, 10):
            flags.append("DetectLanguageUsingLocale")
        if self.version >= (4, 0, 9):
            flags.append("AllowCancelDuringInstall")
        if self.version >= (4, 1, 3):
            flags.append("WizardImageStretch")
        if self.version >= (4, 1, 8):
            flags.append("AppendDefaultDirName")
            flags.append("AppendDefaultGroupName")
        if self.version >= (4, 2, 2) and self.version < (6, 5, 0):
            flags.append("EncryptionUsed")
        if self.version >= (5, 0, 4) and self.version < (5, 6, 1):
            flags.append("ChangesEnvironment")
        if self.version >= (5, 1, 7) and not self.unicode and self.version < (6, 3, 0):
            flags.append("ShowUndisplayableLanguages")
        if self.version >= (5, 1, 13):
            flags.append("SetupLogging")
        if self.version >= (5, 2, 1):
            flags.append("SignedUninstaller")
        if self.version >= (5, 3, 8):
            flags.append("UsePreviousLanguage")
        if self.version >= (5, 3, 9):
            flags.append("DisableWelcomePage")
        if self.version >= (5, 5, 0):
            flags.append("CloseApplications")
            flags.append("RestartApplications")
            flags.append("AllowNetworkDrive")
        if self.version >= (5, 5, 7):
            flags.append("ForceCloseApplications")
        if self.version >= (6, 0, 0):
            flags.append("AppNameHasConsts")
            flags.append("UsePreviousPrivileges")
            flags.append("WizardResizable")
        if self.version >= (6, 3, 0):
            flags.append("UninstallLogging")
        if self.version >= (6, 6, 0):
            flags.append("WizardModern")
            flags.append("WizardBorderStyled")
            flags.append("WizardKeepAspectRatio")
        if self.version >= (6, 6, 0) and self.version < (6, 7, 0):
            flags.append("WizardLightButtonsUnstyled")
        if self.version >= (6, 7, 0):
            flags.append("RedirectionGuard")
            flags.append("WizardBevelsHidden")

      


        for i in range(0, len(flags), MAX_FLAGS_LENGTH):
            yield BitsField(
                    *[Bit(name=x) for x in flags[i:i+MAX_FLAGS_LENGTH]],
                name=f"Flags{i//MAX_FLAGS_LENGTH + 1}")

        if self.version >= (6, 7, 0) and len(flags) < 8 * 8:
            yield Unused((64 - len(flags)) // 8)

                



class Version(Struct):

    def parse(cls):
        yield UInt16(name="Build")
        yield UInt8(name="Minor")
        yield UInt8(name="Major")


class WindowsVersion(Struct):

    def parse(cls):
        yield Version(name="WindowsVersion")
        yield Version(name="NtVersion")
        yield UInt8(name="ServicePackMinor")
        yield UInt8(name="ServicePackMajor")



class SetupLanguage(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield StringType(name="Name")
        if self.version >= (4, 2, 2):
            yield UnicodeString(name="LanguageName", size_in_bytes=True)
        else:
            yield StringType(name="LanguageName")
        yield StringType(name="DialogFont")
        if self.version < (6, 6, 0):
            yield StringType(name="TitleFont")
        yield StringType(name="WelcomeFont")
        if self.version < (6, 6, 0):
            yield StringType(name="CopyrightFont")
        yield PascalString(name="Data")
        if self.version >= (4, 0, 1):
            yield PascalString(name="LicenseText")
            yield PascalString(name="InfoBefore")
            yield PascalString(name="InfoAfter")
        if self.version < (6, 6, 0):
            yield LanguageId(name="LanguageId")
        else:
            yield LanguageIdShort(name="LanguageId")
        if self.version >= (4, 2, 2) and (not self.unicode or self.version < (5, 3, 0)):
            if not self.unicode:
                yield UInt32(name="Codepage")
        yield UInt32(name="DialogFontSize")
        if self.version < (4, 1, 0):
            yield UInt32(name="DialogFontStandardHeight")
        if self.version < (6, 6, 0):
            yield UInt32(name="TitleFontSize")
        if self.version >= (6, 6, 0):
            yield UInt32(name="DialogFontBaseScaleHeight")
            yield UInt32(name="DialogFontBaseScaleWidth")
        yield UInt32(name="WelcomeFontSize")
        if self.version < (6, 6, 0):
            yield UInt32(name="CopyrightFontSize")
        if self.version >= (5, 2, 3):
            yield UInt8(name="RightToLeft")


class SetupMessage(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield StringType(name="EncodedName")
        yield StringType(name="Value")
        yield LanguageId(name="LanguageId")


class SetupType(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield StringType(name="Name")
        yield StringType(name="Description")
        if self.version >= (4, 0, 1):
            yield StringType(name="Languages")
        yield StringType(name="Check")
        yield WindowsVersion(name="MinimumWindowsVersion")
        yield WindowsVersion(name="MaximumWindowsVersion")
        yield UInt8(name="Type", values=[
            ("CustomSetupType", 0),
            ])
        if self.version >= (4, 0, 1):
            yield UInt8(name="SetupType", values=[
                ("User", 0),
                ("DefaultFull", 1),
                ("DefaultCompact", 2),
                ("DefaultCustom", 3),
                ])
        yield UInt64(name="Size")


class SetupComponent(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield StringType(name="Name")
        yield StringType(name="Description")
        yield StringType(name="Types")
        if self.version >= (4, 0, 1):
            yield StringType(name="Languages")
        yield StringType(name="Check")
        yield UInt64(name="ExtraDiskSpace")
        yield UInt32(name="Level")
        yield UInt8(name="Used")
        yield WindowsVersion(name="MinimumWindowsVersion")
        yield WindowsVersion(name="MaximumWindowsVersion")
        if self.version >= (4, 2, 3):
            yield UInt8(name="Flags", values=[
                ("Fixed", 0),
                ("Restart", 1),
                ("DisableNoUninstallWarning", 2),
                ("Exclusive", 3),
                ("DontInheritCheck", 4),
                ])
        yield UInt64(name="Size")



class SetupTask(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield StringType(name="Name")
        yield StringType(name="Description")
        yield StringType(name="GroupDescription")
        yield StringType(name="Components")
        if self.version >= (4, 0, 1):
            yield StringType(name="Languages")
        yield StringType(name="Check")
        if self.version < (6, 7, 0):
            yield UInt32(name="Level")
        else:
            yield UInt8(name="Level")
        yield UInt8(name="Used")
        yield WindowsVersion(name="MinimumWindowsVersion")
        yield WindowsVersion(name="MaximumWindowsVersion")
        flags = [
                "Exclusive",
                "Unchecked",
                "Restart",
                "CheckedOne",
        ]
        if self.version >= (4, 2, 3):
            flags.append("DontInheritCheck")

        for i in range(0, len(flags), MAX_FLAGS_LENGTH):
            yield BitsField(
                    *[Bit(name=x) for x in flags[i:i+MAX_FLAGS_LENGTH]],
                name=f"Flags{i//MAX_FLAGS_LENGTH + 1}")


class SetupCondition(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield StringType(name="Components")
        yield StringType(name="Tasks")
        if self.version >= (4, 0, 1):
            yield StringType(name="Languages")
        yield StringType(name="Check")
        if self.version >= (4, 1, 0):
            yield StringType(name="AfterInstall")
            yield StringType(name="BeforeInstall")
        


class SetupDirectory(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield StringType(name="Name")
        yield SetupCondition(self.version, self.unicode, name="Condition")
        if self.version >= (4, 0, 11) and self.version < (4, 1, 0):
            yield StringType(name="Permissions")
        yield UInt32(name="Attributes")
        yield WindowsVersion(name="MinimumWindowsVersion")
        yield WindowsVersion(name="MaximumWindowsVersion")
        if self.version >= (4, 1, 0):
            yield UInt16(name="Permissions")
        flags = [
                "NeverUninstall",
                "DeleteAfterInstall",
                "AlwaysUninstall",
                "SetNtfsCompression",
                "UnsetNtfsCompression",
        ]
        for i in range(0, len(flags), MAX_FLAGS_LENGTH):
            yield BitsField(
                    *[Bit(name=x) for x in flags[i:i+MAX_FLAGS_LENGTH]],
                name=f"Flags{i//MAX_FLAGS_LENGTH + 1}")

       
class SetupPermission(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString
        yield StringType(name="Permission")

   
class SetupISSigKey(Struct):

    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString
        yield PascalUnicodeString(name="PublicX")
        yield PascalUnicodeString(name="PublicY")
        yield PascalUnicodeString(name="RuntimeID")


class SetupFile(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield StringType(name="Source")
        yield StringType(name="Destination")
        yield StringType(name="InstallFontName")
        if self.version >= (5, 2, 5):
            yield StringType(name="StrongAssemblyName")
        yield SetupCondition(self.version, self.unicode, name="Condition")
        if self.version >= (6, 5, 0):
            yield StringType(name="Excludes")
            yield StringType(name="DownloadISSigSource")
            yield StringType(name="DownloadUserName")
            yield StringType(name="DownloadPassword")
            yield StringType(name="ExtractArchivePassword")
            yield PascalString(name="ISSigAllowedKeys")
            yield Bytes(32, name="Hash")
            yield UInt8(name="VerificationType", values=[("None", 0), ("Hash", 1), ("ISSig", 2), ])
        yield WindowsVersion(name="MinimumWindowsVersion")
        yield WindowsVersion(name="MaximumWindowsVersion")
        yield UInt32(name="Location")
        yield UInt32(name="Attributes")
        yield UInt64(name="ExternalSize")
        if self.version >= (4, 1, 0):
            yield UInt16(name="Permissions")

        flags = [
                "ConfirmOverwrite",
                "NeverUninstall",
                "RestartReplace",
                "DeleteAfterInstall",
                "RegisterServer",
                "RegisterTypeLib",
                "SharedFile",
                "CompareTimeStamp",
                "FontIsNotTrueType",
                "SkipIfSourceDoesntExist",
                "OverwriteReadOnly",
                "OverwriteSameVersion",
                "CustomDestName",
                "OnlyIfDestFileExists",
                "NoRegError",
                "UninsRestartDelete",
                "OnlyIfDoesntExist",
                "IgnoreVersion",
                "PromptIfOlder",
                "DontCopy",
        ]
        if self.version >= (4, 0, 5):
            flags.append("UninsRemoveReadOnly")
        if self.version >= (4, 1, 8):
            flags.append("RecurseSubDirsExternal")
        if self.version >= (4, 2, 1):
            flags.append("ReplaceSameVersionIfContentsDiffer")
        if self.version >= (4, 2, 5):
            flags.append("DontVerifyChecksum")
        if self.version >= (5, 0, 3):
            flags.append("UninsNoSharedFilePrompt")
        if self.version >= (5, 1,0):
            flags.append("CreateAllSubDirs")
        if self.version >= (5, 1, 2):
            flags.append("Bits32")
            flags.append("Bits64")
        if self.version >= (5, 2, 0):
            flags.append("ExternalSizePreset")
            flags.append("SetNtfsCompression")
            flags.append("UnsetNtfsCompression")
        if self.version >= (5, 2, 5):
            flags.append("GacInstall")
        if self.version >= (6, 5, 0):
            flags.append("Download")
            flags.append("ExtractArchive")

        for i in range(0, len(flags), MAX_FLAGS_LENGTH):
            yield BitsField(
                    *[Bit(name=x) for x in flags[i:i+MAX_FLAGS_LENGTH]],
                name=f"Flags{i//MAX_FLAGS_LENGTH + 1}")   
        if self.version >= (6, 7, 0) and len(flags) < 8 * 8:
            yield Unused((64 - len(flags)) // 8)

        yield UInt8(name="Type", values=[
                ("UserFile", 0),
                ("UninstExe", 1),
                ("RegSvrExe", 2),
                ])






class InnoSetupTSetupAnalyzer(FileTypeAnalyzer):
    category = malcat.FileType.PROGRAM
    name = "InnoSetup.TSetup"



    def parse(self, hint):
        self.apps = {}
        self.is_unicode = False
        if hint and "." in hint:
            hint = hint.strip()
            if hint.endswith("u"):
                self.is_unicode = True
            hint = hint[:-1]
            self.version = tuple(map(int, hint.split(".")))
        else:
            self.version = (5,0,0)
            print("Warning: using default version number")

        if not self.is_unicode and self.version >= (6,0,0):
            print("Warning: version >= 6 detected, forcing unicode mode")
            self.is_unicode = True

        self.add_metadata("Version", ".".join(map(str, self.version)))
        self.add_metadata("Unicode", str(self.is_unicode))

        if self.version < (4,0,0):
            raise FatalError("Unsupported version {}".format(hint))

        sh = yield SetupHeader(self.version, self.is_unicode)
        for metakey in ("AppName", "AppVersion", "AppPublisher", "AppPublisherUrl", "DefaultUsername", "DefaultOrganisation"):
            if "String" in sh[metakey]:
                self.add_metadata(metakey, sh[metakey]["String"], category="Application")
                self.apps[metakey] = sh[metakey]["String"]

        structures_to_parse = ["Language", "Message", "Permission", "Type", "Component", "Task", "Directory"]
        if self.version >= (6, 5, 0):
            structures_to_parse.append("ISSigKey")
        structures_to_parse.append("File")
        for c in structures_to_parse:
            count = sh[f"{c}Count"]
            class_ = globals()[f"Setup{c}"]

            class TheStruct(Struct):
                def __init__(self, version, is_unicode, **kwargs):
                    Struct.__init__(self, **kwargs)
                    self.version = version
                    self.is_unicode = is_unicode

                def parse(self):
                    for i in range(count):
                        yield class_(self.version, self.is_unicode, name=c)

            yield TheStruct(self.version, self.is_unicode, name=f"{c}sArray")


      
##################################################################################################################################################


class SetupDataEntry(Struct):
    
    def __init__(self, version, is_unicode, **kwargs):
        Struct.__init__(self, **kwargs)
        self.version = version
        self.unicode = is_unicode

    def parse(self):
        if self.unicode:
            StringType = PascalUnicodeString
        else:
            StringType = PascalString

        yield UInt32(name="FirstSlice")
        yield UInt32(name="LastSlice")
        if self.version >= (6, 5, 0):
            yield UInt64(name="ChunkOffset")
        else:
            yield UInt32(name="ChunkOffset")
        if self.version >= (4, 0, 1):
            yield UInt64(name="Offset")
        yield UInt64(name="FileSize")
        yield UInt64(name="ChunkSize")
        if self.version >= (6, 4, 0):
            yield Bytes(32, name="Checksum", comment="sha256")
        elif self.version >= (5, 3, 9):
            yield Bytes(20, name="Checksum", comment="sha1")
        elif self.version >= (4, 2, 0):
            yield Bytes(16, name="Checksum", comment="md5")
        elif self.version >= (4, 0, 1):
            yield Bytes(4, name="Checksum", comment="crc32")
        yield Filetime(name="Filetime")
        yield UInt32(name="FileVersionMs")
        yield UInt32(name="FileVersionLs")

        flags = [
                "VersionInfoValid",
        ]
        if self.version < (6, 4, 3):
            flags.append("VersionInfoNotValid")
        if self.version < (4, 0, 1):
            flags.append("BZipped")
        if self.version >= (4, 0, 10):
            flags.append("TimeStampInUTC")
        if self.version >= (4, 1, 0) and self.version < (6, 4, 3):
            flags.append("IsUninstallerExe")
        if self.version >= (4, 1, 8):
            flags.append("CallInstructionOptimized")
        if self.version >= (4, 2, 0) and self.version < (6, 4, 3):
            flags.append("Touch")
        if self.version >= (4, 2, 2):
            flags.append("ChunkEncrypted")
        if self.version >= (4, 2, 5):
            flags.append("ChunkCompressed")
        if self.version >= (5, 1, 13) and self.version < (6, 4, 3):
            flags.append("SolidBreak")
        if self.version >= (5, 5, 7) and self.version < (6, 3, 0):
            flags.append("Sign")
            flags.append("SignOnce")

        for i in range(0, len(flags), MAX_FLAGS_LENGTH):
            yield BitsField(
                    *[Bit(name=x) for x in flags[i:i+MAX_FLAGS_LENGTH]],
                name=f"Flags{i//MAX_FLAGS_LENGTH + 1}")   

        if self.version >= (6, 3, 0) and self.version < (6, 4, 3):
            flags = [
                "NoSettings",
                "Yes",
                "Once",
                "Check",
            ]
            for i in range(0, len(flags), MAX_FLAGS_LENGTH):
                yield BitsField(
                        *[Bit(name=x) for x in flags[i:i+MAX_FLAGS_LENGTH]],
                    name=f"SignFlags{i//MAX_FLAGS_LENGTH + 1}")   

        



class InnoSetupTDataAnalyzer(FileTypeAnalyzer):
    category = malcat.FileType.FILESYSTEM
    name = "InnoSetup.TData"

    def parse(self, hint):
        self.is_unicode = False
        if hint and "." in hint:
            hint = hint.strip()
            if hint.endswith("u"):
                self.is_unicode = True
            hint = hint[:-1]
            self.version = tuple(map(int, hint.split(".")))
        else:
            self.version = (5,0,0)
            print("Warning: using default version number")

        self.add_metadata("Version", ".".join(map(str, self.version)))
        self.add_metadata("Unicode", str(self.is_unicode))

        if self.version < (4,0,0):
            raise FatalError("Unsupported version {}".format(hint))

        while self.remaining() > 16:
            yield SetupDataEntry(self.version, self.is_unicode, name="DataEntry")

           
