LoginSignup
8
9

More than 3 years have passed since last update.

Kotlin で zip を安全に圧縮・展開する

Last updated at Posted at 2019-11-27

Kotlin で zip を安全に圧縮・展開する

表題の通りです。展開について、ネット上でサンプルコード探していると意外と少なかったり、ディレクトリトラバーサル攻撃に脆弱な実装もあったりしたので、下記リンクのサンプルコードを参考に作ってみました。圧縮はおまけです。
https://www.jpcert.or.jp/java-rules/ids04-j.html
何か間違いや不具合がありましたら、コメントで教えていただけると幸いです。

使い方

// 展開編
// /tmp 下に archive.zip を展開
unzipFile(Paths.get("/tmp/archive.zip")) 
// /tmp/dest 下に archive.zip を展開
unzipFile(Paths.get("/tmp/archive.zip"), Paths.get("/tmp/dest"))

// 圧縮編
// /tmp/src を /tmp/src.zip に圧縮
zipFile(Paths.get("/tmp/src"))
// /tmp/src を /tmp/archive.zip に圧縮
zipFile(Paths.get("/tmp/src"), Paths.get("/tmp/archive.zip"))

展開

// 最大ファイル数
private const val unzipMaxEntries: Int = 1024 * 5
// 単体の最大ファイルサイズ
private const val unzipMaxFileSize: Long = 1024L * 1024L * 1024L * 5L // 5GiB
// 全ファイルの合計最大ファイルサイズ
private const val unzipMaxTotalSize: Long = 1024L * 1024L * 1024L * 5L // 5GiB
// バッファサイズ (参考値: https://gihyo.jp/admin/clip/01/fdt/200810/31)
private const val unzipBufferSize: Int = 1024 * 1024 // 1MiB


fun unzipFile(targetFilePath: Path, destDirPath: Path = targetFilePath.parent, zipFileCoding: Charset = Charset.forName("Shift_JIS")) {
    ZipInputStream(Files.newInputStream(targetFilePath), zipFileCoding).use { f ->
        var zipEntry: ZipEntry?
        var nEntries = 0
        var totalReads = 0L
        val buffer = ByteArray(unzipBufferSize)
        while (f.nextEntry.also { zipEntry = it } != null) {
            val entryPath = Paths.get(zipEntry!!.name).normalize()
            if (entryPath.startsWith(Paths.get(".."))) {
                throw IllegalStateException("File is outside extraction target directory.")
            }
            if (nEntries++ >= unzipMaxEntries) {
                throw IllegalStateException("Too many files to unzip.")
            }

            val dst = destDirPath.resolve(entryPath)
            if (zipEntry!!.isDirectory) {
                Files.createDirectories(dst)
            } else {
                // System.err.println("inflating: $dst")
                Files.createDirectories(dst.parent)

                var totalFileReads = 0L
                var nReads: Int
                FileOutputStream(dst.toFile()).use { fos ->
                    BufferedOutputStream(fos).use { out ->
                        while (f.read(buffer, 0, buffer.size).also { nReads = it } != -1) {
                            totalReads += nReads
                            if (totalReads > unzipMaxTotalSize) {
                                throw IllegalStateException("Total file size being unzipped is too big.")
                            }
                            totalFileReads += nReads
                            if (totalFileReads > unzipMaxFileSize) {
                                throw IllegalStateException("File being unzipped is too big.")
                            }
                            out.write(buffer, 0, nReads)
                        }
                        out.flush()
                    }
                }
                f.closeEntry()
            }
        }
    }
}

圧縮

private const val zipBufferSize = 1024 * 1024 // 1MiB

fun zipFile(targetPath: Path, destFilePath: Path = Paths.get("${targetPath}.zip"), zipFileCoding: Charset = Charset.forName("Shift_JIS")) {
    FileOutputStream(destFilePath.toString()).use { fileOutputStream ->
        ZipOutputStream(BufferedOutputStream(fileOutputStream), zipFileCoding).use { zipOutStream ->
            for (filePath in Files.walk(targetPath).map { it.normalize() }) {
                val file = filePath.toFile()
                val entryPath = "${targetPath.relativize(filePath)}${if (file.isDirectory) "/" else ""}"
                val zipEntry = ZipEntry(String(entryPath.toByteArray(zipFileCoding), zipFileCoding))
                zipOutStream.putNextEntry(zipEntry)
                if (file.isFile) {
                    FileInputStream(file).use { inputStream ->
                        val bytes = ByteArray(zipBufferSize)
                        var length: Int
                        while (inputStream.read(bytes).also { length = it } >= 0) {
                            zipOutStream.write(bytes, 0, length)
                        }
                    }
                }
                zipOutStream.closeEntry()
            }
        }
    }
}

ライセンス

These codes are licensed under CC0.

[CC0](http://creativecommons.org/publicdomain/zero/1.0/deed.ja

8
9
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9