#!/usr/bin/env python3
# pylint: disable=missing-class-docstring,no-self-use,line-too-long,no-member
from __future__ import annotations

__package__ = __package__ or "tests.analyses.decompiler"  # pylint:disable=redefined-builtin

import logging
import os
import re
import time
import unittest
from functools import wraps

import angr.ailment as ailment

import angr
from angr.knowledge_plugins.variables.variable_manager import VariableManagerInternal
from angr.sim_type import (
    SimTypeInt,
    SimTypePointer,
    SimTypeBottom,
    SimTypeLongLong,
    SimTypeArray,
    SimTypeChar,
    SimTypeFunction,
)
from angr.analyses import (
    VariableRecoveryFast,
    CallingConventionAnalysis,
    CompleteCallingConventionsAnalysis,
    CFGFast,
    Decompiler,
)
from angr.analyses.complete_calling_conventions import CallingConventionAnalysisMode
from angr.analyses.decompiler import DECOMPILATION_PRESETS
from angr.analyses.decompiler.optimization_passes.expr_op_swapper import OpDescriptor
from angr.analyses.decompiler.optimization_passes import (
    DUPLICATING_OPTS,
    CONDENSING_OPTS,
    LoweredSwitchSimplifier,
    CrossJumpReverter,
    InlinedStringTransformationSimplifier,
    ReturnDuplicatorLow,
    ReturnDuplicatorHigh,
    DuplicationReverter,
    ITERegionConverter,
)
from angr.analyses.decompiler.decompilation_options import get_structurer_option, PARAM_TO_OPTION
from angr.analyses.decompiler.structuring import STRUCTURER_CLASSES, PhoenixStructurer, SAILRStructurer
from angr.analyses.decompiler.structuring.phoenix import MultiStmtExprMode
from angr.sim_variable import SimStackVariable
from angr.utils.library import convert_cproto_to_py

from tests.common import bin_location, broken, print_decompilation_result, set_decompiler_option, WORKER


test_location = os.path.join(bin_location, "tests")

l = logging.Logger(__name__)


def normalize_whitespace(s: str) -> str:
    """
    Strips whitespace from start/end of lines, and replace newlines with space.
    """
    return " ".join([l for l in [s.strip() for s in s.splitlines()] if l])


def options_to_structuring_algo(decompiler_options: list[tuple] | None) -> str | None:
    """
    Locates and returns the structuring algorithm specified in the decompiler options.
    If no structuring algorithm is specified, returns None.
    """
    if not decompiler_options:
        return None

    for option, value in decompiler_options:
        if option.param == "structurer_cls":
            return value

    return None


def for_all_structuring_algos(func):
    """
    A helper wrapper that wraps a unittest function that has an option for 'decompiler_options'.
    This option MUST be used when calling the Decompiler interface for the effects of using all
    structuring algorithms.

    In the function its best to call your decompiler like so:
    angr.analyses.Decompiler(f, cfg=..., options=decompiler_options)
    """

    @wraps(func)
    def _for_all_structuring_algos(*args, **kwargs):
        orig_opts = kwargs.pop("decompiler_options", None) or []
        structurer_option = get_structurer_option()
        for structurer in STRUCTURER_CLASSES:
            # skip Phoenix since SAILR supersedes it and is a subclass
            if structurer == PhoenixStructurer.NAME:
                continue

            new_opts = [*orig_opts, (structurer_option, structurer)]
            func(*args, decompiler_options=new_opts, **kwargs)

    return _for_all_structuring_algos


def structuring_algo(algo: str):
    def _structuring_algo(func):
        @wraps(func)
        def inner(*args, **kwargs):
            orig_opts = kwargs.pop("decompiler_options", None) or []
            structurer_option = get_structurer_option()
            new_opts = [*orig_opts, (structurer_option, algo)]
            func(*args, decompiler_options=new_opts, **kwargs)

        return inner

    return _structuring_algo


class TestDecompiler(unittest.TestCase):
    @for_all_structuring_algos
    def test_decompiling_all_x86_64(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "all")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(data_references=True, normalize=True)
        for f in cfg.functions.values():
            if f.is_simprocedure or f.is_plt or f.is_syscall or f.is_alignment:
                l.debug("Skipping SimProcedure %s.", repr(f))
                continue
            dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)

            if dec.codegen is not None and f.name not in {
                "deregister_tm_clones",
                "register_tm_clones",
                "frame_dummy",
                "__libc_csu_init",
            }:
                print_decompilation_result(dec)
                assert dec.codegen.text is not None
                assert "(true)" not in dec.codegen.text and "(false)" not in dec.codegen.text

    @for_all_structuring_algos
    def test_decompiling_babypwn_i386(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "i386", "decompiler", "codegate2017_babypwn")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)
        p.analyses[CompleteCallingConventionsAnalysis].prep()(recover_variables=True)
        for f in cfg.functions.values():
            if f.is_simprocedure:
                l.debug("Skipping SimProcedure %s.", repr(f))
                continue
            if f.addr not in (0x8048A71, 0x8048C6B):
                continue
            dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
            assert dec.codegen is not None, f"Failed to decompile function {f!r}."
            print_decompilation_result(dec)

    @structuring_algo("dream")
    def test_decompiling_loop_x86_64(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "decompiler", "loop")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)
        f = cfg.functions["loop"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)
        # it should be properly structured to a while loop with conditional breaks.
        assert dec.codegen.text is not None
        assert "break" in dec.codegen.text

    @for_all_structuring_algos
    def test_decompiling_all_i386(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "i386", "all")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(data_references=True, normalize=True)

        f = cfg.functions["main"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

    @for_all_structuring_algos
    def test_decompiling_aes_armel(self, decompiler_options=None):
        # EDG Says: This binary is invalid.
        # Consider replacing with some real firmware
        bin_path = os.path.join(test_location, "armel", "aes")
        # TODO: FIXME: EDG says: This binary is actually CortexM
        # It is incorrectly linked. We override this here
        p = angr.Project(bin_path, arch="ARMEL", auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(data_references=True, normalize=True)

        f = cfg.functions["main"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

    @for_all_structuring_algos
    def test_decompiling_mips_allcmps(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "mips", "allcmps")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(collect_data_references=True, normalize=True)

        f = cfg.functions["main"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

    @for_all_structuring_algos
    def test_decompiling_linked_list(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "linked_list")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        f = cfg.functions["sum"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

    @for_all_structuring_algos
    def test_decompiling_dir_gcc_O0_free_ent(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "dir_gcc_-O0")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(normalize=True)

        f = cfg.functions["free_ent"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

    @for_all_structuring_algos
    def test_decompiling_dir_gcc_O0_main(self, decompiler_options=None):
        # tests loop structuring
        bin_path = os.path.join(test_location, "x86_64", "dir_gcc_-O0")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(normalize=True)

        f = cfg.functions["main"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

    @for_all_structuring_algos
    def test_decompiling_dir_gcc_O0_emit_ancillary_info(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "dir_gcc_-O0")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(normalize=True)

        f = cfg.functions["emit_ancillary_info"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

    @for_all_structuring_algos
    def test_decompiling_switch0_x86_64(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "switch_0")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        f = cfg.functions["main"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)

        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)
        code = dec.codegen.text
        assert code is not None
        assert "switch" in code
        assert "case 1:" in code
        assert "case 2:" in code
        assert "case 3:" in code
        assert "case 4:" in code
        assert "case 5:" in code
        assert "case 6:" in code
        assert "case 7:" in code
        assert "default:" in code

    @for_all_structuring_algos
    def test_decompiling_switch1_x86_64(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "switch_1")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        # disable duplicating code
        all_optimization_passes = DECOMPILATION_PRESETS["full"].get_optimization_passes(
            "AMD64", "linux", disable_opts=DUPLICATING_OPTS
        )

        f = cfg.functions["main"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(
            f, cfg=cfg.model, options=decompiler_options, optimization_passes=all_optimization_passes
        )
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)
        code = dec.codegen.text
        assert code is not None
        assert "switch" in code
        assert "case 1:" in code
        assert "case 2:" in code
        assert "case 3:" in code
        assert "case 4:" in code
        assert "case 5:" in code
        assert "case 6:" in code
        assert "case 7:" in code
        assert "case 8:" in code
        assert "default:" not in code

    @for_all_structuring_algos
    def test_decompiling_switch2_x86_64(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "switch_2")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        # disable duplicating code
        all_optimization_passes = DECOMPILATION_PRESETS["full"].get_optimization_passes(
            "AMD64", "linux", disable_opts=DUPLICATING_OPTS
        )

        f = cfg.functions["main"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(
            f, cfg=cfg.model, options=decompiler_options, optimization_passes=all_optimization_passes
        )
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)
        code = dec.codegen.text
        assert code is not None
        assert "switch" in code
        assert "case 1:" in code
        assert "case 2:" in code
        assert "case 3:" in code
        assert "case 4:" in code
        assert "case 5:" in code
        assert "case 6:" in code
        assert "case 7:" in code
        assert "case 8:" not in code
        assert "default:" in code

        assert code.count("break;") == 4

    @for_all_structuring_algos
    def test_decompiling_true_x86_64_0(self, decompiler_options=None):
        # in fact this test case tests if CFGBase._process_jump_table_targeted_functions successfully removes "function"
        # 0x402543, which is an artificial function that the compiler (GCC) created for identified "cold" functions.

        bin_path = os.path.join(test_location, "x86_64", "true_ubuntu_2004")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        # disable duplicating code
        all_optimization_passes = DECOMPILATION_PRESETS["full"].get_optimization_passes(
            "AMD64", "linux", disable_opts=DUPLICATING_OPTS
        )

        f = cfg.functions[0x4048C0]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(
            f, cfg=cfg.model, options=decompiler_options, optimization_passes=all_optimization_passes
        )
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)
        code = dec.codegen.text
        assert code is not None
        assert "switch" in code
        assert "case" in code

    @for_all_structuring_algos
    def test_decompiling_true_x86_64_1(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "true_ubuntu_2004")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        # disable duplicating code
        all_optimization_passes = DECOMPILATION_PRESETS["full"].get_optimization_passes(
            "AMD64", "linux", disable_opts=DUPLICATING_OPTS
        )

        f = cfg.functions[0x404DC0]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(
            f, cfg=cfg.model, options=decompiler_options, optimization_passes=all_optimization_passes
        )
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)
        code: str = dec.codegen.text

        # constant propagation was failing. see https://github.com/angr/angr/issues/2659
        assert (
            code.count("32 <=") == 0
            and code.count("32 >") == 0
            and code.count("((int)32) <=") == 0
            and code.count("((int)32) >") == 0
        )
        if "*(&stack_base-56:32)" in code:
            assert code.count("32") == 3
        else:
            assert code.count("32") == 2

    @broken
    @for_all_structuring_algos
    def test_decompiling_true_a_x86_64_0(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "true_a")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep(show_progressbar=not WORKER)(normalize=True, data_references=True)

        # disable any optimization which may duplicate code to remove gotos since we need them for the switch
        # structure to be recovered
        all_optimization_passes = DECOMPILATION_PRESETS["full"].get_optimization_passes(
            "AMD64", "linux", disable_opts=DUPLICATING_OPTS
        )

        f = cfg.functions[0x401E60]
        dec = p.analyses[Decompiler].prep(show_progressbar=not WORKER)(
            f, cfg=cfg.model, options=decompiler_options, optimization_passes=all_optimization_passes
        )
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

        assert dec.codegen.text.count("switch (") == 3  # there are three switch-cases in total

    @for_all_structuring_algos
    def test_decompiling_true_a_x86_64_1(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "true_a")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        # since this is a case we know where DuplicationReverter eliminates some bad code, we should
        # disable it for this since we want to test the decompiler's ability to handle this case when it has
        # these messed up loops
        all_optimization_passes = DECOMPILATION_PRESETS["full"].get_optimization_passes(
            "AMD64", "linux", disable_opts=[*DUPLICATING_OPTS, DuplicationReverter]
        )

        f = cfg.functions[0x404410]

        dec = p.analyses[Decompiler].prep(fail_fast=True)(
            f,
            cfg=cfg.model,
            options=set_decompiler_option(decompiler_options, [("cstyle_ifs", False)]),
            optimization_passes=all_optimization_passes,
        )
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

        # the decompilation output should somewhat make sense
        assert 'getenv("CHARSETALIASDIR");' in dec.codegen.text
        assert "fscanf(" in dec.codegen.text
        assert '"%50s %50s"' in dec.codegen.text

        # make sure all "break;" is followed by a curly brace
        dec_no_spaces = dec.codegen.text.replace("\n", "").replace(" ", "")
        replaced = dec_no_spaces.replace("break;}", "")
        # TODO: we really should not be making a switch in this function, but the sensitivity needs to be
        #   improved to avoid this. See test_true_a_graph_deduplication to see original source for this func.
        replaced = replaced.replace("break;case", "")
        replaced = replaced.replace("break;default", "")

        assert "break" not in replaced

    @for_all_structuring_algos
    def test_decompiling_true_1804_x86_64(self, decompiler_options=None):
        # true in Ubuntu 18.04, with -O2, has special optimizations that
        # may mess up the way we structure loops and conditionals

        bin_path = os.path.join(test_location, "x86_64", "true_ubuntu1804")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses.CFG(normalize=True, data_references=True)

        f = cfg.functions["usage"]
        dec = p.analyses.Decompiler(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

    @for_all_structuring_algos
    def test_decompiling_true_mips64(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "mips64", "true")
        p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=False)
        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        all_optimization_passes = DECOMPILATION_PRESETS["full"].get_optimization_passes("MIPS64", "linux")

        f = cfg.functions["main"]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(
            f, cfg=cfg.model, options=decompiler_options, optimization_passes=all_optimization_passes
        )
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)
        # make sure strings exist
        assert '"coreutils"' in dec.codegen.text
        assert '"/usr/local/share/locale"' in dec.codegen.text
        assert '"--help"' in dec.codegen.text
        assert '"Jim Meyering"' in dec.codegen.text
        # make sure function calls exist
        assert "set_program_name(" in dec.codegen.text
        assert "setlocale(" in dec.codegen.text
        assert "usage(0);" in dec.codegen.text

    @for_all_structuring_algos
    def test_decompiling_1after909_verify_password(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "1after909")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        # verify_password
        f = cfg.functions["verify_password"]
        # recover calling convention
        p.analyses[VariableRecoveryFast].prep()(f)
        cca = p.analyses[CallingConventionAnalysis].prep()(f)
        f.calling_convention = cca.cc
        f.prototype = cca.prototype
        decompiler_options = decompiler_options or []
        decompiler_options += [("semvar_naming", False), ("loopctr_naming", False)]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(f, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

        code = dec.codegen.text
        assert "stack_base" not in code, "Some stack variables are not recognized"

        m = re.search(r"strncmp\(a1, \S+, 64\)", code)
        assert m is not None
        strncmp_expr = m.group(0)
        strncmp_stmt = strncmp_expr + ";"
        assert strncmp_stmt not in code, "Call expressions folding failed for strncmp()"

        lines = code.split("\n")
        for line in lines:
            if '"%02x"' in line:
                assert "sprintf(" in line
                assert ("v0" in line and "v1" in line and "v2" in line) or (
                    "v2" in line and "v3" in line and "v4" in line
                ), "Failed to find v0, v1, and v2 in the same line. Is propagator over-propagating?"

        assert "= sprintf" not in code, "Failed to remove the unused return value of sprintf()"

        # the stack variable at bp-0x58 is a char array of 64 bytes
        v2 = next(
            iter(
                v for v in dec.codegen.cfunc.variables_in_use if isinstance(v, SimStackVariable) and v.offset == -0x58
            ),
            None,
        )
        assert v2 is not None
        cv2 = dec.codegen.cfunc.variables_in_use[v2]
        assert isinstance(cv2.type, SimTypeArray)
        assert isinstance(cv2.type.elem_type, SimTypeChar)
        assert cv2.type.length == 64

    @for_all_structuring_algos
    def test_decompiling_1after909_doit(self, decompiler_options=None):
        """
        The doit() function has an abnormal loop at 0x1d47 - 0x1da1 - 0x1d73.
        The original source code can be found here:
        https://github.com/shellphish/ictf20-challenges/blob/1e0b7c1fde9b5c8ff2d3e1ca428c4396d63e046e/1after909/src/1after909.c#L298
        """

        bin_path = os.path.join(test_location, "x86_64", "1after909")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(normalize=True, data_references=True)

        # doit
        f = cfg.functions["doit"]
        optimization_passes = DECOMPILATION_PRESETS["full"].get_optimization_passes(
            p.arch, p.simos.name, additional_opts=DUPLICATING_OPTS
        )
        decompiler_options = decompiler_options or []
        decompiler_options += [("semvar_naming", False), ("loopctr_naming", False)]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(
            f, cfg=cfg.model, options=decompiler_options, optimization_passes=optimization_passes
        )
        assert dec.codegen is not None, f"Failed to decompile function {f!r}."
        print_decompilation_result(dec)

        code = dec.codegen.text
        # with ReturnDuplicatorLow applied, there should be no goto!
        assert "goto" not in code.lower(), "Found goto statements. ReturnDuplicator might have failed."
        # with global variables discovered, there should not be any loads of constant addresses.
        assert "fflush(stdout);" in code.lower()

        access_count = code.count("access(")
        assert (
            access_count == 2
        ), f"The decompilation should contain 2 calls to access(), but instead {access_count} calls are present."

        m = re.search(r"if \([\S]*access\([\S]+, [\S]+\) == -1\)", code)
        if m is None:
            # Try without call folding
            m = re.search(r"(\w+) = access\(\w+, 0\);\s*if \(\1 == -1\)", code)
        assert m is not None, "The if branch at 0x401c91 is not found. Structurer is incorrectly removing conditionals."

        # Arguments to the convert call should be fully folded into the call statement itself
        code_lines = [line.strip(" ") for line in code.split("\n")]
        for i, line in enumerate(code_lines):
            if "convert(" in line:
                # the previous line must be a curly brace
                assert i > 0
                assert (
                    code_lines[i - 1] == "{"
                ), "Some arguments to convert() are probably not folded into this call statement."
                break
        else:
            assert False, "Call to convert() is not found in decompilation output."

        # propagator should not replace stack variables
        assert "free(v" in code
        assert "free(NULL" not in code and "free(0" not in code

        # return values are either 0xffffffff or -1
        assert "return 4294967295;" in code or "return -1;" in code

        # the while loop containing puts("Empty title"); must have both continue and break
        for i, line in enumerate(code_lines):
            if line == 'puts("Empty title");':
                assert "break;" in code_lines[i - 9 : i + 9]
                break
        else:
            assert False, "Did not find statement 'puts(\"Empty title\");'"

    @for_all_structuring_algos
    def test_decompiling_libsoap(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "armel", "libsoap.so")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(data_references=True, normalize=True)

        func = cfg.functions[0x41D000]
        dec = p.analyses[Decompiler].prep(fail_fast=True)(func, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {func!r}."
        print_decompilation_result(dec)

    @for_all_structuring_algos
    def test_decompiling_no_arguments_in_variable_list(self, decompiler_options=None):
        # function arguments should never appear in the variable list
        bin_path = os.path.join(test_location, "x86_64", "test_arrays")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(data_references=True, normalize=True)

        func = cfg.functions["main"]

        dec = p.analyses[Decompiler].prep(fail_fast=True)(func, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {func!r}."
        print_decompilation_result(dec)
        print_decompilation_result(dec)
        code = dec.codegen.text
        decls = code.split("\n\n")[0]

        argc_name = " a0"  # update this variable once the decompiler picks up
        # argument names from the common definition of main()
        assert argc_name in decls
        assert code.count(decls) == 1  # it should only appear once

    def test_decompiling_strings_c_representation(self):
        input_expected = [("""Foo"bar""", '"Foo\\"bar"'), ("""Foo'bar""", '"Foo\'bar"')]

        for _input, expected in input_expected:
            result = angr.analyses.decompiler.structured_codegen.c.CConstant.str_to_c_str(_input)
            assert result == expected

    @for_all_structuring_algos
    def test_decompiling_strings_local_strlen(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "types", "strings")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(data_references=True, normalize=True)
        func = cfg.functions["local_strlen"]

        _ = p.analyses[VariableRecoveryFast].prep()(func)
        cca = p.analyses[CallingConventionAnalysis].prep()(func, cfg=cfg.model)
        func.calling_convention = cca.cc
        func.prototype = cca.prototype

        dec = p.analyses[Decompiler].prep(fail_fast=True)(func, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {func!r}."
        print_decompilation_result(dec)

        code = dec.codegen.text
        # Make sure argument a0 is correctly typed to char*
        lines = code.split("\n")
        assert "local_strlen(char *a0)" in lines[0], f"Argument a0 seems to be incorrectly typed: {lines[0]}"

    @for_all_structuring_algos
    def test_decompiling_strings_local_strcat(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "types", "strings")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(data_references=True, normalize=True)
        func = cfg.functions["local_strcat"]

        _ = p.analyses[VariableRecoveryFast].prep()(func)
        cca = p.analyses[CallingConventionAnalysis].prep()(func, cfg=cfg.model)
        func.calling_convention = cca.cc
        func.prototype = cca.prototype

        dec = p.analyses[Decompiler].prep(fail_fast=True)(func, cfg=cfg.model, options=decompiler_options)
        assert dec.codegen is not None, f"Failed to decompile function {func!r}."
        print_decompilation_result(dec)

        code = dec.codegen.text
        # Make sure argument a0 is correctly typed to char*
        lines = code.split("\n")
        assert (
            "local_strcat(char *a0, char *a1)" in lines[0]
        ), f"Argument a0 and a1 seem to be incorrectly typed: {lines[0]}"

    @for_all_structuring_algos
    def test_decompiling_strings_local_strcat_with_local_strlen(self, decompiler_options=None):
        bin_path = os.path.join(test_location, "x86_64", "types", "strings")
        p = angr.Project(bin_path, auto_load_libs=False)

        cfg = p.analyses[CFGFast].prep()(data_references=True, normalize=True)
        func_strlen = cfg.functions["local_strlen"]
        _ = p.analyses[VariableRecoveryFast].prep()(func_strlen)
        cca = p.analyses[CallingConventionAnalysis].prep()(func_strlen, cfg=cfg.model)
        func_strlen.calling_convention = cca.cc
        func_strlen.prototype = cca.prototype
        p.analyses[Decompiler].prep(fail_fast=True)(func_strlen, cfg=cfg.model, options=decompiler_options)

        func = cfg.functions["local_strcat"]

        _ = p.analyses[VariableRecoveryFast].prep()(func)
        cca = p.analyses[CallingConventionAnalysis].prep()(func, cfg=cfg.model)
        func.calling_convention = cca.cc
        func.prototype = cca.prototype

