#!/usr/bin/env python3
"""
DF-0818 crafter — produce a HAMMER2 image with volu_size=0 to test whether
the kernel rejects it at mount time (defense at hammer2_ondisk.c:292
`vol->size == 0`) before it can reach the div0 at hammer2_bulkfree.c:727.

We start from a known-good image, change volu_size from N to 0, recompute
the three volume-header CRCs (icrc_sects[7] = sect0, icrc_sects[6] = sect1,
icrc_volheader = whole-volume), and write the result.

Layout (sys/vfs/hammer2/hammer2_disk.h:1152 hammer2_volume_data):
  magic          u64 @ 0x000
  volu_size      u64 @ 0x028
  ...
  icrc_sects[8] u32[] @ 0x1E0   (sects[7]=sect0 CRC, sects[6]=sect1 CRC)
  icrc_volheader u32 @ 0xFFFC
"""
import struct, sys, zlib

IMG_IN  = sys.argv[1] if len(sys.argv) > 1 else "h2.img"
IMG_OUT = sys.argv[2] if len(sys.argv) > 2 else "h2_zero.img"
NEW_SIZE = int(sys.argv[3], 0) if len(sys.argv) > 3 else 0   # volu_size override

VH_BYTES = 65536
OFF_magic          = 0x000
OFF_volu_size      = 0x028
OFF_icrc_sects     = 0x1E0   # u32[8]; index 7 = sect0 CRC; index 6 = sect1 CRC
OFF_icrc_volheader = 0xFFFC

HAMMER2_VOLUME_ID_HBO = 0x48414d3205172011

def crc32c(buf: bytes) -> int:
    # Castagnoli CRC32-C, reflected, init=0xFFFFFFFF, xorout=0xFFFFFFFF
    # Python's zlib.crc32 is the E.132 polynomial; we need CRC32C.
    # Implement via table.
    poly = 0x82F63B78
    crc = 0xFFFFFFFF
    for b in buf:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ poly if (crc & 1) else (crc >> 1)
    return crc ^ 0xFFFFFFFF

with open(IMG_IN, "rb") as f:
    raw = bytearray(f.read())

if len(raw) < VH_BYTES:
    sys.exit(f"image too small: {len(raw)}")

vh = bytearray(raw[:VH_BYTES])

# Sanity-check we found the volume header
magic = struct.unpack_from("<Q", vh, OFF_magic)[0]
print(f"orig magic = 0x{magic:016x}  HBO=0x{HAMMER2_VOLUME_ID_HBO:016x}")
assert magic == HAMMER2_VOLUME_ID_HBO, "source image magic wrong"

orig_size = struct.unpack_from("<Q", vh, OFF_volu_size)[0]
print(f"orig volu_size = 0x{orig_size:016x} ({orig_size})")

# Overwrite volu_size with the new value (typically 0).
struct.pack_into("<Q", vh, OFF_volu_size, NEW_SIZE)
print(f"new  volu_size = 0x{NEW_SIZE:016x} ({NEW_SIZE})")

# Recompute the three CRCs.
#  - sect0 CRC: covers bytes [0, 512-4) of the volume header; stored at
#    icrc_sects[7] (offset 0x1E0 + 7*4 = 0x1FC).
#  - sect1 CRC: covers bytes [512, 1024) of the volume header; stored at
#    icrc_sects[6] (offset 0x1E0 + 6*4 = 0x1F8).
#  - volheader CRC: covers bytes [0, 65536-4) of the volume header; stored
#    at offset 0xFFFC.

# Zero out the four CRC slots before computing sect0 CRC over the first
# 508 bytes — but the existing code reads them in-place.  We need to
# reproduce the kernel's exact layout: HAMMER2_VOLUME_ICRC0_OFF=0,
# HAMMER2_VOLUME_ICRC0_SIZE=512-4=508.  Note the icrc_sects[] live at
# 0x1E0..0x1FF, INSIDE the sect0 range, so we must zero the sect0 CRC slot
# (icrc_sects[7] @ 0x1FC) before computing.

# 1) sect0: bytes [0,508), zero the slot first
struct.pack_into("<I", vh, OFF_icrc_sects + 7*4, 0)
sect0 = crc32c(bytes(vh[0:508]))
struct.pack_into("<I", vh, OFF_icrc_sects + 7*4, sect0)

# 2) sect1: bytes [512,1024)
sect1 = crc32c(bytes(vh[512:1024]))
struct.pack_into("<I", vh, OFF_icrc_sects + 6*4, sect1)

# 3) volheader: bytes [0, 65536-4), zero the slot first
struct.pack_into("<I", vh, OFF_icrc_volheader, 0)
vhcrc = crc32c(bytes(vh[0:65536-4]))
struct.pack_into("<I", vh, OFF_icrc_volheader, vhcrc)

# HAMMER2 stores 4 redundant copies of the volume header at zones 0..3
# (each at offset i * HAMMER2_ZONE_BYTES64).  Mirror the modified header
# into all of them so the mount code (which picks the one with the highest
# mirror_tid) reads our crafted header.
HAMMER2_ZONE_BYTES64 = 1 << 50  # 1 PB nominal zone stride — see hammer2_disk.h
# Actually the zones are spaced 2^50 apart in nominal addressing, but the
# physical reads use bread(devvp, i * HAMMER2_ZONE_BYTES64, ...).  On a
# 128 MB backing device only zone 0 is reachable; the others read beyond
# EOF and are skipped.  Mirror anyway into the first 4 * 64 KiB just in
# case bread's offset is in block units — physically, only zone 0 matters.
raw[:VH_BYTES] = vh
# Some implementations write 4 adjacent copies; harmless to mirror.
for i in range(1, 4):
    s = i * VH_BYTES
    if s + VH_BYTES <= len(raw):
        raw[s:s+VH_BYTES] = vh

with open(IMG_OUT, "wb") as f:
    f.write(raw)

print(f"wrote {IMG_OUT} ({len(raw)} bytes)")
print(f"  sect0 crc   = 0x{sect0:08x}")
print(f"  sect1 crc   = 0x{sect1:08x}")
print(f"  volheader   = 0x{vhcrc:08x}")
