#!/usr/bin/env python3
# === 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()
# ============================================================================

"""
repair_files2.py — robust salvage from .Funder tails.

Improvement over v1: instead of trusting the ZIP central directory (which is often destroyed
or ZIP64-encoded, or sits in an encrypted chunk), scan for ZIP LOCAL file headers (PK\\x03\\x04)
directly and STREAM-inflate each member. This recovers every archive/OOXML member that happens
to lie in a plaintext region, regardless of central-directory state, ZIP64, or data descriptors.

Reminder on this ransomware: small/mid files are often encrypted in full (no plaintext survives);
large files keep plaintext between the encrypted chunks. Members in plaintext regions recover;
members overlapping an encrypted chunk do not. Files with zero surviving PK headers are fully
encrypted and unrecoverable — reported as such (not a tool failure).
"""
import os, struct, zlib, sys
ROOT="/mnt/d/MS"; TAILS=os.path.join(ROOT,"tails"); OUTR=os.path.join(ROOT,"repaired2")
os.makedirs(OUTR, exist_ok=True)
log=open(os.path.join(OUTR,"_repair_log.txt"),"w")
def L(*a):
    s=" ".join(str(x) for x in a); print(s); log.write(s+"\n"); log.flush()

def safe(nm): return nm.replace("..","__").lstrip("/").replace("\x00","")

def recover_zip_localscan(data, outdir):
    """Scan for local headers, stream-inflate. Returns (recovered, seen_headers)."""
    positions=[]; p=0
    while True:
        q=data.find(b"PK\x03\x04", p)
        if q<0: break
        positions.append(q); p=q+4
    rec=0; names=set()
    for idx,q in enumerate(positions):
        if q+30>len(data): continue
        method=struct.unpack_from("<H",data,q+8)[0]
        csize =struct.unpack_from("<I",data,q+18)[0]
        nlen  =struct.unpack_from("<H",data,q+26)[0]
        elen  =struct.unpack_from("<H",data,q+28)[0]
        if q+30+nlen>len(data): continue
        name=data[q+30:q+30+nlen].decode("utf-8","replace")
        ds=q+30+nlen+elen
        if ds>len(data): continue
        # window: up to next local header (or EOF)
        nxt=positions[idx+1] if idx+1<len(positions) else len(data)
        blob=data[ds:nxt]
        out=None
        try:
            if method==8:
                out=zlib.decompressobj(-15).decompress(blob, 200_000_000)
            elif method==0:
                out=blob[:csize] if csize else blob
            else:
                continue
        except Exception:
            # partial inflate salvage
            try:
                d=zlib.decompressobj(-15); out=d.decompress(blob,200_000_000)
            except Exception:
                continue
        if not out or not name or name.endswith("/"): continue
        dst=os.path.join(outdir, safe(name))
        if dst in names:
            base,ext=os.path.splitext(dst); dst=f"{base}_{idx}{ext}"
        names.add(dst)
        try:
            os.makedirs(os.path.dirname(dst) or outdir, exist_ok=True)
            open(dst,"wb").write(out); rec+=1
        except Exception: pass
    return rec, len(positions)

def main():
    summ={'zip_rec':0,'zip_files_ok':0,'zip_fullenc':0,'zip_rar':0,
          'off_rec':0,'off_files_ok':0,'off_fullenc':0}
    for cat,key in [("archive","zip"),("office","off")]:
        d=os.path.join(TAILS,cat)
        if not os.path.isdir(d): continue
        for fn in sorted(os.listdir(d)):
            if fn.lower().endswith(".rar.tail"):
                summ['zip_rar']+=1; L(f"[rar] {fn}: RAR format — needs `unrar`, not handled here"); continue
            data=open(os.path.join(d,fn),"rb").read()
            if data.count(b"PK\x03\x04")==0:
                summ[f'{key}_fullenc']+=1
                if cat=="archive": L(f"[{cat}] {fn}: 0 headers — FULLY ENCRYPTED, unrecoverable")
                continue
            r,seen=recover_zip_localscan(data, os.path.join(OUTR,cat,fn))
            if r:
                summ[f'{key}_rec']+=r; summ[f'{key}_files_ok']+=1
                L(f"[{cat}] {fn}: recovered {r} members (of {seen} local headers found)")
    L("\n=== REPAIR v2 SUMMARY ===")
    L(f"Standalone archives: {summ['zip_files_ok']} yielded {summ['zip_rec']} files; "
      f"{summ['zip_fullenc']} fully-encrypted; {summ['zip_rar']} RAR (need unrar)")
    L(f"Office (OOXML)     : {summ['off_files_ok']} yielded {summ['off_rec']} parts; "
      f"{summ['off_fullenc']} fully-encrypted")
    L(f"Output: {OUTR}")

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