From 128dcd3250c6007dd0810042bf314bbcad777585 Mon Sep 17 00:00:00 2001 From: Tim Schneeberger Date: Sat, 29 Oct 2022 15:03:57 +0200 Subject: [PATCH] fix: Validate imported presets to prevent crashes --- .../fragment/FileLibraryDialogFragment.kt | 17 ++- .../rootlessjamesdsp/model/Preset.kt | 104 ++++++++++++++---- .../preference/FileLibraryPreference.kt | 9 ++ .../rootlessjamesdsp/utils/StorageUtils.kt | 10 ++ app/src/main/res/values/strings.xml | 3 + 5 files changed, 117 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/FileLibraryDialogFragment.kt b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/FileLibraryDialogFragment.kt index f1b6a7b84..bd51d20dc 100755 --- a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/FileLibraryDialogFragment.kt +++ b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/FileLibraryDialogFragment.kt @@ -259,8 +259,11 @@ class FileLibraryDialogFragment : ListPreferenceDialogFragmentCompat() { builder.setAdapter(createPresetAdapter()) { _, position -> val name = fileLibPreference.entries[position] val path = fileLibPreference.entryValues[position] - Preset(File(path.toString()).name).load() - showMessage(getString(R.string.filelibrary_preset_loaded, name)) + val result = Preset(File(path.toString()).name).load() != null + if(result) + showMessage(getString(R.string.filelibrary_preset_loaded, name)) + else + showMessage(getString(R.string.filelibrary_preset_load_failed, name)) this.dismiss() } } @@ -295,11 +298,21 @@ class FileLibraryDialogFragment : ListPreferenceDialogFragmentCompat() { return } + StorageUtils.openInputStreamSafe(requireContext(), uri)?.use { + if(!fileLibPreference.hasValidContent(it)) { + Timber.e("File rejected due to invalid content") + requireContext().showAlert(R.string.filelibrary_corrupted_title, + R.string.filelibrary_corrupted) + return@also + } + } + val file = StorageUtils.importFile(requireContext(), fileLibPreference.directory?.absolutePath ?: "", uri) if(file == null) { Timber.e("Failed to import file") + return } CoroutineScope(Dispatchers.Main).launch { diff --git a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/model/Preset.kt b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/model/Preset.kt index 7a1b2c81b..d9345729c 100644 --- a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/model/Preset.kt +++ b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/model/Preset.kt @@ -27,7 +27,12 @@ class Preset(val name: String): KoinComponent { return file().renameTo(File(externalPath, newName)) } - fun load(): PresetMetadata { + fun validate(): Boolean { + return Companion.validate(FileInputStream(file())) + } + + fun load(): PresetMetadata? { + val file = file() Timber.d("Loading preset from ${file.path}") @@ -37,27 +42,39 @@ class Preset(val name: String): KoinComponent { targetFolder.mkdir() val metadataBytes = ByteArrayOutputStream() - TarInputStream(BufferedInputStream(FileInputStream(file))).use { tis -> - var entry: TarEntry? - while (tis.nextEntry.also { entry = it } != null) { - val entryName = entry?.name - entryName ?: break - - var count: Int - val data = ByteArray(2048) - BufferedOutputStream(FileOutputStream( - targetFolder.absolutePath + "/" + entryName - )).use { dest -> - while (tis.read(data).also { count = it } != -1) { - if(entryName == "metadata") - metadataBytes.write(data) - else - dest.write(data, 0, count) + try { + TarInputStream(BufferedInputStream(FileInputStream(file))).use { tis -> + var entry: TarEntry? + while (tis.nextEntry.also { entry = it } != null) { + val entryName = entry?.name + entryName ?: break + + if (!isKnownEntry(entryName)) { + Timber.w("Unknown entry name: $entryName") + continue + } + + var count: Int + val data = ByteArray(2048) + BufferedOutputStream(FileOutputStream( + targetFolder.absolutePath + "/" + entryName + )).use { dest -> + while (tis.read(data).also { count = it } != -1) { + if (entryName == "metadata") + metadataBytes.write(data) + else + dest.write(data, 0, count) + } + dest.flush() } - dest.flush() } + metadataBytes.flush() } - metadataBytes.flush() + } + catch(ex: IOException) { + Timber.e("Preset extraction failed.") + Timber.w(ex) + return null } val metadata = mutableMapOf() @@ -70,12 +87,15 @@ class Preset(val name: String): KoinComponent { Timber.d("Loaded preset file version ${metadata[META_VERSION]}") - targetFolder.listFiles()?.forEach next@ { f -> - if(!f.name.startsWith("dsp_") || f.extension != "xml") { - if (f.name != "metadata") - Timber.w("load: Unknown file in archive ${f.name}") + val files = targetFolder.listFiles() + if(files == null || files.isEmpty()) { + Timber.e("Preset archive did not contain any useful data") + return null + } + + files.forEach next@ { f -> + if(!isKnownEntry(f.name) || f.name == "metadata") return@next - } val target = File(currentPath, f.name) f.copyTo(target, overwrite = true) @@ -135,6 +155,42 @@ class Preset(val name: String): KoinComponent { const val META_VERSION = "version" const val META_APP_VERSION = "app_version" const val META_APP_FLAVOR = "app_flavor" + + private fun isKnownEntry(n: String) = (n.startsWith("dsp_") && n.endsWith("xml")) || n == "metadata" + + fun validate(stream: InputStream): Boolean { + Timber.d("Validating preset") + + var knownCount = 0 + try { + TarInputStream(BufferedInputStream(stream)).use { tis -> + var entry: TarEntry? + while (tis.nextEntry.also { entry = it } != null) { + val entryName = entry?.name + entryName ?: break + + if (!isKnownEntry(entryName)) { + Timber.w("Unknown entry name: $entryName") + continue + } + + knownCount++ + } + } + } + catch(ex: IOException) { + Timber.e("Preset validation failed.") + Timber.w(ex) + return false + } + + if (knownCount < 1) { + Timber.e("Preset archive did not contain any useful data") + return false + } + + return true + } } } diff --git a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/preference/FileLibraryPreference.kt b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/preference/FileLibraryPreference.kt index 58a25e2cc..e122748ff 100644 --- a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/preference/FileLibraryPreference.kt +++ b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/preference/FileLibraryPreference.kt @@ -9,7 +9,9 @@ import androidx.preference.ListPreference import androidx.preference.Preference.SummaryProvider import me.timschneeberger.rootlessjamesdsp.R import me.timschneeberger.rootlessjamesdsp.fragment.FileLibraryDialogFragment +import me.timschneeberger.rootlessjamesdsp.model.Preset import java.io.File +import java.io.InputStream class FileLibraryPreference(context: Context, attrs: AttributeSet?) : @@ -93,6 +95,13 @@ class FileLibraryPreference(context: Context, attrs: AttributeSet?) : (isPreset() && hasPresetExtension(it)) } + fun hasValidContent(stream: InputStream): Boolean { + return if (isPreset()) + Preset.validate(stream) + else + true + } + fun isLiveprog(): Boolean { return type.lowercase() == "liveprog" } diff --git a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/utils/StorageUtils.kt b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/utils/StorageUtils.kt index 136b31da6..2ba451bd0 100644 --- a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/utils/StorageUtils.kt +++ b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/utils/StorageUtils.kt @@ -29,6 +29,16 @@ object StorageUtils { return destinationFilename } + fun openInputStreamSafe(context: Context, uri: Uri): InputStream? { + return try { + context.contentResolver.openInputStream(uri) + } catch (ex: Exception) { + Timber.e(ex.message) + ex.printStackTrace() + null + } + } + private fun createFileFromStream(ins: InputStream, destination: File?): Boolean { try { FileOutputStream(destination).use { os -> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 45ddbb9c2..79cb40488 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -334,6 +334,8 @@ New preset Unsupported file type The selected file has not have the correct file extension. + Unsupported or corrupted file + The selected file contains no useable data or is corrupted. Failed to access directory No file selected New file name @@ -345,6 +347,7 @@ Renamed to \'%1$s\' \'%1$s\' deleted Preset \'%1$s\' loaded + File \'%1$s\' is not compatible with this app No presets saved. Tap \'Add\' to create a new one.