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

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

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

### 使い方

```kotlin
// 展開編
// /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"))
```

### 展開

```kotlin
// 最大ファイル数
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()
            }
        }
    }
}
```


### 圧縮

```kotlin
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://i.creativecommons.org/p/zero/1.0/88x31.png "CC0")](http://creativecommons.org/publicdomain/zero/1.0/deed.ja