"""Minimal UnityFS bundle extractor (LZ4/LZ4HC + uncompressed blocks)."""
import struct

def lz4_decompress(src, dst_size):
    out=bytearray(); i=0; n=len(src)
    while i<n:
        tok=src[i]; i+=1
        lit=tok>>4
        if lit==15:
            while True:
                bb=src[i]; i+=1; lit+=bb
                if bb!=255: break
        out+=src[i:i+lit]; i+=lit
        if i>=n: break
        off=src[i]|(src[i+1]<<8); i+=2
        ml=tok&15
        if ml==15:
            while True:
                bb=src[i]; i+=1; ml+=bb
                if bb!=255: break
        ml+=4
        start=len(out)-off
        for j in range(ml):
            out.append(out[start+j])
    return bytes(out[:dst_size])

def extract(path):
    """Return (nodes, data_bytes). nodes=[(off,size,flags,name)]."""
    b=open(path,'rb').read()
    if b[:7]!=b'UnityFS': raise ValueError('not UnityFS')
    p=8
    ver,=struct.unpack_from('>I',b,p); p+=4
    def rstr(p):
        e=b.index(b'\x00',p); return b[p:e].decode(), e+1
    uver,p=rstr(p); ueng,p=rstr(p)
    size,=struct.unpack_from('>q',b,p); p+=8
    cblk,=struct.unpack_from('>I',b,p); p+=4
    ublk,=struct.unpack_from('>I',b,p); p+=4
    flags,=struct.unpack_from('>I',b,p); p+=4
    if flags & 0x80:  # blocksInfo at end
        bi=b[len(b)-cblk:]
    else:
        if flags & 0x200:
            p=(p+15)&~15
        bi=b[p:p+cblk]; p+=cblk
    comp=flags&0x3f
    blocks_info=bi if comp==0 else lz4_decompress(bi,ublk)
    q=16
    bc,=struct.unpack_from('>I',blocks_info,q); q+=4
    blocks=[]
    for _ in range(bc):
        u,c,f=struct.unpack_from('>IIH',blocks_info,q); q+=10
        blocks.append((u,c,f))
    ncount,=struct.unpack_from('>I',blocks_info,q); q+=4
    nodes=[]
    for _ in range(ncount):
        off,sz,fl=struct.unpack_from('>qqI',blocks_info,q); q+=20
        e=blocks_info.index(b'\x00',q); name=blocks_info[q:e].decode(); q=e+1
        nodes.append((off,sz,fl,name))
    if not (flags & 0x80):
        pass
    data=bytearray()
    for (u,c,f) in blocks:
        blk=b[p:p+c]; p+=c
        data += blk if (f&0x3f)==0 else lz4_decompress(blk,u)
    return nodes, bytes(data)

if __name__=='__main__':
    import sys,glob
    needle=sys.argv[1].encode() if len(sys.argv)>1 else b'actualVersion'
    d='/mnt/d/SteamLibrary/steamapps/common/Sand Playtest/Sand_Data/StreamingAssets/aa/StandaloneWindows64'
    for f in sorted(glob.glob(d+'/*.bundle')):
        import os
        if os.path.getsize(f) > 30_000_000:  # skip huge ones in quick pass
            continue
        try:
            nodes,data=extract(f)
            if needle in data:
                print('HIT', os.path.basename(f), 'at', data.find(needle))
        except Exception as e:
            print('ERR', os.path.basename(f), e)
