Mod for TikTok 47.1.3 (arm64) in Material 3 style: bottom bar, profile menu, TikTok settings and comments; downloads without the watermark; switches for ads, live streams and the Shop tab; badges of other mods next to names and the niche mod badge (members service); app icons; mod settings screen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
204 lines
7.2 KiB
Python
204 lines
7.2 KiB
Python
"""Native flag reads are routed through LihonTFlags, and LihonTFlags obeys the settings.
|
|
|
|
The smali rewriter is exercised on small synthetic methods. The runtime class is
|
|
compiled and run on a real JVM against the production settings classes; Android
|
|
itself is only on the classpath, as in test_settings_model.
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from lihont import profile_features
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SRC = ROOT / "src" / "lihont" / "two"
|
|
HARNESS = ROOT / "tests" / "java" / "lihont" / "two" / "FlagsTest.java"
|
|
HOOK = "Llihont/two/LihonTFlags;"
|
|
|
|
ALL_FLAGS = {**profile_features.PROFILE_FLAGS, **profile_features.REPOST_FLAGS}
|
|
|
|
INT_READ = """\
|
|
.class public Lcom/example/Reader;
|
|
.super Ljava/lang/Object;
|
|
|
|
.method public static read()I
|
|
.registers 3
|
|
|
|
const-string v0, "profile_left_align"
|
|
|
|
const/4 v1, 0x0
|
|
|
|
invoke-static {v0, v1}, Lcom/bytedance/ies/abmock/SettingsManager;->LIZ(Ljava/lang/String;I)I
|
|
|
|
move-result v2
|
|
|
|
return v2
|
|
.end method
|
|
"""
|
|
|
|
BOOL_READ = INT_READ.replace("(Ljava/lang/String;I)I", "(Ljava/lang/String;Z)Z").replace(
|
|
"read()I", "read()Z")
|
|
|
|
PARAMETER_READ = """\
|
|
.class public Lcom/example/Reader;
|
|
.super Ljava/lang/Object;
|
|
|
|
.method public run(I)I
|
|
.registers 5
|
|
|
|
const-string v0, "repost_support_digg"
|
|
|
|
invoke-static {v0, p1}, LX/0abc;->LIZ(Ljava/lang/String;I)I
|
|
|
|
move-result p1
|
|
|
|
return p1
|
|
.end method
|
|
"""
|
|
|
|
LEGACY = """\
|
|
.class public Lcom/example/Reader;
|
|
.super Ljava/lang/Object;
|
|
|
|
.method public static read()I
|
|
.registers 3
|
|
|
|
const-string v0, "profile_left_align"
|
|
|
|
const/4 v1, 0x0
|
|
|
|
invoke-static {v0, v1}, Lcom/bytedance/ies/abmock/SettingsManager;->LIZ(Ljava/lang/String;I)I
|
|
|
|
move-result v2
|
|
# lihont feature flag: profile_left_align
|
|
const/16 v2, 0x1
|
|
|
|
return v2
|
|
.end method
|
|
"""
|
|
|
|
|
|
class RewriteTest(unittest.TestCase):
|
|
def test_int_read_goes_through_the_hook(self):
|
|
text, hits = profile_features.rewrite_smali(INT_READ, ALL_FLAGS)
|
|
self.assertEqual(hits, {"profile_left_align": 1})
|
|
self.assertIn(
|
|
" move-result v2\n"
|
|
" # lihont feature flag: profile_left_align\n"
|
|
" invoke-static/range {v2 .. v2}, %s->profile_left_align(I)I\n"
|
|
" move-result v2\n" % HOOK, text)
|
|
self.assertNotIn("const/16", text)
|
|
self.assertIn("SettingsManager;->LIZ(Ljava/lang/String;I)I", text)
|
|
self.assertIn(" return v2\n", text)
|
|
|
|
def test_boolean_read_uses_the_boolean_overload(self):
|
|
text, hits = profile_features.rewrite_smali(BOOL_READ, ALL_FLAGS)
|
|
self.assertEqual(hits, {"profile_left_align": 1})
|
|
self.assertIn("%s->profile_left_align(Z)Z" % HOOK, text)
|
|
|
|
def test_parameter_register_is_kept(self):
|
|
text, hits = profile_features.rewrite_smali(PARAMETER_READ, ALL_FLAGS)
|
|
self.assertEqual(hits, {"repost_support_digg": 1})
|
|
self.assertIn("invoke-static/range {p1 .. p1}, %s->repost_support_digg(I)I" % HOOK, text)
|
|
self.assertIn(" move-result p1\n\n return p1\n", text)
|
|
|
|
def test_unknown_keys_are_left_alone(self):
|
|
text, hits = profile_features.rewrite_smali(
|
|
INT_READ.replace("profile_left_align", "some_other_flag"), ALL_FLAGS)
|
|
self.assertEqual(hits, {})
|
|
self.assertNotIn("LihonTFlags", text)
|
|
|
|
def test_rewriting_twice_changes_nothing(self):
|
|
once, _ = profile_features.rewrite_smali(INT_READ, ALL_FLAGS)
|
|
twice, hits = profile_features.rewrite_smali(once, ALL_FLAGS)
|
|
self.assertEqual(hits, {})
|
|
self.assertEqual(once, twice)
|
|
|
|
def test_old_const_override_is_replaced(self):
|
|
text, hits = profile_features.rewrite_smali(LEGACY, ALL_FLAGS)
|
|
self.assertEqual(hits, {"profile_left_align": 1})
|
|
self.assertNotIn("const/16", text)
|
|
self.assertEqual(text.count("# lihont feature flag"), 1)
|
|
self.assertIn("(I)I\n move-result v2\n\n return v2\n", text)
|
|
|
|
def test_marker_without_a_known_block_does_not_eat_code(self):
|
|
stray = INT_READ.replace(
|
|
" move-result v2\n",
|
|
" move-result v2\n # lihont feature flag: profile_left_align\n")
|
|
text, hits = profile_features.rewrite_smali(stray, ALL_FLAGS)
|
|
self.assertEqual(hits, {"profile_left_align": 1})
|
|
self.assertIn(" return v2\n", text)
|
|
|
|
|
|
class RuntimeTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.javac = shutil.which("javac")
|
|
cls.java = shutil.which("java")
|
|
if not cls.javac or not cls.java:
|
|
raise RuntimeError("Flag runtime tests require a JDK with java and javac")
|
|
cls.android_jar = ROOT / "tools" / "android.jar"
|
|
if not cls.android_jar.is_file():
|
|
raise RuntimeError("tools/android.jar must be available")
|
|
cls.temporary = tempfile.TemporaryDirectory(prefix="lihont-flags-tests-")
|
|
cls.addClassCleanup(cls.temporary.cleanup)
|
|
cls.classes = Path(cls.temporary.name) / "classes"
|
|
cls.classes.mkdir()
|
|
cls.environment = dict(os.environ)
|
|
cls.environment.pop("JAVA_TOOL_OPTIONS", None)
|
|
settings = SRC / "settings"
|
|
cls.run_command([
|
|
cls.javac, "--release", "17", "-encoding", "UTF-8",
|
|
"-cp", str(cls.android_jar), "-d", str(cls.classes),
|
|
str(settings / "ModSettings.java"), str(settings / "SettingsCodec.java"),
|
|
str(settings / "SettingsRepository.java"), str(settings / "AndroidSettingsStorage.java"),
|
|
str(settings / "SettingsHost.java"), str(SRC / "LihonTFlags.java"), str(HARNESS),
|
|
])
|
|
|
|
@classmethod
|
|
def run_command(cls, command):
|
|
result = subprocess.run(
|
|
command, capture_output=True, text=True, encoding="utf-8",
|
|
env=cls.environment, timeout=60,
|
|
)
|
|
if result.returncode:
|
|
raise AssertionError(
|
|
f"Command failed with exit {result.returncode}: {command[0]}\n"
|
|
+ result.stdout + result.stderr
|
|
)
|
|
return result
|
|
|
|
@staticmethod
|
|
def entries():
|
|
return (["banners:%s:%d" % item for item in profile_features.PROFILE_FLAGS.items()]
|
|
+ ["reposts:%s:%d" % item for item in profile_features.REPOST_FLAGS.items()])
|
|
|
|
def run_scenario(self, name):
|
|
result = self.run_command([
|
|
self.java, "-cp", str(self.classes) + os.pathsep + str(self.android_jar),
|
|
"lihont.two.FlagsTest", name,
|
|
] + self.entries())
|
|
return result.stdout.strip()
|
|
|
|
def test_each_switch_controls_only_its_own_flags(self):
|
|
self.assertEqual(self.run_scenario("matrix"), "PASS matrix")
|
|
|
|
def test_saved_change_applies_to_the_next_process(self):
|
|
self.assertEqual(self.run_scenario("snapshot"), "PASS snapshot")
|
|
|
|
def test_defaults_apply_while_settings_cannot_be_read(self):
|
|
self.assertEqual(self.run_scenario("unavailable"), "PASS unavailable")
|
|
|
|
def test_runtime_offers_exactly_what_the_patcher_calls(self):
|
|
offered = set(self.run_scenario("surface").splitlines())
|
|
called = {target.split("->", 1)[1] for target in profile_features.runtime_targets()}
|
|
self.assertEqual(offered, called)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|