72 lines
2.5 KiB
Python
72 lines
2.5 KiB
Python
"""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
|