Skip to content

Commit

Permalink
Merge pull request #595 from d4rken-org/systemcleaner_legacy_filter_i…
Browse files Browse the repository at this point in the history
…mport

SystemCleaner: Support for importing SD Maid 1/Legacy filters
  • Loading branch information
d4rken authored Aug 19, 2023
2 parents 012a2df + 72bf00e commit 358ce6a
Show file tree
Hide file tree
Showing 21 changed files with 452 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package eu.darken.sdmse.common.serialization

import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.ToJson

class RegexAdapter(private val caseInsensitive: Boolean) {
class RegexAdapter {

@ToJson
fun toJson(value: Regex): String = value.toString()
fun toJson(value: Regex): Wrapper = Wrapper(
pattern = value.pattern,
options = value.options,
)

@FromJson
fun fromJson(raw: String): Regex = if (caseInsensitive) {
Regex(raw, RegexOption.IGNORE_CASE)
} else {
Regex(raw)
}
fun fromJson(raw: Wrapper): Regex = Regex(
pattern = raw.pattern,
options = raw.options
)

@JsonClass(generateAdapter = true)
data class Wrapper(
@Json(name = "pattern") val pattern: String,
@Json(name = "options") val options: Set<RegexOption>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class SerializationCommonModule {
add(FileAdapter())
add(UriAdapter())
add(OffsetDateTimeAdapter())
add(RegexAdapter())
}.build()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package eu.darken.sdmse.common.serialization

import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.json.toComparableJson

class RegexAdapterTest : BaseTest() {

val moshi = Moshi.Builder().apply {
add(RegexAdapter())
}.build()

@JsonClass(generateAdapter = true)
data class TestContainer(
val regexValue: Regex?,
val regexList: List<Regex>
)

val adapter = moshi.adapter(TestContainer::class.java)

@Test
fun `serialize test container`() {
val before = TestContainer(
regexValue = Regex("value", RegexOption.LITERAL),
regexList = listOf(
Regex("ele1", RegexOption.COMMENTS),
Regex("ele2", setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.MULTILINE)),
)
)

val rawJson = adapter.toJson(before)

rawJson.toComparableJson() shouldBe """
{
"regexValue": {
"pattern": "value",
"options": [
"LITERAL"
]
},
"regexList": [
{
"pattern": "ele1",
"options": [
"COMMENTS"
]
},
{
"pattern": "ele2",
"options": [
"MULTILINE",
"DOT_MATCHES_ALL"
]
}
]
}
""".toComparableJson()

val after = adapter.fromJson(rawJson)!!
after.regexValue!!.apply {
pattern shouldBe before.regexValue!!.pattern
options shouldBe before.regexValue.options
}
after.regexList[0].apply {
pattern shouldBe before.regexList[0].pattern
options shouldBe before.regexList[0].options
}
after.regexList[1].apply {
pattern shouldBe before.regexList[1].pattern
options shouldBe before.regexList[1].options
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ class CustomFilter @AssistedInject constructor(
}?.toSet(),
pathCriteria = filterConfig.pathCriteria,
nameCriteria = filterConfig.nameCriteria,
pathExclusions = filterConfig.exclusion,
pathExclusions = filterConfig.exclusionCriteria,
minimumSize = filterConfig.sizeMinimum,
maximumSize = filterConfig.sizeMaximum,
minimumAge = filterConfig.ageMinimum,
maximumAge = filterConfig.ageMaximum,
pathRegexes = filterConfig.pathRegexes,
)
sieve = baseSieveFactory.create(sieveConfig)
log(TAG) { "initialized()" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,27 @@ import eu.darken.sdmse.systemcleaner.core.filter.FilterIdentifier
import eu.darken.sdmse.systemcleaner.core.sieve.NameCriterium
import eu.darken.sdmse.systemcleaner.core.sieve.SegmentCriterium
import kotlinx.parcelize.Parcelize
import java.time.Duration
import java.time.Instant

@Parcelize
@JsonClass(generateAdapter = true)
data class CustomFilterConfig(
@Json(name = "configVersion") val configVersion: Int = 1,
@Json(name = "identifier") val identifier: FilterIdentifier,
@Json(name = "configVersion") val configVersion: Long = 6L,
@Json(name = "id") val identifier: FilterIdentifier,
@Json(name = "createdAt") val createdAt: Instant = Instant.now(),
@Json(name = "modifiedAt") val modifiedAt: Instant = Instant.now(),
@Json(name = "label") val label: String,
@Json(name = "areas") val areas: Set<DataArea.Type>? = null,
@Json(name = "fileTypes") val fileTypes: Set<FileType>? = null,
@Json(name = "pathCriteria") val pathCriteria: Set<SegmentCriterium>? = null,
@Json(name = "pathExclusionCriteria") val exclusion: Set<SegmentCriterium>? = null,
@Json(name = "pathExclusionCriteria") val exclusionCriteria: Set<SegmentCriterium>? = null,
@Json(name = "nameCriteria") val nameCriteria: Set<NameCriterium>? = null,
@Json(name = "sizeMin") val sizeMinimum: Long? = null,
@Json(name = "sizeMax") val sizeMaximum: Long? = null,
@Json(name = "ageMin") val ageMinimum: Duration? = null,
@Json(name = "ageMax") val ageMaximum: Duration? = null,
@Json(name = "pathRegexes") val pathRegexes: Set<Regex>? = null,
) : Parcelable {
val isUnderdefined: Boolean
get() = pathCriteria.isNullOrEmpty() && nameCriteria.isNullOrEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import eu.darken.sdmse.common.datastore.value
import eu.darken.sdmse.common.debug.logging.Logging.Priority.ERROR
import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.sdmse.common.debug.logging.Logging.Priority.WARN
import eu.darken.sdmse.common.debug.logging.asLog
import eu.darken.sdmse.common.debug.logging.log
import eu.darken.sdmse.common.debug.logging.logTag
import eu.darken.sdmse.common.serialization.fromFile
Expand All @@ -32,6 +33,7 @@ class CustomFilterRepo @Inject constructor(
private val context: Context,
private val baseMoshi: Moshi,
private val settings: SystemCleanerSettings,
private val legacyImporter: LegacyFilterSupport,
) {

private val moshi by lazy {
Expand Down Expand Up @@ -123,12 +125,16 @@ class CustomFilterRepo @Inject constructor(

suspend fun importFilter(rawFilters: List<RawFilter>) {
log(TAG) { "importFilter($rawFilters)" }
val configs = rawFilters.mapNotNull {
val configs = rawFilters.map { rawFilter ->
try {
configAdapter.fromJson(it.payload)
} catch (e: Exception) {
log(TAG, ERROR) { "Failed to import $it" }
null
configAdapter.fromJson(rawFilter.payload)!!
} catch (ogError: Exception) {
try {
legacyImporter.import(rawFilter.payload)!!
} catch (_: Exception) {
log(TAG, ERROR) { "Failed to import $rawFilter: ${ogError.asLog()}" }
throw ogError
}
}
}.toSet()
save(configs)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package eu.darken.sdmse.systemcleaner.core.filter.custom

import androidx.annotation.Keep
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import eu.darken.sdmse.common.areas.DataArea
import eu.darken.sdmse.common.debug.logging.Logging.Priority.WARN
import eu.darken.sdmse.common.debug.logging.asLog
import eu.darken.sdmse.common.debug.logging.log
import eu.darken.sdmse.common.debug.logging.logTag
import eu.darken.sdmse.common.files.FileType
import eu.darken.sdmse.common.files.toSegs
import eu.darken.sdmse.systemcleaner.core.sieve.NameCriterium
import eu.darken.sdmse.systemcleaner.core.sieve.SegmentCriterium
import java.time.Duration
import java.util.UUID
import javax.inject.Inject


class LegacyFilterSupport @Inject constructor(
private val moshi: Moshi,
) {
private val adapter by lazy { moshi.adapter(Filter::class.java) }

suspend fun import(payload: String): CustomFilterConfig? {
log(TAG) { "Importing $payload" }

val legacyFilter = try {
adapter.fromJson(payload)!!
} catch (e: Exception) {
log(TAG, WARN) { "Failed to import $payload: ${e.asLog()}" }
return null
}

val pathCriteria = mutableSetOf<SegmentCriterium>()
legacyFilter.possibleBasePathes
?.map {
SegmentCriterium(
segments = it.toSegs(),
mode = SegmentCriterium.Mode.Start(allowPartial = true, ignoreCase = true)
)
}
?.forEach { pathCriteria.add(it) }
legacyFilter.possiblePathContains
?.map {
SegmentCriterium(
segments = it.toSegs(),
mode = SegmentCriterium.Mode.Contain(allowPartial = true, ignoreCase = true)
)
}
?.forEach { pathCriteria.add(it) }

val nameCriteria = mutableSetOf<NameCriterium>()
legacyFilter.possibleNameInits
?.map { NameCriterium(name = it, mode = NameCriterium.Mode.Start(ignoreCase = true)) }
?.forEach { nameCriteria.add(it) }

legacyFilter.possibleNameEndings
?.map { NameCriterium(name = it, mode = NameCriterium.Mode.End(ignoreCase = true)) }
?.forEach { nameCriteria.add(it) }

return CustomFilterConfig(
label = legacyFilter.label,
identifier = UUID.randomUUID().toString(),
pathCriteria = pathCriteria,
nameCriteria = nameCriteria,
exclusionCriteria = legacyFilter.exclusions?.map {
SegmentCriterium(
segments = it.toSegs(),
mode = SegmentCriterium.Mode.Contain(allowPartial = true, ignoreCase = true)
)
}?.toSet(),
pathRegexes = legacyFilter.regex?.map {
Regex(it)
}?.toSet(),
sizeMinimum = legacyFilter.minimumSize,
sizeMaximum = legacyFilter.maximumSize,
ageMinimum = legacyFilter.minimumAge?.let { Duration.ofMillis(it) },
ageMaximum = legacyFilter.maximumAge?.let { Duration.ofMillis(it) },
fileTypes = legacyFilter.targetType?.let {
when (it) {
Filter.TargetType.FILE -> setOf(FileType.FILE)
Filter.TargetType.DIRECTORY -> setOf(FileType.DIRECTORY)
Filter.TargetType.UNDEFINED -> null
}
},
areas = legacyFilter.locations?.mapNotNull {
when (it) {
Filter.Location.SDCARD -> DataArea.Type.SDCARD
Filter.Location.PUBLIC_MEDIA -> DataArea.Type.PUBLIC_MEDIA
Filter.Location.PUBLIC_DATA -> DataArea.Type.PUBLIC_DATA
Filter.Location.PUBLIC_OBB -> DataArea.Type.PUBLIC_OBB
Filter.Location.PRIVATE_DATA -> DataArea.Type.PRIVATE_DATA
Filter.Location.APP_LIB -> DataArea.Type.APP_LIB
Filter.Location.APP_ASEC -> DataArea.Type.APP_ASEC
Filter.Location.APP_APP -> DataArea.Type.APP_APP
Filter.Location.APP_APP_PRIVATE -> DataArea.Type.APP_APP_PRIVATE
Filter.Location.DALVIK_DEX -> DataArea.Type.DALVIK_DEX
Filter.Location.DALVIK_PROFILE -> DataArea.Type.DALVIK_PROFILE
Filter.Location.SYSTEM -> DataArea.Type.SYSTEM
Filter.Location.SYSTEM_APP -> DataArea.Type.SYSTEM_APP
Filter.Location.SYSTEM_PRIV_APP -> DataArea.Type.SYSTEM_PRIV_APP
Filter.Location.DOWNLOAD_CACHE -> DataArea.Type.DOWNLOAD_CACHE
Filter.Location.DATA -> DataArea.Type.DATA
Filter.Location.DATA_SYSTEM -> DataArea.Type.DATA_SYSTEM
Filter.Location.DATA_SYSTEM_CE -> DataArea.Type.DATA_SYSTEM_CE
Filter.Location.DATA_SYSTEM_DE -> DataArea.Type.DATA_SYSTEM_DE
Filter.Location.PORTABLE -> DataArea.Type.PORTABLE
Filter.Location.OEM -> DataArea.Type.OEM
Filter.Location.DATA_SDEXT2 -> DataArea.Type.DATA_SDEXT2
Filter.Location.ROOT -> null
Filter.Location.VENDOR -> null
Filter.Location.MNT_SECURE_ASEC -> null
Filter.Location.UNKNOWN -> null
}
}?.toSet()?.takeIf { it.isNotEmpty() }
)
}

@JsonClass(generateAdapter = true)
data class Filter(
@Json(name = "label") val label: String,
@Json(name = "targetType") val targetType: TargetType?,
@Json(name = "isEmpty") val isEmpty: Boolean?,
@Json(name = "locations") val locations: Set<Location>?,
@Json(name = "mainPath") val possibleBasePathes: Set<String>?,
@Json(name = "pathContains") val possiblePathContains: Set<String>?,
@Json(name = "possibleNameInits") val possibleNameInits: Set<String>?,
@Json(name = "possibleNameEndings") val possibleNameEndings: Set<String>?,
@Json(name = "exclusions") val exclusions: Set<String>?,
@Json(name = "regexes") val regex: Set<String>?,
@Json(name = "maximumSize") val maximumSize: Long?,
@Json(name = "minimumSize") val minimumSize: Long?,
@Json(name = "maximumAge") val maximumAge: Long?,
@Json(name = "minimumAge") val minimumAge: Long?,
) {

// @Json(name = "version") private int version = VERSION;
// @Json(name = "identifier") private String identifier;
// @Json(name = "color") private String color;
// @Json(name = "description") private String description;
// @Nullable @Json(name = "rootOnly") private Boolean rootOnly;

@Keep
@JsonClass(generateAdapter = false)
enum class TargetType {
@Json(name = "FILE") FILE,
@Json(name = "DIRECTORY") DIRECTORY,
@Json(name = "UNDEFINED") UNDEFINED,
}

@Keep
@JsonClass(generateAdapter = false)
enum class Location(val raw: String) {
@Json(name = "SDCARD") SDCARD("SDCARD"),
@Json(name = "PUBLIC_MEDIA") PUBLIC_MEDIA("PUBLIC_MEDIA"),
@Json(name = "PUBLIC_DATA") PUBLIC_DATA("PUBLIC_DATA"),
@Json(name = "PUBLIC_OBB") PUBLIC_OBB("PUBLIC_OBB"),
@Json(name = "PRIVATE_DATA") PRIVATE_DATA("PRIVATE_DATA"),
@Json(name = "APP_LIB") APP_LIB("APP_LIB"),
@Json(name = "APP_ASEC") APP_ASEC("APP_ASEC"),
@Json(name = "MNT_SECURE_ASEC") MNT_SECURE_ASEC("MNT_SECURE_ASEC"),
@Json(name = "APP_APP") APP_APP("APP_APP"),
@Json(name = "APP_APP_PRIVATE") APP_APP_PRIVATE("APP_APP_PRIVATE"),
@Json(name = "DALVIK_DEX") DALVIK_DEX("DALVIK_DEX"),
@Json(name = "DALVIK_PROFILE") DALVIK_PROFILE("DALVIK_PROFILE"),
@Json(name = "SYSTEM_APP") SYSTEM_APP("SYSTEM_APP"),
@Json(name = "SYSTEM_PRIV_APP") SYSTEM_PRIV_APP("SYSTEM_PRIV_APP"),
@Json(name = "DOWNLOAD_CACHE") DOWNLOAD_CACHE("DOWNLOAD_CACHE"),
@Json(name = "SYSTEM") SYSTEM("SYSTEM"),
@Json(name = "DATA") DATA("DATA"),
@Json(name = "DATA_SYSTEM") DATA_SYSTEM("DATA_SYSTEM"),
@Json(name = "DATA_SYSTEM_CE") DATA_SYSTEM_CE("DATA_SYSTEM_CE"),
@Json(name = "DATA_SYSTEM_DE") DATA_SYSTEM_DE("DATA_SYSTEM_DE"),
@Json(name = "PORTABLE") PORTABLE("PORTABLE"),
@Json(name = "ROOT") ROOT("ROOT"),
@Json(name = "VENDOR") VENDOR("VENDOR"),
@Json(name = "OEM") OEM("OEM"),
@Json(name = "DATA_SDEXT2") DATA_SDEXT2("DATA_SDEXT2"),
@Json(name = "UNKNOWN") UNKNOWN("UNKNOWN"),
}
}

companion object {
private val TAG = logTag("SystemCleaner", "CustomFilter", "Repo")
}
}
Loading

0 comments on commit 358ce6a

Please sign in to comment.