forked from suyu/suyu
android: Refactor zip code into FileUtil
This commit is contained in:
parent
e9e6296893
commit
c8673a16bb
4 changed files with 89 additions and 91 deletions
|
@ -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.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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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…
Reference in a new issue