Files
Sprinter-SDCC/applications/Volkov/tests/p18_metadata_fixture.py
T
snark13 8e389c03f8 Volkov: добавить Sprinter Commander
Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места.

Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
2026-09-10 10:45:30 +03:00

176 lines
6.2 KiB
Python

#!/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("<I", raw, 446 + 8)[0]
self.part = part * 512
self.bps = struct.unpack_from("<H", raw, self.part + 11)[0]
self.spc = raw[self.part + 13]
reserved = struct.unpack_from("<H", raw, self.part + 14)[0]
fats = raw[self.part + 16]
self.root_entries = struct.unpack_from("<H", raw, self.part + 17)[0]
self.spf = struct.unpack_from("<H", raw, self.part + 22)[0]
self.fat = self.part + reserved * self.bps
self.root = self.fat + fats * self.spf * self.bps
root_sectors = (self.root_entries * 32 + self.bps - 1) // self.bps
self.data = self.root + root_sectors * self.bps
self.cluster_size = self.bps * self.spc
def cluster_offset(self, cluster: int) -> int:
return self.data + (cluster - 2) * self.cluster_size
def next_cluster(self, cluster: int) -> int:
return struct.unpack_from("<H", self.raw, self.fat + cluster * 2)[0]
def directory_bytes(self, cluster: int | None) -> 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("<H", entry, 26)[0]
def file_bytes(self, entry: bytes) -> bytes:
remaining = struct.unpack_from("<I", entry, 28)[0]
cluster = self.cluster(entry)
result = bytearray()
while remaining and 2 <= cluster < 0xFFF8:
offset = self.cluster_offset(cluster)
take = min(remaining, self.cluster_size)
result += self.raw[offset:offset + take]
remaining -= take
cluster = self.next_cluster(cluster)
return bytes(result)
def metadata(entry: bytes) -> tuple[int, int, int]:
return (
entry[11],
struct.unpack_from("<H", entry, 24)[0],
struct.unpack_from("<H", entry, 22)[0],
)
def records(raw_path: pathlib.Path) -> 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()