From 0d1c7965df2528f1aa36485582810d78c00ae1c8 Mon Sep 17 00:00:00 2001 From: lynxnb Date: Fri, 22 Jul 2022 13:17:29 +0200 Subject: [PATCH] Add a `ZipUtils` class for unpacking zip files --- .../main/java/emu/skyline/utils/ZipUtils.kt | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 app/src/main/java/emu/skyline/utils/ZipUtils.kt diff --git a/app/src/main/java/emu/skyline/utils/ZipUtils.kt b/app/src/main/java/emu/skyline/utils/ZipUtils.kt new file mode 100644 index 00000000..46caf9e7 --- /dev/null +++ b/app/src/main/java/emu/skyline/utils/ZipUtils.kt @@ -0,0 +1,111 @@ +/* + * 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.lang.reflect.Field +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 + } + } +}