summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt106
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt67
-rw-r--r--src/android/app/src/main/res/values/strings.xml1
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 d6418a666..16a794dee 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() {
50 } 50 }
51 } 51 }
52} 52}
53
54enum class TaskState {
55 Completed,
56 Failed,
57 Cancelled
58}
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 1164dfe94..0cb701476 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
51import org.yuzu.yuzu_emu.getPublicFilesDir 51import org.yuzu.yuzu_emu.getPublicFilesDir
52import org.yuzu.yuzu_emu.model.GamesViewModel 52import org.yuzu.yuzu_emu.model.GamesViewModel
53import org.yuzu.yuzu_emu.model.HomeViewModel 53import org.yuzu.yuzu_emu.model.HomeViewModel
54import org.yuzu.yuzu_emu.model.TaskState
54import org.yuzu.yuzu_emu.model.TaskViewModel 55import org.yuzu.yuzu_emu.model.TaskViewModel
55import org.yuzu.yuzu_emu.utils.* 56import org.yuzu.yuzu_emu.utils.*
56import java.io.BufferedInputStream 57import java.io.BufferedInputStream
57import java.io.BufferedOutputStream 58import java.io.BufferedOutputStream
58import java.io.FileInputStream
59import java.io.FileOutputStream 59import java.io.FileOutputStream
60import java.time.LocalDateTime 60import java.time.LocalDateTime
61import java.time.format.DateTimeFormatter 61import java.time.format.DateTimeFormatter
62import java.util.zip.ZipEntry 62import java.util.zip.ZipEntry
63import java.util.zip.ZipInputStream 63import java.util.zip.ZipInputStream
64import java.util.zip.ZipOutputStream
65 64
66class MainActivity : AppCompatActivity(), ThemeProvider { 65class MainActivity : AppCompatActivity(), ThemeProvider {
67 private lateinit var binding: ActivityMainBinding 66 private lateinit var binding: ActivityMainBinding
@@ -396,7 +395,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
396 val task: () -> Any = { 395 val task: () -> Any = {
397 var messageToShow: Any 396 var messageToShow: Any
398 try { 397 try {
399 FileUtil.unzip(inputZip, cacheFirmwareDir) 398 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir)
400 val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 399 val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
401 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 400 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
402 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { 401 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
@@ -639,35 +638,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
639 R.string.exporting_user_data, 638 R.string.exporting_user_data,
640 true 639 true
641 ) { 640 ) {
642 val zos = ZipOutputStream( 641 val zipResult = FileUtil.zipFromInternalStorage(
643 BufferedOutputStream(contentResolver.openOutputStream(result)) 642 File(DirectoryInitialization.userDirectory!!),
643 DirectoryInitialization.userDirectory!!,
644 BufferedOutputStream(contentResolver.openOutputStream(result)),
645 taskViewModel.cancelled
644 ) 646 )
645 zos.use { stream -> 647 return@newInstance when (zipResult) {
646 File(DirectoryInitialization.userDirectory!!).walkTopDown().forEach { file -> 648 TaskState.Completed -> getString(R.string.user_data_export_success)
647 if (taskViewModel.cancelled.value) { 649 TaskState.Failed -> R.string.export_failed
648 return@newInstance R.string.user_data_export_cancelled 650 TaskState.Cancelled -> R.string.user_data_export_cancelled
649 }
650
651 if (!file.isDirectory) {
652 val newPath = file.path.substring(
653 DirectoryInitialization.userDirectory!!.length,
654 file.path.length
655 )
656 stream.putNextEntry(ZipEntry(newPath))
657
658 val buffer = ByteArray(8096)
659 var read: Int
660 FileInputStream(file).use { fis ->
661 while (fis.read(buffer).also { read = it } != -1) {
662 stream.write(buffer, 0, read)
663 }
664 }
665
666 stream.closeEntry()
667 }
668 }
669 } 651 }
670 return@newInstance getString(R.string.user_data_export_success)
671 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 652 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
672 } 653 }
673 654
@@ -698,40 +679,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
698 return@newInstance getString(R.string.invalid_yuzu_backup) 679 return@newInstance getString(R.string.invalid_yuzu_backup)
699 } 680 }
700 681
682 // Clear existing user data
701 File(DirectoryInitialization.userDirectory!!).deleteRecursively() 683 File(DirectoryInitialization.userDirectory!!).deleteRecursively()
702 684
703 val zis = 685 // Copy archive to internal storage
704 ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) 686 try {
705 val userDirectory = File(DirectoryInitialization.userDirectory!!) 687 FileUtil.unzipToInternalStorage(
706 val canonicalPath = userDirectory.canonicalPath + '/' 688 BufferedInputStream(contentResolver.openInputStream(result)),
707 zis.use { stream -> 689 File(DirectoryInitialization.userDirectory!!)
708 var ze: ZipEntry? = stream.nextEntry 690 )
709 while (ze != null) { 691 } catch (e: Exception) {
710 val newFile = File(userDirectory, ze!!.name) 692 return@newInstance getString(R.string.invalid_yuzu_backup)
711 val destinationDirectory =
712 if (ze!!.isDirectory) newFile else newFile.parentFile
713
714 if (!newFile.canonicalPath.startsWith(canonicalPath)) {
715 throw SecurityException(
716 "Zip file attempted path traversal! ${ze!!.name}"
717 )
718 }
719
720 if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
721 throw IOException("Failed to create directory $destinationDirectory")
722 }
723
724 if (!ze!!.isDirectory) {
725 val buffer = ByteArray(8096)
726 var read: Int
727 BufferedOutputStream(FileOutputStream(newFile)).use { bos ->
728 while (zis.read(buffer).also { read = it } != -1) {
729 bos.write(buffer, 0, read)
730 }
731 }
732 }
733 ze = stream.nextEntry
734 }
735 } 693 }
736 694
737 // Reinitialize relevant data 695 // Reinitialize relevant data
@@ -758,19 +716,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
758 }.zip" 716 }.zip"
759 ) 717 )
760 outputZipFile.createNewFile() 718 outputZipFile.createNewFile()
761 ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> 719 val result = FileUtil.zipFromInternalStorage(
762 saveFolder.walkTopDown().forEach { file -> 720 saveFolder,
763 val zipFileName = 721 savesFolderRoot,
764 file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") 722 BufferedOutputStream(FileOutputStream(outputZipFile))
765 if (zipFileName == "") { 723 )
766 return@forEach 724 if (result == TaskState.Failed) {
767 } 725 return false
768 val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
769 zos.putNextEntry(entry)
770 if (file.isFile) {
771 file.inputStream().use { fis -> fis.copyTo(zos) }
772 }
773 }
774 } 726 }
775 lastZipCreated = outputZipFile 727 lastZipCreated = outputZipFile
776 } catch (e: Exception) { 728 } catch (e: Exception) {
@@ -832,7 +784,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
832 784
833 NativeLibrary.initializeEmptyUserDirectory() 785 NativeLibrary.initializeEmptyUserDirectory()
834 786
835 val inputZip = applicationContext.contentResolver.openInputStream(result) 787 val inputZip = contentResolver.openInputStream(result)
836 // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. 788 // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
837 var validZip = false 789 var validZip = false
838 val savesFolder = File(savesFolderRoot) 790 val savesFolder = File(savesFolderRoot)
@@ -853,7 +805,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
853 805
854 try { 806 try {
855 CoroutineScope(Dispatchers.IO).launch { 807 CoroutineScope(Dispatchers.IO).launch {
856 FileUtil.unzip(inputZip, cacheSaveDir) 808 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
857 cacheSaveDir.list(filterTitleId)?.forEach { savePath -> 809 cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
858 File(savesFolder, savePath).deleteRecursively() 810 File(savesFolder, savePath).deleteRecursively()
859 File(cacheSaveDir, savePath).copyRecursively( 811 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 142af5f26..c3f53f1c5 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
8import android.net.Uri 8import android.net.Uri
9import android.provider.DocumentsContract 9import android.provider.DocumentsContract
10import androidx.documentfile.provider.DocumentFile 10import androidx.documentfile.provider.DocumentFile
11import kotlinx.coroutines.flow.StateFlow
11import java.io.BufferedInputStream 12import java.io.BufferedInputStream
12import java.io.File 13import java.io.File
13import java.io.FileOutputStream 14import java.io.FileOutputStream
@@ -18,6 +19,9 @@ import java.util.zip.ZipEntry
18import java.util.zip.ZipInputStream 19import java.util.zip.ZipInputStream
19import org.yuzu.yuzu_emu.YuzuApplication 20import org.yuzu.yuzu_emu.YuzuApplication
20import org.yuzu.yuzu_emu.model.MinimalDocumentFile 21import org.yuzu.yuzu_emu.model.MinimalDocumentFile
22import org.yuzu.yuzu_emu.model.TaskState
23import java.io.BufferedOutputStream
24import java.util.zip.ZipOutputStream
21 25
22object FileUtil { 26object FileUtil {
23 const val PATH_TREE = "tree" 27 const val PATH_TREE = "tree"
@@ -282,30 +286,65 @@ object FileUtil {
282 286
283 /** 287 /**
284 * Extracts the given zip file into the given directory. 288 * Extracts the given zip file into the given directory.
285 * @exception IOException if the file was being created outside of the target directory
286 */ 289 */
287 @Throws(SecurityException::class) 290 @Throws(SecurityException::class)
288 fun unzip(zipStream: InputStream, destDir: File): Boolean { 291 fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) {
289 ZipInputStream(BufferedInputStream(zipStream)).use { zis -> 292 ZipInputStream(zipStream).use { zis ->
290 var entry: ZipEntry? = zis.nextEntry 293 var entry: ZipEntry? = zis.nextEntry
291 while (entry != null) { 294 while (entry != null) {
292 val entryName = entry.name 295 val newFile = File(destDir, entry.name)
293 val entryFile = File(destDir, entryName) 296 val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
294 if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) { 297
295 throw SecurityException("Entry is outside of the target dir: " + entryFile.name) 298 if (!newFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
299 throw SecurityException("Zip file attempted path traversal! ${entry.name}")
300 }
301
302 if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
303 throw IOException("Failed to create directory $destinationDirectory")
296 } 304 }
297 if (entry.isDirectory) { 305
298 entryFile.mkdirs() 306 if (!entry.isDirectory) {
299 } else { 307 newFile.outputStream().use { fos -> zis.copyTo(fos) }
300 entryFile.parentFile?.mkdirs()
301 entryFile.createNewFile()
302 entryFile.outputStream().use { fos -> zis.copyTo(fos) }
303 } 308 }
304 entry = zis.nextEntry 309 entry = zis.nextEntry
305 } 310 }
306 } 311 }
312 }
307 313
308 return true 314 /**
315 * Creates a zip file from a directory within internal storage
316 * @param inputFile File representation of the item that will be zipped
317 * @param rootDir Directory containing the inputFile
318 * @param outputStream Stream where the zip file will be output
319 */
320 fun zipFromInternalStorage(
321 inputFile: File,
322 rootDir: String,
323 outputStream: BufferedOutputStream,
324 cancelled: StateFlow<Boolean>? = null
325 ): TaskState {
326 try {
327 ZipOutputStream(outputStream).use { zos ->
328 inputFile.walkTopDown().forEach { file ->
329 if (cancelled?.value == true) {
330 return TaskState.Cancelled
331 }
332
333 if (!file.isDirectory) {
334 val entryName =
335 file.absolutePath.removePrefix(rootDir).removePrefix("/")
336 val entry = ZipEntry(entryName)
337 zos.putNextEntry(entry)
338 if (file.isFile) {
339 file.inputStream().use { fis -> fis.copyTo(zos) }
340 }
341 }
342 }
343 }
344 } catch (e: Exception) {
345 return TaskState.Failed
346 }
347 return TaskState.Completed
309 } 348 }
310 349
311 fun isRootTreeUri(uri: Uri): Boolean { 350 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 067141866..485d4c1dd 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 @@
229 <string name="string_null">Null</string> 229 <string name="string_null">Null</string>
230 <string name="string_import">Import</string> 230 <string name="string_import">Import</string>
231 <string name="export">Export</string> 231 <string name="export">Export</string>
232 <string name="export_failed">Export failed</string>
232 <string name="cancelling">Cancelling</string> 233 <string name="cancelling">Cancelling</string>
233 234
234 <!-- GPU driver installation --> 235 <!-- GPU driver installation -->