From 98c71bbf72ea338ee8331d1802f937bc351d03bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20J=2E=20Rodr=C3=ADguez?= Date: Tue, 23 Jun 2026 19:45:37 +0200 Subject: [PATCH] Add pytest suite and GitHub Actions CI 54 tests covering the architecture abstraction, address helpers, gadget model, gadget finder (canary dedup + sorting), ROPLang parser, operation matching and the ELF loader (including --base relocation). Binary loader tests build minimal ELFs in memory, so no binary blobs are committed. CI runs the suite on Python 3.11, 3.12 and 3.13 for every push to master and every pull request. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 31 +++++++++ pytest.ini | 5 ++ requirements-dev.txt | 2 + tests/conftest.py | 146 +++++++++++++++++++++++++++++++++++++++ tests/test_arch.py | 88 +++++++++++++++++++++++ tests/test_elf.py | 73 ++++++++++++++++++++ tests/test_gadfinder.py | 97 ++++++++++++++++++++++++++ tests/test_gadget.py | 84 ++++++++++++++++++++++ tests/test_operation.py | 56 +++++++++++++++ tests/test_parser.py | 57 +++++++++++++++ tests/test_utils.py | 42 +++++++++++ 11 files changed, 681 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 pytest.ini create mode 100644 requirements-dev.txt create mode 100644 tests/conftest.py create mode 100644 tests/test_arch.py create mode 100644 tests/test_elf.py create mode 100644 tests/test_gadfinder.py create mode 100644 tests/test_gadget.py create mode 100644 tests/test_operation.py create mode 100644 tests/test_parser.py create mode 100644 tests/test_utils.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9e2f82c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.12', '3.13'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Run test suite + run: pytest diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..3818f5d --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +testpaths = tests +python_files = test_*.py +pythonpath = . +addopts = -ra diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..c941202 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest >= 7.0 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..971886e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,146 @@ +''' +This file is part of rop3 (https://github.com/reverseame/rop3). + +rop3 is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +rop3 is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with rop3. If not, see . +''' + +import struct + +import capstone +import pytest + +from rop3.arch import arch_singleton +from rop3.archs.x86_arch import X86_Architecture, X64_Architecture +from rop3.gadget import Gadget + + +@pytest.fixture(autouse=True) +def reset_arch(): + ''' + The architecture is a process-global singleton; reset it around every + test so cases are independent and can pick their own architecture. + ''' + arch_singleton._arch = None + yield + arch_singleton._arch = None + + +@pytest.fixture +def x64(): + arch_singleton._arch = None + arch_singleton.initialize(X64_Architecture()) + return arch_singleton.arch + + +@pytest.fixture +def x86(): + arch_singleton._arch = None + arch_singleton.initialize(X86_Architecture()) + return arch_singleton.arch + + +def make_gadget(code: bytes, vaddr: int, mode=capstone.CS_MODE_64, + filename='test') -> Gadget: + ''' Build a Gadget by disassembling raw bytes with capstone. ''' + md = capstone.Cs(capstone.CS_ARCH_X86, mode) + md.detail = True + decodes = list(md.disasm(code, vaddr)) + return Gadget( + filename=filename, + arch=capstone.CS_ARCH_X86, + mode=mode, + vaddr=vaddr, + decodes=decodes, + bytes=code, + ) + + +# --- Minimal in-memory ELF builder (no committed binary blobs) ------------ + +EM_386 = 3 +EM_X86_64 = 62 +ET_EXEC = 2 +ET_DYN = 3 +SHT_PROGBITS = 1 +SHT_STRTAB = 3 +SHF_ALLOC = 0x2 +SHF_EXECINSTR = 0x4 + + +def build_minimal_elf(elfclass: int, machine: int, text_bytes: bytes, + text_addr: int, e_type: int = ET_DYN) -> bytes: + ''' + Produce a tiny but valid ELF that pyelftools can parse: an ELF header, a + `.text` PROGBITS section flagged executable at `text_addr`, and a + `.shstrtab`. Enough to exercise architecture detection, executable-section + extraction and --base relocation. ET_DYN keeps the image base at 0. + ''' + is64 = elfclass == 64 + shstrtab = b'\x00.text\x00.shstrtab\x00' + name_text = shstrtab.index(b'.text\x00') + name_shstr = shstrtab.index(b'.shstrtab\x00') + + ehsize = 64 if is64 else 52 + shentsize = 64 if is64 else 40 + n_sections = 3 # NULL, .text, .shstrtab + + text_off = ehsize + shstr_off = text_off + len(text_bytes) + shoff = shstr_off + len(shstrtab) + + # e_ident + ei_class = 2 if is64 else 1 + e_ident = b'\x7fELF' + bytes([ei_class, 1, 1, 0]) + b'\x00' * 8 + + if is64: + header = e_ident + struct.pack( + '. +''' + +import capstone +import pytest + +from rop3.arch import arch_singleton, ArchitectureSingleton +from rop3.archs.x86_arch import ( + X86_Architecture, X64_Architecture, REG_BY_WIDTH, +) + + +def test_singleton_initialize_and_matches(): + s = ArchitectureSingleton() + assert not s.is_initialized() + s.initialize(X64_Architecture()) + assert s.is_initialized() + assert s.matches(X64_Architecture()) + assert not s.matches(X86_Architecture()) + + +def test_singleton_initialize_is_sticky(): + ''' initialize() is a no-op once set (single-arch run guarantee). ''' + s = ArchitectureSingleton() + s.initialize(X64_Architecture()) + s.initialize(X86_Architecture()) + assert s.matches(X64_Architecture()) + + +def test_arch_accessed_before_init_raises(): + s = ArchitectureSingleton() + with pytest.raises(RuntimeError): + _ = s.arch + + +def test_arch_modes_and_pointers(): + assert X86_Architecture().mode == capstone.CS_MODE_32 + assert X64_Architecture().mode == capstone.CS_MODE_64 + assert (X86_Architecture().sp, X86_Architecture().bp) == ('esp', 'ebp') + assert (X64_Architecture().sp, X64_Architecture().bp) == ('rsp', 'rbp') + + +@pytest.mark.parametrize('alias,expected', [ + ('rax', 'eax'), ('eax', 'eax'), ('ax', 'eax'), ('al', 'eax'), + ('rbp', 'ebp'), ('r8', 'r8d'), ('r8d', 'r8d'), ('r14', 'r14d'), +]) +def test_normalize_reg_x86_is_32bit(alias, expected): + assert X86_Architecture().normalize_reg(alias) == expected + + +@pytest.mark.parametrize('alias,expected', [ + ('rax', 'rax'), ('eax', 'rax'), ('al', 'rax'), + ('rbp', 'rbp'), ('r8d', 'r8'), ('r14', 'r14'), +]) +def test_normalize_reg_x64_is_64bit(alias, expected): + assert X64_Architecture().normalize_reg(alias) == expected + + +def test_normalize_reg_passthrough_unknown(): + ''' Abstract / unknown register names are returned unchanged. ''' + assert X64_Architecture().normalize_reg('REG1') == 'REG1' + assert X86_Architecture().normalize_reg('dst') == 'dst' + + +def test_reg_by_width_table(): + assert REG_BY_WIDTH['rax'] == {8: 'rax', 4: 'eax'} + assert REG_BY_WIDTH['r8'] == {8: 'r8', 4: 'r8d'} + + +def test_is_valid_abstract_reg_width(): + assert X64_Architecture().is_valid_abstract_reg('rax') + assert not X64_Architecture().is_valid_abstract_reg('eax') + assert X86_Architecture().is_valid_abstract_reg('eax') + assert not X86_Architecture().is_valid_abstract_reg('rax') diff --git a/tests/test_elf.py b/tests/test_elf.py new file mode 100644 index 0000000..a42d26c --- /dev/null +++ b/tests/test_elf.py @@ -0,0 +1,73 @@ +''' +This file is part of rop3 (https://github.com/reverseame/rop3). + +rop3 is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +rop3 is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with rop3. If not, see . +''' + +import pytest + +import rop3.binaries.elf as elfmod +import rop3.binary as binary +from rop3.archs.x86_arch import X86_Architecture, X64_Architecture + +from conftest import build_minimal_elf, EM_386, EM_X86_64, ET_DYN + +TEXT = b'\x58\xc3' # pop rax ; ret + + +def test_elf_detects_x64(): + data = build_minimal_elf(64, EM_X86_64, TEXT, 0x1000, ET_DYN) + assert isinstance(elfmod.ELF(data, None).get_arch(), X64_Architecture) + + +def test_elf_detects_x86(): + data = build_minimal_elf(32, EM_386, TEXT, 0x8048000, ET_DYN) + assert isinstance(elfmod.ELF(data, None).get_arch(), X86_Architecture) + + +def test_elf_exec_section_extraction(): + data = build_minimal_elf(64, EM_X86_64, TEXT, 0x1000, ET_DYN) + secs = elfmod.ELF(data, None).get_exec_sections() + assert len(secs) == 1 + assert secs[0]['vaddr'] == 0x1000 + assert secs[0]['opcodes'] == TEXT + + +def test_elf_base_relocation_pie(): + ''' Issue #13: --base must relocate ELF (ET_DYN image base is 0). ''' + data = build_minimal_elf(64, EM_X86_64, TEXT, 0x1000, ET_DYN) + secs = elfmod.ELF(data, '0x400000').get_exec_sections() + assert secs[0]['vaddr'] == 0x401000 + + +def test_elf_no_base_keeps_addresses(): + data = build_minimal_elf(64, EM_X86_64, TEXT, 0x1000, ET_DYN) + secs = elfmod.ELF(data, None).get_exec_sections() + assert secs[0]['vaddr'] == 0x1000 + + +def test_binary_dispatches_elf_by_magic(tmp_path): + data = build_minimal_elf(64, EM_X86_64, TEXT, 0x1000, ET_DYN) + path = tmp_path / 'fake.elf' + path.write_bytes(data) + b = binary.Binary(str(path), None) + assert isinstance(b.get_arch(), X64_Architecture) + assert b.get_exec_sections()[0]['opcodes'] == TEXT + + +def test_binary_unknown_format_raises(tmp_path): + path = tmp_path / 'junk.bin' + path.write_bytes(b'not a real binary header') + with pytest.raises(binary.BinaryException): + binary.Binary(str(path), None) diff --git a/tests/test_gadfinder.py b/tests/test_gadfinder.py new file mode 100644 index 0000000..cc2bea7 --- /dev/null +++ b/tests/test_gadfinder.py @@ -0,0 +1,97 @@ +''' +This file is part of rop3 (https://github.com/reverseame/rop3). + +rop3 is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +rop3 is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with rop3. If not, see . +''' + +import capstone + +import rop3.gadfinder as gadfinder +from rop3.archs.x86_arch import X86_Architecture + + +def test_avoid_bytes_defaults_to_canary(): + f = gadfinder.GadFinder() + assert f._avoid_bytes(None) == set(gadfinder.CANARY_BYTES) + assert f._avoid_bytes(['0x00', '0x0a', '0x0d', '0xff']) == {0x00, 0x0a, 0x0d, 0xff} + + +def test_avoid_bytes_uses_user_badchars(): + f = gadfinder.GadFinder() + assert f._avoid_bytes(['0x41', '0x42']) == {0x41, 0x42} + + +def test_addr_canary_score(x86): + from conftest import make_gadget + f = gadfinder.GadFinder() + avoid = set(gadfinder.CANARY_BYTES) + clean = make_gadget(b'\xc3', 0x12345620, mode=capstone.CS_MODE_32) + dirty = make_gadget(b'\xc3', 0x1234560a, mode=capstone.CS_MODE_32) # low byte 0x0a + assert f._addr_canary_score(clean, avoid) == 0 + assert f._addr_canary_score(dirty, avoid) == 1 + + +class _FakeBinary: + def __init__(self, vaddr, opcodes, filename='fake'): + self.filename = filename + self._vaddr = vaddr + self._opcodes = opcodes + + def get_exec_sections(self): + return [{'vaddr': self._vaddr, 'opcodes': self._opcodes}] + + def get_arch(self): + return X86_Architecture() + + +def _run_find(flags, buf, base_vaddr): + f = gadfinder.GadFinder(flags=flags) + f._open_binary = lambda fn, b: _FakeBinary(base_vaddr, bytes(buf)) + return f.find(['fake']) + + +def test_dedup_prefers_canary_free_address(x86): + ''' Issue #5: among duplicates keep the address with fewest canary bytes. ''' + base = 0x12345600 + buf = bytearray(0x40) + buf[0x0a] = 0xc3 # ret at ...0a (canary) + buf[0x20] = 0xc3 # ret at ...20 (clean) + + flags = gadfinder.ROP | gadfinder.AVOID_CANARY + rets = [g for g in _run_find(flags, buf, base) if g.text_repr == 'ret'] + assert len(rets) == 1 + assert rets[0].vaddr == 0x12345620 + assert rets[0].count == 2 + + +def test_dedup_keep_canary_keeps_first_seen(x86): + base = 0x12345600 + buf = bytearray(0x40) + buf[0x0a] = 0xc3 + buf[0x20] = 0xc3 + + rets = [g for g in _run_find(gadfinder.ROP, buf, base) if g.text_repr == 'ret'] + assert len(rets) == 1 + assert rets[0].vaddr == 0x1234560a # first seen, no canary preference + assert rets[0].count == 2 + + +def test_results_sorted_by_address(x86): + base = 0x10000 + buf = bytearray(0x40) + for off in (0x30, 0x05, 0x18): + buf[off] = 0xc3 + gadgets = _run_find(gadfinder.ROP, buf, base) + vaddrs = [g.vaddr for g in gadgets] + assert vaddrs == sorted(vaddrs) diff --git a/tests/test_gadget.py b/tests/test_gadget.py new file mode 100644 index 0000000..7a11ef3 --- /dev/null +++ b/tests/test_gadget.py @@ -0,0 +1,84 @@ +''' +This file is part of rop3 (https://github.com/reverseame/rop3). + +rop3 is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +rop3 is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with rop3. If not, see . +''' + +import capstone + +import rop3.gadget as gadget_mod +from rop3.gadget import heuristic_basic_count + +from conftest import make_gadget + + +def test_text_repr_and_equality(): + g1 = make_gadget(b'\x58\xc3', 0x1000) # pop rax ; ret + g2 = make_gadget(b'\x58\xc3', 0x2000) # same text, different addr + g3 = make_gadget(b'\x5b\xc3', 0x1000) # pop rbx ; ret + assert g1.text_repr == 'pop rax ; ret' + assert g1 == g2 # equality is text-based + assert hash(g1) == hash(g2) + assert g1 != g3 + + +def test_calculate_side_effects_excludes_dst_src_sp(x64): + # inc rcx ; pop rbp ; ret + g = make_gadget(b'\x48\xff\xc1\x5d\xc3', 0x1000) + g.calculate_side_effects() + assert 'rcx' in g.side_regs + assert 'rbp' in g.side_regs + + +def test_calculate_side_effects_x86_uses_32bit_names(x86): + # inc ecx ; pop ebp ; ret + g = make_gadget(b'\x41\x5d\xc3', 0x1000, mode=capstone.CS_MODE_32) + g.calculate_side_effects() + assert 'ecx' in g.side_regs and 'ebp' in g.side_regs + # no 64-bit names leak into a 32-bit context + assert not ({'rcx', 'rbp'} & g.side_regs) + + +def test_subsumes(x64): + base = make_gadget(b'\x58\xc3', 0x1000) # pop rax ; ret + noisy = make_gadget(b'\x58\x5b\xc3', 0x2000) # pop rax ; pop rbx ; ret + base.side_regs = set() + noisy.side_regs = {'rbx'} + assert base.subsumes(noisy) + assert not noisy.subsumes(base) + + +def test_heuristic_basic_count(x64): + g = make_gadget(b'\x58\xc3', 0x1000) + g.side_regs = {'rbx'} + # 1 side reg (<<2 = 4) + 2 instructions (<<1 = 4) + assert heuristic_basic_count(g) == 8 + + +def test_str_no_color_when_not_tty(x64, monkeypatch, capsys): + ''' Regression for issue #14: no ANSI escapes on non-TTY output. ''' + monkeypatch.setattr('sys.stdout.isatty', lambda: False, raising=False) + g = make_gadget(b'\x48\xff\xc1\xc3', 0x1000) # inc rcx ; ret + g.calculate_side_effects() + text = str(g) + assert 'modifies' in text + assert '\033' not in text + + +def test_colorize_honors_tty_and_no_color(monkeypatch): + monkeypatch.setattr('sys.stdout.isatty', lambda: True, raising=False) + monkeypatch.delenv('NO_COLOR', raising=False) + assert '\033' in gadget_mod._colorize('x') + monkeypatch.setenv('NO_COLOR', '1') + assert '\033' not in gadget_mod._colorize('x') diff --git a/tests/test_operation.py b/tests/test_operation.py new file mode 100644 index 0000000..951d44e --- /dev/null +++ b/tests/test_operation.py @@ -0,0 +1,56 @@ +''' +This file is part of rop3 (https://github.com/reverseame/rop3). + +rop3 is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +rop3 is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with rop3. If not, see . +''' + +import rop3.operation as operation + +from conftest import make_gadget + + +def test_lc_matches_pop_reg(x64): + ''' lc (load constant) matches `pop ; ret`. ''' + gadgets = [ + make_gadget(b'\x58\xc3', 0x1000), # pop rax ; ret + make_gadget(b'\x5b\xc3', 0x1010), # pop rbx ; ret + make_gadget(b'\x90\xc3', 0x1020), # nop ; ret (no match) + ] + matched = operation.Operation('lc').filter_gadgets(gadgets) + texts = {g.text_repr for g in matched} + assert 'pop rax ; ret' in texts + assert 'pop rbx ; ret' in texts + assert 'nop ; ret' not in texts + + +def test_lc_with_dst_filter(x64): + gadgets = [ + make_gadget(b'\x58\xc3', 0x1000), # pop rax ; ret + make_gadget(b'\x5b\xc3', 0x1010), # pop rbx ; ret + ] + matched = operation.Operation('lc', dst='rax').filter_gadgets(gadgets) + assert [g.text_repr for g in matched] == ['pop rax ; ret'] + assert matched[0].dst == 'rax' + + +def test_filter_gadgets_empty_input(x64): + assert operation.Operation('lc').filter_gadgets([]) == [] + + +def test_operand_parse_imm_supports_hex_and_negative(x64): + ''' Regression: immediates parsed with int(x, 0). ''' + op = operation.Operand('rax') + assert op._parse_imm('0xffffffff') == 0xffffffff + assert op._parse_imm('-1') == -1 + assert op._parse_imm(42) == 42 diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..0d22c9e --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,57 @@ +''' +This file is part of rop3 (https://github.com/reverseame/rop3). + +rop3 is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +rop3 is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with rop3. If not, see . +''' + +import pytest + +import rop3.parser as parser + + +def test_get_ops_loads_roplang(x64): + ops = parser.Parser().get_ops() + names = {getattr(o, 'name', None) for o in ops} + # a representative subset that must always be present + for expected in ('mov', 'lc', 'jmp', 'lsd', 'not', 'xor'): + assert expected in names + + +def test_get_op_unknown_raises(x64): + with pytest.raises(parser.ParserException): + parser.Parser().get_op('definitely_not_an_op') + + +def test_composite_op_is_recognised(x64): + ''' lsd is a composite (compose:) operation. ''' + resolved = parser.Parser().get_op('lsd') + assert isinstance(resolved, parser.CompositeOperation) + assert resolved.steps + + +def _jmp_mov_op1(): + ''' jmp is a composite whose first step is `mov REG_BP, src`. ''' + jmp = parser.Parser().get_op('jmp') + assert isinstance(jmp, parser.CompositeOperation) + mov = next(s for s in jmp.steps if s['operation'] == 'mov') + return mov['op1'] + + +def test_reg_aliases_resolved_per_arch_x64(x64): + ''' REG_BP must resolve to rbp on x64 (not stay as the alias). ''' + assert _jmp_mov_op1() == 'rbp' + + +def test_reg_aliases_resolved_per_arch_x86(x86): + assert _jmp_mov_op1() == 'ebp' diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..49ae113 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,42 @@ +''' +This file is part of rop3 (https://github.com/reverseame/rop3). + +rop3 is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +rop3 is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with rop3. If not, see . +''' + +import capstone +import pytest + +import rop3.utils as utils + + +def test_pretty_addr_padding(): + # padding is the total field width including the '0x' prefix + assert utils.pretty_addr(0x1000, capstone.CS_MODE_32) == '0x001000' + assert utils.pretty_addr(0x1000, capstone.CS_MODE_64) == '0x00000000001000' + assert len(utils.pretty_addr(0x1000, capstone.CS_MODE_32)) == 8 + assert len(utils.pretty_addr(0x1000, capstone.CS_MODE_64)) == 16 + + +def test_pack_addr_endianness_and_width(): + assert utils.pack_addr(0x41424344, capstone.CS_MODE_32) == b'\x44\x43\x42\x41' + assert utils.pack_addr(0x41424344, capstone.CS_MODE_64) == \ + b'\x44\x43\x42\x41\x00\x00\x00\x00' + + +@pytest.mark.parametrize('fn', [utils.pretty_addr, utils.pack_addr]) +def test_addr_helpers_reject_unknown_mode(fn): + ''' Regression for issue #15: unbound local on unsupported mode. ''' + with pytest.raises(ValueError): + fn(0x1000, mode=999)