fix: 不再使用脆弱的反射进行备份与恢复

This commit is contained in:
HapeLee
2026-06-06 13:40:57 +08:00
parent b1ef0098d4
commit e592fcdebf
4 changed files with 156 additions and 74 deletions
@@ -25,7 +25,6 @@ import io.legado.app.utils.createFolderIfNotExist
import io.legado.app.utils.defaultSharedPreferences
import io.legado.app.utils.externalFiles
import io.legado.app.utils.getFile
import io.legado.app.utils.getSharedPreferences
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.normalizeFileName
import io.legado.app.utils.openOutputStream
@@ -178,29 +177,29 @@ object Backup {
.writeText(GSON.toJson(it))
}
currentCoroutineContext().ensureActive()
appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
sp.edit(commit = true) {
appCtx.defaultSharedPreferences.all.forEach { (key, value) ->
if (BackupConfig.keyIsNotIgnore(key)) {
when (key) {
PreferKey.webDavPassword -> {
putString(key, aes.runCatching {
encryptBase64(value.toString())
}.getOrDefault(value.toString()))
}
val configMap = appCtx.defaultSharedPreferences.all
val xmlBuilder = StringBuilder()
xmlBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n")
xmlBuilder.append("<map>\n")
configMap.forEach { (key, value) ->
if (BackupConfig.keyIsNotIgnore(key, true)) {
val finalValue = if (key == PreferKey.webDavPassword) {
aes.runCatching { encryptBase64(value.toString()) }.getOrDefault(value.toString())
} else value
else -> when (value) {
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
is Long -> putLong(key, value)
is Float -> putFloat(key, value)
is String -> putString(key, value)
}
}
}
when (finalValue) {
is String -> xmlBuilder.append(" <string name=\"$key\">${finalValue.replace("&", "&amp;").replace("<", "&lt;")}</string>\n")
is Int -> xmlBuilder.append(" <int name=\"$key\" value=\"$finalValue\" />\n")
is Long -> xmlBuilder.append(" <long name=\"$key\" value=\"$finalValue\" />\n")
is Float -> xmlBuilder.append(" <float name=\"$key\" value=\"$finalValue\" />\n")
is Boolean -> xmlBuilder.append(" <boolean name=\"$key\" value=\"$finalValue\" />\n")
}
}
}
xmlBuilder.append("</map>")
FileUtils.createFileIfNotExist(backupPath + File.separator + "config.xml")
.writeText(xmlBuilder.toString())
currentCoroutineContext().ensureActive()
val zipFileName = getNowZipFileName()
val paths = arrayListOf(*backupFileNames)
@@ -88,7 +88,35 @@ object BackupConfig {
PreferKey.bgImage,
PreferKey.bgImageBlurring,
PreferKey.bgImageN,
PreferKey.bgImageNBlurring
PreferKey.bgImageNBlurring,
PreferKey.themeColor,
PreferKey.secondaryThemeColor,
PreferKey.paletteStyle,
PreferKey.materialVersion,
PreferKey.composeEngine,
PreferKey.customContrast,
PreferKey.customMode,
PreferKey.useMiuixMonet,
PreferKey.containerOpacity,
PreferKey.topBarOpacity,
PreferKey.bottomBarOpacity,
PreferKey.enableBlur,
PreferKey.enableProgressiveBlur,
PreferKey.topBarBlurRadius,
PreferKey.bottomBarBlurRadius,
PreferKey.topBarBlurAlpha,
PreferKey.bottomBarBlurAlpha,
PreferKey.bottomBarLensRadius,
PreferKey.useFlexibleTopAppBar,
PreferKey.cBackground,
PreferKey.cBBackground,
PreferKey.cNBackground,
PreferKey.cNBBackground,
PreferKey.enableDeepPersonalization,
PreferKey.primaryTextColor,
PreferKey.secondaryTextColor,
PreferKey.themeBackgroundColor,
PreferKey.labelContainerColor
)
private val coverPrefKeys = arrayOf(
@@ -100,9 +128,10 @@ object BackupConfig {
PreferKey.coverShowAuthorN
)
fun keyIsNotIgnore(key: String): Boolean {
fun keyIsNotIgnore(key: String, isBackup: Boolean = false): Boolean {
if (ignorePrefKeys.contains(key)) return false
if (isBackup) return true
return when {
ignorePrefKeys.contains(key) -> false
ignoreReadConfig && readPrefKeys.contains(key) -> false
ignoreThemeConfig && themePrefKeys.contains(key) -> false
ignoreCoverConfig && coverPrefKeys.contains(key) -> false
@@ -116,17 +145,17 @@ object BackupConfig {
val ignoreReadConfig: Boolean
get() = ignoreConfig[readConfigKey] == true
private val ignoreThemeMode: Boolean
val ignoreThemeMode: Boolean
get() = ignoreConfig[PreferKey.themeMode] == true
private val ignoreThemeConfig: Boolean
val ignoreThemeConfig: Boolean
get() = ignoreConfig[themeConfigKey] == true
private val ignoreCoverConfig: Boolean
val ignoreCoverConfig: Boolean
get() = ignoreConfig[coverConfigKey] == true
private val ignoreBookshelfLayout: Boolean
val ignoreBookshelfLayout: Boolean
get() = ignoreConfig[PreferKey.bookshelfLayout] == true
private val ignoreShowRss: Boolean
val ignoreShowRss: Boolean
get() = ignoreConfig[PreferKey.showRss] == true
private val ignoreThreadCount: Boolean
val ignoreThreadCount: Boolean
get() = ignoreConfig[PreferKey.threadCount] == true
val ignoreLocalBook: Boolean
get() = ignoreConfig[localBookKey] == true
@@ -48,7 +48,6 @@ import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefString
import io.legado.app.utils.getSharedPreferences
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.isJsonArray
import io.legado.app.utils.openInputStream
@@ -58,6 +57,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
import splitties.init.appCtx
import java.io.File
import java.io.FileInputStream
@@ -273,17 +274,19 @@ object Restore : KoinComponent {
AppLog.put("恢复直链上传出错\n${it.localizedMessage}", it)
}
//恢复主题配置
File(path, OldThemeConfig.configFileName).takeIf {
it.exists()
}?.runCatching {
FileUtils.delete(OldThemeConfig.configFilePath)
copyTo(File(OldThemeConfig.configFilePath))
OldThemeConfig.upConfig()
}?.onFailure {
AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
if (!BackupConfig.ignoreThemeConfig) {
File(path, OldThemeConfig.configFileName).takeIf {
it.exists()
}?.runCatching {
FileUtils.delete(OldThemeConfig.configFilePath)
copyTo(File(OldThemeConfig.configFilePath))
OldThemeConfig.upConfig()
}?.onFailure {
AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
}
}
File(path, BookCover.configFileName).takeIf {
it.exists()
it.exists() && !BackupConfig.ignoreCoverConfig
}?.runCatching {
val json = readText()
BookCover.saveCoverRule(json)
@@ -311,43 +314,19 @@ object Restore : KoinComponent {
AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it)
}
}
//AppWebDav.downBgs()
appCtx.getSharedPreferences(path, "config")?.all?.let { map ->
val finalMap = mutableMapOf<String, Any>()
appCtx.defaultSharedPreferences.edit {
map.forEach { (key, value) ->
if (BackupConfig.keyIsNotIgnore(key)) {
when (key) {
PreferKey.webDavPassword -> {
val password = kotlin.runCatching {
aes.decryptStr(value.toString())
}.getOrNull() ?: let {
if (appCtx.getPrefString(PreferKey.webDavPassword).isNullOrBlank()) {
value.toString()
} else null
}
password?.let {
putString(key, it)
finalMap[key] = it
}
}
else -> {
when (value) {
is Int -> { putInt(key, value); finalMap[key] = value }
is Boolean -> { putBoolean(key, value); finalMap[key] = value }
is Long -> { putLong(key, value); finalMap[key] = value }
is Float -> { putFloat(key, value); finalMap[key] = value }
is String -> { putString(key, value); finalMap[key] = value }
}
}
}
}
// 恢复配置文件 (手动解析 XML,替代反射逻辑)
val configFile = File(path, "config.xml")
if (configFile.exists()) {
try {
val map = readXmlToMap(configFile)
if (map.isNotEmpty()) {
applyConfigMap(map, aes)
}
} catch (e: Exception) {
AppLog.put("恢复配置 XML 出错\n${e.localizedMessage}", e)
}
// 同步恢复到 DataStore
settingsRepository.batchPutFromMap(finalMap)
}
ReadBookConfig.apply {
comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect)
readStyleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect)
@@ -366,6 +345,81 @@ object Restore : KoinComponent {
}
}
private suspend fun applyConfigMap(map: Map<String, Any?>, aes: BackupAES) {
val finalMap = mutableMapOf<String, Any>()
appCtx.defaultSharedPreferences.edit {
map.forEach { (key, value) ->
if (BackupConfig.keyIsNotIgnore(key)) {
when (key) {
PreferKey.webDavPassword -> {
val password = kotlin.runCatching {
aes.decryptStr(value.toString())
}.getOrNull() ?: let {
if (appCtx.getPrefString(PreferKey.webDavPassword).isNullOrBlank()) {
value.toString()
} else null
}
password?.let {
putString(key, it)
finalMap[key] = it
}
}
else -> {
if (value != null) {
when (value) {
is Int -> { putInt(key, value); finalMap[key] = value }
is Boolean -> { putBoolean(key, value); finalMap[key] = value }
is Long -> { putLong(key, value); finalMap[key] = value }
is Double -> { // JSON 数字会被解析为 Double
val floatValue = value.toFloat()
putFloat(key, floatValue)
finalMap[key] = floatValue
}
is Float -> { putFloat(key, value); finalMap[key] = value }
is String -> { putString(key, value); finalMap[key] = value }
}
}
}
}
}
}
}
// 同步恢复到 DataStore
settingsRepository.batchPutFromMap(finalMap)
}
private fun readXmlToMap(file: File): Map<String, Any?> {
val map = mutableMapOf<String, Any?>()
try {
val factory = XmlPullParserFactory.newInstance()
val parser = factory.newPullParser()
FileInputStream(file).use { fis ->
parser.setInput(fis, "UTF-8")
var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
val tagName = parser.name
val name = parser.getAttributeValue(null, "name")
if (name != null) {
when (tagName) {
"string" -> map[name] = parser.nextText()
"int" -> map[name] = parser.getAttributeValue(null, "value").toInt()
"long" -> map[name] = parser.getAttributeValue(null, "value").toLong()
"float" -> map[name] = parser.getAttributeValue(null, "value").toFloat()
"boolean" -> map[name] = parser.getAttributeValue(null, "value").toBoolean()
}
}
}
eventType = parser.next()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return map
}
private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
try {
val file = File(path, fileName)
@@ -374,7 +374,7 @@ fun BackupConfigScreen(
AppTextField(
value = tempPassword,
onValueChange = { tempPassword = it },
backgroundColor = MiuixTheme.colorScheme.surface,
backgroundColor = LegadoTheme.colorScheme.surface,
label = stringResource(R.string.web_dav_pw),
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),