diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 6eb0958e7c..811d2d1806 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -28,8 +28,8 @@ jobs: - name: Build and install capstone run: pip install ./bindings/python - - name: Install cstest dependencies - run: pip install pyyaml + - name: Install py_cstest + run: pip install ./bindings/python/py_cstest - name: Run legacy tests run: python ./bindings/python/tests/test_all.py @@ -37,4 +37,4 @@ jobs: - name: cstest.py integration tests run: | cd suite/cstest/test/ - python3 ./integration_tests.py "python3 ../../../bindings/python/py_cstest/cstest.py" + python3 ./integration_tests.py py_cstest diff --git a/bindings/python/.gitignore b/bindings/python/.gitignore index 61178e6a1b..5ed7ca830b 100644 --- a/bindings/python/.gitignore +++ b/bindings/python/.gitignore @@ -1,6 +1,5 @@ MANIFEST dist/ -src/ capstone/lib capstone/include pyx/lib diff --git a/bindings/python/py_cstest/pyproject.toml b/bindings/python/py_cstest/pyproject.toml new file mode 100644 index 0000000000..b1286c5e2a --- /dev/null +++ b/bindings/python/py_cstest/pyproject.toml @@ -0,0 +1,18 @@ +# Copyright © 2024 Rot127 +# SPDX-License-Identifier: BSD-3 + +[project] +name = "py_cstest" +version = "0.1.0" +dependencies = [ + "pyyaml >= 6.0.2", + "capstone >= 5.0.0", +] +requires-python = ">= 3.8" + +[tool.setuptools] +packages = ["py_cstest"] +package-dir = {"" = "src"} + +[project.scripts] +py_cstest = "py_cstest.cstest:main" diff --git a/bindings/python/py_cstest/src/py_cstest.egg-info/PKG-INFO b/bindings/python/py_cstest/src/py_cstest.egg-info/PKG-INFO new file mode 100644 index 0000000000..99a78982d9 --- /dev/null +++ b/bindings/python/py_cstest/src/py_cstest.egg-info/PKG-INFO @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: py_cstest +Version: 0.1.0 +Requires-Python: >=3.8 +Requires-Dist: pyyaml>=6.0.2 +Requires-Dist: capstone>=5.0.0 diff --git a/bindings/python/py_cstest/src/py_cstest.egg-info/SOURCES.txt b/bindings/python/py_cstest/src/py_cstest.egg-info/SOURCES.txt new file mode 100644 index 0000000000..7f8ae77fb8 --- /dev/null +++ b/bindings/python/py_cstest/src/py_cstest.egg-info/SOURCES.txt @@ -0,0 +1,11 @@ +README.md +pyproject.toml +src/py_cstest/compare.py +src/py_cstest/cs_modes.py +src/py_cstest/cstest.py +src/py_cstest.egg-info/PKG-INFO +src/py_cstest.egg-info/SOURCES.txt +src/py_cstest.egg-info/dependency_links.txt +src/py_cstest.egg-info/entry_points.txt +src/py_cstest.egg-info/requires.txt +src/py_cstest.egg-info/top_level.txt \ No newline at end of file diff --git a/bindings/python/py_cstest/src/py_cstest.egg-info/dependency_links.txt b/bindings/python/py_cstest/src/py_cstest.egg-info/dependency_links.txt new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/bindings/python/py_cstest/src/py_cstest.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/bindings/python/py_cstest/src/py_cstest.egg-info/entry_points.txt b/bindings/python/py_cstest/src/py_cstest.egg-info/entry_points.txt new file mode 100644 index 0000000000..ca930f5055 --- /dev/null +++ b/bindings/python/py_cstest/src/py_cstest.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +py_cstest = py_cstest.cstest:main diff --git a/bindings/python/py_cstest/src/py_cstest.egg-info/requires.txt b/bindings/python/py_cstest/src/py_cstest.egg-info/requires.txt new file mode 100644 index 0000000000..9c1778bb5c --- /dev/null +++ b/bindings/python/py_cstest/src/py_cstest.egg-info/requires.txt @@ -0,0 +1,2 @@ +pyyaml>=6.0.2 +capstone>=5.0.0 diff --git a/bindings/python/py_cstest/src/py_cstest.egg-info/top_level.txt b/bindings/python/py_cstest/src/py_cstest.egg-info/top_level.txt new file mode 100644 index 0000000000..c4f81bda90 --- /dev/null +++ b/bindings/python/py_cstest/src/py_cstest.egg-info/top_level.txt @@ -0,0 +1 @@ +py_cstest diff --git a/bindings/python/py_cstest/src/py_cstest/compare.py b/bindings/python/py_cstest/src/py_cstest/compare.py new file mode 100644 index 0000000000..d454ac5228 --- /dev/null +++ b/bindings/python/py_cstest/src/py_cstest/compare.py @@ -0,0 +1,155 @@ +# Copyright © 2024 Rot127 +# SPDX-License-Identifier: BSD-3 + +import capstone +import logging as log +import re + + +def compare_asm_text(a_insn: capstone.CsInsn, expected: str, arch_bits: int) -> bool: + actual = f"{a_insn.mnemonic} {a_insn.op_str}" + actual = actual.strip() + actual = re.sub(r"\s+", " ", actual) + # Replace hex numbers with decimals + for hex_num in re.findall(r"0x[0-9a-fA-F]+", actual): + actual = re.sub(hex_num, f"{int(hex_num, base=16)}", actual) + # Replace negatives with twos-complement + for num in re.findall(r"\d+", actual): + actual = re.sub(num, f"{~(num % (1 << arch_bits)) + 1}", actual) + actual = actual.lower() + + if actual != expected: + log.error( + "Normalized asm-text doesn't match:\n" + f"decoded: '{actual}'\n" + f"expected: '{expected}'\n" + ) + return False + return True + + +def compare_str(actual: str, expected: str, msg: str) -> bool: + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_tbool(actual: bool, expected: int, msg: str) -> bool: + if expected == 0: + # Unset + return True + + if (expected < 0 and actual) or (expected > 0 and not actual): + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_uint8(actual: int, expected: int, msg: str) -> bool: + actual = actual & 0xFF + expected = expected & 0xFF + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_int8(actual: int, expected: int, msg: str) -> bool: + actual = actual & 0xFF + expected = expected & 0xFF + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_uint16(actual: int, expected: int, msg: str) -> bool: + actual = actual & 0xFFFF + expected = expected & 0xFFFF + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_int16(actual: int, expected: int, msg: str) -> bool: + actual = actual & 0xFFFF + expected = expected & 0xFFFF + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_uint32(actual: int, expected: int, msg: str) -> bool: + actual = actual & 0xFFFFFFFF + expected = expected & 0xFFFFFFFF + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_int32(actual: int, expected: int, msg: str) -> bool: + actual = actual & 0xFFFFFFFF + expected = expected & 0xFFFFFFFF + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_uint64(actual: int, expected: int, msg: str) -> bool: + actual = actual & 0xFFFFFFFFFFFFFFFF + expected = expected & 0xFFFFFFFFFFFFFFFF + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_int64(actual: int, expected: int, msg: str) -> bool: + actual = actual & 0xFFFFFFFFFFFFFFFF + expected = expected & 0xFFFFFFFFFFFFFFFF + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_fp(actual: float, expected: float, msg: str) -> bool: + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_enum(actual, expected, msg: str) -> bool: + enum_val = getattr(capstone, expected) + if not enum_val: + log.error(f"capstone package doesn't have the an attribute '{expected}'") + return False + if actual != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True + + +def compare_bit_flags(actual: int, expected: list[str], msg: str) -> bool: + for flag in expected: + enum_val = getattr(capstone, flag) + if not enum_val: + log.error(f"capstone package doesn't have the an attribute '{expected}'") + return False + if not actual & enum_val: + log.error(f"{msg}: In {actual:x} the flag {expected} isn't set.") + return False + return True + + +def compare_reg(handle: capstone.Cs, actual: int, expected: str, msg: str) -> bool: + if handle.reg_name(actual) != expected: + log.error(f"{msg}: {actual} != {expected}") + return False + return True diff --git a/bindings/python/py_cstest/src/py_cstest/cs_modes.py b/bindings/python/py_cstest/src/py_cstest/cs_modes.py new file mode 100644 index 0000000000..0290bca796 --- /dev/null +++ b/bindings/python/py_cstest/src/py_cstest/cs_modes.py @@ -0,0 +1,41 @@ +# Copyright © 2024 Rot127 +# SPDX-License-Identifier: BSD-3 + +import capstone as cs + +configs = { + "CS_OPT_DETAIL": {"type": cs.CS_OPT_DETAIL, "val": cs.CS_OPT_ON}, + "CS_OPT_DETAIL_REAL": { + "type": cs.CS_OPT_DETAIL, + "val": cs.CS_OPT_DETAIL_REAL | cs.CS_OPT_ON, + }, + "CS_OPT_SKIPDATA": {"type": cs.CS_OPT_SKIPDATA, "val": cs.CS_OPT_ON}, + "CS_OPT_UNSIGNED": {"type": cs.CS_OPT_UNSIGNED, "val": cs.CS_OPT_ON}, + "CS_OPT_NO_BRANCH_OFFSET": { + "type": cs.CS_OPT_NO_BRANCH_OFFSET, + "val": cs.CS_OPT_ON, + }, + "CS_OPT_SYNTAX_DEFAULT": { + "type": cs.CS_OPT_SYNTAX, + "val": cs.CS_OPT_SYNTAX_DEFAULT, + }, + "CS_OPT_SYNTAX_INTEL": {"type": cs.CS_OPT_SYNTAX, "val": cs.CS_OPT_SYNTAX_INTEL}, + "CS_OPT_SYNTAX_ATT": {"type": cs.CS_OPT_SYNTAX, "val": cs.CS_OPT_SYNTAX_ATT}, + "CS_OPT_SYNTAX_NOREGNAME": { + "type": cs.CS_OPT_SYNTAX, + "val": cs.CS_OPT_SYNTAX_NOREGNAME, + }, + "CS_OPT_SYNTAX_MASM": {"type": cs.CS_OPT_SYNTAX, "val": cs.CS_OPT_SYNTAX_MASM}, + "CS_OPT_SYNTAX_MOTOROLA": { + "type": cs.CS_OPT_SYNTAX, + "val": cs.CS_OPT_SYNTAX_MOTOROLA, + }, + "CS_OPT_SYNTAX_CS_REG_ALIAS": { + "type": cs.CS_OPT_SYNTAX, + "val": cs.CS_OPT_SYNTAX_CS_REG_ALIAS, + }, + "CS_OPT_SYNTAX_PERCENT": { + "type": cs.CS_OPT_SYNTAX, + "val": cs.CS_OPT_SYNTAX_PERCENT, + }, +} diff --git a/bindings/python/py_cstest/cstest.py b/bindings/python/py_cstest/src/py_cstest/cstest.py similarity index 98% rename from bindings/python/py_cstest/cstest.py rename to bindings/python/py_cstest/src/py_cstest/cstest.py index 017abe8b99..c0fc38f285 100755 --- a/bindings/python/py_cstest/cstest.py +++ b/bindings/python/py_cstest/src/py_cstest/cstest.py @@ -9,10 +9,11 @@ import os import yaml import capstone -import cs_modes from capstone import CsInsn, Cs, CS_ARCH_AARCH64, CS_MODE_64, CS_MODE_16 -from compare import ( + +from py_cstest.cs_modes import configs +from py_cstest.compare import ( compare_asm_text, compare_str, compare_tbool, @@ -185,8 +186,8 @@ def setup(self): if mode: new_mode |= mode continue - if "CS_OPT_" in opt and opt in cs_modes.configs: - mtype, val = cs_modes.configs[opt] + if "CS_OPT_" in opt and opt in configs: + mtype, val = configs[opt] self.handle.option(mtype, val) continue log.warning(f"Option: '{opt}' not used") @@ -452,7 +453,7 @@ def parse_args() -> argparse.Namespace: return arguments -if __name__ == "__main__": +def main(): log_levels = { "debug": logging.DEBUG, "info": logging.INFO, @@ -477,3 +478,7 @@ def parse_args() -> argparse.Namespace: log.addHandler(h1) log.addHandler(h2) CSTest(args.search_dir, args.exclude, args.include).run_tests() + + +if __name__ == "__main__": + main()