#!/usr/bin/env python3
"""Parse a DragonFly journal raw-record stream (vfs_journal.c format).
A race-free stream is a contiguous chain of records with begmagic
0x1234 and strictly increasing transids (each stream record +2, pads
interleave at +1: see journal_reserve()/journal_build_pad()).
Reports: record count, monotonicity violations (transid went
backwards/equal => concurrent unsynchronized reserve), forged/garbage
headers, and chain breaks (gap before EOF)."""
import struct, sys

BEG, PAD_SID = 0x1234, (0x8000 | 0x4000 | 0x0001)
data = open(sys.argv[1], 'rb').read()
off = recs = pads = mono_viol = 0
last_tid = None
first_break = None
tids = {}
while off + 16 <= len(data):
    beg, sid = struct.unpack_from('<HH', data, off)
    rs, = struct.unpack_from('<i', data, off + 4)
    tid, = struct.unpack_from('<q', data, off + 8)
    if beg != BEG:
        first_break = (off, beg, rs)
        break
    step = (rs + 15) & ~15 if rs >= 0 else -1
    if step < 16 or step > (128 << 20):
        print(f"FATAL: bogus recsize {rs:#x} at off {off:#x} sid {sid:#x} tid {tid:#x}")
        first_break = (off, beg, rs)
        break
    if sid == PAD_SID:
        pads += 1
        # 16-byte pads overlay their trailer on the transid field:
        # journal.h says the pad transid MUST be ignored.
        if rs != 16 and last_tid is not None and tid <= last_tid:
            mono_viol += 1
            print(f"MONO VIOLATION (pad>=32): off {off:#x} tid {tid:#x} <= {last_tid:#x}")
    else:
        tids[tid] = tids.get(tid, 0) + 1
    if sid != PAD_SID and last_tid is not None and tid <= last_tid:
        mono_viol += 1
        if mono_viol <= 8:
            print(f"MONO VIOLATION #{mono_viol}: off {off:#x} tid {tid:#x} "
                  f"<= previous {last_tid:#x} (sid {sid:#x} recsize {rs})")
    last_tid = tid
    off += step
    recs += 1
dupes = {t: c for t, c in tids.items() if c > 1}
print(f"stream={sys.argv[1]} bytes={len(data)} records={recs} pads={pads}")
print(f"chain_offset_end={off:#x} eof={len(data):#x} "
      f"chain_break={'YES at ' + hex(first_break[0]) + f' beg={first_break[1]:#x} rs={first_break[2]}' if first_break else 'no'}")
print(f"transid_monotonic_violations={mono_viol}")
print(f"duplicate_transids={len(dupes)}" + (f" sample={list(dupes.items())[:5]}" if dupes else ""))
