mirror of
https://github.com/MG1937/ASC.git
synced 2026-10-02 00:25:28 +08:00
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user