136 lines
5.3 KiB
Python
136 lines
5.3 KiB
Python
"""FAT32 + MBR layout for an SD card, as raw byte writes (no OS formatter).
|
|
|
|
Follows the Microsoft FAT specification (fatgen103): BPB at sector 0,
|
|
FSInfo at 1, backup boot sector at 6, two FATs, root directory in cluster 2
|
|
holding the volume label. Cluster sizes follow Windows' defaults. The
|
|
partition starts at 1 MiB (SD erase-block friendly), MBR type 0x0C
|
|
(FAT32 LBA), which is what the firmware's FatFs expects.
|
|
|
|
Pure functions: the result is a list of (offset, bytes) writes plus zero
|
|
ranges relative to the start of the DISK, so it can be tested on a buffer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import struct
|
|
from dataclasses import dataclass
|
|
|
|
PART_START = 1024 * 1024
|
|
FAT32_MAX_BYTES = 32 * 1024**3 # Windows convention, and all the box needs
|
|
MIN_CLUSTERS, MAX_CLUSTERS = 65525, 0x0FFFFFF5
|
|
RESERVED = 32
|
|
NUM_FATS = 2
|
|
MEDIA = 0xF8
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Fat32Layout:
|
|
bytes_per_sector: int
|
|
part_start: int # bytes from disk start
|
|
part_sectors: int
|
|
sectors_per_cluster: int
|
|
fat_sectors: int
|
|
clusters: int
|
|
|
|
@property
|
|
def data_start(self) -> int: # sectors from partition start
|
|
return RESERVED + NUM_FATS * self.fat_sectors
|
|
|
|
|
|
def _spc(sectors: int, bps: int) -> int:
|
|
size = sectors * bps
|
|
for limit, cluster in ((260 * 1024**2, 512), (8 * 1024**3, 4096), (16 * 1024**3, 8192),
|
|
(32 * 1024**3, 16384)):
|
|
if size <= limit:
|
|
return max(1, cluster // bps)
|
|
return max(1, 32768 // bps)
|
|
|
|
|
|
def plan(disk_bytes: int, bps: int = 512) -> Fat32Layout:
|
|
part_bytes = min(disk_bytes - PART_START, FAT32_MAX_BYTES)
|
|
part_bytes -= part_bytes % (1024 * 1024) # whole MiB
|
|
sectors = part_bytes // bps
|
|
if sectors <= 0:
|
|
raise ValueError("card is too small")
|
|
spc = _spc(sectors, bps)
|
|
fat = 1
|
|
for _ in range(20): # converge FAT size <-> cluster count
|
|
clusters = (sectors - RESERVED - NUM_FATS * fat) // spc
|
|
need = math.ceil((clusters + 2) * 4 / bps)
|
|
if need == fat:
|
|
break
|
|
fat = need
|
|
clusters = (sectors - RESERVED - NUM_FATS * fat) // spc
|
|
if not MIN_CLUSTERS <= clusters < MAX_CLUSTERS:
|
|
raise ValueError(f"card size gives {clusters} clusters - not valid for FAT32")
|
|
return Fat32Layout(bps, PART_START, sectors, spc, fat, clusters)
|
|
|
|
|
|
def _boot_sector(lay: Fat32Layout, label: bytes, vol_id: int) -> bytes:
|
|
bps = lay.bytes_per_sector
|
|
b = bytearray(bps)
|
|
b[0:3] = b"\xEB\x58\x90"
|
|
b[3:11] = b"MSWIN4.1"
|
|
struct.pack_into("<HBHBHHBHHHII", b, 11, bps, lay.sectors_per_cluster, RESERVED, NUM_FATS, 0, 0,
|
|
MEDIA, 0, 63, 255, lay.part_start // bps, lay.part_sectors)
|
|
struct.pack_into("<IHHIHH", b, 36, lay.fat_sectors, 0, 0, 2, 1, 6)
|
|
struct.pack_into("<BBBI", b, 64, 0x80, 0, 0x29, vol_id)
|
|
b[71:82] = label
|
|
b[82:90] = b"FAT32 "
|
|
b[90:93] = b"\xF4\xEB\xFD" # not bootable: hlt loop
|
|
b[510:512] = b"\x55\xAA"
|
|
return bytes(b)
|
|
|
|
|
|
def _fsinfo(lay: Fat32Layout) -> bytes:
|
|
b = bytearray(lay.bytes_per_sector)
|
|
struct.pack_into("<I", b, 0, 0x41615252)
|
|
struct.pack_into("<III", b, 484, 0x61417272, lay.clusters - 1, 3) # root uses cluster 2
|
|
struct.pack_into("<I", b, 508, 0xAA550000)
|
|
return bytes(b)
|
|
|
|
|
|
def mbr(lay: Fat32Layout, signature: int) -> bytes:
|
|
b = bytearray(512)
|
|
struct.pack_into("<I", b, 0x1B8, signature)
|
|
entry = struct.pack("<B3sB3sII", 0x00, b"\xFE\xFF\xFF", 0x0C, b"\xFE\xFF\xFF",
|
|
lay.part_start // lay.bytes_per_sector, lay.part_sectors)
|
|
b[0x1BE:0x1BE + 16] = entry
|
|
b[510:512] = b"\x55\xAA"
|
|
return bytes(b[:512]) + bytes(lay.bytes_per_sector - 512)
|
|
|
|
|
|
def volume_label(text: str) -> bytes:
|
|
label = text.upper().encode("ascii", "replace")[:11]
|
|
return label.ljust(11, b" ")
|
|
|
|
|
|
def build(lay: Fat32Layout, label: str, vol_id: int, disk_signature: int,
|
|
disk_bytes: int) -> tuple[list[tuple[int, int]], list[tuple[int, bytes]]]:
|
|
"""(zero ranges, writes) in disk byte offsets; apply zero ranges first."""
|
|
bps, p = lay.bytes_per_sector, lay.part_start
|
|
lab = volume_label(label)
|
|
fat_bytes = lay.fat_sectors * bps
|
|
fat1 = p + RESERVED * bps
|
|
fat2 = fat1 + fat_bytes
|
|
root = p + lay.data_start * bps
|
|
cluster = lay.sectors_per_cluster * bps
|
|
zeros = [
|
|
(0, PART_START), # old MBR / GPT header and gap
|
|
(disk_bytes - PART_START, PART_START), # backup GPT at the end of the disk
|
|
(p, RESERVED * bps), # reserved region
|
|
(fat1, NUM_FATS * fat_bytes), # both FATs
|
|
(root, cluster), # root directory cluster
|
|
]
|
|
boot, info = _boot_sector(lay, lab, vol_id), _fsinfo(lay)
|
|
first_fat = struct.pack("<III", 0x0FFFFF00 | MEDIA, 0x0FFFFFFF, 0x0FFFFFFF)
|
|
label_entry = lab + bytes([0x08]) + bytes(20) # 32-byte volume-label directory entry
|
|
writes = [
|
|
(p, boot), (p + bps, info), (p + 6 * bps, boot), (p + 7 * bps, info),
|
|
(fat1, first_fat), (fat2, first_fat),
|
|
(root, label_entry),
|
|
(0, mbr(lay, disk_signature)), # last: the partition appears only now
|
|
]
|
|
return zeros, writes
|