Merge pull request #26 from MG1937/dev-0.1.0

Dev 0.1.0
This commit is contained in:
MG193.7
2026-09-16 02:43:35 +08:00
committed by GitHub
4 changed files with 88 additions and 2 deletions
+1 -1
View File
@@ -421,7 +421,7 @@ class GuiDexStore:
"result_path": result_path,
}
cmd = [sys.executable, "-m", "src.asc_client.gui.search_worker"]
cmd = [sys.executable, "-m", "droidasc.asc_client.gui.search_worker"]
creationflags = 0
if os.name == "nt":
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
@@ -54,7 +54,16 @@ class InsnLocator(BaseLocator):
insns_re.append(b"[" + re.escape(bytes(insns[oplen])) + b"]" +
b"." * (oplen * 2 - 1)) # -1 for exclude opcode itself
# utilzes c-regex cap for fast matching, rather than performing linear matching in py
InsnLocator.INSN_VERIFY = re.compile(b"(?>(?:" + b"|".join(insns_re) + b")*)", re.DOTALL)
body = b"|".join(insns_re)
# The atomic group keeps the greedy star from backtracking over the instruction ranges
# that fail to verify, which is the common case on a real DEX. It is pure optimisation,
# but it is also Python 3.11+ syntax: pyproject declares requires-python >=3.10, where
# re.compile raises "unknown extension ?>" and every reference search and decompile dies
# at import. Keep it where the interpreter understands it, fall back otherwise.
try:
InsnLocator.INSN_VERIFY = re.compile(b"(?>(?:" + body + b")*)", re.DOTALL)
except re.error:
InsnLocator.INSN_VERIFY = re.compile(b"(?:" + body + b")*", re.DOTALL)
def _encoded_method_parse(self, data : bytes, pos, midx):
if pos == 0:
+13
View File
@@ -93,3 +93,16 @@ assert sys.modules['mutf8.cmutf8'].decode_modified_utf8.__module__ == '_asc_clie
self.assertEqual(store.get_source('Lexample/Test;'), first)
self.assertIn('class Test', first[1])
self.assertTrue(store.search_members('method', 'first'))
def test_gui_store_subprocess_search_uses_package_module(self):
from droidasc.asc_client.gui.runtime import GuiDexStore
with tempfile.TemporaryDirectory() as directory:
apk = Path(directory) / 'fixture.apk'
with zipfile.ZipFile(apk, 'w') as archive:
archive.writestr('classes.dex', make_dex())
store = GuiDexStore(str(apk), max_workers=1)
store.load()
result = store.search('string', 'token', max_workers=1)
self.assertEqual(result['backend'], 'subprocess')
self.assertEqual(result['total_hits'], 2)
self.assertEqual(len(result['results']), 2)
+64
View File
@@ -1,3 +1,4 @@
import re
import struct
import sys
import unittest
@@ -67,6 +68,69 @@ class ReferenceTests(unittest.TestCase):
self.assertTrue(any('->second' in line for line in lines))
class InsnVerifyPatternTests(unittest.TestCase):
"""INSN_VERIFY must compile on every Python the project declares support for."""
def setUp(self):
InsnLocator(DEX.parse(memoryview(make_dex()), 'fixture.dex'))
def test_pattern_compiles_on_the_declared_interpreter(self):
# pyproject declares requires-python >=3.10. The atomic group is Python 3.11+
# syntax, so on 3.10 re.compile raises "unknown extension ?>" and every
# reference search and decompile dies at import time.
self.assertIsNotNone(InsnLocator.INSN_VERIFY)
self.assertTrue(InsnLocator.INSN_VERIFY.match(b'\x0e\x00'))
def test_atomic_group_is_only_used_when_the_interpreter_supports_it(self):
try:
re.compile(b'(?>(?:a|b)*)')
supports_atomic = True
except re.error:
supports_atomic = False
self.assertEqual(b'(?>' in InsnLocator.INSN_VERIFY.pattern, supports_atomic)
def test_fallback_pattern_verifies_identically_without_atomic_groups(self):
# The atomic group only stops the greedy star from backtracking; it must not
# change which instruction runs verify. Rebuild both forms from the same opcode
# table and check they agree, so the 3.10 fallback is provably not a behaviour
# change. Skipped where the interpreter cannot compile the atomic form.
try:
probe = re.compile(b'(?>(?:a|b)*)')
except re.error:
self.skipTest('interpreter has no atomic groups; fallback is the only form')
self.assertIsNotNone(probe)
from droidasc.asc_core.models import dvm_opcode
buckets = {}
for opcode in dvm_opcode.opcodes:
buckets.setdefault(dvm_opcode.opcodes[opcode].oplen, set()).add(opcode)
seen = set()
for oplen, opcodes in buckets.items():
self.assertFalse(seen.intersection(opcodes),
f'oplen {oplen} shares an opcode with another length')
seen.update(opcodes)
self.assertEqual(len(seen), len(dvm_opcode.opcodes))
parts = [b'[' + re.escape(bytes(buckets[oplen])) + b']' + b'.' * (oplen * 2 - 1)
for oplen in sorted(buckets)]
body = b'|'.join(parts)
plain = re.compile(b'(?:' + body + b')*', re.DOTALL)
atomic = re.compile(b'(?>(?:' + body + b')*)', re.DOTALL)
# 0x0e 0x00 is return-void; 0x3e is not a dalvik opcode, so a range that
# contains it must fail to verify as a whole.
cases = [(b'\x0e\x00', b'\x0e\x00'),
(b'\x0e\x00\x0e\x00', b'\x0e\x00\x0e\x00'),
(b'\x0e\x00\x3e\x00', None)]
for data, expected in cases:
with self.subTest(data=data):
plain_match = plain.fullmatch(data)
atomic_match = atomic.fullmatch(data)
self.assertEqual(plain_match.group(0) if plain_match else None,
atomic_match.group(0) if atomic_match else None)
self.assertEqual(plain_match.group(0) if plain_match else None, expected)
class StringTests(unittest.TestCase):
def locate(self, data, query):
from droidasc.asc_core.findrefs.locator.string_locator import StringLocator