Volkov: добавить Sprinter Commander
Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места. Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user