# === StraightArc Technologies Open Toolset : banner & disclaimer =============
def _sta_accept():
    import sys, os
    line = "=" * 68
    print(line)
    print("               StraightArc Technologies Open Toolset")
    print(line)
    print('DISCLAIMER: This tool is provided on an "AS IS" basis. StraightArc')
    print("Technologies or any of its associates shall not be responsible for")
    print("the consequences of its use. Always operate on a COPY of the media,")
    print("never the original evidence.")
    print(line)
    if os.environ.get("STRAIGHTARC_ACCEPT") == "1":
        return
    if not sys.stdin.isatty():
        sys.exit("Non-interactive run: set STRAIGHTARC_ACCEPT=1 to proceed.")
    try:
        ans = input("Type 'accept' to acknowledge and continue: ").strip().lower()
    except (EOFError, KeyboardInterrupt):
        sys.exit("\nDisclaimer not accepted. Exiting.")
    if ans not in ("accept", "y", "yes"):
        sys.exit("Disclaimer not accepted. Exiting.")
_sta_accept()
# ============================================================================

import struct, sys, os, json

MFT="mft.raw"
REC=1024
SEC=512

def apply_fixups(rec):
    off_usa = struct.unpack_from("<H", rec, 4)[0]
    n_usa   = struct.unpack_from("<H", rec, 6)[0]
    if off_usa==0 or n_usa==0 or off_usa+ n_usa*2 > REC: return rec
    usn = rec[off_usa:off_usa+2]
    rec = bytearray(rec)
    for i in range(1, n_usa):
        fix = rec[off_usa+i*2: off_usa+i*2+2]
        pos = i*SEC - 2
        if pos+2 <= len(rec):
            rec[pos:pos+2] = fix
    return bytes(rec)

def parse_runlist(data, off):
    """Return list of (lcn, length_clusters). Handles sparse (lcn None)."""
    runs=[]; lcn=0; i=off
    while i < len(data):
        hdr = data[i]
        if hdr==0: break
        len_sz = hdr & 0x0F
        off_sz = (hdr>>4) & 0x0F
        i+=1
        if len_sz==0 or i+len_sz+off_sz > len(data): break
        run_len = int.from_bytes(data[i:i+len_sz], "little", signed=False); i+=len_sz
        if off_sz>0:
            run_off = int.from_bytes(data[i:i+off_sz], "little", signed=True); i+=off_sz
            lcn += run_off
            runs.append((lcn, run_len))
        else:
            runs.append((None, run_len))  # sparse
    return runs

def parse_record(rec, inode):
    if rec[0:4]!=b"FILE": return None
    rec = apply_fixups(rec)
    flags = struct.unpack_from("<H", rec, 22)[0]
    alloc = bool(flags & 0x01)
    is_dir= bool(flags & 0x02)
    first = struct.unpack_from("<H", rec, 20)[0]
    name=None; name_ns=-1; parent=None
    data_size=None; data_nonres=False; has_data=False; resident_data=None; runlist=None
    i=first
    while i+8 <= REC:
        atype = struct.unpack_from("<I", rec, i)[0]
        if atype==0xFFFFFFFF: break
        alen = struct.unpack_from("<I", rec, i+4)[0]
        if alen==0 or i+alen>REC: break
        nonres = rec[i+8]
        if atype==0x30:  # FILE_NAME (always resident)
            coff = struct.unpack_from("<H", rec, i+20)[0]
            c=i+coff
            p = struct.unpack_from("<Q", rec, c)[0] & 0x0000FFFFFFFFFFFF
            nlen = rec[c+0x40]
            ns   = rec[c+0x41]
            try:
                nm = rec[c+0x42:c+0x42+nlen*2].decode("utf-16le","replace")
            except Exception:
                nm=None
            # prefer Win32(1)/Win32&DOS(3) over DOS(2)/POSIX(0)
            if nm is not None and (name is None or ns in (1,3) and name_ns not in (1,3)):
                name=nm; name_ns=ns; parent=p
        elif atype==0x80:  # DATA
            nlen_a = rec[i+9]  # attribute name length; 0 = unnamed main stream
            if nlen_a==0:
                has_data=True
                if nonres==0:
                    csize=struct.unpack_from("<I", rec, i+16)[0]
                    coff =struct.unpack_from("<H", rec, i+20)[0]
                    resident_data=rec[i+coff:i+coff+csize]
                    data_size=csize
                else:
                    data_nonres=True
                    real=struct.unpack_from("<Q", rec, i+0x30)[0]
                    data_size=real
                    rloff=struct.unpack_from("<H", rec, i+0x20)[0]
                    runlist=parse_runlist(rec[i:i+alen], rloff)
        i+=alen
    return dict(inode=inode, alloc=alloc, is_dir=is_dir, name=name, parent=parent,
                has_data=has_data, size=data_size, nonres=data_nonres,
                resident=resident_data, runlist=runlist)

def main():
    sz=os.path.getsize(MFT); n=sz//REC
    recs={}
    f=open(MFT,"rb")
    parsed=0
    for inode in range(n):
        rec=f.read(REC)
        if len(rec)<REC: break
        if rec[0:4]!=b"FILE": continue
        r=parse_record(rec, inode)
        if r: recs[inode]=r; parsed+=1
    print(f"parsed {parsed} valid FILE records of {n}", file=sys.stderr)
    # save compact metadata (no resident/runlist) as json for analysis
    meta={}
    for ino,r in recs.items():
        meta[ino]=dict(a=int(r["alloc"]), d=int(r["is_dir"]), n=r["name"],
                       p=r["parent"], hd=int(r["has_data"]), s=r["size"], nr=int(r["nonres"]))
    json.dump(meta, open("mft_meta.json","w"))
    print("wrote mft_meta.json", file=sys.stderr)

if __name__=="__main__":
    main()


# ============================================================================
# DISCLAIMER
# This script is provided on an "AS IS" basis. StraightArc Technologies or any
# of its associates shall not be responsible for the consequences of its use.
#
# A Contribution from StraightArc Technologies (info@straightarc.com)
# ============================================================================
