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.defaultSharedPreferences
import io.legado.app.utils.externalFiles import io.legado.app.utils.externalFiles
import io.legado.app.utils.getFile import io.legado.app.utils.getFile
import io.legado.app.utils.getSharedPreferences
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import io.legado.app.utils.normalizeFileName import io.legado.app.utils.normalizeFileName
import io.legado.app.utils.openOutputStream import io.legado.app.utils.openOutputStream
@@ -178,29 +177,29 @@ object Backup {
.writeText(GSON.toJson(it)) .writeText(GSON.toJson(it))
} }
currentCoroutineContext().ensureActive() currentCoroutineContext().ensureActive()
appCtx.getSharedPreferences(backupPath, "config")?.let { sp -> val configMap = appCtx.defaultSharedPreferences.all
sp.edit(commit = true) { val xmlBuilder = StringBuilder()
appCtx.defaultSharedPreferences.all.forEach { (key, value) -> xmlBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n")
if (BackupConfig.keyIsNotIgnore(key)) { xmlBuilder.append("<map>\n")
when (key) { configMap.forEach { (key, value) ->
PreferKey.webDavPassword -> { if (BackupConfig.keyIsNotIgnore(key, true)) {
putString(key, aes.runCatching { val finalValue = if (key == PreferKey.webDavPassword) {
encryptBase64(value.toString()) aes.runCatching { encryptBase64(value.toString()) }.getOrDefault(value.toString())
}.getOrDefault(value.toString())) } else value
}
else -> when (value) { when (finalValue) {
is Int -> putInt(key, value) is String -> xmlBuilder.append(" <string name=\"$key\">${finalValue.replace("&", "&amp;").replace("<", "&lt;")}</string>\n")
is Boolean -> putBoolean(key, value) is Int -> xmlBuilder.append(" <int name=\"$key\" value=\"$finalValue\" />\n")
is Long -> putLong(key, value) is Long -> xmlBuilder.append(" <long name=\"$key\" value=\"$finalValue\" />\n")
is Float -> putFloat(key, value) is Float -> xmlBuilder.append(" <float name=\"$key\" value=\"$finalValue\" />\n")
is String -> putString(key, value) 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() currentCoroutineContext().ensureActive()
val zipFileName = getNowZipFileName() val zipFileName = getNowZipFileName()
val paths = arrayListOf(*backupFileNames) val paths = arrayListOf(*backupFileNames)
@@ -88,7 +88,35 @@ object BackupConfig {
PreferKey.bgImage, PreferKey.bgImage,
PreferKey.bgImageBlurring, PreferKey.bgImageBlurring,
PreferKey.bgImageN, 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( private val coverPrefKeys = arrayOf(
@@ -100,9 +128,10 @@ object BackupConfig {
PreferKey.coverShowAuthorN 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 { return when {
ignorePrefKeys.contains(key) -> false
ignoreReadConfig && readPrefKeys.contains(key) -> false ignoreReadConfig && readPrefKeys.contains(key) -> false
ignoreThemeConfig && themePrefKeys.contains(key) -> false ignoreThemeConfig && themePrefKeys.contains(key) -> false
ignoreCoverConfig && coverPrefKeys.contains(key) -> false ignoreCoverConfig && coverPrefKeys.contains(key) -> false
@@ -116,17 +145,17 @@ object BackupConfig {
val ignoreReadConfig: Boolean val ignoreReadConfig: Boolean
get() = ignoreConfig[readConfigKey] == true get() = ignoreConfig[readConfigKey] == true
private val ignoreThemeMode: Boolean val ignoreThemeMode: Boolean
get() = ignoreConfig[PreferKey.themeMode] == true get() = ignoreConfig[PreferKey.themeMode] == true
private val ignoreThemeConfig: Boolean val ignoreThemeConfig: Boolean
get() = ignoreConfig[themeConfigKey] == true get() = ignoreConfig[themeConfigKey] == true
private val ignoreCoverConfig: Boolean val ignoreCoverConfig: Boolean
get() = ignoreConfig[coverConfigKey] == true get() = ignoreConfig[coverConfigKey] == true
private val ignoreBookshelfLayout: Boolean val ignoreBookshelfLayout: Boolean
get() = ignoreConfig[PreferKey.bookshelfLayout] == true get() = ignoreConfig[PreferKey.bookshelfLayout] == true
private val ignoreShowRss: Boolean val ignoreShowRss: Boolean
get() = ignoreConfig[PreferKey.showRss] == true get() = ignoreConfig[PreferKey.showRss] == true
private val ignoreThreadCount: Boolean val ignoreThreadCount: Boolean
get() = ignoreConfig[PreferKey.threadCount] == true get() = ignoreConfig[PreferKey.threadCount] == true
val ignoreLocalBook: Boolean val ignoreLocalBook: Boolean
get() = ignoreConfig[localBookKey] == true 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.getPrefBoolean
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import io.legado.app.utils.getSharedPreferences
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.openInputStream import io.legado.app.utils.openInputStream
@@ -58,6 +57,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@@ -273,17 +274,19 @@ object Restore : KoinComponent {
AppLog.put("恢复直链上传出错\n${it.localizedMessage}", it) AppLog.put("恢复直链上传出错\n${it.localizedMessage}", it)
} }
//恢复主题配置 //恢复主题配置
File(path, OldThemeConfig.configFileName).takeIf { if (!BackupConfig.ignoreThemeConfig) {
it.exists() File(path, OldThemeConfig.configFileName).takeIf {
}?.runCatching { it.exists()
FileUtils.delete(OldThemeConfig.configFilePath) }?.runCatching {
copyTo(File(OldThemeConfig.configFilePath)) FileUtils.delete(OldThemeConfig.configFilePath)
OldThemeConfig.upConfig() copyTo(File(OldThemeConfig.configFilePath))
}?.onFailure { OldThemeConfig.upConfig()
AppLog.put("恢复主题出错\n${it.localizedMessage}", it) }?.onFailure {
AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
}
} }
File(path, BookCover.configFileName).takeIf { File(path, BookCover.configFileName).takeIf {
it.exists() it.exists() && !BackupConfig.ignoreCoverConfig
}?.runCatching { }?.runCatching {
val json = readText() val json = readText()
BookCover.saveCoverRule(json) BookCover.saveCoverRule(json)
@@ -311,43 +314,19 @@ object Restore : KoinComponent {
AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it) AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it)
} }
} }
//AppWebDav.downBgs() // 恢复配置文件 (手动解析 XML,替代反射逻辑)
appCtx.getSharedPreferences(path, "config")?.all?.let { map -> val configFile = File(path, "config.xml")
val finalMap = mutableMapOf<String, Any>() if (configFile.exists()) {
appCtx.defaultSharedPreferences.edit { try {
map.forEach { (key, value) -> val map = readXmlToMap(configFile)
if (BackupConfig.keyIsNotIgnore(key)) { if (map.isNotEmpty()) {
when (key) { applyConfigMap(map, aes)
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 }
}
}
}
}
} }
} catch (e: Exception) {
AppLog.put("恢复配置 XML 出错\n${e.localizedMessage}", e)
} }
// 同步恢复到 DataStore
settingsRepository.batchPutFromMap(finalMap)
} }
ReadBookConfig.apply { ReadBookConfig.apply {
comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect) comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect)
readStyleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect) 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>? { private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
try { try {
val file = File(path, fileName) val file = File(path, fileName)
@@ -374,7 +374,7 @@ fun BackupConfigScreen(
AppTextField( AppTextField(
value = tempPassword, value = tempPassword,
onValueChange = { tempPassword = it }, onValueChange = { tempPassword = it },
backgroundColor = MiuixTheme.colorScheme.surface, backgroundColor = LegadoTheme.colorScheme.surface,
label = stringResource(R.string.web_dav_pw), label = stringResource(R.string.web_dav_pw),
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),