Browse Source

android: Refactor zip code into FileUtil

nce_cpp
Charles Lombardo 2 years ago
parent
commit
8c7d6c0675
  1. 6
      src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
  2. 106
      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
}

106
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
}
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) }.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}"
)
}
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 // 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 (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
throw IOException("Failed to create directory $destinationDirectory")
} }
if (entry.isDirectory) {
entryFile.mkdirs()
} else {
entryFile.parentFile?.mkdirs()
entryFile.createNewFile()
entryFile.outputStream().use { fos -> zis.copyTo(fos) }
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