#!/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 # Layout: the allocation bitmap (as many clusters as it needs — one per # 8*cluster_bytes clusters of the volume), then the up-case table, the root # directory, and the seeded file. A single-cluster bitmap (small volumes, # e.g. the 48 MiB fixture) puts root at cluster 4, as before. self.bitmap_bytes = (self.cluster_count + 7) // 8 self.bitmap_clusters = (self.bitmap_bytes + self.cluster_bytes - 1) // self.cluster_bytes self.bitmap_cluster = 2 self.upcase_cluster = self.bitmap_cluster + self.bitmap_clusters self.root_cluster = self.upcase_cluster + 1 self.hello_cluster = self.root_cluster + 1 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))