skyline/app/src/main/java/emu/skyline/utils/ZipUtils.kt
2022-11-23 19:49:42 +05:30

111 lines
4.9 KiB
Kotlin

/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.utils
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
interface ZipUtils {
companion object {
/**
* Extracts a zip file to the given target directory.
* @exception IOException if unzipping fails for any reason
*/
@Throws(IOException::class)
fun unzip(file : File, targetDirectory : File) {
ZipFile(file).use { zipFile ->
for (zipEntry in zipFile.entries()) {
val destFile = createNewFile(targetDirectory, zipEntry)
// If the zip entry is a file, we need to create its parent directories
val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile
// Create the destination directory
if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs()))
throw FileNotFoundException("Failed to create destination directory: $destDirectory")
// If the entry is a directory we don't need to copy anything
if (zipEntry.isDirectory)
continue
// Copy bytes to destination
try {
zipFile.getInputStream(zipEntry).use { inputStream ->
destFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
} catch (e : IOException) {
if (destFile.exists())
destFile.delete()
throw e
}
}
}
}
/**
* Extracts a zip file from the given stream to the given target directory.
*
* This method is ~5x slower than [unzip], as [ZipInputStream] uses a fixed `512` bytes buffer for inflation,
* instead of `8192` bytes or more used by input streams returned by [ZipFile].
* This results in ~8x the amount of JNI calls, producing an increased number of array bounds checking, which kills performance.
* Usage of this method is discouraged when possible, [unzip] should be used instead.
* Nevertheless, it's the only option when extracting zips obtained from content URIs, as a [File] object cannot be obtained from them.
* @exception IOException if unzipping fails for any reason
*/
@Throws(IOException::class)
fun unzip(stream : InputStream, targetDirectory : File) {
ZipInputStream(BufferedInputStream(stream)).use { zis ->
do {
// Get the next entry, break if we've reached the end
val zipEntry = zis.nextEntry ?: break
val destFile = createNewFile(targetDirectory, zipEntry)
// If the zip entry is a file, we need to create its parent directories
val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile
// Create the destination directory
if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs()))
throw FileNotFoundException("Failed to create destination directory: $destDirectory")
// If the entry is a directory we don't need to copy anything
if (zipEntry.isDirectory)
continue
// Copy bytes to destination
try {
BufferedOutputStream(destFile.outputStream()).use { zis.copyTo(it) }
} catch (e : IOException) {
if (destFile.exists())
destFile.delete()
throw e
}
} while (true)
}
}
/**
* Safely creates a new destination file where the given zip entry will be extracted to.
*
* @exception IOException if the file was being created outside of the target directory
* **see:** [Zip Slip](https://github.com/snyk/zip-slip-vulnerability)
*/
@Throws(IOException::class)
private fun createNewFile(destinationDir : File, zipEntry : ZipEntry) : File {
val destFile = File(destinationDir, zipEntry.name)
val destDirPath = destinationDir.canonicalPath
val destFilePath = destFile.canonicalPath
if (!destFilePath.startsWith(destDirPath + File.separator))
throw IOException("Entry is outside of the target dir: " + zipEntry.name)
return destFile
}
}
}