android: Refactor zip code into FileUtil
This commit is contained in:
parent
e9e6296893
commit
c8673a16bb
|
@ -50,3 +50,9 @@ class TaskViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class TaskState {
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled
|
||||
}
|
||||
|
|
|
@ -51,17 +51,16 @@ import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
|||
import org.yuzu.yuzu_emu.getPublicFilesDir
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
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.utils.*
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
@ -396,7 +395,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
val task: () -> Any = {
|
||||
var messageToShow: Any
|
||||
try {
|
||||
FileUtil.unzip(inputZip, cacheFirmwareDir)
|
||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir)
|
||||
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
|
||||
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
|
||||
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
|
||||
|
@ -639,35 +638,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
R.string.exporting_user_data,
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -698,40 +679,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
return@newInstance getString(R.string.invalid_yuzu_backup)
|
||||
}
|
||||
|
||||
// Clear existing user data
|
||||
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
|
||||
|
@ -758,19 +716,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
}.zip"
|
||||
)
|
||||
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
|
||||
} catch (e: Exception) {
|
||||
|
@ -832,7 +784,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
|
||||
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.
|
||||
var validZip = false
|
||||
val savesFolder = File(savesFolderRoot)
|
||||
|
@ -853,7 +805,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
|
||||
try {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
FileUtil.unzip(inputZip, cacheSaveDir)
|
||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
||||
cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
|
||||
File(savesFolder, savePath).deleteRecursively()
|
||||
File(cacheSaveDir, savePath).copyRecursively(
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.database.Cursor
|
|||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
@ -18,6 +19,9 @@ import java.util.zip.ZipEntry
|
|||
import java.util.zip.ZipInputStream
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
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 {
|
||||
const val PATH_TREE = "tree"
|
||||
|
@ -282,30 +286,65 @@ object FileUtil {
|
|||
|
||||
/**
|
||||
* 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)
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -229,6 +229,7 @@
|
|||
<string name="string_null">Null</string>
|
||||
<string name="string_import">Import</string>
|
||||
<string name="export">Export</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="cancelling">Cancelling</string>
|
||||
|
||||
<!-- GPU driver installation -->
|
||||
|
|
Loading…
Reference in a new issue