Commit 6f58abf1 authored by songjianyu's avatar songjianyu

[修复] 修复影响混淆的代码

parent ef6da0a8
...@@ -36,4 +36,7 @@ ...@@ -36,4 +36,7 @@
# 保持 Facebook SDK 的类和方法 # 保持 Facebook SDK 的类和方法
-keep class com.facebook.** { *; } -keep class com.facebook.** { *; }
-keep interface com.facebook.** { *; } -keep interface com.facebook.** { *; }
\ No newline at end of file -obfuscationdictionary dictionary.txt
-classobfuscationdictionary dictionary.txt
-packageobfuscationdictionary dictionary.txt
\ No newline at end of file
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
</provider> </provider>
<receiver <receiver
android:name=".push.receiver.AlarmReceiver" android:name=".business.push.receiver.AlarmReceiver"
android:enabled="true" android:enabled="true"
android:exported="true" android:exported="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED"> android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
......
...@@ -34,8 +34,8 @@ import com.easy.clean.business.ads.AdsMgr ...@@ -34,8 +34,8 @@ import com.easy.clean.business.ads.AdsMgr
import com.easy.clean.business.helper.NewComUtils.spConfig import com.easy.clean.business.helper.NewComUtils.spConfig
import com.easy.clean.business.push.fcm.FCMManager import com.easy.clean.business.push.fcm.FCMManager
import com.easy.clean.business.push.notification.MyNotificationManager import com.easy.clean.business.push.notification.MyNotificationManager
import com.easy.clean.business.push.receiver.ScreenStatusReceiver
import com.easy.clean.push.receiver.AlarmReceiver.Companion.startAlarm import com.easy.clean.push.receiver.AlarmReceiver.Companion.startAlarm
import com.easy.clean.push.receiver.ScreenStatusReceiver
import com.easy.clean.push.timer.TimerManager.Companion.changeTimer import com.easy.clean.push.timer.TimerManager.Companion.changeTimer
import com.easy.clean.push.work.RepeatingWorker.Companion.schedulePeriodicWork import com.easy.clean.push.work.RepeatingWorker.Companion.schedulePeriodicWork
import com.easy.clean.ui.batteryinfo.BatteryReceiver.Companion.registerBatteryReceiver import com.easy.clean.ui.batteryinfo.BatteryReceiver.Companion.registerBatteryReceiver
......
package com.easy.clean.push.receiver package com.easy.clean.business.push.receiver
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlarmManager import android.app.AlarmManager
import android.app.PendingIntent import android.app.PendingIntent
...@@ -12,7 +11,7 @@ import com.easy.clean.bean.push.NotificationSendBean ...@@ -12,7 +11,7 @@ import com.easy.clean.bean.push.NotificationSendBean
import com.easy.clean.bean.config.PopupConfigBean import com.easy.clean.bean.config.PopupConfigBean
import com.easy.clean.business.helper.EventUtils import com.easy.clean.business.helper.EventUtils
import com.easy.clean.business.push.notification.MyNotificationManager import com.easy.clean.business.push.notification.MyNotificationManager
import com.easy.clean.push.work.RepeatingWorker import com.easy.clean.business.push.work.RepeatingWorker
import com.easy.clean.utils.LogEx import com.easy.clean.utils.LogEx
import java.util.Calendar import java.util.Calendar
......
package com.easy.clean.push.receiver package com.easy.clean.business.push.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
......
package com.easy.clean.push.timer package com.easy.clean.business.push.timer
import android.util.Log import android.util.Log
import com.easy.clean.MyApplication import com.easy.clean.MyApplication
import com.easy.clean.bean.push.NotificationSendBean import com.easy.clean.bean.push.NotificationSendBean
import com.easy.clean.bean.push.NotificationSendBean.Companion.POPUP_WHERE_TIMER import com.easy.clean.bean.push.NotificationSendBean.Companion.POPUP_WHERE_TIMER
import com.easy.clean.bean.config.PopupConfigBean.Companion.popupConfigBean import com.easy.clean.bean.config.PopupConfigBean.Companion.popupConfigBean
import com.easy.clean.business.push.notification.MyNotificationManager import com.easy.clean.business.push.notification.MyNotificationManager
import com.easy.clean.push.receiver.ScreenStatusReceiver import com.easy.clean.business.push.receiver.ScreenStatusReceiver
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
......
package com.easy.clean.push.work package com.easy.clean.business.push.work
import android.content.Context import android.content.Context
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
......
...@@ -12,12 +12,12 @@ import com.easy.clean.databinding.DialogExitBinding ...@@ -12,12 +12,12 @@ import com.easy.clean.databinding.DialogExitBinding
class ExitDialog(val activity: Activity) { class ExitDialog(val activity: Activity) {
val dialog = AppCompatDialog(activity) val dialog = AppCompatDialog(activity)
val bind = DialogExitBinding.inflate(activity.layoutInflater) val binding = DialogExitBinding.inflate(activity.layoutInflater)
var clean: (() -> Unit)? = null var clean: (() -> Unit)? = null
var ad: (() -> Unit)? = null var ad: (() -> Unit)? = null
fun init() { fun init() {
dialog.setContentView(bind.root) dialog.setContentView(binding.root)
dialog.setCanceledOnTouchOutside(true) dialog.setCanceledOnTouchOutside(true)
val params = dialog.window?.attributes val params = dialog.window?.attributes
dialog.window?.attributes = params dialog.window?.attributes = params
...@@ -27,14 +27,14 @@ class ExitDialog(val activity: Activity) { ...@@ -27,14 +27,14 @@ class ExitDialog(val activity: Activity) {
} }
private fun initView() { private fun initView() {
bind.ivClose.setOnClickListener { binding.ivClose.setOnClickListener {
dialog.dismiss() dialog.dismiss()
} }
bind.tvExit.setOnClickListener { binding.tvExit.setOnClickListener {
dialog.dismiss() dialog.dismiss()
activity.finish() activity.finish()
} }
bind.tvClean.setOnClickListener { binding.tvClean.setOnClickListener {
dialog.dismiss() dialog.dismiss()
clean?.invoke() clean?.invoke()
} }
...@@ -44,9 +44,9 @@ class ExitDialog(val activity: Activity) { ...@@ -44,9 +44,9 @@ class ExitDialog(val activity: Activity) {
fun show() { fun show() {
dialog.show() dialog.show()
if (AdConfigBean.adsConfigBean.isAdShow) { if (AdConfigBean.adsConfigBean.isAdShow) {
AdsMgr.showNative(bind.flAd, R.layout.layout_admob_native_custom_r16) AdsMgr.showNative(binding.flAd, R.layout.layout_admob_native_custom_r16)
} else { } else {
bind.clAd.visibility = View.GONE binding.clAd.visibility = View.GONE
} }
} }
......
...@@ -46,8 +46,8 @@ class VideoCleanActivity : ...@@ -46,8 +46,8 @@ class VideoCleanActivity :
parent: ViewGroup, parent: ViewGroup,
viewType: Int, viewType: Int,
): VideoHolder { ): VideoHolder {
val bind = ItemVideoBinding.inflate(layoutInflater, parent, false) val binding = ItemVideoBinding.inflate(layoutInflater, parent, false)
return VideoHolder(bind) return VideoHolder(binding)
} }
override fun onBindViewHolder(holder: VideoHolder, position: Int) { override fun onBindViewHolder(holder: VideoHolder, position: Int) {
......
import json
import os
import random
import re
import shutil
from base64 import b64encode, b64decode
import cv2
import numpy as np
import regex as re2
# from Cryptodome.Cipher import AES
# from Cryptodome.Random import get_random_bytes
# import chardet
import string
import random
def detect_encoding(file_path):
with open(file_path, 'rb') as f:
raw_data = f.read(1024)
result = chardet.detect(raw_data)
return result['encoding']
def get_classes_impl(result, class_type, path='.'):
print("get_classes_impl start")
listdir = os.listdir(path)
for i in listdir:
print(i)
path_join = os.path.join(path, i)
if os.path.isdir(path_join):
if path.endswith('.') and i in ["build", "debug", "release"]:
continue
if path.endswith('src') and i in ["androidTest", "test"]:
continue
get_classes_impl(result, class_type, path_join)
elif os.path.isfile(path_join):
if os.path.splitext(i)[-1][1:] in class_type:
result.append(path_join)
print(result)
print("get_classes_impl over")
def get_classes(class_type=None):
print("get_classes start")
if class_type is None:
class_type = ["kt", "java"]
result = []
get_classes_impl(result, class_type)
print(result)
print("get_classes over")
return result
def find_text_impl(reg, result, skip_name, path, is_one):
listdir = os.listdir(path)
for i in listdir:
if is_one and len(result) > 0:
return
path_join = os.path.join(path, i)
if os.path.isdir(path_join):
if path.endswith('.') and i in ["build", "debug", "release"]:
continue
if path.endswith('src') and i in ["androidTest", "test"]:
continue
find_text_impl(reg, result, skip_name, path_join, is_one)
elif os.path.isfile(path_join):
if path.endswith('.') and i.split('.')[-1] in ['py']:
continue
is_skip = False
for j in skip_name:
if j in i:
is_skip = True
break
if is_skip:
continue
with open(path_join, 'r', encoding='utf-8') as f:
if not f.readable():
continue
try:
text = f.read()
except ValueError:
continue
findall = re.findall(reg, text)
if len(findall) > 0:
result.extend(findall)
def find_text(reg, path='.', is_one=False, skip_name=None):
if skip_name is None:
skip_name = []
result = []
if os.path.isdir(path):
find_text_impl(reg, result, skip_name, path, is_one)
else:
with open(path, 'r', encoding='utf-8') as f:
if f.readable():
try:
text = f.read()
findall = re.findall(reg, text)
if len(findall) > 0:
result.extend(findall)
except ValueError:
pass
if is_one:
if len(result) > 0:
return result[0]
else:
return None
return result
def find_file_impl(reg, result, skip_name, path='.'):
listdir = os.listdir(path)
for i in listdir:
if len(result) > 0:
return
path_join = os.path.join(path, i)
if os.path.isdir(path_join):
if path.endswith('.') and i in ["build", "debug", "release"]:
continue
if path.endswith('src') and i in ["androidTest", "test"]:
continue
find_file_impl(reg, result, skip_name, path_join)
elif os.path.isfile(path_join):
if path.endswith('.') and i not in ["pro"]:
continue
is_skip = False
for j in skip_name:
if j in i:
is_skip = True
break
if is_skip:
continue
with open(path_join, 'r', encoding='utf-8') as f:
if not f.readable():
continue
try:
text = f.read()
except ValueError:
continue
findall = re.findall(reg, text)
if len(findall) > 0:
result.append(path_join)
def find_file(reg, path='.', skip_name=None):
print(find_file)
if skip_name is None:
skip_name = []
result = []
if os.path.isdir(path):
find_file_impl(reg, result, skip_name, path)
else:
with open(path, 'r', encoding='utf-8') as f:
if f.readable():
try:
text = f.read()
findall = re.findall(reg, text)
if len(findall) > 0:
result.append(path)
except ValueError:
pass
return result[0] if len(result) > 0 else ""
def get_class_path(path):
with open(path, 'r') as f:
text = f.read()
package = re.search('package (.*?)\\s', text).group(1)
name = os.path.basename(path)
name = name[:name.rfind('.')]
return package + '.' + name
def replace_map_text(mapping, path='.'):
listdir = os.listdir(path)
for i in listdir:
path_join = os.path.join(path, i)
if os.path.isdir(path_join):
if path.endswith('.') and i in ["build", "debug", "release"]:
continue
if path.endswith('src') and i in ["androidTest", "test"]:
continue
replace_map_text(mapping, path_join)
elif os.path.isfile(path_join):
if path.endswith('.') and os.path.splitext(i)[-1][1:] not in ["pro"]:
continue
with open(path_join, 'r', encoding='utf-8') as f:
if not f.readable():
continue
try:
text = f.read()
except ValueError:
continue
with open(path_join, 'w', encoding='utf-8') as f:
reverse = sorted(list(mapping.keys()), key=len, reverse=True)
for name in reverse:
# text = text.replace(str(name), mapping[name])
text_result = re.sub(rf'\b{name}\b', mapping[name], text_result)
f.write(text)
# 修复某些字符无法匹配的问题,如android:OEM
def sub_map_text(mapping, is_reverse=True, path='.', skip_type=None, skip_name=None):
print("sub_map_text start")
if len(mapping) == 0:
return []
if skip_type is None:
skip_type = []
if skip_name is None:
skip_name = []
skip_name.append('databinding') # 跳过 databinding
result = []
sub_map_text_impl(mapping, result, skip_type, skip_name, is_reverse, path)
print(result)
print("sub_map_text over")
return result
# def sub_map_text_impl(mapping, result, skip_type, skip_name, is_reverse=True, path='.'):
# print("sub_map_text_impl start")
# listdir = os.listdir(path)
# for i in listdir:
# path_join = os.path.join(path, i)
# if os.path.isdir(path_join):
# if path.endswith('.') and i in ["build", "debug", "release"]:
# continue
# if path.endswith('src') and i in ["androidTest", "test"]:
# continue
# sub_map_text_impl(mapping, result, skip_type, skip_name, is_reverse, path_join)
# elif os.path.isfile(path_join):
# if path.endswith('.') and os.path.splitext(i)[-1][1:] not in ["pro"]:
# continue
# if os.path.splitext(i)[-1][1:] in skip_type:
# continue
# is_skip = False
# for j in skip_name:
# if j in i:
# print(j)
# is_skip = True
# break
# if is_skip:
# continue
# with open(path_join, 'r', encoding='utf-8') as f:
# if not f.readable():
# continue
# try:
# text = f.read()
# except ValueError:
# continue
# with open(path_join, 'w', encoding='utf-8') as f:
# reverse = sorted(list(mapping.keys()), key=len, reverse=is_reverse)
# text_result = text
# for name in reverse:
# text_result = re2.sub(str(name), mapping[name], text_result)
# print(text_result)
# if text_result != text:
# result.append(path_join)
# print(result)
# f.write(text_result)
# print("sub_map_text_impl over")
# 修复datbinding问题
def sub_map_text_impl(mapping, result, skip_type, skip_name, is_reverse=True, path='.'):
print("sub_map_text_impl start")
listdir = os.listdir(path)
for i in listdir:
path_join = os.path.join(path, i)
if os.path.isdir(path_join):
if path.endswith('.') and i in ["build", "debug", "release"]:
continue
if path.endswith('src') and i in ["androidTest", "test"]:
continue
sub_map_text_impl(mapping, result, skip_type, skip_name, is_reverse, path_join)
elif os.path.isfile(path_join):
if path.endswith('.') and os.path.splitext(i)[-1][1:] not in ["pro"]:
continue
if os.path.splitext(i)[-1][1:] in skip_type:
continue
is_skip = False
for j in skip_name:
if j in i:
print(j)
is_skip = True
break
if is_skip:
continue
# 开始读取文件内容
with open(path_join, 'r', encoding='utf-8') as f:
if not f.readable():
continue
try:
text = f.read()
except ValueError:
continue
# 替换逻辑
with open(path_join, 'w', encoding='utf-8') as f:
reverse = sorted(list(mapping.keys()), key=len, reverse=is_reverse)
text_result = text
patterns = sorted(mapping.keys(), key=len, reverse=is_reverse)
for pattern in patterns:
try:
print(f"正在替换 pattern: {pattern}")
text_result = re.sub(pattern, mapping[pattern], text_result)
except Exception as e:
print(f"替换失败: pattern={pattern}, 错误: {e}")
# # 遍历映射规则替换
# for name in reverse:
# text_result = re2.sub(str(name), mapping[name], text_result)
# 方案 3:针对特定 import 的直接替换
text_result = re.sub(
r'import varietyed\.scratched\.assemblyed\.(R|databinding\.\w+)',
r'import com.pots.qasdf.phonemanager.\1',
text_result
)
# 检查替换结果
if text_result != text:
result.append(path_join)
f.write(text_result)
print("sub_map_text_impl over")
def replace_text(old, new, path='.'):
print("replace_text")
listdir = os.listdir(path)
for i in listdir:
path_join = os.path.join(path, i)
if os.path.isdir(path_join):
if path.endswith('.') and i in ["build", "debug", "release"]:
continue
if path.endswith('src') and i in ["androidTest", "test"]:
continue
replace_text(old, new, path_join)
elif os.path.isfile(path_join):
if path.endswith('.') and os.path.splitext(i)[-1][1:] not in ["pro"]:
continue
with open(path_join, 'r', encoding='utf-8') as f:
if not f.readable():
continue
try:
text = f.read()
except ValueError:
continue
with open(path_join, 'w', encoding='utf-8') as f:
f.write(text.replace(old, new))
print(text)
def get_random_package():
packages = [get_random_string(4) for _ in range(get_random_int(1, 3))]
return '.'.join(packages)
def check_mapping():
print("check_mapping start")
try:
loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
except (ValueError, IOError):
loads = {}
old_mapping = loads.get('dir', {})
packages = find_text('package (.*?)[;]?\\s', 'src')
packages.append(applicationId)
packages = sorted(set(packages), key=packages.index)
packages.sort(key=len)
print(packages)
mapping = {}
for i in packages:
# 解决databingding包被混淆
if 'databinding' in i: # 跳过 databinding 包
mapping[i] = i
continue
if applicationId not in i:
continue
value = old_mapping.get(i)
if value is not None:
mapping[i] = value
continue
mapping[i] = get_random_package()
value = mapping.get(i[:i.rfind('.')])
if value is not None:
mapping[i] = value + '.' + mapping[i]
loads["dir"] = mapping
json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
print(json)
print("check_mapping over")
return mapping
def check_class_mapping():
print("check_class_mapping start")
try:
loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
print(loads)
except (ValueError, IOError):
loads = {}
old_mapping = loads.get('class', {})
mapping = {}
classes = get_classes()
for i in classes:
if '_D' in i or os.path.basename(i) == "GlobalConfig.kt": # 跳过 GlobalConfig.kt
continue
print(i)
name = os.path.basename(i)
name = name[:name.rfind('.')]
value = old_mapping.get(name)
print(value)
if value is not None:
mapping[name] = value
continue
mapping[name] = get_title(get_random_string(4), True)
print(mapping[name])
# method_variable_mapping = generate_method_variable_mapping(i) # 生成方法名和变量名的混淆映射
# for old, new in method_variable_mapping.items():
# mapping[old] = new # 添加方法名和变量名的混淆映射
loads["class"] = mapping
json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
print(json)
print("check_class_mapping over")
return mapping
# def generate_method_variable_mapping(class_file):
# # 生成方法名和变量名的混淆映射
# mapping = {}
#
# with open(class_file, 'r', encoding='utf-8') as f:
# text = f.read()
#
# methods = re.findall(
# r'\b(public|private|protected|static|final|abstract|synchronized)?\s*(<[^>]+>\s*)?(\w+)\s+(\w+)\s*\(',
# text
# )
# variables = re.findall(
# r'\b(public|private|protected|static|final|transient|volatile)?\s*(<[^>]+>\s*)?(\w+)\s+(\w+)\s*[;=]',
# text
# )
#
# for method in methods:
# old_name = method[3]
# new_name = get_random_string(6)
# mapping[old_name] = new_name
#
# for variable in variables:
# old_name = variable[3]
# new_name = get_random_string(6)
# mapping[old_name] = new_name
#
# return mapping
def check_type_ids_mapping(ids_type):
try:
loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
except (ValueError, IOError):
loads = {}
old_mapping = loads.get(ids_type + "_ids", {})
mapping = {}
type_ids = find_text('<' + ids_type + ' name="(.*?)".*?>', get_path('.\\src\\main\\res'))
for i in type_ids:
if i.endswith('_D'):
continue
value = old_mapping.get(i)
if value is not None:
mapping[i] = value
continue
mapping[i] = get_random_string(5, False)
type_ids = find_text('<item name="(.*?)".*? type="' + ids_type + '">', get_path('.\\src\\main\\res'))
for i in type_ids:
value = old_mapping.get(i)
if value is not None:
mapping[i] = value
continue
mapping[i] = get_random_string(5, False)
if len(mapping) != 0:
loads[ids_type + "_ids"] = mapping
json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
return mapping
def check_styleable_mapping():
try:
loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
except (ValueError, IOError):
loads = {}
old_mapping = loads.get("styleable", {})
mapping = {}
styleables = find_text('<declare-styleable name=".*?">[\\s\\S]*?</declare-styleable>')
for i in styleables:
styleable_id = re.findall('<declare-styleable name="(.*?)">', i)[0]
mapping[styleable_id] = old_mapping.get(styleable_id, get_random_string(5, False))
# styleables_attr = re.findall('<attr name="(.*?)".*?/>', i)
styleables_attr = re.findall('<attr name="(.*?)".*?>', i) # 包括单行和多行定义
for attr in styleables_attr:
mapping[attr + '_A'] = old_mapping.get(attr + '_A', get_random_string(5))
if len(mapping) != 0:
loads["styleable"] = mapping
json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
return mapping
def deal_ids_type(ids_type):
type_ids_mapping = check_type_ids_mapping(ids_type)
if len(type_ids_mapping) == 0:
return
print(type_ids_mapping)
sub_map = {}
for i in type_ids_mapping:
if i in type_ids_mapping and type_ids_mapping[i].startswith("android"):
continue
# mapped = type_ids_mapping[i]
#
# sub_map['<' + ids_type + ' name="' + i + '"'] = '<' + ids_type + ' name="' + mapped + '"'
# sub_map['<item name="' + i + '"(.*? type="' + ids_type + '")>'] = '<item name="' + mapped + '"\\g<1>>'
# sub_map[r'(?<=[^\.])R\.' + ids_type + r'\.' + re.escape(i) + r'(?=\W)'] = 'R.' + ids_type + '.' + mapped
# sub_map[r'(?<=>|")@' + ids_type + '/' + re.escape(i) + r'(?=<|")'] = '@' + ids_type + '/' + mapped
# sub_map[r'(?<=[@=:])@' + ids_type + '/' + re.escape(i) + r'(?=[}"\'\s])'] = '@' + ids_type + '/' + mapped
#
# # ✅ DataBinding 表达式中所有 @xxx/yyy(包括拼接字符串等复杂表达式)
# sub_map[r'(?<=@{[^}]*?)@' + ids_type + r'/' + re.escape(i) + r'(?=[^}]*})'] = '@' + ids_type + '/' + mapped
#
# # ✅ <style name="..." parent="xxx"> 资源引用
# sub_map[r'<' + ids_type + r'(.*?) parent="' + re.escape(i) + r'"(.*?)>'] = (
# r'<' + ids_type + r'\g<1> parent="' + mapped + r'"\g<2>>'
# )
sub_map['<' + ids_type + ' name="' + i + '"'] = '<' + ids_type + ' name="' + type_ids_mapping[i] + '"'
sub_map['<item name="' + i + '"(.*? type="' + ids_type + '")>'] = '<item name="' + type_ids_mapping[
i] + '"\\g<1>>'
sub_map['(?<=[^\\.])R\\.' + ids_type + '\\.' + i + '(?=\\W)'] = 'R.' + ids_type + '.' + type_ids_mapping[i]
sub_map['(?<=>|")@' + ids_type + '/' + i + '(?=<|")'] = '@' + ids_type + '/' + type_ids_mapping[i]
sub_map[r'(?<=[@=:])@' + ids_type + '/' + i + r'(?=[}"\'\s])'] = '@' + ids_type + '/' + type_ids_mapping[i]
sub_map[r'(?<=@{[^}]*?)@' + ids_type + r'/' + i + r'(?=[^}]*})'] = '@' + ids_type + '/' + type_ids_mapping[
i] # ⬅ 适配 data binding 表达式
sub_map['<' + ids_type + '(.*?) parent="' + i + '"(.*?)>'] = '<' + ids_type + '\\g<1> parent="' + \
type_ids_mapping[i] + '"\\g<2>>'
escaped_i = re.escape(i)
sub_map[rf'(?<![a-zA-Z0-9_/])@{ids_type}/{escaped_i}(?![a-zA-Z0-9_])'] = f'@{ids_type}/{type_ids_mapping[i]}'
# 带包名前缀的资源路径 com.retrytech.ledgeapp.R.color.xxx
# sub_map[rf'(\b[\w\.]+)\.R\.{ids_type}\.{re.escape(i)}\b'] = rf'\1.R.{ids_type}.{type_ids_mapping[i]}'
# sub_map[
# rf'(?<!android\.)(\b[\w\.]+)\.R\.{ids_type}\.{re.escape(i)}\b'] = rf'\1.R.{ids_type}.{type_ids_mapping[i]}'
sub_map[
rf'(?<!\bandroid\.)\b([\w\.]+)\.R\.{ids_type}\.{re.escape(i)}\b'] = rf'\1.R.{ids_type}.{type_ids_mapping[i]}'
# ✅ 推荐的替换逻辑(万能适配 DataBinding 场景)
# sub_map[f'@{ids_type}/{i}'] = f'@{ids_type}/{type_ids_mapping[i]}'
# # ✅ 适配 DataBinding 表达式中包括拼接、三目等形式的字符串资源引用
# sub_map[rf'(?<=@{{[^}}]*?)@{ids_type}/{re.escape(i)}(?=[^}}]*}})'] = f'@{ids_type}/{type_ids_mapping[i]}'
# # DataBinding 表达式中字符串拼接,如:@{@string/subscribe_to+" "+@string/pro}
# sub_map[r'(?<=@{[^}]*?)@' + ids_type + r'/' + re.escape(i) + r'(?=[^}]*})'] = '@' + ids_type + '/' + \
# type_ids_mapping[i]
# sub_map['<' + ids_type + ' name="' + i + '"'] = '<' + ids_type + ' name="' + type_ids_mapping[i] + '"'
# sub_map['<item name="' + i + '"(.*? type="' + ids_type + '")>'] = \
# '<item name="' + type_ids_mapping[i] + '"\\g<1>>'
# sub_map['(?<=[^\\.])R\\.' + ids_type + '\\.' + i + '(?=\\W)'] = 'R.' + ids_type + '.' + type_ids_mapping[i]
# sub_map['(?<=>|")@' + ids_type + '/' + i + '(?=<|")'] = '@' + ids_type + '/' + type_ids_mapping[i]
# sub_map['(?<=[@=:])@' + ids_type + '/' + i + r'(?=[}"\'\s])'] = '@' + ids_type + '/' + type_ids_mapping[i]
# sub_map['<' + ids_type + '(.*?) parent="' + i + '"(.*?)>'] = \
# '<' + ids_type + '\\g<1> parent="' + type_ids_mapping[i] + '"\\g<2>>'
# # 匹配 @color/xxx 出现在 @{} 表达式中,适配 data binding 多种情况
# sub_map[r'(?<=@{[^}]*?)@' + ids_type + r'/' + i + r'(?=[^}]*})'] = '@' + ids_type + '/' + type_ids_mapping[i]
sub_map[
rf'android\.R\.color\.(\w+)'] = lambda m: 'android.R.color.' + type_ids_mapping.get(m.group(1), m.group(1))
sub_map_text(sub_map)
def check_view_ids_mapping():
try:
loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
except (ValueError, IOError):
loads = {}
mapping = loads.get('view_ids', {})
view_ids = find_text('"\\@\\+id/(.*?)"', get_path('.\\src\\main\\res'))
for i in view_ids:
if i == 'root':
continue
key = get_title(i)
if mapping.get(key) is not None:
continue
mapping[key] = get_random_string(7)
loads["view_ids"] = mapping
json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
return mapping
def check_layout_mapping():
try:
loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
except (ValueError, IOError):
loads = {}
mapping = loads.get('layout', {})
layout_path = get_path('.\\src\\main\\res\\layout')
listdir = os.listdir(layout_path)
for i in listdir:
name = i[:i.rfind('.')]
if mapping.get(name) is not None:
continue
mapping[name] = get_random_string(6)
loads["layout"] = mapping
json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
return mapping
def check_res_mapping(res_type):
try:
loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
except (ValueError, IOError):
loads = {}
mapping = loads.get(res_type, {})
res_path = get_path('.\\src\\main\\res')
listdir = os.listdir(res_path)
for i in listdir:
if not i.startswith(res_type):
continue
for j in os.listdir(os.path.join(res_path, i)):
if j.startswith('book_'):
continue
if j.endswith('.9.png'):
name = j[:-len('.9.png')]
else:
name = j.rsplit('.', 1)[0]
if mapping.get(name) is not None:
continue
mapping[name] = normalize_filename(get_random_string(8))
if len(mapping) != 0:
loads[res_type] = mapping
json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
return mapping
used_words = set()
def get_random_string(length=8, is_ascii=True):
global used_words # 使用全局变量 used_words
words = load_words_from_file('word_file.json')
if words:
while words:
random_word = random.choice(words)
if random_word not in used_words:
used_words.add(random_word)
if is_ascii and all(ord(c) < 128 for c in random_word):
return random_word
elif not is_ascii:
return random_word
return None
def load_words_from_file(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get("word", []) # 返回单词列表
except FileNotFoundError:
print(f"Error: Word file not found at: {file_path}")
return []
except json.JSONDecodeError:
print(f"Error: Invalid JSON format in: {file_path}")
return []
def get_random_int(a=0, b=10000):
return random.randint(a, b)
def get_dictionary_string(length=10):
random_str = random.choice(string.ascii_letters)
random_str += ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length - 1))
return random_str
def check_obfuscation_dictionary():
if not os.path.exists('dictionary.txt'):
dictionary = set()
while len(dictionary) != 5000:
dictionary.add(get_dictionary_string())
with open('dictionary.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(dictionary))
with open('proguard-rules.pro', 'r', encoding='utf-8') as f:
text = f.read()
if '-obfuscationdictionary' in text:
return
text += '\n-obfuscationdictionary dictionary.txt'
text += '\n-classobfuscationdictionary dictionary.txt'
text += '\n-packageobfuscationdictionary dictionary.txt'
with open('proguard-rules.pro', 'w', encoding='utf-8') as f:
f.write(text)
def mkdir(dir_path):
try:
os.makedirs(dir_path)
except OSError:
pass
def replace_click_method():
print("replace_click_method start")
try:
path = get_path('.\\src\\main\\java')
file = find_file('fun View.setTrackedOnClickListener', path)
if file:
class_path = get_class_path(file) + '.setTrackedOnClickListener'
# java类不能直接使用扩展函数
sub_map_text({
'([.@])setOnClickListener': '\\g<1>setTrackedOnClickListener',
}, path=path, skip_type=['java'])
add_import(class_path, get_classes(['kt']))
print("replace_click_method over")
except IndexError:
return
def replace_package_in_imports(root_path, old_package, new_package):
# 匹配旧的 import 包名部分的正则表达式
patterns = [
(re.compile(rf'import {re.escape(old_package)}(\.databinding\.\w+)'), rf'import {new_package}\1'),
(re.compile(rf'import {re.escape(old_package)}\.R\b'), rf'import {new_package}.R'),
(re.compile(rf'import {re.escape(old_package)}\.BuildConfig'), rf'import {new_package}.BuildConfig'),
(re.compile(rf'{re.escape(old_package)}\.R\b'), rf'{new_package}.R'),
]
# 遍历目录中的所有文件
for dirpath, _, filenames in os.walk(root_path):
for filename in filenames:
# 只处理 .kt 和 .java 文件
if filename.endswith(('.kt', '.java')):
file_path = os.path.join(dirpath, filename)
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# 替换匹配到的旧包名
updated_content = content
for pattern, replacement in patterns:
updated_content = pattern.sub(replacement, updated_content)
# 如果内容发生了改变,写回文件
if content != updated_content:
with open(file_path, 'w', encoding='utf-8') as file:
file.write(updated_content)
print(f"已更新文件: {file_path}")
# def get_file_names_without_extension(root_path):
# # 遍历 root_path 下的所有 .kt 和 .java 文件
# all_files = []
# for dirpath, _, filenames in os.walk(root_path):
# for filename in filenames:
# if filename.endswith(('.kt', '.java')):
# all_files.append(os.path.join(dirpath, filename))
#
# return all_files
# def get_class_name(text, filename):
# """
# 获取文件中定义的类名(适用于 Java 和 Kotlin 文件)
# :param text: 文件内容
# :param filename: 文件名
# :return: 类名列表
# """
# if filename.endswith('.kt'):
# class_pattern = r'\bclass\s+(\w+)'
# elif filename.endswith('.java'):
# class_pattern = r'\bclass\s+(\w+)'
# else:
# return []
#
# # 获取类名
# return re.findall(class_pattern, text)
def get_method_names(text, filename):
"""
获取 Java 或 Kotlin 文件中的所有方法名
:param text: 文件内容
:param filename: 文件名
:return: 方法名列表
"""
if filename.endswith('.kt'):
# Kotlin 方法名:匹配包括访问修饰符(private/public)和 fun 关键字,不排除 override
method_pattern = r'\b(?:private|protected|public|internal|inline|suspend|)?\s*fun\s+(\w+)\s*\((.*?)\)\s*(?::\s*([\w<>,\s?*]*))?\s*{'
elif filename.endswith('.java'):
# Java 方法名:包括常见的修饰符和方法名
method_pattern = r'\b(?:public|private|protected|static|final|void|int|boolean|float|long|double|char|short|byte|void|[\w<>]+)\s+(\w+)\s*\(.*\)\s*\{'
else:
return []
# 匹配所有方法名
return re.findall(method_pattern, text)
def get_variable_names(text, filename):
"""
获取 Java 或 Kotlin 文件中的所有变量名
:param text: 文件内容
:param filename: 文件名
:return: 变量名列表
"""
if filename.endswith('.kt'):
# Kotlin 变量名:支持 `val` 和 `var` 关键字的变量声明
variable_pattern = r'\b(?:val|var)\s+(\w+)\s*(?=[;=])'
elif filename.endswith('.java'):
# Java 变量名:包括常见的类型声明
variable_pattern = r'\b(?:int|boolean|float|long|double|char|short|byte|String|[\w<>]+)\s+(\w+)\s*(?=[;=])'
else:
return []
# 匹配所有变量名
return re.findall(variable_pattern, text)
def get_file_names_without_extension(root_path):
"""
获取所有 `.kt` 和 `.java` 文件的路径(无扩展名)
:param root_path: 根目录路径
:return: 所有文件的路径列表
"""
all_files = []
for dirpath, _, filenames in os.walk(root_path):
for filename in filenames:
if filename.endswith(('.kt', '.java')):
all_files.append(os.path.join(dirpath, filename))
return all_files
# 存储每个文件名的映射关系
file_to_class_map = {}
def get_constant_names(text, filename):
"""
获取 Java 或 Kotlin 文件中的所有常量名
:param text: 文件内容
:param filename: 文件名
:return: 常量名列表
"""
if filename.endswith('.kt'):
# Kotlin 常量名:匹配以 `const val` 声明的常量
constant_pattern = r'const\s+val\s+(\w+)\s*=\s*.*'
elif filename.endswith('.java'):
# Java 常量名:匹配以 `static final` 声明的常量
constant_pattern = r'public\s+static\s+final\s+\w+\s+(\w+)\s*=\s*.*'
else:
return []
# 匹配所有常量名
return re.findall(constant_pattern, text)
def obfuscate_code_in_files(all_files):
"""
遍历所有文件,混淆其中的方法名、变量名和常量名,并返回混淆名称的映射。
:param all_files: 所有文件的路径列表。
:return: 一个字典,键为原始名称,值为混淆后的名称。
"""
name_mapping = {} # 用于存储原始名称和混淆名称的映射
constant_mapping = {} # 用于存储常量的映射
for file_path in all_files:
current_file_name = os.path.basename(file_path).split('.')[0] # 获取文件名
# 读取文件内容
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# 获取文件中的常量名
constants = get_constant_names("".join(lines), file_path)
# 为常量名生成混淆名称
for constant in constants:
if constant not in name_mapping:
name_mapping[constant] = get_random_string()
constant_mapping[constant] = name_mapping[constant]
# 获取类名(用于判断常量是否在类内引用)
new_lines = []
for line in lines:
if line.startswith("import "): # 跳过 import 行
new_lines.append(line)
else:
# 替换常量名:进行统一的替换,不区分引用的类型
for constant, obfuscated_name in constant_mapping.items():
# 直接替换常量,无论是在类内还是外部引用
line = re.sub(r'\b' + re.escape(constant) + r'\b', obfuscated_name, line)
# 替换方法名和变量名
for original_name, obfuscated_name in name_mapping.items():
line = re.sub(r'\b' + re.escape(original_name) + r'\b', obfuscated_name, line)
new_lines.append(line)
# 写回文件
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
return constant_mapping
def replace_with_mapped_names(all_files, name_mapping, constant_mapping):
"""
在所有文件中替换已经混淆的名称,保证不同文件间共享混淆名称。
:param all_files: 所有文件的路径列表。
:param name_mapping: 名称映射字典。
:param constant_mapping: 常量名称映射字典。
"""
for file_path in all_files:
# 使用在 obfuscate_code_in_files 中存储的文件名
current_file_name = file_to_class_map.get(file_path, None)
if not current_file_name:
print(f"未找到文件 {file_path} 的类名映射,跳过。")
continue
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
excluded_constants = set()
# 第一步:提取 `const val` 定义的常量名并排除混淆
for line in lines:
match = re.match(r'^\s*const\s+val\s+(\w+)\s*=.*$', line)
if match:
constant_name = match.group(1)
excluded_constants.add(constant_name)
for line in lines:
if line.startswith("import "): # 跳过 import 行
new_lines.append(line)
continue
# 跳过 context 或 activity 的调用
if 'context.' in line or 'activity.' in line:
new_lines.append(line)
continue
# 替换变量和方法名
for original_name, obfuscated_name in name_mapping.items():
if isinstance(original_name, tuple):
original_name = ''.join(original_name)
# 如果是 `excluded_constants` 中的常量名,跳过替换
if original_name in excluded_constants:
continue
# 替换代码中的变量或方法名
line = re.sub(r'\b' + re.escape(original_name) + r'\b', obfuscated_name, line)
new_lines.append(line)
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print(f"文件 {file_path} 引用的名称已进行统一混淆处理。")
def deal_code():
print("deal_code start")
# 替换点击事件
replace_click_method()
# 生成包映射文件
mapping = check_mapping()
packages = list(mapping.keys())
print("mapping: " + json.dumps(mapping, indent=4))
# 移动文件
root_path = get_path('.\\src\\main\\java')
for key in packages:
key = str(key)
print(key)
old_path = get_path(root_path + '\\' + key.replace('.', '\\'))
new_path = get_path(root_path + '\\' + mapping[key].replace('.', '\\'))
print("mapping[key]" + mapping[key])
if not os.path.exists(old_path):
mkdir(old_path)
if not os.path.exists(new_path):
mkdir(new_path)
listdir = os.listdir(old_path)
for i in listdir:
path_join = os.path.join(old_path, i)
if os.path.isdir(path_join):
continue
shutil.move(path_join, os.path.join(new_path, i))
shutil.rmtree(get_path(root_path + '\\' + applicationId[:applicationId.find('.')]))
# 修改manifest
with open(get_path('.\\src\\main\\AndroidManifest.xml'), 'r', encoding='utf-8') as f:
text = f.read()
findall = re.findall('android:name=(".*?")', text)
for i in findall:
if i.startswith('".'):
text = text.replace(i, '"' + applicationId + i[1:-1] + '"')
print(text)
print(applicationId)
with open(get_path('.\\src\\main\\AndroidManifest.xml'), 'w', encoding='utf-8') as f:
f.write(text)
# 修改依赖
sub_map = {}
for i in mapping:
sub_map['(?<=\\W)' + i + '(?=\\W)'] = mapping[i]
print(sub_map['(?<=\\W)' + i + '(?=\\W)'])
print(mapping[i])
# 过滤修改
sub_map['(?<=\\W)' + mapping[applicationId] + '.R(?=\\W)'] = applicationId + '.R'
sub_map['(?<=\\W)' + mapping[applicationId] + '.databinding(?=\\W)'] = applicationId + '.databinding'
sub_map['(?<=\\W)' + mapping[applicationId] + '.BuildConfig(?=\\W)'] = applicationId + '.BuildConfig'
sub_map_text(sub_map)
# 根包名下 R 单独处理
new_path = get_path(root_path + '\\' + mapping[packages[0]].replace('.', '\\'))
listdir = os.listdir(new_path)
result_path = []
for i in listdir:
if os.path.isdir(os.path.join(new_path, i)):
continue
result_path.append(os.path.join(new_path, i))
# add_import(applicationId + '.R', result_path)
# add_import(applicationId + '.BuildConfig', result_path)
# 类名
print("类名 start")
class_mapping = check_class_mapping()
classes = get_classes()
print(class_mapping)
for i in classes:
if '_D' in i or os.path.basename(i) == "GlobalConfig.kt": # 跳过 GlobalConfig.kt
continue
name = os.path.basename(i)
name = name[:name.rfind('.')]
shutil.move(i, i.replace(name, class_mapping[name]))
sub_map = {}
for i in class_mapping:
# 原规则匹配整个单词
# 添加负向环视以排除前后是引号的情况,解决匹配attrs的问题,
# 添加排除前缀 "styleable." 的逻辑
# 原规则匹配整个单词,添加排除前缀 "styleable." 的逻辑,并允许后面有引号的匹配
regex_pattern = r'(?<!")(?<!\bstyleable\.)\b' + i + r'\b'
if 'AppComponent' in class_mapping:
obfuscated_name = class_mapping['AppComponent']
dagger_pattern = rf'(?<!")(?<!\bstyleable\.)\bDaggerAppComponent\b'
sub_map[dagger_pattern] = f'Dagger{obfuscated_name}'
else:
print("⚠️ class_mapping 中没有 AppComponent,跳过 DaggerAppComponent 规则")
# 保持原有规则的匹配,优先处理非引号的情况
sub_map[regex_pattern] = class_mapping[i]
print(class_mapping[i])
sub_map_text(sub_map)
print("类名 over")
# 混淆字典
check_obfuscation_dictionary()
print(root_path)
print(mapping[applicationId])
print(applicationId)
# 获取混淆名称映射
# all_files = get_file_names_without_extension(root_path)
# name_mapping = obfuscate_code_in_files(all_files)
#
# # 获取常量映射
# constant_mapping = {} # 获取常量的映射
# for file_path in all_files:
# with open(file_path, 'r', encoding='utf-8') as f:
# lines = f.readlines()
# constants = get_constant_names("".join(lines), file_path)
# for constant in constants:
# if constant not in name_mapping:
# name_mapping[constant] = get_random_string()
# constant_mapping[constant] = name_mapping[constant]
#
# # 第一步:混淆代码,并获取所有混淆的名称映射
# name_mapping = obfuscate_code_in_files(all_files)
#
# # 第二步:替换其他文件中的引用,保证同样的混淆名
# replace_with_mapped_names(all_files, name_mapping, constant_mapping)
replace_package_in_imports(root_path, mapping[applicationId], applicationId)
def deal_res_type(res_type):
type_mapping = check_res_mapping(res_type)
if len(type_mapping) == 0:
return
print(type_mapping)
res_path = get_path('.\\src\\main\\res')
listdir = os.listdir(res_path)
for i in listdir:
if not i.startswith(res_type):
continue
path_join = os.path.join(res_path, i)
for j in os.listdir(path_join):
if j.startswith('book_'):
continue
if j.endswith('.9.png'):
name = j[:-len('.9.png')]
else:
name = j.rsplit('.', 1)[0]
path = os.path.join(path_join, j)
if '.DS_Store' not in path:
# shutil.move(path, path.replace(name, type_mapping[name]))
# 仅替换文件名部分
dir_path, file_name = os.path.split(path)
new_file_name = file_name.replace(name, type_mapping[name])
new_path = os.path.join(dir_path, new_file_name)
# 打印路径调试信息
print(f"源路径: {path}")
print(f"目标路径: {new_path}")
# 移动文件
shutil.move(path, new_path)
sub_map = {}
for i in type_mapping:
mapped = type_mapping[i]
# 普通 R.xxx.xxx
sub_map[r'R\.' + res_type + r'\.' + re.escape(i) + r'(\W)'] = r'R.' + res_type + '.' + mapped + r'\1'
# # # 包名限定的 com.retrytech.ledgeapp.R.drawable.xxx
# sub_map[r'(\b[\w\.]+)\.R\.' + res_type + r'\.' + re.escape(
# i) + r'(\W)'] = r'\1.R.' + res_type + '.' + mapped + r'\2'
# sub_map[r'(\b[\w\.]+)\.R\.' + res_type + r'\.' + re.escape(
# i) + r'(?=[\s)\];,])'] = r'\1.R.' + res_type + '.' + mapped
# ✅ 推荐统一写法:以单词边界结尾,适配更多结尾符号(避免重复)
sub_map[rf'(\b[\w\.]+)\.R\.{res_type}\.{re.escape(i)}\b'] = rf'\1.R.{res_type}.{mapped}'
# XML 资源:@drawable/xxx
sub_map[r'(>|")@' + res_type + r'/' + re.escape(i) + r'(<|")'] = r'\1@' + res_type + '/' + mapped + r'\2'
# DataBinding 表达式中:@{ condition ? @drawable/xxx :@drawable/yyy }
# # 匹配 ? 后的资源
# sub_map[r'(?<=\? *@' + res_type + r'\/)' + re.escape(i) + r'(?= *:)'] = mapped
# # 匹配 : 后的资源
# sub_map[r'(?<=: *@' + res_type + r'\/)' + re.escape(i) + r'(?= *})'] = mapped
# ? 后的资源
sub_map[r'(?<=\? *@' + res_type + r'/)' + re.escape(i) + r'(?=\b)'] = mapped
# : 后的资源
sub_map[r'(?<=: *@' + res_type + r'/)' + re.escape(i) + r'(?=\b)'] = mapped
# ✅ 通配 DataBinding 内的任意资源引用
sub_map[r'(@\{[^}]*?)@' + res_type + r'/' + re.escape(i) + r'(?=[^}]*})'] = r'\1@' + res_type + '/' + mapped
# 通用 DataBinding 表达式中任意位置(不拆三元)
# sub_map[r'(?<=@{[^}]*?)@' + res_type + r'/' + re.escape(i) + r'(?=[^}]*})'] = '@' + res_type + '/' + mapped
# sub_map['R\\.' + res_type + '\\.' + i + '(\\W)'] = 'R.' + res_type + '.' + type_mapping[i] + '\\g<1>'
# sub_map[
# r'(\b[\w\.]+)\.R\.' + res_type + r'\.' + i + r'(\W)'
# ] = r'\1.R.' + res_type + '.' + type_mapping[i] + r'\2'
#
# sub_map['(>|")@' + res_type + '/' + i + '(<|")'] = '\\g<1>@' + res_type + '/' + type_mapping[i] + '\\g<2>'
# # 适配 DataBinding 表达式中的 @drawable/xxx 写法
# sub_map[r'(?<=@{[^}]*?)@' + res_type + r'/' + i + r'(?=[^}]*})'] = '@' + res_type + '/' + type_mapping[i]
sub_map_text(sub_map)
# 解决mipmip命名中含有大写字母造成的报错
# 'E' is not a valid file-based resource name character: File-based resource names must contain only lowercase a-z, 0-9, or underscore
def generate_random_letter():
# 生成一个随机小写字母
return random.choice(string.ascii_lowercase)
def normalize_filename(name):
# 将名称转为小写
normalized_name = name.lower()
# 替换所有非法字符为随机字母
normalized_name = re.sub(r'[^a-z]', lambda match: generate_random_letter(), normalized_name)
# 确保名称以字母开头,如果不是,则以随机字母开头
if not normalized_name[0].isalpha():
normalized_name = generate_random_letter() + normalized_name[1:]
return normalized_name
def add_image_noise(path):
try:
image = cv2.imread(path, cv2.IMREAD_UNCHANGED)
if path.endswith('.9.png'):
return
# # 添加随机噪声
# noise = np.random.randint(0, 2, image.shape, np.uint8)
# image = cv2.add(image, noise)
# 添加 ±10 的随机噪声
noise = np.random.randint(0, 2, image.shape, np.int16) # 噪声范围 [-10, 10]
noisy_image = np.clip(image.astype(np.int16) + noise, 0, 255).astype(np.uint8) # 防止溢出
#计算原始图像与添加噪声后图像的差异:如果差值的范围包含 10(噪声范围),说明噪声添加成功。
difference = np.abs(image.astype(np.int16) - noisy_image.astype(np.int16))
print("Difference range:", difference.min(), "-", difference.max())
new_path = path.rsplit('.', 1)[0] + '.webp'
cv2.imwrite(new_path, noisy_image, [cv2.IMWRITE_WEBP_QUALITY, 75])
if new_path != path:
os.remove(path)
except AttributeError:
return
def deal_image():
res_path = get_path('.\\src\\main\\res')
listdir = os.listdir(res_path)
for i in listdir:
path_join = os.path.join(res_path, i)
if os.path.isdir(path_join):
for j in os.listdir(path_join):
# add_image_noise(os.path.join(path_join, j))
if j.endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.webp')):
add_image_noise(os.path.join(path_join, j))
def get_title(string, is_all_upper=False):
splits = string.split('_')
print(splits)
s = ''
for i in range(len(splits)):
if i == 0 and not is_all_upper:
s = splits[i]
elif len(splits[i]) > 0:
s += splits[i][0].upper() + splits[i][1:]
return s
def add_import(path, listfile):
for i in listfile:
if 'databinding' in path: # 跳过 databinding
continue
with open(i, 'r', encoding='utf-8') as f:
text = f.read()
if len(re.findall('import ' + path, text)) != 0:
continue
if i.endswith('kt'):
text = re.sub('(package \\S+?\\s+?)(?=\\S)', '\\g<1>import ' + path + '\n', text, flags=re.S)
print(text)
elif i.endswith('java'):
text = re.sub('(package \\S+?\\s+?)(?=\\S)', '\\g<1>import ' + path + ';\n', text, flags=re.S)
with open(i, 'w', encoding='utf-8') as f:
f.write(text)
def check_class_string_mapping():
try:
loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
except (ValueError, IOError):
loads = {}
mapping = loads.get('class_string', {})
classes = get_classes(['kt'])
for path in classes:
if os.path.basename(path).startswith('AESHelper'):
continue
with open(path, 'r', encoding='utf-8') as f:
text = f.read()
text = re.sub('@SuppressLint.*?\n', '\n', text)
text = re.sub('//[^"]*?(("[^"]*?){2})*?\n', '\n', text)
text = re.sub(' +\n', '\n', text)
text = re.sub('=\\s+', '= ', text)
text = re.sub(':\\s+', ': ', text)
text = re.sub(',\\s+', ', ', text)
text = re.sub('\n{2,}', '\n\n', text)
result_text = text
strings = re.findall('".*?[^\\\\]"', re.sub(r'@[^)]*', lambda x: x.group().replace('"', "'"), result_text))
last_string = ''
for string in strings:
if len(string) <= 4 or string.isspace() or \
string[1:-1] in ['\\\\n', '\\\\r', '\\\\\'', '\\\\\\"', '\\\\?', '&amp;', 'UTF-8']:
continue
if string in last_string:
continue
start = text.find(string)
if start == -1:
continue
index = start + 1
sign_stack = ['"']
while len(sign_stack) != 0:
if text[index] == '\\':
index += 1
elif text[index] == '"' and sign_stack[-1] == '"':
sign_stack.pop()
elif text[index] == '"':
sign_stack.append('"')
elif text[index] == '}' and sign_stack[-1] == '${':
sign_stack.pop()
elif text[index - 1:index + 1] == '${':
sign_stack.append('${')
index += 1
string = text[start:index]
last_string = string
params = []
result = string
if '$' in string:
count = 0
last = -1
res = []
for i in range(len(string)):
if last == -1 and string[i] == '$':
if count == 0:
last = i
elif last != -1:
if string[i] == '{':
count += 1
elif string[i] == '}':
count -= 1
if count == 0:
res.append((last, i + 1))
last = -1
elif count == 0:
last = -1
for i in re.findall('\\$\\w*', string):
if i == '$':
continue
find = string.find(i)
res.append((find, find + len(i)))
res = sorted(res, key=lambda l: l[0])
last_res = None
for i in res:
if last_res is not None and i[0] < last_res[1]:
continue
params.append(string[i[0]:i[1]])
last_res = i
for i in params:
result = result.replace(i, "[str]")
result = result[1:-1].replace('%', '%%').replace('\\\\', '\\').replace('[str]', '%s')
if mapping.get(result) is not None:
continue
mapping[result] = get_random_string(5, False) + '_D'
loads['class_string'] = mapping
json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
return mapping
def deal_class_string():
# 获取映射
string_mapping = check_class_string_mapping()
print(string_mapping)
with open(get_path('.\\src\\main\\res\\values\\strings.xml'), 'r', encoding='utf-8') as f:
text = f.read()
rfind = text.rfind('\n')
text_result = text[:rfind]
for i in string_mapping:
string = i
string = string.replace('&', '&amp;')
string = string.replace('?', '\\?')
string = string.replace('\'', '\\\'')
text_result += '\n <string name="' + string_mapping[i] + '" translatable="false">' + string + '</string>'
text_result += text[rfind:]
with open(get_path('.\\src\\main\\res\\values\\strings.xml'), 'w', encoding='utf-8') as f:
f.write(text_result)
# 搜索 Int.string() 路径
file_path = find_file('fun Int.string', get_path('.\\src\\main\\java'))
class_path = get_class_path(file_path) + '.string' if file_path else ""
classes = get_classes(['kt'])
for path in classes:
# 跳过 AESHelper 类中的字符串,避免嵌套
if os.path.basename(path).startswith('AESHelper'):
continue
with open(path, 'r', encoding='utf-8') as f:
text = f.read()
text = re.sub('@SuppressLint.*?\n', '\n', text)
text = re.sub('//[^"]*?(("[^"]*?){2})*?\n', '\n', text)
text = re.sub(' +\n', '\n', text)
text = re.sub('=\\s+', '= ', text)
text = re.sub(':\\s+', ': ', text)
text = re.sub(',\\s+', ', ', text)
text = re.sub('\n{2,}', '\n\n', text)
result_text = text
strings = re.findall('".*?[^\\\\]"', re.sub(r'@[^)]*', lambda x: x.group().replace('"', "'"), result_text))
last_string = ''
for string in strings:
if len(string) <= 4 or string.isspace() or \
string[1:-1] in ['\\\\n', '\\\\r', '\\\\\'', '\\\\\\"', '\\\\?', '&amp;', 'UTF-8']:
continue
if string in last_string:
continue
start = text.find(string)
if start == -1:
continue
index = start + 1
sign_stack = ['"']
while len(sign_stack) != 0:
if text[index] == '\\':
index += 1
elif text[index] == '"' and sign_stack[-1] == '"':
sign_stack.pop()
elif text[index] == '"':
sign_stack.append('"')
elif text[index] == '}' and sign_stack[-1] == '${':
sign_stack.pop()
elif text[index - 1:index + 1] == '${':
sign_stack.append('${')
index += 1
# 得到完整字符串
string = text[start:index]
last_string = string
# 参数
params = []
result = string
# 处理字符串拼接
if '$' in string:
count = 0
last = -1
res = []
for i in range(len(string)):
if last == -1 and string[i] == '$':
if count == 0:
last = i
elif last != -1:
if string[i] == '{':
count += 1
elif string[i] == '}':
count -= 1
if count == 0:
res.append((last, i + 1))
last = -1
elif count == 0:
last = -1
for i in re.findall('\\$\\w*', string):
if i == '$':
continue
find = string.find(i)
res.append((find, find + len(i)))
res = sorted(res, key=lambda l: l[0])
last_res = None
for i in res:
if last_res is not None and i[0] < last_res[1]:
continue
params.append(string[i[0]:i[1]])
last_res = i
for i in params:
result = result.replace(i, "[str]")
for i in range(len(params)):
if params[i][1] == '{':
params[i] = params[i][2:-1]
else:
params[i] = params[i][1:]
# 每个参数加上 toString
params[i] = '(' + params[i] + ').toString()'
# 拼接部分替换为 %s
result = result[1:-1].replace('%', '%%').replace('\\\\', '\\').replace('[str]', '%s')
# 添加 Int.string() 方法引用
if class_path:
if len(re.findall('import ' + class_path, text)) == 0:
result_text = re.sub('(package \\S+?\\s+?)(?=\\S)',
'\\g<1>import ' + class_path + '\n', result_text, flags=re.S)
# 添加资源 R 引用
if len(re.findall('import ' + applicationId + '.R', text)) == 0:
result_text = re.sub('(package \\S+?\\s+?)(?=\\S)',
'\\g<1>import ' + applicationId + '.R' + '\n', result_text, flags=re.S)
# 去除 const
result_text = re.sub('const (val.*?R\\.string\\.\\S+?\\.string\\(\\))', '\\g<1>', result_text)
with open(path, 'w', encoding='utf-8') as f:
f.write(result_text)
def check_xml_string_mapping():
try:
loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
except (ValueError, IOError):
loads = {}
old_mapping = loads.get('xml_string', {})
strings = find_text('android:text="(?<!@string/)([^@"]+?)"', get_path('.\\src\\main\\res\\layout'),
skip_name=['notify', 'notity'])
mapping = {}
for i in strings:
value = old_mapping.get(i)
if value is not None:
mapping[i] = value
continue
# 添加 '_D' 结尾,资源混淆时过滤不用再次改名
mapping[i] = get_random_string(5, False) + '_D'
loads['xml_string'] = mapping
json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
return mapping
def deal_xml_string():
# 获取映射
string_mapping = check_xml_string_mapping()
print(string_mapping)
with open(get_path('.\\src\\main\\res\\values\\strings.xml'), 'r', encoding='utf-8') as f:
text = f.read()
rfind = text.rfind('\n')
text_result = text[:rfind]
for i in string_mapping:
string = i
string = string.replace('&', '&amp;')
string = string.replace('?', '\\?')
string = string.replace('\'', '\\\'')
text_result += '\n <string name="' + string_mapping[i] + '" translatable="false">' + string + '</string>'
text_result += text[rfind:]
with open(get_path('.\\src\\main\\res\\values\\strings.xml'), 'w', encoding='utf-8') as f:
f.write(text_result)
sub_map = {}
for i in string_mapping:
sub_map['(?<=android:text=")' + re.escape(i) + '(?=")'] = '@string/' + re.escape(string_mapping[i])
# 替换时跳过通知布局
sub_map_text(sub_map, path=get_path('.\\src\\main\\res\\layout'), skip_name=['notify', 'notity'])
def deal_code_string():
# 处理布局文件中的明文字符串
deal_xml_string()
# 处理代码文件中的明文字符串
deal_class_string()
def deal_res():
# 改 string id
deal_ids_type('string')
# 改 color id
deal_ids_type('color')
# 改 dimen id
deal_ids_type('dimen')
# 改 style id
deal_ids_type('style')
# 改 declare-styleable
styleable_mapping = check_styleable_mapping()
print(styleable_mapping)
sub_map = {}
styleables = find_text('<declare-styleable name=".*?">[\\s\\S]*?</declare-styleable>')
for i in styleables:
result = i
styleable_id = re.findall('<declare-styleable name="(.*?)">', i)[0]
result = result.replace('<declare-styleable name="' + styleable_id + '">',
'<declare-styleable name="' + styleable_mapping[styleable_id] + '">')
sub_map['R\\.styleable\\.' + styleable_id + '(\\W)'] = \
'R.styleable.' + styleable_mapping[styleable_id] + '\\g<1>'
# styleables_attr = re.findall('<attr name="(.*?)".*?/>', i)
styleables_attr = re.findall('<attr name="(.*?)".*?>', i) # 包括单行和多行定义
for attr in styleables_attr:
# else:
# new_attr = attr
result = result.replace('<attr name="' + attr + '"', '<attr name="' + styleable_mapping[attr + '_A'] + '"')
sub_map['R\\.styleable\\.' + styleable_id + '_' + attr + '(\\W)'] = \
'R.styleable.' + styleable_mapping[styleable_id] + '_' + styleable_mapping[attr + '_A'] + '\\g<1>'
sub_map['app:' + attr + '='] = 'app:' + styleable_mapping[attr + '_A'] + '='
sub_map['\\?attr/' + attr] = '?attr/' + styleable_mapping[attr + '_A'] # 新增的替换规则
sub_map['<item name=\"' + attr + '\"'] = '<item name=\"' + styleable_mapping[attr + '_A'] + '\"' # 新增的替换规则
# 判断 attr 是否以 "android:" 开头
if attr.startswith("android:"):
new_attr = "android_" + attr[8:] # 去掉 "android:" 并替换为 "android_"
sub_map['R\\.styleable\\.' + styleable_id + '_' + new_attr + '(\\W)'] = \
'R.styleable.' + styleable_mapping[styleable_id] + '_' + styleable_mapping[attr + '_A'] + '\\g<1>'
sub_map[re.escape(i)] = result
sub_map_text(sub_map)
# 改 view id
view_ids = find_text('"\\@\\+id/(.*?)"', get_path('.\\src\\main\\res'))
view_ids_mapping = check_view_ids_mapping()
print(view_ids_mapping)
sub_map = {}
for i in view_ids:
if i == 'root':
continue
key = get_title(i)
sub_map['(>|")@(\\+)?id/' + i + '(<|")'] = '\\g<1>@\\g<2>id/' + view_ids_mapping[key] + '\\g<3>'
sub_map['R\\.id\\.' + i + '(\\W)'] = 'R.id.' + view_ids_mapping[key] + '\\g<1>'
sub_map['([bB]inding\\??(?:\\.\\w+)?\\??)\\.' + key + '(\\W)'] = '\\g<1>.' + view_ids_mapping[key] + '\\g<2>'
sub_map['([bB]inding\\??(?:\\.\\w+)?\\??)\\.' + key + '\\.' + key + '(\\W)'] = '\\g<1>.' + view_ids_mapping[
key] + '.' + view_ids_mapping[key] + '\\g<2>'
sub_map[r'\(binding as (\w+Binding)\)\.' + key + r'(\W)'] = r'(binding as \1).' + view_ids_mapping[key] + r'\2'
sub_map['(?<=app:constraint_referenced_ids=".*?)' + i + '(?=.*?")'] = view_ids_mapping[key]
# 新增 Kotlin 特有写法适配
sub_map[r'(binding!!(?:\.\w+)*?)\.' + key + r'(\W)'] = r'\1.' + view_ids_mapping[key] + r'\2'
sub_map[r'(binding\?\?(?:\.\w+)*?)\.' + key + r'(\W)'] = r'\1.' + view_ids_mapping[key] + r'\2'
sub_map[r'(binding(?:\.\w+)*?)\.' + key + r'(\W)'] = r'\1.' + view_ids_mapping[key] + r'\2'
sub_map['this\\.' + i + '(\\W)'] = 'this.' + view_ids_mapping[key] + '\\g<1>'
sub_map['this\\.' + i + '(\\.[\\w\\?]+(?:\\([^)]*\\))?)'] = 'this.' + view_ids_mapping[key] + '\\g<1>'
# sub_map['this\\.(\\w+)(\\.[\\w\\?]+(?:\\([^)]*\\))?)'] = 'this.' + view_ids_mapping[key] + '\\g<1>\\g<2>'
sub_map['popupView\\.' + i + '(\\W)'] = 'popupView.' + view_ids_mapping[key] + '\\g<1>'
# sub_map['\\b' + i + '\\.(\\w+)(\\s*=\\s*[^\\s]+)'] = view_ids_mapping[key] + '.\\g<1>\\g<2>'
sub_map['(?<!\\()\\b' + i + '\\.(\\w+)(\\s*=\\s*[^\\s]+)'] = view_ids_mapping[key] + '.\\g<1>\\g<2>'
# sub_map['this\\.' + view_ids_mapping['title']] = 'this.title'
if i == 'imgIcon':
sub_map[r'\.into\(\s*this\.(imgIcon)\s*\)'] = r'.into(this.' + view_ids_mapping[key] + ')'
sub_map_text(sub_map)
# 改 layout 文件名
layout_mapping = check_layout_mapping()
print(layout_mapping)
layout_path = get_path('.\\src\\main\\res\\layout')
listdir = os.listdir(layout_path)
for i in listdir:
name = i[:i.rfind('.')]
if layout_mapping.get(name) is None:
continue
path_join = os.path.join(layout_path, i)
#解决layout出现大写的问题
shutil.move(path_join, path_join.replace(name, normalize_filename(layout_mapping[name])))
sub_map = {}
for i in layout_mapping:
sub_map['R\\.layout\\.' + i + '(\\W)'] = 'R.layout.' + layout_mapping[i] + '\\g<1>'
# ViewBinding 同步修改
sub_map[get_title(i, True) + 'Binding'] = get_title(layout_mapping[i], True) + 'Binding'
sub_map['(>|")@layout/' + i + '(<|")'] = '\\g<1>@layout/' + layout_mapping[i] + '\\g<2>'
sub_map_text(sub_map)
# 改 drawable 文件名
deal_res_type('drawable')
# 改 mipmap 文件名
deal_res_type('mipmap')
# 改 raw 文件名
deal_res_type('raw')
# 改 raw 文件名
deal_res_type('xml')
# 改图片内容
deal_image()
# 主执行逻辑
mapping_file = "./mapping.json"
java_path = get_path('.\\src\\main\\java')
mapping_data = load_color_mapping(mapping_file) # 调用正确的函数
# 调试信息:检查 color_ids_mapping 内容
print("Color IDs Mapping:")
for original_color, mapped_color in mapping_data.items():
print(f"{original_color}: {mapped_color}")
# 进行文件处理
process_color_files(java_path, mapping_data)
def process_color_files(src_path, color_ids):
for root, dirs, files in os.walk(src_path):
for file in files:
if file.endswith(".kt") or file.endswith(".java"):
file_path = os.path.join(root, file)
print(f"Processing file: {file_path}")
replace_color_ids_in_file(file_path, color_ids)
def replace_color_ids_in_file(file_path, color_ids_mapping):
print("replace_color_ids_in_file")
print(color_ids_mapping)
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if "paperedto" in content:
print(f"Found 'paperedto' in: {file_path}")
if "android.R.color.paperedto" in content:
print(f"Found 'android.R.color.paperedto' in: {file_path}")
changed = False
for original_color, mapped_color in color_ids_mapping.items():
print('original_color'+original_color)
print('mapped_color'+mapped_color)
if mapped_color in content:
pattern = r'android.R.color.' + re.escape(mapped_color) + r'(?=[^\w])'
print(pattern)
replacement = 'android.R.color.' + original_color
print(replacement)
if re.search(pattern, content):
print(f"Matched: {mapped_color} → {original_color}")
matches = re.findall(pattern, content)
if matches:
print(f"Matched: {mapped_color} → {original_color} ({len(matches)} times)")
content = re.sub(pattern, replacement, content)
changed = True
if changed:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
def get_path(path):
paths = path.split('\\')
return str(os.path.join(*paths))
def main():
if not os.path.exists(get_path('.\\src\\main\\java\\' + applicationId[:applicationId.find('.')])):
print(get_path('.\\src\\main\\java\\' + applicationId[:applicationId.find('.')]))
return
# 资源混淆
deal_res()
deal_code()
def load_mapping(file_path):
"""加载 mapping.json 文件并打印内容."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print("Loaded Mapping:")
print(json.dumps(data, indent=4, ensure_ascii=False)) # 打印映射内容
return data.get("view_ids", {})
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading mapping file: {e}")
return {}
def replace_identifiers_in_file(file_path, mapping, replacements_log):
"""替换单个文件中的标识符,并存储替换的内容到 JSON 日志."""
with open(file_path, 'r', encoding='utf-8') as f:
original_text = f.read()
print(f"\nProcessing File: {file_path}")
print("Original Content:")
print(original_text) # 打印文件原始内容
modified_text = original_text
replacements = []
# 替换所有绑定相关的引用
def replace_binding_references(match):
identifier = match.group(2) or match.group(3)
return f"{match.group(1)}.{mapping.get(identifier, identifier)}"
# 替换类似 `binding.tvTest1` 和 `it.tvTest1` 的结构
modified_text = re.sub(
r'\b(binding|it)\.([a-zA-Z_0-9]+)\b',
replace_binding_references,
modified_text
)
# 替换无前缀的变量名,如 `tvTest1` 和 `tvTest2`,根据 mapping 文件中的 view_ids 替换
def replace_variable_names(match):
identifier = match.group(1)
# 使用 mapping 中的 view_ids 替换变量名
new_identifier = mapping.get(identifier, identifier)
if new_identifier != identifier:
replacements.append({"original": identifier, "modified": new_identifier})
return new_identifier
# 先提取所有引号中的内容
quoted_texts = re.findall(r'"([^"]*)"', modified_text)
# 将引号中的内容临时替换为占位符
placeholders = [f"__PLACEHOLDER_{i}__" for i in range(len(quoted_texts))]
for i, quoted in enumerate(quoted_texts):
modified_text = modified_text.replace(f'"{quoted}"', placeholders[i])
# 过滤掉方法调用中的标识符(例如 taichiPref.edit() 不替换 edit)
def is_function_call(match):
# 检查匹配的标识符后是否跟着 `()`
return f"{match.group(1)}()" if match.group(2) else None
# 替换变量名,排除函数调用
modified_text = re.sub(
r'\b(' + '|'.join(mapping.keys()) + r')\b(?!\()',
replace_variable_names,
modified_text
)
# 将占位符替换回原始引号内容
for i, quoted in enumerate(quoted_texts):
modified_text = modified_text.replace(placeholders[i], f'"{quoted}"')
# 过滤重复的替换项:根据 (original, modified) 进行去重
seen_replacements = set()
unique_replacements = []
for replacement in replacements:
replacement_tuple = (replacement["original"], replacement["modified"])
if replacement_tuple not in seen_replacements:
unique_replacements.append(replacement)
seen_replacements.add(replacement_tuple)
if unique_replacements:
replacements_log[file_path] = unique_replacements
# 写入修改后的内容
with open(file_path, 'w', encoding='utf-8') as f:
f.write(modified_text)
else:
print("No replacements made.")
def process_files_in_directory(directory, mapping, replacements_log):
"""处理目录下的所有文件并打印文件路径."""
for root, _, files in os.walk(directory):
for file in files:
if file.endswith(".kt"): # 仅处理 Kotlin 文件
file_path = os.path.join(root, file)
replace_identifiers_in_file(file_path, mapping, replacements_log)
def solve_runapplyalso():
# 配置路径
mapping_file = "mapping.json"
src_directory = "./src/main/java"
log_file = "replacement_log.json"
# 加载映射表
mapping = load_mapping(mapping_file)
# 检查路径
if not os.path.exists(src_directory):
print(f"Source directory not found: {src_directory}")
elif not mapping:
print("Mapping file is empty or not found.")
else:
# 存储替换日志
replacements_log = {}
# 开始处理文件
process_files_in_directory(src_directory, mapping, replacements_log)
# 将替换日志保存为 JSON 文件
with open(log_file, 'w', encoding='utf-8') as log:
json.dump(replacements_log, log, ensure_ascii=False, indent=4)
print(f"\nReplacement completed. Log saved to {log_file}.")
def load_color_mapping(file_path):
"""加载 mapping.json 文件并打印内容."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print("Loaded color Mapping:")
print(json.dumps(data, indent=4, ensure_ascii=False)) # 打印映射内容
color_ids = data.get("color_ids", {})
print(f"color_ids extracted: {color_ids}") # 检查 color_ids 是否存在
return color_ids
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading mapping file: {e}")
return {}
if __name__ == '__main__':
if os.path.exists('build.gradle'):
gradle_path = 'build.gradle'
elif os.path.exists('build.gradle.kts'):
gradle_path = 'build.gradle.kts'
else:
exit('找不到 build.gradle 文件')
applicationId = re.search('namespace .*?["\'](.*?)["\']', open(gradle_path, 'r', encoding='utf-8').read())[1]
print(applicationId)
main()
# solve_runapplyalso()
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment