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>
211 lines
8.4 KiB
Python
211 lines
8.4 KiB
Python
import unittest
|
|
|
|
from lihont import feed_patch as fp
|
|
|
|
AWEME = "Lcom/ss/android/ugc/aweme/feed/model/Aweme;"
|
|
|
|
FEED_PAGE = """\
|
|
.class public Lcom/ss/android/ugc/aweme/feed/model/FeedItemList;
|
|
.super Ljava/lang/Object;
|
|
|
|
.method public getItems()Ljava/util/List;
|
|
.registers 4
|
|
.annotation system Ldalvik/annotation/Signature;
|
|
value = {
|
|
"()",
|
|
"Ljava/util/List<",
|
|
"Lcom/ss/android/ugc/aweme/feed/model/Aweme;",
|
|
">;"
|
|
}
|
|
.end annotation
|
|
|
|
.prologue
|
|
.line 1
|
|
iget-object v0, p0, Lcom/ss/android/ugc/aweme/feed/model/FeedItemList;->items:Ljava/util/List;
|
|
|
|
return-object v0
|
|
.end method
|
|
|
|
.method public getPreloadAds()Ljava/util/List;
|
|
.registers 2
|
|
|
|
iget-object v0, p0, Lcom/ss/android/ugc/aweme/feed/model/FeedItemList;->preloadAds:Ljava/util/List;
|
|
|
|
return-object v0
|
|
.end method
|
|
|
|
.method public isHasAd()Z
|
|
.registers 2
|
|
|
|
iget-boolean v0, p0, Lcom/ss/android/ugc/aweme/feed/model/FeedItemList;->hasAd:Z
|
|
|
|
return v0
|
|
.end method
|
|
|
|
.method public setItems(Ljava/util/List;)V
|
|
.registers 4
|
|
|
|
iput-object p1, p0, Lcom/ss/android/ugc/aweme/feed/model/FeedItemList;->items:Ljava/util/List;
|
|
|
|
return-void
|
|
.end method
|
|
"""
|
|
|
|
MID_AD = """\
|
|
.class public final Lcom/ss/android/ugc/aweme/commercialize/feed/assem/midad/MidAdResponse;
|
|
.super Ljava/lang/Object;
|
|
|
|
.method public final getAweme()Lcom/ss/android/ugc/aweme/feed/model/Aweme;
|
|
.registers 2
|
|
|
|
iget-object v0, p0, Lcom/ss/android/ugc/aweme/commercialize/feed/assem/midad/MidAdResponse;->aweme:Lcom/ss/android/ugc/aweme/feed/model/Aweme;
|
|
|
|
return-object v0
|
|
.end method
|
|
"""
|
|
|
|
CANDIDATES = """\
|
|
.class public final Lcom/ss/android/ugc/aweme/commercialize/feed/candidate/CandidateRequestImpl;
|
|
.super Ljava/lang/Object;
|
|
|
|
.method public final LIZ(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
|
|
.registers 4
|
|
|
|
invoke-interface {v0}, Lcom/ss/android/ugc/aweme/commercialize/feed/candidate/CandidateAdRequestApi;->request(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)LX/12b3;
|
|
|
|
return-void
|
|
.end method
|
|
"""
|
|
|
|
TOP_VIEW = """\
|
|
.class public final Lcom/bytedance/ies/ugc/aweme/commercialize/splash/topview/TopViewJsonManager;
|
|
.super Ljava/lang/Object;
|
|
|
|
.method public final LIZIZ(Ljava/lang/String;)Lcom/ss/android/ugc/aweme/feed/model/Aweme;
|
|
.registers 5
|
|
|
|
invoke-virtual {p0, p1}, Lcom/bytedance/ies/ugc/aweme/commercialize/splash/topview/TopViewJsonManager;->LIZLLL(Ljava/lang/String;)Ljava/util/Map;
|
|
|
|
const/4 v0, 0x0
|
|
|
|
return-object v0
|
|
.end method
|
|
|
|
.method public final declared-synchronized LJ()Ljava/util/Map;
|
|
.registers 2
|
|
.annotation system Ldalvik/annotation/Signature;
|
|
value = {
|
|
"()",
|
|
"Ljava/util/Map<",
|
|
"Ljava/lang/String;",
|
|
"LX/0cWK;",
|
|
">;"
|
|
}
|
|
.end annotation
|
|
|
|
monitor-enter p0
|
|
|
|
iget-object v0, p0, Lcom/bytedance/ies/ugc/aweme/commercialize/splash/topview/TopViewJsonManager;->LJ:Ljava/util/concurrent/ConcurrentHashMap;
|
|
|
|
monitor-exit p0
|
|
|
|
return-object v0
|
|
.end method
|
|
"""
|
|
|
|
|
|
class HelperTest(unittest.TestCase):
|
|
def test_parameter_registers_count_this_and_wide_arguments(self):
|
|
self.assertEqual(1, fp._parameter_registers("public final x()V"))
|
|
self.assertEqual(0, fp._parameter_registers("public static x()V"))
|
|
self.assertEqual(4, fp._parameter_registers("public x(IJ)V"))
|
|
self.assertEqual(4, fp._parameter_registers("public x(Ljava/lang/String;[ILjava/util/List;)V"))
|
|
self.assertEqual(3, fp._parameter_registers("public static x(DZ)V"))
|
|
|
|
def test_a_local_is_made_when_the_method_has_none(self):
|
|
self.assertIn(".registers 3", fp._with_local("public x(I)V", " .registers 2\n return-void\n"))
|
|
self.assertIn(".registers 4", fp._with_local("public x(I)V", " .registers 4\n return-void\n"))
|
|
|
|
def test_the_first_instruction_comes_after_annotations_and_debug_lines(self):
|
|
body = (" .registers 4\n .annotation system Lx/Y;\n value = {\n }\n .end annotation\n\n"
|
|
" .prologue\n .line 3\n const/4 v0, 0x1\n")
|
|
self.assertEqual(" const/4 v0, 0x1\n", body[fp._first_instruction(body):])
|
|
|
|
|
|
class RewriteTest(unittest.TestCase):
|
|
def test_the_feed_page_is_cleaned_when_read_or_written(self):
|
|
patched, hits = fp.rewrite_feed_page(FEED_PAGE)
|
|
self.assertEqual({"items_read", "items_write", "preload_read", "ad_flag"}, set(hits))
|
|
self.assertEqual(4, patched.count(fp.MARKER))
|
|
self.assertEqual(4, patched.count(fp.CLEAN))
|
|
self.assertEqual(1, patched.count(fp.FILTER))
|
|
# the call goes after the annotation block and before the method's own code
|
|
items = patched.split(".method public getItems()")[1].split(".end method")[0]
|
|
self.assertLess(items.index(".end annotation"), items.index(fp.CLEAN))
|
|
self.assertLess(items.index(fp.CLEAN), items.index("iget-object v0"))
|
|
self.assertEqual(FEED_PAGE.count(".method "), patched.count(".method "))
|
|
|
|
def test_the_filtered_list_replaces_the_argument_of_set_items(self):
|
|
patched, _ = fp.rewrite_feed_page(FEED_PAGE)
|
|
setter = patched.split(".method public setItems(")[1]
|
|
self.assertLess(setter.index("move-result-object p1"), setter.index("iput-object p1"))
|
|
|
|
def test_the_mid_ad_response_ends_early_while_ads_are_off(self):
|
|
patched, hits = fp.rewrite_mid_ad(MID_AD)
|
|
self.assertEqual({"mid_ad": 1}, hits)
|
|
self.assertLess(patched.index(fp.ADS), patched.index("\n :lihont_keep\n"))
|
|
self.assertIn("const/4 v0, 0x0\n\n return-object v0", patched)
|
|
self.assertIn("iget-object v0, p0, Lcom/ss/android/ugc/aweme/commercialize/feed/assem/midad/MidAdResponse;->aweme", patched)
|
|
|
|
def test_the_candidate_request_is_not_sent_while_ads_are_off(self):
|
|
patched, hits = fp.rewrite_candidates(CANDIDATES)
|
|
self.assertEqual({"candidate_request": 1}, hits)
|
|
label = "\n :lihont_keep\n"
|
|
self.assertLess(patched.index("return-void"), patched.index(label))
|
|
self.assertLess(patched.index(label), patched.index("CandidateAdRequestApi"))
|
|
|
|
def test_the_top_view_store_is_empty_while_ads_are_off(self):
|
|
patched, hits = fp.rewrite_top_view(TOP_VIEW)
|
|
self.assertEqual({"top_view", "top_view_store"}, set(hits))
|
|
store = patched.split("declared-synchronized LJ()")[1]
|
|
self.assertLess(store.index(fp.ADS), store.index("monitor-enter p0"))
|
|
self.assertIn("new-instance v0, Ljava/util/HashMap;", store)
|
|
# LIZIZ(String) has p0 and p1 (two registers) and five in all: v0 is already a local there
|
|
self.assertIn(".registers 5", patched)
|
|
|
|
def test_a_method_without_a_local_gets_one(self):
|
|
tight = MID_AD.replace(".registers 2", ".registers 1").replace("iget-object v0, p0,", "iget-object p0, p0,").replace("return-object v0", "return-object p0")
|
|
patched, _ = fp.rewrite_mid_ad(tight)
|
|
self.assertIn(".registers 2", patched)
|
|
|
|
def test_a_second_pass_is_refused(self):
|
|
for rewrite, text in ((fp.rewrite_feed_page, FEED_PAGE), (fp.rewrite_mid_ad, MID_AD),
|
|
(fp.rewrite_candidates, CANDIDATES), (fp.rewrite_top_view, TOP_VIEW)):
|
|
with self.assertRaises(ValueError):
|
|
rewrite(rewrite(text)[0])
|
|
|
|
def test_a_class_that_changed_is_refused(self):
|
|
with self.assertRaises(ValueError):
|
|
fp.rewrite_feed_page(FEED_PAGE.replace("->items:Ljava/util/List;", "->list:Ljava/util/List;"))
|
|
with self.assertRaises(ValueError):
|
|
fp.rewrite_feed_page(FEED_PAGE.replace("isHasAd", "hasAnAd"))
|
|
with self.assertRaises(ValueError):
|
|
fp.rewrite_mid_ad(MID_AD.replace("->aweme:", "->other:"))
|
|
with self.assertRaises(ValueError):
|
|
fp.rewrite_candidates(CANDIDATES.replace("CandidateAdRequestApi", "Other"))
|
|
with self.assertRaises(ValueError):
|
|
fp.rewrite_top_view(TOP_VIEW.replace("->LJ:Ljava/util/concurrent/ConcurrentHashMap;", "->LJ:Ljava/util/Map;"))
|
|
with self.assertRaises(ValueError):
|
|
fp.rewrite_feed_page(MID_AD)
|
|
|
|
def test_every_rewrite_has_its_dex_and_runtime_calls(self):
|
|
self.assertEqual(set(fp.REWRITES), set(fp.DEXES))
|
|
both = "".join(fp.REWRITES[owner](text)[0] for owner, text in (
|
|
(fp.FEED_PAGE, FEED_PAGE), (fp.MID_AD, MID_AD), (fp.CANDIDATES, CANDIDATES), (fp.TOP_VIEW, TOP_VIEW)))
|
|
for target in fp.runtime_targets():
|
|
self.assertIn(target, both)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|