"""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>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)