From 213ad9399d2f54d9b879c6d02099663dc4aa1c35 Mon Sep 17 00:00:00 2001 From: SN <2005kan@mail.ru> Date: Fri, 2 May 2025 14:14:40 +0500 Subject: [PATCH 1/3] v1 v2 v3 Update README.md --- .flake8 | 21 + .pre-commit-config.yaml | 29 ++ .venv/.gitignore | 3 + PE_parser/PE_structures.py | 425 ++++++++++++++++++ PE_parser/parser.py | 250 +++++++++++ PE_parser/test_parser.py | 162 +++++++ PE_parser/test_pe_structures.py | 326 ++++++++++++++ README.md | 104 ++++- mypy.ini | 27 ++ poetry.lock | 753 ++++++++++++++++++++++++++++++++ pyproject.toml | 66 +++ 11 files changed, 2165 insertions(+), 1 deletion(-) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 .venv/.gitignore create mode 100644 PE_parser/PE_structures.py create mode 100644 PE_parser/parser.py create mode 100644 PE_parser/test_parser.py create mode 100644 PE_parser/test_pe_structures.py create mode 100644 mypy.ini create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..ff1ad85 --- /dev/null +++ b/.flake8 @@ -0,0 +1,21 @@ +[flake8] +max-line-length = 120 + +ignore = + E203, + W503, + E501, + +exclude = + .git, + __pycache__, + .venv, + .mypy_cache, + build, + dist, + migrations, + venv + +filename = *.py + +max-complexity = 12 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..dce9030 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.6.1 + hooks: + - id: mypy + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + + - repo: https://github.com/psf/black + rev: 23.10.1 + hooks: + - id: black + args: [--skip-string-normalization] + + - repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + args: [--max-line-length=120] diff --git a/.venv/.gitignore b/.venv/.gitignore new file mode 100644 index 0000000..c560ba5 --- /dev/null +++ b/.venv/.gitignore @@ -0,0 +1,3 @@ +# created by virtualenv automatically +.idea +.venv diff --git a/PE_parser/PE_structures.py b/PE_parser/PE_structures.py new file mode 100644 index 0000000..823fcb4 --- /dev/null +++ b/PE_parser/PE_structures.py @@ -0,0 +1,425 @@ +from dataclasses import dataclass, field +import enum +import struct +from typing import Union + + +class MachineType(enum.IntEnum): + """Типы машин, поддерживаемые PE файлами""" + + IMAGE_FILE_MACHINE_I386 = 0x014C # 32-bit x86 + IMAGE_FILE_MACHINE_AMD64 = 0x8664 # 64-bit x86 (AMD64) + IMAGE_FILE_MACHINE_IA64 = 0x0200 # Intel Itanium + + +class SubsystemType(enum.IntEnum): + """Типы подсистем, которые могут быть указаны в PE файле""" + + IMAGE_SUBSYSTEM_NATIVE = 1 # Драйверы Windows + IMAGE_SUBSYSTEM_WINDOWS_GUI = 2 # Приложение с графическим интерфейсом + IMAGE_SUBSYSTEM_WINDOWS_CUI = 3 # Консольное приложение + IMAGE_SUBSYSTEM_EFI_APPLICATION = 10 # EFI приложение + + +@dataclass +class DOSHeader: + """Заголовок DOS, присутствующий в начале каждого PE файла""" + + e_magic: bytes # Сигнатура "MZ" + e_lfanew: int # Смещение до PE заголовка + + @classmethod + def from_bytes(cls, data: bytes) -> 'DOSHeader': + """Создает DOSHeader из байтовой строки""" + e_magic = data[:2] + e_lfanew = struct.unpack(" 'PEHeader': + """Создает PEHeader из байтовой строки""" + offset = 4 + machine = MachineType(struct.unpack(" 'DataDirectory': + """Создает DataDirectory из байтовой строки""" + unpacked = struct.unpack("<2I", data[:8]) + return cls(*unpacked) + + +@dataclass +class OptionalHeaderPE32: + """Опциональный заголовок для 32-битных PE файлов (IMAGE_OPTIONAL_HEADER32)""" + + magic: int # Магическое число + major_linker_version: int # Версия линкера + minor_linker_version: int # Минорная версия линкера + size_of_code: int # Размер кода + size_of_initialized_data: int # Размер инициализированных данных + size_of_uninitialized_data: int # Размер неинициализированных данных + address_of_entry_point: int # Точка входа + base_of_code: int # Базовый адрес кода + base_of_data: int # Базовый адрес данных + image_base: int # Базовый адрес образа в памяти + section_alignment: int # Выравнивание секций в памяти + file_alignment: int # Выравнивание секций в файле + major_os_version: int # Версия ОС + minor_os_version: int # Минорная версия ОС + major_image_version: int # Версия образа + minor_image_version: int # Минорная версия образа + major_subsystem_version: int # Версия подсистемы + minor_subsystem_version: int # Минорная версия подсистемы + win32_version_value: int # Версия Win32 + size_of_image: int # Размер образа в памяти + size_of_headers: int # Размер заголовков + checksum: int # Контрольная сумма + subsystem: SubsystemType # Тип подсистемы + dll_characteristics: int # Характеристики DLL + size_of_stack_reserve: int # Размер стека + size_of_stack_commit: int # Размер выделяемого стека + size_of_heap_reserve: int # Размер кучи + size_of_heap_commit: int # Размер выделяемой кучи + loader_flags: int # Флаги загрузчика + number_of_rva_and_sizes: int # Количество элементов в таблице данных + + @classmethod + def from_bytes(cls, data: bytes) -> 'OptionalHeaderPE32': + """Создает OptionalHeaderPE32 из байтовой строки""" + fmt = " 'OptionalHeaderPE64': + """Создает OptionalHeaderPE64 из байтовой строки""" + fmt = " 'SectionHeader': + """Создает SectionHeader из байтовой строки""" + fmt = "<8s6I2HI" + unpacked = struct.unpack(fmt, data[:40]) + return cls( + name=unpacked[0], + virtual_size=unpacked[1], + virtual_address=unpacked[2], + size_of_raw_data=unpacked[3], + pointer_to_raw_data=unpacked[4], + pointer_to_relocations=unpacked[5], + pointer_to_linenumbers=unpacked[6], + number_of_relocations=unpacked[7], + number_of_linenumbers=unpacked[8], + characteristics=unpacked[9], + ) + + +@dataclass +class ImportDescriptor: + """Дескриптор импорта (IMAGE_IMPORT_DESCRIPTOR)""" + + original_first_thunk: int + time_date_stamp: int + forwarder_chain: int + name: int + first_thunk: int + + @classmethod + def from_bytes(cls, data: bytes) -> 'ImportDescriptor': + """Создает ImportDescriptor из байтовой строки""" + fmt = "<5I" + unpacked = struct.unpack(fmt, data[:20]) + return cls(*unpacked) + + def is_null(self) -> bool: + """Проверяет, является ли дескриптор нулевым (маркер конца массива)""" + return all( + v == 0 + for v in ( + self.original_first_thunk, + self.time_date_stamp, + self.forwarder_chain, + self.name, + self.first_thunk, + ) + ) + + +def _read_null_terminated_string(data: bytes, offset: int) -> bytes: + """Читает нуль-терминированную строку из данных (приватная функция)""" + end = offset + while end < len(data) and data[end] != 0: + end += 1 + return data[offset:end] + + +@dataclass +class PEFile: + """Основной класс для представления PE файла""" + + dos_header: DOSHeader # DOS заголовок + pe_header: PEHeader # PE заголовок + optional_header: Union[OptionalHeaderPE32, OptionalHeaderPE64] # Опциональный заголовок + sections: list[SectionHeader] # Список заголовков секций + imported_dlls: list[str] = field(default_factory=list) # Список импортируемых DLL + + @classmethod + def parse(cls, file_path: str) -> 'PEFile': + """Парсит PE файл и возвращает объект PEFile""" + with open(file_path, "rb") as f: + data = f.read() + dos_header = DOSHeader.from_bytes(data) + + pe_offset = dos_header.e_lfanew + pe_header = PEHeader.from_bytes(data[pe_offset : pe_offset + 24]) + + magic_offset = pe_offset + 24 + magic = struct.unpack(" None: + """Читает список импортируемых DLL из данных (приватный метод)""" + if not hasattr(self.optional_header, 'number_of_rva_and_sizes'): + return + + pe_offset = self.dos_header.e_lfanew + opt_header_start = pe_offset + 24 + + if isinstance(self.optional_header, OptionalHeaderPE32): + data_dir_offset = opt_header_start + 96 + else: + data_dir_offset = opt_header_start + 112 + + import_dir_offset = data_dir_offset + 8 + if import_dir_offset + 8 > len(data): + return + + import_rva = struct.unpack(" len(data): + break + + descriptor = ImportDescriptor.from_bytes(data[import_offset : import_offset + 20]) + if descriptor.is_null(): + break + + name_offset = self._rva_to_file_offset(descriptor.name) + if name_offset is None: + continue + + dll_name = _read_null_terminated_string(data, name_offset) + if dll_name: + self.imported_dlls.append(dll_name.decode('ascii')) + + import_offset += 20 + + def _rva_to_file_offset(self, rva: int) -> int | None: + """Конвертирует RVA в смещение в файле (приватный метод)""" + for section in self.sections: + if section.virtual_address <= rva < section.virtual_address + section.virtual_size: + return rva - section.virtual_address + section.pointer_to_raw_data + return None diff --git a/PE_parser/parser.py b/PE_parser/parser.py new file mode 100644 index 0000000..97baa85 --- /dev/null +++ b/PE_parser/parser.py @@ -0,0 +1,250 @@ +import argparse +from enum import Enum +import json +from typing import Any, Optional + +from PE_structures import MachineType, PEFile, SubsystemType + + +class OutputFormat(Enum): + """Перечисление форматов вывода результатов анализа PE файла (text/json).""" + + TEXT = "text" + JSON = "json" + + +class Color: + """ANSI коды цветов для терминала.""" + + HEADER = '\033[95m' + RED = '\033[91m' + PINK = '\033[95m' + GREEN = '\033[92m' + END = '\033[0m' + BOLD = '\033[1m' + + +def colorize(text: str, color: str) -> str: + """Добавляет ANSI цвет к тексту и сбрасывает форматирование.""" + return f"{color}{text}{Color.END}" + + +def print_section_header(title: str) -> None: + """Выводит цветной заголовок секции с разделителями.""" + print(colorize(f"\n=== {title} ===", Color.RED + Color.BOLD)) + + +def print_field(name: str, value: Any, indent: int = 0) -> None: + """Выводит имя и значение поля с отступом и цветным оформлением.""" + indent_str = " " * indent + print(f"{indent_str}{colorize(name + ':', Color.PINK)} {value}") + + +def format_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> dict[str, Any]: + """Форматирует информацию о PE файле в словарь для JSON вывода.""" + result: dict[str, Any] = {} + + if sections is None or "dos" in sections: + dos_header = { + "Magic": pe_file.dos_header.e_magic.decode('ascii'), + "PE Header Offset": f"0x{pe_file.dos_header.e_lfanew:X}", + } + result["DOS Header"] = dos_header + + if sections is None or "pe" in sections: + pe_header = { + "Signature": pe_file.pe_header.signature.decode('ascii'), + "Machine": MachineType(pe_file.pe_header.machine).name, + "Number of Sections": pe_file.pe_header.number_of_sections, + "Size Of Optional Header": pe_file.pe_header.size_of_optional_header, + "Timestamp": f"0x{pe_file.pe_header.time_date_stamp:X}", + "Characteristics": f"0x{pe_file.pe_header.characteristics:X}", + } + result["PE Header"] = pe_header + + if sections is None or "optional" in sections: + optional_header = { + "Address Of Entry Point": f"0x{pe_file.optional_header.address_of_entry_point:X}", + "Image Base": f"0x{pe_file.optional_header.image_base:X}", + "Section Alignment": f"0x{pe_file.optional_header.section_alignment:X}", + "File Alignment": f"0x{pe_file.optional_header.file_alignment:X}", + "Major Subsystem Version": f"0x{pe_file.optional_header.major_subsystem_version:X}", + "Minor Subsystem Version": f"0x{pe_file.optional_header.minor_subsystem_version:X}", + "Subsystem": SubsystemType(pe_file.optional_header.subsystem).name, + "Size of Image": f"0x{pe_file.optional_header.size_of_image:X}", + "Size of Headers": f"0x{pe_file.optional_header.size_of_headers:X}", + "Number Of Rva And Sizes": f"0x{pe_file.optional_header.number_of_rva_and_sizes:X}", + } + result["Optional Header"] = optional_header + + if sections is None or "sections" in sections: + sections_list = [] + for i, section in enumerate(pe_file.sections): + section_info = { + "Name": section.name.decode('ascii').strip(), + "Virtual Size": f"0x{section.virtual_size:X}", + "Virtual Address": f"0x{section.virtual_address:X}", + "Raw Size": f"0x{section.size_of_raw_data:X}", + "Raw Pointer": f"0x{section.pointer_to_raw_data:X}", + "Characteristics": f"0x{section.characteristics:X}", + } + sections_list.append(section_info) + result["Sections"] = sections_list + + if sections is None or "imports" in sections: + imports = pe_file.imported_dlls if pe_file.imported_dlls else ["No imports found"] + result["Imports"] = imports + + return result + + +def print_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> None: + """Выводит информацию о PE файле в текстовом формате с цветным оформлением.""" + if sections is None or "dos" in sections: + print_section_header("DOS Header") + print_field("Magic", pe_file.dos_header.e_magic.decode('ascii')) + print_field("PE Header Offset", f"0x{pe_file.dos_header.e_lfanew:X}") + + if sections is None or "pe" in sections: + print_section_header("PE Header") + print_field("Signature", pe_file.pe_header.signature.decode('ascii')) + print_field("Machine", MachineType(pe_file.pe_header.machine).name) + print_field("Number of Sections", pe_file.pe_header.number_of_sections) + print_field("Size Of Optional Header", pe_file.pe_header.size_of_optional_header) + print_field("Timestamp", f"0x{pe_file.pe_header.time_date_stamp:X}") + print_field("Characteristics", f"0x{pe_file.pe_header.characteristics:X}") + + if sections is None or "optional" in sections: + print_section_header("Optional Header") + print_field("Address Of Entry Point", f"0x{pe_file.optional_header.address_of_entry_point:X}") + print_field("Image Base", f"0x{pe_file.optional_header.image_base:X}") + print_field("Section Alignment", f"0x{pe_file.optional_header.section_alignment:X}") + print_field("File Alignment", f"0x{pe_file.optional_header.file_alignment:X}") + print_field("Major Subsystem Version", f"0x{pe_file.optional_header.major_subsystem_version:X}") + print_field("Minor Subsystem Version", f"0x{pe_file.optional_header.minor_subsystem_version:X}") + print_field("Subsystem", SubsystemType(pe_file.optional_header.subsystem).name) + print_field("Size of Image", f"0x{pe_file.optional_header.size_of_image:X}") + print_field("Size of Headers", f"0x{pe_file.optional_header.size_of_headers:X}") + print_field("Number Of Rva And Sizes", f"0x{pe_file.optional_header.number_of_rva_and_sizes:X}") + + if sections is None or "sections" in sections: + print_section_header("Sections") + for i, section in enumerate(pe_file.sections): + print_field(f"Section {i + 1}", "", 2) + print_field("Name", section.name.decode('ascii').strip(), 4) + print_field("Virtual Size", f"0x{section.virtual_size:X}", 4) + print_field("Virtual Address", f"0x{section.virtual_address:X}", 4) + print_field("Raw Size", f"0x{section.size_of_raw_data:X}", 4) + print_field("Raw Pointer", f"0x{section.pointer_to_raw_data:X}", 4) + print_field("Characteristics", f"0x{section.characteristics:X}", 4) + + if sections is None or "imports" in sections: + print_section_header("Imports") + if pe_file.imported_dlls: + for dll in pe_file.imported_dlls: + print_field(" -", dll, 2) + else: + print_field("", "No imports found", 2) + + +def main() -> None: + """Основная функция: парсит аргументы командной строки и запускает анализ PE файла.""" + parser = argparse.ArgumentParser( + description="PE file parser", formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("file", help="Path to the PE file to analyze") + + output_group = parser.add_argument_group("Output options") + output_group.add_argument( + "-f", "--format", choices=[f.value for f in OutputFormat], default=OutputFormat.TEXT.value, help="Output format" + ) + output_group.add_argument("-o", "--output", type=str, help="Output file path (default: stdout)") + output_group.add_argument("--no-color", action="store_true", help="Disable colored output") + + section_group = parser.add_argument_group("Section selection") + section_group.add_argument( + "-s", + "--sections", + nargs="+", + choices=["dos", "pe", "optional", "sections", "imports"], + help="Select specific sections to display", + ) + + args = parser.parse_args() + + pe_file = PEFile.parse(args.file) + + if args.format == OutputFormat.JSON.value: + result = format_pe_info(pe_file, args.sections) + output = json.dumps(result, indent=2) + else: + + class LocalColor: + """Локальные настройки цветов для отключения оформления.""" + + HEADER = Color.HEADER + RED = Color.RED + PINK = Color.PINK + GREEN = Color.GREEN + END = Color.END + BOLD = Color.BOLD + + if args.no_color: + LocalColor.HEADER = '' + LocalColor.RED = '' + LocalColor.PINK = '' + LocalColor.GREEN = '' + LocalColor.END = '' + LocalColor.BOLD = '' + + output_lines: list[str] = [] + + def capture_print(*values: Any) -> None: + """Перехватывает вывод print для сохранения в буфер.""" + output_lines.append(" ".join(str(v) for v in values)) + + import builtins + + original_print = builtins.print + builtins.print = capture_print # type: ignore[assignment] + + original_colorize = globals()['colorize'] + original_print_section_header = globals()['print_section_header'] + original_print_field = globals()['print_field'] + + def local_colorize(text: str, color: str) -> str: + """Локальная версия colorize с настраиваемыми цветами.""" + return f"{color}{text}{LocalColor.END}" + + def local_print_section_header(title: str) -> None: + """Локальная версия print_section_header с настраиваемыми цветами.""" + print(local_colorize(f"\n=== {title} ===", LocalColor.RED + LocalColor.BOLD)) + + def local_print_field(name: str, value: Any, indent: int = 0) -> None: + """Локальная версия print_field с настраиваемыми цветами.""" + indent_str = " " * indent + print(f"{indent_str}{local_colorize(name + ':', LocalColor.PINK)} {value}") + + globals()['colorize'] = local_colorize + globals()['print_section_header'] = local_print_section_header + globals()['print_field'] = local_print_field + + try: + print_pe_info(pe_file, args.sections) + finally: + builtins.print = original_print + globals()['colorize'] = original_colorize + globals()['print_section_header'] = original_print_section_header + globals()['print_field'] = original_print_field + + output = "\n".join(output_lines) + + if args.output: + with open(args.output, "w") as f: + f.write(output) + else: + print(output) + + +if __name__ == "__main__": + main() diff --git a/PE_parser/test_parser.py b/PE_parser/test_parser.py new file mode 100644 index 0000000..f0a5f0f --- /dev/null +++ b/PE_parser/test_parser.py @@ -0,0 +1,162 @@ +import json +from parser import Color, colorize, format_pe_info, main, print_field, print_pe_info, print_section_header +from typing import Any +from unittest.mock import patch + +from PE_structures import DOSHeader, MachineType, OptionalHeaderPE32, PEFile, PEHeader, SectionHeader, SubsystemType +import pytest +from pytest import CaptureFixture + + +def test_colorize() -> None: + """Проверяет добавление ANSI-цветов к тексту (красный цвет и сброс форматирования).""" + colored = colorize("test", Color.RED) + assert colored.startswith('\033[91m') + assert colored.endswith('\033[0m') + assert "test" in colored + + +def test_print_section_header(capsys: CaptureFixture[str]) -> None: + """Тестирует вывод цветного заголовка секции с разделителями '==='.""" + print_section_header("Test Section") + captured = capsys.readouterr() + assert "=== Test Section ===" in captured.out + assert '\033[91m' in captured.out + + +def test_print_field(capsys: CaptureFixture[str]) -> None: + """Проверяет форматирование вывода поля и значения с фиолетовым цветом метки.""" + print_field("Field", "Value") + captured = capsys.readouterr() + assert "Field:" in captured.out + assert "Value" in captured.out + assert '\033[95m' in captured.out + + +def test_format_pe_info(sample_pe_object: PEFile) -> None: + """Тестирует преобразование PEFile в словарь с проверкой основных заголовков и секций.""" + result = format_pe_info(sample_pe_object) + assert isinstance(result, dict) + assert result["DOS Header"]["Magic"] == "MZ" + assert result["PE Header"]["Machine"] == "IMAGE_FILE_MACHINE_I386" + assert len(result["Sections"]) == 3 + assert result["Imports"] == ["KERNEL32.DLL", "USER32.DLL"] + + +def test_format_pe_info_with_sections(sample_pe_object: PEFile) -> None: + """Проверяет фильтрацию разделов при преобразовании PEFile (только PE Header и Imports).""" + result = format_pe_info(sample_pe_object, sections=["pe", "imports"]) + assert "DOS Header" not in result + assert "PE Header" in result + assert "Sections" not in result + assert "Imports" in result + + +def test_print_pe_info(sample_pe_object: PEFile, capsys: CaptureFixture[str]) -> None: + """Тестирует полный вывод информации о PE-файле в консоль (все разделы).""" + print_pe_info(sample_pe_object) + captured = capsys.readouterr() + assert "DOS Header" in captured.out + assert "PE Header" in captured.out + assert ".text" in captured.out + assert "KERNEL32.DLL" in captured.out + + +def test_print_pe_info_with_sections(sample_pe_object: PEFile, capsys: CaptureFixture[str]) -> None: + """Проверяет частичный вывод информации (только PE Header и Optional Header).""" + print_pe_info(sample_pe_object, sections=["pe", "optional"]) + captured = capsys.readouterr() + assert "DOS Header" not in captured.out + assert "PE Header" in captured.out + assert "Optional Header" in captured.out + assert ".text" not in captured.out + + +@patch('parser.PEFile.parse') +def test_main_json_output( + mock_parse: Any, sample_pe_object: PEFile, tmp_path: Any, capsys: CaptureFixture[str] +) -> None: + """Тестирует сохранение данных в JSON-файл и отсутствие вывода в консоль.""" + mock_parse.return_value = sample_pe_object + output_file = tmp_path / "output.json" + + with patch('sys.argv', ['parser.py', 'test.exe', '-f', 'json', '-o', str(output_file)]): + main() + + assert output_file.exists() + content = json.loads(output_file.read_text()) + assert content["DOS Header"]["Magic"] == "MZ" + + captured = capsys.readouterr() + assert not captured.out + + +@patch('parser.PEFile.parse') +def test_main_text_output(mock_parse: Any, sample_pe_object: PEFile, capsys: CaptureFixture[str]) -> None: + """Проверяет стандартный текстовый вывод информации о PE-файле в консоль.""" + mock_parse.return_value = sample_pe_object + + with patch('sys.argv', ['parser.py', 'test.exe']): + main() + + captured = capsys.readouterr() + assert "DOS Header" in captured.out + assert "PE Header" in captured.out + + +@patch('parser.PEFile.parse') +def test_main_no_color(mock_parse: Any, sample_pe_object: PEFile, capsys: CaptureFixture[str]) -> None: + """Тестирует вывод без цветового форматирования (отсутствие ANSI-кодов).""" + mock_parse.return_value = sample_pe_object + + with patch('sys.argv', ['parser.py', 'test.exe', '--no-color']): + main() + + captured = capsys.readouterr() + assert "DOS Header" in captured.out + assert '\033[' not in captured.out + + +@pytest.fixture +def sample_pe_object() -> PEFile: + """Фикстура: создает тестовый объект PEFile с заполненными заголовками и секциями.""" + dos_header = DOSHeader(b'MZ', 0x100) + pe_header = PEHeader(b'PE\x00\x00', MachineType.IMAGE_FILE_MACHINE_I386, 3, 0x12345678, 0, 0, 0xE0, 0x123) + optional_header = OptionalHeaderPE32( + 0x10B, + 1, + 2, + 0x100, + 0x200, + 0x300, + 0x400, + 0x500, + 0x600, + 0x7000000, + 0x1000, + 0x200, + 1, + 0, + 1, + 0, + 4, + 0, + 0, + 0x3000, + 0x400, + 0x1234, + SubsystemType.IMAGE_SUBSYSTEM_WINDOWS_CUI, + 0, + 0x10000, + 0x1000, + 0x20000, + 0x2000, + 0, + 16, + ) + sections = [ + SectionHeader(b'.text\x00\x00\x00', 0x1000, 0x1000, 0x200, 0x400, 0, 0, 0, 0, 0x60000020), + SectionHeader(b'.data\x00\x00\x00', 0x1000, 0x2000, 0x200, 0x600, 0, 0, 0, 0, 0xC0000040), + SectionHeader(b'.rsrc\x00\x00\x00', 0x1000, 0x3000, 0x200, 0x800, 0, 0, 0, 0, 0x40000040), + ] + return PEFile(dos_header, pe_header, optional_header, sections, ["KERNEL32.DLL", "USER32.DLL"]) diff --git a/PE_parser/test_pe_structures.py b/PE_parser/test_pe_structures.py new file mode 100644 index 0000000..ae0dfa4 --- /dev/null +++ b/PE_parser/test_pe_structures.py @@ -0,0 +1,326 @@ +from PE_structures import ( + DataDirectory, + DOSHeader, + ImportDescriptor, + MachineType, + OptionalHeaderPE32, + OptionalHeaderPE64, + PEFile, + PEHeader, + SectionHeader, + SubsystemType, + _read_null_terminated_string, +) + + +TEST_DOS_HEADER_DATA = b'MZ' + b'\x00' * 58 + b'\x80\x00\x00\x00' +TEST_PE_HEADER_DATA = b'PE\x00\x00' + b'\x4c\x01' + b'\x03\x00' + b'\x11\x22\x33\x44' + b'\x00' * 16 +TEST_OPTIONAL_HEADER32_DATA = ( + b'\x0b\x01' + + b'\x01\x02' + + b'\x11\x00\x00\x00' * 3 + + b'\x55\x00\x00\x00' + + b'\x66\x00\x00\x00' + + b'\x77\x00\x00\x00' + + b'\x88\x00\x00\x00' + + b'\x99\x00\x00\x00' * 2 + + b'\x01\x00' * 6 + + b'\xaa\x00\x00\x00' * 4 + + b'\x02\x00' + + b'\xbb\x00' + + b'\xcc\x00\x00\x00' * 4 + + b'\xdd\x00\x00\x00' * 2 +) +TEST_OPTIONAL_HEADER64_DATA = ( + b'\x0b\x02' + + b'\x01\x02' + + b'\x11\x00\x00\x00' * 3 + + b'\x55\x00\x00\x00' + + b'\x66\x00\x00\x00' + + b'\x88\x00\x00\x00\x00\x00\x00\x00' + + b'\x99\x00\x00\x00' * 2 + + b'\x01\x00' * 6 + + b'\xaa\x00\x00\x00' * 4 + + b'\x02\x00' + + b'\xbb\x00' + + b'\xcc\x00\x00\x00\x00\x00\x00\x00' * 4 + + b'\xdd\x00\x00\x00' * 2 +) +TEST_SECTION_HEADER_DATA = ( + b'.text\x00\x00\x00' + + b'\x11\x00\x00\x00' + + b'\x22\x00\x00\x00' + + b'\x33\x00\x00\x00' + + b'\x44\x00\x00\x00' + + b'\x00\x00\x00\x00' * 3 + + b'\x20\x00\x00\x60' +) +TEST_IMPORT_DESCRIPTOR_DATA = ( + b'\x11\x00\x00\x00' + b'\x22\x00\x00\x00' + b'\x33\x00\x00\x00' + b'\x44\x00\x00\x00' + b'\x55\x00\x00\x00' +) + + +def create_mock_pe_file(is_64bit: bool = False) -> bytes: + """Создает mock PE файл в памяти""" + dos_header = b'MZ' + b'\x00' * 58 + b'\x80\x00\x00\x00' + + pe_header = ( + b'PE\x00\x00' + + (b'\x64\x86' if is_64bit else b'\x4c\x01') + + b'\x02\x00' + + b'\x11\x22\x33\x44' + + b'\x00\x00\x00\x00' + + b'\x00\x00\x00\x00' + + b'\xe0\x00' + + b'\x01\x00' + ) + + if is_64bit: + opt_header = ( + b'\x0b\x02' + + b'\x01\x02' + + b'\x11\x00\x00\x00' * 3 + + b'\x55\x00\x00\x00' + + b'\x66\x00\x00\x00' + + b'\x88\x00\x00\x00\x00\x00\x00\x00' + + b'\x99\x00\x00\x00' * 2 + + b'\x01\x00' * 6 + + b'\xaa\x00\x00\x00' * 4 + + b'\x02\x00' + + b'\xbb\x00' + + b'\xcc\x00\x00\x00\x00\x00\x00\x00' * 4 + + b'\xdd\x00\x00\x00' * 2 + + b'\x00\x20\x00\x00' + + b'\x28\x00\x00\x00' + + b'\x00' * (128 - 8) + ) + else: + opt_header = ( + b'\x0b\x01' + + b'\x01\x02' + + b'\x11\x00\x00\x00' * 3 + + b'\x55\x00\x00\x00' + + b'\x66\x00\x00\x00' + + b'\x77\x00\x00\x00' + + b'\x88\x00\x00\x00' + + b'\x99\x00\x00\x00' * 2 + + b'\x01\x00' * 6 + + b'\xaa\x00\x00\x00' * 4 + + b'\x02\x00' + + b'\xbb\x00' + + b'\xcc\x00\x00\x00' * 4 + + b'\xdd\x00\x00\x00' * 2 + + b'\x00\x20\x00\x00' + + b'\x28\x00\x00\x00' + + b'\x00' * (128 - 8) + ) + + text_section = ( + b'.text\x00\x00\x00' + + b'\x00\x10\x00\x00' + + b'\x00\x10\x00\x00' + + b'\x00\x10\x00\x00' + + b'\x00\x10\x00\x00' + + b'\x00\x00\x00\x00' * 3 + + b'\x20\x00\x00\x60' + ) + + data_section = ( + b'.data\x00\x00\x00' + + b'\x00\x10\x00\x00' + + b'\x00\x20\x00\x00' + + b'\x00\x10\x00\x00' + + b'\x00\x20\x00\x00' + + b'\x00\x00\x00\x00' * 3 + + b'\x40\x00\x00\xc0' + ) + + import_descriptors = ( + b'\x00\x30\x00\x00' + + b'\x00\x00\x00\x00' + + b'\x00\x00\x00\x00' + + b'\x00\x40\x00\x00' + + b'\x00\x50\x00\x00' + + b'\x00\x60\x00\x00' + + b'\x00\x00\x00\x00' + + b'\x00\x00\x00\x00' + + b'\x0c\x40\x00\x00' + + b'\x00\x70\x00\x00' + + b'\x00\x00\x00\x00' * 5 + ) + + dll_names = b'kernel32.dll\x00user32.dll\x00' + iat_data = b'\x00' * 32 + + pe_data = ( + dos_header.ljust(0x80, b'\x00') + + pe_header + + opt_header + + text_section + + data_section + + b'\x00' * (0x1000 - 0x80 - len(pe_header) - len(opt_header) - len(text_section) - len(data_section)) + + import_descriptors.ljust(0x1000, b'\x00') + + dll_names.ljust(0x1000, b'\x00') + + iat_data.ljust(0x1000, b'\x00') + ) + + return pe_data + + +def test_dos_header_parsing() -> None: + """Тест парсинга DOS заголовка""" + dos_header = DOSHeader.from_bytes(TEST_DOS_HEADER_DATA) + assert dos_header.e_magic == b'MZ' + assert dos_header.e_lfanew == 0x80 + + +def test_pe_header_parsing() -> None: + """Тест парсинга PE заголовка""" + pe_header = PEHeader.from_bytes(TEST_PE_HEADER_DATA) + assert pe_header.signature == b'PE\x00\x00' + assert pe_header.machine == MachineType.IMAGE_FILE_MACHINE_I386 + assert pe_header.number_of_sections == 0x3 + assert pe_header.time_date_stamp == 0x44332211 + + +def test_optional_header_pe32_parsing() -> None: + """Тест парсинга 32-битного опционального заголовка""" + opt_header = OptionalHeaderPE32.from_bytes(TEST_OPTIONAL_HEADER32_DATA) + assert opt_header.magic == 0x10B + assert opt_header.major_linker_version == 0x1 + assert opt_header.minor_linker_version == 0x2 + assert opt_header.address_of_entry_point == 0x55 + assert opt_header.subsystem == SubsystemType.IMAGE_SUBSYSTEM_WINDOWS_GUI + + +def test_optional_header_pe64_parsing() -> None: + """Тест парсинга 64-битного опционального заголовка""" + opt_header = OptionalHeaderPE64.from_bytes(TEST_OPTIONAL_HEADER64_DATA) + assert opt_header.magic == 0x20B + assert opt_header.major_linker_version == 0x1 + assert opt_header.minor_linker_version == 0x2 + assert opt_header.address_of_entry_point == 0x55 + assert opt_header.subsystem == SubsystemType.IMAGE_SUBSYSTEM_WINDOWS_GUI + assert opt_header.image_base == 0x88 + + +def test_section_header_parsing() -> None: + """Тест парсинга заголовка секции""" + section = SectionHeader.from_bytes(TEST_SECTION_HEADER_DATA) + assert section.name == b'.text\x00\x00\x00' + assert section.virtual_size == 0x11 + assert section.virtual_address == 0x22 + assert section.characteristics == 0x60000020 + + +def test_import_descriptor_parsing() -> None: + """Тест парсинга дескриптора импорта""" + descriptor = ImportDescriptor.from_bytes(TEST_IMPORT_DESCRIPTOR_DATA) + assert descriptor.original_first_thunk == 0x11 + assert descriptor.name == 0x44 + assert not descriptor.is_null() + + null_descriptor = ImportDescriptor(0, 0, 0, 0, 0) + assert null_descriptor.is_null() + + +def test_rva_to_file_offset_conversion() -> None: + """Тест конвертации RVA в файловое смещение""" + sections = [ + SectionHeader( + name=b'.text\x00\x00\x00', + virtual_size=0x1000, + virtual_address=0x1000, + size_of_raw_data=0x800, + pointer_to_raw_data=0x400, + pointer_to_relocations=0, + pointer_to_linenumbers=0, + number_of_relocations=0, + number_of_linenumbers=0, + characteristics=0, + ), + SectionHeader( + name=b'.data\x00\x00\x00', + virtual_size=0x1000, + virtual_address=0x2000, + size_of_raw_data=0x600, + pointer_to_raw_data=0xC00, + pointer_to_relocations=0, + pointer_to_linenumbers=0, + number_of_relocations=0, + number_of_linenumbers=0, + characteristics=0, + ), + ] + + pe_file = PEFile( + dos_header=DOSHeader(e_magic=b'MZ', e_lfanew=0), + pe_header=PEHeader( + signature=b'PE\x00\x00', + machine=MachineType.IMAGE_FILE_MACHINE_I386, + number_of_sections=2, + time_date_stamp=0, + pointer_to_symbol_table=0, + number_of_symbols=0, + size_of_optional_header=0, + characteristics=0, + ), + optional_header=OptionalHeaderPE32( + magic=0, + major_linker_version=0, + minor_linker_version=0, + size_of_code=0, + size_of_initialized_data=0, + size_of_uninitialized_data=0, + address_of_entry_point=0, + base_of_code=0, + base_of_data=0, + image_base=0, + section_alignment=0, + file_alignment=0, + major_os_version=0, + minor_os_version=0, + major_image_version=0, + minor_image_version=0, + major_subsystem_version=0, + minor_subsystem_version=0, + win32_version_value=0, + size_of_image=0, + size_of_headers=0, + checksum=0, + subsystem=SubsystemType.IMAGE_SUBSYSTEM_WINDOWS_CUI, + dll_characteristics=0, + size_of_stack_reserve=0, + size_of_stack_commit=0, + size_of_heap_reserve=0, + size_of_heap_commit=0, + loader_flags=0, + number_of_rva_and_sizes=0, + ), + sections=sections, + ) + + assert pe_file._rva_to_file_offset(0x1000) == 0x400 + assert pe_file._rva_to_file_offset(0x100A) == 0x40A + + assert pe_file._rva_to_file_offset(0x2000) == 0xC00 + assert pe_file._rva_to_file_offset(0x200B) == 0xC0B + + assert pe_file._rva_to_file_offset(0x3000) is None + assert pe_file._rva_to_file_offset(0x500) is None + + +def test_data_directory_parsing() -> None: + """Тест парсинга DataDirectory""" + data = b'\x11\x22\x33\x44\x55\x66\x77\x88' + dd = DataDirectory.from_bytes(data) + assert dd.virtual_address == 0x44332211 + assert dd.size == 0x88776655 + + +def test_read_null_terminated_string() -> None: + """Тест чтения null-terminated строки""" + data = b'hello\x00world\x00' + assert _read_null_terminated_string(data, 0) == b'hello' + assert _read_null_terminated_string(data, 6) == b'world' + assert _read_null_terminated_string(data, 12) == b'' diff --git a/README.md b/README.md index da88733..de03043 100644 --- a/README.md +++ b/README.md @@ -1 +1,103 @@ -# pe-parser \ No newline at end of file +# PE File Parser 🔍 + +## 🚀 Установка и запуск + +## 📋 Предварительные требования +- Python 3.9+ +- Poetry или pip + +## ⚙️ Установка +```bash +git clone https://github.com/ваш-репозиторий/pe-parser.git +cd pe-parser +poetry install +``` + +## 🏃 Запуск анализатора +```bash +python parser.py путь_к_файлу.exe +``` + +## Для справки: +```bash +python parser.py --help +``` + +## 🌟 Возможности + +- 🔍 Анализ структуры PE файлов (32/64 бит) + +- 📊 Поддержка вывода в текстовом и JSON форматах + +- 🎨 Цветной вывод в терминал (с возможностью отключения) + +- 🧩 Выборочный анализ конкретных секций файла + +- 💾 Сохранение результатов в файл + +## 📊 Поддерживаемые секции PE файла +| Секция | Описание | +|:------------:|:---------------------------------------------:| +| **DOS Header** | Анализ DOS-заголовка (сигнатура "MZ") | +| **PE Header** | Основной PE-заголовок (архитектура, характеристики) | +| **Optional** | Опциональный заголовок (точка входа, база образа) | +| **Sections** | Информация о секциях исполняемого файла | +| **Imports** | Список импортируемых DLL | + +## 🛠️ Примеры использования +Базовый анализ: + +```bash +python parser.py sample.exe +``` +Анализ с выводом в JSON: + +```bash +python parser.py sample.exe --format json -o report.json +``` +Выборочный анализ секций: +"dos", "pe", "optional", "sections", "imports" +```bash +python parser.py sample.exe --sections dos pe imports +``` + +Отключение цветного вывода: + +```bash +python parser.py sample.exe --no-color +``` +## 🏗️ Структура проекта +pe-parser/ +├── parser.py # Основной скрипт анализатора +├── PE_structures.py # Структуры данных PE файла +├── pyproject.toml # Конфигурация проекта (Poetry) +├── poetry.lock # Зависимости Poetry +├── .gitignore +├── mypy.ini # Конфигурация mypy +├── .flake8 # Конфигурация flake8 +├── test_parser.py # Тесты parser.py +├── test_pe_structures.py # Тесты PE_structures.py +└── README.md # Документация +## 📌 Особенности реализации +- Полная типизация с использованием Python Type Hints + +- Поддержка как 32-битных, так и 64-битных PE файлов + +- Чтение и анализ таблицы импорта + +- Гибкая система вывода результатов + +## 👥 Автор +Самородова Александра + +Checklist: +- [x] Добавлен `.gitignore`. Убедитесь, что там есть `.venv` и `.idea` +- [x] Создано виртуальное окружение +- [x] Есть файл `requirements.txt` или `pyproject.toml`. Исключения: если у вас нет внешних зависимостей. +- [x] Настроены линтеры: `mypy` и `flake8` +- [x] Настроены форматтеры: `isort` и `black` +- [x] Написаны тесты +- [x] Написана документация к каждому методу, классу и функции +- [x] Написан красивый `README.md` (для форматирования можно использовать markdown), где есть информация о том, как проект установить и запустить, что он делает и умеет, какие фунции там есть +- [x] (Для консольных утилит) написан help +- [x] (Опционально) Есть прекоммит diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..90ba7b9 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,27 @@ +[mypy] + +python_version = 3.12 + +enable_error_code = truthy-bool, ignore-without-code + +check_untyped_defs = True + +strict_optional = True +disallow_any_generics = True +disallow_incomplete_defs = True +disallow_untyped_defs = True +no_implicit_optional = True + +warn_unused_ignores = True +warn_return_any = True +warn_no_return = True +warn_redundant_casts = True +warn_unreachable = True + +show_error_codes = True +show_column_numbers = True +pretty = True + +ignore_missing_imports = True +follow_imports = normal +follow_imports_for_stubs = True diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..0f4eddd --- /dev/null +++ b/poetry.lock @@ -0,0 +1,753 @@ +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "black" +version = "25.1.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, + {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, + {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, + {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, + {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, + {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, + {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, + {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, + {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, + {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, + {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, + {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, + {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, + {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, + {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, + {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, + {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, + {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, + {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, + {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, + {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, + {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.8.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "filelock" +version = "3.18.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] + +[[package]] +name = "flake8" +version = "7.2.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343"}, + {file = "flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.13.0,<2.14.0" +pyflakes = ">=3.3.0,<3.4.0" + +[[package]] +name = "identify" +version = "2.6.10" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25"}, + {file = "identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "isort" +version = "6.0.1" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, + {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.15.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, + {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, + {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, + {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, + {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, + {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, + {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, + {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, + {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, + {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, + {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, + {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, + {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "4.2.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pycodestyle" +version = "2.13.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"}, + {file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"}, +] + +[[package]] +name = "pydantic" +version = "2.11.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, + {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pyflakes" +version = "3.3.2" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a"}, + {file = "pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"}, +] + +[[package]] +name = "pytest" +version = "8.3.5" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "types-colorama" +version = "0.4.15.20240311" +description = "Typing stubs for colorama" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "types-colorama-0.4.15.20240311.tar.gz", hash = "sha256:a28e7f98d17d2b14fb9565d32388e419f4108f557a7d939a66319969b2b99c7a"}, + {file = "types_colorama-0.4.15.20240311-py3-none-any.whl", hash = "sha256:6391de60ddc0db3f147e31ecb230006a6823e81e380862ffca1e4695c13a0b8e"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, + {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "virtualenv" +version = "20.30.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, + {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.12, <3.14" +content-hash = "ddb1308af4e93e8612c006cbd93fa0eadce8296c34e6be56e493704b4de35500" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..88b7ef7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[tool.poetry] +name = "pe-analyzer" +version = "0.1.0" +description = "Анализатор PE-файлов (Portable Executable) с детализированным выводом структуры, импортов и метаданных" +authors = [ + "Samorodova Aleksandra", +] +package-mode = false + +[tool.poetry.dependencies] +python = ">=3.12, <3.14" +colorama = "^0.4.6" +pydantic = "^2.11.3" + +[tool.poetry.group.dev.dependencies] +mypy = "^1.15.0" +black = "^25.1.0" +flake8 = "^7.2.0" +isort = "^6.0.1" +pre-commit = "^4.2.0" +pytest = "^8.2.0" +pytest-cov = "^5.0.0" +types-colorama = "^0.4.0" + +[tool.black] +line-length = 120 +target-version = ['py312'] +skip-string-normalization = true + +[tool.isort] +profile = "black" +line_length = 120 +known_first_party = ["pe_analyzer"] +sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +default_section = "THIRDPARTY" +skip = [".venv", "venv", "build", "dist"] +force_sort_within_sections = true +lines_after_imports = 2 +combine_as_imports = true +include_trailing_comma = true + +[tool.flake8] +max-line-length = 120 +ignore = "E203, W503" +exclude = ".git, __pycache__, .venv" +show-source = true +statistics = true + +[tool.mypy] +python_version = "3.12" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true + +[tool.pytest.ini_options] +addopts = "--cov=./ --cov-report=term-missing" +testpaths = ["tests"] +python_files = "test_*.py" From 9297068d5ae63df7e79d59c1893acc0e7c8e8d2b Mon Sep 17 00:00:00 2001 From: SN <2005kan@mail.ru> Date: Thu, 8 May 2025 17:12:39 +0500 Subject: [PATCH 2/3] v4 --- PE_parser/parser.py | 205 +++++++++++++++++++++++++++----------------- pyproject.toml | 7 +- 2 files changed, 129 insertions(+), 83 deletions(-) diff --git a/PE_parser/parser.py b/PE_parser/parser.py index 97baa85..3ca4f4d 100644 --- a/PE_parser/parser.py +++ b/PE_parser/parser.py @@ -1,19 +1,31 @@ import argparse -from enum import Enum +import builtins +from contextlib import contextmanager +from enum import StrEnum import json -from typing import Any, Optional +from typing import Any, Callable, Iterator, cast from PE_structures import MachineType, PEFile, SubsystemType -class OutputFormat(Enum): +class OutputFormat(StrEnum): """Перечисление форматов вывода результатов анализа PE файла (text/json).""" TEXT = "text" JSON = "json" -class Color: +class SectionChoice(StrEnum): + """Перечисление доступных секций для вывода.""" + + DOS = "dos" + PE = "pe" + OPTIONAL = "optional" + SECTIONS = "sections" + IMPORTS = "imports" + + +class Color(StrEnum): """ANSI коды цветов для терминала.""" HEADER = '\033[95m' @@ -40,18 +52,23 @@ def print_field(name: str, value: Any, indent: int = 0) -> None: print(f"{indent_str}{colorize(name + ':', Color.PINK)} {value}") -def format_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> dict[str, Any]: +def format_pe_info(pe_file: PEFile, sections: list[str] | None = None) -> dict[str, Any]: """Форматирует информацию о PE файле в словарь для JSON вывода.""" result: dict[str, Any] = {} + sections_to_show = ( + {SectionChoice.DOS, SectionChoice.PE, SectionChoice.OPTIONAL, SectionChoice.SECTIONS, SectionChoice.IMPORTS} + if sections is None + else {SectionChoice(s) for s in sections} + ) - if sections is None or "dos" in sections: + if SectionChoice.DOS in sections_to_show: dos_header = { "Magic": pe_file.dos_header.e_magic.decode('ascii'), "PE Header Offset": f"0x{pe_file.dos_header.e_lfanew:X}", } result["DOS Header"] = dos_header - if sections is None or "pe" in sections: + if SectionChoice.PE in sections_to_show: pe_header = { "Signature": pe_file.pe_header.signature.decode('ascii'), "Machine": MachineType(pe_file.pe_header.machine).name, @@ -62,7 +79,7 @@ def format_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> dic } result["PE Header"] = pe_header - if sections is None or "optional" in sections: + if SectionChoice.OPTIONAL in sections_to_show: optional_header = { "Address Of Entry Point": f"0x{pe_file.optional_header.address_of_entry_point:X}", "Image Base": f"0x{pe_file.optional_header.image_base:X}", @@ -77,7 +94,7 @@ def format_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> dic } result["Optional Header"] = optional_header - if sections is None or "sections" in sections: + if SectionChoice.SECTIONS in sections_to_show: sections_list = [] for i, section in enumerate(pe_file.sections): section_info = { @@ -91,21 +108,26 @@ def format_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> dic sections_list.append(section_info) result["Sections"] = sections_list - if sections is None or "imports" in sections: + if SectionChoice.IMPORTS in sections_to_show: imports = pe_file.imported_dlls if pe_file.imported_dlls else ["No imports found"] result["Imports"] = imports return result -def print_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> None: +def print_pe_info(pe_file: PEFile, sections: list[str] | None = None) -> None: """Выводит информацию о PE файле в текстовом формате с цветным оформлением.""" - if sections is None or "dos" in sections: + sections_to_show = ( + {SectionChoice.DOS, SectionChoice.PE, SectionChoice.OPTIONAL, SectionChoice.SECTIONS, SectionChoice.IMPORTS} + if sections is None + else {SectionChoice(s) for s in sections} + ) + if SectionChoice.DOS in sections_to_show: print_section_header("DOS Header") print_field("Magic", pe_file.dos_header.e_magic.decode('ascii')) print_field("PE Header Offset", f"0x{pe_file.dos_header.e_lfanew:X}") - if sections is None or "pe" in sections: + if SectionChoice.PE in sections_to_show: print_section_header("PE Header") print_field("Signature", pe_file.pe_header.signature.decode('ascii')) print_field("Machine", MachineType(pe_file.pe_header.machine).name) @@ -114,7 +136,7 @@ def print_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> None print_field("Timestamp", f"0x{pe_file.pe_header.time_date_stamp:X}") print_field("Characteristics", f"0x{pe_file.pe_header.characteristics:X}") - if sections is None or "optional" in sections: + if SectionChoice.OPTIONAL in sections_to_show: print_section_header("Optional Header") print_field("Address Of Entry Point", f"0x{pe_file.optional_header.address_of_entry_point:X}") print_field("Image Base", f"0x{pe_file.optional_header.image_base:X}") @@ -127,7 +149,7 @@ def print_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> None print_field("Size of Headers", f"0x{pe_file.optional_header.size_of_headers:X}") print_field("Number Of Rva And Sizes", f"0x{pe_file.optional_header.number_of_rva_and_sizes:X}") - if sections is None or "sections" in sections: + if SectionChoice.SECTIONS in sections_to_show: print_section_header("Sections") for i, section in enumerate(pe_file.sections): print_field(f"Section {i + 1}", "", 2) @@ -138,7 +160,7 @@ def print_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> None print_field("Raw Pointer", f"0x{section.pointer_to_raw_data:X}", 4) print_field("Characteristics", f"0x{section.characteristics:X}", 4) - if sections is None or "imports" in sections: + if SectionChoice.IMPORTS in sections_to_show: print_section_header("Imports") if pe_file.imported_dlls: for dll in pe_file.imported_dlls: @@ -147,8 +169,51 @@ def print_pe_info(pe_file: PEFile, sections: Optional[list[str]] = None) -> None print_field("", "No imports found", 2) -def main() -> None: - """Основная функция: парсит аргументы командной строки и запускает анализ PE файла.""" +def local_colorize(text: str, color: str, end_color: str = Color.END) -> str: + """Локальная версия colorize с настраиваемыми цветами.""" + return f"{color}{text}{end_color}" + + +def local_print_section_header( + title: str, color_red: str = Color.RED, color_bold: str = Color.BOLD, end_color: str = Color.END +) -> None: + """Локальная версия print_section_header с настраиваемыми цветами.""" + print(local_colorize(f"\n=== {title} ===", color_red + color_bold, end_color)) + + +def local_print_field( + name: str, value: Any, indent: int = 0, color_pink: str = Color.PINK, end_color: str = Color.END +) -> None: + """Локальная версия print_field с настраиваемыми цветами.""" + indent_str = " " * indent + print(f"{indent_str}{local_colorize(name + ':', color_pink, end_color)} {value}") + + +@contextmanager +def override_print_functions(color_settings: dict[str, str]) -> Iterator[None]: + """Контекстный менеджер для временного переопределения функций вывода.""" + originals = { + 'colorize': globals()['colorize'], + 'print_section_header': globals()['print_section_header'], + 'print_field': globals()['print_field'], + } + + try: + globals()['colorize'] = lambda text, color: local_colorize(text, color, color_settings['end']) + globals()['print_section_header'] = lambda title: local_print_section_header( + title, color_settings['red'], color_settings['bold'], color_settings['end'] + ) + globals()['print_field'] = lambda name, value, indent=0: local_print_field( + name, value, indent, color_settings['pink'], color_settings['end'] + ) + yield + finally: + for name, func in originals.items(): + globals()[name] = func + + +def parse_arguments() -> argparse.Namespace: + """Парсит аргументы командной строки.""" parser = argparse.ArgumentParser( description="PE file parser", formatter_class=argparse.ArgumentDefaultsHelpFormatter ) @@ -166,84 +231,68 @@ def main() -> None: "-s", "--sections", nargs="+", - choices=["dos", "pe", "optional", "sections", "imports"], + choices=[s.value for s in SectionChoice], help="Select specific sections to display", ) - args = parser.parse_args() - - pe_file = PEFile.parse(args.file) - - if args.format == OutputFormat.JSON.value: - result = format_pe_info(pe_file, args.sections) - output = json.dumps(result, indent=2) - else: - - class LocalColor: - """Локальные настройки цветов для отключения оформления.""" + return parser.parse_args() - HEADER = Color.HEADER - RED = Color.RED - PINK = Color.PINK - GREEN = Color.GREEN - END = Color.END - BOLD = Color.BOLD - if args.no_color: - LocalColor.HEADER = '' - LocalColor.RED = '' - LocalColor.PINK = '' - LocalColor.GREEN = '' - LocalColor.END = '' - LocalColor.BOLD = '' +def generate_json_output(pe_file: PEFile, sections: list[str] | None) -> str: + """Генерирует вывод в формате JSON.""" + result = format_pe_info(pe_file, sections) + return json.dumps(result, indent=2) - output_lines: list[str] = [] - def capture_print(*values: Any) -> None: - """Перехватывает вывод print для сохранения в буфер.""" - output_lines.append(" ".join(str(v) for v in values)) +def generate_text_output(pe_file: PEFile, sections: list[str] | None, no_color: bool) -> str: + """Генерирует вывод в текстовом формате.""" + color_settings = { + 'header': '' if no_color else Color.HEADER, + 'red': '' if no_color else Color.RED, + 'pink': '' if no_color else Color.PINK, + 'green': '' if no_color else Color.GREEN, + 'end': '' if no_color else Color.END, + 'bold': '' if no_color else Color.BOLD, + } - import builtins + output_lines: list[str] = [] - original_print = builtins.print - builtins.print = capture_print # type: ignore[assignment] + def capture_print(*values: Any, sep: str = " ", end: str = "\n") -> None: + """Перехватывает вывод print для сохранения в буфер.""" + output_lines.append(sep.join(str(v) for v in values) + end) - original_colorize = globals()['colorize'] - original_print_section_header = globals()['print_section_header'] - original_print_field = globals()['print_field'] + original_print = builtins.print + builtins.print = cast(Callable[..., None], capture_print) - def local_colorize(text: str, color: str) -> str: - """Локальная версия colorize с настраиваемыми цветами.""" - return f"{color}{text}{LocalColor.END}" + try: + with override_print_functions(color_settings): + print_pe_info(pe_file, sections) + finally: + builtins.print = original_print - def local_print_section_header(title: str) -> None: - """Локальная версия print_section_header с настраиваемыми цветами.""" - print(local_colorize(f"\n=== {title} ===", LocalColor.RED + LocalColor.BOLD)) + return "\n".join(output_lines) - def local_print_field(name: str, value: Any, indent: int = 0) -> None: - """Локальная версия print_field с настраиваемыми цветами.""" - indent_str = " " * indent - print(f"{indent_str}{local_colorize(name + ':', LocalColor.PINK)} {value}") - globals()['colorize'] = local_colorize - globals()['print_section_header'] = local_print_section_header - globals()['print_field'] = local_print_field +def save_output(output: str, output_path: str | None) -> None: + """Сохраняет вывод в файл или выводит на экран.""" + if output_path: + with open(output_path, "w") as f: + f.write(output) + else: + print(output) - try: - print_pe_info(pe_file, args.sections) - finally: - builtins.print = original_print - globals()['colorize'] = original_colorize - globals()['print_section_header'] = original_print_section_header - globals()['print_field'] = original_print_field - output = "\n".join(output_lines) +def main() -> None: + """Основная функция: парсит аргументы командной строки и запускает анализ PE файла.""" + args = parse_arguments() + pe_file = PEFile.parse(args.file) - if args.output: - with open(args.output, "w") as f: - f.write(output) + if args.format == OutputFormat.JSON.value: + output = generate_json_output(pe_file, args.sections) else: - print(output) + output = generate_text_output(pe_file, args.sections, args.no_color) + + save_output(output, args.output) if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index 88b7ef7..e514eb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,11 +41,8 @@ include_trailing_comma = true [tool.flake8] max-line-length = 120 -ignore = "E203, W503" -exclude = ".git, __pycache__, .venv" -show-source = true -statistics = true - +extend-ignore = "E203, W503" +extend-select = "ALL" [tool.mypy] python_version = "3.12" warn_return_any = true From 0728eab0396a214ee73a2d007721d3446ed82b32 Mon Sep 17 00:00:00 2001 From: SN <2005kan@mail.ru> Date: Tue, 20 May 2025 00:14:53 +0500 Subject: [PATCH 3/3] v5 (task part 2) --- PE_parser/PE_structures.py | 404 +++++++++++++++++++++++--------- PE_parser/parser.py | 161 +++++++++---- PE_parser/test_parser.py | 77 ++++++ PE_parser/test_pe_structures.py | 79 +++++++ 4 files changed, 572 insertions(+), 149 deletions(-) diff --git a/PE_parser/PE_structures.py b/PE_parser/PE_structures.py index 823fcb4..daa8fa5 100644 --- a/PE_parser/PE_structures.py +++ b/PE_parser/PE_structures.py @@ -7,26 +7,26 @@ class MachineType(enum.IntEnum): """Типы машин, поддерживаемые PE файлами""" - IMAGE_FILE_MACHINE_I386 = 0x014C # 32-bit x86 - IMAGE_FILE_MACHINE_AMD64 = 0x8664 # 64-bit x86 (AMD64) - IMAGE_FILE_MACHINE_IA64 = 0x0200 # Intel Itanium + IMAGE_FILE_MACHINE_I386 = 0x014C + IMAGE_FILE_MACHINE_AMD64 = 0x8664 + IMAGE_FILE_MACHINE_IA64 = 0x0200 class SubsystemType(enum.IntEnum): """Типы подсистем, которые могут быть указаны в PE файле""" - IMAGE_SUBSYSTEM_NATIVE = 1 # Драйверы Windows - IMAGE_SUBSYSTEM_WINDOWS_GUI = 2 # Приложение с графическим интерфейсом - IMAGE_SUBSYSTEM_WINDOWS_CUI = 3 # Консольное приложение - IMAGE_SUBSYSTEM_EFI_APPLICATION = 10 # EFI приложение + IMAGE_SUBSYSTEM_NATIVE = 1 + IMAGE_SUBSYSTEM_WINDOWS_GUI = 2 + IMAGE_SUBSYSTEM_WINDOWS_CUI = 3 + IMAGE_SUBSYSTEM_EFI_APPLICATION = 10 @dataclass class DOSHeader: """Заголовок DOS, присутствующий в начале каждого PE файла""" - e_magic: bytes # Сигнатура "MZ" - e_lfanew: int # Смещение до PE заголовка + e_magic: bytes + e_lfanew: int @classmethod def from_bytes(cls, data: bytes) -> 'DOSHeader': @@ -43,14 +43,14 @@ def from_bytes(cls, data: bytes) -> 'DOSHeader': class PEHeader: """Основной PE заголовок (IMAGE_NT_HEADERS)""" - signature: bytes # Сигнатура "PE\0\0" - machine: MachineType # Архитектура процессора - number_of_sections: int # Количество секций - time_date_stamp: int # Временная метка создания - pointer_to_symbol_table: int # Указатель на таблицу символов (устарело) - number_of_symbols: int # Количество символов (устарело) - size_of_optional_header: int # Размер опционального заголовка - characteristics: int # Флаги характеристик + signature: bytes + machine: MachineType + number_of_sections: int + time_date_stamp: int + pointer_to_symbol_table: int + number_of_symbols: int + size_of_optional_header: int + characteristics: int @classmethod def from_bytes(cls, data: bytes) -> 'PEHeader': @@ -58,7 +58,6 @@ def from_bytes(cls, data: bytes) -> 'PEHeader': offset = 4 machine = MachineType(struct.unpack(" 'PEHeader': class DataDirectory: """Элемент таблицы данных (IMAGE_DATA_DIRECTORY)""" - virtual_address: int # RVA адрес данных - size: int # Размер данных + virtual_address: int + size: int @classmethod def from_bytes(cls, data: bytes) -> 'DataDirectory': @@ -101,36 +100,37 @@ def from_bytes(cls, data: bytes) -> 'DataDirectory': class OptionalHeaderPE32: """Опциональный заголовок для 32-битных PE файлов (IMAGE_OPTIONAL_HEADER32)""" - magic: int # Магическое число - major_linker_version: int # Версия линкера - minor_linker_version: int # Минорная версия линкера - size_of_code: int # Размер кода - size_of_initialized_data: int # Размер инициализированных данных - size_of_uninitialized_data: int # Размер неинициализированных данных - address_of_entry_point: int # Точка входа - base_of_code: int # Базовый адрес кода - base_of_data: int # Базовый адрес данных - image_base: int # Базовый адрес образа в памяти - section_alignment: int # Выравнивание секций в памяти - file_alignment: int # Выравнивание секций в файле - major_os_version: int # Версия ОС - minor_os_version: int # Минорная версия ОС - major_image_version: int # Версия образа - minor_image_version: int # Минорная версия образа - major_subsystem_version: int # Версия подсистемы - minor_subsystem_version: int # Минорная версия подсистемы - win32_version_value: int # Версия Win32 - size_of_image: int # Размер образа в памяти - size_of_headers: int # Размер заголовков - checksum: int # Контрольная сумма - subsystem: SubsystemType # Тип подсистемы - dll_characteristics: int # Характеристики DLL - size_of_stack_reserve: int # Размер стека - size_of_stack_commit: int # Размер выделяемого стека - size_of_heap_reserve: int # Размер кучи - size_of_heap_commit: int # Размер выделяемой кучи - loader_flags: int # Флаги загрузчика - number_of_rva_and_sizes: int # Количество элементов в таблице данных + magic: int + major_linker_version: int + minor_linker_version: int + size_of_code: int + size_of_initialized_data: int + size_of_uninitialized_data: int + address_of_entry_point: int + base_of_code: int + base_of_data: int + image_base: int + section_alignment: int + file_alignment: int + major_os_version: int + minor_os_version: int + major_image_version: int + minor_image_version: int + major_subsystem_version: int + minor_subsystem_version: int + win32_version_value: int + size_of_image: int + size_of_headers: int + checksum: int + subsystem: SubsystemType + dll_characteristics: int + size_of_stack_reserve: int + size_of_stack_commit: int + size_of_heap_reserve: int + size_of_heap_commit: int + loader_flags: int + number_of_rva_and_sizes: int + data_directories: list[DataDirectory] = field(default_factory=list) @classmethod def from_bytes(cls, data: bytes) -> 'OptionalHeaderPE32': @@ -138,7 +138,7 @@ def from_bytes(cls, data: bytes) -> 'OptionalHeaderPE32': fmt = " 'OptionalHeaderPE32': number_of_rva_and_sizes=unpacked[29], ) + obj.data_directories = [] + for i in range(obj.number_of_rva_and_sizes): + offset = 96 + i * 8 + if offset + 8 <= len(data): + obj.data_directories.append(DataDirectory.from_bytes(data[offset : offset + 8])) + return obj + @dataclass class OptionalHeaderPE64: """Опциональный заголовок для 64-битных PE файлов (IMAGE_OPTIONAL_HEADER64)""" - magic: int # Магическое число - major_linker_version: int # Версия линкера - minor_linker_version: int # Минорная версия линкера - size_of_code: int # Размер кода - size_of_initialized_data: int # Размер инициализированных данных - size_of_uninitialized_data: int # Размер неинициализированных данных - address_of_entry_point: int # Точка входа - base_of_code: int # Базовый адрес кода - image_base: int # Базовый адрес образа в памяти - section_alignment: int # Выравнивание секций в памяти - file_alignment: int # Выравнивание секций в файле - major_os_version: int # Версия ОС - minor_os_version: int # Минорная версия ОС - major_image_version: int # Версия образа - minor_image_version: int # Минорная версия образа - major_subsystem_version: int # Версия подсистемы - minor_subsystem_version: int # Минорная версия подсистемы - win32_version_value: int # Версия Win32 - size_of_image: int # Размер образа в памяти - size_of_headers: int # Размер заголовков - checksum: int # Контрольная сумма - subsystem: SubsystemType # Тип подсистемы - dll_characteristics: int # Характеристики DLL - size_of_stack_reserve: int # Размер стека - size_of_stack_commit: int # Размер выделяемого стека - size_of_heap_reserve: int # Размер кучи - size_of_heap_commit: int # Размер выделяемой кучи - loader_flags: int # Флаги загрузчика - number_of_rva_and_sizes: int # Количество элементов в таблице данных + magic: int + major_linker_version: int + minor_linker_version: int + size_of_code: int + size_of_initialized_data: int + size_of_uninitialized_data: int + address_of_entry_point: int + base_of_code: int + image_base: int + section_alignment: int + file_alignment: int + major_os_version: int + minor_os_version: int + major_image_version: int + minor_image_version: int + major_subsystem_version: int + minor_subsystem_version: int + win32_version_value: int + size_of_image: int + size_of_headers: int + checksum: int + subsystem: SubsystemType + dll_characteristics: int + size_of_stack_reserve: int + size_of_stack_commit: int + size_of_heap_reserve: int + size_of_heap_commit: int + loader_flags: int + number_of_rva_and_sizes: int + data_directories: list[DataDirectory] = field(default_factory=list) @classmethod def from_bytes(cls, data: bytes) -> 'OptionalHeaderPE64': @@ -212,7 +220,7 @@ def from_bytes(cls, data: bytes) -> 'OptionalHeaderPE64': fmt = " 'OptionalHeaderPE64': number_of_rva_and_sizes=unpacked[28], ) + obj.data_directories = [] + for i in range(obj.number_of_rva_and_sizes): + offset = 112 + i * 8 + if offset + 8 <= len(data): + obj.data_directories.append(DataDirectory.from_bytes(data[offset : offset + 8])) + return obj + @dataclass class SectionHeader: """Заголовок секции PE файла (IMAGE_SECTION_HEADER)""" - name: bytes # Имя секции - virtual_size: int # Размер в памяти - virtual_address: int # Адрес в памяти (RVA) - size_of_raw_data: int # Размер в файле - pointer_to_raw_data: int # Смещение в файле - pointer_to_relocations: int # Смещение до релокаций - pointer_to_linenumbers: int # Смещение до номеров строк - number_of_relocations: int # Количество релокаций - number_of_linenumbers: int # Количество номеров строк - characteristics: int # Характеристики секции + name: bytes + virtual_size: int + virtual_address: int + size_of_raw_data: int + pointer_to_raw_data: int + pointer_to_relocations: int + pointer_to_linenumbers: int + number_of_relocations: int + number_of_linenumbers: int + characteristics: int @classmethod def from_bytes(cls, data: bytes) -> 'SectionHeader': @@ -310,6 +325,58 @@ def is_null(self) -> bool: ) +@dataclass +class ExportDirectory: + """Директория экспорта (IMAGE_EXPORT_DIRECTORY)""" + + characteristics: int + time_date_stamp: int + major_version: int + minor_version: int + name_rva: int + ordinal_base: int + number_of_functions: int + number_of_names: int + address_of_functions: int + address_of_names: int + address_of_name_ordinals: int + + @classmethod + def from_bytes(cls, data: bytes) -> 'ExportDirectory': + """Создает ExportDirectory из байтовой строки""" + if len(data) < 40: + raise ValueError(f"Not enough data for ExportDirectory (need 40 bytes, got {len(data)})") + + try: + fmt = "<2I2H7I" + unpacked = struct.unpack(fmt, data[:40]) + + return cls( + characteristics=unpacked[0], + time_date_stamp=unpacked[1], + major_version=unpacked[2], + minor_version=unpacked[3], + name_rva=unpacked[4], + ordinal_base=unpacked[5], + number_of_functions=unpacked[6], + number_of_names=unpacked[7], + address_of_functions=unpacked[8], + address_of_names=unpacked[9], + address_of_name_ordinals=unpacked[10], + ) + except struct.error as e: + raise ValueError(f"Failed to unpack export directory data: {e}") + + +@dataclass +class ExportFunction: + """Экспортируемая функция""" + + name: str + ordinal: int + rva: int + + def _read_null_terminated_string(data: bytes, offset: int) -> bytes: """Читает нуль-терминированную строку из данных (приватная функция)""" end = offset @@ -318,21 +385,140 @@ def _read_null_terminated_string(data: bytes, offset: int) -> bytes: return data[offset:end] +def _validate_export_directory_entry(entry: DataDirectory) -> bool: + """Проверяет валидность записи директории экспорта""" + if entry.virtual_address == 0 or entry.size == 0: + print("No export directory present") + return False + return True + + +def _validate_export_data_size(data: bytes, export_offset: int) -> bool: + """Проверяет достаточно ли данных для экспортной директории""" + if export_offset + 40 > len(data): + print(f"Error: Not enough data for export directory (need 40 bytes at offset 0x{export_offset:X})") + return False + return True + + +def _parse_export_directory(data: bytes, export_offset: int) -> ExportDirectory: + """Парсит данные экспортной директории""" + export_data = data[export_offset : export_offset + 40] + return ExportDirectory.from_bytes(export_data) + + @dataclass class PEFile: """Основной класс для представления PE файла""" - dos_header: DOSHeader # DOS заголовок - pe_header: PEHeader # PE заголовок - optional_header: Union[OptionalHeaderPE32, OptionalHeaderPE64] # Опциональный заголовок - sections: list[SectionHeader] # Список заголовков секций - imported_dlls: list[str] = field(default_factory=list) # Список импортируемых DLL + dos_header: DOSHeader + pe_header: PEHeader + optional_header: Union[OptionalHeaderPE32, OptionalHeaderPE64] + sections: list[SectionHeader] + imported_dlls: list[str] = field(default_factory=list) + exported_functions: list[ExportFunction] = field(default_factory=list) + dependencies: list[str] = field(default_factory=list) + + def _read_export_directory(self, data: bytes) -> None: + """Считывает каталог экспорта из данных PE-файла с надлежащей обработкой ошибок""" + if not self._validate_optional_header(): + return + + export_dir_entry = self.optional_header.data_directories[0] + if not _validate_export_directory_entry(export_dir_entry): + return + + export_offset = self._get_export_offset(data, export_dir_entry.virtual_address) + if export_offset is None: + return + + if not _validate_export_data_size(data, export_offset): + return + + try: + export_dir = _parse_export_directory(data, export_offset) + except Exception as e: + print(f"Failed to parse export directory: {e}") + return + + self._process_export_functions(data, export_dir) + + def _validate_optional_header(self) -> bool: + """Проверяет наличие и валидность optional header""" + if not hasattr(self.optional_header, 'data_directories'): + print("Optional header has no data_directories attribute") + return False + if len(self.optional_header.data_directories) < 1: + print("Not enough data directories (need at least 1 for exports)") + return False + return True + + def _get_export_offset(self, data: bytes, export_rva: int) -> int | None: + """Получает смещение экспортной директории""" + export_offset = self._rva_to_file_offset(export_rva) + if export_offset is None: + print(f"Failed to convert RVA 0x{export_rva:X} to file offset") + return export_offset + + def _process_export_functions(self, data: bytes, export_dir: ExportDirectory) -> None: + """Обрабатывает экспортированные функции""" + names_offset = self._rva_to_file_offset(export_dir.address_of_names) + ordinals_offset = self._rva_to_file_offset(export_dir.address_of_name_ordinals) + functions_offset = self._rva_to_file_offset(export_dir.address_of_functions) + + if names_offset is None or ordinals_offset is None or functions_offset is None: + return + + for i in range(export_dir.number_of_names): + self._process_single_export_function(data, export_dir, i, names_offset, ordinals_offset, functions_offset) + + def _process_single_export_function( + self, + data: bytes, + export_dir: ExportDirectory, + index: int, + names_offset: int, + ordinals_offset: int, + functions_offset: int, + ) -> None: + """Обрабатывает одну экспортированную функцию""" + current_names_offset = names_offset + index * 4 + current_ordinals_offset = ordinals_offset + index * 2 + + if current_names_offset + 4 > len(data) or current_ordinals_offset + 2 > len(data): + return + + name_rva = struct.unpack(" len(data): + return + + func_rva = struct.unpack(" None: + """Анализирует зависимости (приватный метод)""" + self.dependencies = sorted(list(set(self.imported_dlls))) @classmethod def parse(cls, file_path: str) -> 'PEFile': - """Парсит PE файл и возвращает объект PEFile""" with open(file_path, "rb") as f: data = f.read() + dos_header = DOSHeader.from_bytes(data) pe_offset = dos_header.e_lfanew @@ -342,14 +528,16 @@ def parse(cls, file_path: str) -> 'PEFile': magic = struct.unpack(" 'PEFile': ) pe_file._read_imported_dlls(data) + pe_file._read_export_directory(data) + pe_file._find_dependencies() return pe_file def _read_imported_dlls(self, data: bytes) -> None: @@ -418,8 +608,12 @@ def _read_imported_dlls(self, data: bytes) -> None: import_offset += 20 def _rva_to_file_offset(self, rva: int) -> int | None: - """Конвертирует RVA в смещение в файле (приватный метод)""" - for section in self.sections: - if section.virtual_address <= rva < section.virtual_address + section.virtual_size: - return rva - section.virtual_address + section.pointer_to_raw_data + """Конвертирует RVA в смещение в файле с подробной отладкой""" + for i, section in enumerate(self.sections): + section_start = section.virtual_address + section_end = section_start + max(section.virtual_size, section.size_of_raw_data) + + if section_start <= rva < section_end: + offset = rva - section.virtual_address + section.pointer_to_raw_data + return offset return None diff --git a/PE_parser/parser.py b/PE_parser/parser.py index 3ca4f4d..f1dda19 100644 --- a/PE_parser/parser.py +++ b/PE_parser/parser.py @@ -23,6 +23,8 @@ class SectionChoice(StrEnum): OPTIONAL = "optional" SECTIONS = "sections" IMPORTS = "imports" + EXPORTS = "exports" + DEPENDENCIES = "dependencies" class Color(StrEnum): @@ -56,7 +58,15 @@ def format_pe_info(pe_file: PEFile, sections: list[str] | None = None) -> dict[s """Форматирует информацию о PE файле в словарь для JSON вывода.""" result: dict[str, Any] = {} sections_to_show = ( - {SectionChoice.DOS, SectionChoice.PE, SectionChoice.OPTIONAL, SectionChoice.SECTIONS, SectionChoice.IMPORTS} + { + SectionChoice.DOS, + SectionChoice.PE, + SectionChoice.OPTIONAL, + SectionChoice.SECTIONS, + SectionChoice.IMPORTS, + SectionChoice.EXPORTS, + SectionChoice.DEPENDENCIES, + } if sections is None else {SectionChoice(s) for s in sections} ) @@ -112,61 +122,126 @@ def format_pe_info(pe_file: PEFile, sections: list[str] | None = None) -> dict[s imports = pe_file.imported_dlls if pe_file.imported_dlls else ["No imports found"] result["Imports"] = imports + if SectionChoice.EXPORTS in sections_to_show: + if pe_file.exported_functions: + result["Exports"] = { + "functions": [{"name": func.name, "rva": f"0x{func.rva:X}"} for func in pe_file.exported_functions] + } + else: + result["Exports"] = "No exports found" + + if SectionChoice.DEPENDENCIES in sections_to_show: + result["Dependencies"] = pe_file.dependencies if pe_file.dependencies else "No dependencies found" + return result +def _print_dos_header(pe_file: PEFile) -> None: + print_section_header("DOS Header") + print_field("Magic", pe_file.dos_header.e_magic.decode('ascii')) + print_field("PE Header Offset", f"0x{pe_file.dos_header.e_lfanew:X}") + + +def _print_pe_header(pe_file: PEFile) -> None: + print_section_header("PE Header") + print_field("Signature", pe_file.pe_header.signature.decode('ascii')) + print_field("Machine", MachineType(pe_file.pe_header.machine).name) + print_field("Number of Sections", pe_file.pe_header.number_of_sections) + print_field("Size Of Optional Header", pe_file.pe_header.size_of_optional_header) + print_field("Timestamp", f"0x{pe_file.pe_header.time_date_stamp:X}") + print_field("Characteristics", f"0x{pe_file.pe_header.characteristics:X}") + + +def _print_optional_header(pe_file: PEFile) -> None: + print_section_header("Optional Header") + print_field("Address Of Entry Point", f"0x{pe_file.optional_header.address_of_entry_point:X}") + print_field("Image Base", f"0x{pe_file.optional_header.image_base:X}") + print_field("Section Alignment", f"0x{pe_file.optional_header.section_alignment:X}") + print_field("File Alignment", f"0x{pe_file.optional_header.file_alignment:X}") + print_field("Major Subsystem Version", f"0x{pe_file.optional_header.major_subsystem_version:X}") + print_field("Minor Subsystem Version", f"0x{pe_file.optional_header.minor_subsystem_version:X}") + print_field("Subsystem", SubsystemType(pe_file.optional_header.subsystem).name) + print_field("Size of Image", f"0x{pe_file.optional_header.size_of_image:X}") + print_field("Size of Headers", f"0x{pe_file.optional_header.size_of_headers:X}") + print_field("Number Of Rva And Sizes", f"0x{pe_file.optional_header.number_of_rva_and_sizes:X}") + + +def _print_sections(pe_file: PEFile) -> None: + print_section_header("Sections") + for i, section in enumerate(pe_file.sections): + print_field(f"Section {i + 1}", "", 2) + print_field("Name", section.name.decode('ascii').strip(), 4) + print_field("Virtual Size", f"0x{section.virtual_size:X}", 4) + print_field("Virtual Address", f"0x{section.virtual_address:X}", 4) + print_field("Raw Size", f"0x{section.size_of_raw_data:X}", 4) + print_field("Raw Pointer", f"0x{section.pointer_to_raw_data:X}", 4) + print_field("Characteristics", f"0x{section.characteristics:X}", 4) + + +def _print_imports(pe_file: PEFile) -> None: + print_section_header("Imports") + if pe_file.imported_dlls: + for dll in pe_file.imported_dlls: + print_field(" -", dll, 2) + else: + print_field("", "No imports found", 2) + + +def _print_exports(pe_file: PEFile) -> None: + print_section_header("Exports") + if pe_file.exported_functions: + exports_per_line = 5 + exports_lines = [ + pe_file.exported_functions[i : i + exports_per_line] + for i in range(0, len(pe_file.exported_functions), exports_per_line) + ] + + for line in exports_lines: + functions_str = ", ".join(f"{func.name} (0x{func.rva:X})" for func in line) + print_field("", functions_str, 4) + else: + print_field("", "No exports found", 2) + + +def _print_dependencies(pe_file: PEFile) -> None: + print_section_header("Dependencies") + if pe_file.dependencies: + for dep in pe_file.dependencies: + print_field(" -", dep, 2) + else: + print_field("", "No dependencies found", 2) + + def print_pe_info(pe_file: PEFile, sections: list[str] | None = None) -> None: """Выводит информацию о PE файле в текстовом формате с цветным оформлением.""" sections_to_show = ( - {SectionChoice.DOS, SectionChoice.PE, SectionChoice.OPTIONAL, SectionChoice.SECTIONS, SectionChoice.IMPORTS} + { + SectionChoice.DOS, + SectionChoice.PE, + SectionChoice.OPTIONAL, + SectionChoice.SECTIONS, + SectionChoice.IMPORTS, + SectionChoice.EXPORTS, + SectionChoice.DEPENDENCIES, + } if sections is None else {SectionChoice(s) for s in sections} ) - if SectionChoice.DOS in sections_to_show: - print_section_header("DOS Header") - print_field("Magic", pe_file.dos_header.e_magic.decode('ascii')) - print_field("PE Header Offset", f"0x{pe_file.dos_header.e_lfanew:X}") + if SectionChoice.DOS in sections_to_show: + _print_dos_header(pe_file) if SectionChoice.PE in sections_to_show: - print_section_header("PE Header") - print_field("Signature", pe_file.pe_header.signature.decode('ascii')) - print_field("Machine", MachineType(pe_file.pe_header.machine).name) - print_field("Number of Sections", pe_file.pe_header.number_of_sections) - print_field("Size Of Optional Header", pe_file.pe_header.size_of_optional_header) - print_field("Timestamp", f"0x{pe_file.pe_header.time_date_stamp:X}") - print_field("Characteristics", f"0x{pe_file.pe_header.characteristics:X}") - + _print_pe_header(pe_file) if SectionChoice.OPTIONAL in sections_to_show: - print_section_header("Optional Header") - print_field("Address Of Entry Point", f"0x{pe_file.optional_header.address_of_entry_point:X}") - print_field("Image Base", f"0x{pe_file.optional_header.image_base:X}") - print_field("Section Alignment", f"0x{pe_file.optional_header.section_alignment:X}") - print_field("File Alignment", f"0x{pe_file.optional_header.file_alignment:X}") - print_field("Major Subsystem Version", f"0x{pe_file.optional_header.major_subsystem_version:X}") - print_field("Minor Subsystem Version", f"0x{pe_file.optional_header.minor_subsystem_version:X}") - print_field("Subsystem", SubsystemType(pe_file.optional_header.subsystem).name) - print_field("Size of Image", f"0x{pe_file.optional_header.size_of_image:X}") - print_field("Size of Headers", f"0x{pe_file.optional_header.size_of_headers:X}") - print_field("Number Of Rva And Sizes", f"0x{pe_file.optional_header.number_of_rva_and_sizes:X}") - + _print_optional_header(pe_file) if SectionChoice.SECTIONS in sections_to_show: - print_section_header("Sections") - for i, section in enumerate(pe_file.sections): - print_field(f"Section {i + 1}", "", 2) - print_field("Name", section.name.decode('ascii').strip(), 4) - print_field("Virtual Size", f"0x{section.virtual_size:X}", 4) - print_field("Virtual Address", f"0x{section.virtual_address:X}", 4) - print_field("Raw Size", f"0x{section.size_of_raw_data:X}", 4) - print_field("Raw Pointer", f"0x{section.pointer_to_raw_data:X}", 4) - print_field("Characteristics", f"0x{section.characteristics:X}", 4) - + _print_sections(pe_file) if SectionChoice.IMPORTS in sections_to_show: - print_section_header("Imports") - if pe_file.imported_dlls: - for dll in pe_file.imported_dlls: - print_field(" -", dll, 2) - else: - print_field("", "No imports found", 2) + _print_imports(pe_file) + if SectionChoice.EXPORTS in sections_to_show: + _print_exports(pe_file) + if SectionChoice.DEPENDENCIES in sections_to_show: + _print_dependencies(pe_file) def local_colorize(text: str, color: str, end_color: str = Color.END) -> str: @@ -234,7 +309,6 @@ def parse_arguments() -> argparse.Namespace: choices=[s.value for s in SectionChoice], help="Select specific sections to display", ) - return parser.parse_args() @@ -291,7 +365,6 @@ def main() -> None: output = generate_json_output(pe_file, args.sections) else: output = generate_text_output(pe_file, args.sections, args.no_color) - save_output(output, args.output) diff --git a/PE_parser/test_parser.py b/PE_parser/test_parser.py index f0a5f0f..abb4327 100644 --- a/PE_parser/test_parser.py +++ b/PE_parser/test_parser.py @@ -160,3 +160,80 @@ def sample_pe_object() -> PEFile: SectionHeader(b'.rsrc\x00\x00\x00', 0x1000, 0x3000, 0x200, 0x800, 0, 0, 0, 0, 0x40000040), ] return PEFile(dos_header, pe_header, optional_header, sections, ["KERNEL32.DLL", "USER32.DLL"]) + + +def test_generate_json_output(sample_pe_object: PEFile) -> None: + """Тестирует генерацию JSON вывода.""" + from parser import generate_json_output + + json_output = generate_json_output(sample_pe_object, None) + data = json.loads(json_output) + assert isinstance(data, dict) + assert "DOS Header" in data + assert "PE Header" in data + + +def test_generate_text_output(sample_pe_object: PEFile) -> None: + """Тестирует генерацию текстового вывода.""" + from parser import generate_text_output + + text_output = generate_text_output(sample_pe_object, None, False) + assert "DOS Header" in text_output + assert "PE Header" in text_output + + +def test_save_output_to_file(tmp_path: Any) -> None: + """Тестирует сохранение вывода в файл.""" + from parser import save_output + + output_file = tmp_path / "output.txt" + test_content = "test output" + save_output(test_content, str(output_file)) + assert output_file.read_text() == test_content + + +def test_save_output_to_stdout(capsys: CaptureFixture[str]) -> None: + """Тестирует вывод в stdout.""" + from parser import save_output + + test_content = "test output" + save_output(test_content, None) + captured = capsys.readouterr() + assert captured.out.strip() == test_content + + +def test_local_colorize() -> None: + """Тестирует локальную версию colorize с настраиваемыми цветами.""" + from parser import local_colorize + + colored = local_colorize("test", "\033[91m", "\033[0m") + assert colored.startswith('\033[91m') + assert colored.endswith('\033[0m') + + +def test_override_print_functions(capsys: CaptureFixture[str]) -> None: + """Тестирует контекстный менеджер override_print_functions.""" + from parser import override_print_functions, print_field + + original_print_field = print_field + color_settings = {'red': '\033[91m', 'pink': '\033[95m', 'green': '\033[92m', 'end': '\033[0m', 'bold': '\033[1m'} + + with override_print_functions(color_settings): + print_field("Test", "value") + captured = capsys.readouterr() + assert "Test:" in captured.out + assert "value" in captured.out + + assert print_field == original_print_field + + +@patch('parser.PEFile.parse') +def test_main_with_sections_arg(mock_parse: Any, sample_pe_object: PEFile, capsys: CaptureFixture[str]) -> None: + """Тестирует запуск main с аргументом выбора секций.""" + mock_parse.return_value = sample_pe_object + with patch('sys.argv', ['parser.py', 'test.exe', '-s', 'pe', 'optional']): + main() + captured = capsys.readouterr() + assert "PE Header" in captured.out + assert "Optional Header" in captured.out + assert "DOS Header" not in captured.out diff --git a/PE_parser/test_pe_structures.py b/PE_parser/test_pe_structures.py index ae0dfa4..56236a1 100644 --- a/PE_parser/test_pe_structures.py +++ b/PE_parser/test_pe_structures.py @@ -1,3 +1,5 @@ +from typing import Any + from PE_structures import ( DataDirectory, DOSHeader, @@ -324,3 +326,80 @@ def test_read_null_terminated_string() -> None: assert _read_null_terminated_string(data, 0) == b'hello' assert _read_null_terminated_string(data, 6) == b'world' assert _read_null_terminated_string(data, 12) == b'' + + +def test_pe_file_parsing_32bit(tmp_path: Any) -> None: + """Тест парсинга 32-битного PE файла.""" + pe_data = create_mock_pe_file(is_64bit=False) + test_file = tmp_path / "test32.exe" + test_file.write_bytes(pe_data) + pe_file = PEFile.parse(str(test_file)) + + assert pe_file.dos_header.e_magic == b'MZ' + assert isinstance(pe_file.optional_header, OptionalHeaderPE32) + assert len(pe_file.sections) == 2 + + +def test_pe_file_parsing_64bit(tmp_path: Any) -> None: + """Тест парсинга 64-битного PE файла.""" + pe_data = create_mock_pe_file(is_64bit=True) + test_file = tmp_path / "test64.exe" + test_file.write_bytes(pe_data) + pe_file = PEFile.parse(str(test_file)) + + assert pe_file.dos_header.e_magic == b'MZ' + assert isinstance(pe_file.optional_header, OptionalHeaderPE64) + assert len(pe_file.sections) == 2 + + +def test_pe_file_export_parsing(tmp_path: Any) -> None: + """Тест парсинга экспортируемых функций.""" + pe_data = create_mock_pe_file(is_64bit=False) + test_file = tmp_path / "test_export.exe" + test_file.write_bytes(pe_data) + pe_file = PEFile.parse(str(test_file)) + pe_file._read_export_directory(test_file.read_bytes()) + + +def test_pe_file_dependencies() -> None: + """Тест поиска зависимостей.""" + pe_file = PEFile( + dos_header=DOSHeader(b'MZ', 0), + pe_header=PEHeader(b'PE\x00\x00', MachineType.IMAGE_FILE_MACHINE_I386, 0, 0, 0, 0, 0, 0), + optional_header=OptionalHeaderPE32( + magic=0, + major_linker_version=0, + minor_linker_version=0, + size_of_code=0, + size_of_initialized_data=0, + size_of_uninitialized_data=0, + address_of_entry_point=0, + base_of_code=0, + base_of_data=0, + image_base=0, + section_alignment=0, + file_alignment=0, + major_os_version=0, + minor_os_version=0, + major_image_version=0, + minor_image_version=0, + major_subsystem_version=0, + minor_subsystem_version=0, + win32_version_value=0, + size_of_image=0, + size_of_headers=0, + checksum=0, + subsystem=SubsystemType.IMAGE_SUBSYSTEM_WINDOWS_CUI, + dll_characteristics=0, + size_of_stack_reserve=0, + size_of_stack_commit=0, + size_of_heap_reserve=0, + size_of_heap_commit=0, + loader_flags=0, + number_of_rva_and_sizes=0, + ), + sections=[], + imported_dlls=["KERNEL32.DLL", "USER32.DLL", "KERNEL32.DLL"], + ) + pe_file._find_dependencies() + assert pe_file.dependencies == ["KERNEL32.DLL", "USER32.DLL"]