diff --git a/tools/make-exfat-image.py b/tools/make-exfat-image.py new file mode 100644 index 0000000..9eaa3a3 --- /dev/null +++ b/tools/make-exfat-image.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +"""Format a real exFAT image from scratch — the danos exFAT test volume. + +Pure Python 3 standard library (no mkfs.exfat / mtools). It writes a valid exFAT +filesystem — a Main Boot Sector + its boot-region checksum + a backup region, the +32-bit FAT, an allocation bitmap, an up-case table (with its checksum), and a root +directory whose entry sets a real exFAT reader (and the danos exfat engine) mount +and walk. Mirrors tools/make-fat-image.py in spirit. + + make-exfat-image.py [--serial ] [--label ] + make-exfat-image.py --verify + +The image seeds one file, HELLO.TXT, so a mount can be proven by reading it. +""" + +import struct +import sys + +SECTOR = 512 +UPCASE_UNITS = 256 # a-z -> A-Z, the rest identity; covers ASCII names + + +def align_up(value, to): + return (value + to - 1) // to * to + + +def rotr16(v): + return ((v >> 1) | (v << 15)) & 0xFFFF + + +def rotr32(v): + return ((v >> 1) | (v << 31)) & 0xFFFFFFFF + + +def boot_checksum(region): + """32-bit rotate-right sum over the boot region, skipping VolumeFlags + (106,107) and PercentInUse (112) of the first sector.""" + checksum = 0 + for i, byte in enumerate(region): + if i in (106, 107, 112): + continue + checksum = (rotr32(checksum) + byte) & 0xFFFFFFFF + return checksum + + +def upcase_checksum(table_bytes): + checksum = 0 + for byte in table_bytes: + checksum = (rotr32(checksum) + byte) & 0xFFFFFFFF + return checksum + + +def set_checksum(entries): + """16-bit rotate-right sum over a directory-entry set, skipping its own two + checksum bytes (offset 2..3 of the first entry).""" + checksum = 0 + for i, byte in enumerate(entries): + if i in (2, 3): + continue + checksum = (rotr16(checksum) + byte) & 0xFFFF + return checksum + + +def name_hash(upcased_units): + h = 0 + for unit in upcased_units: + h = (rotr16(h) + (unit & 0xFF)) & 0xFFFF + h = (rotr16(h) + (unit >> 8)) & 0xFFFF + return h + + +def ascii_upper(unit): + return unit - ord("a") + ord("A") if ord("a") <= unit <= ord("z") else unit + + +def solve_geometry(total_sectors, spc): + """Solve for cluster count / FAT length / heap offset that fit. The FAT sits + after the main + backup boot regions (24 sectors).""" + fat_offset = 24 + fat_length = 1 + while True: + heap_offset = align_up(fat_offset + fat_length, spc) + cluster_count = (total_sectors - heap_offset) // spc + needed = ((cluster_count + 2) * 4 + SECTOR - 1) // SECTOR + if needed <= fat_length: + return cluster_count, fat_offset, fat_length, heap_offset + fat_length = needed + + +class ExfatImage: + def __init__(self, size_mib, volume_id=0x1234ABCD, label="DANOS"): + self.total_sectors = size_mib * 1024 * 1024 // SECTOR + self.spc = 8 # 4 KiB clusters + self.volume_id = volume_id & 0xFFFFFFFF + self.label = label + self.cluster_count, self.fat_offset, self.fat_length, self.heap_offset = solve_geometry(self.total_sectors, self.spc) + if self.cluster_count < 16: + sys.exit(f"error: image too small for exFAT ({self.cluster_count} clusters)") + self.cluster_bytes = self.spc * SECTOR + self.root_cluster = 4 # 2=bitmap, 3=up-case, 4=root + self.image = bytearray(self.total_sectors * SECTOR) + + def cluster_offset(self, cluster): + return (self.heap_offset + (cluster - 2) * self.spc) * SECTOR + + def set_fat(self, cluster, value): + struct.pack_into(" A-Z. + upcase = bytearray(UPCASE_UNITS * 2) + for i in range(UPCASE_UNITS): + struct.pack_into("] [--label ] \n" + " make-exfat-image.py --verify ") + out_path = argv[1] + size_mib = int(argv[2]) + image = ExfatImage(size_mib, volume_id, label) + with open(out_path, "wb") as handle: + handle.write(image.serialize()) + print(f"make-exfat-image: wrote {out_path} " + f"({size_mib} MiB exFAT, {image.cluster_count} clusters, serial 0x{image.volume_id:08X})") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv))