diff --git a/app/src/main/java/emu/skyline/AppDialog.kt b/app/src/main/java/emu/skyline/AppDialog.kt index 7bb82b71..0ce26ab1 100644 --- a/app/src/main/java/emu/skyline/AppDialog.kt +++ b/app/src/main/java/emu/skyline/AppDialog.kt @@ -34,10 +34,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.File import java.io.FileOutputStream import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream /** @@ -65,20 +67,31 @@ class AppDialog : BottomSheetDialogFragment() { private val savesFolderRoot by lazy { "${requireContext().getPublicFilesDir().canonicalPath}/switch/nand/user/save/0000000000000000/00000000000000000000000000000001/" } private val documentPicker = registerForActivityResult(ActivityResultContracts.OpenDocument()) { it?.let { uri -> - if (uri.toString().takeLast(20).removeSuffix(".zip") == item.titleId) { - val saveFolder = File(savesFolderRoot + item.titleId) - val inputZip = requireContext().contentResolver.openInputStream(uri) + try { + val savesFolder = File(savesFolderRoot) + var inputZip = requireContext().contentResolver.openInputStream(uri) + var validZip = false + // A TitleID must be the first folder name inside the zip in order to be considered valid. if (inputZip != null) { - CoroutineScope(Dispatchers.IO).launch { - ZipUtils.unzip(inputZip, saveFolder) - withContext(Dispatchers.Main){ - binding.deleteSave.isEnabled = true - binding.exportSave.isEnabled = true - } + ZipInputStream(BufferedInputStream(inputZip)).use { zis -> + validZip = Regex("^0100[0-9A-Fa-f]{12}/\$").matches(zis.nextEntry.name) } } - } else { - Snackbar.make(binding.root, getString(R.string.zip_with_save_must_have_name_equal_titleid), Snackbar.LENGTH_LONG).show() + inputZip = requireContext().contentResolver.openInputStream(uri) + if (inputZip != null && validZip){ + CoroutineScope(Dispatchers.IO).launch { + ZipUtils.unzip(inputZip, savesFolder) + withContext(Dispatchers.Main){ + val isSaveFileOfThisGame = File("$savesFolderRoot${item.titleId}").exists() + binding.deleteSave.isEnabled = isSaveFileOfThisGame + binding.exportSave.isEnabled = isSaveFileOfThisGame + } + } + } else { + Snackbar.make(binding.root, getString(R.string.save_file_invalid_zip_structure), Snackbar.LENGTH_LONG).show() + } + } catch (e: Exception) { + Snackbar.make(binding.root, getString(R.string.error), Snackbar.LENGTH_LONG).show() } } } @@ -162,24 +175,31 @@ class AppDialog : BottomSheetDialogFragment() { binding.exportSave.isEnabled = saveExists binding.exportSave.setOnClickListener { CoroutineScope(Dispatchers.IO).launch { - val saveFolder = File(saveFolderPath) - //val outputZipFile = File.createTempFile("out", ".zip") - val outputZipFile = File("$saveFolderPath.zip") - if (outputZipFile.exists()) outputZipFile.delete() - outputZipFile.createNewFile() - ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> - saveFolder.walkTopDown().forEach { file -> - val zipFileName = file.absolutePath.removePrefix(saveFolder.absolutePath).removePrefix("/") - if (zipFileName == "") return@forEach - val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") - zos.putNextEntry(entry) - if (file.isFile) { - file.inputStream().use { fis -> fis.copyTo(zos) } + try { + val saveFolder = File(saveFolderPath) + val outputZipFile = File("$savesFolderRoot${item.title}.zip") + outputZipFile.delete() + outputZipFile.createNewFile() + ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> + saveFolder.walkTopDown().forEach { file -> + val zipFileName = file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") + if (zipFileName == "") return@forEach + val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") + zos.putNextEntry(entry) + if (file.isFile) { + file.inputStream().use { fis -> fis.copyTo(zos) } + } } } + } catch (e: Exception) { + withContext(Dispatchers.Main){ + Snackbar.make(binding.root, e.message as CharSequence, Snackbar.LENGTH_LONG).show() + } + return@launch } + withContext(Dispatchers.Main) { - val file = DocumentFile.fromSingleUri(requireContext(), DocumentsContract.buildDocumentUri(DocumentsProvider.AUTHORITY, "${DocumentsProvider.ROOT_ID}/switch/nand/user/save/0000000000000000/00000000000000000000000000000001/${item.titleId}.zip"))!! + val file = DocumentFile.fromSingleUri(requireContext(), DocumentsContract.buildDocumentUri(DocumentsProvider.AUTHORITY, "${DocumentsProvider.ROOT_ID}/switch/nand/user/save/0000000000000000/00000000000000000000000000000001/${item.title}.zip"))!! val intent = Intent(Intent.ACTION_SEND) .setDataAndType(file.uri, "application/zip") .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) @@ -208,6 +228,6 @@ class AppDialog : BottomSheetDialogFragment() { override fun onDestroyView(){ super.onDestroyView() - File("$savesFolderRoot${item.titleId}.zip").delete() + File("$savesFolderRoot${item.title}.zip").delete() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de352be2..38197622 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -283,5 +283,5 @@ This action is irreversible Yes No - Zip file must have as name the TitleID of the game + Failed to unzip the provided save file