8e389c03f8
Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места. Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
176 lines
6.2 KiB
Python
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()
|