From c8673a16bbf84bcbacbe73cfae5500bc3bfe992b Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Tue, 26 Sep 2023 13:26:06 -0400 Subject: [PATCH] android: Refactor zip code into FileUtil --- .../org/yuzu/yuzu_emu/model/TaskViewModel.kt | 6 + .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 106 +++++------------- .../java/org/yuzu/yuzu_emu/utils/FileUtil.kt | 67 ++++++++--- .../app/src/main/res/values/strings.xml | 1 + 4 files changed, 89 insertions(+), 91 deletions(-) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt index d6418a666b..16a794deeb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt @@ -50,3 +50,9 @@ class TaskViewModel : ViewModel() { } } } + +enum class TaskState { + Completed, + Failed, + Cancelled +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 1164dfe94f..0cb7014766 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -51,17 +51,16 @@ import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.getPublicFilesDir import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.utils.* import java.io.BufferedInputStream import java.io.BufferedOutputStream -import java.io.FileInputStream import java.io.FileOutputStream import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.zip.ZipEntry import java.util.zip.ZipInputStream -import java.util.zip.ZipOutputStream class MainActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityMainBinding @@ -396,7 +395,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val task: () -> Any = { var messageToShow: Any try { - FileUtil.unzip(inputZip, cacheFirmwareDir) + FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir) val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { @@ -639,35 +638,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider { R.string.exporting_user_data, true ) { - val zos = ZipOutputStream( - BufferedOutputStream(contentResolver.openOutputStream(result)) + val zipResult = FileUtil.zipFromInternalStorage( + File(DirectoryInitialization.userDirectory!!), + DirectoryInitialization.userDirectory!!, + BufferedOutputStream(contentResolver.openOutputStream(result)), + taskViewModel.cancelled ) - zos.use { stream -> - File(DirectoryInitialization.userDirectory!!).walkTopDown().forEach { file -> - if (taskViewModel.cancelled.value) { - return@newInstance R.string.user_data_export_cancelled - } - - if (!file.isDirectory) { - val newPath = file.path.substring( - DirectoryInitialization.userDirectory!!.length, - file.path.length - ) - stream.putNextEntry(ZipEntry(newPath)) - - val buffer = ByteArray(8096) - var read: Int - FileInputStream(file).use { fis -> - while (fis.read(buffer).also { read = it } != -1) { - stream.write(buffer, 0, read) - } - } - - stream.closeEntry() - } - } + return@newInstance when (zipResult) { + TaskState.Completed -> getString(R.string.user_data_export_success) + TaskState.Failed -> R.string.export_failed + TaskState.Cancelled -> R.string.user_data_export_cancelled } - return@newInstance getString(R.string.user_data_export_success) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) } @@ -698,40 +679,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return@newInstance getString(R.string.invalid_yuzu_backup) } + // Clear existing user data File(DirectoryInitialization.userDirectory!!).deleteRecursively() - val zis = - ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) - val userDirectory = File(DirectoryInitialization.userDirectory!!) - val canonicalPath = userDirectory.canonicalPath + '/' - zis.use { stream -> - var ze: ZipEntry? = stream.nextEntry - while (ze != null) { - val newFile = File(userDirectory, ze!!.name) - val destinationDirectory = - if (ze!!.isDirectory) newFile else newFile.parentFile - - if (!newFile.canonicalPath.startsWith(canonicalPath)) { - throw SecurityException( - "Zip file attempted path traversal! ${ze!!.name}" - ) - } - - if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) { - throw IOException("Failed to create directory $destinationDirectory") - } - - if (!ze!!.isDirectory) { - val buffer = ByteArray(8096) - var read: Int - BufferedOutputStream(FileOutputStream(newFile)).use { bos -> - while (zis.read(buffer).also { read = it } != -1) { - bos.write(buffer, 0, read) - } - } - } - ze = stream.nextEntry - } + // Copy archive to internal storage + try { + FileUtil.unzipToInternalStorage( + BufferedInputStream(contentResolver.openInputStream(result)), + File(DirectoryInitialization.userDirectory!!) + ) + } catch (e: Exception) { + return@newInstance getString(R.string.invalid_yuzu_backup) } // Reinitialize relevant data @@ -758,19 +716,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { }.zip" ) 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) } - } - } + val result = FileUtil.zipFromInternalStorage( + saveFolder, + savesFolderRoot, + BufferedOutputStream(FileOutputStream(outputZipFile)) + ) + if (result == TaskState.Failed) { + return false } lastZipCreated = outputZipFile } catch (e: Exception) { @@ -832,7 +784,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { NativeLibrary.initializeEmptyUserDirectory() - val inputZip = applicationContext.contentResolver.openInputStream(result) + val inputZip = contentResolver.openInputStream(result) // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. var validZip = false val savesFolder = File(savesFolderRoot) @@ -853,7 +805,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { try { CoroutineScope(Dispatchers.IO).launch { - FileUtil.unzip(inputZip, cacheSaveDir) + FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) cacheSaveDir.list(filterTitleId)?.forEach { savePath -> File(savesFolder, savePath).deleteRecursively() File(cacheSaveDir, savePath).copyRecursively( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index 142af5f264..c3f53f1c52 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -8,6 +8,7 @@ import android.database.Cursor import android.net.Uri import android.provider.DocumentsContract import androidx.documentfile.provider.DocumentFile +import kotlinx.coroutines.flow.StateFlow import java.io.BufferedInputStream import java.io.File import java.io.FileOutputStream @@ -18,6 +19,9 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.model.MinimalDocumentFile +import org.yuzu.yuzu_emu.model.TaskState +import java.io.BufferedOutputStream +import java.util.zip.ZipOutputStream object FileUtil { const val PATH_TREE = "tree" @@ -282,30 +286,65 @@ object FileUtil { /** * Extracts the given zip file into the given directory. - * @exception IOException if the file was being created outside of the target directory */ @Throws(SecurityException::class) - fun unzip(zipStream: InputStream, destDir: File): Boolean { - ZipInputStream(BufferedInputStream(zipStream)).use { zis -> + fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) { + ZipInputStream(zipStream).use { zis -> var entry: ZipEntry? = zis.nextEntry while (entry != null) { - val entryName = entry.name - val entryFile = File(destDir, entryName) - if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) { - throw SecurityException("Entry is outside of the target dir: " + entryFile.name) + val newFile = File(destDir, entry.name) + val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile + + if (!newFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) { + throw SecurityException("Zip file attempted path traversal! ${entry.name}") } - if (entry.isDirectory) { - entryFile.mkdirs() - } else { - entryFile.parentFile?.mkdirs() - entryFile.createNewFile() - entryFile.outputStream().use { fos -> zis.copyTo(fos) } + + if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) { + throw IOException("Failed to create directory $destinationDirectory") + } + + if (!entry.isDirectory) { + newFile.outputStream().use { fos -> zis.copyTo(fos) } } entry = zis.nextEntry } } + } - return true + /** + * Creates a zip file from a directory within internal storage + * @param inputFile File representation of the item that will be zipped + * @param rootDir Directory containing the inputFile + * @param outputStream Stream where the zip file will be output + */ + fun zipFromInternalStorage( + inputFile: File, + rootDir: String, + outputStream: BufferedOutputStream, + cancelled: StateFlow? = null + ): TaskState { + try { + ZipOutputStream(outputStream).use { zos -> + inputFile.walkTopDown().forEach { file -> + if (cancelled?.value == true) { + return TaskState.Cancelled + } + + if (!file.isDirectory) { + val entryName = + file.absolutePath.removePrefix(rootDir).removePrefix("/") + val entry = ZipEntry(entryName) + zos.putNextEntry(entry) + if (file.isFile) { + file.inputStream().use { fis -> fis.copyTo(zos) } + } + } + } + } + } catch (e: Exception) { + return TaskState.Failed + } + return TaskState.Completed } fun isRootTreeUri(uri: Uri): Boolean { diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 067141866e..485d4c1ddf 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -229,6 +229,7 @@ Null Import Export + Export failed Cancelling