diff options
| author | 2023-09-26 13:26:06 -0400 | |
|---|---|---|
| committer | 2023-09-26 13:26:20 -0400 | |
| commit | c8673a16bbf84bcbacbe73cfae5500bc3bfe992b (patch) | |
| tree | 0c350a7f2501e693e3eb1906679ae0dc7957893b | |
| parent | android: Consolidate installers to one fragment (diff) | |
| download | yuzu-c8673a16bbf84bcbacbe73cfae5500bc3bfe992b.tar.gz yuzu-c8673a16bbf84bcbacbe73cfae5500bc3bfe992b.tar.xz yuzu-c8673a16bbf84bcbacbe73cfae5500bc3bfe992b.zip | |
android: Refactor zip code into FileUtil
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 | |||
| 54 | enum 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 | |||
| 51 | import org.yuzu.yuzu_emu.getPublicFilesDir | 51 | import org.yuzu.yuzu_emu.getPublicFilesDir |
| 52 | import org.yuzu.yuzu_emu.model.GamesViewModel | 52 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 53 | import org.yuzu.yuzu_emu.model.HomeViewModel | 53 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 54 | import org.yuzu.yuzu_emu.model.TaskState | ||
| 54 | import org.yuzu.yuzu_emu.model.TaskViewModel | 55 | import org.yuzu.yuzu_emu.model.TaskViewModel |
| 55 | import org.yuzu.yuzu_emu.utils.* | 56 | import org.yuzu.yuzu_emu.utils.* |
| 56 | import java.io.BufferedInputStream | 57 | import java.io.BufferedInputStream |
| 57 | import java.io.BufferedOutputStream | 58 | import java.io.BufferedOutputStream |
| 58 | import java.io.FileInputStream | ||
| 59 | import java.io.FileOutputStream | 59 | import java.io.FileOutputStream |
| 60 | import java.time.LocalDateTime | 60 | import java.time.LocalDateTime |
| 61 | import java.time.format.DateTimeFormatter | 61 | import java.time.format.DateTimeFormatter |
| 62 | import java.util.zip.ZipEntry | 62 | import java.util.zip.ZipEntry |
| 63 | import java.util.zip.ZipInputStream | 63 | import java.util.zip.ZipInputStream |
| 64 | import java.util.zip.ZipOutputStream | ||
| 65 | 64 | ||
| 66 | class MainActivity : AppCompatActivity(), ThemeProvider { | 65 | class 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 | |||
| 8 | import android.net.Uri | 8 | import android.net.Uri |
| 9 | import android.provider.DocumentsContract | 9 | import android.provider.DocumentsContract |
| 10 | import androidx.documentfile.provider.DocumentFile | 10 | import androidx.documentfile.provider.DocumentFile |
| 11 | import kotlinx.coroutines.flow.StateFlow | ||
| 11 | import java.io.BufferedInputStream | 12 | import java.io.BufferedInputStream |
| 12 | import java.io.File | 13 | import java.io.File |
| 13 | import java.io.FileOutputStream | 14 | import java.io.FileOutputStream |
| @@ -18,6 +19,9 @@ import java.util.zip.ZipEntry | |||
| 18 | import java.util.zip.ZipInputStream | 19 | import java.util.zip.ZipInputStream |
| 19 | import org.yuzu.yuzu_emu.YuzuApplication | 20 | import org.yuzu.yuzu_emu.YuzuApplication |
| 20 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile | 21 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile |
| 22 | import org.yuzu.yuzu_emu.model.TaskState | ||
| 23 | import java.io.BufferedOutputStream | ||
| 24 | import java.util.zip.ZipOutputStream | ||
| 21 | 25 | ||
| 22 | object FileUtil { | 26 | object 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 --> |