May 21May 21 Localization Does anyone have an idea to write a tutorial to restore filenames from Yeti.big and unpack data from GLBs (not to be confused with the open standard 3D model file format by Khronos Group)? The GLBs have data compression/encryption/obfuscation. I tried all Aluigi's QuickBMS script but they failed. Yeti.big: https://mega.nz/file/sSRkFTCY#9N-2CMFeRyzTDshGhqnBZf6tpq4BrifLchIG2L7u7fU .glb: https://mega.nz/file/NaRjQIpD#Mvs83RV9HtZMiYzHQn0-TSMwsn-0_dcRGOxu2pM_GVQ Here are the Xbox 360 executables to analyze if you want to log filenames from Yeti.big and decrypt GLBs graw2xbox360executables.rar
May 22May 22 Localization Regarding the glb files, I managed to figure out their structure: Header: 4 bytes, version/ID 4 bytes file count Name index: 4 bytes name size name 4 bytes compressed_size 4 bytes decompressed_size 4 bytes compression_flag ? 4 bytes offset - data adress ect. RAW DATA -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Unfortunately, I was unable to decompress them. Here's the code to unpack -- no decompression import os import struct import tkinter as tk from tkinter import filedialog, messagebox, scrolledtext def read_u32(f): data = f.read(4) if len(data) != 4: raise EOFError("Unexpected end of file while reading uint32.") return struct.unpack("<I", data)[0] def safe_decode_name(raw): for enc in ("utf-8", "cp1250", "latin-1"): try: return raw.decode(enc).strip("\x00") except Exception: pass return raw.hex() def sanitize_filename(name): bad = '<>:"/\\|?*' for ch in bad: name = name.replace(ch, "_") name = name.strip() return name or "unnamed_file" def extract_glb_archive(input_path, output_dir, log): with open(input_path, "rb") as f: archive_size = os.path.getsize(input_path) version_id = f.read(4) if len(version_id) != 4: raise Exception("File is too small or corrupted.") file_count = read_u32(f) log("") log(f"Archive ID / Version : {version_id.hex(' ').upper()}") log(f"Total Files : {file_count}") entries = [] for i in range(file_count): name_size = read_u32(f) if name_size <= 0 or name_size > 4096: raise Exception(f"Suspicious name size at entry {i}: {name_size}") name_raw = f.read(name_size) if len(name_raw) != name_size: raise Exception(f"Failed to read file name at entry {i}") name = safe_decode_name(name_raw) compressed_size = read_u32(f) decompressed_size = read_u32(f) compression_flag = read_u32(f) data_offset = read_u32(f) entries.append({ "name": name, "compressed_size": compressed_size, "decompressed_size": decompressed_size, "compression_flag": compression_flag, "offset": data_offset, }) extracted_count = 0 skipped_count = 0 total_compressed = 0 total_decompressed = 0 for i, entry in enumerate(entries): name = sanitize_filename(entry["name"]) compressed_size = entry["compressed_size"] decompressed_size = entry["decompressed_size"] compression_flag = entry["compression_flag"] offset = entry["offset"] total_compressed += compressed_size total_decompressed += decompressed_size if offset >= archive_size: log(f"[SKIP] {name}: offset is outside archive.") skipped_count += 1 continue if offset + compressed_size > archive_size: log(f"[WARN] {name}: compressed size exceeds archive bounds. Truncating.") compressed_size = archive_size - offset out_path = os.path.join(output_dir, name) os.makedirs(os.path.dirname(out_path), exist_ok=True) f.seek(offset) data = f.read(compressed_size) with open(out_path, "wb") as out: out.write(data) extracted_count += 1 log( f"{name} | " f"Extracted: {len(data):,} bytes | " f"Decompressed Size: {decompressed_size:,} bytes | " f"Flag: 0x{compression_flag:08X} | " f"Offset: 0x{offset:08X}" ) log("") log("Extraction completed successfully.") class ExtractorGUI: def __init__(self, root): self.root = root self.root.title("GLB Archive Extractor") self.root.geometry("950x620") self.root.resizable(False, False) self.input_path = tk.StringVar() self.output_dir = tk.StringVar() tk.Label(root, text="GLB Archive File:", font=("Arial", 10, "bold")).pack(anchor="w", padx=10, pady=(10, 2)) frame1 = tk.Frame(root) frame1.pack(fill="x", padx=10) tk.Entry(frame1, textvariable=self.input_path).pack(side="left", fill="x", expand=True) tk.Button(frame1, text="Browse", command=self.select_input).pack(side="left", padx=5) tk.Label(root, text="Output Folder:", font=("Arial", 10, "bold")).pack(anchor="w", padx=10, pady=(10, 2)) frame2 = tk.Frame(root) frame2.pack(fill="x", padx=10) tk.Entry(frame2, textvariable=self.output_dir).pack(side="left", fill="x", expand=True) tk.Button(frame2, text="Browse", command=self.select_output).pack(side="left", padx=5) tk.Button( root, text="Extract Files", command=self.extract, bg="#2e7d32", fg="white", font=("Arial", 12, "bold"), height=2 ).pack(fill="x", padx=10, pady=15) self.log_box = scrolledtext.ScrolledText(root, font=("Consolas", 9)) self.log_box.pack(fill="both", expand=True, padx=10, pady=(0, 10)) def log(self, text): self.log_box.insert(tk.END, text + "\n") self.log_box.see(tk.END) self.root.update_idletasks() def select_input(self): path = filedialog.askopenfilename( title="Select GLB archive file", filetypes=[ ("GLB files", "*.glb"), ("All files", "*.*") ] ) if path: self.input_path.set(path) def select_output(self): path = filedialog.askdirectory(title="Select output folder") if path: self.output_dir.set(path) def extract(self): input_path = self.input_path.get().strip() output_dir = self.output_dir.get().strip() if not input_path: messagebox.showerror("Error", "No GLB archive file selected.") return if not output_dir: messagebox.showerror("Error", "No output folder selected.") return try: self.log_box.delete("1.0", tk.END) extract_glb_archive(input_path, output_dir, self.log) messagebox.showinfo("Done", "Extraction completed.") except Exception as e: self.log(f"[ERROR] {e}") messagebox.showerror("Error", str(e)) if __name__ == "__main__": root = tk.Tk() app = ExtractorGUI(root) root.mainloop()
May 22May 22 Localization the .glb definitely use some compression algo with a huffman tree with the tree being at the beginning of the data, it's a list of u16 terminated by "00 00" This is at least true for all the compressed XPR2 files I've looked at Scratch all that, it's BPE Edited May 22May 22 by NeoGT404
May 22May 22 Localization Here's my current decompressor that's able to decompress shorter files but not longer ones: EXPORT ssize_t decompress_bpe(const uint8_t *restrict src, const size_t zsize, uint8_t *restrict dst, const ssize_t usize) { if (usize == 0) return 0; if (zsize < 2) return -1; uint8_t ls[0x100]; uint8_t rs[0x100]; uint16_t pc = 0; size_t ip = 0; while (ip < zsize) { uint8_t l = src[ip++]; if (!l) break; if (ip >= zsize) return -1; uint8_t r = src[ip++]; ls[pc] = l; rs[pc++] = r; if (pc >= 0x100) break; } uint16_t tokb = 0x100 - pc; ssize_t op = 0; uint8_t stack[0x200]; uint16_t sp = 0; while ((ip < zsize || sp > 0) && (op < usize || usize == -1)) { uint8_t v; if (sp > 0) v = stack[--sp]; else v = src[ip++]; if (v >= tokb) { if (sp + 2 > 0x200) return -1; stack[sp++] = rs[v - tokb]; stack[sp++] = ls[v - tokb]; } else dst[op++] = v; } return op; }
May 22May 22 Localization 1 hour ago, NeoGT404 said: Here's my current decompressor that's able to decompress shorter files but not longer ones: EXPORT ssize_t decompress_bpe(const uint8_t *restrict src, const size_t zsize, uint8_t *restrict dst, const ssize_t usize) { if (usize == 0) return 0; if (zsize < 2) return -1; uint8_t ls[0x100]; uint8_t rs[0x100]; uint16_t pc = 0; size_t ip = 0; while (ip < zsize) { uint8_t l = src[ip++]; if (!l) break; if (ip >= zsize) return -1; uint8_t r = src[ip++]; ls[pc] = l; rs[pc++] = r; if (pc >= 0x100) break; } uint16_t tokb = 0x100 - pc; ssize_t op = 0; uint8_t stack[0x200]; uint16_t sp = 0; while ((ip < zsize || sp > 0) && (op < usize || usize == -1)) { uint8_t v; if (sp > 0) v = stack[--sp]; else v = src[ip++]; if (v >= tokb) { if (sp + 2 > 0x200) return -1; stack[sp++] = rs[v - tokb]; stack[sp++] = ls[v - tokb]; } else dst[op++] = v; } return op; } I applied your code, and apart from the correct decompression sizes, nothing else matches. The data is completely corrupted. At first, there are several different bytes, and then there's a constant repetition of one byte.
May 22May 22 Author Localization On 5/22/2026 at 7:04 PM, NeoGT404 said: Here's my current decompressor that's able to decompress shorter files but not longer ones: EXPORT ssize_t decompress_bpe(const uint8_t *restrict src, const size_t zsize, uint8_t *restrict dst, const ssize_t usize) { if (usize == 0) return 0; if (zsize < 2) return -1; uint8_t ls[0x100]; uint8_t rs[0x100]; uint16_t pc = 0; size_t ip = 0; while (ip < zsize) { uint8_t l = src[ip++]; if (!l) break; if (ip >= zsize) return -1; uint8_t r = src[ip++]; ls[pc] = l; rs[pc++] = r; if (pc >= 0x100) break; } uint16_t tokb = 0x100 - pc; ssize_t op = 0; uint8_t stack[0x200]; uint16_t sp = 0; while ((ip < zsize || sp > 0) && (op < usize || usize == -1)) { uint8_t v; if (sp > 0) v = stack[--sp]; else v = src[ip++]; if (v >= tokb) { if (sp + 2 > 0x200) return -1; stack[sp++] = rs[v - tokb]; stack[sp++] = ls[v - tokb]; } else dst[op++] = v; } return op; } I'm looking for tools that unpack Yeti.big with filenames. Edited May 25May 25 by mrmaller1905
May 22May 22 Localization 1 hour ago, zbirow said: I applied your code, and apart from the correct decompression sizes, nothing else matches. The data is completely corrupted. At first, there are several different bytes, and then there's a constant repetition of one byte. it works on some of the small (< ~1kb) xml like files (.mfx, .cbo) and I'm pretty sure there's some extra stuff going on because the first token thingy is seemingly garbage (at least to my current decompressor) but I think that might dictate the rest of the compression Edited May 22May 22 by NeoGT404
May 22May 22 Localization 35 minutes ago, NeoGT404 said: it works on some of the small (< ~1kb) xml like files (.mfx, .cbo) and I'm pretty sure there's some extra stuff going on because the first token thingy is seemingly garbage (at least to my current decompressor) but think that might dictate the rest of the compression Notice that it always starts with FE 7F, so it could be some kind of Magic Header or settings.
May 22May 22 Localization 3 hours ago, NeoGT404 said: it works on some of the small (< ~1kb) xml like files (.mfx, .cbo) and I'm pretty sure there's some extra stuff going on because the first token thingy is seemingly garbage (at least to my current decompressor) but think that might dictate the rest of the compression probable BPE structure, but I'm not 100% sure yet. BPE dictionary: pair_l, pair_r pair_l, pair_r pair_l, pair_r ... 00 chunk: 1 byte chunk_size chunk_size bytes compressed stream Edited May 22May 22 by zbirow
May 22May 22 Localization 13 hours ago, NeoGT404 said: that could be it, then maybe a chunk size of 0 being a special command? I analyzed several files and found a repeating structure FE 7F start header FE FF end header 3 unk bytes 00 - separator size data data Although you should pay attention to this 1kb file, because there's only one file in it, and it only has FE 7F FE FF 00 31, so there's nothing after FE FF, because there's no compression there. EDIT Or, 00 is not a sprarator. BPE table - x bytes 2 bytes flags/settings \\\ 00 00 / 40 00 2 bytes big-endian compressed chunk size \\\ 00 31/ 00 6D data which is suitable for larger files, e.g. 40 00 01 29. 40 00 - these are the settings, 01 29 - this is the data block size. Edited May 23May 23 by zbirow
May 24May 24 Localization BPE Structure in C #include <stdint.h> #include <stddef.h> typedef long ssize_t; static uint16_t read_be16(const uint8_t *p) { return ((uint16_t)p[0] << 8) | p[1]; } ssize_t decompress_graw_bpe( const uint8_t *restrict src, size_t zsize, uint8_t *restrict dst, ssize_t usize ) { size_t ip = 0; ssize_t op = 0; while (ip < zsize && (usize < 0 || op < usize)) { uint8_t left[0x100]; uint8_t right[0x100]; for (int i = 0; i < 0x100; i++) { left[i] = (uint8_t)i; right[i] = 0; } uint16_t code = 0; while (code < 0x100) { if (ip >= zsize) return -1; uint8_t count = src[ip++]; if (count > 0x7F) { code += count - 0x7F; count = 0; } for (uint16_t i = 0; i <= count && code < 0x100; i++, code++) { if (ip >= zsize) return -1; uint8_t l = src[ip++]; left[code] = l; if (l != code) { if (ip >= zsize) return -1; right[code] = src[ip++]; } } } if (ip + 2 > zsize) return -1; uint16_t packed_size = read_be16(src + ip); ip += 2; if (ip + packed_size > zsize) return -1; size_t block_end = ip + packed_size; while (ip < block_end) { uint8_t stack[0x1000]; uint16_t sp = 0; stack[sp++] = src[ip++]; while (sp > 0) { uint8_t v = stack[--sp]; if (left[v] == v) { if (usize >= 0 && op >= usize) return -1; dst[op++] = v; } else { if (sp + 2 > sizeof(stack)) return -1; stack[sp++] = right[v]; stack[sp++] = left[v]; } } } } if (usize >= 0 && op != usize) return -1; return op; } Python unpack code: import argparse import os import struct import sys import threading import queue def read_u32(f): data = f.read(4) if len(data) != 4: raise EOFError("Unexpected end of file while reading uint32.") return struct.unpack("<I", data)[0] def safe_decode_name(raw): for enc in ("utf-8", "cp1250", "latin-1"): try: return raw.decode(enc).strip("\x00") except Exception: pass return raw.hex() def sanitize_filename(name): bad = '<>:"/\\|?*' for ch in bad: name = name.replace(ch, "_") name = name.strip() return name or "unnamed_file" def parse_entries(input_path): entries = [] with open(input_path, "rb") as f: version_id = f.read(4) if len(version_id) != 4: raise ValueError("File is too small or corrupted.") file_count = read_u32(f) for i in range(file_count): name_size = read_u32(f) if name_size <= 0 or name_size > 4096: raise ValueError(f"Suspicious name size at entry {i}: {name_size}") name_raw = f.read(name_size) if len(name_raw) != name_size: raise ValueError(f"Failed to read file name at entry {i}") entries.append( { "index": i, "name": safe_decode_name(name_raw), "compressed_size": read_u32(f), "decompressed_size": read_u32(f), "compression_flag": read_u32(f), "offset": read_u32(f), } ) return version_id, entries def decode_bpe_to_file(data, out, expected_size=None): """Decode Philip Gage style block BPE used by these GLB entries.""" pos = 0 decoded_size = 0 block_count = 0 write_buffer = bytearray() def emit(value): nonlocal decoded_size, write_buffer write_buffer.append(value) decoded_size += 1 if len(write_buffer) >= 65536: out.write(write_buffer) write_buffer.clear() while pos < len(data): left = list(range(256)) right = [0] * 256 code = 0 if pos >= len(data): break count = data[pos] pos += 1 while True: if count > 127: code += count - 127 count = 0 if code == 256: break for _ in range(count + 1): if pos >= len(data): raise EOFError("Truncated BPE pair table.") left_value = data[pos] pos += 1 left[code] = left_value if code != left_value: if pos >= len(data): raise EOFError("Truncated BPE pair table right value.") right[code] = data[pos] pos += 1 code += 1 if code == 256: break if code == 256: break if pos >= len(data): raise EOFError("Truncated BPE table control byte.") count = data[pos] pos += 1 if pos + 2 > len(data): raise EOFError("Truncated BPE block size.") packed_size = (data[pos] << 8) | data[pos + 1] pos += 2 if pos + packed_size > len(data): raise EOFError( f"Truncated BPE block data: need {packed_size}, have {len(data) - pos}." ) end = pos + packed_size stack = [] while pos < end: stack.append(data[pos]) pos += 1 while stack: value = stack.pop() if value == left[value]: emit(value) else: stack.append(right[value]) stack.append(left[value]) block_count += 1 if write_buffer: out.write(write_buffer) if expected_size is not None and decoded_size != expected_size: raise ValueError( f"Decoded size mismatch: got {decoded_size}, expected {expected_size}." ) return decoded_size, block_count def extract_glb_archive(input_path, output_dir, raw=False, log=print): archive_size = os.path.getsize(input_path) version_id, entries = parse_entries(input_path) log(f"Archive : {input_path}") log(f"Archive ID / Version : {version_id.hex(' ').upper()}") log(f"Total Files : {len(entries)}") extracted_count = 0 skipped_count = 0 with open(input_path, "rb") as f: for entry in entries: name = sanitize_filename(entry["name"]) compressed_size = entry["compressed_size"] decompressed_size = entry["decompressed_size"] compression_flag = entry["compression_flag"] offset = entry["offset"] if offset >= archive_size: log(f"[SKIP] {name}: offset is outside archive.") skipped_count += 1 continue if offset + compressed_size > archive_size: raise ValueError(f"{name}: compressed size exceeds archive bounds.") out_path = os.path.join(output_dir, name) os.makedirs(os.path.dirname(out_path), exist_ok=True) f.seek(offset) data = f.read(compressed_size) with open(out_path, "wb") as out: if raw or compression_flag == 0: out.write(data) written_size = len(data) block_count = 0 else: written_size, block_count = decode_bpe_to_file( data, out, decompressed_size ) extracted_count += 1 log( f"{entry['index']:04d} {name} | " f"written={written_size:,} | " f"packed={compressed_size:,} | " f"expected={decompressed_size:,} | " f"flag=0x{compression_flag:08X} | " f"blocks={block_count}" ) log("") log(f"Done. Extracted={extracted_count}, skipped={skipped_count}") def default_output_dir(input_path): parent = os.path.dirname(os.path.abspath(input_path)) name = os.path.splitext(os.path.basename(input_path))[0] return os.path.join(parent, name) def launch_gui(): import tkinter as tk from tkinter import filedialog, messagebox, scrolledtext class ExtractorGUI: def __init__(self, root): self.root = root self.root.title("GRAW 2 GLB Extractor") self.root.geometry("980x640") self.root.minsize(780, 520) self.input_path = tk.StringVar() self.output_dir = tk.StringVar() self.events = queue.Queue() self.worker = None tk.Label(root, text="GLB archive:", font=("Arial", 10, "bold")).pack( anchor="w", padx=10, pady=(10, 2) ) file_frame = tk.Frame(root) file_frame.pack(fill="x", padx=10) tk.Entry(file_frame, textvariable=self.input_path).pack( side="left", fill="x", expand=True ) tk.Button(file_frame, text="Browse", command=self.select_input).pack( side="left", padx=(5, 0) ) tk.Label(root, text="Output folder:", font=("Arial", 10, "bold")).pack( anchor="w", padx=10, pady=(10, 2) ) output_frame = tk.Frame(root) output_frame.pack(fill="x", padx=10) tk.Entry(output_frame, textvariable=self.output_dir, state="readonly").pack( side="left", fill="x", expand=True ) self.extract_button = tk.Button( root, text="Extract Files", command=self.extract, bg="#2e7d32", fg="white", font=("Arial", 12, "bold"), height=2, ) self.extract_button.pack(fill="x", padx=10, pady=15) self.log_box = scrolledtext.ScrolledText(root, font=("Consolas", 9)) self.log_box.pack(fill="both", expand=True, padx=10, pady=(0, 10)) self.root.after(100, self.poll_events) def select_input(self): path = filedialog.askopenfilename( title="Select GLB archive", filetypes=[("GLB archives", "*.glb"), ("All files", "*.*")], ) if not path: return self.input_path.set(path) self.output_dir.set(default_output_dir(path)) def log(self, text): self.log_box.insert(tk.END, text + "\n") self.log_box.see(tk.END) def set_running(self, running): self.extract_button.config( state=("disabled" if running else "normal"), text=("Extracting..." if running else "Extract Files"), ) def extract(self): input_path = self.input_path.get().strip() if not input_path: messagebox.showerror("Error", "No GLB archive selected.") return output_dir = default_output_dir(input_path) self.output_dir.set(output_dir) self.log_box.delete("1.0", tk.END) self.log(f"Output folder: {output_dir}") self.log("") self.set_running(True) self.worker = threading.Thread( target=self.run_extract, args=(input_path, output_dir), daemon=True ) self.worker.start() def run_extract(self, input_path, output_dir): try: extract_glb_archive( input_path, output_dir, log=lambda text: self.events.put(("log", text)), ) self.events.put(("done", output_dir)) except Exception as exc: self.events.put(("error", str(exc))) def poll_events(self): try: while True: event, value = self.events.get_nowait() if event == "log": self.log(value) elif event == "done": self.set_running(False) self.log("") self.log(f"Finished. Files are in: {value}") messagebox.showinfo("Done", f"Extraction completed:\n{value}") elif event == "error": self.set_running(False) self.log(f"[ERROR] {value}") messagebox.showerror("Error", value) except queue.Empty: pass self.root.after(100, self.poll_events) root = tk.Tk() ExtractorGUI(root) root.mainloop() def main(): if len(sys.argv) == 1: launch_gui() return parser = argparse.ArgumentParser( description="Extract and BPE-decompress GRAW Xbox 360 GLB archives." ) parser.add_argument("archives", nargs="*", help="Input .glb archive(s).") parser.add_argument( "-o", "--output", default="_decoded_glb", help="Output directory. Defaults to _decoded_glb.", ) parser.add_argument( "--raw", action="store_true", help="Write packed entry data without BPE decompression.", ) parser.add_argument( "--gui", action="store_true", help="Launch the graphical interface.", ) args = parser.parse_args() if args.gui: launch_gui() return if not args.archives: parser.error("No archives supplied. Use --gui or pass one or more .glb files.") multiple = len(args.archives) > 1 for archive in args.archives: base_output = args.output if multiple: base_output = os.path.join( args.output, os.path.splitext(os.path.basename(archive))[0] ) extract_glb_archive(archive, base_output, raw=args.raw) if __name__ == "__main__": main() Edited May 24May 24 by zbirow
Create an account or sign in to comment