#!/usr/bin/env python3
from __future__ import annotations

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

import os
import unittest

import angr

from tests.common import bin_location


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

addresses_fauxware = {
    "armel": 0x8524,
    "armhf": 0x104C9,  # addr+1 to force thumb
    #'i386': 0x8048524, # commenting out because of the freaking stack check
    "mips": 0x400710,
    "mipsel": 0x4006D0,
    "ppc": 0x1000054C,
    "ppc64": 0x10000698,
    "x86_64": 0x400664,
}


class TestSimulationManager(unittest.TestCase):
    def _run_fauxware(self, arch, threads):
        p = angr.Project(os.path.join(test_location, arch, "fauxware"), load_options={"auto_load_libs": False})

        pg = p.factory.simulation_manager(threads=threads)
        assert len(pg.active) == 1
        assert pg.active[0].history.depth == 0

        # step until the backdoor split occurs
        pg2 = pg.step(until=lambda lpg: len(lpg.active) > 1, step_func=lambda lpg: lpg.prune())
        assert len(pg2.active) == 2
        assert any(b"SOSNEAKY" in s for s in pg2.mp_active.posix.dumps(0).mp_items)
        assert not all(b"SOSNEAKY" in s for s in pg2.mp_active.posix.dumps(0).mp_items)

        # separate out the backdoor and normal paths
        pg3 = pg2.stash(lambda path: b"SOSNEAKY" in path.posix.dumps(0), to_stash="backdoor").move("active", "auth")
        assert len(pg3.active) == 0
        assert len(pg3.backdoor) == 1
        assert len(pg3.auth) == 1

        # step the backdoor path until it returns to main
        pg4 = pg3.step(until=lambda lpg: lpg.backdoor[0].history.jumpkinds[-1] == "Ijk_Ret", stash="backdoor")
        main_addr = pg4.backdoor[0].addr

        assert len(pg4.active) == 0
        assert len(pg4.backdoor) == 1
        assert len(pg4.auth) == 1

        # now step the real path until the real authentication paths return to the same place
        pg5 = pg4.explore(find=main_addr, num_find=2, stash="auth").move("found", "auth")

        assert len(pg5.active) == 0
        assert len(pg5.backdoor) == 1
        assert len(pg5.auth) == 2

        # now unstash everything
        pg6 = pg5.unstash(from_stash="backdoor").unstash(from_stash="auth")
        assert len(pg6.active) == 3
        assert len(pg6.backdoor) == 0
        assert len(pg6.auth) == 0

        assert len(set(pg6.mp_active.addr.mp_items)) == 1

        # now merge them!
        pg7 = pg6.merge()
        assert len(pg7.active) == 2
        assert len(pg7.backdoor) == 0
        assert len(pg7.auth) == 0

        # test selecting paths to step
        pg8 = p.factory.simulation_manager()
        pg8.step(until=lambda lpg: len(lpg.active) > 1, step_func=lambda lpg: lpg.prune().drop(stash="pruned"))
        st1, st2 = pg8.active
        pg8.step(selector_func=lambda p: p is st1, step_func=lambda lpg: lpg.prune().drop(stash="pruned"))
        assert st2 is pg8.active[1]
        assert st1 is not pg8.active[0]

        total_active = len(pg8.active)

        # test special stashes
        assert len(pg8.stashes["stashed"]) == 0
        pg8.stash(filter_func=lambda p: p is pg8.active[1], to_stash="asdf")
        assert len(pg8.stashes["stashed"]) == 0
        assert len(pg8.asdf) == 1
        assert len(pg8.active) == total_active - 1
        pg8.stash(from_stash=pg8.ALL, to_stash="fdsa")
        assert len(pg8.asdf) == 0
        assert len(pg8.active) == 0
        assert len(pg8.fdsa) == total_active
        pg8.stash(from_stash=pg8.ALL, to_stash=pg8.DROP)
        assert all(len(s) == 0 for s in pg8.stashes.values())

    def test_fauxware_armel(self):
        self._run_fauxware("armel", None)

    def test_fauxware_armhf(self):
        self._run_fauxware("armhf", None)

    def test_fauxware_mips(self):
        self._run_fauxware("mips", None)

    def test_fauxware_mipsel(self):
        self._run_fauxware("mipsel", None)

    def test_fauxware_ppc(self):
        self._run_fauxware("ppc", None)

    def test_fauxware_ppc64(self):
        self._run_fauxware("ppc64", None)

    def test_fauxware_x86_64(self):
        self._run_fauxware("x86_64", None)

    def test_find_to_middle(self):
        # Test the ability of PathGroup to execute until an instruction in the middle of a basic block
        p = angr.Project(os.path.join(test_location, "x86_64", "fauxware"), load_options={"auto_load_libs": False})

        pg = p.factory.simulation_manager()
        pg.explore(find=(0x4006EE,))

        assert len(pg.found) == 1
        assert pg.found[0].addr == 0x4006EE

    def test_explore_with_cfg(self):
        p = angr.Project(os.path.join(test_location, "x86_64", "fauxware"), load_options={"auto_load_libs": False})

        cfg = p.analyses.CFGEmulated()

        pg = p.factory.simulation_manager()
        pg.use_technique(angr.exploration_techniques.Explorer(find=0x4006ED, cfg=cfg, num_find=3))
        pg.run()

        assert len(pg.active) == 0
        assert len(pg.avoid) == 1
        assert len(pg.found) == 2
        assert pg.found[0].addr == 0x4006ED
        assert pg.found[1].addr == 0x4006ED
        assert pg.avoid[0].addr == 0x4007C9


if __name__ == "__main__":
    unittest.main()
