#!/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()
# ============================================================================

"""Reconstruct complete Tally company folders from unencrypted Quick Heal .cfa backups.
Groups files by adjacent backup-id clusters (same company/snapshot). Small .cfa = QHBK+zlib;
large .cfa = raw original file."""
import json, struct, zlib, re, os
REC=1024;SEC=512;CLU=4096;DEV="/dev/sdf2"
OUT="/mnt/d/MS/quickheal_recovered/tally"
M={int(k):v for k,v in json.load(open("mft_meta.json")).items()}
def parse(nm):
    b=nm[:-7] if nm.endswith(".Funder") else nm
    if b.endswith(".cfa"): b=b[:-4]
    m=re.match(r'(.+)_(\d+)$',b); return (m.group(1),int(m.group(2))) if m else (b,None)
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 prl(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
fm=open("mft.raw","rb"); dev=open(DEV,"rb")
def extract(ino):
    fm.seek(ino*REC);rec=fixup(fm.read(REC))
    first=struct.unpack_from("<H",rec,20)[0];i=first;size=0;rl=None;res=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];res=rec[i+co:i+co+cs];size=cs
            else:
                size=struct.unpack_from("<Q",rec,i+0x30)[0];rl=prl(rec[i:i+al],struct.unpack_from("<H",rec,i+0x20)[0])
        i+=al
    if res is not None:return res
    out=bytearray()
    for lcn,rln in (rl or []):
        if lcn is None:out+=b"\x00"*(rln*CLU)
        else:dev.seek(lcn*CLU);out+=dev.read(rln*CLU)
    return bytes(out[:size])
def unwrap(d):
    if d[:4]==b'QHBK':
        z=d.find(b'\x78\x9c',4)
        if 0<=z<=16:
            try: return zlib.decompressobj().decompress(d[z:])
            except: return None
    return d   # large/raw .cfa = original file as-is

# gather unencrypted Tally company-data files
TSET=("company.900","manager.900","tranmgr.900","linkmgr.900","cmpsave.900","addlcmp.900","sumtran.900",
      "texcl.tsf","taccess.tsf","tstate.tsf","tupdate.tsf","tintmsg.tsf","tmessage.tsf")
files=[]
for ino,r in M.items():
    if r["d"] or not r["n"]: continue
    nm=r["n"]
    if not (nm.lower().endswith(".cfa") and not nm.endswith(".Funder")): continue
    base,bid=parse(nm)
    if base.lower() in TSET and bid is not None:
        files.append((bid,base,r["s"] or 0,ino))
files.sort()
clusters=[]; cur=[]
for f in files:
    if cur and f[0]-cur[-1][0]>20: clusters.append(cur); cur=[]
    cur.append(f)
if cur: clusters.append(cur)
# keep only COMPLETE sets (have TranMgr.900 = actual company data)
complete=[c for c in clusters if any(b.lower()=="tranmgr.900" for _,b,_,_ in c)]
print(f"complete company sets: {len(complete)}")
for idx,c in enumerate(complete,1):
    tm=[s for _,b,s,_ in c if b.lower()=="tranmgr.900"][0]
    folder=os.path.join(OUT, f"Company_{idx}_TranMgr{tm//1048576}MB")
    os.makedirs(folder, exist_ok=True)
    ok=0
    for bid,base,sz,ino in c:
        d=extract(ino); orig=unwrap(d)
        if orig is None: continue
        # last write wins per filename (keep the one from this cluster)
        open(os.path.join(folder,base),"wb").write(orig); ok+=1
    print(f"  {os.path.basename(folder)}: wrote {ok} files (ids {c[0][0]}-{c[-1][0]})")
print(f"output: {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)
# ============================================================================
