From ab484a0350bf5d6d116862c6fcc3b87947b6fb42 Mon Sep 17 00:00:00 2001 From: maelstrom Date: Tue, 20 Aug 2024 00:58:17 +0200 Subject: [PATCH] Rewrote patcher.py --- .vscode/launch.json | 2 +- TODO.txt | 4 +- buildtool/polly/parser.py | 60 ++- buildtool/polly/patcher.py | 147 ++++--- buildtool/task/assemble_apk.py | 2 + buildtool/task/fileutil.py | 2 +- requirements.txt | 1 + src/patches/resource/ykit_settings.patch | 9 + src/resources/AndroidManifest.xml | 491 ----------------------- 9 files changed, 154 insertions(+), 564 deletions(-) create mode 100644 requirements.txt delete mode 100644 src/resources/AndroidManifest.xml diff --git a/.vscode/launch.json b/.vscode/launch.json index b180193..23e9f1d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "Python Debugger: Current File", "type": "debugpy", "request": "launch", - "program": "test.py", + "program": "buildtool/build.py", "console": "integratedTerminal" } ] diff --git a/TODO.txt b/TODO.txt index 9bf0ccc..dfcba47 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,2 +1,4 @@ Implement smali patches + migrate all smali code to patches -Implement Java stack \ No newline at end of file +Implement Java stack + +Fix weird bug where it sometimes thinks changes have been made even though there have not been, thus causing the APK to be reassambled unnecessarily. \ No newline at end of file diff --git a/buildtool/polly/parser.py b/buildtool/polly/parser.py index c46cb89..5475bed 100644 --- a/buildtool/polly/parser.py +++ b/buildtool/polly/parser.py @@ -1,12 +1,48 @@ +from pathlib import Path from lark import Lark from lark.lexer import Token from lark.tree import Branch +from dataclasses import dataclass + import os import re +@dataclass +class LineColumn: + line: int + column: int + +@dataclass +class CharIndex: + char: int + +type Location = LineColumn | CharIndex + +@dataclass +class InsertionAction: + content: str + location: Location + +@dataclass +class RegexReplaceAction: + ... + +type PatchAction = InsertionAction | RegexReplaceAction + +@dataclass +class Patch: + target: Path + actions: list[PatchAction] + +@dataclass +class PatchFile: + patches: list[Patch] + modified: float + def parse_string(token: Token): + assert isinstance(token.value, str) if token.type == 'STRING': string = token.value[1:-1] string = re.sub(r"\\n","\n", string) @@ -21,19 +57,19 @@ def parse_string(token: Token): string = re.match(re.compile(r"<<\s*(?P[^\n]+)\n(.*)\n(?P=terminator)", re.MULTILINE + re.DOTALL),token.value) assert string is not None string = string.group(2) - return string + return str(string) + else: + raise ValueError() def parse_location(location: Token): assert isinstance(location.value, str) - m = re.match(r"ln([0-9]+)(?:c([0-9]+))?", location.value) - if m: - ln, col = m.groups() - return {"type": "lncol", "line": int(ln), "column": int(col) if col else 0} + if location[:2] == "ln": + line, column = re.match(r"ln([0-9]+)(?:c([0-9]+))?", location.value).groups() + return LineColumn(int(line), int(column or '1')) - m = re.match(r"ch([0-9]+)", location.value) - if m: - ch = m.groups()[0] - return {"type": "char", "index": int(ch)} + if location[:2] == "ch": + char = re.match(r"ch([0-9]+)", location.value).groups()[0] + return CharIndex(int(char)) raise RuntimeError("Cannot parse location") @@ -41,15 +77,15 @@ def parse_patch(branch: Branch[Token], mtime: float): # First instruction is always file declaration target_file = parse_string(branch.children[0].children[0]) # pyright: ignore[reportUnknownMemberType, reportArgumentType] - actions = [] + actions: list[PatchAction] = [] for inst in branch.children[1:]: match inst.data: case "insert": - actions.append({"type": "insert", "at": parse_location(inst.children[0]), "content": parse_string(inst.children[1])}) + actions.append(InsertionAction(location=parse_location(inst.children[0]), content=parse_string(inst.children[1]))) # print(f"Inserting {parse_string(inst.children[1])} at {inst.children[0]} in {target_file}") - return {"target": target_file, "actions": actions, "timestamp": mtime} + return Patch(target=Path(target_file), actions=actions) def parse_patch_file(file: str): lark = Lark.open('grammar.lark', rel_to=__file__) diff --git a/buildtool/polly/patcher.py b/buildtool/polly/patcher.py index 85b2833..87a0c3e 100644 --- a/buildtool/polly/patcher.py +++ b/buildtool/polly/patcher.py @@ -1,72 +1,103 @@ -from .parser import parse_patch_file +from dataclasses import dataclass +from typing import Any +from .parser import CharIndex, InsertionAction, PatchAction, Location, parse_patch_file from pathlib import Path from task.util import * -def resolve_position(location, src: str): - if location["type"] == "char": - return location["index"] - elif location["type"] == "lncol": - j = 0 - for i, x in enumerate(src.split('\n')): - if (i+1) == location["line"]: - if location["column"] > len(x): - print(f"Cannot insert at line {location['line']} column {location['column']}. The line is not that long.") - exit(-1) - return j + location["column"] - j += len(x) + 1 - - print(f"Cannot insert at line {location['line']}. The file is not long enough.") - exit(-1) - -def read_insertion(action, src): - position = resolve_position(action["at"], src) - return {"content": action["content"], "pos": position} - -def process_patch_file(patch_file: str, check_date: bool = True): - patches = parse_patch_file(patch_file) +# Converts nebulous locations to real positions +def evaluate_position(content: str, location: Location): + if isinstance(location, CharIndex): + return location.char - for patch in patches: - target_src = EXTRACTED_DIR / patch["target"] - target_dest = PATCHED_RSC_DIR / patch["target"] + # Ineligent yet effective solution... + chars_read = 0 + + if location.line > len(content.split('\n')): + raise RuntimeError("Line number out of range") + + for i, line in enumerate(content.split('\n')): + if location.line == i + 1: + if location.column > len(line): + raise RuntimeError("Column number out of range") - # Skip old patches - if check_date and target_dest.exists() and target_dest.stat().st_mtime > patch["timestamp"]: - print(f"Skipping patch {patch['target']}...") + return chars_read + location.column - 1 + + chars_read += len(line) + 1 # Don't forget the newline character!!! + + raise RuntimeError("Line number out of range") + +@dataclass +class ProcessedInsertion: + position: int + content: str + patch_file_name: str + +type ProcessedAction = ProcessedInsertion + +def update_positions_of_patches(actions: list[ProcessedAction], at: int, by: int): + for action in actions: + if action.position < at: continue - print(f"Patching {patch['target']} with {len(patch['actions'])} actions") - - # Read the source of the file - with open(target_src, 'r') as f: - src = f.read() - - # Resolve the actual char locations in the patches - insertions = [read_insertion(action, src) for action in patch["actions"]] - insertions = [x for x in insertions if x is not None] - - for insertion in insertions: - src = src[:insertion["pos"]] + insertion["content"] + src[insertion["pos"]:] + action.position += by - # Correct insertions that follow this one - for i2 in insertions: - # Only update insertions that come *after* this one - if i2 == insertion or i2["pos"] < insertion["pos"]: - continue - - i2["pos"] += len(insertion["content"]) - - # Make parent dirs - target_dest.parent.mkdir(parents=True, exist_ok=True) - - # Write the changed output - with open(target_dest, 'w') as f: - f.write(src) - def process_all_patches(dir: Path | str, check_date: bool = True): dir = Path(dir) + patches: dict[Path, list[ProcessedAction]] = {} + + # First, we collect all actions from all patches in each patch file for patch_file in dir.rglob("*.patch"): - print("Processing patch {}".format(patch_file.relative_to(dir))) - process_patch_file(str(patch_file), check_date=check_date) \ No newline at end of file + pfrn = patch_file.relative_to(dir) + for patch in parse_patch_file(str(patch_file)): + patches[patch.target] = patches.get(patch.target, []) + src_file = EXTRACTED_DIR / patch.target + dest_file = PATCHED_RSC_DIR / patch.target + + for action in patch.actions: + + # Skip patch if it is older than the target file + if dest_file.exists() and src_file.stat().st_mtime <= dest_file.stat().st_mtime: + print(f"Skipped {pfrn}::{patch.target}") + continue + + with open(src_file, 'r') as f: + content = f.read() + + # We also consider the true position of each insertion, too + if isinstance(action, InsertionAction): + processed_action = ProcessedInsertion(position=evaluate_position(content, action.location), content=action.content, patch_file_name=str(pfrn)) + else: + raise RuntimeError("I don't know how to deal with those kinds of patches") + + patches[patch.target].append(processed_action) + + # Now we actually do all the patching + for target_file, actions in patches.items(): + src_file = EXTRACTED_DIR / target_file + dest_file = PATCHED_RSC_DIR / target_file + + with open(src_file, 'r') as f: + content = f.read() + + print(f"Processing patches for: {target_file}") + + for action in actions: + if isinstance(action, ProcessedInsertion): + # Update the positions of patches which modify after this one, so that their position isn't scrambled by this one + position = action.position + # update_positions_of_patches(actions, position, len(action.content)) + + # Update content + content = content[:position] + action.content + content[position:] + + print(f" |- {action.patch_file_name}") + else: + raise RuntimeError("I don't know how to deal with those kinds of patches") + + # Commit changes + dest_file.parent.mkdir(parents=True, exist_ok=True) + with open(dest_file, 'w') as f: + _ = f.write(content) \ No newline at end of file diff --git a/buildtool/task/assemble_apk.py b/buildtool/task/assemble_apk.py index ccff9b8..a354512 100644 --- a/buildtool/task/assemble_apk.py +++ b/buildtool/task/assemble_apk.py @@ -6,6 +6,8 @@ import subprocess def assemble_apk(): updated = merge_resources() + print(f"IS UPDATED: {updated}") + if not updated and Path(ASSEMBLED_APK).exists(): print("Skipping assembly") return False diff --git a/buildtool/task/fileutil.py b/buildtool/task/fileutil.py index 3654372..639cc4c 100644 --- a/buildtool/task/fileutil.py +++ b/buildtool/task/fileutil.py @@ -24,7 +24,7 @@ def merge_into(src: Path | str, dest: Path | str, check_date: bool = True, touch dest_file = dest / src_file.relative_to(src) # Don't update if dest is newer than source - if check_date and dest_file.exists() and src_file.stat().st_mtime < dest_file.stat().st_mtime: + if check_date and dest_file.exists() and src_file.stat().st_mtime <= dest_file.stat().st_mtime: continue dest_file.parent.mkdir(parents=True, exist_ok=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cf93603 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +lark==1.2.2 \ No newline at end of file diff --git a/src/patches/resource/ykit_settings.patch b/src/patches/resource/ykit_settings.patch index 2a13ec8..2269e8f 100644 --- a/src/patches/resource/ykit_settings.patch +++ b/src/patches/resource/ykit_settings.patch @@ -5,4 +5,13 @@ file 'res/layout/fragment_blog_settings.xml' insert ln10 << @end +@end + +# Registers the Ykit Settings activity + +file 'AndroidManifest.xml' + +insert ln489 << @end + + @end \ No newline at end of file diff --git a/src/resources/AndroidManifest.xml b/src/resources/AndroidManifest.xml deleted file mode 100644 index df80f0e..0000000 --- a/src/resources/AndroidManifest.xml +++ /dev/null @@ -1,491 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file