Skip to content

Commit 71d4869

Browse files
committed
feat(smalikit): add idempotent method append support and migrate HTMLViewer patch flow
SmaliKit enhancements: - Add new append_method argument to SmaliArgs in both runtime paths (src/utils and core modifiers) to keep argument contracts aligned. - Extend SmaliKit initialization to allow append-only operations without requiring method/seek filters. - Implement append_method processing that appends method blocks only when the signature is missing, preserving idempotency. - Expose append support in CLI via -am METHOD_SIGNATURE METHOD_BODY. HTMLViewer refactor: - Replace manual full-file regex rewrite in htmlviewer plugin with standard smali_patch flow. - Keep doInBackground rewrite in SmaliKit (method+return_type+remake) and add helper method through append_method. - Reuse shared _find_file helper from ApkModifierPlugin and remove plugin-local duplicate search logic. - Remove now-unused imports tied to manual regex patching. Tests: - Add tests/utils/test_smalikit.py covering append_method behavior for both missing-signature append and existing-signature no-op paths.
1 parent 2b48216 commit 71d4869

4 files changed

Lines changed: 123 additions & 35 deletions

File tree

src/core/modifiers/plugins/apk/htmlviewer.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""HTMLViewer modification plugin for EU ROMs."""
22

3-
import re
43
from pathlib import Path
5-
from typing import Optional
64

75
from src.core.modifiers.plugins.apk.base import ApkModifierPlugin, ApkModifierRegistry
86

@@ -35,7 +33,7 @@ def _apply_patches(self, work_dir: Path):
3533
self._patch_device_info_utils(work_dir)
3634

3735
def _patch_device_info_utils(self, work_dir: Path):
38-
smali_file = self._find_smali_file(work_dir, "MiuiDeviceInfoUtils$AsyncTask.smali")
36+
smali_file = self._find_file(work_dir, "MiuiDeviceInfoUtils$AsyncTask.smali")
3937

4038
if not smali_file:
4139
self.logger.warning("MiuiDeviceInfoUtils$AsyncTask.smali not found")
@@ -49,8 +47,7 @@ def _patch_device_info_utils(self, work_dir: Path):
4947

5048
self.logger.info("Patching MiuiDeviceInfoUtils$AsyncTask.smali...")
5149

52-
new_method = """.method protected varargs doInBackground([Landroid/util/Pair;)Ljava/lang/Object;
53-
.locals 16
50+
do_in_background_remake = """.locals 16
5451
.annotation system Ldalvik/annotation/Signature;
5552
value = {
5653
"([",
@@ -265,7 +262,6 @@ def _patch_device_info_utils(self, work_dir: Path):
265262
move-object/from16 v5, p0
266263
:goto_a
267264
return-object v4
268-
.end method
269265
"""
270266

271267
helper_method = """.method private getLocalizedValue(Ljava/lang/Object;)Ljava/lang/String;
@@ -314,20 +310,18 @@ def _patch_device_info_utils(self, work_dir: Path):
314310
.end method
315311
"""
316312

317-
old_method_pattern = (
318-
r"\.method protected varargs doInBackground\(\[Landroid/util/Pair;\)Ljava/lang/Object;.*?"
319-
r"\.end method"
313+
helper_signature = "getLocalizedValue(Ljava/lang/Object;)Ljava/lang/String;"
314+
self.smali_patch(
315+
work_dir,
316+
file_path=str(smali_file),
317+
method="doInBackground",
318+
return_type="Ljava/lang/Object;",
319+
remake=do_in_background_remake,
320+
)
321+
self.smali_patch(
322+
work_dir,
323+
file_path=str(smali_file),
324+
append_method=(helper_signature, helper_method),
320325
)
321326

322-
replacement = f"{new_method}{helper_method}"
323-
324-
content = re.sub(old_method_pattern, replacement, content, flags=re.DOTALL)
325-
326-
smali_file.write_text(content, encoding="utf-8")
327327
self.logger.info("Patched MiuiDeviceInfoUtils$AsyncTask.smali successfully")
328-
329-
def _find_smali_file(self, work_dir: Path, filename: str) -> Optional[Path]:
330-
for smali_dir in work_dir.glob("smali*"):
331-
for f in smali_dir.rglob(filename):
332-
return f
333-
return None

src/core/modifiers/smali_args.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ def __init__(self, **kwargs):
2020
self.insert_line = None
2121
self.recursive = False
2222
self.return_type = None
23+
self.append_method = None
2324

2425
self.__dict__.update(kwargs)

src/utils/smalikit.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env python3
2-
import os
3-
import sys
4-
import re
52
import argparse
63
import logging
4+
import os
5+
import re
6+
import sys
77

88

99
class SmaliArgs:
@@ -25,6 +25,7 @@ def __init__(self, **kwargs):
2525
self.insert_line = None
2626
self.recursive = False
2727
self.return_type = None
28+
self.append_method = None
2829

2930
# Override default values with passed keyword arguments
3031
self.__dict__.update(kwargs)
@@ -45,6 +46,7 @@ def __init__(self, args, logger=None):
4546
self.args = args
4647
self.target_method = args.method
4748
self.seek_keyword = args.seek_keyword
49+
self.append_method = getattr(args, "append_method", None)
4850
self.logger = logger or logging.getLogger("SmaliKit")
4951

5052
# 1. If -m is specified, method name match must be satisfied first
@@ -61,17 +63,46 @@ def __init__(self, args, logger=None):
6163
elif self.seek_keyword:
6264
method_name_pattern = r".*?"
6365

66+
# 3. Allow file-level append without method targeting
67+
elif self.append_method:
68+
method_name_pattern = None
69+
6470
else:
6571
self.logger.error("You must provide either -m (Method Name) or -seek (Keyword search)")
6672
sys.exit(1)
6773

6874
# Compile regex
69-
self.method_pattern = re.compile(
70-
r"(?P<header>^\s*\.method[^\n\r]*?\s%s[^\n\r]*)"
71-
r"(?P<body>.*?)"
72-
r"(?P<footer>^\s*\.end method)" % method_name_pattern,
73-
re.DOTALL | re.MULTILINE,
74-
)
75+
if method_name_pattern is None:
76+
self.method_pattern = None
77+
else:
78+
self.method_pattern = re.compile(
79+
r"(?P<header>^\s*\.method[^\n\r]*?\s%s[^\n\r]*)"
80+
r"(?P<body>.*?)"
81+
r"(?P<footer>^\s*\.end method)" % method_name_pattern,
82+
re.DOTALL | re.MULTILINE,
83+
)
84+
85+
def _apply_append_method(self, content):
86+
append_spec = self.append_method
87+
if not append_spec:
88+
return content, False
89+
90+
if not isinstance(append_spec, (tuple, list)) or len(append_spec) != 2:
91+
self.log("[ERROR] append_method must be a (signature, method_block) pair", Colors.FAIL)
92+
return content, False
93+
94+
method_signature, method_block = append_spec
95+
if not method_signature or not method_block:
96+
self.log("[ERROR] append_method signature/method_block cannot be empty", Colors.FAIL)
97+
return content, False
98+
99+
if method_signature in content:
100+
return content, False
101+
102+
normalized_block = method_block.replace("\\n", "\n").strip("\n")
103+
new_content = f"{content.rstrip()}\n\n{normalized_block}\n"
104+
self.log(f" -> [SUCCESS] Appended method: {method_signature}", Colors.OKGREEN)
105+
return new_content, True
75106

76107
def log(self, message, color=Colors.ENDC):
77108
# Log to the logger, stripping color codes if it's going to a file (optional)
@@ -159,10 +190,7 @@ def apply_modifications(self, original_body):
159190
return new_body, is_modified
160191

161192
def process_content(self, content, file_path):
162-
matches = list(self.method_pattern.finditer(content))
163-
164-
if not matches:
165-
return content, False
193+
matches = list(self.method_pattern.finditer(content)) if self.method_pattern else []
166194

167195
file_modified = False
168196
replacements = []
@@ -188,7 +216,7 @@ def process_content(self, content, file_path):
188216
)
189217

190218
if self.args.delete_method:
191-
self.log(f" -> Applying -dm (Delete Method)...", Colors.FAIL)
219+
self.log(" -> Applying -dm (Delete Method)...", Colors.FAIL)
192220
replacements.append((full_block, ""))
193221
file_modified = True
194222
continue
@@ -205,7 +233,8 @@ def process_content(self, content, file_path):
205233
for old, new in replacements:
206234
new_content = new_content.replace(old, new, 1)
207235

208-
return new_content, file_modified
236+
new_content, appended = self._apply_append_method(new_content)
237+
return new_content, file_modified or appended
209238

210239
def walk_and_patch(self, start_path):
211240
if os.path.isfile(start_path):
@@ -281,6 +310,13 @@ def main():
281310
parser.add_argument(
282311
"-ret", dest="return_type", help="Filter by Smali return type (e.g. Z, V, I)"
283312
)
313+
parser.add_argument(
314+
"-am",
315+
dest="append_method",
316+
nargs=2,
317+
metavar=("METHOD_SIGNATURE", "METHOD_BODY"),
318+
help="Append method if METHOD_SIGNATURE does not exist",
319+
)
284320

285321
args, unknown = parser.parse_known_args()
286322

tests/utils/test_smalikit.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import logging
2+
3+
from src.utils.smalikit import SmaliArgs, SmaliKit
4+
5+
6+
def test_append_method_when_missing():
7+
content = """
8+
.class public Lcom/example/Test;
9+
.super Ljava/lang/Object;
10+
11+
.method public test()V
12+
.locals 0
13+
return-void
14+
.end method
15+
""".strip()
16+
signature = "getLocalizedValue(Ljava/lang/Object;)Ljava/lang/String;"
17+
method_block = """
18+
.method private getLocalizedValue(Ljava/lang/Object;)Ljava/lang/String;
19+
.locals 1
20+
const-string v0, ""
21+
return-object v0
22+
.end method
23+
""".strip()
24+
25+
args = SmaliArgs(append_method=(signature, method_block))
26+
patcher = SmaliKit(args, logger=logging.getLogger("test.smalikit"))
27+
28+
new_content, patched = patcher.process_content(content, "Test.smali")
29+
30+
assert patched is True
31+
assert signature in new_content
32+
assert new_content.count(".method private getLocalizedValue") == 1
33+
34+
35+
def test_append_method_skips_when_exists():
36+
signature = "getLocalizedValue(Ljava/lang/Object;)Ljava/lang/String;"
37+
existing_method = """
38+
.method private getLocalizedValue(Ljava/lang/Object;)Ljava/lang/String;
39+
.locals 1
40+
const-string v0, ""
41+
return-object v0
42+
.end method
43+
""".strip()
44+
content = f"""
45+
.class public Lcom/example/Test;
46+
.super Ljava/lang/Object;
47+
48+
{existing_method}
49+
""".strip()
50+
51+
args = SmaliArgs(append_method=(signature, existing_method))
52+
patcher = SmaliKit(args, logger=logging.getLogger("test.smalikit"))
53+
54+
new_content, patched = patcher.process_content(content, "Test.smali")
55+
56+
assert patched is False
57+
assert new_content == content

0 commit comments

Comments
 (0)