#!/usr/bin/env python3 """Extract exact NE segments, entry points, modules, and relocation sites.""" from __future__ import annotations import argparse import csv import struct from dataclasses import dataclass from pathlib import Path ROOT = Path(__file__).resolve().parent.parent SOURCE_TYPE_NAMES = { 0: "low-byte", 2: "selector-16", 3: "far-pointer-32", 5: "offset-16", 11: "offset-32", 13: "pointer-48", } TARGET_TYPE_NAMES = { 0: "internal", 1: "import-ordinal", 2: "import-name", 3: "os-fixup", } @dataclass(frozen=True) class Segment: number: int file_offset: int file_length: int flags: int allocation_size: int @property def selector(self) -> int: return 0x1000 + (self.number - 1) * 8 def address(self, offset: int) -> str: return f"{self.selector:04x}:{offset:04x}" def u16(data: bytes, offset: int) -> int: return struct.unpack_from(" str: length = data[offset] return data[offset + 1 : offset + 1 + length].decode("latin-1") def parse_segments(data: bytes, ne_offset: int) -> list[Segment]: count = u16(data, ne_offset + 0x1C) table = ne_offset + u16(data, ne_offset + 0x22) shift = u16(data, ne_offset + 0x32) segments = [] for index in range(count): sector, length, flags, allocation = struct.unpack_from( " dict[int, tuple[int, int, int, str]]: table = ne_offset + u16(data, ne_offset + 0x04) end = table + u16(data, ne_offset + 0x06) ordinal = 1 entries = {} cursor = table while cursor < end: count = data[cursor] segment_indicator = data[cursor + 1] cursor += 2 if count == 0: break if segment_indicator == 0: ordinal += count continue for _ in range(count): flags = data[cursor] if segment_indicator == 0xFF: int3f = u16(data, cursor + 1) segment = data[cursor + 3] offset = u16(data, cursor + 4) kind = f"movable-int3f-{int3f:04x}" cursor += 6 else: segment = segment_indicator offset = u16(data, cursor + 1) kind = "fixed" cursor += 3 entries[ordinal] = (segment, offset, flags, kind) ordinal += 1 return entries def parse_modules(data: bytes, ne_offset: int) -> list[str]: count = u16(data, ne_offset + 0x1E) module_table = ne_offset + u16(data, ne_offset + 0x28) import_table = ne_offset + u16(data, ne_offset + 0x2A) return [ pascal_string(data, import_table + u16(data, module_table + index * 2)) for index in range(count) ] def relocation_sites( data: bytes, segment: Segment, first: int, additive: bool ) -> list[int]: if additive: return [first] sites = [] current = first seen = set() while current != 0xFFFF: if current in seen: raise ValueError( f"segment {segment.number}: cyclic relocation chain at {current:04x}" ) if current + 2 > segment.file_length: raise ValueError( f"segment {segment.number}: relocation source outside file image: {current:04x}" ) seen.add(current) sites.append(current) current = u16(data, segment.file_offset + current) return sites def write_tsv( path: Path, columns: tuple[str, ...], rows: list[dict[str, object]] ) -> None: with path.open("w", newline="", encoding="utf-8") as handle: writer = csv.DictWriter( handle, fieldnames=columns, delimiter="\t", lineterminator="\n" ) writer.writeheader() writer.writerows(rows) def extract(source: Path, output_dir: Path) -> None: data = source.read_bytes() ne_offset = struct.unpack_from(" int: parser = argparse.ArgumentParser() parser.add_argument("source", nargs="?", type=Path, default=ROOT / "TDKPIN.EXE") parser.add_argument("--output-dir", type=Path, default=ROOT) args = parser.parse_args() extract(args.source.resolve(), args.output_dir.resolve()) return 0 if __name__ == "__main__": raise SystemExit(main())