android: Refactor zip code into FileUtil

This commit is contained in:
Charles Lombardo 2023-09-26 13:26:06 -04:00
parent e9e6296893
commit c8673a16bb
4 changed files with 89 additions and 91 deletions

View file

@ -50,3 +50,9 @@ class TaskViewModel : ViewModel() {
} }
} }
} }
enum class TaskState {
Completed,
Failed,
Cancelled
}

View file

@ -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( val zipResult = FileUtil.zipFromInternalStorage(
BufferedOutputStream(contentResolver.openOutputStream(result)) File(DirectoryInitialization.userDirectory!!),
DirectoryInitialization.userDirectory!!,
BufferedOutputStream(contentResolver.openOutputStream(result)),
taskViewModel.cancelled
) )
zos.use { stream -> return@newInstance when (zipResult) {
File(DirectoryInitialization.userDirectory!!).walkTopDown().forEach { file -> TaskState.Completed -> getString(R.string.user_data_export_success)
if (taskViewModel.cancelled.value) { TaskState.Failed -> R.string.export_failed
return@newInstance R.string.user_data_export_cancelled 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 = // Copy archive to internal storage
ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) try {
val userDirectory = File(DirectoryInitialization.userDirectory!!) FileUtil.unzipToInternalStorage(
val canonicalPath = userDirectory.canonicalPath + '/' BufferedInputStream(contentResolver.openInputStream(result)),
zis.use { stream -> File(DirectoryInitialization.userDirectory!!)
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}"
) )
} } catch (e: Exception) {
return@newInstance getString(R.string.invalid_yuzu_backup)
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
}
} }
// 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 -> val result = FileUtil.zipFromInternalStorage(
saveFolder.walkTopDown().forEach { file -> saveFolder,
val zipFileName = savesFolderRoot,
file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") BufferedOutputStream(FileOutputStream(outputZipFile))
if (zipFileName == "") { )
return@forEach if (result == TaskState.Failed) {
} return false
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) }
}
}
} }
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(

View file

@ -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 { fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) {
ZipInputStream(BufferedInputStream(zipStream)).use { zis -> 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 newFile = File(destDir, entry.name)
val entryFile = File(destDir, entryName) val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
throw SecurityException("Entry is outside of the target dir: " + entryFile.name) if (!newFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
throw SecurityException("Zip file attempted path traversal! ${entry.name}")
} }
if (entry.isDirectory) {
entryFile.mkdirs() if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
} else { throw IOException("Failed to create directory $destinationDirectory")
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 {

View file

@ -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 -->