Browse Source

android: Refactor zip code into FileUtil

pull/15/merge
Charles Lombardo 2 years ago
parent
commit
c8673a16bb
  1. 6
      src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
  2. 104
      src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
  3. 67
      src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
  4. 1
      src/android/app/src/main/res/values/strings.xml

6
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
}

104
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.getPublicFilesDir
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel 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.model.TaskViewModel
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class MainActivity : AppCompatActivity(), ThemeProvider { class MainActivity : AppCompatActivity(), ThemeProvider {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@ -396,7 +395,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val task: () -> Any = { val task: () -> Any = {
var messageToShow: Any var messageToShow: Any
try { try {
FileUtil.unzip(inputZip, cacheFirmwareDir)
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir)
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
@ -639,35 +638,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
R.string.exporting_user_data, R.string.exporting_user_data,
true 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
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
} }
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 getString(R.string.user_data_export_success)
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
@ -698,40 +679,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@newInstance getString(R.string.invalid_yuzu_backup) return@newInstance getString(R.string.invalid_yuzu_backup)
} }
// Clear existing user data
File(DirectoryInitialization.userDirectory!!).deleteRecursively() 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}"
// Copy archive to internal storage
try {
FileUtil.unzipToInternalStorage(
BufferedInputStream(contentResolver.openInputStream(result)),
File(DirectoryInitialization.userDirectory!!)
) )
}
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
}
} catch (e: Exception) {
return@newInstance getString(R.string.invalid_yuzu_backup)
} }
// Reinitialize relevant data // Reinitialize relevant data
@ -758,19 +716,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}.zip" }.zip"
) )
outputZipFile.createNewFile() 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 lastZipCreated = outputZipFile
} catch (e: Exception) { } catch (e: Exception) {
@ -832,7 +784,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
NativeLibrary.initializeEmptyUserDirectory() 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. // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
var validZip = false var validZip = false
val savesFolder = File(savesFolderRoot) val savesFolder = File(savesFolderRoot)
@ -853,7 +805,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
try { try {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
FileUtil.unzip(inputZip, cacheSaveDir)
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
cacheSaveDir.list(filterTitleId)?.forEach { savePath -> cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
File(savesFolder, savePath).deleteRecursively() File(savesFolder, savePath).deleteRecursively()
File(cacheSaveDir, savePath).copyRecursively( File(cacheSaveDir, savePath).copyRecursively(

67
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.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.flow.StateFlow
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@ -18,6 +19,9 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.MinimalDocumentFile 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 { object FileUtil {
const val PATH_TREE = "tree" const val PATH_TREE = "tree"
@ -282,30 +286,65 @@ object FileUtil {
/** /**
* Extracts the given zip file into the given directory. * 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) @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 var entry: ZipEntry? = zis.nextEntry
while (entry != null) { 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 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<Boolean>? = 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 { fun isRootTreeUri(uri: Uri): Boolean {

1
src/android/app/src/main/res/values/strings.xml

@ -229,6 +229,7 @@
<string name="string_null">Null</string> <string name="string_null">Null</string>
<string name="string_import">Import</string> <string name="string_import">Import</string>
<string name="export">Export</string> <string name="export">Export</string>
<string name="export_failed">Export failed</string>
<string name="cancelling">Cancelling</string> <string name="cancelling">Cancelling</string>
<!-- GPU driver installation --> <!-- GPU driver installation -->

Loading…
Cancel
Save