#!/usr/bin/env python3 """Создаёт payload и проверяет точные FAT16-метаданные после F5.""" from __future__ import annotations import datetime import os import pathlib import struct import sys EXPECTED_ATTR = 0x27 # readonly + hidden + system + archive EXPECTED_DATE = ((2001 - 1980) << 9) | (2 << 5) | 3 EXPECTED_TIME = (4 << 11) | (5 << 5) | (6 // 2) PAYLOAD = bytes(((index * 43 + 17) & 0xFF) for index in range(4097)) def create(root: pathlib.Path) -> None: source = root / "MSRC" target = root / "MDST" source.mkdir(parents=True) target.mkdir(parents=True) path = source / "META.BIN" path.write_bytes(PAYLOAD) timestamp = datetime.datetime(2001, 2, 3, 4, 5, 6).timestamp() os.utime(path, (timestamp, timestamp)) class Fat16: def __init__(self, raw: bytes) -> None: self.raw = raw part = struct.unpack_from(" int: return self.data + (cluster - 2) * self.cluster_size def next_cluster(self, cluster: int) -> int: return struct.unpack_from(" bytes: if cluster is None: return self.raw[self.root:self.root + self.root_entries * 32] result = bytearray() while 2 <= cluster < 0xFFF8: offset = self.cluster_offset(cluster) result += self.raw[offset:offset + self.cluster_size] cluster = self.next_cluster(cluster) return bytes(result) @staticmethod def short_name(name: str) -> bytes: stem, dot, ext = name.upper().partition(".") if len(stem) > 8 or len(ext) > 3: raise ValueError(f"not an 8.3 name: {name}") return stem.ljust(8).encode("ascii") + ext.ljust(3).encode("ascii") def find(self, directory: int | None, name: str) -> bytes | None: wanted = self.short_name(name) data = self.directory_bytes(directory) for offset in range(0, len(data), 32): entry = data[offset:offset + 32] if entry[0] == 0x00: break if entry[0] == 0xE5 or entry[11] == 0x0F: continue if entry[:11] == wanted: return entry return None @staticmethod def cluster(entry: bytes) -> int: return struct.unpack_from(" bytes: remaining = struct.unpack_from(" tuple[int, int, int]: return ( entry[11], struct.unpack_from(" tuple[Fat16, bytes, bytes | None]: fat = Fat16(raw_path.read_bytes()) source_dir = fat.find(None, "MSRC") target_dir = fat.find(None, "MDST") if source_dir is None or target_dir is None: raise RuntimeError("MSRC/MDST отсутствуют в root FAT") source = fat.find(fat.cluster(source_dir), "META.BIN") target = fat.find(fat.cluster(target_dir), "META.BIN") if source is None: raise RuntimeError("MSRC/META.BIN отсутствует") return fat, source, target def verify_source(raw_path: pathlib.Path) -> None: fat, source, target = records(raw_path) failures: list[str] = [] if target is not None: failures.append("MDST не пуст до MAME") if metadata(source) != (EXPECTED_ATTR, EXPECTED_DATE, EXPECTED_TIME): failures.append(f"неверные source metadata: {metadata(source)!r}") if fat.file_bytes(source) != PAYLOAD: failures.append("source payload повреждён") if failures: for failure in failures: print(f"FAIL: {failure}") raise SystemExit(1) print("PASS: source FAT metadata prepared exactly.") def verify(raw_path: pathlib.Path) -> None: fat, source, target = records(raw_path) failures: list[str] = [] if target is None: failures.append("MDST/META.BIN не создан") else: if metadata(target) != metadata(source): failures.append( f"metadata differ: src={metadata(source)!r} dst={metadata(target)!r}" ) if fat.file_bytes(target) != PAYLOAD: failures.append("target payload не совпадает") if metadata(source) != (EXPECTED_ATTR, EXPECTED_DATE, EXPECTED_TIME): failures.append("source metadata изменены") if failures: for failure in failures: print(f"FAIL: {failure}") raise SystemExit(1) print("PASS: F5 preserved exact FAT date/time and R/H/S/A attributes.") def main() -> None: if len(sys.argv) != 3 or sys.argv[1] not in { "create", "verify-source", "verify" }: raise SystemExit( f"usage: {sys.argv[0]} create|verify-source|verify PATH" ) path = pathlib.Path(sys.argv[2]) if sys.argv[1] == "create": create(path) elif sys.argv[1] == "verify-source": verify_source(path) else: verify(path) if __name__ == "__main__": main()