#!/usr/bin/env python3 """exfat.py -- read and (minimally) write an exFAT file system in an image file, without mounting it (mounting needs root or FUSE). exfat.py IMAGE[@OFFSET] info volume label, cluster size, number of FATs exfat.py IMAGE[@OFFSET] ls [DIR] list a directory ("/" by default) exfat.py IMAGE[@OFFSET] cat PATH print a file's bytes to stdout exfat.py IMAGE[@OFFSET] put DIR copy the files directly in DIR into the root directory (fresh volume only) OFFSET is where the file system starts inside IMAGE, in bytes (for a partition of a whole-disk image), e.g. foundry.img@95420416. Foundry OS uses it for two things: * `put`: mkfs.exfat (exfatprogs) can format a volume but not fill it, so the share partition's README is written with this (`make image`). * `ls`/`cat`: after `make test` the host reads back what Rustbelt wrote to the share, independently of the kernel's driver. fsck.exfat (exfatprogs) checks the result both times. `put` is deliberately tiny: every file gets one contiguous run of free clusters with the NoFatChain flag (so the FAT itself is not touched), the same fixed timestamp, and an entry in the root directory. That is all valid exFAT, and enough for a few files on a fresh volume. (The kernel's build/exfat_populate.rs takes the same approach; this is a separate, independent implementation.) """ import os import struct import sys ATTR_DIRECTORY = 0x10 ATTR_ARCHIVE = 0x20 NO_FAT_CHAIN = 0x02 ALLOC_POSSIBLE = 0x01 # 2026-01-01 00:00:00, exFAT/DOS format: year-1980 <<25 | month <<21 | day <<16. TIMESTAMP = (46 << 25) | (1 << 21) | (1 << 16) def rotr16_add(acc, byte): return ((((acc & 1) << 15) | (acc >> 1)) + byte) & 0xFFFF class Volume: def __init__(self, path, offset): self.path, self.offset = path, offset with open(path, "rb") as f: f.seek(offset) boot = f.read(512) if boot[3:11] != b"EXFAT ": sys.exit(f"exfat.py: no exFAT file system at offset {offset} of {path}") self.sector = 1 << boot[108] self.cluster = self.sector << boot[109] self.fat_at = struct.unpack_from("> 8) s[32] = 0xC0 s[33] = ALLOC_POSSIBLE | (NO_FAT_CHAIN if first else 0) s[35] = len(units) struct.pack_into("> ((c - 2) % 8) & 1 nxt = max(c for c in range(2, self.count + 2) if used(c)) + 1 root_clusters = self.clusters(self.root, None, True) root = bytearray(self.read_chain(self.root, None, True)) slot = next(i for e, i in self.entries(root) if e[0] == 0x00) for name in sorted(os.listdir(src_dir)): path = os.path.join(src_dir, name) if not os.path.isfile(path): continue data = open(path, "rb").read() n = -(-len(data) // self.cluster) if nxt + n > self.count + 2: sys.exit("exfat.py: volume full") first = nxt if n else 0 for c in range(nxt, nxt + n): bm[(c - 2) // 8] |= 1 << ((c - 2) % 8) self.write_at(self.pos(c), data[(c - nxt) * self.cluster : (c - nxt + 1) * self.cluster].ljust(self.cluster, b"\0")) nxt += n es = self.entry_set(name, ATTR_ARCHIVE, first, len(data)) if slot + len(es) > len(root): sys.exit("exfat.py: root directory full (put only fills its first cluster(s))") root[slot : slot + len(es)] = es slot += len(es) for k, c in enumerate(root_clusters): self.write_at(self.pos(c), root[k * self.cluster : (k + 1) * self.cluster]) for k, c in enumerate(self.clusters(self.bitmap_cluster, self.bitmap_len, False)): self.write_at(self.pos(c), bytes(bm[k * self.cluster : (k + 1) * self.cluster]).ljust(self.cluster, b"\0")) in_use = sum(used(c) for c in range(2, self.count + 2)) # PercentInUse (boot sector byte 112) is not covered by the boot checksum. self.write_at(self.offset + 112, bytes([in_use * 100 // self.count])) self.f.flush() def main(): if len(sys.argv) < 3: sys.exit(__doc__) img, _, off = sys.argv[1].partition("@") vol = Volume(img, int(off or 0)) cmd, args = sys.argv[2], sys.argv[3:] if cmd == "info": print(f"label {vol.label!r}, {vol.cluster}-byte clusters, {vol.count} clusters, " f"{vol.num_fats} FAT(s){' (TexFAT)' if vol.num_fats == 2 else ''}, volume flags {vol.volume_flags:#x}") elif cmd == "ls": first, size, chained, attrs = vol.lookup(args[0] if args else "/") for name, a, _, sz, _ in vol.list_dir(first, size if first != vol.root else None, chained): print(f"{'' if a & ATTR_DIRECTORY else sz:>10} {name}") elif cmd == "cat": first, size, chained, attrs = vol.lookup(args[0]) sys.stdout.buffer.write(vol.read_chain(first, size, chained)) elif cmd == "put": vol.put(args[0]) else: sys.exit(__doc__) if __name__ == "__main__": main()