"""Write an Acorn DFS single-sided disk image (.ssd) natively. DFS layout: sectors 0-1 are the catalogue; files follow from sector 2, each starting on a 256-byte sector boundary. Addresses are 18-bit (load/exec/length high bits packed into the per-file flag byte). Boot option (*OPT 4,n) lives in sector 1 byte 6 bits 4-5. """ from __future__ import annotations SECTOR = 256 TRACKS = 80 SECTORS = TRACKS * 10 # 800 sectors = 200K, single sided class DfsError(RuntimeError): pass def _name7(name: str) -> bytes: n = "".join(c for c in name.upper() if 32 <= ord(c) < 127)[:7] return n.ljust(7).encode("ascii") def build_ssd(files, title="8BITLENSER", boot_option=0) -> bytes: """files: list of (name, load_addr, exec_addr, data). boot_option 0-3 (*OPT 4,n): 0 none, 1 *LOAD, 2 *RUN, 3 *EXEC of the first matching $.!BOOT.""" if len(files) > 31: raise DfsError("DFS holds at most 31 files") cat0 = bytearray(SECTOR) cat1 = bytearray(SECTOR) t = title.ljust(12)[:12].encode("ascii", "replace") cat0[0:8] = t[0:8] cat1[0:4] = t[8:12] cat1[4] = 0 # cycle number cat1[5] = len(files) * 8 # (#files) * 8 cat1[6] = ((SECTORS >> 8) & 0x03) | ((boot_option & 0x03) << 4) cat1[7] = SECTORS & 0xFF body = bytearray() start_sector = 2 for i, (name, load, exec_, data) in enumerate(files): e = 8 + i * 8 cat0[e:e + 7] = _name7(name) cat0[e + 7] = ord("$") # directory '$' (unlocked) length = len(data) cat1[e + 0] = load & 0xFF cat1[e + 1] = (load >> 8) & 0xFF cat1[e + 2] = exec_ & 0xFF cat1[e + 3] = (exec_ >> 8) & 0xFF cat1[e + 4] = length & 0xFF cat1[e + 5] = (length >> 8) & 0xFF cat1[e + 6] = (((exec_ >> 16) & 0x03) << 6 | ((length >> 16) & 0x03) << 4 | ((load >> 16) & 0x03) << 2 | ((start_sector >> 8) & 0x03)) cat1[e + 7] = start_sector & 0xFF nsec = (length + SECTOR - 1) // SECTOR body += bytes(data) + bytes((-length) % SECTOR) start_sector += nsec img = bytes(cat0) + bytes(cat1) + bytes(body) if len(img) > SECTORS * SECTOR: raise DfsError("files exceed disk capacity") return img + bytes(SECTORS * SECTOR - len(img)) def write_ssd(path: str, files, title="8BITLENSER", boot_option=0) -> str: with open(path, "wb") as f: f.write(build_ssd(files, title, boot_option)) return path