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.