From f309ac22bb70217c48116c5ba7b213450252897a Mon Sep 17 00:00:00 2001 From: suleram <154234762+suleram@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:10:40 +0100 Subject: [PATCH 01/67] Open disassembly file as unicode --- Parser/parse_v8cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Parser/parse_v8cache.py b/Parser/parse_v8cache.py index 24925d6..302dc17 100644 --- a/Parser/parse_v8cache.py +++ b/Parser/parse_v8cache.py @@ -30,7 +30,7 @@ def run_disassembler_binary(binary_path, file_name, out_file_name): ) # Open the output file in write mode - with open(out_file_name, 'w') as outfile: + with open(out_file_name, 'w', encoding="unicode") as outfile: # Call the binary with the file name as argument and pipe the output to the file try: result = subprocess.run([binary_path, file_name], stdout=outfile, stderr=subprocess.PIPE, text=True) From ca2c158144f722aef7c7fd38a10ebe43bb146d71 Mon Sep 17 00:00:00 2001 From: suleram <154234762+suleram@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:11:57 +0100 Subject: [PATCH 02/67] Propagate arguments passed by two-dimensional Scopes --- Parser/sfi_file_parser.py | 8 +++++--- Simplify/global_scope_replace.py | 14 +++++++++----- Simplify/simplify.py | 11 ++++++----- view8.py | 5 ++--- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Parser/sfi_file_parser.py b/Parser/sfi_file_parser.py index ac5d780..bbcb332 100644 --- a/Parser/sfi_file_parser.py +++ b/Parser/sfi_file_parser.py @@ -1,6 +1,7 @@ from Parser.shared_function_info import SharedFunctionInfo, CodeLine from parse import parse import re +import json all_functions = {} repeat_last_line = False @@ -12,7 +13,7 @@ def set_repeat_line_flag(flag): def get_next_line(file): - with open(file) as f: + with open(file, errors='ignore') as f: for line in f: line = line.strip() if not line: @@ -75,8 +76,9 @@ def parse_const_line(lines, func_name): if not address: return var_idx, value if value.startswith(" ').replace('"', '\\"') - return var_idx, f'"{value}"' + value = json.dumps(value.split("#", 1)[-1].rstrip('> ')) #.replace('"', '\\"') + #return var_idx, f'"{value}"' + return var_idx, value if value.startswith(" ') if " " in value else "" return var_idx, parse_shared_function_info(lines, value, func_name) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index 2eaada6..d63aeab 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -5,7 +5,7 @@ def replace_global_scope(all_functions): scope_assignments = {} scope_counts = defaultdict(int) - + # Regex pattern to match Scope[num][num] = value pattern = re.compile(r'Scope\[(\d+)\]\[(\d+)\] = (\S+)') @@ -18,6 +18,7 @@ def replace_global_scope(all_functions): key = (match.group(1), match.group(2)) value = match.group(3) if value in ("null", "undefined"): + line_obj.decompiled = "" continue if key in scope_assignments or not value.startswith("func_"): # If the same Scope is assigned different values, mark it as invalid @@ -26,14 +27,17 @@ def replace_global_scope(all_functions): scope_assignments[key] = value scope_counts[key] += 1 + pattern = re.compile(r'Scope\[(\d+)\]\[(\d+)\]') # Second pass: Replace Scope[num][num] with value if it's set only once for func in all_functions.values(): for line_obj in func.code: new_line = line_obj.decompiled - for key, count in scope_counts.items(): - if count == 1 and scope_assignments[key] is not None: - scope_pattern = re.escape(f'Scope[{key[0]}][{key[1]}]') - new_line = re.sub(scope_pattern, scope_assignments[key], new_line) + match = pattern.search(new_line) + if match: + + key = (match.group(1), match.group(2)) + if scope_counts[key] == 1 and scope_assignments[key] is not None: + new_line = new_line.replace(match.group(0), scope_assignments[key]) line_obj.decompiled = new_line diff --git a/Simplify/simplify.py b/Simplify/simplify.py index 1df5f91..50be147 100644 --- a/Simplify/simplify.py +++ b/Simplify/simplify.py @@ -47,12 +47,13 @@ def reg_is_constant(reg, value): def get_context_idx_from_var(var): - if var.was_overwritten: - return - pattern = r"Scope\[(\d+)\]" + #if var.was_overwritten: + # return + pattern = r"^Scope\[(\d+)\]$" match = re.match(pattern, var.value) if match: return int(match.group(1)) + return None @@ -149,10 +150,10 @@ def replace_scope(match): scope_start, steps = scope.split("-") start_context = reg_scope['current_context'] - if scope_start in reg_scope: + if scope_start in reg_scope and get_context_idx_from_var(reg_scope[scope_start]) != None: start_context = get_context_idx_from_var(reg_scope[scope_start]) - elif scope_start in prev_reg_scope: + elif scope_start in prev_reg_scope and get_context_idx_from_var(prev_reg_scope[scope_start]) != None: start_context = get_context_idx_from_var(prev_reg_scope[scope_start]) return f"Scope[{function_context_stack.get_context(start_context, int(steps))}]" diff --git a/view8.py b/view8.py index 171d834..aea26fa 100644 --- a/view8.py +++ b/view8.py @@ -22,8 +22,7 @@ def decompile(all_functions): print(f"Decompiling {len(all_functions)} functions.") for name in list(all_functions)[::-1]: all_functions[name].decompile() - # replace_global_scope(all_functions) - + replace_global_scope(all_functions) def export_to_file(out_name, all_functions, format_list): print(f"Exporting to file {out_name}.") @@ -52,6 +51,6 @@ def main(): export_to_file(args.output_file, all_func, args.export_format) print(f"Done.") - if __name__ == "__main__": main() + From 285ac212b1208f9a540e0f48fc8f5a04ab576d50 Mon Sep 17 00:00:00 2001 From: suleram <154234762+suleram@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:13:18 +0100 Subject: [PATCH 03/67] Include/exclude defined functions from decompilation --- view8.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/view8.py b/view8.py index aea26fa..29f628d 100644 --- a/view8.py +++ b/view8.py @@ -24,6 +24,42 @@ def decompile(all_functions): all_functions[name].decompile() replace_global_scope(all_functions) + +def build_declaration_map(functions): + declared_by = {} + + for func_name, sfi in functions.items(): + declarer = sfi.declarer + if declarer: + if declarer not in declared_by: + declared_by[declarer] = [] + declared_by[declarer].append(func_name) + + return declared_by + +def remove_exclude_functions(all_functions, exclude_list): + declaration_table = build_declaration_map(all_functions) + number_of_functoin = len(exclude_list) + while exclude_list: + current_function = exclude_list.pop() + del all_functions[current_function] + next_level = declaration_table.get(current_function, []) + number_of_functoin += len(next_level) + exclude_list += next_level + print(f"Removed {number_of_functoin} functions") + +def get_included_functions(all_functions, include_list): + declaration_table = build_declaration_map(all_functions) + number_of_functoin = len(include_list) + new_all_func = {} + while include_list: + current_function = include_list.pop() + new_all_func[current_function] = all_functions[current_function] + next_level = declaration_table.get(current_function, []) + number_of_functoin += len(next_level) + include_list += next_level + return new_all_func + def export_to_file(out_name, all_functions, format_list): print(f"Exporting to file {out_name}.") with open(out_name, "w") as f: @@ -40,6 +76,8 @@ def main(): parser.add_argument('--export_format', '-e', nargs='+', choices=['v8_opcode', 'translated', 'decompiled'], help="Specify the export format(s). Options are 'v8_opcode', 'translated', and 'decompiled'. Multiple options can be combined.", default=['decompiled']) + parser.add_argument('--include', '-i', nargs='+', help="Functions tree to Include.", default=[]) + parser.add_argument('--exclude', '-x', nargs='+', help="Functions tree to Exclude.", default=[]) args = parser.parse_args() @@ -47,10 +85,18 @@ def main(): raise FileNotFoundError(f"The input file {args.input_file} does not exist.") all_func = disassemble(args.input_file, args.disassembled, args.path) + + if args.exclude: + remove_exclude_functions(all_func, args.exclude) #["func_unknown_0x1445baf27101", "func_unknown_0x95277699f59"]) + decompile(all_func) + + if args.include: + all_func = get_included_functions(all_func, args.include) + export_to_file(args.output_file, all_func, args.export_format) print(f"Done.") + if __name__ == "__main__": main() - From 101e179cc72d5f4ac987cad1c27a2adf314e9ca0 Mon Sep 17 00:00:00 2001 From: suleram <154234762+suleram@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:17:46 +0100 Subject: [PATCH 04/67] Removed quotes around global symbols --- Parser/shared_function_info.py | 23 ++++++++++++++++++----- Simplify/simplify.py | 4 ++-- Translate/translate_table.py | 18 +++++++++--------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index ff373e2..bf7a285 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -1,6 +1,6 @@ from Translate.translate import translate_bytecode from Simplify.simplify import simplify_translated_bytecode - +import re class CodeLine: def __init__(self, opcode="", line="", inst="", translated="", decompiled=""): @@ -41,12 +41,25 @@ def simplify_bytecode(self): simplify_translated_bytecode(self, self.code) def replace_const_pool(self): - replacements = {f"ConstPool[{idx}]": var for idx, var in enumerate(self.const_pool)} + + def replacement(match): + index = int(match.group(2)) + value = self.const_pool[index] + if match.group(1) == "ConstPool": #Not ConstPoolLiteral + return value.strip('"') + return value + + # Regular expression to match patterns A[NUMBER] or B[NUMBER] + pattern = r'(ConstPoolLiteral|ConstPool)\[(\d+)\]' + + #replacements = {f"ConstPool[{idx}]": var.strip('"') for idx, var in enumerate(self.const_pool)} + #replacements.update({f"ConstPoolLiteral[{idx}]": var for idx, var in enumerate(self.const_pool)}) + for line in self.code: - if not line.visible: + if not line.visible or "ConstPool" not in line.decompiled: continue - for const_id, var in replacements.items(): - line.decompiled = line.decompiled.replace(const_id, var) + + line.decompiled = re.sub(pattern, replacement, line.decompiled) def decompile(self): self.translate_bytecode() diff --git a/Simplify/simplify.py b/Simplify/simplify.py index 50be147..775b755 100644 --- a/Simplify/simplify.py +++ b/Simplify/simplify.py @@ -36,11 +36,11 @@ def reg_is_constant(reg, value): return False # Variable is set to a constant value - if re.search(r"^[\(]*(Scope|ConstPool|<|true|false|Undefined|Null|null|[+-]?\d)", value): + if re.search(r"^[\(]*(Scope|ConstPool|ConstPoolLiteral|<|true|false|Undefined|Null|null|[+-]?\d)", value): return True # Variable is set to register[ConstPool[idx]] - if re.search(r"^[ra]\d+\[[\(]*ConstPool\[\d+\]", value): + if re.search(r"^[ra]\d+\[[\(]*(ConstPool|ConstPoolLiteral)\[\d+\]", value): return True return False diff --git a/Translate/translate_table.py b/Translate/translate_table.py index 364cd54..1aed0a8 100644 --- a/Translate/translate_table.py +++ b/Translate/translate_table.py @@ -174,11 +174,11 @@ def get_scope_id(args): "LdaLookupSlot": lambda obj: f"ACCU = ConstPool{obj.args[0]}", "LdaContextSlot": lambda obj: f"ACCU = Scope[{get_scope_id(obj.args)}]{obj.args[1]}", "LdaLookupContextSlot": lambda obj: f"ACCU = Scope[CURRENT-{obj.args[2][1:-1]}]{obj.args[1]}", - "LdaConstant": lambda obj: f"ACCU = ConstPool{obj.args[0]}", - "LdaNamedProperty": lambda obj: f"ACCU = {obj.args[0]}[ConstPool{obj.args[1]}]", - "LdaNamedPropertyFromSuper": lambda obj: f"ACCU = ACCU[ConstPool{obj.args[1]}]", - "GetNamedPropertyFromSuper": lambda obj: f"ACCU = ACCU[ConstPool{obj.args[1]}]", - "GetNamedProperty": lambda obj: f"ACCU = {obj.args[0]}[ConstPool{obj.args[1]}]", + "LdaConstant": lambda obj: f"ACCU = ConstPoolLiteral{obj.args[0]}", + "LdaNamedProperty": lambda obj: f"ACCU = {obj.args[0]}[ConstPoolLiteral{obj.args[1]}]", + "LdaNamedPropertyFromSuper": lambda obj: f"ACCU = ACCU[ConstPoolLiteral{obj.args[1]}]", + "GetNamedPropertyFromSuper": lambda obj: f"ACCU = ACCU[ConstPoolLiteral{obj.args[1]}]", + "GetNamedProperty": lambda obj: f"ACCU = {obj.args[0]}[ConstPoolLiteral{obj.args[1]}]", "GetKeyedProperty": lambda obj: f"ACCU = {obj.args[0]}[ACCU]", "GetTemplateObject": lambda obj: f"ACCU = ConstPool{obj.args[0]}", "LdaKeyedProperty": lambda obj: f"ACCU = {obj.args[0]}[ACCU]", @@ -214,14 +214,14 @@ def get_scope_id(args): # "StaLookupContextSlot": lambda obj: f"Scope[{get_scope_id(obj.args)}]{obj.args[1]} = ACCU", "StaCurrentContextSlot": lambda obj: f"Scope[CURRENT]{obj.args[0]} = ACCU", "StaInArrayLiteral": lambda obj: f"{obj.args[0]}[{obj.args[1]}] = ACCU", - "StaNamedOwnProperty": lambda obj: f"{obj.args[0]}[ConstPool{obj.args[1]}] = ACCU", - "StaNamedProperty": lambda obj: f"{obj.args[0]}[ConstPool{obj.args[1]}] = ACCU", + "StaNamedOwnProperty": lambda obj: f"{obj.args[0]}[ConstPoolLiteral{obj.args[1]}] = ACCU", + "StaNamedProperty": lambda obj: f"{obj.args[0]}[ConstPoolLiteral{obj.args[1]}] = ACCU", "StaKeyedProperty": lambda obj: f"{obj.args[0]}[{obj.args[1]}] = ACCU", "StaKeyedPropertyAsDefine": lambda obj: f"{obj.args[0]}[{obj.args[1]}] = ACCU", "StaDataPropertyInLiteral": lambda obj: f"{obj.args[0]}.{obj.args[1]} = ACCU", - "SetNamedProperty": lambda obj: f"{obj.args[0]}[ConstPool{obj.args[1]}] = ACCU", + "SetNamedProperty": lambda obj: f"{obj.args[0]}[ConstPoolLiteral{obj.args[1]}] = ACCU", "SetKeyedProperty": lambda obj: f"{obj.args[0]}[{obj.args[1]}] = ACCU", - "DefineNamedOwnProperty": lambda obj: f"{obj.args[0]}[ConstPool{obj.args[1]}] = ACCU", + "DefineNamedOwnProperty": lambda obj: f"{obj.args[0]}[ConstPoolLiteral{obj.args[1]}] = ACCU", "DefineKeyedOwnPropertyInLiteral": lambda obj: f"{obj.args[0]}[{obj.args[1]}] = ACCU", "DefineKeyedOwnProperty": lambda obj: f"{obj.args[0]}[{obj.args[1]}] = ACCU", From b11b4bd5d785d527198b0a63e23cbaf4cd988a8a Mon Sep 17 00:00:00 2001 From: suleram <154234762+suleram@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:21:29 +0100 Subject: [PATCH 05/67] Fixed mapping Scope variables --- Simplify/simplify.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Simplify/simplify.py b/Simplify/simplify.py index 775b755..b6fc8a1 100644 --- a/Simplify/simplify.py +++ b/Simplify/simplify.py @@ -67,9 +67,15 @@ def is_reg_defined_in_reg_value(reg, value): def create_loop_reg_scope(prev_reg_scope): + reg_scope = {} # Because loop regs can be overwritten during loop iteration we define prev scope as overwritten - reg_scope = {k: Register("", v.all_initialized_index[0], True) for k, v in prev_reg_scope.items() if - not isinstance(v, int)} + for k,v in prev_reg_scope.items(): + if isinstance(v, int): + continue + if get_context_idx_from_var(v) != None: + reg_scope[k] = prev_reg_scope[k] + continue + reg_scope[k] = Register("", v.all_initialized_index[0], True) reg_scope["current_context"] = prev_reg_scope["current_context"] return reg_scope From cdc2129efeb0e6be0bda82aff133e89fe8e91ea1 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 18:28:25 +0100 Subject: [PATCH 06/67] Added .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..915fb32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*/__pycache__/* +__pycache__/* +data/ +*.bak + From 0934fc7ce7680adff57bbc8fa488a1be77908980 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 18:32:03 +0100 Subject: [PATCH 07/67] [FEATURE] Changed commandline arguments --- README.md | 8 ++++---- view8.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) mode change 100644 => 100755 view8.py diff --git a/README.md b/README.md index 9769294..f6faf65 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@

Usage

Command-Line Arguments

    -
  • input_file: The input file name.
  • -
  • output_file: The output file name.
  • +
  • --inp, -i: The input file name
  • +
  • --out, -o: Path to the output (depending on the type of the output, a single file or a directory tree may be generated)
  • --path, -p: Path to disassembler binary (optional).
  • --disassembled, -d: Indicate if the input file is already disassembled (optional).
  • --export_format, -e: Specify the export format(s). Options are v8_opcode, translated, and decompiled. Multiple options can be combined (optional, default: decompiled).
  • @@ -27,10 +27,10 @@

    Basic Usage

    To decompile a V8 bytecode file and export the decompiled code:

    -
    python view8.py input_file output_file
    +
    python view8.py -i input_file -o output_file

    Disassembler Path

    By default, view8 detects the V8 bytecode version of the input file (using VersionDetector.exe) and automatically searches for a compatible disassembler binary in the Bin folder. This can be changed by specifing a different disassembler binary, use the --path (or -p) option:

    -
    python view8.py input_file output_file --path /path/to/disassembler
    +
    python view8.py -i input_file -o output_file --path /path/to/disassembler

    Processing Disassembled Files

    To skip the disassembling process and provide an already disassembled file as the input, use the --disassembled (or -d) flag:

    python view8.py input_file output_file --disassembled
    diff --git a/view8.py b/view8.py old mode 100644 new mode 100755 index 29f628d..4046370 --- a/view8.py +++ b/view8.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 import argparse import os from Parser.parse_v8cache import parse_v8cache_file, parse_disassembled_file @@ -65,26 +66,24 @@ def export_to_file(out_name, all_functions, format_list): with open(out_name, "w") as f: for function_name in list(all_functions)[::-1]: f.write(all_functions[function_name].export(export_v8code="v8_opcode" in format_list, export_translated="translated" in format_list, export_decompiled="decompiled" in format_list)) - def main(): parser = argparse.ArgumentParser(description="View8: V8 cache decompiler.") - parser.add_argument('input_file', help="The input file name.") - parser.add_argument('output_file', help="The output file name.") + parser.add_argument('--inp', '-i', help="The input file name.", default=None) + parser.add_argument('--out', '-o', help="The output file name.", default=None) parser.add_argument('--path', '-p', help="Path to disassembler binary.", default=None) parser.add_argument('--disassembled', '-d', action='store_true', help="Indicate if the input file is already disassembled.") parser.add_argument('--export_format', '-e', nargs='+', choices=['v8_opcode', 'translated', 'decompiled'], help="Specify the export format(s). Options are 'v8_opcode', 'translated', and 'decompiled'. Multiple options can be combined.", default=['decompiled']) - parser.add_argument('--include', '-i', nargs='+', help="Functions tree to Include.", default=[]) + parser.add_argument('--include', '-n', nargs='+', help="Functions tree to Include.", default=[]) parser.add_argument('--exclude', '-x', nargs='+', help="Functions tree to Exclude.", default=[]) - args = parser.parse_args() - if not os.path.isfile(args.input_file): + if not os.path.isfile(args.inp): raise FileNotFoundError(f"The input file {args.input_file} does not exist.") - all_func = disassemble(args.input_file, args.disassembled, args.path) + all_func = disassemble(args.inp, args.disassembled, args.path) if args.exclude: remove_exclude_functions(all_func, args.exclude) #["func_unknown_0x1445baf27101", "func_unknown_0x95277699f59"]) @@ -93,8 +92,9 @@ def main(): if args.include: all_func = get_included_functions(all_func, args.include) - - export_to_file(args.output_file, all_func, args.export_format) + + if args.out: + export_to_file(args.out, all_func, args.export_format) print(f"Done.") From 6f6039207771b149c75576d9468769689c142c49 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 18:36:05 +0100 Subject: [PATCH 08/67] Fixed README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6faf65..421169d 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@
  • decompiled

For example, to export both V8 opcodes and decompiled code side by side:

-
python view8.py input_file output_file -e v8_opcode decompiled
+
python view8.py -i input_file -o output_file -e v8_opcode decompiled

By default, the format used is decompiled.

VersionDetector.exe

From 707941a45e0b6dc84d4b99de3c662ff94b203f75 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 18:40:05 +0100 Subject: [PATCH 09/67] [BUGFIX] Make the input argument required --- view8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view8.py b/view8.py index 4046370..b2a5a57 100755 --- a/view8.py +++ b/view8.py @@ -69,7 +69,7 @@ def export_to_file(out_name, all_functions, format_list): def main(): parser = argparse.ArgumentParser(description="View8: V8 cache decompiler.") - parser.add_argument('--inp', '-i', help="The input file name.", default=None) + parser.add_argument('--inp', '-i', help="The input file name.", default=None, required=True) parser.add_argument('--out', '-o', help="The output file name.", default=None) parser.add_argument('--path', '-p', help="Path to disassembler binary.", default=None) parser.add_argument('--disassembled', '-d', action='store_true', help="Indicate if the input file is already disassembled.") From 9b4b03a16e2de0b714aaa5cfbf3acfced4a4505f Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 19:04:47 +0100 Subject: [PATCH 10/67] [FEATURE] Allow to serialize/deserialize decompiled files --- Parser/shared_function_info.py | 26 +++++++++++++++++++++ README.md | 13 ++++++++--- view8.py | 42 +++++++++++++++++++++++++--------- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index bf7a285..85ffe85 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -1,6 +1,8 @@ from Translate.translate import translate_bytecode from Simplify.simplify import simplify_translated_bytecode import re +import pickle +from typing import List, Optional class CodeLine: def __init__(self, opcode="", line="", inst="", translated="", decompiled=""): @@ -83,3 +85,27 @@ def export(self, export_v8code=False, export_translated=False, export_decompiled if export_line: export_func += export_line + '\n' return export_func + +#### + +# Helper function for serializing multiple functions +def serialize_functions(functions: List[SharedFunctionInfo]) -> bytes: + """Serialize a list of SharedFunctionInfo objects""" + return pickle.dumps(functions, protocol=pickle.HIGHEST_PROTOCOL) + + +def deserialize_functions(data: bytes) -> List[SharedFunctionInfo]: + """Deserialize a list of SharedFunctionInfo objects""" + return pickle.loads(data) + + +def save_functions_to_file(functions: List[SharedFunctionInfo], filename: str): + """Save multiple functions to a file""" + with open(filename, 'wb') as f: + f.write(serialize_functions(functions)) + + +def load_functions_from_file(filename: str) -> List[SharedFunctionInfo]: + """Load multiple functions from a file""" + with open(filename, 'rb') as f: + return deserialize_functions(f.read()) diff --git a/README.md b/README.md index 421169d..e6552f2 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@
  • --out, -o: Path to the output (depending on the type of the output, a single file or a directory tree may be generated)
  • --path, -p: Path to disassembler binary (optional).
  • --disassembled, -d: Indicate if the input file is already disassembled (optional).
  • -
  • --export_format, -e: Specify the export format(s). Options are v8_opcode, translated, and decompiled. Multiple options can be combined (optional, default: decompiled).
  • +
  • --input_format, -f: Indicate format of the input. Options are: raw: the output is a raw JSC file; disassembled: the input file is already disassembled; serialized: the input is already decompiled, and stored in a serialized format (as an object structure, rather than text)
  • +
  • --export_format, -e: Specify the export format(s). Options are v8_opcode, translated, decompiled, and serialized. Multiple options can be combined (optional, default: decompiled).
  • Basic Usage

    @@ -32,14 +33,20 @@

    By default, view8 detects the V8 bytecode version of the input file (using VersionDetector.exe) and automatically searches for a compatible disassembler binary in the Bin folder. This can be changed by specifing a different disassembler binary, use the --path (or -p) option:

    python view8.py -i input_file -o output_file --path /path/to/disassembler

    Processing Disassembled Files

    -

    To skip the disassembling process and provide an already disassembled file as the input, use the --disassembled (or -d) flag:

    -
    python view8.py input_file output_file --disassembled
    +

    To skip the disassembling process and provide an already disassembled file as the input, use the --input_format disassembled (or -f disassembled) option:

    +
    python view8.py -i input_file -o output_file -f disassembled
    +

    Creating and Processing Serialized Files

    +

    Sometimes we may want to decompile the file into a serialized format (preserving all the objects and structures). This type of an output may be easier to post-process than a text format, and useful i.e. for further deobfuscation. To create a serialized output we use a specific export format: --export_format serialized (or -e serialized)

    +
    python view8.py -i input_file -o output_file -e serialized
    +

    If we ever want to load the serialized output back, and decompile it as a different type of an output, we can do it using --input_format serialized (or -f serialized) option:

    +
    python view8.py -i input_file -o output_file -f serialized

    Export Formats

    Specify the export format(s) using the --export_format (or -e) option. You can combine multiple formats:

    • v8_opcode
    • translated
    • decompiled
    • +
    • serialized

    For example, to export both V8 opcodes and decompiled code side by side:

    python view8.py -i input_file -o output_file -e v8_opcode decompiled
    diff --git a/view8.py b/view8.py index b2a5a57..686ffb5 100755 --- a/view8.py +++ b/view8.py @@ -2,8 +2,10 @@ import argparse import os from Parser.parse_v8cache import parse_v8cache_file, parse_disassembled_file +from Parser.shared_function_info import * from Simplify.global_scope_replace import replace_global_scope +#### def disassemble(in_file, input_is_disassembled, disassembler): out_name = 'disasm.tmp' @@ -17,7 +19,6 @@ def disassemble(in_file, input_is_disassembled, disassembler): return parse_disassembled_file(out_name) - def decompile(all_functions): # Decompile print(f"Decompiling {len(all_functions)} functions.") @@ -25,7 +26,6 @@ def decompile(all_functions): all_functions[name].decompile() replace_global_scope(all_functions) - def build_declaration_map(functions): declared_by = {} @@ -62,18 +62,32 @@ def get_included_functions(all_functions, include_list): return new_all_func def export_to_file(out_name, all_functions, format_list): - print(f"Exporting to file {out_name}.") + serialize_only = False + if ('serialized' in format_list): + serialized_name = out_name + if len(format_list) == 1: + serialize_only = True + else: + serialized_name += ".pkl" + print(f"Serializing to file: {serialized_name}") + save_functions_to_file(all_functions, serialized_name) + if serialize_only: + return with open(out_name, "w") as f: + print(f"Exporting to file: {out_name}") for function_name in list(all_functions)[::-1]: f.write(all_functions[function_name].export(export_v8code="v8_opcode" in format_list, export_translated="translated" in format_list, export_decompiled="decompiled" in format_list)) def main(): parser = argparse.ArgumentParser(description="View8: V8 cache decompiler.") + group = parser.add_mutually_exclusive_group(required=False) + group.add_argument('--input_format', '-f', choices=['raw', 'serialized', 'disassembled'], + help="Specify the input format. Options are: 'raw', 'serialized', 'disassembled'(mutually exclusive)", + default='raw') parser.add_argument('--inp', '-i', help="The input file name.", default=None, required=True) parser.add_argument('--out', '-o', help="The output file name.", default=None) parser.add_argument('--path', '-p', help="Path to disassembler binary.", default=None) - parser.add_argument('--disassembled', '-d', action='store_true', help="Indicate if the input file is already disassembled.") - parser.add_argument('--export_format', '-e', nargs='+', choices=['v8_opcode', 'translated', 'decompiled'], + parser.add_argument('--export_format', '-e', nargs='+', choices=['v8_opcode', 'translated', 'decompiled', 'serialized'], help="Specify the export format(s). Options are 'v8_opcode', 'translated', and 'decompiled'. Multiple options can be combined.", default=['decompiled']) parser.add_argument('--include', '-n', nargs='+', help="Functions tree to Include.", default=[]) @@ -81,15 +95,21 @@ def main(): args = parser.parse_args() if not os.path.isfile(args.inp): - raise FileNotFoundError(f"The input file {args.input_file} does not exist.") + raise FileNotFoundError(f"The input file {args.inp} does not exist.") + + if ('serialized' in args.input_format): + print(f"Reading from serialized, already decompiled input: {args.inp}") + all_func = load_functions_from_file(args.inp) + else: + disassembled = False + if 'disassembled' in args.input_format: + disassembled = True + all_func = disassemble(args.inp, disassembled, args.path) + decompile(all_func) - all_func = disassemble(args.inp, args.disassembled, args.path) - if args.exclude: remove_exclude_functions(all_func, args.exclude) #["func_unknown_0x1445baf27101", "func_unknown_0x95277699f59"]) - - decompile(all_func) - + if args.include: all_func = get_included_functions(all_func, args.include) From 7b3351f83359a9379455795a5f24cac61eb4e370 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 19:30:04 +0100 Subject: [PATCH 11/67] [FEATURE] Allow to split output into separate files. Refactoring --- README.md | 8 ++- view8.py | 74 ++++++-------------------- view8_util.py | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 60 deletions(-) create mode 100755 view8_util.py diff --git a/README.md b/README.md index e6552f2..5239f5e 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,13 @@
    • --inp, -i: The input file name
    • --out, -o: Path to the output (depending on the type of the output, a single file or a directory tree may be generated)
    • -
    • --path, -p: Path to disassembler binary (optional).
    • -
    • --disassembled, -d: Indicate if the input file is already disassembled (optional).
    • --input_format, -f: Indicate format of the input. Options are: raw: the output is a raw JSC file; disassembled: the input file is already disassembled; serialized: the input is already decompiled, and stored in a serialized format (as an object structure, rather than text)
    • --export_format, -e: Specify the export format(s). Options are v8_opcode, translated, decompiled, and serialized. Multiple options can be combined (optional, default: decompiled).
    • +
    • --path, -p: Path to disassembler binary. Required if the input is in the raw format.
    • +
    • --tree, -t: Split output into a tree structure (rather than storing all functions in one file). Specify the function that will be used as a top node of the tree. To start from the default main function, use 'start' (optional).
    • +
    • --mainlimit, -l: In tree mode: a tree with depth above this limit will be treated as different module than main (optional).
    • +
    • --include, -n: Functions tree to Include in the output (optional).
    • +
    • --exclude, -x: Functions tree to Exclude from the output (optional).

    Basic Usage

    @@ -59,3 +62,4 @@
  • -d: Retrieves a hash (little-endian) and returns its corresponding version using brute force.
  • -f: Retrieves a file and returns its version.
  • + diff --git a/view8.py b/view8.py index 686ffb5..2aec9aa 100755 --- a/view8.py +++ b/view8.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import argparse import os + +from view8_util import * from Parser.parse_v8cache import parse_v8cache_file, parse_disassembled_file from Parser.shared_function_info import * from Simplify.global_scope_replace import replace_global_scope @@ -26,62 +28,12 @@ def decompile(all_functions): all_functions[name].decompile() replace_global_scope(all_functions) -def build_declaration_map(functions): - declared_by = {} - - for func_name, sfi in functions.items(): - declarer = sfi.declarer - if declarer: - if declarer not in declared_by: - declared_by[declarer] = [] - declared_by[declarer].append(func_name) - - return declared_by - -def remove_exclude_functions(all_functions, exclude_list): - declaration_table = build_declaration_map(all_functions) - number_of_functoin = len(exclude_list) - while exclude_list: - current_function = exclude_list.pop() - del all_functions[current_function] - next_level = declaration_table.get(current_function, []) - number_of_functoin += len(next_level) - exclude_list += next_level - print(f"Removed {number_of_functoin} functions") - -def get_included_functions(all_functions, include_list): - declaration_table = build_declaration_map(all_functions) - number_of_functoin = len(include_list) - new_all_func = {} - while include_list: - current_function = include_list.pop() - new_all_func[current_function] = all_functions[current_function] - next_level = declaration_table.get(current_function, []) - number_of_functoin += len(next_level) - include_list += next_level - return new_all_func - -def export_to_file(out_name, all_functions, format_list): - serialize_only = False - if ('serialized' in format_list): - serialized_name = out_name - if len(format_list) == 1: - serialize_only = True - else: - serialized_name += ".pkl" - print(f"Serializing to file: {serialized_name}") - save_functions_to_file(all_functions, serialized_name) - if serialize_only: - return - with open(out_name, "w") as f: - print(f"Exporting to file: {out_name}") - for function_name in list(all_functions)[::-1]: - f.write(all_functions[function_name].export(export_v8code="v8_opcode" in format_list, export_translated="translated" in format_list, export_decompiled="decompiled" in format_list)) +### def main(): parser = argparse.ArgumentParser(description="View8: V8 cache decompiler.") group = parser.add_mutually_exclusive_group(required=False) - group.add_argument('--input_format', '-f', choices=['raw', 'serialized', 'disassembled'], + group.add_argument('--input_format', '-f', choices=['raw', 'serialized', 'disassembled'], help="Specify the input format. Options are: 'raw', 'serialized', 'disassembled'(mutually exclusive)", default='raw') parser.add_argument('--inp', '-i', help="The input file name.", default=None, required=True) @@ -90,6 +42,8 @@ def main(): parser.add_argument('--export_format', '-e', nargs='+', choices=['v8_opcode', 'translated', 'decompiled', 'serialized'], help="Specify the export format(s). Options are 'v8_opcode', 'translated', and 'decompiled'. Multiple options can be combined.", default=['decompiled']) + parser.add_argument('--tree', '-t', help="Show functions tree, starting from a given node. To start from the default main function, use 'start'", default=None) + parser.add_argument('--mainlimit', '-l', help="In tree mode: a tree with depth above this limit will be treated as different module than main", type=int, default=1) parser.add_argument('--include', '-n', nargs='+', help="Functions tree to Include.", default=[]) parser.add_argument('--exclude', '-x', nargs='+', help="Functions tree to Exclude.", default=[]) args = parser.parse_args() @@ -107,14 +61,18 @@ def main(): all_func = disassemble(args.inp, disassembled, args.path) decompile(all_func) - if args.exclude: - remove_exclude_functions(all_func, args.exclude) #["func_unknown_0x1445baf27101", "func_unknown_0x95277699f59"]) - - if args.include: - all_func = get_included_functions(all_func, args.include) + if args.tree: + tree_root = args.tree + if tree_root == "start": + tree_root = get_start_function(all_func) + items_map = split_trees(all_func, tree_root) + if args.out: + save_trees(all_func, tree_root, args.mainlimit, items_map, args.out, args.export_format) + print(f"Done.") + return if args.out: - export_to_file(args.out, all_func, args.export_format) + export_to_file(args.out, all_func, args.export_format, args.include, args.exclude) print(f"Done.") diff --git a/view8_util.py b/view8_util.py new file mode 100755 index 0000000..26d32da --- /dev/null +++ b/view8_util.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +import argparse +import os + +from Parser.shared_function_info import * + +def is_root(sfi): + if sfi.declarer is None: + return True + return False + +def get_start_function(functions): + curr_func = next(iter(functions)) + while True: + sfi = functions.get(curr_func) + if is_root(sfi): + return curr_func + if sfi is None: + break + if sfi.declarer is None: + break + curr_func = sfi.declarer + return None + +def get_all_children(functions, curr_func): + children_list = [ ] + for func_name, sfi in functions.items(): + declarer = sfi.declarer + if declarer == curr_func: + children_list.append(func_name) + return children_list + +### + +def build_declaration_map(functions): + declared_by = {} + + for func_name, sfi in functions.items(): + declarer = sfi.declarer + if declarer: + if declarer not in declared_by: + declared_by[declarer] = [] + declared_by[declarer].append(func_name) + + return declared_by + +def remove_exclude_functions(all_functions, exclude_list): + declaration_table = build_declaration_map(all_functions) + number_of_functoin = len(exclude_list) + while exclude_list: + current_function = exclude_list.pop() + del all_functions[current_function] + next_level = declaration_table.get(current_function, []) + number_of_functoin += len(next_level) + exclude_list += next_level + print(f"Removed {number_of_functoin} functions") + +def get_included_functions(all_functions, include_list): + declaration_table = build_declaration_map(all_functions) + number_of_functoin = len(include_list) + new_all_func = {} + while include_list: + current_function = include_list.pop() + new_all_func[current_function] = all_functions[current_function] + next_level = declaration_table.get(current_function, []) + number_of_functoin += len(next_level) + include_list += next_level + return new_all_func +### + +def export_to_file(out_name, all_functions, format_list, included_list = None, excluded_list = None): + serialize_only = False + if ('serialized' in format_list): + serialized_name = out_name + if len(format_list) == 1: + serialize_only = True + else: + serialized_name += ".pkl" + print(f"Serializing to file: {serialized_name}") + save_functions_to_file(all_functions, serialized_name) + if serialize_only: + return + with open(out_name, "w") as f: + print(f"Exporting to file: {out_name}") + for function_name in list(all_functions)[::-1]: + include = True + if (excluded_list is not None) and (function_name in excluded_list): + include = False + if (included_list is not None) and (function_name not in included_list): + include = False + if include: + f.write(all_functions[function_name].export(export_v8code="v8_opcode" in format_list, export_translated="translated" in format_list, export_decompiled="decompiled" in format_list)) + +### + +def split_trees(functions, curr_func): + sfi = functions.get(curr_func) + print("Tree root: " + sfi.name) + if sfi.declarer is None: + print("Declarer Root") + else: + print("Parent: " + sfi.declarer) + children = get_all_children(functions, curr_func) + my_map = dict() + for c in children: + family = get_included_functions(functions, [c]) + my_map[c] = family + sorted_map = dict(sorted(my_map.items(), key=lambda item: len(item[1]))) + return sorted_map + +def create_dirs(nested_directory): + is_ok = False + try: + os.makedirs(nested_directory) + is_ok = True + except FileExistsError: + is_ok = True + except Exception as e: + print(f"An error occurred: {e}") + return is_ok + +def save_trees(all_functions, main_func, main_limit, items_map, out_dir, export_format): + # export the root function and directly related: + main_set = [main_func] + for name, filtered_func in items_map.items(): + if len(filtered_func) <= main_limit: + main_set += filtered_func + file_name = f"{main_func}.txt" + create_dirs(out_dir) + out_path = os.path.join(out_dir, file_name) + export_to_file(out_path, all_functions, export_format, main_set) + + # export the subtrees: + for name, filtered_func in items_map.items(): + if len(filtered_func) <= main_limit: + continue #skip + print(f"Name: {name}, List Length: {len(filtered_func)}") + subdir = f"{len(filtered_func)}" + file_name = f"{name}.txt" + dirs = os.path.join(out_dir, subdir) + create_dirs(dirs) + out_path = os.path.join(out_dir, subdir, file_name) + export_to_file(out_path, all_functions, export_format, filtered_func) From 32be7c2e1cc9f0b0b9c57061949ce8dab345d7d6 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 19:45:09 +0100 Subject: [PATCH 12/67] [BUGFIX] Substitute ConstPool values not only in the visible lines --- Parser/shared_function_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 85ffe85..589e674 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -58,7 +58,7 @@ def replacement(match): #replacements.update({f"ConstPoolLiteral[{idx}]": var for idx, var in enumerate(self.const_pool)}) for line in self.code: - if not line.visible or "ConstPool" not in line.decompiled: + if "ConstPool" not in line.decompiled: continue line.decompiled = re.sub(pattern, replacement, line.decompiled) From bf10b37abfce5f89d4a2523f758d6c92b7b0efc1 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 19:57:28 +0100 Subject: [PATCH 13/67] [FEATURE] Added verbosity argument --- Simplify/global_scope_replace.py | 19 ++++++++++--------- view8.py | 10 +++++----- view8_util.py | 24 +++++++++++++----------- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index d63aeab..83bdb61 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -2,7 +2,7 @@ from collections import defaultdict -def replace_global_scope(all_functions): +def replace_global_scope(all_functions, verbosity): scope_assignments = {} scope_counts = defaultdict(int) @@ -15,6 +15,8 @@ def replace_global_scope(all_functions): line = line_obj.decompiled match = pattern.search(line) if match: + if verbosity > 0: + print(f"Matched: {line}") key = (match.group(1), match.group(2)) value = match.group(3) if value in ("null", "undefined"): @@ -31,14 +33,13 @@ def replace_global_scope(all_functions): # Second pass: Replace Scope[num][num] with value if it's set only once for func in all_functions.values(): for line_obj in func.code: - new_line = line_obj.decompiled - match = pattern.search(new_line) + line = line_obj.decompiled + match = pattern.search(line) if match: - key = (match.group(1), match.group(2)) if scope_counts[key] == 1 and scope_assignments[key] is not None: - new_line = new_line.replace(match.group(0), scope_assignments[key]) - line_obj.decompiled = new_line - - - + new_line = line.replace(match.group(0), scope_assignments[key]) + line_obj.decompiled = new_line + if verbosity > 0: + print(f"Replaced:\n\t{line}\n\t{new_line}") + diff --git a/view8.py b/view8.py index 2aec9aa..088a5c0 100755 --- a/view8.py +++ b/view8.py @@ -2,10 +2,9 @@ import argparse import os -from view8_util import * +from view8_util import export_to_file from Parser.parse_v8cache import parse_v8cache_file, parse_disassembled_file -from Parser.shared_function_info import * -from Simplify.global_scope_replace import replace_global_scope +from Simplify.global_scope_replace import * #### @@ -21,12 +20,12 @@ def disassemble(in_file, input_is_disassembled, disassembler): return parse_disassembled_file(out_name) -def decompile(all_functions): +def decompile(all_functions, verbosity): # Decompile print(f"Decompiling {len(all_functions)} functions.") for name in list(all_functions)[::-1]: all_functions[name].decompile() - replace_global_scope(all_functions) + replace_global_scope(all_functions, verbosity) ### @@ -46,6 +45,7 @@ def main(): parser.add_argument('--mainlimit', '-l', help="In tree mode: a tree with depth above this limit will be treated as different module than main", type=int, default=1) parser.add_argument('--include', '-n', nargs='+', help="Functions tree to Include.", default=[]) parser.add_argument('--exclude', '-x', nargs='+', help="Functions tree to Exclude.", default=[]) + parser.add_argument('--verbosity', '-v', help="Verbosity level (0-3)", default=0, type=int, required=False) args = parser.parse_args() if not os.path.isfile(args.inp): diff --git a/view8_util.py b/view8_util.py index 26d32da..4d16473 100755 --- a/view8_util.py +++ b/view8_util.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import argparse import os from Parser.shared_function_info import * @@ -68,6 +67,18 @@ def get_included_functions(all_functions, include_list): return new_all_func ### +def _export_to_file(out_name, all_functions, format_list, included_list = None, excluded_list = None): + with open(out_name, "w") as f: + print(f"Exporting to file {out_name}.") + for function_name in list(all_functions)[::-1]: + include = True + if (excluded_list is not None and len(excluded_list)) and (function_name in excluded_list): + include = False + if (included_list is not None and len(included_list)) and (function_name not in included_list): + include = False + if include: + f.write(all_functions[function_name].export(export_v8code="v8_opcode" in format_list, export_translated="translated" in format_list, export_decompiled="decompiled" in format_list)) + def export_to_file(out_name, all_functions, format_list, included_list = None, excluded_list = None): serialize_only = False if ('serialized' in format_list): @@ -80,16 +91,7 @@ def export_to_file(out_name, all_functions, format_list, included_list = None, e save_functions_to_file(all_functions, serialized_name) if serialize_only: return - with open(out_name, "w") as f: - print(f"Exporting to file: {out_name}") - for function_name in list(all_functions)[::-1]: - include = True - if (excluded_list is not None) and (function_name in excluded_list): - include = False - if (included_list is not None) and (function_name not in included_list): - include = False - if include: - f.write(all_functions[function_name].export(export_v8code="v8_opcode" in format_list, export_translated="translated" in format_list, export_decompiled="decompiled" in format_list)) + _export_to_file(out_name, all_functions, format_list, included_list, excluded_list) ### From ab581e8e9e0562dce536c3cf38facfec7ecf4622 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 20:30:35 +0100 Subject: [PATCH 14/67] [BUGFIX] Fixed missing verbosity argument --- view8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view8.py b/view8.py index 088a5c0..d067683 100755 --- a/view8.py +++ b/view8.py @@ -59,7 +59,7 @@ def main(): if 'disassembled' in args.input_format: disassembled = True all_func = disassemble(args.inp, disassembled, args.path) - decompile(all_func) + decompile(all_func, args.verbosity) if args.tree: tree_root = args.tree From 0ec27a4350b792a6d67c374fd21477462fd7e1f1 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 20:32:23 +0100 Subject: [PATCH 15/67] [FEATURE] Added visibility and metadata to CodeLine and to FunctionInfo --- Parser/shared_function_info.py | 14 ++++-------- Translate/jump_blocks.py | 39 +++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 589e674..21a3a8f 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -1,19 +1,11 @@ from Translate.translate import translate_bytecode +from Translate.jump_blocks import CodeLine from Simplify.simplify import simplify_translated_bytecode + import re import pickle from typing import List, Optional -class CodeLine: - def __init__(self, opcode="", line="", inst="", translated="", decompiled=""): - self.v8_opcode = opcode - self.line_num = line - self.v8_instruction = inst - self.translated = translated - self.decompiled = decompiled - self.visible = True - - class SharedFunctionInfo: def __init__(self): self.name = None @@ -24,6 +16,8 @@ def __init__(self): self.code = None self.const_pool = None self.exception_table = None + self.visible = True + self.metadata = None def is_fully_parsed(self): return all( diff --git a/Translate/jump_blocks.py b/Translate/jump_blocks.py index 35809bb..590aec0 100644 --- a/Translate/jump_blocks.py +++ b/Translate/jump_blocks.py @@ -1,12 +1,45 @@ class CodeLine: - def __init__(self, opcode="", line="", inst="", translated=""): + def __init__(self, opcode="", line="", inst="", translated="", decompiled=""): self.v8_opcode = opcode self.line_num = line self.v8_instruction = inst self.translated = translated - self.decompiled = "" + self.decompiled = decompiled self.visible = True - + self.metadata = None + + def set_metadata(self, meta_type, meta_val): + """ + Set metadata of particular type the code line + """ + if not self.metadata: + self.metadata = dict() + self.metadata[meta_type] = meta_val + + def get_metadata(self, meta_type): + """ + Retrieve metadata of particular type from the code line + """ + if not self.metadata: + return None + if not isinstance(self.metadata, dict): + return None + if not meta_type in self.metadata: + return None + return self.metadata[meta_type] + + def drop_metatata(self, meta_type): + """ + Remove metadata of particular type from the code line + """ + if not self.metadata: + return False + if not isinstance(self.metadata, dict): + return False + if not meta_type in self.metadata: + return False + self.metadata.pop(meta_type, None) + return True class JumpBlocks: def __init__(self, name, code, jump_table): From a43de3f6d27db66c22e72e0986741eb3c517b138 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 20:39:25 +0100 Subject: [PATCH 16/67] [FEATURE] Added _global prefix to global variables --- Parser/shared_function_info.py | 99 +++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 21a3a8f..14186cb 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -1,11 +1,80 @@ from Translate.translate import translate_bytecode from Translate.jump_blocks import CodeLine from Simplify.simplify import simplify_translated_bytecode - import re import pickle from typing import List, Optional +### + +class GlobalVars: + def __init__(self): + self.strings_set = None + self.funcs_map = None + + def parse(self, value): + + def _extract_name(func): + return func[len("func_"):func.rindex("_0x")] + + is_parsed = False + _strings_set = set(re.findall(r'"([^"\\]*(?:\\.[^"\\]*)*)"', value)) + _funcs_set = set(re.findall(r'\bfunc_[A-Za-z0-9_$]+\b', value)) + if _strings_set: + is_parsed = True + if not self.strings_set: + self.strings_set = set() + self.strings_set.update(_strings_set) + + if _funcs_set: + is_parsed = True + if not self.funcs_map: + self.funcs_map = {} + for func in _funcs_set: + short_name = _extract_name(func) + self.funcs_map[short_name] = func + return is_parsed + + def is_filled(self): + if self.strings_set or self.funcs_map: + return True + return False + + def has_value(self, value): + val = value.strip('"') + if (value in self.strings_set or val in self.strings_set): + return True + if value in self.funcs_map.keys(): + return True + return False + + def resolve_global_name(self, value): + + def _is_string(value): + if value.startswith('"') and value.endswith('"'): + return True + return False + + if not self.is_filled(): + return None + + if not _is_string(value): + return None + + val = value.strip('"') + if (value in self.strings_set or val in self.strings_set): + return "global_" + val + + if val in self.funcs_map.keys(): + return self.funcs_map[val] + + return None +### + +g_GlobalVars = GlobalVars() + +### + class SharedFunctionInfo: def __init__(self): self.name = None @@ -36,12 +105,38 @@ def translate_bytecode(self): def simplify_bytecode(self): simplify_translated_bytecode(self, self.code) + def fill_global_variables(self): + """ + If the Global Vars were defined anywhere in this function, fill them in and store in the global structure. + """ + global g_GlobalVars + + patternDef = re.compile(r'ConstPoolLiteral\[(\d+)\]') + + for obj in self.code: + line = obj.decompiled + if "DeclareGlobals(" not in line: + continue + match = re.search(patternDef, line.strip()) + if not match: + continue + index = int(match.group(1)) + if g_GlobalVars.parse(self.const_pool[index]): + return True + return False + def replace_const_pool(self): + global g_GlobalVars def replacement(match): index = int(match.group(2)) value = self.const_pool[index] if match.group(1) == "ConstPool": #Not ConstPoolLiteral + + global_symbol = g_GlobalVars.resolve_global_name(value) + if global_symbol: + return global_symbol + return value.strip('"') return value @@ -54,12 +149,12 @@ def replacement(match): for line in self.code: if "ConstPool" not in line.decompiled: continue - line.decompiled = re.sub(pattern, replacement, line.decompiled) def decompile(self): self.translate_bytecode() self.simplify_bytecode() + self.fill_global_variables() self.replace_const_pool() def export(self, export_v8code=False, export_translated=False, export_decompiled=True): From 118fa4d25bfc372eba50dff8639d16371d695dd0 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 20:50:02 +0100 Subject: [PATCH 17/67] [BUGFIX] In global_scope_replace: improved patterns. Keep replacing till the point of saturation --- Simplify/global_scope_replace.py | 76 ++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index 83bdb61..d7f6be4 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -1,45 +1,85 @@ import re from collections import defaultdict +def _print_assignments(scope_assignments): + for key in scope_assignments.keys(): + if scope_assignments[key] is None: + continue + (x,y) = key + print(f"Scope[{x}][{y}] = {scope_assignments[key]}") -def replace_global_scope(all_functions, verbosity): +def _replace_global_scope2_func(all_functions, verbosity) -> int: scope_assignments = {} scope_counts = defaultdict(int) # Regex pattern to match Scope[num][num] = value - pattern = re.compile(r'Scope\[(\d+)\]\[(\d+)\] = (\S+)') + pattern = re.compile(r'Scope\[(\d+)\]\[(\d+)\] = (\S+)$') + value_pattern = re.compile(r'([\w#$]+|\"[\w#$]+\")$') + exclusion_pattern = re.compile(r'(ACCU|r\d+|a\d+)$') # First pass: Find all unique Scope assignments for func in all_functions.values(): for line_obj in func.code: - line = line_obj.decompiled - match = pattern.search(line) + line = line_obj.decompiled.strip() + match = pattern.match(line) if match: - if verbosity > 0: - print(f"Matched: {line}") key = (match.group(1), match.group(2)) value = match.group(3) if value in ("null", "undefined"): - line_obj.decompiled = "" + line_obj.visible = False continue - if key in scope_assignments or not value.startswith("func_"): + + if key in scope_assignments.keys() or not value_pattern.match(value) or exclusion_pattern.match(value): # If the same Scope is assigned different values, mark it as invalid scope_assignments[key] = None else: scope_assignments[key] = value scope_counts[key] += 1 + if verbosity > 1: + _print_assignments(scope_assignments) - pattern = re.compile(r'Scope\[(\d+)\]\[(\d+)\]') + pattern2 = re.compile(r'Scope\[(\d+)\]\[(\d+)\](?![\[])') # Scope[num][num] but not: Scope[num][num][num] + replaced_count = 0 # Second pass: Replace Scope[num][num] with value if it's set only once for func in all_functions.values(): for line_obj in func.code: line = line_obj.decompiled - match = pattern.search(line) - if match: - key = (match.group(1), match.group(2)) - if scope_counts[key] == 1 and scope_assignments[key] is not None: - new_line = line.replace(match.group(0), scope_assignments[key]) - line_obj.decompiled = new_line - if verbosity > 0: - print(f"Replaced:\n\t{line}\n\t{new_line}") - + + # Split into left-hand and right-hand side of assignment + if '=' in line: + lhs, rhs = line.split('=', 1) + + # Only replace Scope[x][y] if it appears **not** in LHS + def replace_usage(match): + key = (match.group(1), match.group(2)) + if scope_counts[key] == 1 and scope_assignments[key] is not None: + return scope_assignments[key] + return match.group(0) + new_rhs = pattern2.sub(replace_usage, rhs) + new_line = lhs + '=' + new_rhs + else: + # No assignment; apply replacements freely + new_line = pattern2.sub(lambda m: ( + scope_assignments[(m.group(1), m.group(2))] + if scope_counts[(m.group(1), m.group(2))] == 1 and scope_assignments[(m.group(1), m.group(2))] is not None + else m.group(0) + ), line) + + if new_line != line: + replaced_count += 1 + if verbosity > 0: + print(f"[G] Replaced:\n\t{line}\n\t{new_line}") + line_obj.decompiled = new_line + return replaced_count + +def replace_global_scope(all_functions, verbosity) -> int: + total_repl = 0 + round = 0 + while True: + repl_cnt = _replace_global_scope2_func(all_functions, verbosity) + if not repl_cnt: + break + total_repl += repl_cnt + if verbosity: + print(f"[G] Replaced count: {repl_cnt}") + return total_repl From bb10c1d74c10b7912e7bd796f3b10e38871ce8d6 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 20:56:20 +0100 Subject: [PATCH 18/67] [FEATURE] In view8_util: added new util functions. Included and excluded functions loaded from files --- view8.py | 73 +++++++++++++++++----- view8_util.py | 164 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 215 insertions(+), 22 deletions(-) diff --git a/view8.py b/view8.py index d067683..f5d4e19 100755 --- a/view8.py +++ b/view8.py @@ -2,10 +2,9 @@ import argparse import os -from view8_util import export_to_file +from view8_util import * from Parser.parse_v8cache import parse_v8cache_file, parse_disassembled_file -from Simplify.global_scope_replace import * - +from Simplify.global_scope_replace import replace_global_scope #### def disassemble(in_file, input_is_disassembled, disassembler): @@ -20,37 +19,66 @@ def disassemble(in_file, input_is_disassembled, disassembler): return parse_disassembled_file(out_name) -def decompile(all_functions, verbosity): +def decompile(all_functions): # Decompile print(f"Decompiling {len(all_functions)} functions.") for name in list(all_functions)[::-1]: all_functions[name].decompile() - replace_global_scope(all_functions, verbosity) + +def propagate_global_scope(all_func, verbosity): + if replace_global_scope(all_func, verbosity): + if verbosity: + print("Replace global scope done.") + return True + return False ### +def load_functions_set(filename): + try: + with open(filename, "r") as file: + deobf_funcs = set(line.strip() for line in file) + return deobf_funcs + except FileNotFoundError: + return None + return None + def main(): parser = argparse.ArgumentParser(description="View8: V8 cache decompiler.") group = parser.add_mutually_exclusive_group(required=False) group.add_argument('--input_format', '-f', choices=['raw', 'serialized', 'disassembled'], - help="Specify the input format. Options are: 'raw', 'serialized', 'disassembled'(mutually exclusive)", - default='raw') + help="Specify the input format. Options are: 'raw', 'serialized', 'disassembled'(mutually exclusive)", default='raw') parser.add_argument('--inp', '-i', help="The input file name.", default=None, required=True) parser.add_argument('--out', '-o', help="The output file name.", default=None) - parser.add_argument('--path', '-p', help="Path to disassembler binary.", default=None) + parser.add_argument('--path', '-p', help="Path to disassembler binary. Required if the input is in the raw format.", default=None) parser.add_argument('--export_format', '-e', nargs='+', choices=['v8_opcode', 'translated', 'decompiled', 'serialized'], help="Specify the export format(s). Options are 'v8_opcode', 'translated', and 'decompiled'. Multiple options can be combined.", default=['decompiled']) + parser.add_argument('--scope', help="Propagate scope arguments.", default=1, type=int, required=False) parser.add_argument('--tree', '-t', help="Show functions tree, starting from a given node. To start from the default main function, use 'start'", default=None) parser.add_argument('--mainlimit', '-l', help="In tree mode: a tree with depth above this limit will be treated as different module than main", type=int, default=1) - parser.add_argument('--include', '-n', nargs='+', help="Functions tree to Include.", default=[]) - parser.add_argument('--exclude', '-x', nargs='+', help="Functions tree to Exclude.", default=[]) + parser.add_argument('--include', '-n', help="Functions to Include (file containig a list)", default=None) + parser.add_argument('--exclude', '-x', help="Functions to Exclude (file containig a list)", default=None) + parser.add_argument('--func', help="A function to be displayed.", default=None, required=False) + parser.add_argument('--show_all', help="Should show lines marked as hidden (in function display mode)", default=False, required=False, action='store_true') parser.add_argument('--verbosity', '-v', help="Verbosity level (0-3)", default=0, type=int, required=False) args = parser.parse_args() if not os.path.isfile(args.inp): raise FileNotFoundError(f"The input file {args.inp} does not exist.") + funcs_to_include = None + if args.include: + funcs_to_include = load_functions_set(args.include) + if funcs_to_include: + print(f"Include: {len(funcs_to_include)} functions") + + funcs_to_exclude = None + if args.exclude: + funcs_to_exclude = load_functions_set(args.exclude) + if funcs_to_exclude: + print(f"Exclude: {len(funcs_to_exclude)} functions") + if ('serialized' in args.input_format): print(f"Reading from serialized, already decompiled input: {args.inp}") all_func = load_functions_from_file(args.inp) @@ -59,7 +87,24 @@ def main(): if 'disassembled' in args.input_format: disassembled = True all_func = disassemble(args.inp, disassembled, args.path) - decompile(all_func, args.verbosity) + decompile(all_func) + + if args.scope: + print("Propagating scope arguments...") + propagate_global_scope(all_func, args.verbosity) + + # print a single selected function: + if args.func: + func_name = args.func + filtered = find_functions_by_name(all_func, func_name) + if not func_name in filtered: + print(f"Function {func_name} was not found. Found {len(filtered)} similar names.") + for key in filtered.keys(): + print(key) + if len(filtered) == 0: + return + print_funcs(filtered, args.show_all) + return if args.tree: tree_root = args.tree @@ -67,12 +112,12 @@ def main(): tree_root = get_start_function(all_func) items_map = split_trees(all_func, tree_root) if args.out: - save_trees(all_func, tree_root, args.mainlimit, items_map, args.out, args.export_format) + save_trees(all_func, tree_root, args.mainlimit, items_map, args.out, args.export_format, funcs_to_exclude) print(f"Done.") return - if args.out: - export_to_file(args.out, all_func, args.export_format, args.include, args.exclude) + if args.out: + export_to_file(args.out, all_func, args.export_format, funcs_to_include, funcs_to_exclude) print(f"Done.") diff --git a/view8_util.py b/view8_util.py index 4d16473..7830331 100755 --- a/view8_util.py +++ b/view8_util.py @@ -29,6 +29,132 @@ def get_all_children(functions, curr_func): children_list.append(func_name) return children_list +def next_visible_line(func, indx, is_backward=False): + """ + Next non-empty, visible line + Returns Index + """ + if is_backward: + step = (-1) + else: + step = 1 + indx += step + while (indx >= 0 and indx < len(func.code)): + if ((not func.code[indx].visible) or (not func.code[indx].decompiled) or (func.code[indx].decompiled.strip() == "")): + indx += step + continue + return indx + return None + +### + +def _rename_functions( + functions: dict[str, SharedFunctionInfo], + renamed_dict: dict[str, str], +) -> int: + renamed_count = 0 + new_functions: dict[str, SharedFunctionInfo] = {} + + for name, func in functions.items(): + if name in renamed_dict: + new_name = renamed_dict[name] + func.name = new_name + new_functions[new_name] = func + renamed_count += 1 + else: + new_functions[name] = func + + # mutate the original dict in place (if callers hold a reference) + functions.clear() + functions.update(new_functions) + return renamed_count + +def rename_functions_in_code( + functions: dict[str, SharedFunctionInfo], + renamed_dict: dict[str, str], + verbosity: int +) -> int: + + renamed = _rename_functions(functions, renamed_dict) + if verbosity: + print(f"Renamed functions: {renamed}") + + func_pattern = r'\b(func_[a-zA-Z0-9_$]+_0x[0-9a-fA-F]+)\b' + regex = re.compile(func_pattern) + + for func in functions.values(): + indx = 0 + while True: + indx = next_visible_line(func, indx) + if indx is None: + break + + line = func.code[indx].decompiled + + # Replace only if the found name is in renamed_dict + def repl(m): + name = m.group(1) + return renamed_dict.get(name, name) + + new_line = regex.sub(repl, line) + + if new_line != line: + func.code[indx].decompiled = new_line + if verbosity: + print(f"REPL: {line.strip()} -> {new_line.strip()}") + return renamed + +### + +def print_func(func_name, func, show_hidden=False, show_line_num=True, show_const=False, show_line_meta=False): + print("###") + print(f"# {func_name}") + print(f"# Declarer: {func.declarer}") + if func.metadata: + print(f"Metadata: {func.metadata}") + if show_const: + print(f"# Const Pool") + print(func.const_pool) + print(f"# Code") + indx = 0 + i = 0 + for i in range(len(func.code)): + line_obj = func.code[i] + if not line_obj.decompiled: + continue + if not show_hidden and not line_obj.visible: + continue + indx += 1 + line = line_obj.decompiled + + meta = "" + if show_line_meta: + if line_obj.metadata: + meta = f" # {line_obj.metadata}" + + if show_line_num: + if indx != i: + print(f"{indx}|{i}|{line}{meta}") + else: + print(f"{indx}|{line}{meta}") + else: + print(f"{line}") +# + +def print_funcs(functions, show_hidden=False, show_line_num=True, show_const=False, show_line_meta=False): + for func_name, func in functions.items(): + print_func(func_name, func, show_hidden, show_line_num, show_const, show_line_meta) + +def find_functions_by_name(all_func, name): + funcs = dict() + if name in all_func: + funcs[name] = all_func[name] + return funcs + sub_name = "_" + name + "_" + for key in all_func.keys(): + if sub_name in key: + funcs[key] = all_func[key] + return funcs ### def build_declaration_map(functions): @@ -69,28 +195,47 @@ def get_included_functions(all_functions, include_list): def _export_to_file(out_name, all_functions, format_list, included_list = None, excluded_list = None): with open(out_name, "w") as f: - print(f"Exporting to file {out_name}.") + print(f"Exporting to file {out_name}") for function_name in list(all_functions)[::-1]: include = True if (excluded_list is not None and len(excluded_list)) and (function_name in excluded_list): include = False if (included_list is not None and len(included_list)) and (function_name not in included_list): include = False - if include: - f.write(all_functions[function_name].export(export_v8code="v8_opcode" in format_list, export_translated="translated" in format_list, export_decompiled="decompiled" in format_list)) + if not all_functions[function_name].visible: + include = False + if not include: + continue + f.write(all_functions[function_name].export(export_v8code="v8_opcode" in format_list, export_translated="translated" in format_list, export_decompiled="decompiled" in format_list)) + +def _get_extension(filename): + ext = None + if '.' in filename: + ext = '.' + filename.rsplit('.', 1)[-1] + return ext + +def _add_or_replace_extension(filename, new_ext): + if not new_ext.startswith('.'): + new_ext = '.' + new_ext + base = filename.rsplit('.', 1)[0] if '.' in filename else filename + return base + new_ext def export_to_file(out_name, all_functions, format_list, included_list = None, excluded_list = None): serialize_only = False + serialized_ext = ".pkl" + text_ext = ".txt" if ('serialized' in format_list): - serialized_name = out_name if len(format_list) == 1: serialize_only = True - else: - serialized_name += ".pkl" + + serialized_name = _add_or_replace_extension(out_name, serialized_ext) print(f"Serializing to file: {serialized_name}") save_functions_to_file(all_functions, serialized_name) + if serialize_only: return + if _get_extension(out_name) == serialized_ext: + out_name = _add_or_replace_extension(out_name, text_ext) _export_to_file(out_name, all_functions, format_list, included_list, excluded_list) ### @@ -121,7 +266,7 @@ def create_dirs(nested_directory): print(f"An error occurred: {e}") return is_ok -def save_trees(all_functions, main_func, main_limit, items_map, out_dir, export_format): +def save_trees(all_functions, main_func, main_limit, items_map, out_dir, export_format, excluded_list): # export the root function and directly related: main_set = [main_func] for name, filtered_func in items_map.items(): @@ -142,4 +287,7 @@ def save_trees(all_functions, main_func, main_limit, items_map, out_dir, export_ dirs = os.path.join(out_dir, subdir) create_dirs(dirs) out_path = os.path.join(out_dir, subdir, file_name) - export_to_file(out_path, all_functions, export_format, filtered_func) + export_to_file(out_path, all_functions, export_format, filtered_func, excluded_list) + +### + From a5186906db43787380058cdd11124870b6f6f4b3 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 20:57:46 +0100 Subject: [PATCH 19/67] [NOBIN] Set view8_util as non executable --- view8_util.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 view8_util.py diff --git a/view8_util.py b/view8_util.py old mode 100755 new mode 100644 From c8599d46a5f0e9396546af0c1d801f082c99b2ec Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 22:26:26 +0100 Subject: [PATCH 20/67] [FEATURE] Added module init --- __init__.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e3c2f8c --- /dev/null +++ b/__init__.py @@ -0,0 +1,7 @@ +import sys +import os + +_view8_dir = os.path.dirname(os.path.abspath(__file__)) +if _view8_dir not in sys.path: + sys.path.insert(0, _view8_dir) + From 42a26d9c274ae0d0dc8fd4e9562b71cc60d272bb Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 22:27:23 +0100 Subject: [PATCH 21/67] [BUGFIX] Changed unicode to utf-8 --- Parser/parse_v8cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Parser/parse_v8cache.py b/Parser/parse_v8cache.py index 302dc17..8a7f5ba 100644 --- a/Parser/parse_v8cache.py +++ b/Parser/parse_v8cache.py @@ -30,7 +30,7 @@ def run_disassembler_binary(binary_path, file_name, out_file_name): ) # Open the output file in write mode - with open(out_file_name, 'w', encoding="unicode") as outfile: + with open(out_file_name, 'w', encoding="utf-8") as outfile: # Call the binary with the file name as argument and pipe the output to the file try: result = subprocess.run([binary_path, file_name], stdout=outfile, stderr=subprocess.PIPE, text=True) From 671df73e2130f2404d23bc441ddab75272a55e12 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Sun, 15 Feb 2026 23:16:33 +0100 Subject: [PATCH 22/67] [REFACT] Cleanup and optimizations --- Parser/parse_v8cache.py | 27 +++++++++++++++++---------- Parser/shared_function_info.py | 32 ++++++++++++++++++++++---------- README.md | 3 ++- Translate/jump_blocks.py | 3 ++- view8.py | 16 ++++++++++++---- view8_util.py | 3 ++- 6 files changed, 57 insertions(+), 27 deletions(-) diff --git a/Parser/parse_v8cache.py b/Parser/parse_v8cache.py index 8a7f5ba..6eb7ae2 100644 --- a/Parser/parse_v8cache.py +++ b/Parser/parse_v8cache.py @@ -30,17 +30,24 @@ def run_disassembler_binary(binary_path, file_name, out_file_name): ) # Open the output file in write mode - with open(out_file_name, 'w', encoding="utf-8") as outfile: + with open(out_file_name, 'w', encoding="utf-8", errors="replace") as outfile: # Call the binary with the file name as argument and pipe the output to the file - try: - result = subprocess.run([binary_path, file_name], stdout=outfile, stderr=subprocess.PIPE, text=True) - - # Check the return status code - if result.stderr: - raise RuntimeError( - f"Binary execution failed with status code {result.returncode}: {result.stderr.strip()}") - except subprocess.CalledProcessError as e: - raise RuntimeError(f"Error calling the binary: {e}") + result = subprocess.run( + [binary_path, file_name], + stdout=outfile, + stderr=subprocess.PIPE, + text=True, + ) + + # Treat only non-zero exit codes as failure. Some tools may emit warnings to stderr on success. + if result.returncode != 0: + err = (result.stderr or "").strip() + raise RuntimeError( + f"Binary execution failed with status code {result.returncode}." + (f" Stderr: {err}" if err else "") + ) + + if result.stderr: + print(f"[!] Disassembler stderr: {result.stderr.strip()}") def parse_v8cache_file(file_name, out_name, view8_dir, binary_path): diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 14186cb..6ef5bf1 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -3,7 +3,7 @@ from Simplify.simplify import simplify_translated_bytecode import re import pickle -from typing import List, Optional +from typing import Dict, List, Optional, Union ### @@ -70,7 +70,7 @@ def _is_string(value): return None ### - +## TODO: make it per file g_GlobalVars = GlobalVars() ### @@ -177,24 +177,36 @@ def export(self, export_v8code=False, export_translated=False, export_decompiled #### +FunctionsBlob = Union[Dict[str, "SharedFunctionInfo"], List["SharedFunctionInfo"]] + # Helper function for serializing multiple functions -def serialize_functions(functions: List[SharedFunctionInfo]) -> bytes: - """Serialize a list of SharedFunctionInfo objects""" +def serialize_functions(functions: FunctionsBlob) -> bytes: + """Serialize decompiled output using pickle. + + SECURITY NOTE: + Pickle is unsafe for untrusted input. Only load serialized files that you + generated yourself. + """ return pickle.dumps(functions, protocol=pickle.HIGHEST_PROTOCOL) -def deserialize_functions(data: bytes) -> List[SharedFunctionInfo]: - """Deserialize a list of SharedFunctionInfo objects""" +def deserialize_functions(data: bytes) -> FunctionsBlob: + """Deserialize decompiled output using pickle. + + SECURITY NOTE: + Unpickling can execute arbitrary code. Do not load files from untrusted + sources. + """ return pickle.loads(data) -def save_functions_to_file(functions: List[SharedFunctionInfo], filename: str): - """Save multiple functions to a file""" +def save_functions_to_file(functions: FunctionsBlob, filename: str): + """Save decompiled output to a file (pickle).""" with open(filename, 'wb') as f: f.write(serialize_functions(functions)) -def load_functions_from_file(filename: str) -> List[SharedFunctionInfo]: - """Load multiple functions from a file""" +def load_functions_from_file(filename: str) -> FunctionsBlob: + """Load decompiled output from a file (pickle).""" with open(filename, 'rb') as f: return deserialize_functions(f.read()) diff --git a/README.md b/README.md index 5239f5e..864241d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@
    • --inp, -i: The input file name
    • --out, -o: Path to the output (depending on the type of the output, a single file or a directory tree may be generated)
    • -
    • --input_format, -f: Indicate format of the input. Options are: raw: the output is a raw JSC file; disassembled: the input file is already disassembled; serialized: the input is already decompiled, and stored in a serialized format (as an object structure, rather than text)
    • +
    • --input_format, -f: Indicate format of the input. Options are: raw: the output is a raw JSC file; disassembled: the input file is already disassembled; serialized: the input is already decompiled, and stored in a serialized format (pickle; trusted input only)
    • --export_format, -e: Specify the export format(s). Options are v8_opcode, translated, decompiled, and serialized. Multiple options can be combined (optional, default: decompiled).
    • --path, -p: Path to disassembler binary. Required if the input is in the raw format.
    • --tree, -t: Split output into a tree structure (rather than storing all functions in one file). Specify the function that will be used as a top node of the tree. To start from the default main function, use 'start' (optional).
    • @@ -40,6 +40,7 @@
      python view8.py -i input_file -o output_file -f disassembled

      Creating and Processing Serialized Files

      Sometimes we may want to decompile the file into a serialized format (preserving all the objects and structures). This type of an output may be easier to post-process than a text format, and useful i.e. for further deobfuscation. To create a serialized output we use a specific export format: --export_format serialized (or -e serialized)

      +

      Security warning: the current serialized format is a Python pickle file (.pkl). Unpickling data from untrusted sources can execute arbitrary code. Only load serialized files that you generated yourself.

      python view8.py -i input_file -o output_file -e serialized

      If we ever want to load the serialized output back, and decompile it as a different type of an output, we can do it using --input_format serialized (or -f serialized) option:

      python view8.py -i input_file -o output_file -f serialized
      diff --git a/Translate/jump_blocks.py b/Translate/jump_blocks.py index 590aec0..955abeb 100644 --- a/Translate/jump_blocks.py +++ b/Translate/jump_blocks.py @@ -28,7 +28,7 @@ def get_metadata(self, meta_type): return None return self.metadata[meta_type] - def drop_metatata(self, meta_type): + def drop_metadata(self, meta_type): """ Remove metadata of particular type from the code line """ @@ -47,6 +47,7 @@ def __init__(self, name, code, jump_table): self.code_list = code self.code = {i.line_num: i for i in code} self.code_offset = list(self.code) + self._offset_to_idx = {off: idx for idx, off in enumerate(self.code_offset)} self.jump_table = jump_table def jump_done(self, jmp): diff --git a/view8.py b/view8.py index f5d4e19..d254f28 100755 --- a/view8.py +++ b/view8.py @@ -2,8 +2,16 @@ import argparse import os -from view8_util import * +from view8_util import ( + export_to_file, + find_functions_by_name, + get_start_function, + print_funcs, + save_trees, + split_trees, +) from Parser.parse_v8cache import parse_v8cache_file, parse_disassembled_file +from Parser.shared_function_info import load_functions_from_file from Simplify.global_scope_replace import replace_global_scope #### @@ -47,7 +55,7 @@ def main(): parser = argparse.ArgumentParser(description="View8: V8 cache decompiler.") group = parser.add_mutually_exclusive_group(required=False) group.add_argument('--input_format', '-f', choices=['raw', 'serialized', 'disassembled'], - help="Specify the input format. Options are: 'raw', 'serialized', 'disassembled'(mutually exclusive)", default='raw') + help="Specify the input format. Options are: 'raw', 'serialized' (pickle; trusted input only), 'disassembled' (mutually exclusive)", default='raw') parser.add_argument('--inp', '-i', help="The input file name.", default=None, required=True) parser.add_argument('--out', '-o', help="The output file name.", default=None) parser.add_argument('--path', '-p', help="Path to disassembler binary. Required if the input is in the raw format.", default=None) @@ -79,12 +87,12 @@ def main(): if funcs_to_exclude: print(f"Exclude: {len(funcs_to_exclude)} functions") - if ('serialized' in args.input_format): + if args.input_format == 'serialized': print(f"Reading from serialized, already decompiled input: {args.inp}") all_func = load_functions_from_file(args.inp) else: disassembled = False - if 'disassembled' in args.input_format: + if args.input_format == 'disassembled': disassembled = True all_func = disassemble(args.inp, disassembled, args.path) decompile(all_func) diff --git a/view8_util.py b/view8_util.py index 7830331..2a90caf 100644 --- a/view8_util.py +++ b/view8_util.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 import os +import re -from Parser.shared_function_info import * +from Parser.shared_function_info import SharedFunctionInfo, save_functions_to_file def is_root(sfi): if sfi.declarer is None: From 9fe07fa8941b4d58e1669b623632d81964851d76 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Mon, 16 Feb 2026 00:07:30 +0100 Subject: [PATCH 23/67] [BUGFIX] In get_relative_offset: validate offsets, fixed off by one --- Translate/jump_blocks.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Translate/jump_blocks.py b/Translate/jump_blocks.py index 955abeb..8e7b308 100644 --- a/Translate/jump_blocks.py +++ b/Translate/jump_blocks.py @@ -47,7 +47,6 @@ def __init__(self, name, code, jump_table): self.code_list = code self.code = {i.line_num: i for i in code} self.code_offset = list(self.code) - self._offset_to_idx = {off: idx for idx, off in enumerate(self.code_offset)} self.jump_table = jump_table def jump_done(self, jmp): @@ -57,10 +56,14 @@ def jump_done(self, jmp): def get_relative_offset(self, offset, n): # return a relative line offset to a given offset - new_offset = self.code_offset.index(offset) + n - if 0 <= new_offset <= len(self.code_offset): - return self.code_offset[new_offset] - raise Exception(f"relative offset {new_offset} from {offset} out of range") + try: + base_idx = self.code_offset.index(offset) + except ValueError: + raise KeyError(f"offset {offset} not found in code offsets") + new_idx = base_idx + n + if 0 <= new_idx < len(self.code_offset): + return self.code_offset[new_idx] + raise IndexError(f"relative offset {new_idx} from {offset} out of range") def get_all_jump_list(self): # Combine all jumps from the jump tables into one list From a8918069602fe756c7b75260a6cb37fa880c093f Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 02:20:13 +0100 Subject: [PATCH 24/67] [BUGFIX] Fixed typos in argument descriptions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/view8.py b/view8.py index d254f28..ad0fa48 100755 --- a/view8.py +++ b/view8.py @@ -65,8 +65,8 @@ def main(): parser.add_argument('--scope', help="Propagate scope arguments.", default=1, type=int, required=False) parser.add_argument('--tree', '-t', help="Show functions tree, starting from a given node. To start from the default main function, use 'start'", default=None) parser.add_argument('--mainlimit', '-l', help="In tree mode: a tree with depth above this limit will be treated as different module than main", type=int, default=1) - parser.add_argument('--include', '-n', help="Functions to Include (file containig a list)", default=None) - parser.add_argument('--exclude', '-x', help="Functions to Exclude (file containig a list)", default=None) + parser.add_argument('--include', '-n', help="Functions to Include (file containing a list)", default=None) + parser.add_argument('--exclude', '-x', help="Functions to Exclude (file containing a list)", default=None) parser.add_argument('--func', help="A function to be displayed.", default=None, required=False) parser.add_argument('--show_all', help="Should show lines marked as hidden (in function display mode)", default=False, required=False, action='store_true') parser.add_argument('--verbosity', '-v', help="Verbosity level (0-3)", default=0, type=int, required=False) From f01247028f7747aa0ae9f0393be7da18fa651d68 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 02:21:33 +0100 Subject: [PATCH 25/67] [REFACT] Removed unused variable Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Simplify/global_scope_replace.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index d7f6be4..5ee8a14 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -74,7 +74,6 @@ def replace_usage(match): def replace_global_scope(all_functions, verbosity) -> int: total_repl = 0 - round = 0 while True: repl_cnt = _replace_global_scope2_func(all_functions, verbosity) if not repl_cnt: From b98c1758d6288c8da93361419a148587af27d18e Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 02:23:27 +0100 Subject: [PATCH 26/67] [BUGFIX] Fixed typo in the variable name Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8_util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/view8_util.py b/view8_util.py index 2a90caf..0070a15 100644 --- a/view8_util.py +++ b/view8_util.py @@ -172,24 +172,24 @@ def build_declaration_map(functions): def remove_exclude_functions(all_functions, exclude_list): declaration_table = build_declaration_map(all_functions) - number_of_functoin = len(exclude_list) + number_of_function = len(exclude_list) while exclude_list: current_function = exclude_list.pop() del all_functions[current_function] next_level = declaration_table.get(current_function, []) - number_of_functoin += len(next_level) + number_of_function += len(next_level) exclude_list += next_level - print(f"Removed {number_of_functoin} functions") + print(f"Removed {number_of_function} functions") def get_included_functions(all_functions, include_list): declaration_table = build_declaration_map(all_functions) - number_of_functoin = len(include_list) + number_of_function = len(include_list) new_all_func = {} while include_list: current_function = include_list.pop() new_all_func[current_function] = all_functions[current_function] next_level = declaration_table.get(current_function, []) - number_of_functoin += len(next_level) + number_of_function += len(next_level) include_list += next_level return new_all_func ### From 87b08231629903ccd91b00d55056c44967f4769e Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 02:24:19 +0100 Subject: [PATCH 27/67] [NOBIN] Fixed indentations Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8_util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/view8_util.py b/view8_util.py index 0070a15..71f0cbb 100644 --- a/view8_util.py +++ b/view8_util.py @@ -186,11 +186,11 @@ def get_included_functions(all_functions, include_list): number_of_function = len(include_list) new_all_func = {} while include_list: - current_function = include_list.pop() - new_all_func[current_function] = all_functions[current_function] - next_level = declaration_table.get(current_function, []) - number_of_function += len(next_level) - include_list += next_level + current_function = include_list.pop() + new_all_func[current_function] = all_functions[current_function] + next_level = declaration_table.get(current_function, []) + number_of_functoin += len(next_level) + include_list += next_level return new_all_func ### From fec4adb507ffa41373a3387a8f375c8a0a491dea Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 02:25:37 +0100 Subject: [PATCH 28/67] [REFACT] Fixed comparison convention Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Simplify/simplify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Simplify/simplify.py b/Simplify/simplify.py index b6fc8a1..7dc9b41 100644 --- a/Simplify/simplify.py +++ b/Simplify/simplify.py @@ -72,7 +72,7 @@ def create_loop_reg_scope(prev_reg_scope): for k,v in prev_reg_scope.items(): if isinstance(v, int): continue - if get_context_idx_from_var(v) != None: + if get_context_idx_from_var(v) is not None: reg_scope[k] = prev_reg_scope[k] continue reg_scope[k] = Register("", v.all_initialized_index[0], True) From 8fd266b40f04db6ac7b9d9ffcf787409dcfb232c Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 02:26:16 +0100 Subject: [PATCH 29/67] [BUGFIX] Fixed argument description Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view8.py b/view8.py index ad0fa48..47a6eac 100755 --- a/view8.py +++ b/view8.py @@ -60,7 +60,7 @@ def main(): parser.add_argument('--out', '-o', help="The output file name.", default=None) parser.add_argument('--path', '-p', help="Path to disassembler binary. Required if the input is in the raw format.", default=None) parser.add_argument('--export_format', '-e', nargs='+', choices=['v8_opcode', 'translated', 'decompiled', 'serialized'], - help="Specify the export format(s). Options are 'v8_opcode', 'translated', and 'decompiled'. Multiple options can be combined.", + help="Specify the export format(s). Options are 'v8_opcode', 'translated', 'decompiled', and 'serialized'. Multiple options can be combined.", default=['decompiled']) parser.add_argument('--scope', help="Propagate scope arguments.", default=1, type=int, required=False) parser.add_argument('--tree', '-t', help="Show functions tree, starting from a given node. To start from the default main function, use 'start'", default=None) From 84818a7bf7c727d8e9f71577c4286a733c67c24d Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 02:30:29 +0100 Subject: [PATCH 30/67] [BUGFIX] More robust deletion from a map Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view8_util.py b/view8_util.py index 71f0cbb..ee535d3 100644 --- a/view8_util.py +++ b/view8_util.py @@ -175,7 +175,7 @@ def remove_exclude_functions(all_functions, exclude_list): number_of_function = len(exclude_list) while exclude_list: current_function = exclude_list.pop() - del all_functions[current_function] + all_functions.pop(current_function, None) next_level = declaration_table.get(current_function, []) number_of_function += len(next_level) exclude_list += next_level From 6ed83143d7144ceee13c732b7f3495ed00ca416e Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 02:49:06 +0100 Subject: [PATCH 31/67] [BUGFIX] In GlobalVars: has_value - protect against searching values in uninitialized collections --- Parser/shared_function_info.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 6ef5bf1..ac04069 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -41,11 +41,13 @@ def is_filled(self): return False def has_value(self, value): - val = value.strip('"') - if (value in self.strings_set or val in self.strings_set): - return True - if value in self.funcs_map.keys(): - return True + if self.strings_set is not None: + val = value.strip('"') + if (value in self.strings_set or val in self.strings_set): + return True + if self.funcs_map is not None: + if value in self.funcs_map.keys(): + return True return False def resolve_global_name(self, value): From 51531618a704e5e8e88740e2dcf13819048b99cd Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 02:53:41 +0100 Subject: [PATCH 32/67] [REFACT] Fixed convention: comparison with None --- Simplify/simplify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Simplify/simplify.py b/Simplify/simplify.py index 7dc9b41..f511f10 100644 --- a/Simplify/simplify.py +++ b/Simplify/simplify.py @@ -156,10 +156,10 @@ def replace_scope(match): scope_start, steps = scope.split("-") start_context = reg_scope['current_context'] - if scope_start in reg_scope and get_context_idx_from_var(reg_scope[scope_start]) != None: + if (scope_start in reg_scope) and (get_context_idx_from_var(reg_scope[scope_start]) is not None): start_context = get_context_idx_from_var(reg_scope[scope_start]) - elif scope_start in prev_reg_scope and get_context_idx_from_var(prev_reg_scope[scope_start]) != None: + elif (scope_start in prev_reg_scope) and (get_context_idx_from_var(prev_reg_scope[scope_start]) is not None): start_context = get_context_idx_from_var(prev_reg_scope[scope_start]) return f"Scope[{function_context_stack.get_context(start_context, int(steps))}]" From 86058436d3aab81e667eb9876c6d772a09e1e2d9 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 03:10:25 +0100 Subject: [PATCH 33/67] [BUGFIX] Use utf-8 encoding for exporting output to file Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view8_util.py b/view8_util.py index ee535d3..cebf31c 100644 --- a/view8_util.py +++ b/view8_util.py @@ -195,7 +195,7 @@ def get_included_functions(all_functions, include_list): ### def _export_to_file(out_name, all_functions, format_list, included_list = None, excluded_list = None): - with open(out_name, "w") as f: + with open(out_name, "w", encoding="utf-8") as f: print(f"Exporting to file {out_name}") for function_name in list(all_functions)[::-1]: include = True From 789936d2d3e320bd1b83c0ee28ce96ff0181f21e Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 03:11:31 +0100 Subject: [PATCH 34/67] [BUGFIX] Fixed invalid variable name Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view8_util.py b/view8_util.py index cebf31c..919e381 100644 --- a/view8_util.py +++ b/view8_util.py @@ -189,7 +189,7 @@ def get_included_functions(all_functions, include_list): current_function = include_list.pop() new_all_func[current_function] = all_functions[current_function] next_level = declaration_table.get(current_function, []) - number_of_functoin += len(next_level) + number_of_function += len(next_level) include_list += next_level return new_all_func ### From d94e98c84f8a7311876b280f790d1c29dada3d83 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 03:13:47 +0100 Subject: [PATCH 35/67] [BUGFIX] More robust LHS/RHS splitting --- Simplify/global_scope_replace.py | 45 ++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index 5ee8a14..472d8b3 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -1,6 +1,45 @@ import re from collections import defaultdict + +def find_assignment_op(line: str) -> int | None: + """ + Return the index of the first assignment '=' in `line` that is not part of + ==, ===, !=, <=, >=, =>. Ignores '=' inside single or double quoted strings. + Note: backtick/template strings are not tracked (they don't appear in + decompiled bytecode output). + """ + in_sq = False # inside single-quoted string + in_dq = False # inside double-quoted string + esc = False + + for i, ch in enumerate(line): + if esc: + esc = False + continue + if ch == '\\' and (in_sq or in_dq): + esc = True + continue + if ch == "'" and not in_dq: + in_sq = not in_sq + continue + if ch == '"' and not in_sq: + in_dq = not in_dq + continue + if in_sq or in_dq: + continue + if ch == '=': + prev = line[i - 1] if i > 0 else '' + nxt = line[i + 1] if i + 1 < len(line) else '' + if nxt in ('=', '>'): # == / === / => + continue + if prev in ('!', '<', '>'): # != / <= / >= (drop '=' from prev check) + continue + return i + return None + +### + def _print_assignments(scope_assignments): for key in scope_assignments.keys(): if scope_assignments[key] is None: @@ -46,8 +85,10 @@ def _replace_global_scope2_func(all_functions, verbosity) -> int: line = line_obj.decompiled # Split into left-hand and right-hand side of assignment - if '=' in line: - lhs, rhs = line.split('=', 1) + idx = find_assignment_op(line) + if idx is not None: + lhs = line[:idx] + rhs = line[idx + 1:] # Only replace Scope[x][y] if it appears **not** in LHS def replace_usage(match): From ed8e705c33b91b020528e28842665f4ea1f0c164 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 03:19:07 +0100 Subject: [PATCH 36/67] [BUGFIX] In is_root: protect against None argument --- view8_util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/view8_util.py b/view8_util.py index 919e381..1eaa4ac 100644 --- a/view8_util.py +++ b/view8_util.py @@ -5,6 +5,8 @@ from Parser.shared_function_info import SharedFunctionInfo, save_functions_to_file def is_root(sfi): + if sfi is None: + return False if sfi.declarer is None: return True return False From 83c905b8ff7b4615a38fb00411a9d145933f795b Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 03:30:37 +0100 Subject: [PATCH 37/67] [BUGFIX] In rename_functions_in_code: don't skip line with index 0 --- view8_util.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/view8_util.py b/view8_util.py index 1eaa4ac..b3071b9 100644 --- a/view8_util.py +++ b/view8_util.py @@ -86,14 +86,10 @@ def rename_functions_in_code( regex = re.compile(func_pattern) for func in functions.values(): - indx = 0 - while True: - indx = next_visible_line(func, indx) - if indx is None: - break - + for indx in range(len(func.code)): + if (not func.code[indx].visible) or (not func.code[indx].decompiled): + continue line = func.code[indx].decompiled - # Replace only if the found name is in renamed_dict def repl(m): name = m.group(1) From 576d373aa8c2572fe6880d7571ab1a72b7c3c5d7 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 03:35:44 +0100 Subject: [PATCH 38/67] [BUGFIX] In split_trees: protect against current function not present in the input set --- view8_util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/view8_util.py b/view8_util.py index b3071b9..5e4bc18 100644 --- a/view8_util.py +++ b/view8_util.py @@ -241,6 +241,8 @@ def export_to_file(out_name, all_functions, format_list, included_list = None, e def split_trees(functions, curr_func): sfi = functions.get(curr_func) + if sfi is None: + return None print("Tree root: " + sfi.name) if sfi.declarer is None: print("Declarer Root") From 0ff0788b6ecf37ee644d2338e0acedc3d35cff98 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 04:10:20 +0100 Subject: [PATCH 39/67] [REFACT] In init: insert View8 path at the last position --- __init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index e3c2f8c..e97e1fc 100644 --- a/__init__.py +++ b/__init__.py @@ -3,5 +3,5 @@ _view8_dir = os.path.dirname(os.path.abspath(__file__)) if _view8_dir not in sys.path: - sys.path.insert(0, _view8_dir) - + sys.path.append(_view8_dir) + \ No newline at end of file From c10a36848e6b6564cfd114a9b6c05758e186e322 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 04:19:58 +0100 Subject: [PATCH 40/67] [REFACT] Renamed function: get_all_children to get_declared_children --- view8_util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/view8_util.py b/view8_util.py index 5e4bc18..cb344ba 100644 --- a/view8_util.py +++ b/view8_util.py @@ -24,7 +24,7 @@ def get_start_function(functions): curr_func = sfi.declarer return None -def get_all_children(functions, curr_func): +def get_declared_children(functions, curr_func): children_list = [ ] for func_name, sfi in functions.items(): declarer = sfi.declarer @@ -248,7 +248,7 @@ def split_trees(functions, curr_func): print("Declarer Root") else: print("Parent: " + sfi.declarer) - children = get_all_children(functions, curr_func) + children = get_declared_children(functions, curr_func) my_map = dict() for c in children: family = get_included_functions(functions, [c]) From 6e644dcaeb21b0d6fc215340fb1f1f09fad80574 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 04:35:42 +0100 Subject: [PATCH 41/67] [BUGFIX] Removed g_GlobalVars singleton --- Parser/shared_function_info.py | 22 +++++++++------------- view8.py | 12 +++++++----- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index ac04069..f4b067b 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -1,6 +1,7 @@ from Translate.translate import translate_bytecode from Translate.jump_blocks import CodeLine from Simplify.simplify import simplify_translated_bytecode + import re import pickle from typing import Dict, List, Optional, Union @@ -71,9 +72,6 @@ def _is_string(value): return self.funcs_map[val] return None -### -## TODO: make it per file -g_GlobalVars = GlobalVars() ### @@ -107,11 +105,10 @@ def translate_bytecode(self): def simplify_bytecode(self): simplify_translated_bytecode(self, self.code) - def fill_global_variables(self): + def fill_global_variables(self, global_vars: GlobalVars): """ If the Global Vars were defined anywhere in this function, fill them in and store in the global structure. """ - global g_GlobalVars patternDef = re.compile(r'ConstPoolLiteral\[(\d+)\]') @@ -123,19 +120,18 @@ def fill_global_variables(self): if not match: continue index = int(match.group(1)) - if g_GlobalVars.parse(self.const_pool[index]): + if global_vars.parse(self.const_pool[index]): return True return False - def replace_const_pool(self): - global g_GlobalVars - + def replace_const_pool(self, global_vars: GlobalVars): + def replacement(match): index = int(match.group(2)) value = self.const_pool[index] if match.group(1) == "ConstPool": #Not ConstPoolLiteral - global_symbol = g_GlobalVars.resolve_global_name(value) + global_symbol = global_vars.resolve_global_name(value) if global_symbol: return global_symbol @@ -153,11 +149,11 @@ def replacement(match): continue line.decompiled = re.sub(pattern, replacement, line.decompiled) - def decompile(self): + def decompile(self, global_vars: GlobalVars): self.translate_bytecode() self.simplify_bytecode() - self.fill_global_variables() - self.replace_const_pool() + self.fill_global_variables(global_vars) + self.replace_const_pool(global_vars) def export(self, export_v8code=False, export_translated=False, export_decompiled=True): export_func = self.create_function_header() + '\n' diff --git a/view8.py b/view8.py index 47a6eac..402911a 100755 --- a/view8.py +++ b/view8.py @@ -2,6 +2,10 @@ import argparse import os +from Parser.parse_v8cache import parse_v8cache_file, parse_disassembled_file +from Parser.shared_function_info import GlobalVars, load_functions_from_file +from Simplify.global_scope_replace import replace_global_scope + from view8_util import ( export_to_file, find_functions_by_name, @@ -10,9 +14,6 @@ save_trees, split_trees, ) -from Parser.parse_v8cache import parse_v8cache_file, parse_disassembled_file -from Parser.shared_function_info import load_functions_from_file -from Simplify.global_scope_replace import replace_global_scope #### def disassemble(in_file, input_is_disassembled, disassembler): @@ -27,11 +28,12 @@ def disassemble(in_file, input_is_disassembled, disassembler): return parse_disassembled_file(out_name) + def decompile(all_functions): - # Decompile + global_vars = GlobalVars() print(f"Decompiling {len(all_functions)} functions.") for name in list(all_functions)[::-1]: - all_functions[name].decompile() + all_functions[name].decompile(global_vars) def propagate_global_scope(all_func, verbosity): if replace_global_scope(all_func, verbosity): From 1b48dd61db09142ba619a43a9ffee4719c4847a0 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 04:42:08 +0100 Subject: [PATCH 42/67] [BUGFIX] In resolve_global_name: guard against empty name mappings --- Parser/shared_function_info.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index f4b067b..b2597b3 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -65,11 +65,13 @@ def _is_string(value): return None val = value.strip('"') - if (value in self.strings_set or val in self.strings_set): - return "global_" + val + if self.strings_set is not None: + if (value in self.strings_set or val in self.strings_set): + return "global_" + val - if val in self.funcs_map.keys(): - return self.funcs_map[val] + if self.funcs_map is not None: + if val in self.funcs_map.keys(): + return self.funcs_map[val] return None From 10b75237cf8889ac11156c69d3b5f30ac9cacd4b Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 15:32:37 +0100 Subject: [PATCH 43/67] [REFACT] Refactored annotation for backward compat. --- Simplify/global_scope_replace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index 472d8b3..8d7b68b 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -1,8 +1,8 @@ import re from collections import defaultdict +from typing import Optional - -def find_assignment_op(line: str) -> int | None: +def find_assignment_op(line: str) -> Optional[int]: """ Return the index of the first assignment '=' in `line` that is not part of ==, ===, !=, <=, >=, =>. Ignores '=' inside single or double quoted strings. From 0c5082c74fdf927ab56bddad9869d8ffa7a42e80 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 15:37:56 +0100 Subject: [PATCH 44/67] [BUGFIX] In global replacements: if index not found in const_pool, leave the line unchanged --- Parser/shared_function_info.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index b2597b3..14bf615 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -130,6 +130,9 @@ def replace_const_pool(self, global_vars: GlobalVars): def replacement(match): index = int(match.group(2)) + if index not in self.const_pool: + return match.group(0) #Leave unchanged + value = self.const_pool[index] if match.group(1) == "ConstPool": #Not ConstPoolLiteral From db9de7f8feb5422b34cb41c1a68ae494e85c2321 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 15:46:16 +0100 Subject: [PATCH 45/67] [BUGFIX] In replace_global_scope: more robust fetching of keys Prevent errors if the key is undefined Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Simplify/global_scope_replace.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index 8d7b68b..0f4affa 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -93,18 +93,23 @@ def _replace_global_scope2_func(all_functions, verbosity) -> int: # Only replace Scope[x][y] if it appears **not** in LHS def replace_usage(match): key = (match.group(1), match.group(2)) - if scope_counts[key] == 1 and scope_assignments[key] is not None: - return scope_assignments[key] + cnt = scope_counts.get(key, 0) + val = scope_assignments.get(key) + if cnt == 1 and val is not None: + return val return match.group(0) new_rhs = pattern2.sub(replace_usage, rhs) new_line = lhs + '=' + new_rhs else: # No assignment; apply replacements freely - new_line = pattern2.sub(lambda m: ( - scope_assignments[(m.group(1), m.group(2))] - if scope_counts[(m.group(1), m.group(2))] == 1 and scope_assignments[(m.group(1), m.group(2))] is not None - else m.group(0) - ), line) + def replace_free(match): + key = (match.group(1), match.group(2)) + cnt = scope_counts.get(key, 0) + val = scope_assignments.get(key) + if cnt == 1 and val is not None: + return val + return match.group(0) + new_line = pattern2.sub(replace_free, line) if new_line != line: replaced_count += 1 From c6f5bfab83352e6be83f913c940beddd3ee5342f Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 15:50:11 +0100 Subject: [PATCH 46/67] [BUGFIX] In save_trees: use excluded_list to filter functions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view8_util.py b/view8_util.py index cb344ba..60ce238 100644 --- a/view8_util.py +++ b/view8_util.py @@ -276,7 +276,7 @@ def save_trees(all_functions, main_func, main_limit, items_map, out_dir, export_ file_name = f"{main_func}.txt" create_dirs(out_dir) out_path = os.path.join(out_dir, file_name) - export_to_file(out_path, all_functions, export_format, main_set) + export_to_file(out_path, all_functions, export_format, main_set, excluded_list) # export the subtrees: for name, filtered_func in items_map.items(): From da083f6361ae7eae59458da9119903c686254c9d Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 16:14:25 +0100 Subject: [PATCH 47/67] [REFACT] Cleanup global_scope_replace, added a comment --- Simplify/global_scope_replace.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index 0f4affa..55686a2 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -48,6 +48,20 @@ def _print_assignments(scope_assignments): print(f"Scope[{x}][{y}] = {scope_assignments[key]}") def _replace_global_scope2_func(all_functions, verbosity) -> int: + """ + Collect 2 dimensional Scope definitions, i.e. `Scope[x][y] = value` + Replace their occurrences in the code with the literal value. + Only the Scope values that are assigned once are used for the replacements. + """ + + def _replace_value(match): + key = (match.group(1), match.group(2)) + cnt = scope_counts.get(key, 0) + val = scope_assignments.get(key) + if cnt == 1 and val is not None: + return val + return match.group(0) + scope_assignments = {} scope_counts = defaultdict(int) @@ -91,25 +105,11 @@ def _replace_global_scope2_func(all_functions, verbosity) -> int: rhs = line[idx + 1:] # Only replace Scope[x][y] if it appears **not** in LHS - def replace_usage(match): - key = (match.group(1), match.group(2)) - cnt = scope_counts.get(key, 0) - val = scope_assignments.get(key) - if cnt == 1 and val is not None: - return val - return match.group(0) - new_rhs = pattern2.sub(replace_usage, rhs) + new_rhs = pattern2.sub(_replace_value, rhs) new_line = lhs + '=' + new_rhs else: # No assignment; apply replacements freely - def replace_free(match): - key = (match.group(1), match.group(2)) - cnt = scope_counts.get(key, 0) - val = scope_assignments.get(key) - if cnt == 1 and val is not None: - return val - return match.group(0) - new_line = pattern2.sub(replace_free, line) + new_line = pattern2.sub(_replace_value, line) if new_line != line: replaced_count += 1 From c3ef3e2a06f7b7b6268ee22b9ce15dff9a3f8e83 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 16:24:31 +0100 Subject: [PATCH 48/67] [NOBIN] Documented a function export_to_file --- view8_util.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/view8_util.py b/view8_util.py index 60ce238..3e49ace 100644 --- a/view8_util.py +++ b/view8_util.py @@ -220,6 +220,15 @@ def _add_or_replace_extension(filename, new_ext): return base + new_ext def export_to_file(out_name, all_functions, format_list, included_list = None, excluded_list = None): + """ + Saves the decompiled functions into a file of a given format. + + :param out_name: Name of the output file (the extension may be autoadjusted to the output format) + :param all_functions: Decompiled functions to be exported + :param format_list: A list defining the format/s that will be used to export the content. + :param included_list: If defined: only functions to be included in the output (param ignored in `serialized` format) + :param excluded_list: If defined: functions to be excluded from the output (param ignored in `serialized` format) + """ serialize_only = False serialized_ext = ".pkl" text_ext = ".txt" From 9ac9b28fe8aa5181145663474329ec85668f0ab5 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:04:35 +0100 Subject: [PATCH 49/67] [BUGFIX] Fixed searching for the assignment op Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Simplify/global_scope_replace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index 55686a2..01c4a1e 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -31,9 +31,9 @@ def find_assignment_op(line: str) -> Optional[int]: if ch == '=': prev = line[i - 1] if i > 0 else '' nxt = line[i + 1] if i + 1 < len(line) else '' - if nxt in ('=', '>'): # == / === / => + if nxt in ('=', '>'): # == / === / => continue - if prev in ('!', '<', '>'): # != / <= / >= (drop '=' from prev check) + if prev in ('!', '<', '>', '='): # != / <= / >= / == / === continue return i return None From 83dcc7a0feab352ee408191db20426d2befcfc24 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:10:39 +0100 Subject: [PATCH 50/67] [BUGFIX] In replace_const_pool: fixed index check Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Parser/shared_function_info.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 14bf615..070c197 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -130,11 +130,12 @@ def replace_const_pool(self, global_vars: GlobalVars): def replacement(match): index = int(match.group(2)) - if index not in self.const_pool: - return match.group(0) #Leave unchanged - + # Ensure const_pool exists and index is within valid bounds; otherwise leave unchanged + if self.const_pool is None or not (0 <= index < len(self.const_pool)): + return match.group(0) # Leave unchanged + value = self.const_pool[index] - if match.group(1) == "ConstPool": #Not ConstPoolLiteral + if match.group(1) == "ConstPool": # Not ConstPoolLiteral global_symbol = global_vars.resolve_global_name(value) if global_symbol: From c4f7182cb46d73fe38be6007146ee4ef08451fbf Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:12:57 +0100 Subject: [PATCH 51/67] [BUGFIX] Added error checks in tree splitting Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/view8.py b/view8.py index 402911a..d0542aa 100755 --- a/view8.py +++ b/view8.py @@ -120,7 +120,13 @@ def main(): tree_root = args.tree if tree_root == "start": tree_root = get_start_function(all_func) + if not tree_root: + print("Error: tree root function not found.") + return items_map = split_trees(all_func, tree_root) + if items_map is None: + print(f"Error: could not build tree from root '{tree_root}'.") + return if args.out: save_trees(all_func, tree_root, args.mainlimit, items_map, args.out, args.export_format, funcs_to_exclude) print(f"Done.") From 85dc76709df466f2fa9ce3182b5d2166d8f03c8f Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:13:31 +0100 Subject: [PATCH 52/67] [BUGFIX] Added error check in get_start_function Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8_util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/view8_util.py b/view8_util.py index 3e49ace..3cd0612 100644 --- a/view8_util.py +++ b/view8_util.py @@ -12,6 +12,8 @@ def is_root(sfi): return False def get_start_function(functions): + if not functions: + return None curr_func = next(iter(functions)) while True: sfi = functions.get(curr_func) From 72d3d852da5a973ceb1573aff941699578c04b85 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:25:12 +0100 Subject: [PATCH 53/67] [REFACT] More precise definition of pattern recognizing functions --- Parser/shared_function_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 070c197..b7d7cdc 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -20,7 +20,7 @@ def _extract_name(func): is_parsed = False _strings_set = set(re.findall(r'"([^"\\]*(?:\\.[^"\\]*)*)"', value)) - _funcs_set = set(re.findall(r'\bfunc_[A-Za-z0-9_$]+\b', value)) + _funcs_set = set(re.findall(r'\bfunc_[A-Za-z0-9_$]+_0x[0-9a-fA-F]+\b', value)) if _strings_set: is_parsed = True if not self.strings_set: From 10a40ae47e15e71b88dddc647779be4279b30bfb Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:41:57 +0100 Subject: [PATCH 54/67] [NOBIN] Fixed grammar in function comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Translate/jump_blocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Translate/jump_blocks.py b/Translate/jump_blocks.py index 8e7b308..5541632 100644 --- a/Translate/jump_blocks.py +++ b/Translate/jump_blocks.py @@ -10,7 +10,7 @@ def __init__(self, opcode="", line="", inst="", translated="", decompiled=""): def set_metadata(self, meta_type, meta_val): """ - Set metadata of particular type the code line + Set metadata of a particular type for the code line """ if not self.metadata: self.metadata = dict() From aef5ca10217af4554f2fe0fb35fcd7258c10e303 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:42:35 +0100 Subject: [PATCH 55/67] [NOBIN] Improved a comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Simplify/global_scope_replace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index 01c4a1e..b0db4a4 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -83,7 +83,7 @@ def _replace_value(match): continue if key in scope_assignments.keys() or not value_pattern.match(value) or exclusion_pattern.match(value): - # If the same Scope is assigned different values, mark it as invalid + # If the same Scope is assigned more than once, or the value is not eligible, mark it as invalid scope_assignments[key] = None else: scope_assignments[key] = value From 4494c7bfd9cfa4f46e86342dee870bc23a5ac76b Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:43:39 +0100 Subject: [PATCH 56/67] [NOBIN] In GlobalVars: annotate output types --- Parser/shared_function_info.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index b7d7cdc..7bddcf9 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -13,7 +13,7 @@ def __init__(self): self.strings_set = None self.funcs_map = None - def parse(self, value): + def parse(self, value) -> bool: def _extract_name(func): return func[len("func_"):func.rindex("_0x")] @@ -36,12 +36,12 @@ def _extract_name(func): self.funcs_map[short_name] = func return is_parsed - def is_filled(self): + def is_filled(self) -> bool: if self.strings_set or self.funcs_map: return True return False - def has_value(self, value): + def has_value(self, value) -> bool: if self.strings_set is not None: val = value.strip('"') if (value in self.strings_set or val in self.strings_set): @@ -51,7 +51,7 @@ def has_value(self, value): return True return False - def resolve_global_name(self, value): + def resolve_global_name(self, value) -> Optional[str]: def _is_string(value): if value.startswith('"') and value.endswith('"'): @@ -128,7 +128,7 @@ def fill_global_variables(self, global_vars: GlobalVars): def replace_const_pool(self, global_vars: GlobalVars): - def replacement(match): + def _replacement(match): index = int(match.group(2)) # Ensure const_pool exists and index is within valid bounds; otherwise leave unchanged if self.const_pool is None or not (0 <= index < len(self.const_pool)): @@ -153,7 +153,7 @@ def replacement(match): for line in self.code: if "ConstPool" not in line.decompiled: continue - line.decompiled = re.sub(pattern, replacement, line.decompiled) + line.decompiled = re.sub(pattern, _replacement, line.decompiled) def decompile(self, global_vars: GlobalVars): self.translate_bytecode() From cedb704cb35ec5264a0b43f92e23e68ba7c55fd2 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:47:31 +0100 Subject: [PATCH 57/67] [BUGFIX] Prevent from loading empty lines to the set of included/excluded functions. Allow for commenting lines out Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/view8.py b/view8.py index d0542aa..e1a7061 100755 --- a/view8.py +++ b/view8.py @@ -47,7 +47,12 @@ def propagate_global_scope(all_func, verbosity): def load_functions_set(filename): try: with open(filename, "r") as file: - deobf_funcs = set(line.strip() for line in file) + deobf_funcs = set() + for line in file: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + deobf_funcs.add(stripped) return deobf_funcs except FileNotFoundError: return None From ade829b524a36444b5683e0f4ef760420ad21022 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 17:50:20 +0100 Subject: [PATCH 58/67] [BUGFIX] In fill_global_variables: protect against fetching undefined index from const_pool Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Parser/shared_function_info.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 7bddcf9..24be0a2 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -122,6 +122,9 @@ def fill_global_variables(self, global_vars: GlobalVars): if not match: continue index = int(match.group(1)) + # Ensure const_pool exists and index is within valid bounds; otherwise skip + if self.const_pool is None or not (0 <= index < len(self.const_pool)): + continue if global_vars.parse(self.const_pool[index]): return True return False From 3a58beb567fb5be08e7147dae79fc95332f38b5d Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 18:06:19 +0100 Subject: [PATCH 59/67] [BUGFIX] When renaming functions: rename declarers too --- view8_util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/view8_util.py b/view8_util.py index 3cd0612..29b0dcb 100644 --- a/view8_util.py +++ b/view8_util.py @@ -61,9 +61,11 @@ def _rename_functions( new_functions: dict[str, SharedFunctionInfo] = {} for name, func in functions.items(): - if name in renamed_dict: + if name in renamed_dict.keys(): new_name = renamed_dict[name] func.name = new_name + if func.declarer in renamed_dict.keys(): + func.declarer = renamed_dict[func.declarer] new_functions[new_name] = func renamed_count += 1 else: From 8ede2819f4982098adefecb13d8b340fe8b510f3 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 18:10:59 +0100 Subject: [PATCH 60/67] [REFACT] Fixed input_format argument Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/view8.py b/view8.py index e1a7061..0801b08 100755 --- a/view8.py +++ b/view8.py @@ -60,9 +60,8 @@ def load_functions_set(filename): def main(): parser = argparse.ArgumentParser(description="View8: V8 cache decompiler.") - group = parser.add_mutually_exclusive_group(required=False) - group.add_argument('--input_format', '-f', choices=['raw', 'serialized', 'disassembled'], - help="Specify the input format. Options are: 'raw', 'serialized' (pickle; trusted input only), 'disassembled' (mutually exclusive)", default='raw') + parser.add_argument('--input_format', '-f', choices=['raw', 'serialized', 'disassembled'], + help="Specify the input format. Options are: 'raw', 'serialized' (pickle; trusted input only), 'disassembled'.", default='raw') parser.add_argument('--inp', '-i', help="The input file name.", default=None, required=True) parser.add_argument('--out', '-o', help="The output file name.", default=None) parser.add_argument('--path', '-p', help="Path to disassembler binary. Required if the input is in the raw format.", default=None) From 5ed6722379a4754c1da7ea730ff83403783659cf Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 18:23:28 +0100 Subject: [PATCH 61/67] [NOBIN] Fixed indentation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Simplify/global_scope_replace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index b0db4a4..e64c276 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -43,7 +43,7 @@ def find_assignment_op(line: str) -> Optional[int]: def _print_assignments(scope_assignments): for key in scope_assignments.keys(): if scope_assignments[key] is None: - continue + continue (x,y) = key print(f"Scope[{x}][{y}] = {scope_assignments[key]}") From f7b0143737bb6343458e78405ce150ff2abf6983 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 18:24:10 +0100 Subject: [PATCH 62/67] [NOBIN] Improved style Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view8.py b/view8.py index 0801b08..73b5ea4 100755 --- a/view8.py +++ b/view8.py @@ -111,7 +111,7 @@ def main(): if args.func: func_name = args.func filtered = find_functions_by_name(all_func, func_name) - if not func_name in filtered: + if func_name not in filtered: print(f"Function {func_name} was not found. Found {len(filtered)} similar names.") for key in filtered.keys(): print(key) From b34c0665ea582340a02f9637f5361c37656c47e5 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 18:25:42 +0100 Subject: [PATCH 63/67] [NOBIN] Removed a redundant space Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- view8_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view8_util.py b/view8_util.py index 29b0dcb..90bdc96 100644 --- a/view8_util.py +++ b/view8_util.py @@ -27,7 +27,7 @@ def get_start_function(functions): return None def get_declared_children(functions, curr_func): - children_list = [ ] + children_list = [] for func_name, sfi in functions.items(): declarer = sfi.declarer if declarer == curr_func: From 096b5c4de99f970cfc0c2370f1078375214fdca8 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 18:34:33 +0100 Subject: [PATCH 64/67] [REFACT] In GlobalVars: optimization, cleaned up searching functions and strings --- Parser/shared_function_info.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 24be0a2..74e2ac3 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -9,31 +9,33 @@ ### class GlobalVars: + _STRING_RE = re.compile(r'"([^"\\]*(?:\\.[^"\\]*)*)"') + _FUNC_RE = re.compile(r'\b(func_([A-Za-z0-9_$]+)_0x[0-9a-fA-F]+)\b') + def __init__(self): self.strings_set = None self.funcs_map = None def parse(self, value) -> bool: + is_parsed = False - def _extract_name(func): - return func[len("func_"):func.rindex("_0x")] + strings = set(self._STRING_RE.findall(value)) + funcs = list(self._FUNC_RE.finditer(value)) - is_parsed = False - _strings_set = set(re.findall(r'"([^"\\]*(?:\\.[^"\\]*)*)"', value)) - _funcs_set = set(re.findall(r'\bfunc_[A-Za-z0-9_$]+_0x[0-9a-fA-F]+\b', value)) - if _strings_set: + if strings: is_parsed = True - if not self.strings_set: - self.strings_set = set() - self.strings_set.update(_strings_set) + self.strings_set = (self.strings_set or set()) + self.strings_set.update(strings) - if _funcs_set: + if funcs: is_parsed = True - if not self.funcs_map: - self.funcs_map = {} - for func in _funcs_set: - short_name = _extract_name(func) - self.funcs_map[short_name] = func + self.funcs_map = (self.funcs_map or {}) + + for match in funcs: + full_name = match.group(1) + short_name = match.group(2) + self.funcs_map[short_name] = full_name + return is_parsed def is_filled(self) -> bool: From 1695ff79d48c48ae7286c71d4eb799a96b29e72c Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 18:41:57 +0100 Subject: [PATCH 65/67] [REFACT] Convention cleanups --- Parser/shared_function_info.py | 2 +- Simplify/global_scope_replace.py | 2 +- Translate/jump_blocks.py | 2 +- view8.py | 2 +- view8_util.py | 10 +++++----- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Parser/shared_function_info.py b/Parser/shared_function_info.py index 74e2ac3..1349c31 100644 --- a/Parser/shared_function_info.py +++ b/Parser/shared_function_info.py @@ -72,7 +72,7 @@ def _is_string(value): return "global_" + val if self.funcs_map is not None: - if val in self.funcs_map.keys(): + if val in self.funcs_map: return self.funcs_map[val] return None diff --git a/Simplify/global_scope_replace.py b/Simplify/global_scope_replace.py index e64c276..192bd94 100644 --- a/Simplify/global_scope_replace.py +++ b/Simplify/global_scope_replace.py @@ -82,7 +82,7 @@ def _replace_value(match): line_obj.visible = False continue - if key in scope_assignments.keys() or not value_pattern.match(value) or exclusion_pattern.match(value): + if key in scope_assignments or not value_pattern.match(value) or exclusion_pattern.match(value): # If the same Scope is assigned more than once, or the value is not eligible, mark it as invalid scope_assignments[key] = None else: diff --git a/Translate/jump_blocks.py b/Translate/jump_blocks.py index 5541632..c7e738a 100644 --- a/Translate/jump_blocks.py +++ b/Translate/jump_blocks.py @@ -24,7 +24,7 @@ def get_metadata(self, meta_type): return None if not isinstance(self.metadata, dict): return None - if not meta_type in self.metadata: + if meta_type not in self.metadata: return None return self.metadata[meta_type] diff --git a/view8.py b/view8.py index 73b5ea4..af34f21 100755 --- a/view8.py +++ b/view8.py @@ -113,7 +113,7 @@ def main(): filtered = find_functions_by_name(all_func, func_name) if func_name not in filtered: print(f"Function {func_name} was not found. Found {len(filtered)} similar names.") - for key in filtered.keys(): + for key in filtered: print(key) if len(filtered) == 0: return diff --git a/view8_util.py b/view8_util.py index 90bdc96..f8f05fb 100644 --- a/view8_util.py +++ b/view8_util.py @@ -61,10 +61,10 @@ def _rename_functions( new_functions: dict[str, SharedFunctionInfo] = {} for name, func in functions.items(): - if name in renamed_dict.keys(): + if name in renamed_dict: new_name = renamed_dict[name] func.name = new_name - if func.declarer in renamed_dict.keys(): + if func.declarer in renamed_dict: func.declarer = renamed_dict[func.declarer] new_functions[new_name] = func renamed_count += 1 @@ -154,7 +154,7 @@ def find_functions_by_name(all_func, name): funcs[name] = all_func[name] return funcs sub_name = "_" + name + "_" - for key in all_func.keys(): + for key in all_func: if sub_name in key: funcs[key] = all_func[key] return funcs @@ -196,7 +196,7 @@ def get_included_functions(all_functions, include_list): return new_all_func ### -def _export_to_file(out_name, all_functions, format_list, included_list = None, excluded_list = None): +def _export_to_file(out_name, all_functions, format_list, included_list=None, excluded_list=None): with open(out_name, "w", encoding="utf-8") as f: print(f"Exporting to file {out_name}") for function_name in list(all_functions)[::-1]: @@ -223,7 +223,7 @@ def _add_or_replace_extension(filename, new_ext): base = filename.rsplit('.', 1)[0] if '.' in filename else filename return base + new_ext -def export_to_file(out_name, all_functions, format_list, included_list = None, excluded_list = None): +def export_to_file(out_name, all_functions, format_list, included_list=None, excluded_list=None): """ Saves the decompiled functions into a file of a given format. From c9c1d844cbb57a5f014f86cc4b74f4012c04fe11 Mon Sep 17 00:00:00 2001 From: hasherezade Date: Wed, 18 Feb 2026 19:00:11 +0100 Subject: [PATCH 66/67] [BUGFIX] Ensure utf-8 encoding (in `get_next_line(file)`) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Parser/sfi_file_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Parser/sfi_file_parser.py b/Parser/sfi_file_parser.py index bbcb332..180b3fb 100644 --- a/Parser/sfi_file_parser.py +++ b/Parser/sfi_file_parser.py @@ -13,7 +13,7 @@ def set_repeat_line_flag(flag): def get_next_line(file): - with open(file, errors='ignore') as f: + with open(file, encoding='utf-8', errors='ignore') as f: for line in f: line = line.strip() if not line: From 344646bf6ef45958cd30f154bc0c309fa8c75ddd Mon Sep 17 00:00:00 2001 From: hasherezade Date: Thu, 19 Feb 2026 03:27:59 +0100 Subject: [PATCH 67/67] [BUGFIX] In get_start_function: don't use iterator to navigate the collection --- view8_util.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/view8_util.py b/view8_util.py index f8f05fb..95fadc6 100644 --- a/view8_util.py +++ b/view8_util.py @@ -14,14 +14,11 @@ def is_root(sfi): def get_start_function(functions): if not functions: return None - curr_func = next(iter(functions)) - while True: + for curr_func in functions: sfi = functions.get(curr_func) if is_root(sfi): return curr_func - if sfi is None: - break - if sfi.declarer is None: + if (sfi is None) or (sfi.declarer is None): break curr_func = sfi.declarer return None