# === 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, os, json, re, sys
REC=1024; SEC=512; CLU=4096; BND=393216; FOOT=72
DEV="/dev/sde2"; OUT="/mnt/d/MS"
dirs=["recovered_deleted","thumbnails","pdf_images","tails/video","tails/archive","tails/pdf","tails/office"]
for d in dirs: os.makedirs(os.path.join(OUT,d), exist_ok=True)
log=open(os.path.join(OUT,"_extraction_log.txt"),"w")
def L(*a):
    s=" ".join(str(x) for x in a); print(s); log.write(s+"\n"); log.flush()

def fixup(rec):
    o=struct.unpack_from("<H",rec,4)[0]; n=struct.unpack_from("<H",rec,6)[0]
    if not o or not n: return rec
    rec=bytearray(rec)
    for i in range(1,n):
        p=i*SEC-2
        if p+2<=len(rec): rec[p:p+2]=rec[o+i*2:o+i*2+2]
    return bytes(rec)
def parse_runlist(data,off):
    runs=[];lcn=0;i=off
    while i<len(data):
        h=data[i]
        if h==0:break
        ls=h&0x0F;os_=(h>>4)&0x0F;i+=1
        if ls==0 or i+ls+os_>len(data):break
        rl=int.from_bytes(data[i:i+ls],"little");i+=ls
        if os_:
            ro=int.from_bytes(data[i:i+os_],"little",signed=True);i+=os_
            lcn+=ro;runs.append((lcn,rl))
        else:runs.append((None,rl))
    return runs
dev=open(DEV,"rb")
def read_range(runs, start, end, size):
    end=min(end,size); out=bytearray(); foff=0
    for lcn,rl in runs:
        rb=rl*CLU; rs=foff; re_=foff+rb
        if re_>start and rs<end:
            a=max(start,rs); b=min(end,re_)
            if lcn is None: out+=b"\x00"*(b-a)
            else: dev.seek(lcn*CLU+(a-rs)); out+=dev.read(b-a)
        foff=re_
        if foff>=end: break
    return bytes(out)

def get_attrs(rec):
    rec=fixup(rec); first=struct.unpack_from("<H",rec,20)[0]; i=first
    resident=None;runlist=None
    while i+8<=REC:
        at=struct.unpack_from("<I",rec,i)[0]
        if at==0xFFFFFFFF:break
        al=struct.unpack_from("<I",rec,i+4)[0]
        if al==0 or i+al>REC:break
        if at==0x80 and rec[i+9]==0:
            if rec[i+8]==0:
                cs=struct.unpack_from("<I",rec,i+16)[0];co=struct.unpack_from("<H",rec,i+20)[0]
                resident=rec[i+co:i+co+cs]
            else:
                rloff=struct.unpack_from("<H",rec,i+0x20)[0];runlist=parse_runlist(rec[i:i+al],rloff)
        i+=al
    return resident,runlist

def safe(name): return re.sub(r'[^A-Za-z0-9._-]','_',name)[:80]
def carve_jpegs(data, outdir, prefix, cap=50):
    n=0; pos=0
    while n<cap:
        s=data.find(b'\xff\xd8\xff',pos)
        if s<0: break
        e=data.find(b'\xff\xd9',s+3)
        if e<0: break
        jpg=data[s:e+2]
        if len(jpg)>=1024:  # skip tiny noise
            open(os.path.join(outdir,f"{prefix}_{n}.jpg"),"wb").write(jpg); n+=1
        pos=e+2
    return n

meta=json.load(open("mft_meta.json")); M={int(k):v for k,v in meta.items()}
recset={r[0] for r in json.load(open("recoverable_deleted.json"))}
VID={'.mp4','.mov','.avi','.3gp'}; ARC={'.zip','.rar'}; OFF={'.doc','.docx','.xls','.xlsx','.ppt','.pptx'}

f=open("mft.raw","rb"); n=os.path.getsize("mft.raw")//REC
c_del=c_thumb=c_pdf=c_pdfimg=c_vid=c_arc=c_off=0; bytes_out=0
for ino in range(n):
    rec=f.read(REC)
    if rec[0:4]!=b"FILE": continue
    m=M.get(ino)
    if not m or not m["n"]: continue
    nm=m["n"]; ext=os.path.splitext(nm)[1].lower(); size=m["s"] or 0
    # (A) complete recovery of validated deleted files
    if ino in recset:
        resident,runlist=get_attrs(rec)
        data = resident if resident is not None else (read_range(runlist,0,size,size) if runlist else b"")
        if data:
            fn=os.path.join(OUT,"recovered_deleted",f"{ino}_{safe(nm)}")
            open(fn,"wb").write(data); c_del+=1; bytes_out+=len(data)
            if nm.lower()=="thumbs.db":
                c_thumb+=carve_jpegs(data, os.path.join(OUT,"thumbnails"), f"{ino}")
        continue
    # (B) partial tails of large encrypted files
    if not (m["a"] and not m["d"] and nm.endswith(".Funder") and size>BND+FOOT): continue
    base=nm[:-7]; bext=os.path.splitext(base)[1].lower()
    cat=None
    if bext=='.pdf': cat="pdf"
    elif bext in VID: cat="video"
    elif bext in ARC: cat="archive"
    elif bext in OFF: cat="office"
    else: continue
    resident,runlist=get_attrs(rec)
    if not runlist: continue
    tail=read_range(runlist, BND, size-FOOT, size)
    if not tail: continue
    fn=os.path.join(OUT,"tails",cat,f"{ino}_{safe(base)}.tail")
    open(fn,"wb").write(tail); bytes_out+=len(tail)
    if cat=="pdf":
        c_pdf+=1; c_pdfimg+=carve_jpegs(tail, os.path.join(OUT,"pdf_images"), f"{ino}", cap=30)
    elif cat=="video": c_vid+=1
    elif cat=="archive": c_arc+=1
    elif cat=="office": c_off+=1
    if (c_pdf+c_vid+c_arc+c_off)%500==0 and (c_pdf+c_vid+c_arc+c_off)>0:
        L(f"progress: tails pdf={c_pdf} vid={c_vid} arc={c_arc} off={c_off}  written={bytes_out/1e9:.2f}GB")

L("=== DONE ===")
L(f"complete deleted files recovered : {c_del}")
L(f"thumbnails carved (from Thumbs.db): {c_thumb}")
L(f"PDF tails extracted              : {c_pdf}  (images carved: {c_pdfimg})")
L(f"video tails extracted            : {c_vid}")
L(f"archive tails extracted          : {c_arc}")
L(f"office tails extracted           : {c_off}")
L(f"total bytes written              : {bytes_out/1e9:.2f} GB")
L(f"output folder                    : {OUT}")


# ============================================================================
# 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)
# ============================================================================
