Module refinery.lib.nsis.decompiler

Expand source code Browse git
from __future__ import annotations

import dataclasses
import io
import struct

from typing import Callable

from refinery.lib.nsis.archive import (
    OP_PARAMETER_COUNT,
    NSArchive,
    NSHeader,
    NSHeaderFlags,
    NSMethod,
    NSScriptExtendedInstruction,
    NSScriptFlags,
    NSScriptInstruction,
    NSSection,
    NSSectionFlags,
    NSType,
    Op,
)

PAGE_LICENSE = 0
PAGE_SELCOM = 1
PAGE_DIR = 2
PAGE_INSTFILES = 3
PAGE_UNINST = 4
PAGE_COMPLETED = 5
PAGE_CUSTOM = 6

PAGE_TYPES = (
    'license',
    'components',
    'directory',
    'instfiles',
    'uninstConfirm',
    'COMPLETED',
    'custom',
)

PAGE_SIZE = 64

PF_CANCEL_ENABLE = 4
PF_LICENSE_FORCE_SELECTION = 32
PF_LICENSE_NO_FORCE_SELECTION = 64
PF_PAGE_EX = 512
PF_DIR_NO_BTN_DISABLE = 1024

IDD_LICENSE_FSRB = 108
IDD_LICENSE_FSCB = 109

DEL_DIR = 1
DEL_RECURSE = 2
DEL_REBOOT = 4

ON_FUNCS = (
    'Init',
    'InstSuccess',
    'InstFailed',
    'UserAbort',
    'GUIInit',
    'GUIEnd',
    'MouseOverSection',
    'VerifyInstDir',
    'SelChange',
    'RebootFailed',
)

CMD_REF_Goto = 1 << 0
CMD_REF_Call = 1 << 1
CMD_REF_Pre = 1 << 2
CMD_REF_Show = 1 << 3
CMD_REF_Leave = 1 << 4
CMD_REF_OnFunc = 1 << 5
CMD_REF_Section = 1 << 6
CMD_REF_InitPluginDir = 1 << 7
CMD_REF_OnFunc_NumShifts = 28
CMD_REF_OnFunc_Mask = 0xF0000000
CMD_REF_Page_NumShifts = 16
CMD_REF_Page_Mask = 0x0FFF0000

INITPLUGINDIR_OPCODES = (13, 26, 31, 13, 19, 21, 11, 14, 25, 31, 1, 22, 4, 1)

_MB_BUTTONS = (
    'OK',
    'OKCANCEL',
    'ABORTRETRYIGNORE',
    'YESNOCANCEL',
    'YESNO',
    'RETRYCANCEL',
    'CANCELTRYCONTINUE',
)

_MB_ICONS = (
    None,
    'ICONSTOP',
    'ICONQUESTION',
    'ICONEXCLAMATION',
    'ICONINFORMATION',
)

_MB_FLAGS = (
    'HELP',
    'NOFOCUS',
    'SETFOREGROUND',
    'DEFAULT_DESKTOP_ONLY',
    'TOPMOST',
    'RIGHT',
    'RTLREADING',
)

_BUTTON_IDS = (
    '0',
    'IDOK',
    'IDCANCEL',
    'IDABORT',
    'IDRETRY',
    'IDIGNORE',
    'IDYES',
    'IDNO',
    'IDCLOSE',
    'IDHELP',
    'IDTRYAGAIN',
    'IDCONTINUE',
)

_SHOWWINDOW_COMMANDS = (
    'HIDE',
    'SHOWNORMAL',
    'SHOWMINIMIZED',
    'SHOWMAXIMIZED',
    'SHOWNOACTIVATE',
    'SHOW',
    'MINIMIZE',
    'SHOWMINNOACTIVE',
    'SHOWNA',
    'RESTORE',
    'SHOWDEFAULT',
    'FORCEMINIMIZE',
)

_REG_ROOTS = {
    0x00000000: 'SHCTX',
    0x80000000: 'HKCR',
    0x80000001: 'HKCU',
    0x80000002: 'HKLM',
    0x80000003: 'HKU',
    0x80000004: 'HKPD',
    0x80000005: 'HKCC',
    0x80000006: 'HKDD',
    0x80000050: 'HKPT',
    0x80000060: 'HKPN',
}

_EXEC_FLAGS = (
    'AutoClose',
    'ShellVarContext',
    'Errors',
    'Abort',
    'RebootFlag',
    'reboot_called',
    'cur_insttype',
    'plugin_api_version',
    'Silent',
    'InstDirError',
    'rtl',
    'ErrorLevel',
    'RegView',
    'DetailsPrint',
)

_FLAG_VALUE_NAMES: dict[int, dict[int, str]] = {
    0 : {0: 'false', 1: 'true'},                              # AutoClose
    1 : {0: 'current', 1: 'all'},                             # ShellVarContext
    4 : {0: 'false', 1: 'true'},                              # RebootFlag
    8 : {0: 'normal', 1: 'silent'},                           # Silent
    12: {0: '32', 256: '64'},                                 # RegView
    13: {0: 'both', 2: 'textonly', 4: 'listonly', 6: 'none'}, # DetailsPrint
}

_SECTION_VARS = (
    'Text',
    'InstTypes',
    'Flags',
    'Code',
    'CodeSize',
    'Size',
)

_WIN_ATTRIBS = (
    'READONLY',
    'HIDDEN',
    'SYSTEM',
    None,
    'DIRECTORY',
    'ARCHIVE',
    'DEVICE',
    'NORMAL',
    'TEMPORARY',
    'SPARSE_FILE',
    'REPARSE_POINT',
    'COMPRESSED',
    'OFFLINE',
    'NOT_CONTENT_INDEXED',
    'ENCRYPTED',
    None,
    'VIRTUAL',
)

_OVERWRITE_MODES = (
    'on',
    'off',
    'try',
    'ifnewer',
    'ifdiff',
)

VK_F1 = 0x70

NSIS_MAX_INST_TYPES = 32

_POST_STRINGS = (
    'install_directory_auto_append',
    'uninstchild',
    'uninstcmd',
    'wininit',
)


def _decode_flag_value(flag_id: int, s: str) -> str | None:
    table = _FLAG_VALUE_NAMES.get(flag_id)
    if table is None:
        return None
    try:
        v = int(s, 0)
    except (ValueError, TypeError):
        return None
    return table.get(v)


def decode_messagebox(param: int) -> str:
    """
    Decode MB_* flags from a single uint32 into a pipe-delimited string.
    """
    parts: list[str] = []
    v = param & 0xF
    if v < len(_MB_BUTTONS):
        parts.append(F'MB_{_MB_BUTTONS[v]}')
    else:
        parts.append(F'MB_Buttons_{v}')
    icon = (param >> 4) & 0x7
    if icon != 0:
        if icon < len(_MB_ICONS) and _MB_ICONS[icon] is not None:
            parts.append(F'MB_{_MB_ICONS[icon]}')
        else:
            parts.append(F'MB_Icon_{icon}')
    if param & 0x80:
        parts.append('MB_USERICON')
    def_button = (param >> 8) & 0xF
    if def_button != 0:
        parts.append(F'MB_DEFBUTTON{def_button + 1}')
    modal = (param >> 12) & 0x3
    if modal == 1:
        parts.append('MB_SYSTEMMODAL')
    elif modal == 2:
        parts.append('MB_TASKMODAL')
    elif modal == 3:
        parts.append('0x3000')
    flags = param >> 14
    for i, name in enumerate(_MB_FLAGS):
        if flags & (1 << i):
            parts.append(F'MB_{name}')
    return '|'.join(parts)


def decode_button_id(button_id: int) -> str:
    """
    Map a button ID integer to its name (IDOK, IDCANCEL, ...).
    """
    if button_id < len(_BUTTON_IDS):
        return _BUTTON_IDS[button_id]
    return F'Button_{button_id}'


def decode_showwindow(cmd: int) -> str:
    """
    Map a SW_* command index to its name.
    """
    if cmd < len(_SHOWWINDOW_COMMANDS):
        return F'SW_{_SHOWWINDOW_COMMANDS[cmd]}'
    return str(cmd)


def decode_reg_root(val: int) -> str:
    """
    Map a registry root value to its name.
    """
    name = _REG_ROOTS.get(val)
    if name is not None:
        return name
    return F'0x{val:08X}'


def decode_exec_flags(flags_type: int) -> str:
    """
    Map an exec flag type index to its name.
    """
    if flags_type < len(_EXEC_FLAGS):
        return _EXEC_FLAGS[flags_type]
    return F'_{flags_type}'


def decode_sect_op(op_type: int) -> str:
    """
    Map a section operation type index to its name.
    """
    if op_type < len(_SECTION_VARS):
        return _SECTION_VARS[op_type]
    return F'_{op_type}'


def decode_file_attributes(flags: int) -> str:
    """
    Decode a file attribute bitmask into a pipe-delimited string.
    """
    parts: list[str] = []
    remaining = flags
    for i, name in enumerate(_WIN_ATTRIBS):
        bit = 1 << i
        if remaining & bit:
            if name is not None:
                parts.append(name)
                remaining &= ~bit
    if remaining:
        parts.append(F'0x{remaining:X}')
    return '|'.join(parts)


def decode_setoverwrite(mode: int) -> str:
    """
    Map an overwrite mode index to its name.
    """
    if mode < len(_OVERWRITE_MODES):
        return _OVERWRITE_MODES[mode]
    return str(mode)


def decode_shortcut_hotkey(spec: int) -> str:
    """
    Decode a CreateShortcut hotkey spec into a human-readable string. The high byte contains
    modifier flags, the next byte is the virtual key.
    """
    mod_key = (spec >> 24) & 0xFF
    key = (spec >> 16) & 0xFF
    if mod_key == 0 and key == 0:
        return ''
    parts: list[str] = []
    if mod_key & 1:
        parts.append('SHIFT')
    if mod_key & 2:
        parts.append('CONTROL')
    if mod_key & 4:
        parts.append('ALT')
    if mod_key & 8:
        parts.append('EXT')
    if VK_F1 <= key <= VK_F1 + 23:
        parts.append(F'F{key - VK_F1 + 1}')
    elif (0x41 <= key <= 0x5A) or (0x30 <= key <= 0x39):
        parts.append(chr(key))
    else:
        parts.append(F'Char_{key}')
    return '|'.join(parts)


class NSScriptWriter:
    """
    Thin wrapper around an output buffer providing NSIS script formatting helpers.
    """

    def __init__(self):
        self._buf = io.StringIO()
        self._indent = 0
        self._block_comment = False
        self._line_start = True

    def _emit_line_prefix(self):
        if self._line_start and self._block_comment:
            self._buf.write('; ')
        self._line_start = False

    def indent(self):
        self._indent += 1

    def dedent(self):
        if self._indent > 0:
            self._indent -= 1

    def write(self, s: str):
        self._emit_line_prefix()
        self._buf.write(s)

    def newline(self):
        self._buf.write('\n')
        self._line_start = True

    def space(self):
        self._buf.write(' ')

    def tab(self, commented: bool = False):
        self._emit_line_prefix()
        if commented and not self._block_comment:
            self._buf.write('    ; ')
        else:
            self._buf.write('  ' + '  ' * self._indent)

    def big_space_comment(self):
        self._buf.write('    ; ')

    def small_space_comment(self):
        self._buf.write(' ; ')

    def comment(self, s: str):
        self._emit_line_prefix()
        self._buf.write(F'; {s}')

    def separator(self):
        self.newline()
        self.comment(F'{"":-<72}')
        self.newline()

    def comment_open(self):
        self._block_comment = True
        self._line_start = True

    def comment_close(self):
        self._block_comment = False

    def add_uint(self, v: int):
        self._buf.write(str(v))

    def add_hex(self, v: int):
        self._buf.write(F'0x{v:08X}')

    def add_color(self, v: int):
        v = ((v & 0xFF) << 16) | (v & 0xFF00) | ((v >> 16) & 0xFF)
        self._buf.write(F'0x{v:06X}')

    def add_quoted(self, s: str):
        self._emit_line_prefix()
        if _is_bare_identifier(s) or _is_numeric(s):
            self._buf.write(s)
        else:
            self._buf.write(F'"{s}"')

    def space_quoted(self, s: str):
        self.space()
        self.add_quoted(s)

    def add_string_lf(self, s: str):
        self._emit_line_prefix()
        self._buf.write(s)
        self.newline()

    def tab_string(self, s: str):
        self.tab()
        self._buf.write(s)

    def add_quotes(self):
        self._buf.write('""')

    def getvalue(self) -> str:
        return self._buf.getvalue()


def nsis_escape(s: str) -> str:
    """
    Apply NSIS escape sequences to a string for decompiler output.
    """
    out: list[str] = []
    for c in s:
        if c == '\t':
            out.append('$\\t')
        elif c == '\n':
            out.append('$\\n')
        elif c == '\r':
            out.append('$\\r')
        elif c == '"':
            out.append('$\\"')
        else:
            out.append(c)
    return ''.join(out)


def _is_bare_identifier(s: str) -> bool:
    if len(s) < 2 or s[0] != '$':
        return False
    return all(c.isascii() and (c.isalnum() or c == '_') for c in s[1:])


def _is_numeric(s: str) -> bool:
    if not s:
        return False
    t = s
    if t[0] in '+-':
        t = t[1:]
    if not t:
        return False
    if t.startswith('0x') or t.startswith('0X'):
        t = t[2:]
        return len(t) > 0 and all(c in '0123456789abcdefABCDEF' for c in t)
    return t.isdigit()


def format_section_begin(
    writer: NSScriptWriter,
    section: NSSection,
    index: int,
    read_string: Callable[[int], str | None],
    is_installer: bool,
) -> bool:
    """
    Write Section/SectionGroup/SectionGroupEnd header. Returns True if the section is a group
    marker (no body to close with SectionEnd).
    """
    flags = NSSectionFlags(section.flags)
    name = read_string(section.name)
    if name is None:
        name = ''
    if not is_installer and name.lower() != 'uninstall':
        name = F'un.{name}'
    if flags & NSSectionFlags.BOLD:
        name = F'!{name}'

    if flags & NSSectionFlags.SECGRPEND:
        writer.add_string_lf('SectionGroupEnd')
        return True

    if flags & NSSectionFlags.SECGRP:
        writer.write('SectionGroup')
        if flags & NSSectionFlags.EXPAND:
            writer.write(' /e')
        writer.space_quoted(name)
        writer.small_space_comment()
        writer.write(F'Section_{index}')
        writer.newline()
        return True

    writer.write('Section')
    if not (flags & NSSectionFlags.SELECTED):
        writer.write(' /o')
    if name:
        writer.space_quoted(name)
    writer.small_space_comment()
    writer.write(F'Section_{index}')
    writer.newline()

    if section.size_kb != 0:
        writer.tab()
        writer.comment(F'AddSize {section.size_kb}')
        writer.newline()

    need_section_in = (
        (section.name != 0 and section.install_types != 0)
        or (section.name == 0 and section.install_types != 0xFFFFFFFF)
    )
    if need_section_in or (flags & NSSectionFlags.RO):
        writer.tab_string('SectionIn')
        inst_types = section.install_types
        for i in range(32):
            if inst_types & (1 << i):
                writer.write(F' {i + 1}')
        if flags & NSSectionFlags.RO:
            writer.write(' RO')
        writer.newline()

    return False


def format_section_end(writer: NSScriptWriter):
    """
    Write SectionEnd.
    """
    writer.add_string_lf('SectionEnd')
    writer.newline()


def _func_name(labels: list[int], index: int) -> str:
    """
    Generate a function name string from the label flags at the given index.
    """
    mask = labels[index]
    if mask & CMD_REF_OnFunc:
        func_id = (mask >> CMD_REF_OnFunc_NumShifts) & 0xF
        if func_id < len(ON_FUNCS):
            return F'.on{ON_FUNCS[func_id]}'
        return F'.onFunc_{func_id}'
    if mask & CMD_REF_InitPluginDir:
        return 'Initialize_____Plugins'
    return F'func_{index}'


def _add_param_func(
    writer: NSScriptWriter,
    labels: list[int],
    index: int,
):
    """
    Write a space followed by a function name or empty quotes.
    """
    writer.space()
    signed = index if index < 0x80000000 else index - 0x100000000
    if signed >= 0 and index < len(labels):
        writer.write(_func_name(labels, index))
    else:
        writer.add_quotes()


def emit_pages(
    writer: NSScriptWriter,
    header: NSHeader,
    labels: list[int],
    is_installer: bool,
) -> None:
    """
    Emit Page/UninstPage/PageEx blocks from the bh_pages data. Updates the labels array with
    CMD_REF_Pre/Show/Leave for page callbacks.
    """
    bh = header.bh_pages
    if bh.count == 0:
        return

    writer.separator()
    writer.comment(F'PAGES: {bh.count}')
    writer.newline()

    raw = bytes(header.raw_data)
    num_instructions = len(labels)

    if (
        bh.count > (1 << 12)
        or bh.offset > len(raw)
        or bh.count * PAGE_SIZE > len(raw) - bh.offset
    ):
        writer.big_space_comment()
        writer.write('!!! ERROR: Pages error')
        writer.newline()
        return

    writer.newline()

    for page_index in range(bh.count):
        base = bh.offset + page_index * PAGE_SIZE
        p = raw[base:base + PAGE_SIZE]
        if len(p) < PAGE_SIZE:
            break

        dlg_id = struct.unpack_from('<I', p, 0)[0]
        wnd_proc_id = struct.unpack_from('<I', p, 4)[0]
        pre_func = struct.unpack_from('<I', p, 8)[0]
        show_func = struct.unpack_from('<I', p, 12)[0]
        leave_func = struct.unpack_from('<I', p, 16)[0]
        flags = struct.unpack_from('<I', p, 20)[0]
        caption = struct.unpack_from('<I', p, 24)[0]
        next_param = struct.unpack_from('<I', p, 32)[0]
        params = [struct.unpack_from('<I', p, 44 + 4 * i)[0] for i in range(5)]

        def _set_func_ref(func_index: int, flag: int):
            signed = func_index if func_index < 0x80000000 else func_index - 0x100000000
            if signed >= 0 and func_index < num_instructions:
                labels[func_index] = (
                    (labels[func_index] & ~CMD_REF_Page_Mask)
                    | flag
                    | (page_index << CMD_REF_Page_NumShifts)
                )

        _set_func_ref(pre_func, CMD_REF_Pre)
        _set_func_ref(show_func, CMD_REF_Show)
        _set_func_ref(leave_func, CMD_REF_Leave)

        if wnd_proc_id == PAGE_COMPLETED:
            writer.comment_open()

        writer.comment(F'Page {page_index}')
        writer.newline()

        if flags & PF_PAGE_EX:
            writer.write('PageEx ')
            if not is_installer:
                writer.write('un.')
        else:
            writer.write('Page ' if is_installer else 'UninstPage ')

        if wnd_proc_id < len(PAGE_TYPES):
            writer.write(PAGE_TYPES[wnd_proc_id])
        else:
            writer.add_uint(wnd_proc_id)

        pre_signed = pre_func if pre_func < 0x80000000 else pre_func - 0x100000000
        show_signed = show_func if show_func < 0x80000000 else show_func - 0x100000000
        leave_signed = leave_func if leave_func < 0x80000000 else leave_func - 0x100000000
        need_callbacks = pre_signed >= 0 or show_signed >= 0 or leave_signed >= 0

        if flags & PF_PAGE_EX:
            writer.newline()
            if need_callbacks:
                writer.tab_string('PageCallbacks')

        if need_callbacks:
            _add_param_func(writer, labels, pre_func)
            if wnd_proc_id != PAGE_CUSTOM:
                _add_param_func(writer, labels, show_func)
            _add_param_func(writer, labels, leave_func)

        if not (flags & PF_PAGE_EX):
            if flags & PF_CANCEL_ENABLE:
                writer.write(' /ENABLECANCEL')
            writer.newline()
        else:
            writer.newline()
            if caption != 0:
                writer.tab_string('Caption')
                writer.space_quoted(header._read_string(caption) or '')
                writer.newline()

        if wnd_proc_id == PAGE_LICENSE:
            _emit_license_page(writer, header, flags, dlg_id, params, next_param)
        elif wnd_proc_id == PAGE_SELCOM:
            _emit_page_option(writer, header, params, 3, 'ComponentsText')
        elif wnd_proc_id == PAGE_DIR:
            _emit_page_option(writer, header, params, 4, 'DirText')
            if params[4] != 0:
                writer.tab_string('DirVar')
                writer.space()
                writer.write(header._string_code_variable(params[4] - 1))
                writer.newline()
            if flags & PF_DIR_NO_BTN_DISABLE:
                writer.tab_string('DirVerify leave')
                writer.newline()
        elif wnd_proc_id == PAGE_INSTFILES:
            if params[2] != 0:
                writer.tab_string('CompletedText')
                writer.space_quoted(header._read_string(params[2]) or '')
                writer.newline()
            if params[1] != 0:
                writer.tab_string('DetailsButtonText')
                writer.space_quoted(header._read_string(params[1]) or '')
                writer.newline()
        elif wnd_proc_id == PAGE_UNINST:
            if params[4] != 0:
                writer.tab_string('DirVar')
                writer.space()
                writer.write(header._string_code_variable(params[4] - 1))
                writer.newline()
            _emit_page_option(writer, header, params, 2, 'UninstallText')

        if flags & PF_PAGE_EX:
            writer.write('PageExEnd')
            writer.newline()
        if wnd_proc_id == PAGE_COMPLETED:
            writer.comment_close()
        writer.newline()


def _emit_license_page(
    writer: NSScriptWriter,
    header: NSHeader,
    flags: int,
    dlg_id: int,
    params: list[int],
    next_param: int,
) -> None:
    if (flags & PF_LICENSE_NO_FORCE_SELECTION) or (flags & PF_LICENSE_FORCE_SELECTION):
        writer.tab_string('LicenseForceSelection ')
        if flags & PF_LICENSE_NO_FORCE_SELECTION:
            writer.write('off')
        else:
            if dlg_id == IDD_LICENSE_FSCB:
                writer.write('checkbox')
            elif dlg_id == IDD_LICENSE_FSRB:
                writer.write('radiobuttons')
            else:
                writer.add_uint(dlg_id)
            for i in range(2, 4):
                if params[i] != 0:
                    writer.space_quoted(header._read_string(params[i]) or '')
        writer.newline()

    if params[0] != 0 or next_param != 0:
        writer.tab_string('LicenseText')
        writer.space_quoted(header._read_string(params[0]) or '')
        if next_param != 0:
            writer.space_quoted(header._read_string(next_param) or '')
        writer.newline()

    if params[1] != 0:
        writer.tab_string('LicenseData')
        signed = params[1] if params[1] < 0x80000000 else params[1] - 0x100000000
        if signed < 0:
            writer.space_quoted(header._read_string(params[1]) or '')
        else:
            writer.write(F' #{params[1]}')
        writer.newline()


def _emit_page_option(
    writer: NSScriptWriter,
    header: NSHeader,
    params: list[int],
    num: int,
    name: str,
) -> None:
    actual_num = num
    while actual_num > 0 and params[actual_num - 1] == 0:
        actual_num -= 1
    if actual_num == 0:
        return
    writer.tab_string(name)
    for i in range(actual_num):
        writer.space_quoted(header._read_string(params[i]) or '')
    writer.newline()


def _format_description(header: NSHeader) -> str:
    """
    Build the NSIS type description string (e.g. "NSIS-3", "NSIS-Park-1").
    """
    if header.type >= NSType.Park1:
        suffix = '1'
        if header.type is NSType.Park2:
            suffix = '2'
        elif header.type is NSType.Park3:
            suffix = '3'
        s = F'NSIS-Park-{suffix}'
    else:
        s = 'NSIS-2' if header.type is NSType.Nsis2 else 'NSIS-3'
    if header._is_nsis200:
        s += '.00'
    elif header._is_nsis225:
        s += '.25'
    if header.unicode:
        s += ' Unicode'
    if header._log_cmd_is_enabled:
        s += ' log'
    if header._bad_cmd >= 0:
        s += F' BadCmd={header._bad_cmd}'
    return s


def emit_header_info(
    writer: NSScriptWriter,
    header: NSHeader,
    archive: NSArchive | None,
    is_installer: bool,
) -> None:
    """
    Emit header metadata: NSIS type, compressor, flags, language tables, InstType list,
    InstallDir, and post-header strings.
    """
    writer.comment('NSIS script')
    if header.unicode:
        writer.write(' (UTF-8)')
    writer.space()
    writer.write(_format_description(header))
    writer.newline()

    writer.comment('Install' if is_installer else 'Uninstall')
    writer.newline()
    writer.newline()

    if header.unicode:
        writer.add_string_lf('Unicode true')

    if archive is not None and archive.method is not NSMethod.Copy:
        writer.write('SetCompressor')
        if archive.solid:
            writer.write(' /SOLID')
        method_name = {
            NSMethod.Deflate: 'zlib',
            NSMethod.NSGzip: 'zlib',
            NSMethod.BZip2: 'bzip2',
            NSMethod.LZMA: 'lzma',
        }.get(archive.method)
        if method_name:
            writer.space()
            writer.write(method_name)
        writer.newline()

    if archive is not None and archive.method is NSMethod.LZMA and archive.lzma_options is not None:
        writer.write('SetCompressorDictSize ')
        writer.add_uint(archive.lzma_options.dictionary_size >> 20)
        writer.newline()

    writer.separator()

    writer.comment(F'HEADER SIZE: {header.raw_size}')
    writer.newline()

    if header.bh_pages.offset != 0:
        writer.comment(F'START HEADER SIZE: {header.bh_pages.offset}')
        writer.newline()

    bh_sections = header.bh_sections
    bh_entries = header.bh_entries
    if bh_sections.count > 0 and bh_entries.offset > bh_sections.offset:
        section_size = (bh_entries.offset - bh_sections.offset) // bh_sections.count
        if section_size >= 24:
            max_string_len = section_size - 24
            if header.unicode:
                max_string_len >>= 1
            if max_string_len == 0:
                writer.comment(F'SECTION SIZE: {section_size}')
            else:
                writer.comment(F'MAX STRING LENGTH: {max_string_len}')
            writer.newline()

    num_string_chars = header.bh_langtbl.offset - header.bh_strings.offset
    if header.unicode:
        num_string_chars >>= 1
    writer.comment(F'STRING CHARS: {num_string_chars}')
    writer.newline()

    if header.bh_ctlcolors.offset > header.raw_size:
        writer.big_space_comment()
        writer.add_string_lf('Bad COLORS TABLE')

    if header.bh_ctlcolors.count != 0:
        writer.comment(F'COLORS Num: {header.bh_ctlcolors.count}')
        writer.newline()

    if header.bh_font.count != 0:
        writer.comment(F'FONTS Num: {header.bh_font.count}')
        writer.newline()

    if header.bh_data.count != 0:
        writer.comment(F'DATA NUM: {header.bh_data.count}')
        writer.newline()

    writer.newline()
    writer.add_string_lf('OutFile [NSIS].exe')
    writer.add_string_lf('!include WinMessages.nsh')
    writer.newline()

    raw = bytes(header.raw_data)
    if len(raw) < 4:
        return

    eh_flags = NSScriptFlags(struct.unpack_from('<I', raw, 0)[0])
    show_details = int(eh_flags) & 3
    if 1 <= show_details <= 2:
        writer.write('ShowInstDetails' if is_installer else 'ShowUninstDetails')
        writer.add_string_lf(' show' if show_details == 1 else ' nevershow')

    if eh_flags & NSScriptFlags.PROGRESS_COLORED:
        writer.add_string_lf('InstProgressFlags colored')
    if eh_flags & (NSScriptFlags.SILENT | NSScriptFlags.SILENT_LOG):
        writer.write('SilentInstall ' if is_installer else 'SilentUnInstall ')
        writer.add_string_lf('silentlog' if (eh_flags & NSScriptFlags.SILENT_LOG) else 'silent')
    if eh_flags & NSScriptFlags.AUTO_CLOSE:
        writer.add_string_lf('AutoCloseWindow true')
    if not (eh_flags & NSScriptFlags.NO_ROOT_DIR):
        writer.add_string_lf('AllowRootDirInstall true')
    if eh_flags & NSScriptFlags.NO_CUSTOM:
        writer.add_string_lf('InstType /NOCUSTOM')
    if eh_flags & NSScriptFlags.COMP_ONLY_ON_CUSTOM:
        writer.add_string_lf('InstType /COMPONENTSONLYONCUSTOM')

    bho_size = 12 if header.is64bit else 8
    params_offset = 4 + bho_size * 8
    if header.bh_pages.offset == 276:
        params_offset -= bho_size

    if params_offset + 40 > len(raw):
        return

    def _get32(off: int) -> int:
        return struct.unpack_from('<I', raw, off)[0]

    p2 = params_offset

    root_key = _get32(p2)
    sub_key = _get32(p2 + 4)
    value = _get32(p2 + 8)
    if (root_key != 0 and root_key != 0xFFFFFFFF) or sub_key != 0 or value != 0:
        writer.write('InstallDirRegKey')
        writer.space()
        writer.write(decode_reg_root(root_key))
        s = header._read_string(sub_key)
        if s:
            writer.space_quoted(s)
        s = header._read_string(value)
        if s:
            writer.space_quoted(s)
        writer.newline()

    bg_color1 = _get32(p2 + 12)
    bg_color2 = _get32(p2 + 16)
    bg_textcolor = _get32(p2 + 20)
    if bg_color1 != 0xFFFFFFFF or bg_color2 != 0xFFFFFFFF or bg_textcolor != 0xFFFFFFFF:
        writer.write('BGGradient')
        if bg_color1 != 0 or bg_color2 != 0xFF0000 or bg_textcolor != 0xFFFFFFFF:
            writer.space()
            writer.add_color(bg_color1)
            writer.space()
            writer.add_color(bg_color2)
            if bg_textcolor != 0xFFFFFFFF:
                writer.space()
                writer.add_color(bg_textcolor)
        writer.newline()

    lb_bg = _get32(p2 + 24)
    lb_fg = _get32(p2 + 28)
    if (lb_bg != 0xFFFFFFFF or lb_fg != 0xFFFFFFFF) and (lb_bg != 0 or lb_fg != 0xFF00):
        writer.write('InstallColors')
        writer.space()
        writer.add_color(lb_fg)
        writer.space()
        writer.add_color(lb_bg)
        writer.newline()

    license_bg = _get32(p2 + 36)
    signed_license_bg = license_bg if license_bg < 0x80000000 else license_bg - 0x100000000
    if license_bg != 0xFFFFFFFF and signed_license_bg != -15:
        writer.write('LicenseBkColor')
        if signed_license_bg == -5:
            writer.write(' /windows')
        else:
            writer.space()
            writer.add_color(license_bg)
        writer.newline()

    langtable_size = _get32(p2 + 32)
    bh_langtbl = header.bh_langtbl
    if bh_langtbl.count > 0 and langtable_size != 0xFFFFFFFF:
        if langtable_size >= 10:
            num_strings = (langtable_size - 10) // 4
        else:
            num_strings = 0

        writer.newline()
        writer.separator()
        writer.comment(F'LANG TABLES: {bh_langtbl.count}')
        writer.newline()
        writer.comment(F'LANG STRINGS: {num_strings}')
        writer.newline()
        writer.newline()

        license_lang_index = -1
        bh_pages = header.bh_pages
        if bh_pages.count > 0:
            for page_i in range(bh_pages.count):
                page_base = bh_pages.offset + page_i * PAGE_SIZE
                if page_base + PAGE_SIZE > len(raw):
                    break
                wnd_proc_id = struct.unpack_from('<I', raw, page_base + 4)[0]
                param1 = struct.unpack_from('<I', raw, page_base + 44 + 4)[0]
                if wnd_proc_id != PAGE_LICENSE or param1 == 0:
                    continue
                signed_param1 = param1 if param1 < 0x80000000 else param1 - 0x100000000
                if signed_param1 < 0:
                    license_lang_index = -(signed_param1 + 1)

        if license_lang_index >= 0:
            for i in range(bh_langtbl.count):
                lang_base = bh_langtbl.offset + langtable_size * i
                if lang_base + 10 + (license_lang_index + 1) * 4 > len(raw):
                    break
                lang_id = struct.unpack_from('<H', raw, lang_base)[0]
                val = struct.unpack_from('<I', raw, lang_base + 10 + license_lang_index * 4)[0]
                if val != 0:
                    writer.write(F'LicenseLangString LSTR_{license_lang_index}')
                    writer.write(F' {lang_id}')
                    s = header._read_string(val)
                    if s:
                        writer.space_quoted(nsis_escape(s))
                    writer.newline()
            writer.newline()

        branding_text = 0
        name_val = 0
        for i in range(bh_langtbl.count):
            lang_base = bh_langtbl.offset + langtable_size * i
            if lang_base + 10 + 3 * 4 > len(raw):
                break
            lang_id = struct.unpack_from('<H', raw, lang_base)[0]
            v0 = struct.unpack_from('<I', raw, lang_base + 10 + 0 * 4)[0]
            v2 = struct.unpack_from('<I', raw, lang_base + 10 + 2 * 4)[0]
            if v0 != 0 and (lang_id == 1033 or branding_text == 0):
                branding_text = v0
            if v2 != 0 and (lang_id == 1033 or name_val == 0):
                name_val = v2

        if name_val != 0:
            writer.write('Name')
            s = header._read_string(name_val)
            if s:
                writer.space_quoted(s)
            writer.newline()

        if branding_text != 0:
            writer.write('BrandingText')
            s = header._read_string(branding_text)
            if s:
                writer.space_quoted(s)
            writer.newline()

        for i in range(bh_langtbl.count):
            lang_base = bh_langtbl.offset + langtable_size * i
            if lang_base + 10 > len(raw):
                break
            lang_id = struct.unpack_from('<H', raw, lang_base)[0]
            writer.newline()
            writer.comment(F'LANG: {lang_id}')
            writer.newline()

            for j in range(num_strings):
                str_off = lang_base + 10 + j * 4
                if str_off + 4 > len(raw):
                    break
                val = struct.unpack_from('<I', raw, str_off)[0]
                if val != 0 and j != license_lang_index:
                    writer.write(F'LangString LSTR_{j} {lang_id}')
                    s = header._read_string(val)
                    if s:
                        writer.space_quoted(nsis_escape(s))
                    writer.newline()
            writer.newline()

    on_func_count = len(ON_FUNCS)
    if header.bh_pages.offset == 276:
        on_func_count -= 1

    p2_after = params_offset + 40 + on_func_count * 4
    writer.newline()

    for i in range(NSIS_MAX_INST_TYPES + 1):
        off = p2_after + i * 4
        if off + 4 > len(raw):
            break
        inst_type = _get32(off)
        if inst_type != 0:
            writer.write('InstType')
            s = ''
            if not is_installer:
                s = 'un.'
            resolved = header._read_string(inst_type)
            if resolved:
                s += resolved
            writer.space_quoted(s)
            writer.newline()

    install_dir_off = p2_after + (NSIS_MAX_INST_TYPES + 1) * 4
    if install_dir_off + 4 <= len(raw):
        install_dir = _get32(install_dir_off)
        if install_dir != 0:
            writer.write('InstallDir')
            s = header._read_string(install_dir)
            if s:
                writer.space_quoted(nsis_escape(s))
            writer.newline()

    if header.bh_pages.offset >= 288:
        post_off = install_dir_off + 4
        for i in range(4):
            if i != 0 and header.bh_pages.offset < 300:
                break
            if post_off + 4 > len(raw):
                break
            param = _get32(post_off + 4 * i)
            if param == 0 or param == 0xFFFFFFFF:
                continue
            writer.comment(F'{_POST_STRINGS[i]} =')
            s = header._read_string(param)
            if s:
                writer.space_quoted(nsis_escape(s))
            writer.newline()

    writer.newline()


SW_HIDE = 0
SW_SHOWNORMAL = 1
SW_SHOWMINIMIZED = 2
SW_SHOWMINNOACTIVE = 7
SW_SHOWNA = 8

GENERIC_READ = 1 << 31
GENERIC_WRITE = 1 << 30
GENERIC_EXECUTE = 1 << 29
GENERIC_ALL = 1 << 28

CREATE_NEW = 1
CREATE_ALWAYS = 2
OPEN_EXISTING = 3
OPEN_ALWAYS = 4
TRUNCATE_EXISTING = 5

TRANSPARENT = 1

COLORS_TEXT = 1
COLORS_TEXT_SYS = 2
COLORS_BK = 4
COLORS_BK_SYS = 8
COLORS_BKB = 16

CTL_COLORS_SIZE = 24

K_EXEC_FLAGS_ERRORS = 2
K_EXEC_FLAGS_DETAILS_PRINT = 13

K_INTOP_OPS = '+-*/|&^!|&%<>'


def _add_goto_var(writer: NSScriptWriter, header: NSHeader, param: int):
    writer.space()
    signed = param if param < 0x80000000 else param - 0x100000000
    if signed < 0:
        writer.write(header._string_code_variable(-(signed + 1)))
    else:
        writer.write(F'label_{param - 1}')


def _add_goto_var1(writer: NSScriptWriter, header: NSHeader, param: int):
    if param == 0:
        writer.write(' 0')
    else:
        _add_goto_var(writer, header, param)


def _add_goto_vars2(writer: NSScriptWriter, header: NSHeader, p0: int, p1: int):
    _add_goto_var1(writer, header, p0)
    if p1 != 0:
        _add_goto_var(writer, header, p1)


def _add_param(writer: NSScriptWriter, header: NSHeader, param: int):
    writer.space()
    s = header._read_string(param)
    if s is not None:
        writer.add_quoted(nsis_escape(s))
    else:
        writer.add_uint(param)


def _add_param_var(writer: NSScriptWriter, header: NSHeader, param: int):
    writer.space()
    writer.write(header._string_code_variable(param))


def _add_params(writer: NSScriptWriter, header: NSHeader, params: list[int], num: int):
    for i in range(num):
        _add_param(writer, header, params[i])


def _add_optional_param(writer: NSScriptWriter, header: NSHeader, param: int):
    if param != 0:
        _add_param(writer, header, param)


def _add_optional_params(
    writer: NSScriptWriter, header: NSHeader, params: list[int], num: int,
):
    actual = num
    while actual > 0 and params[actual - 1] == 0:
        actual -= 1
    _add_params(writer, header, params, actual)


def render_opcode(
    writer: NSScriptWriter,
    header: NSHeader,
    instruction: NSScriptInstruction | NSScriptExtendedInstruction,
    opcode: Op,
    labels: list[int],
    raw: bytes,
    overwrite_state: list[int],
    address: int,
    commented: bool,
) -> None:
    """
    Render a single NSIS opcode's arguments to the writer. Handles all opcodes except Ret and
    INVALID_OPCODE, which require structural context handled by the caller.
    """
    params = instruction.arguments

    if opcode is Op.Nop:
        if params[0] == 0:
            writer.write('Nop')
        else:
            writer.write('Goto')
            _add_goto_var(writer, header, params[0])

    elif opcode is Op.Abort:
        writer.write(opcode.name)
        _add_optional_param(writer, header, params[0])

    elif opcode is Op.Quit:
        writer.write(opcode.name)

    elif opcode is Op.Call:
        writer.write('Call ')
        signed_p0 = params[0] if params[0] < 0x80000000 else params[0] - 0x100000000
        if signed_p0 < 0:
            writer.write(header._string_code_variable(-(signed_p0 + 1)))
        elif params[0] == 0:
            writer.write('0')
        else:
            val = params[0] - 1
            if params[1] == 1:
                writer.write(F':label_{val}')
            else:
                writer.write(_func_name(labels, val))

    elif opcode is Op.DetailPrint or opcode is Op.Sleep:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])

    elif opcode is Op.BringToFront:
        writer.write(opcode.name)

    elif opcode is Op.SetDetailsView:
        writer.write(opcode.name)
        if params[0] == SW_SHOWNA and params[1] == SW_HIDE:
            writer.write(' show')
        elif params[1] == SW_SHOWNA and params[0] == SW_HIDE:
            writer.write(' hide')
        else:
            for i in range(2):
                writer.space()
                writer.write(decode_showwindow(params[i]))

    elif opcode is Op.SetFileAttributes:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        writer.space()
        writer.write(decode_file_attributes(params[1]))

    elif opcode is Op.CreateDirectory:
        is_set_out_path = params[1] != 0
        writer.write('SetOutPath' if is_set_out_path else 'CreateDirectory')
        _add_param(writer, header, params[0])
        if params[2] != 0:
            writer.small_space_comment()
            writer.write('CreateRestrictedDirectory')

    elif opcode is Op.IfFileExists:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        _add_goto_vars2(writer, header, params[1], params[2])

    elif opcode is Op.SetFlag:
        s_val = header._read_string(params[1]) or ''
        if params[0] == K_EXEC_FLAGS_ERRORS and params[2] == 0:
            if s_val == '0':
                writer.write('ClearErrors')
            else:
                writer.write('SetErrors')
        else:
            writer.write('Set')
            writer.write(decode_exec_flags(params[0]))
            if params[2] != 0:
                writer.write(' lastused')
            else:
                decoded = _decode_flag_value(params[0], s_val)
                if decoded is not None:
                    writer.space()
                    writer.write(decoded)
                else:
                    writer.space_quoted(nsis_escape(s_val))

    elif opcode is Op.IfFlag:
        writer.write('If')
        writer.write(decode_exec_flags(params[2]))
        _add_goto_vars2(writer, header, params[0], params[1])

    elif opcode is Op.GetFlag:
        writer.write('Get')
        writer.write(decode_exec_flags(params[1]))
        _add_param_var(writer, header, params[0])

    elif opcode is Op.Rename:
        writer.write(opcode.name)
        if params[2] != 0:
            writer.write(' /REBOOTOK')
        _add_params(writer, header, params, 2)
        if params[3] != 0:
            writer.small_space_comment()
            _add_param(writer, header, params[3])

    elif opcode is Op.GetFullPathName:
        writer.write(opcode.name)
        if params[2] == 0:
            writer.write(' /SHORT')
        _add_param_var(writer, header, params[1])
        _add_param(writer, header, params[0])

    elif opcode is Op.SearchPath or opcode is Op.StrLen:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.GetTempFileName:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        s = header._read_string(params[1])
        if s and s != '$TEMP':
            writer.space_quoted(nsis_escape(s))

    elif opcode is Op.ExtractFile:
        overwrite = params[0] & 0x7
        if overwrite != overwrite_state[0]:
            writer.write('SetOverwrite ')
            writer.write(decode_setoverwrite(overwrite))
            overwrite_state[0] = overwrite
            writer.newline()
            writer.tab(commented)
        writer.write('File')
        _add_param(writer, header, params[1])

    elif opcode is Op.Delete:
        writer.write(opcode.name)
        flag = params[1]
        if flag & DEL_REBOOT:
            writer.write(' /REBOOTOK')
        _add_param(writer, header, params[0])

    elif opcode is Op.MessageBox:
        writer.write(opcode.name)
        writer.space()
        writer.write(decode_messagebox(params[0]))
        _add_param(writer, header, params[1])
        button_id = params[0] >> 21
        if button_id != 0:
            writer.write(' /SD')
            writer.write(decode_button_id(button_id))
        for i in range(2, 6, 2):
            if params[i] != 0:
                writer.space()
                writer.write(decode_button_id(params[i]))
                _add_goto_var1(writer, header, params[i + 1])

    elif opcode is Op.RMDir:
        writer.write(opcode.name)
        flag = params[1]
        if flag & DEL_RECURSE:
            writer.write(' /r')
        if flag & DEL_REBOOT:
            writer.write(' /REBOOTOK')
        _add_param(writer, header, params[0])

    elif opcode is Op.AssignVar:
        writer.write('StrCpy')
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])
        _add_optional_params(writer, header, params[2:4], 2)

    elif opcode is Op.StrCmp:
        writer.write('StrCmpS' if params[4] != 0 else 'StrCmp')
        _add_params(writer, header, params, 2)
        _add_goto_vars2(writer, header, params[2], params[3])

    elif opcode is Op.ReadEnvStr:
        writer.write('ReadEnvStr' if params[2] != 0 else 'ExpandEnvStrings')
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.IntCmp:
        writer.write('IntCmpU' if params[5] != 0 else 'IntCmp')
        _add_params(writer, header, params, 2)
        _add_goto_var1(writer, header, params[2])
        if params[3] != 0 or params[4] != 0:
            _add_goto_vars2(writer, header, params[3], params[4])

    elif opcode is Op.IntOp:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        op_index = params[3]
        c = K_INTOP_OPS[op_index] if op_index < 13 else '?'
        c2 = '' if (op_index < 8 or op_index == 10) else c
        num_ops = 1 if op_index == 7 else 2
        _add_param(writer, header, params[1])
        writer.space()
        writer.write(c)
        if num_ops != 1:
            if c2:
                writer.write(c2)
            _add_param(writer, header, params[2])

    elif opcode is Op.IntFmt:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_params(writer, header, params[1:3], 2)

    elif opcode is Op.PushPop:
        if params[2] != 0:
            writer.write('Exch')
            if params[2] != 1:
                writer.write(F' {params[2]}')
        elif params[1] != 0:
            writer.write('Pop')
            _add_param_var(writer, header, params[0])
        else:
            writer.write('Push')
            _add_param(writer, header, params[0])

    elif opcode is Op.FindWindow:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])
        _add_optional_params(writer, header, params[2:5], 3)

    elif opcode is Op.SendMessage:
        writer.write(opcode.name)
        _add_param(writer, header, params[1])
        _add_param(writer, header, params[2])
        spec = params[5]
        for i in range(2):
            prefix = 'STR:' if (spec & (1 << i)) else ''
            s = header._read_string(params[3 + i])
            writer.space_quoted(prefix + nsis_escape(s or ''))
        signed_p0 = params[0] if params[0] < 0x80000000 else params[0] - 0x100000000
        if signed_p0 >= 0:
            _add_param_var(writer, header, params[0])
        timeout = spec >> 2
        if timeout != 0:
            writer.write(F' /TIMEOUT={timeout}')

    elif opcode is Op.IsWindow:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        _add_goto_vars2(writer, header, params[1], params[2])

    elif opcode is Op.GetDlgItem:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_params(writer, header, params[1:3], 2)

    elif opcode is Op.SetCtlColors:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        offset = params[1]
        bh_cc = header.bh_ctlcolors
        if (
            header.raw_size < bh_cc.offset
            or header.raw_size - bh_cc.offset < offset
            or header.raw_size - bh_cc.offset - offset < CTL_COLORS_SIZE
        ):
            writer.write(' ; bad offset')
        else:
            p2 = bh_cc.offset + offset
            cc_text = struct.unpack_from('<I', raw, p2)[0]
            cc_bkc = struct.unpack_from('<I', raw, p2 + 4)[0]
            cc_bkmode = struct.unpack_from('<i', raw, p2 + 16)[0]
            cc_flags = struct.unpack_from('<i', raw, p2 + 20)[0]
            if (cc_flags & COLORS_BK_SYS) or (cc_flags & COLORS_TEXT_SYS):
                writer.write(' /BRANDING')
            bk = ''
            bkc_print = False
            if cc_bkmode == TRANSPARENT:
                bk = ' transparent'
            elif cc_flags & COLORS_BKB:
                if not (cc_flags & COLORS_BK_SYS) and (cc_flags & COLORS_BK):
                    bkc_print = True
            if (cc_flags & COLORS_TEXT) or bk or bkc_print:
                writer.space()
                if (cc_flags & COLORS_TEXT_SYS) or not (cc_flags & COLORS_TEXT):
                    writer.add_quotes()
                else:
                    writer.add_color(cc_text)
            writer.write(bk)
            if bkc_print:
                writer.space()
                writer.add_color(cc_bkc)

    elif opcode is Op.SetBrandingImage:
        writer.write(opcode.name)
        writer.write(F' /IMGID={params[1]}')
        if params[2] == 1:
            writer.write(' /RESIZETOFIT')
        _add_param(writer, header, params[0])

    elif opcode is Op.CreateFont:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])
        _add_optional_params(writer, header, params[2:4], 2)
        if params[4] & 1:
            writer.write(' /ITALIC')
        if params[4] & 2:
            writer.write(' /UNDERLINE')
        if params[4] & 4:
            writer.write(' /STRIKE')

    elif opcode is Op.ShowWindow:
        if params[3] != 0:
            writer.write('EnableWindow')
        else:
            writer.write('ShowWindow')
        _add_param(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.ExecShell:
        writer.write(opcode.name)
        _add_params(writer, header, params, 2)
        if params[2] != 0 or params[3] != SW_SHOWNORMAL:
            _add_param(writer, header, params[2])
            if params[3] != SW_SHOWNORMAL:
                writer.space()
                writer.write(decode_showwindow(params[3]))
        if params[5] != 0:
            writer.write('    ;')
            _add_param(writer, header, params[5])

    elif opcode is Op.Exec:
        writer.write('ExecWait' if params[2] != 0 else 'Exec')
        _add_param(writer, header, params[0])
        signed_p1 = params[1] if params[1] < 0x80000000 else params[1] - 0x100000000
        if params[2] != 0 and signed_p1 >= 0:
            _add_param_var(writer, header, params[1])

    elif opcode is Op.GetFileTime or opcode is Op.GetDLLVersion:
        writer.write(opcode.name)
        _add_param(writer, header, params[2])
        _add_param_var(writer, header, params[0])
        _add_param_var(writer, header, params[1])

    elif opcode is Op.RegisterDll:
        func_str = header._read_string(params[1]) or ''
        print_func = True
        if params[2] == 0:
            writer.write('CallInstDLL')
            _add_param(writer, header, params[0])
            if params[3] == 1:
                writer.write(' /NOUNLOAD')
        else:
            if func_str == 'DllUnregisterServer':
                writer.write('UnRegDLL')
                print_func = False
            else:
                writer.write('RegDLL')
                if func_str == 'DllRegisterServer':
                    print_func = False
            _add_param(writer, header, params[0])
        if print_func:
            writer.space_quoted(nsis_escape(func_str))

    elif opcode is Op.CreateShortCut:
        writer.write(opcode.name)
        num_params = 6
        while num_params > 2 and params[num_params - 1] == 0:
            num_params -= 1
        spec = params[4]
        if spec & 0x8000:
            writer.write(' /NoWorkingDir')
        _add_params(writer, header, params, min(num_params, 4))
        if num_params <= 4:
            pass
        else:
            icon = spec & 0xFF
            writer.space()
            if icon != 0:
                writer.add_uint(icon)
            else:
                writer.add_quotes()
            sw = (spec >> 8) & 0x7F
            if (spec >> 8) == 0 and num_params < 6:
                pass
            else:
                if sw == SW_SHOWMINNOACTIVE:
                    sw = SW_SHOWMINIMIZED
                writer.space()
                if sw == 0:
                    writer.add_quotes()
                else:
                    writer.write(decode_showwindow(sw))
                hotkey = decode_shortcut_hotkey(spec)
                if hotkey:
                    writer.space()
                    writer.write(hotkey)
                elif num_params >= 6:
                    writer.space()
                    writer.add_quotes()
                _add_optional_param(writer, header, params[5])

    elif opcode is Op.CopyFiles:
        writer.write(opcode.name)
        if params[2] & 0x04:
            writer.write(' /SILENT')
        if params[2] & 0x80:
            writer.write(' /FILESONLY')
        _add_params(writer, header, params, 2)
        if params[3] != 0:
            writer.write('    ;')
            _add_param(writer, header, params[3])

    elif opcode is Op.Reboot:
        writer.write(opcode.name)
        if params[0] != 0xBADF00D:
            writer.write(' ; Corrupted ???')

    elif opcode is Op.WriteINI:
        num_always = 0
        if params[0] == 0:
            writer.write('FlushINI')
        elif params[4] != 0:
            writer.write('WriteINIStr')
            num_always = 3
        else:
            writer.write('DeleteINI')
            writer.write('Sec' if params[1] == 0 else 'Str')
            num_always = 1
        _add_param(writer, header, params[3])
        _add_params(writer, header, params, num_always)
        _add_optional_params(writer, header, params[num_always:3], 3 - num_always)

    elif opcode is Op.ReadINIStr:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[3])
        _add_params(writer, header, params[1:3], 2)

    elif opcode is Op.DeleteReg:
        writer.write(opcode.name)
        if params[4] == 0:
            writer.write('Value')
        else:
            writer.write('Key')
            if params[4] & 2:
                writer.write(' /ifempty')
        writer.space()
        writer.write(decode_reg_root(params[1]))
        _add_param(writer, header, params[2])
        _add_optional_param(writer, header, params[3])

    elif opcode is Op.WriteReg:
        writer.write(opcode.name)
        reg_type_name = {1: 'Str', 2: 'ExpandStr', 3: 'Bin', 4: 'DWORD'}.get(params[4])
        if params[4] == 1 and params[5] == 2:
            reg_type_name = 'ExpandStr'
        if reg_type_name:
            writer.write(reg_type_name)
        else:
            writer.write(F'?{params[4]}')
        writer.space()
        writer.write(decode_reg_root(params[0]))
        _add_params(writer, header, params[1:3], 2)
        if params[4] != 3:
            _add_param(writer, header, params[3])
        else:
            writer.write(F' data[{params[3]} ... ]')

    elif opcode is Op.ReadReg:
        writer.write(opcode.name)
        writer.write('DWORD' if params[4] == 1 else 'Str')
        _add_param_var(writer, header, params[0])
        writer.space()
        writer.write(decode_reg_root(params[1]))
        _add_params(writer, header, params[2:4], 2)

    elif opcode is Op.EnumReg:
        writer.write(opcode.name)
        writer.write('Key' if params[4] != 0 else 'Value')
        _add_param_var(writer, header, params[0])
        writer.space()
        writer.write(decode_reg_root(params[1]))
        _add_params(writer, header, params[2:4], 2)

    elif opcode is Op.FileClose or opcode is Op.FindClose:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])

    elif opcode is Op.FileOpen:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[3])
        acc = params[1]
        creat = params[2]
        if acc != 0 or creat != 0:
            cc = ''
            if acc == GENERIC_READ and creat == OPEN_EXISTING:
                cc = 'r'
            elif creat == CREATE_ALWAYS and acc == GENERIC_WRITE:
                cc = 'w'
            elif creat == OPEN_ALWAYS and acc == (GENERIC_WRITE | GENERIC_READ):
                cc = 'a'
            if cc:
                writer.write(F' {cc}')
            else:
                if acc & GENERIC_READ:
                    writer.write(' GENERIC_READ')
                if acc & GENERIC_WRITE:
                    writer.write(' GENERIC_WRITE')
                if acc & GENERIC_EXECUTE:
                    writer.write(' GENERIC_EXECUTE')
                if acc & GENERIC_ALL:
                    writer.write(' GENERIC_ALL')
                creat_name = {
                    CREATE_NEW: 'CREATE_NEW',
                    CREATE_ALWAYS: 'CREATE_ALWAYS',
                    OPEN_EXISTING: 'OPEN_EXISTING',
                    OPEN_ALWAYS: 'OPEN_ALWAYS',
                    TRUNCATE_EXISTING: 'TRUNCATE_EXISTING',
                }.get(creat)
                writer.space()
                writer.write(creat_name or str(creat))

    elif opcode is Op.FileWrite or opcode is Op.FileWriteW:
        writer.write('FileWrite')
        if opcode is Op.FileWriteW:
            writer.write('UTF16LE' if params[2] == 0 else 'Word')
        elif params[2] != 0:
            writer.write('Byte')
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.FileRead or opcode is Op.FileReadW:
        writer.write('FileRead')
        if opcode is Op.FileReadW:
            writer.write('UTF16LE' if params[3] == 0 else 'Word')
        elif params[3] != 0:
            writer.write('Byte')
        _add_param_var(writer, header, params[0])
        _add_param_var(writer, header, params[1])
        _add_optional_param(writer, header, params[2])

    elif opcode is Op.FileSeek:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[2])
        if params[3] == 1:
            writer.write(' CUR')
        if params[3] == 2:
            writer.write(' END')
        signed_p1 = params[1] if params[1] < 0x80000000 else params[1] - 0x100000000
        if signed_p1 >= 0:
            if params[3] == 0:
                writer.write(' SET')
            _add_param_var(writer, header, params[1])

    elif opcode is Op.FindNext:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[1])
        _add_param_var(writer, header, params[0])

    elif opcode is Op.FindFirst:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[1])
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[2])

    elif opcode is Op.WriteUninstaller:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        if params[3] != 0:
            writer.small_space_comment()
            _add_param(writer, header, params[3])

    elif opcode is Op.SectionSet:
        writer.write('Section')
        signed_p2 = params[2] if params[2] < 0x80000000 else params[2] - 0x100000000
        if signed_p2 >= 0:
            writer.write('Get')
            writer.write(decode_sect_op(params[2]))
            _add_param(writer, header, params[0])
            _add_param_var(writer, header, params[1])
        else:
            writer.write('Set')
            t = -(signed_p2) - 1
            writer.write(decode_sect_op(t))
            _add_param(writer, header, params[0])
            _add_param(writer, header, params[4] if t == 0 else params[1])

    elif opcode is Op.InstTypeSet:
        if params[3] == 0:
            if params[2] == 0:
                writer.write('InstTypeGetText')
                _add_param(writer, header, params[0])
                _add_param_var(writer, header, params[1])
            else:
                writer.write('InstTypeSetText')
                _add_params(writer, header, params, 2)
        else:
            if params[2] == 0:
                writer.write('GetCurInstType')
                _add_param_var(writer, header, params[1])
            else:
                writer.write('SetCurInstType')
                _add_param(writer, header, params[0])

    elif opcode is Op.LockWindow:
        writer.write(opcode.name)
        writer.write(' on' if params[0] == 0 else ' off')

    elif opcode is Op.Log:
        if params[0] != 0:
            writer.write('LogSet ')
            writer.write('off' if params[1] == 0 else 'on')
        else:
            writer.write('LogText')
            _add_param(writer, header, params[1])

    elif opcode is Op.FindProc:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.GetFontVersion or opcode is Op.GetFontName:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.GetOSInfo:
        writer.write(opcode.name)

    elif opcode is Op.ReservedOpcode:
        writer.write('GetFunctionAddress')

    else:
        writer.write(F'Command{int(opcode)}')


def emit_commands(
    writer: NSScriptWriter,
    header: NSHeader,
    label_info: LabelInfo,
    sections: list[NSSection],
    is_installer: bool,
) -> None:
    """
    Emit the main instruction body: per-opcode decompilation switch, section open/close, function
    open/close, and label markers. Function bodies are rendered using structural analysis to
    recover If/Else constructs with indentation.
    """
    from refinery.lib.nsis.controlflow import (
        build_cfg,
        eliminate_dead_code,
        linearize,
        reduce_if_else,
        reduce_loops,
        render_node,
    )

    labels = label_info.labels
    num_instructions = len(header.instructions)
    num_sections = len(sections)

    overwrite_state = [0]
    cur_section_index = 0
    section_is_open = False
    on_func_is_open = False
    end_comment_index = 0

    raw = bytes(header.raw_data)

    func_ranges: dict[int, int] = {}
    k = 0
    while k < num_instructions:
        flg = labels[k]
        if not is_func(flg):
            k += 1
            continue
        func_start = k
        k += 1
        while k < num_instructions:
            insn = header.instructions[k]
            opcode = header.opcode(insn)
            if opcode is Op.Ret:
                if k + 1 >= num_instructions or is_probably_end_of_func(labels[k + 1]):
                    func_ranges[func_start] = k + 1
                    k += 1
                    break
            k += 1
        else:
            func_ranges[func_start] = k

    writer.separator()

    kkk = 0
    while kkk < num_instructions:
        instruction = header.instructions[kkk]
        opcode = header.opcode(instruction)

        is_section_group = False
        while cur_section_index < num_sections:
            sect = sections[cur_section_index]
            if section_is_open:
                if sect.start_cmd_index + sect.num_commands + 1 != kkk:
                    break
                format_section_end(writer)
                section_is_open = False
                cur_section_index += 1
                continue
            if sect.start_cmd_index != kkk:
                break
            if format_section_begin(writer, sect, cur_section_index, header._read_string, is_installer):
                is_section_group = True
                cur_section_index += 1
            else:
                section_is_open = True

        if labels[kkk] != 0 and labels[kkk] != CMD_REF_Section:
            flg = labels[kkk]
            if is_func(flg):
                if kkk == label_info.init_plugins_dir_start:
                    writer.comment_open()
                on_func_is_open = True
                writer.write('Function ')
                writer.write(_func_name(labels, kkk))
                if is_page_func(flg):
                    writer.big_space_comment()
                    writer.write('Page ')
                    writer.add_uint((flg & CMD_REF_Page_Mask) >> CMD_REF_Page_NumShifts)
                    if flg & CMD_REF_Leave:
                        writer.write(', Leave')
                    if flg & CMD_REF_Pre:
                        writer.write(', Pre')
                    if flg & CMD_REF_Show:
                        writer.write(', Show')
                writer.newline()

                if kkk in func_ranges:
                    func_end = func_ranges[kkk]
                    body_end = func_end - 1
                    cfg = build_cfg(header, kkk, body_end)
                    root = linearize(header, cfg, labels, kkk, body_end)
                    reduced = reduce_loops(root, header)
                    reduced = reduce_if_else(reduced, header)
                    reduced = eliminate_dead_code(reduced)
                    render_node(
                        reduced, writer, header, labels, raw,
                        overwrite_state, end_comment_index,
                    )
                    if labels[body_end] & CMD_REF_Goto:
                        writer.write(F'label_{body_end}:')
                        writer.newline()
                    writer.add_string_lf('FunctionEnd')
                    if func_end == label_info.init_plugins_dir_end:
                        writer.comment_close()
                    writer.newline()
                    on_func_is_open = False
                    kkk = func_end
                    continue

            if flg & CMD_REF_Goto:
                writer.write(F'label_{kkk}:')
                writer.newline()

        commented = kkk < end_comment_index

        if opcode is Op.INVALID_OPCODE:
            writer.tab(commented)
            writer.write(F'Command{instruction.opcode}')
            writer.newline()

        elif opcode is Op.Ret:
            if on_func_is_open:
                if kkk == num_instructions - 1 or is_probably_end_of_func(labels[kkk + 1]):
                    writer.add_string_lf('FunctionEnd')
                    if kkk + 1 == label_info.init_plugins_dir_end:
                        writer.comment_close()
                    writer.newline()
                    on_func_is_open = False
                    kkk += 1
                    continue
            if is_section_group:
                kkk += 1
                continue
            if section_is_open:
                sect = sections[cur_section_index]
                if sect.start_cmd_index + sect.num_commands == kkk:
                    format_section_end(writer)
                    section_is_open = False
                    cur_section_index += 1
                    kkk += 1
                    continue
            writer.tab_string('Return')
            writer.newline()

        else:
            writer.tab(commented)
            render_opcode(writer, header, instruction, opcode, labels, raw, overwrite_state, kkk, commented)
            writer.newline()

        kkk += 1

    if section_is_open and cur_section_index < num_sections:
        sect = sections[cur_section_index]
        if sect.start_cmd_index + sect.num_commands + 1 == num_instructions:
            format_section_end(writer)
            cur_section_index += 1

    while cur_section_index < num_sections:
        sect = sections[cur_section_index]
        if section_is_open:
            if sect.start_cmd_index + sect.num_commands != num_instructions:
                break
            format_section_end(writer)
            section_is_open = False
            cur_section_index += 1
            continue
        if sect.start_cmd_index != num_instructions:
            break
        format_section_begin(writer, sect, cur_section_index, header._read_string, is_installer)
        cur_section_index += 1


_GOTO_MASKS: dict[Op, int] = {
    Op.Nop           : 1 << 0,
    Op.IfFileExists  : 3 << 1,
    Op.IfFlag        : 3 << 0,
    Op.MessageBox    : 5 << 3,
    Op.StrCmp        : 3 << 2,
    Op.IntCmp        : 7 << 2,
    Op.IsWindow      : 3 << 1,
}


def is_func(flag: int) -> bool:
    """
    Check whether a label flag indicates a function start.
    """
    return bool(flag & (CMD_REF_Call | CMD_REF_Pre | CMD_REF_Show | CMD_REF_Leave | CMD_REF_OnFunc))


def is_page_func(flag: int) -> bool:
    """
    Check whether a label flag indicates a page callback function.
    """
    return bool(flag & (CMD_REF_Pre | CMD_REF_Show | CMD_REF_Leave))


def is_probably_end_of_func(flag: int) -> bool:
    """
    Check whether a label flag likely ends a function body.
    """
    return flag != 0 and flag != CMD_REF_Goto


@dataclasses.dataclass
class LabelInfo:
    """
    Result of label analysis pass over NSIS instructions.
    """
    labels: list[int]
    init_plugins_dir_start: int = -1
    init_plugins_dir_end: int = -1


def build_labels(header: NSHeader, sections: list[NSSection]) -> LabelInfo:
    """
    Build the label array from an NSHeader. Each entry is a bitmask indicating whether the
    instruction at that index is a jump target, call target, function start, section start,
    onFunc entry, or InitPluginsDir.
    """
    num_instructions = len(header.instructions)
    labels = [0] * num_instructions
    raw = bytes(header.raw_data)

    bho_size = 12 if header.is64bit else 8
    params_offset = 4 + bho_size * 8
    if header.bh_pages.offset == 276:
        params_offset -= bho_size
    on_func_offset = params_offset + 40
    num_on_func = len(ON_FUNCS)
    if header.bh_pages.offset == 276:
        num_on_func -= 1

    for i in range(num_on_func):
        off = on_func_offset + 4 * i
        if off + 4 <= len(raw):
            func = struct.unpack_from('<I', raw, off)[0]
            if func < num_instructions:
                labels[func] = (labels[func] & ~CMD_REF_OnFunc_Mask) | (CMD_REF_OnFunc | (i << CMD_REF_OnFunc_NumShifts))

    for section in sections:
        if section.start_cmd_index < num_instructions:
            labels[section.start_cmd_index] |= CMD_REF_Section

    for instruction in header.instructions:
        opcode = header.opcode(instruction)
        args = instruction.arguments

        if opcode is Op.Call:
            if len(args) > 1 and args[1] == 1:
                param0 = args[0]
                if 0 < param0 <= num_instructions:
                    labels[param0 - 1] |= CMD_REF_Goto
            else:
                param0 = args[0]
                if param0 > 0 and param0 <= num_instructions:
                    labels[param0 - 1] |= CMD_REF_Call
            continue

        mask = _GOTO_MASKS.get(opcode)
        if mask is None:
            continue

        for i in range(6):
            if not mask:
                break
            if mask & 1:
                param = args[i] if i < len(args) else 0
                signed = param if param < 0x80000000 else param - 0x100000000
                if signed > 0 and param <= num_instructions:
                    labels[param - 1] |= CMD_REF_Goto
            mask >>= 1

    init_start = -1
    init_end = -1
    ipd_len = len(INITPLUGINDIR_OPCODES)
    for k in range(num_instructions):
        flg = labels[k]
        if not is_func(flg):
            continue
        if num_instructions - k < ipd_len:
            continue
        match = True
        for j in range(ipd_len):
            insn = header.instructions[k + j]
            cmd_id = header.opcode(insn)
            if cmd_id != INITPLUGINDIR_OPCODES[j]:
                match = False
                break
        if match:
            init_start = k
            init_end = k + ipd_len
            labels[k] |= CMD_REF_InitPluginDir
            break

    return LabelInfo(
        labels=labels,
        init_plugins_dir_start=init_start,
        init_plugins_dir_end=init_end,
    )


class NSDecompiler:
    """
    Produces NSIS script output from a parsed NSIS header, combining header info emission, page
    emission, and per-opcode command emission.
    """

    def __init__(self, header: NSHeader, archive: NSArchive | None = None):
        self.header = header
        self.archive = archive
        self.is_installer = True
        if archive is not None:
            self.is_installer = not bool(archive.flags & NSHeaderFlags.Uninstall)

    def decompile(self) -> str:
        header = self.header
        writer = NSScriptWriter()
        sections = header.sections
        label_info = build_labels(header, sections)
        emit_header_info(writer, header, self.archive, self.is_installer)
        emit_pages(writer, header, label_info.labels, self.is_installer)
        emit_commands(writer, header, label_info, sections, self.is_installer)
        return writer.getvalue()


class NSDisassembler:
    """
    Produces a flat opcode listing from an NSIS script header.
    """

    def __init__(self, header: NSHeader):
        self.header = header

    def disassemble(self) -> str:
        header = self.header
        script = io.StringIO()
        name_width = max(len(op.name) for op in Op)
        addr_width = len(F'{len(header.instructions):X}')
        for k, instruction in enumerate(header.instructions):
            if k > 0:
                script.write('\n')
            opcode = header.opcode(instruction)
            if opcode is Op.INVALID_OPCODE:
                name = F'Command{instruction.opcode}'
            else:
                name = opcode.name
            script.write(F'0x{k:0{addr_width}X}: {name:<{name_width}} ')
            for j, arg in enumerate(
                instruction.arguments[:OP_PARAMETER_COUNT.get(opcode, 6)]
            ):
                if j > 0:
                    script.write(', ')
                if arg > 20 and header._is_good_string(arg):
                    script.write(repr(header._read_string(arg)))
                elif arg < 0x100:
                    script.write(str(arg))
                elif arg < 0x10000:
                    script.write(F'0x{arg:04X}')
                else:
                    script.write(F'0x{arg:08X}')
        return script.getvalue()

Functions

def decode_messagebox(param)

Decode MB_* flags from a single uint32 into a pipe-delimited string.

Expand source code Browse git
def decode_messagebox(param: int) -> str:
    """
    Decode MB_* flags from a single uint32 into a pipe-delimited string.
    """
    parts: list[str] = []
    v = param & 0xF
    if v < len(_MB_BUTTONS):
        parts.append(F'MB_{_MB_BUTTONS[v]}')
    else:
        parts.append(F'MB_Buttons_{v}')
    icon = (param >> 4) & 0x7
    if icon != 0:
        if icon < len(_MB_ICONS) and _MB_ICONS[icon] is not None:
            parts.append(F'MB_{_MB_ICONS[icon]}')
        else:
            parts.append(F'MB_Icon_{icon}')
    if param & 0x80:
        parts.append('MB_USERICON')
    def_button = (param >> 8) & 0xF
    if def_button != 0:
        parts.append(F'MB_DEFBUTTON{def_button + 1}')
    modal = (param >> 12) & 0x3
    if modal == 1:
        parts.append('MB_SYSTEMMODAL')
    elif modal == 2:
        parts.append('MB_TASKMODAL')
    elif modal == 3:
        parts.append('0x3000')
    flags = param >> 14
    for i, name in enumerate(_MB_FLAGS):
        if flags & (1 << i):
            parts.append(F'MB_{name}')
    return '|'.join(parts)
def decode_button_id(button_id)

Map a button ID integer to its name (IDOK, IDCANCEL, …).

Expand source code Browse git
def decode_button_id(button_id: int) -> str:
    """
    Map a button ID integer to its name (IDOK, IDCANCEL, ...).
    """
    if button_id < len(_BUTTON_IDS):
        return _BUTTON_IDS[button_id]
    return F'Button_{button_id}'
def decode_showwindow(cmd)

Map a SW_* command index to its name.

Expand source code Browse git
def decode_showwindow(cmd: int) -> str:
    """
    Map a SW_* command index to its name.
    """
    if cmd < len(_SHOWWINDOW_COMMANDS):
        return F'SW_{_SHOWWINDOW_COMMANDS[cmd]}'
    return str(cmd)
def decode_reg_root(val)

Map a registry root value to its name.

Expand source code Browse git
def decode_reg_root(val: int) -> str:
    """
    Map a registry root value to its name.
    """
    name = _REG_ROOTS.get(val)
    if name is not None:
        return name
    return F'0x{val:08X}'
def decode_exec_flags(flags_type)

Map an exec flag type index to its name.

Expand source code Browse git
def decode_exec_flags(flags_type: int) -> str:
    """
    Map an exec flag type index to its name.
    """
    if flags_type < len(_EXEC_FLAGS):
        return _EXEC_FLAGS[flags_type]
    return F'_{flags_type}'
def decode_sect_op(op_type)

Map a section operation type index to its name.

Expand source code Browse git
def decode_sect_op(op_type: int) -> str:
    """
    Map a section operation type index to its name.
    """
    if op_type < len(_SECTION_VARS):
        return _SECTION_VARS[op_type]
    return F'_{op_type}'
def decode_file_attributes(flags)

Decode a file attribute bitmask into a pipe-delimited string.

Expand source code Browse git
def decode_file_attributes(flags: int) -> str:
    """
    Decode a file attribute bitmask into a pipe-delimited string.
    """
    parts: list[str] = []
    remaining = flags
    for i, name in enumerate(_WIN_ATTRIBS):
        bit = 1 << i
        if remaining & bit:
            if name is not None:
                parts.append(name)
                remaining &= ~bit
    if remaining:
        parts.append(F'0x{remaining:X}')
    return '|'.join(parts)
def decode_setoverwrite(mode)

Map an overwrite mode index to its name.

Expand source code Browse git
def decode_setoverwrite(mode: int) -> str:
    """
    Map an overwrite mode index to its name.
    """
    if mode < len(_OVERWRITE_MODES):
        return _OVERWRITE_MODES[mode]
    return str(mode)
def decode_shortcut_hotkey(spec)

Decode a CreateShortcut hotkey spec into a human-readable string. The high byte contains modifier flags, the next byte is the virtual key.

Expand source code Browse git
def decode_shortcut_hotkey(spec: int) -> str:
    """
    Decode a CreateShortcut hotkey spec into a human-readable string. The high byte contains
    modifier flags, the next byte is the virtual key.
    """
    mod_key = (spec >> 24) & 0xFF
    key = (spec >> 16) & 0xFF
    if mod_key == 0 and key == 0:
        return ''
    parts: list[str] = []
    if mod_key & 1:
        parts.append('SHIFT')
    if mod_key & 2:
        parts.append('CONTROL')
    if mod_key & 4:
        parts.append('ALT')
    if mod_key & 8:
        parts.append('EXT')
    if VK_F1 <= key <= VK_F1 + 23:
        parts.append(F'F{key - VK_F1 + 1}')
    elif (0x41 <= key <= 0x5A) or (0x30 <= key <= 0x39):
        parts.append(chr(key))
    else:
        parts.append(F'Char_{key}')
    return '|'.join(parts)
def nsis_escape(s)

Apply NSIS escape sequences to a string for decompiler output.

Expand source code Browse git
def nsis_escape(s: str) -> str:
    """
    Apply NSIS escape sequences to a string for decompiler output.
    """
    out: list[str] = []
    for c in s:
        if c == '\t':
            out.append('$\\t')
        elif c == '\n':
            out.append('$\\n')
        elif c == '\r':
            out.append('$\\r')
        elif c == '"':
            out.append('$\\"')
        else:
            out.append(c)
    return ''.join(out)
def format_section_begin(writer, section, index, read_string, is_installer)

Write Section/SectionGroup/SectionGroupEnd header. Returns True if the section is a group marker (no body to close with SectionEnd).

Expand source code Browse git
def format_section_begin(
    writer: NSScriptWriter,
    section: NSSection,
    index: int,
    read_string: Callable[[int], str | None],
    is_installer: bool,
) -> bool:
    """
    Write Section/SectionGroup/SectionGroupEnd header. Returns True if the section is a group
    marker (no body to close with SectionEnd).
    """
    flags = NSSectionFlags(section.flags)
    name = read_string(section.name)
    if name is None:
        name = ''
    if not is_installer and name.lower() != 'uninstall':
        name = F'un.{name}'
    if flags & NSSectionFlags.BOLD:
        name = F'!{name}'

    if flags & NSSectionFlags.SECGRPEND:
        writer.add_string_lf('SectionGroupEnd')
        return True

    if flags & NSSectionFlags.SECGRP:
        writer.write('SectionGroup')
        if flags & NSSectionFlags.EXPAND:
            writer.write(' /e')
        writer.space_quoted(name)
        writer.small_space_comment()
        writer.write(F'Section_{index}')
        writer.newline()
        return True

    writer.write('Section')
    if not (flags & NSSectionFlags.SELECTED):
        writer.write(' /o')
    if name:
        writer.space_quoted(name)
    writer.small_space_comment()
    writer.write(F'Section_{index}')
    writer.newline()

    if section.size_kb != 0:
        writer.tab()
        writer.comment(F'AddSize {section.size_kb}')
        writer.newline()

    need_section_in = (
        (section.name != 0 and section.install_types != 0)
        or (section.name == 0 and section.install_types != 0xFFFFFFFF)
    )
    if need_section_in or (flags & NSSectionFlags.RO):
        writer.tab_string('SectionIn')
        inst_types = section.install_types
        for i in range(32):
            if inst_types & (1 << i):
                writer.write(F' {i + 1}')
        if flags & NSSectionFlags.RO:
            writer.write(' RO')
        writer.newline()

    return False
def format_section_end(writer)

Write SectionEnd.

Expand source code Browse git
def format_section_end(writer: NSScriptWriter):
    """
    Write SectionEnd.
    """
    writer.add_string_lf('SectionEnd')
    writer.newline()
def emit_pages(writer, header, labels, is_installer)

Emit Page/UninstPage/PageEx blocks from the bh_pages data. Updates the labels array with CMD_REF_Pre/Show/Leave for page callbacks.

Expand source code Browse git
def emit_pages(
    writer: NSScriptWriter,
    header: NSHeader,
    labels: list[int],
    is_installer: bool,
) -> None:
    """
    Emit Page/UninstPage/PageEx blocks from the bh_pages data. Updates the labels array with
    CMD_REF_Pre/Show/Leave for page callbacks.
    """
    bh = header.bh_pages
    if bh.count == 0:
        return

    writer.separator()
    writer.comment(F'PAGES: {bh.count}')
    writer.newline()

    raw = bytes(header.raw_data)
    num_instructions = len(labels)

    if (
        bh.count > (1 << 12)
        or bh.offset > len(raw)
        or bh.count * PAGE_SIZE > len(raw) - bh.offset
    ):
        writer.big_space_comment()
        writer.write('!!! ERROR: Pages error')
        writer.newline()
        return

    writer.newline()

    for page_index in range(bh.count):
        base = bh.offset + page_index * PAGE_SIZE
        p = raw[base:base + PAGE_SIZE]
        if len(p) < PAGE_SIZE:
            break

        dlg_id = struct.unpack_from('<I', p, 0)[0]
        wnd_proc_id = struct.unpack_from('<I', p, 4)[0]
        pre_func = struct.unpack_from('<I', p, 8)[0]
        show_func = struct.unpack_from('<I', p, 12)[0]
        leave_func = struct.unpack_from('<I', p, 16)[0]
        flags = struct.unpack_from('<I', p, 20)[0]
        caption = struct.unpack_from('<I', p, 24)[0]
        next_param = struct.unpack_from('<I', p, 32)[0]
        params = [struct.unpack_from('<I', p, 44 + 4 * i)[0] for i in range(5)]

        def _set_func_ref(func_index: int, flag: int):
            signed = func_index if func_index < 0x80000000 else func_index - 0x100000000
            if signed >= 0 and func_index < num_instructions:
                labels[func_index] = (
                    (labels[func_index] & ~CMD_REF_Page_Mask)
                    | flag
                    | (page_index << CMD_REF_Page_NumShifts)
                )

        _set_func_ref(pre_func, CMD_REF_Pre)
        _set_func_ref(show_func, CMD_REF_Show)
        _set_func_ref(leave_func, CMD_REF_Leave)

        if wnd_proc_id == PAGE_COMPLETED:
            writer.comment_open()

        writer.comment(F'Page {page_index}')
        writer.newline()

        if flags & PF_PAGE_EX:
            writer.write('PageEx ')
            if not is_installer:
                writer.write('un.')
        else:
            writer.write('Page ' if is_installer else 'UninstPage ')

        if wnd_proc_id < len(PAGE_TYPES):
            writer.write(PAGE_TYPES[wnd_proc_id])
        else:
            writer.add_uint(wnd_proc_id)

        pre_signed = pre_func if pre_func < 0x80000000 else pre_func - 0x100000000
        show_signed = show_func if show_func < 0x80000000 else show_func - 0x100000000
        leave_signed = leave_func if leave_func < 0x80000000 else leave_func - 0x100000000
        need_callbacks = pre_signed >= 0 or show_signed >= 0 or leave_signed >= 0

        if flags & PF_PAGE_EX:
            writer.newline()
            if need_callbacks:
                writer.tab_string('PageCallbacks')

        if need_callbacks:
            _add_param_func(writer, labels, pre_func)
            if wnd_proc_id != PAGE_CUSTOM:
                _add_param_func(writer, labels, show_func)
            _add_param_func(writer, labels, leave_func)

        if not (flags & PF_PAGE_EX):
            if flags & PF_CANCEL_ENABLE:
                writer.write(' /ENABLECANCEL')
            writer.newline()
        else:
            writer.newline()
            if caption != 0:
                writer.tab_string('Caption')
                writer.space_quoted(header._read_string(caption) or '')
                writer.newline()

        if wnd_proc_id == PAGE_LICENSE:
            _emit_license_page(writer, header, flags, dlg_id, params, next_param)
        elif wnd_proc_id == PAGE_SELCOM:
            _emit_page_option(writer, header, params, 3, 'ComponentsText')
        elif wnd_proc_id == PAGE_DIR:
            _emit_page_option(writer, header, params, 4, 'DirText')
            if params[4] != 0:
                writer.tab_string('DirVar')
                writer.space()
                writer.write(header._string_code_variable(params[4] - 1))
                writer.newline()
            if flags & PF_DIR_NO_BTN_DISABLE:
                writer.tab_string('DirVerify leave')
                writer.newline()
        elif wnd_proc_id == PAGE_INSTFILES:
            if params[2] != 0:
                writer.tab_string('CompletedText')
                writer.space_quoted(header._read_string(params[2]) or '')
                writer.newline()
            if params[1] != 0:
                writer.tab_string('DetailsButtonText')
                writer.space_quoted(header._read_string(params[1]) or '')
                writer.newline()
        elif wnd_proc_id == PAGE_UNINST:
            if params[4] != 0:
                writer.tab_string('DirVar')
                writer.space()
                writer.write(header._string_code_variable(params[4] - 1))
                writer.newline()
            _emit_page_option(writer, header, params, 2, 'UninstallText')

        if flags & PF_PAGE_EX:
            writer.write('PageExEnd')
            writer.newline()
        if wnd_proc_id == PAGE_COMPLETED:
            writer.comment_close()
        writer.newline()
def emit_header_info(writer, header, archive, is_installer)

Emit header metadata: NSIS type, compressor, flags, language tables, InstType list, InstallDir, and post-header strings.

Expand source code Browse git
def emit_header_info(
    writer: NSScriptWriter,
    header: NSHeader,
    archive: NSArchive | None,
    is_installer: bool,
) -> None:
    """
    Emit header metadata: NSIS type, compressor, flags, language tables, InstType list,
    InstallDir, and post-header strings.
    """
    writer.comment('NSIS script')
    if header.unicode:
        writer.write(' (UTF-8)')
    writer.space()
    writer.write(_format_description(header))
    writer.newline()

    writer.comment('Install' if is_installer else 'Uninstall')
    writer.newline()
    writer.newline()

    if header.unicode:
        writer.add_string_lf('Unicode true')

    if archive is not None and archive.method is not NSMethod.Copy:
        writer.write('SetCompressor')
        if archive.solid:
            writer.write(' /SOLID')
        method_name = {
            NSMethod.Deflate: 'zlib',
            NSMethod.NSGzip: 'zlib',
            NSMethod.BZip2: 'bzip2',
            NSMethod.LZMA: 'lzma',
        }.get(archive.method)
        if method_name:
            writer.space()
            writer.write(method_name)
        writer.newline()

    if archive is not None and archive.method is NSMethod.LZMA and archive.lzma_options is not None:
        writer.write('SetCompressorDictSize ')
        writer.add_uint(archive.lzma_options.dictionary_size >> 20)
        writer.newline()

    writer.separator()

    writer.comment(F'HEADER SIZE: {header.raw_size}')
    writer.newline()

    if header.bh_pages.offset != 0:
        writer.comment(F'START HEADER SIZE: {header.bh_pages.offset}')
        writer.newline()

    bh_sections = header.bh_sections
    bh_entries = header.bh_entries
    if bh_sections.count > 0 and bh_entries.offset > bh_sections.offset:
        section_size = (bh_entries.offset - bh_sections.offset) // bh_sections.count
        if section_size >= 24:
            max_string_len = section_size - 24
            if header.unicode:
                max_string_len >>= 1
            if max_string_len == 0:
                writer.comment(F'SECTION SIZE: {section_size}')
            else:
                writer.comment(F'MAX STRING LENGTH: {max_string_len}')
            writer.newline()

    num_string_chars = header.bh_langtbl.offset - header.bh_strings.offset
    if header.unicode:
        num_string_chars >>= 1
    writer.comment(F'STRING CHARS: {num_string_chars}')
    writer.newline()

    if header.bh_ctlcolors.offset > header.raw_size:
        writer.big_space_comment()
        writer.add_string_lf('Bad COLORS TABLE')

    if header.bh_ctlcolors.count != 0:
        writer.comment(F'COLORS Num: {header.bh_ctlcolors.count}')
        writer.newline()

    if header.bh_font.count != 0:
        writer.comment(F'FONTS Num: {header.bh_font.count}')
        writer.newline()

    if header.bh_data.count != 0:
        writer.comment(F'DATA NUM: {header.bh_data.count}')
        writer.newline()

    writer.newline()
    writer.add_string_lf('OutFile [NSIS].exe')
    writer.add_string_lf('!include WinMessages.nsh')
    writer.newline()

    raw = bytes(header.raw_data)
    if len(raw) < 4:
        return

    eh_flags = NSScriptFlags(struct.unpack_from('<I', raw, 0)[0])
    show_details = int(eh_flags) & 3
    if 1 <= show_details <= 2:
        writer.write('ShowInstDetails' if is_installer else 'ShowUninstDetails')
        writer.add_string_lf(' show' if show_details == 1 else ' nevershow')

    if eh_flags & NSScriptFlags.PROGRESS_COLORED:
        writer.add_string_lf('InstProgressFlags colored')
    if eh_flags & (NSScriptFlags.SILENT | NSScriptFlags.SILENT_LOG):
        writer.write('SilentInstall ' if is_installer else 'SilentUnInstall ')
        writer.add_string_lf('silentlog' if (eh_flags & NSScriptFlags.SILENT_LOG) else 'silent')
    if eh_flags & NSScriptFlags.AUTO_CLOSE:
        writer.add_string_lf('AutoCloseWindow true')
    if not (eh_flags & NSScriptFlags.NO_ROOT_DIR):
        writer.add_string_lf('AllowRootDirInstall true')
    if eh_flags & NSScriptFlags.NO_CUSTOM:
        writer.add_string_lf('InstType /NOCUSTOM')
    if eh_flags & NSScriptFlags.COMP_ONLY_ON_CUSTOM:
        writer.add_string_lf('InstType /COMPONENTSONLYONCUSTOM')

    bho_size = 12 if header.is64bit else 8
    params_offset = 4 + bho_size * 8
    if header.bh_pages.offset == 276:
        params_offset -= bho_size

    if params_offset + 40 > len(raw):
        return

    def _get32(off: int) -> int:
        return struct.unpack_from('<I', raw, off)[0]

    p2 = params_offset

    root_key = _get32(p2)
    sub_key = _get32(p2 + 4)
    value = _get32(p2 + 8)
    if (root_key != 0 and root_key != 0xFFFFFFFF) or sub_key != 0 or value != 0:
        writer.write('InstallDirRegKey')
        writer.space()
        writer.write(decode_reg_root(root_key))
        s = header._read_string(sub_key)
        if s:
            writer.space_quoted(s)
        s = header._read_string(value)
        if s:
            writer.space_quoted(s)
        writer.newline()

    bg_color1 = _get32(p2 + 12)
    bg_color2 = _get32(p2 + 16)
    bg_textcolor = _get32(p2 + 20)
    if bg_color1 != 0xFFFFFFFF or bg_color2 != 0xFFFFFFFF or bg_textcolor != 0xFFFFFFFF:
        writer.write('BGGradient')
        if bg_color1 != 0 or bg_color2 != 0xFF0000 or bg_textcolor != 0xFFFFFFFF:
            writer.space()
            writer.add_color(bg_color1)
            writer.space()
            writer.add_color(bg_color2)
            if bg_textcolor != 0xFFFFFFFF:
                writer.space()
                writer.add_color(bg_textcolor)
        writer.newline()

    lb_bg = _get32(p2 + 24)
    lb_fg = _get32(p2 + 28)
    if (lb_bg != 0xFFFFFFFF or lb_fg != 0xFFFFFFFF) and (lb_bg != 0 or lb_fg != 0xFF00):
        writer.write('InstallColors')
        writer.space()
        writer.add_color(lb_fg)
        writer.space()
        writer.add_color(lb_bg)
        writer.newline()

    license_bg = _get32(p2 + 36)
    signed_license_bg = license_bg if license_bg < 0x80000000 else license_bg - 0x100000000
    if license_bg != 0xFFFFFFFF and signed_license_bg != -15:
        writer.write('LicenseBkColor')
        if signed_license_bg == -5:
            writer.write(' /windows')
        else:
            writer.space()
            writer.add_color(license_bg)
        writer.newline()

    langtable_size = _get32(p2 + 32)
    bh_langtbl = header.bh_langtbl
    if bh_langtbl.count > 0 and langtable_size != 0xFFFFFFFF:
        if langtable_size >= 10:
            num_strings = (langtable_size - 10) // 4
        else:
            num_strings = 0

        writer.newline()
        writer.separator()
        writer.comment(F'LANG TABLES: {bh_langtbl.count}')
        writer.newline()
        writer.comment(F'LANG STRINGS: {num_strings}')
        writer.newline()
        writer.newline()

        license_lang_index = -1
        bh_pages = header.bh_pages
        if bh_pages.count > 0:
            for page_i in range(bh_pages.count):
                page_base = bh_pages.offset + page_i * PAGE_SIZE
                if page_base + PAGE_SIZE > len(raw):
                    break
                wnd_proc_id = struct.unpack_from('<I', raw, page_base + 4)[0]
                param1 = struct.unpack_from('<I', raw, page_base + 44 + 4)[0]
                if wnd_proc_id != PAGE_LICENSE or param1 == 0:
                    continue
                signed_param1 = param1 if param1 < 0x80000000 else param1 - 0x100000000
                if signed_param1 < 0:
                    license_lang_index = -(signed_param1 + 1)

        if license_lang_index >= 0:
            for i in range(bh_langtbl.count):
                lang_base = bh_langtbl.offset + langtable_size * i
                if lang_base + 10 + (license_lang_index + 1) * 4 > len(raw):
                    break
                lang_id = struct.unpack_from('<H', raw, lang_base)[0]
                val = struct.unpack_from('<I', raw, lang_base + 10 + license_lang_index * 4)[0]
                if val != 0:
                    writer.write(F'LicenseLangString LSTR_{license_lang_index}')
                    writer.write(F' {lang_id}')
                    s = header._read_string(val)
                    if s:
                        writer.space_quoted(nsis_escape(s))
                    writer.newline()
            writer.newline()

        branding_text = 0
        name_val = 0
        for i in range(bh_langtbl.count):
            lang_base = bh_langtbl.offset + langtable_size * i
            if lang_base + 10 + 3 * 4 > len(raw):
                break
            lang_id = struct.unpack_from('<H', raw, lang_base)[0]
            v0 = struct.unpack_from('<I', raw, lang_base + 10 + 0 * 4)[0]
            v2 = struct.unpack_from('<I', raw, lang_base + 10 + 2 * 4)[0]
            if v0 != 0 and (lang_id == 1033 or branding_text == 0):
                branding_text = v0
            if v2 != 0 and (lang_id == 1033 or name_val == 0):
                name_val = v2

        if name_val != 0:
            writer.write('Name')
            s = header._read_string(name_val)
            if s:
                writer.space_quoted(s)
            writer.newline()

        if branding_text != 0:
            writer.write('BrandingText')
            s = header._read_string(branding_text)
            if s:
                writer.space_quoted(s)
            writer.newline()

        for i in range(bh_langtbl.count):
            lang_base = bh_langtbl.offset + langtable_size * i
            if lang_base + 10 > len(raw):
                break
            lang_id = struct.unpack_from('<H', raw, lang_base)[0]
            writer.newline()
            writer.comment(F'LANG: {lang_id}')
            writer.newline()

            for j in range(num_strings):
                str_off = lang_base + 10 + j * 4
                if str_off + 4 > len(raw):
                    break
                val = struct.unpack_from('<I', raw, str_off)[0]
                if val != 0 and j != license_lang_index:
                    writer.write(F'LangString LSTR_{j} {lang_id}')
                    s = header._read_string(val)
                    if s:
                        writer.space_quoted(nsis_escape(s))
                    writer.newline()
            writer.newline()

    on_func_count = len(ON_FUNCS)
    if header.bh_pages.offset == 276:
        on_func_count -= 1

    p2_after = params_offset + 40 + on_func_count * 4
    writer.newline()

    for i in range(NSIS_MAX_INST_TYPES + 1):
        off = p2_after + i * 4
        if off + 4 > len(raw):
            break
        inst_type = _get32(off)
        if inst_type != 0:
            writer.write('InstType')
            s = ''
            if not is_installer:
                s = 'un.'
            resolved = header._read_string(inst_type)
            if resolved:
                s += resolved
            writer.space_quoted(s)
            writer.newline()

    install_dir_off = p2_after + (NSIS_MAX_INST_TYPES + 1) * 4
    if install_dir_off + 4 <= len(raw):
        install_dir = _get32(install_dir_off)
        if install_dir != 0:
            writer.write('InstallDir')
            s = header._read_string(install_dir)
            if s:
                writer.space_quoted(nsis_escape(s))
            writer.newline()

    if header.bh_pages.offset >= 288:
        post_off = install_dir_off + 4
        for i in range(4):
            if i != 0 and header.bh_pages.offset < 300:
                break
            if post_off + 4 > len(raw):
                break
            param = _get32(post_off + 4 * i)
            if param == 0 or param == 0xFFFFFFFF:
                continue
            writer.comment(F'{_POST_STRINGS[i]} =')
            s = header._read_string(param)
            if s:
                writer.space_quoted(nsis_escape(s))
            writer.newline()

    writer.newline()
def render_opcode(writer, header, instruction, opcode, labels, raw, overwrite_state, address, commented)

Render a single NSIS opcode's arguments to the writer. Handles all opcodes except Ret and INVALID_OPCODE, which require structural context handled by the caller.

Expand source code Browse git
def render_opcode(
    writer: NSScriptWriter,
    header: NSHeader,
    instruction: NSScriptInstruction | NSScriptExtendedInstruction,
    opcode: Op,
    labels: list[int],
    raw: bytes,
    overwrite_state: list[int],
    address: int,
    commented: bool,
) -> None:
    """
    Render a single NSIS opcode's arguments to the writer. Handles all opcodes except Ret and
    INVALID_OPCODE, which require structural context handled by the caller.
    """
    params = instruction.arguments

    if opcode is Op.Nop:
        if params[0] == 0:
            writer.write('Nop')
        else:
            writer.write('Goto')
            _add_goto_var(writer, header, params[0])

    elif opcode is Op.Abort:
        writer.write(opcode.name)
        _add_optional_param(writer, header, params[0])

    elif opcode is Op.Quit:
        writer.write(opcode.name)

    elif opcode is Op.Call:
        writer.write('Call ')
        signed_p0 = params[0] if params[0] < 0x80000000 else params[0] - 0x100000000
        if signed_p0 < 0:
            writer.write(header._string_code_variable(-(signed_p0 + 1)))
        elif params[0] == 0:
            writer.write('0')
        else:
            val = params[0] - 1
            if params[1] == 1:
                writer.write(F':label_{val}')
            else:
                writer.write(_func_name(labels, val))

    elif opcode is Op.DetailPrint or opcode is Op.Sleep:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])

    elif opcode is Op.BringToFront:
        writer.write(opcode.name)

    elif opcode is Op.SetDetailsView:
        writer.write(opcode.name)
        if params[0] == SW_SHOWNA and params[1] == SW_HIDE:
            writer.write(' show')
        elif params[1] == SW_SHOWNA and params[0] == SW_HIDE:
            writer.write(' hide')
        else:
            for i in range(2):
                writer.space()
                writer.write(decode_showwindow(params[i]))

    elif opcode is Op.SetFileAttributes:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        writer.space()
        writer.write(decode_file_attributes(params[1]))

    elif opcode is Op.CreateDirectory:
        is_set_out_path = params[1] != 0
        writer.write('SetOutPath' if is_set_out_path else 'CreateDirectory')
        _add_param(writer, header, params[0])
        if params[2] != 0:
            writer.small_space_comment()
            writer.write('CreateRestrictedDirectory')

    elif opcode is Op.IfFileExists:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        _add_goto_vars2(writer, header, params[1], params[2])

    elif opcode is Op.SetFlag:
        s_val = header._read_string(params[1]) or ''
        if params[0] == K_EXEC_FLAGS_ERRORS and params[2] == 0:
            if s_val == '0':
                writer.write('ClearErrors')
            else:
                writer.write('SetErrors')
        else:
            writer.write('Set')
            writer.write(decode_exec_flags(params[0]))
            if params[2] != 0:
                writer.write(' lastused')
            else:
                decoded = _decode_flag_value(params[0], s_val)
                if decoded is not None:
                    writer.space()
                    writer.write(decoded)
                else:
                    writer.space_quoted(nsis_escape(s_val))

    elif opcode is Op.IfFlag:
        writer.write('If')
        writer.write(decode_exec_flags(params[2]))
        _add_goto_vars2(writer, header, params[0], params[1])

    elif opcode is Op.GetFlag:
        writer.write('Get')
        writer.write(decode_exec_flags(params[1]))
        _add_param_var(writer, header, params[0])

    elif opcode is Op.Rename:
        writer.write(opcode.name)
        if params[2] != 0:
            writer.write(' /REBOOTOK')
        _add_params(writer, header, params, 2)
        if params[3] != 0:
            writer.small_space_comment()
            _add_param(writer, header, params[3])

    elif opcode is Op.GetFullPathName:
        writer.write(opcode.name)
        if params[2] == 0:
            writer.write(' /SHORT')
        _add_param_var(writer, header, params[1])
        _add_param(writer, header, params[0])

    elif opcode is Op.SearchPath or opcode is Op.StrLen:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.GetTempFileName:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        s = header._read_string(params[1])
        if s and s != '$TEMP':
            writer.space_quoted(nsis_escape(s))

    elif opcode is Op.ExtractFile:
        overwrite = params[0] & 0x7
        if overwrite != overwrite_state[0]:
            writer.write('SetOverwrite ')
            writer.write(decode_setoverwrite(overwrite))
            overwrite_state[0] = overwrite
            writer.newline()
            writer.tab(commented)
        writer.write('File')
        _add_param(writer, header, params[1])

    elif opcode is Op.Delete:
        writer.write(opcode.name)
        flag = params[1]
        if flag & DEL_REBOOT:
            writer.write(' /REBOOTOK')
        _add_param(writer, header, params[0])

    elif opcode is Op.MessageBox:
        writer.write(opcode.name)
        writer.space()
        writer.write(decode_messagebox(params[0]))
        _add_param(writer, header, params[1])
        button_id = params[0] >> 21
        if button_id != 0:
            writer.write(' /SD')
            writer.write(decode_button_id(button_id))
        for i in range(2, 6, 2):
            if params[i] != 0:
                writer.space()
                writer.write(decode_button_id(params[i]))
                _add_goto_var1(writer, header, params[i + 1])

    elif opcode is Op.RMDir:
        writer.write(opcode.name)
        flag = params[1]
        if flag & DEL_RECURSE:
            writer.write(' /r')
        if flag & DEL_REBOOT:
            writer.write(' /REBOOTOK')
        _add_param(writer, header, params[0])

    elif opcode is Op.AssignVar:
        writer.write('StrCpy')
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])
        _add_optional_params(writer, header, params[2:4], 2)

    elif opcode is Op.StrCmp:
        writer.write('StrCmpS' if params[4] != 0 else 'StrCmp')
        _add_params(writer, header, params, 2)
        _add_goto_vars2(writer, header, params[2], params[3])

    elif opcode is Op.ReadEnvStr:
        writer.write('ReadEnvStr' if params[2] != 0 else 'ExpandEnvStrings')
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.IntCmp:
        writer.write('IntCmpU' if params[5] != 0 else 'IntCmp')
        _add_params(writer, header, params, 2)
        _add_goto_var1(writer, header, params[2])
        if params[3] != 0 or params[4] != 0:
            _add_goto_vars2(writer, header, params[3], params[4])

    elif opcode is Op.IntOp:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        op_index = params[3]
        c = K_INTOP_OPS[op_index] if op_index < 13 else '?'
        c2 = '' if (op_index < 8 or op_index == 10) else c
        num_ops = 1 if op_index == 7 else 2
        _add_param(writer, header, params[1])
        writer.space()
        writer.write(c)
        if num_ops != 1:
            if c2:
                writer.write(c2)
            _add_param(writer, header, params[2])

    elif opcode is Op.IntFmt:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_params(writer, header, params[1:3], 2)

    elif opcode is Op.PushPop:
        if params[2] != 0:
            writer.write('Exch')
            if params[2] != 1:
                writer.write(F' {params[2]}')
        elif params[1] != 0:
            writer.write('Pop')
            _add_param_var(writer, header, params[0])
        else:
            writer.write('Push')
            _add_param(writer, header, params[0])

    elif opcode is Op.FindWindow:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])
        _add_optional_params(writer, header, params[2:5], 3)

    elif opcode is Op.SendMessage:
        writer.write(opcode.name)
        _add_param(writer, header, params[1])
        _add_param(writer, header, params[2])
        spec = params[5]
        for i in range(2):
            prefix = 'STR:' if (spec & (1 << i)) else ''
            s = header._read_string(params[3 + i])
            writer.space_quoted(prefix + nsis_escape(s or ''))
        signed_p0 = params[0] if params[0] < 0x80000000 else params[0] - 0x100000000
        if signed_p0 >= 0:
            _add_param_var(writer, header, params[0])
        timeout = spec >> 2
        if timeout != 0:
            writer.write(F' /TIMEOUT={timeout}')

    elif opcode is Op.IsWindow:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        _add_goto_vars2(writer, header, params[1], params[2])

    elif opcode is Op.GetDlgItem:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_params(writer, header, params[1:3], 2)

    elif opcode is Op.SetCtlColors:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        offset = params[1]
        bh_cc = header.bh_ctlcolors
        if (
            header.raw_size < bh_cc.offset
            or header.raw_size - bh_cc.offset < offset
            or header.raw_size - bh_cc.offset - offset < CTL_COLORS_SIZE
        ):
            writer.write(' ; bad offset')
        else:
            p2 = bh_cc.offset + offset
            cc_text = struct.unpack_from('<I', raw, p2)[0]
            cc_bkc = struct.unpack_from('<I', raw, p2 + 4)[0]
            cc_bkmode = struct.unpack_from('<i', raw, p2 + 16)[0]
            cc_flags = struct.unpack_from('<i', raw, p2 + 20)[0]
            if (cc_flags & COLORS_BK_SYS) or (cc_flags & COLORS_TEXT_SYS):
                writer.write(' /BRANDING')
            bk = ''
            bkc_print = False
            if cc_bkmode == TRANSPARENT:
                bk = ' transparent'
            elif cc_flags & COLORS_BKB:
                if not (cc_flags & COLORS_BK_SYS) and (cc_flags & COLORS_BK):
                    bkc_print = True
            if (cc_flags & COLORS_TEXT) or bk or bkc_print:
                writer.space()
                if (cc_flags & COLORS_TEXT_SYS) or not (cc_flags & COLORS_TEXT):
                    writer.add_quotes()
                else:
                    writer.add_color(cc_text)
            writer.write(bk)
            if bkc_print:
                writer.space()
                writer.add_color(cc_bkc)

    elif opcode is Op.SetBrandingImage:
        writer.write(opcode.name)
        writer.write(F' /IMGID={params[1]}')
        if params[2] == 1:
            writer.write(' /RESIZETOFIT')
        _add_param(writer, header, params[0])

    elif opcode is Op.CreateFont:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])
        _add_optional_params(writer, header, params[2:4], 2)
        if params[4] & 1:
            writer.write(' /ITALIC')
        if params[4] & 2:
            writer.write(' /UNDERLINE')
        if params[4] & 4:
            writer.write(' /STRIKE')

    elif opcode is Op.ShowWindow:
        if params[3] != 0:
            writer.write('EnableWindow')
        else:
            writer.write('ShowWindow')
        _add_param(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.ExecShell:
        writer.write(opcode.name)
        _add_params(writer, header, params, 2)
        if params[2] != 0 or params[3] != SW_SHOWNORMAL:
            _add_param(writer, header, params[2])
            if params[3] != SW_SHOWNORMAL:
                writer.space()
                writer.write(decode_showwindow(params[3]))
        if params[5] != 0:
            writer.write('    ;')
            _add_param(writer, header, params[5])

    elif opcode is Op.Exec:
        writer.write('ExecWait' if params[2] != 0 else 'Exec')
        _add_param(writer, header, params[0])
        signed_p1 = params[1] if params[1] < 0x80000000 else params[1] - 0x100000000
        if params[2] != 0 and signed_p1 >= 0:
            _add_param_var(writer, header, params[1])

    elif opcode is Op.GetFileTime or opcode is Op.GetDLLVersion:
        writer.write(opcode.name)
        _add_param(writer, header, params[2])
        _add_param_var(writer, header, params[0])
        _add_param_var(writer, header, params[1])

    elif opcode is Op.RegisterDll:
        func_str = header._read_string(params[1]) or ''
        print_func = True
        if params[2] == 0:
            writer.write('CallInstDLL')
            _add_param(writer, header, params[0])
            if params[3] == 1:
                writer.write(' /NOUNLOAD')
        else:
            if func_str == 'DllUnregisterServer':
                writer.write('UnRegDLL')
                print_func = False
            else:
                writer.write('RegDLL')
                if func_str == 'DllRegisterServer':
                    print_func = False
            _add_param(writer, header, params[0])
        if print_func:
            writer.space_quoted(nsis_escape(func_str))

    elif opcode is Op.CreateShortCut:
        writer.write(opcode.name)
        num_params = 6
        while num_params > 2 and params[num_params - 1] == 0:
            num_params -= 1
        spec = params[4]
        if spec & 0x8000:
            writer.write(' /NoWorkingDir')
        _add_params(writer, header, params, min(num_params, 4))
        if num_params <= 4:
            pass
        else:
            icon = spec & 0xFF
            writer.space()
            if icon != 0:
                writer.add_uint(icon)
            else:
                writer.add_quotes()
            sw = (spec >> 8) & 0x7F
            if (spec >> 8) == 0 and num_params < 6:
                pass
            else:
                if sw == SW_SHOWMINNOACTIVE:
                    sw = SW_SHOWMINIMIZED
                writer.space()
                if sw == 0:
                    writer.add_quotes()
                else:
                    writer.write(decode_showwindow(sw))
                hotkey = decode_shortcut_hotkey(spec)
                if hotkey:
                    writer.space()
                    writer.write(hotkey)
                elif num_params >= 6:
                    writer.space()
                    writer.add_quotes()
                _add_optional_param(writer, header, params[5])

    elif opcode is Op.CopyFiles:
        writer.write(opcode.name)
        if params[2] & 0x04:
            writer.write(' /SILENT')
        if params[2] & 0x80:
            writer.write(' /FILESONLY')
        _add_params(writer, header, params, 2)
        if params[3] != 0:
            writer.write('    ;')
            _add_param(writer, header, params[3])

    elif opcode is Op.Reboot:
        writer.write(opcode.name)
        if params[0] != 0xBADF00D:
            writer.write(' ; Corrupted ???')

    elif opcode is Op.WriteINI:
        num_always = 0
        if params[0] == 0:
            writer.write('FlushINI')
        elif params[4] != 0:
            writer.write('WriteINIStr')
            num_always = 3
        else:
            writer.write('DeleteINI')
            writer.write('Sec' if params[1] == 0 else 'Str')
            num_always = 1
        _add_param(writer, header, params[3])
        _add_params(writer, header, params, num_always)
        _add_optional_params(writer, header, params[num_always:3], 3 - num_always)

    elif opcode is Op.ReadINIStr:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[3])
        _add_params(writer, header, params[1:3], 2)

    elif opcode is Op.DeleteReg:
        writer.write(opcode.name)
        if params[4] == 0:
            writer.write('Value')
        else:
            writer.write('Key')
            if params[4] & 2:
                writer.write(' /ifempty')
        writer.space()
        writer.write(decode_reg_root(params[1]))
        _add_param(writer, header, params[2])
        _add_optional_param(writer, header, params[3])

    elif opcode is Op.WriteReg:
        writer.write(opcode.name)
        reg_type_name = {1: 'Str', 2: 'ExpandStr', 3: 'Bin', 4: 'DWORD'}.get(params[4])
        if params[4] == 1 and params[5] == 2:
            reg_type_name = 'ExpandStr'
        if reg_type_name:
            writer.write(reg_type_name)
        else:
            writer.write(F'?{params[4]}')
        writer.space()
        writer.write(decode_reg_root(params[0]))
        _add_params(writer, header, params[1:3], 2)
        if params[4] != 3:
            _add_param(writer, header, params[3])
        else:
            writer.write(F' data[{params[3]} ... ]')

    elif opcode is Op.ReadReg:
        writer.write(opcode.name)
        writer.write('DWORD' if params[4] == 1 else 'Str')
        _add_param_var(writer, header, params[0])
        writer.space()
        writer.write(decode_reg_root(params[1]))
        _add_params(writer, header, params[2:4], 2)

    elif opcode is Op.EnumReg:
        writer.write(opcode.name)
        writer.write('Key' if params[4] != 0 else 'Value')
        _add_param_var(writer, header, params[0])
        writer.space()
        writer.write(decode_reg_root(params[1]))
        _add_params(writer, header, params[2:4], 2)

    elif opcode is Op.FileClose or opcode is Op.FindClose:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])

    elif opcode is Op.FileOpen:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[3])
        acc = params[1]
        creat = params[2]
        if acc != 0 or creat != 0:
            cc = ''
            if acc == GENERIC_READ and creat == OPEN_EXISTING:
                cc = 'r'
            elif creat == CREATE_ALWAYS and acc == GENERIC_WRITE:
                cc = 'w'
            elif creat == OPEN_ALWAYS and acc == (GENERIC_WRITE | GENERIC_READ):
                cc = 'a'
            if cc:
                writer.write(F' {cc}')
            else:
                if acc & GENERIC_READ:
                    writer.write(' GENERIC_READ')
                if acc & GENERIC_WRITE:
                    writer.write(' GENERIC_WRITE')
                if acc & GENERIC_EXECUTE:
                    writer.write(' GENERIC_EXECUTE')
                if acc & GENERIC_ALL:
                    writer.write(' GENERIC_ALL')
                creat_name = {
                    CREATE_NEW: 'CREATE_NEW',
                    CREATE_ALWAYS: 'CREATE_ALWAYS',
                    OPEN_EXISTING: 'OPEN_EXISTING',
                    OPEN_ALWAYS: 'OPEN_ALWAYS',
                    TRUNCATE_EXISTING: 'TRUNCATE_EXISTING',
                }.get(creat)
                writer.space()
                writer.write(creat_name or str(creat))

    elif opcode is Op.FileWrite or opcode is Op.FileWriteW:
        writer.write('FileWrite')
        if opcode is Op.FileWriteW:
            writer.write('UTF16LE' if params[2] == 0 else 'Word')
        elif params[2] != 0:
            writer.write('Byte')
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.FileRead or opcode is Op.FileReadW:
        writer.write('FileRead')
        if opcode is Op.FileReadW:
            writer.write('UTF16LE' if params[3] == 0 else 'Word')
        elif params[3] != 0:
            writer.write('Byte')
        _add_param_var(writer, header, params[0])
        _add_param_var(writer, header, params[1])
        _add_optional_param(writer, header, params[2])

    elif opcode is Op.FileSeek:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[2])
        if params[3] == 1:
            writer.write(' CUR')
        if params[3] == 2:
            writer.write(' END')
        signed_p1 = params[1] if params[1] < 0x80000000 else params[1] - 0x100000000
        if signed_p1 >= 0:
            if params[3] == 0:
                writer.write(' SET')
            _add_param_var(writer, header, params[1])

    elif opcode is Op.FindNext:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[1])
        _add_param_var(writer, header, params[0])

    elif opcode is Op.FindFirst:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[1])
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[2])

    elif opcode is Op.WriteUninstaller:
        writer.write(opcode.name)
        _add_param(writer, header, params[0])
        if params[3] != 0:
            writer.small_space_comment()
            _add_param(writer, header, params[3])

    elif opcode is Op.SectionSet:
        writer.write('Section')
        signed_p2 = params[2] if params[2] < 0x80000000 else params[2] - 0x100000000
        if signed_p2 >= 0:
            writer.write('Get')
            writer.write(decode_sect_op(params[2]))
            _add_param(writer, header, params[0])
            _add_param_var(writer, header, params[1])
        else:
            writer.write('Set')
            t = -(signed_p2) - 1
            writer.write(decode_sect_op(t))
            _add_param(writer, header, params[0])
            _add_param(writer, header, params[4] if t == 0 else params[1])

    elif opcode is Op.InstTypeSet:
        if params[3] == 0:
            if params[2] == 0:
                writer.write('InstTypeGetText')
                _add_param(writer, header, params[0])
                _add_param_var(writer, header, params[1])
            else:
                writer.write('InstTypeSetText')
                _add_params(writer, header, params, 2)
        else:
            if params[2] == 0:
                writer.write('GetCurInstType')
                _add_param_var(writer, header, params[1])
            else:
                writer.write('SetCurInstType')
                _add_param(writer, header, params[0])

    elif opcode is Op.LockWindow:
        writer.write(opcode.name)
        writer.write(' on' if params[0] == 0 else ' off')

    elif opcode is Op.Log:
        if params[0] != 0:
            writer.write('LogSet ')
            writer.write('off' if params[1] == 0 else 'on')
        else:
            writer.write('LogText')
            _add_param(writer, header, params[1])

    elif opcode is Op.FindProc:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.GetFontVersion or opcode is Op.GetFontName:
        writer.write(opcode.name)
        _add_param_var(writer, header, params[0])
        _add_param(writer, header, params[1])

    elif opcode is Op.GetOSInfo:
        writer.write(opcode.name)

    elif opcode is Op.ReservedOpcode:
        writer.write('GetFunctionAddress')

    else:
        writer.write(F'Command{int(opcode)}')
def emit_commands(writer, header, label_info, sections, is_installer)

Emit the main instruction body: per-opcode decompilation switch, section open/close, function open/close, and label markers. Function bodies are rendered using structural analysis to recover If/Else constructs with indentation.

Expand source code Browse git
def emit_commands(
    writer: NSScriptWriter,
    header: NSHeader,
    label_info: LabelInfo,
    sections: list[NSSection],
    is_installer: bool,
) -> None:
    """
    Emit the main instruction body: per-opcode decompilation switch, section open/close, function
    open/close, and label markers. Function bodies are rendered using structural analysis to
    recover If/Else constructs with indentation.
    """
    from refinery.lib.nsis.controlflow import (
        build_cfg,
        eliminate_dead_code,
        linearize,
        reduce_if_else,
        reduce_loops,
        render_node,
    )

    labels = label_info.labels
    num_instructions = len(header.instructions)
    num_sections = len(sections)

    overwrite_state = [0]
    cur_section_index = 0
    section_is_open = False
    on_func_is_open = False
    end_comment_index = 0

    raw = bytes(header.raw_data)

    func_ranges: dict[int, int] = {}
    k = 0
    while k < num_instructions:
        flg = labels[k]
        if not is_func(flg):
            k += 1
            continue
        func_start = k
        k += 1
        while k < num_instructions:
            insn = header.instructions[k]
            opcode = header.opcode(insn)
            if opcode is Op.Ret:
                if k + 1 >= num_instructions or is_probably_end_of_func(labels[k + 1]):
                    func_ranges[func_start] = k + 1
                    k += 1
                    break
            k += 1
        else:
            func_ranges[func_start] = k

    writer.separator()

    kkk = 0
    while kkk < num_instructions:
        instruction = header.instructions[kkk]
        opcode = header.opcode(instruction)

        is_section_group = False
        while cur_section_index < num_sections:
            sect = sections[cur_section_index]
            if section_is_open:
                if sect.start_cmd_index + sect.num_commands + 1 != kkk:
                    break
                format_section_end(writer)
                section_is_open = False
                cur_section_index += 1
                continue
            if sect.start_cmd_index != kkk:
                break
            if format_section_begin(writer, sect, cur_section_index, header._read_string, is_installer):
                is_section_group = True
                cur_section_index += 1
            else:
                section_is_open = True

        if labels[kkk] != 0 and labels[kkk] != CMD_REF_Section:
            flg = labels[kkk]
            if is_func(flg):
                if kkk == label_info.init_plugins_dir_start:
                    writer.comment_open()
                on_func_is_open = True
                writer.write('Function ')
                writer.write(_func_name(labels, kkk))
                if is_page_func(flg):
                    writer.big_space_comment()
                    writer.write('Page ')
                    writer.add_uint((flg & CMD_REF_Page_Mask) >> CMD_REF_Page_NumShifts)
                    if flg & CMD_REF_Leave:
                        writer.write(', Leave')
                    if flg & CMD_REF_Pre:
                        writer.write(', Pre')
                    if flg & CMD_REF_Show:
                        writer.write(', Show')
                writer.newline()

                if kkk in func_ranges:
                    func_end = func_ranges[kkk]
                    body_end = func_end - 1
                    cfg = build_cfg(header, kkk, body_end)
                    root = linearize(header, cfg, labels, kkk, body_end)
                    reduced = reduce_loops(root, header)
                    reduced = reduce_if_else(reduced, header)
                    reduced = eliminate_dead_code(reduced)
                    render_node(
                        reduced, writer, header, labels, raw,
                        overwrite_state, end_comment_index,
                    )
                    if labels[body_end] & CMD_REF_Goto:
                        writer.write(F'label_{body_end}:')
                        writer.newline()
                    writer.add_string_lf('FunctionEnd')
                    if func_end == label_info.init_plugins_dir_end:
                        writer.comment_close()
                    writer.newline()
                    on_func_is_open = False
                    kkk = func_end
                    continue

            if flg & CMD_REF_Goto:
                writer.write(F'label_{kkk}:')
                writer.newline()

        commented = kkk < end_comment_index

        if opcode is Op.INVALID_OPCODE:
            writer.tab(commented)
            writer.write(F'Command{instruction.opcode}')
            writer.newline()

        elif opcode is Op.Ret:
            if on_func_is_open:
                if kkk == num_instructions - 1 or is_probably_end_of_func(labels[kkk + 1]):
                    writer.add_string_lf('FunctionEnd')
                    if kkk + 1 == label_info.init_plugins_dir_end:
                        writer.comment_close()
                    writer.newline()
                    on_func_is_open = False
                    kkk += 1
                    continue
            if is_section_group:
                kkk += 1
                continue
            if section_is_open:
                sect = sections[cur_section_index]
                if sect.start_cmd_index + sect.num_commands == kkk:
                    format_section_end(writer)
                    section_is_open = False
                    cur_section_index += 1
                    kkk += 1
                    continue
            writer.tab_string('Return')
            writer.newline()

        else:
            writer.tab(commented)
            render_opcode(writer, header, instruction, opcode, labels, raw, overwrite_state, kkk, commented)
            writer.newline()

        kkk += 1

    if section_is_open and cur_section_index < num_sections:
        sect = sections[cur_section_index]
        if sect.start_cmd_index + sect.num_commands + 1 == num_instructions:
            format_section_end(writer)
            cur_section_index += 1

    while cur_section_index < num_sections:
        sect = sections[cur_section_index]
        if section_is_open:
            if sect.start_cmd_index + sect.num_commands != num_instructions:
                break
            format_section_end(writer)
            section_is_open = False
            cur_section_index += 1
            continue
        if sect.start_cmd_index != num_instructions:
            break
        format_section_begin(writer, sect, cur_section_index, header._read_string, is_installer)
        cur_section_index += 1
def is_func(flag)

Check whether a label flag indicates a function start.

Expand source code Browse git
def is_func(flag: int) -> bool:
    """
    Check whether a label flag indicates a function start.
    """
    return bool(flag & (CMD_REF_Call | CMD_REF_Pre | CMD_REF_Show | CMD_REF_Leave | CMD_REF_OnFunc))
def is_page_func(flag)

Check whether a label flag indicates a page callback function.

Expand source code Browse git
def is_page_func(flag: int) -> bool:
    """
    Check whether a label flag indicates a page callback function.
    """
    return bool(flag & (CMD_REF_Pre | CMD_REF_Show | CMD_REF_Leave))
def is_probably_end_of_func(flag)

Check whether a label flag likely ends a function body.

Expand source code Browse git
def is_probably_end_of_func(flag: int) -> bool:
    """
    Check whether a label flag likely ends a function body.
    """
    return flag != 0 and flag != CMD_REF_Goto
def build_labels(header, sections)

Build the label array from an NSHeader. Each entry is a bitmask indicating whether the instruction at that index is a jump target, call target, function start, section start, onFunc entry, or InitPluginsDir.

Expand source code Browse git
def build_labels(header: NSHeader, sections: list[NSSection]) -> LabelInfo:
    """
    Build the label array from an NSHeader. Each entry is a bitmask indicating whether the
    instruction at that index is a jump target, call target, function start, section start,
    onFunc entry, or InitPluginsDir.
    """
    num_instructions = len(header.instructions)
    labels = [0] * num_instructions
    raw = bytes(header.raw_data)

    bho_size = 12 if header.is64bit else 8
    params_offset = 4 + bho_size * 8
    if header.bh_pages.offset == 276:
        params_offset -= bho_size
    on_func_offset = params_offset + 40
    num_on_func = len(ON_FUNCS)
    if header.bh_pages.offset == 276:
        num_on_func -= 1

    for i in range(num_on_func):
        off = on_func_offset + 4 * i
        if off + 4 <= len(raw):
            func = struct.unpack_from('<I', raw, off)[0]
            if func < num_instructions:
                labels[func] = (labels[func] & ~CMD_REF_OnFunc_Mask) | (CMD_REF_OnFunc | (i << CMD_REF_OnFunc_NumShifts))

    for section in sections:
        if section.start_cmd_index < num_instructions:
            labels[section.start_cmd_index] |= CMD_REF_Section

    for instruction in header.instructions:
        opcode = header.opcode(instruction)
        args = instruction.arguments

        if opcode is Op.Call:
            if len(args) > 1 and args[1] == 1:
                param0 = args[0]
                if 0 < param0 <= num_instructions:
                    labels[param0 - 1] |= CMD_REF_Goto
            else:
                param0 = args[0]
                if param0 > 0 and param0 <= num_instructions:
                    labels[param0 - 1] |= CMD_REF_Call
            continue

        mask = _GOTO_MASKS.get(opcode)
        if mask is None:
            continue

        for i in range(6):
            if not mask:
                break
            if mask & 1:
                param = args[i] if i < len(args) else 0
                signed = param if param < 0x80000000 else param - 0x100000000
                if signed > 0 and param <= num_instructions:
                    labels[param - 1] |= CMD_REF_Goto
            mask >>= 1

    init_start = -1
    init_end = -1
    ipd_len = len(INITPLUGINDIR_OPCODES)
    for k in range(num_instructions):
        flg = labels[k]
        if not is_func(flg):
            continue
        if num_instructions - k < ipd_len:
            continue
        match = True
        for j in range(ipd_len):
            insn = header.instructions[k + j]
            cmd_id = header.opcode(insn)
            if cmd_id != INITPLUGINDIR_OPCODES[j]:
                match = False
                break
        if match:
            init_start = k
            init_end = k + ipd_len
            labels[k] |= CMD_REF_InitPluginDir
            break

    return LabelInfo(
        labels=labels,
        init_plugins_dir_start=init_start,
        init_plugins_dir_end=init_end,
    )

Classes

class NSScriptWriter

Thin wrapper around an output buffer providing NSIS script formatting helpers.

Expand source code Browse git
class NSScriptWriter:
    """
    Thin wrapper around an output buffer providing NSIS script formatting helpers.
    """

    def __init__(self):
        self._buf = io.StringIO()
        self._indent = 0
        self._block_comment = False
        self._line_start = True

    def _emit_line_prefix(self):
        if self._line_start and self._block_comment:
            self._buf.write('; ')
        self._line_start = False

    def indent(self):
        self._indent += 1

    def dedent(self):
        if self._indent > 0:
            self._indent -= 1

    def write(self, s: str):
        self._emit_line_prefix()
        self._buf.write(s)

    def newline(self):
        self._buf.write('\n')
        self._line_start = True

    def space(self):
        self._buf.write(' ')

    def tab(self, commented: bool = False):
        self._emit_line_prefix()
        if commented and not self._block_comment:
            self._buf.write('    ; ')
        else:
            self._buf.write('  ' + '  ' * self._indent)

    def big_space_comment(self):
        self._buf.write('    ; ')

    def small_space_comment(self):
        self._buf.write(' ; ')

    def comment(self, s: str):
        self._emit_line_prefix()
        self._buf.write(F'; {s}')

    def separator(self):
        self.newline()
        self.comment(F'{"":-<72}')
        self.newline()

    def comment_open(self):
        self._block_comment = True
        self._line_start = True

    def comment_close(self):
        self._block_comment = False

    def add_uint(self, v: int):
        self._buf.write(str(v))

    def add_hex(self, v: int):
        self._buf.write(F'0x{v:08X}')

    def add_color(self, v: int):
        v = ((v & 0xFF) << 16) | (v & 0xFF00) | ((v >> 16) & 0xFF)
        self._buf.write(F'0x{v:06X}')

    def add_quoted(self, s: str):
        self._emit_line_prefix()
        if _is_bare_identifier(s) or _is_numeric(s):
            self._buf.write(s)
        else:
            self._buf.write(F'"{s}"')

    def space_quoted(self, s: str):
        self.space()
        self.add_quoted(s)

    def add_string_lf(self, s: str):
        self._emit_line_prefix()
        self._buf.write(s)
        self.newline()

    def tab_string(self, s: str):
        self.tab()
        self._buf.write(s)

    def add_quotes(self):
        self._buf.write('""')

    def getvalue(self) -> str:
        return self._buf.getvalue()

Methods

def indent(self)
Expand source code Browse git
def indent(self):
    self._indent += 1
def dedent(self)
Expand source code Browse git
def dedent(self):
    if self._indent > 0:
        self._indent -= 1
def write(self, s)
Expand source code Browse git
def write(self, s: str):
    self._emit_line_prefix()
    self._buf.write(s)
def newline(self)
Expand source code Browse git
def newline(self):
    self._buf.write('\n')
    self._line_start = True
def space(self)
Expand source code Browse git
def space(self):
    self._buf.write(' ')
def tab(self, commented=False)
Expand source code Browse git
def tab(self, commented: bool = False):
    self._emit_line_prefix()
    if commented and not self._block_comment:
        self._buf.write('    ; ')
    else:
        self._buf.write('  ' + '  ' * self._indent)
def big_space_comment(self)
Expand source code Browse git
def big_space_comment(self):
    self._buf.write('    ; ')
def small_space_comment(self)
Expand source code Browse git
def small_space_comment(self):
    self._buf.write(' ; ')
def comment(self, s)
Expand source code Browse git
def comment(self, s: str):
    self._emit_line_prefix()
    self._buf.write(F'; {s}')
def separator(self)
Expand source code Browse git
def separator(self):
    self.newline()
    self.comment(F'{"":-<72}')
    self.newline()
def comment_open(self)
Expand source code Browse git
def comment_open(self):
    self._block_comment = True
    self._line_start = True
def comment_close(self)
Expand source code Browse git
def comment_close(self):
    self._block_comment = False
def add_uint(self, v)
Expand source code Browse git
def add_uint(self, v: int):
    self._buf.write(str(v))
def add_hex(self, v)
Expand source code Browse git
def add_hex(self, v: int):
    self._buf.write(F'0x{v:08X}')
def add_color(self, v)
Expand source code Browse git
def add_color(self, v: int):
    v = ((v & 0xFF) << 16) | (v & 0xFF00) | ((v >> 16) & 0xFF)
    self._buf.write(F'0x{v:06X}')
def add_quoted(self, s)
Expand source code Browse git
def add_quoted(self, s: str):
    self._emit_line_prefix()
    if _is_bare_identifier(s) or _is_numeric(s):
        self._buf.write(s)
    else:
        self._buf.write(F'"{s}"')
def space_quoted(self, s)
Expand source code Browse git
def space_quoted(self, s: str):
    self.space()
    self.add_quoted(s)
def add_string_lf(self, s)
Expand source code Browse git
def add_string_lf(self, s: str):
    self._emit_line_prefix()
    self._buf.write(s)
    self.newline()
def tab_string(self, s)
Expand source code Browse git
def tab_string(self, s: str):
    self.tab()
    self._buf.write(s)
def add_quotes(self)
Expand source code Browse git
def add_quotes(self):
    self._buf.write('""')
def getvalue(self)
Expand source code Browse git
def getvalue(self) -> str:
    return self._buf.getvalue()
class LabelInfo (labels, init_plugins_dir_start=-1, init_plugins_dir_end=-1)

Result of label analysis pass over NSIS instructions.

Expand source code Browse git
@dataclasses.dataclass
class LabelInfo:
    """
    Result of label analysis pass over NSIS instructions.
    """
    labels: list[int]
    init_plugins_dir_start: int = -1
    init_plugins_dir_end: int = -1

Instance variables

var labels

The type of the None singleton.

var init_plugins_dir_start

The type of the None singleton.

var init_plugins_dir_end

The type of the None singleton.

class NSDecompiler (header, archive=None)

Produces NSIS script output from a parsed NSIS header, combining header info emission, page emission, and per-opcode command emission.

Expand source code Browse git
class NSDecompiler:
    """
    Produces NSIS script output from a parsed NSIS header, combining header info emission, page
    emission, and per-opcode command emission.
    """

    def __init__(self, header: NSHeader, archive: NSArchive | None = None):
        self.header = header
        self.archive = archive
        self.is_installer = True
        if archive is not None:
            self.is_installer = not bool(archive.flags & NSHeaderFlags.Uninstall)

    def decompile(self) -> str:
        header = self.header
        writer = NSScriptWriter()
        sections = header.sections
        label_info = build_labels(header, sections)
        emit_header_info(writer, header, self.archive, self.is_installer)
        emit_pages(writer, header, label_info.labels, self.is_installer)
        emit_commands(writer, header, label_info, sections, self.is_installer)
        return writer.getvalue()

Methods

def decompile(self)
Expand source code Browse git
def decompile(self) -> str:
    header = self.header
    writer = NSScriptWriter()
    sections = header.sections
    label_info = build_labels(header, sections)
    emit_header_info(writer, header, self.archive, self.is_installer)
    emit_pages(writer, header, label_info.labels, self.is_installer)
    emit_commands(writer, header, label_info, sections, self.is_installer)
    return writer.getvalue()
class NSDisassembler (header)

Produces a flat opcode listing from an NSIS script header.

Expand source code Browse git
class NSDisassembler:
    """
    Produces a flat opcode listing from an NSIS script header.
    """

    def __init__(self, header: NSHeader):
        self.header = header

    def disassemble(self) -> str:
        header = self.header
        script = io.StringIO()
        name_width = max(len(op.name) for op in Op)
        addr_width = len(F'{len(header.instructions):X}')
        for k, instruction in enumerate(header.instructions):
            if k > 0:
                script.write('\n')
            opcode = header.opcode(instruction)
            if opcode is Op.INVALID_OPCODE:
                name = F'Command{instruction.opcode}'
            else:
                name = opcode.name
            script.write(F'0x{k:0{addr_width}X}: {name:<{name_width}} ')
            for j, arg in enumerate(
                instruction.arguments[:OP_PARAMETER_COUNT.get(opcode, 6)]
            ):
                if j > 0:
                    script.write(', ')
                if arg > 20 and header._is_good_string(arg):
                    script.write(repr(header._read_string(arg)))
                elif arg < 0x100:
                    script.write(str(arg))
                elif arg < 0x10000:
                    script.write(F'0x{arg:04X}')
                else:
                    script.write(F'0x{arg:08X}')
        return script.getvalue()

Methods

def disassemble(self)
Expand source code Browse git
def disassemble(self) -> str:
    header = self.header
    script = io.StringIO()
    name_width = max(len(op.name) for op in Op)
    addr_width = len(F'{len(header.instructions):X}')
    for k, instruction in enumerate(header.instructions):
        if k > 0:
            script.write('\n')
        opcode = header.opcode(instruction)
        if opcode is Op.INVALID_OPCODE:
            name = F'Command{instruction.opcode}'
        else:
            name = opcode.name
        script.write(F'0x{k:0{addr_width}X}: {name:<{name_width}} ')
        for j, arg in enumerate(
            instruction.arguments[:OP_PARAMETER_COUNT.get(opcode, 6)]
        ):
            if j > 0:
                script.write(', ')
            if arg > 20 and header._is_good_string(arg):
                script.write(repr(header._read_string(arg)))
            elif arg < 0x100:
                script.write(str(arg))
            elif arg < 0x10000:
                script.write(F'0x{arg:04X}')
            else:
                script.write(F'0x{arg:08X}')
    return script.getvalue()