JavaでZIPファイルを圧縮・解凍する方法|完全なサンプルとZip Slip対策

JavaでZIPファイルを扱うには、標準の java.util.zip パッケージを使います。この記事では、そのまま動く完全なサンプルで、ZIPの解凍圧縮(ディレクトリの再帰圧縮)を実装します。あわせて、解凍時に必ず必要なZip Slip(パストラバーサル)対策も解説します。

この記事の結論:解凍は ZipInputStream、圧縮は ZipOutputStream を使います。リソースは try-with-resources で確実に閉じ、解凍時は展開先がフォルダ外に出ていないか必ず検証(Zip Slip対策)します。
スポンサーリンク

ZIPファイルを解凍する

ZipInputStream でエントリを順に読み取り、展開先に書き出します。ディレクトリのエントリはフォルダを作成し、ファイルは中身をコピーします。

Unzip.java(完全なサンプル)
import java.io.*;
import java.nio.file.*;
import java.util.zip.*;

public class Unzip {
    public static void unzip(String zipFilePath, String destDir) throws IOException {
        Path destRoot = Paths.get(destDir).toAbsolutePath().normalize();
        Files.createDirectories(destRoot);

        try (ZipInputStream zis =
                 new ZipInputStream(new BufferedInputStream(new FileInputStream(zipFilePath)))) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                // Zip Slip 対策: 展開先が destRoot の外に出ていないか検証
                Path target = destRoot.resolve(entry.getName()).normalize();
                if (!target.startsWith(destRoot)) {
                    throw new IOException("不正なエントリ: " + entry.getName());
                }

                if (entry.isDirectory()) {
                    Files.createDirectories(target);
                } else {
                    Files.createDirectories(target.getParent());
                    try (OutputStream os =
                             new BufferedOutputStream(Files.newOutputStream(target))) {
                        byte[] buf = new byte[8192];
                        int len;
                        while ((len = zis.read(buf)) > 0) {
                            os.write(buf, 0, len);
                        }
                    }
                }
                zis.closeEntry();
            }
        }
    }
}
解凍では必ずZip Slip対策を入れてください。悪意あるZIPには ../../etc/passwd のようなエントリ名が含まれることがあり、検証せずに展開するとフォルダ外の任意の場所にファイルを書き込まれる恐れがあります。上のコードのように、展開先パスが想定フォルダ内(startsWith(destRoot))かを必ず確認します。

ファイル・フォルダをZIPに圧縮する

ZipOutputStream でエントリを追加していきます。フォルダを丸ごと圧縮するには、中のファイルを再帰的にたどってエントリ名を付けます。

Zip.java(ディレクトリを再帰圧縮)
import java.io.*;
import java.nio.file.*;
import java.util.zip.*;

public class Zip {
    public static void zipDirectory(String sourceDir, String outputZip) throws IOException {
        Path source = Paths.get(sourceDir).toAbsolutePath().normalize();

        try (ZipOutputStream zos =
                 new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(outputZip)))) {
            Files.walk(source)
                 .filter(path -> !Files.isDirectory(path))
                 .forEach(path -> {
                     // ZIP内のパスは / 区切りにする
                     String entryName = source.relativize(path).toString().replace("\\", "/");
                     try {
                         zos.putNextEntry(new ZipEntry(entryName));
                         Files.copy(path, zos);
                         zos.closeEntry();
                     } catch (IOException e) {
                         throw new UncheckedIOException(e);
                     }
                 });
        }
    }
}
1ファイルだけ圧縮するなら、Files.walk の代わりにputNextEntry(new ZipEntry("ファイル名"))Files.copycloseEntry() を1回行うだけです。

呼び出し例

Main.java
public class Main {
    public static void main(String[] args) throws Exception {
        // フォルダを圧縮
        Zip.zipDirectory("input_folder", "output.zip");

        // ZIPを解凍
        Unzip.unzip("output.zip", "extracted");
    }
}

文字コードの注意(日本語ファイル名)

ZIP内の日本語ファイル名は文字化けすることがあります。文字コードを明示したい場合は、ZipInputStream / ZipOutputStream のコンストラクタで Charset を指定できます(Windows由来のZIPは Shift_JIS、最近の標準は UTF-8)。

文字コードを指定する
import java.nio.charset.Charset;

// 解凍時に文字コードを指定
new ZipInputStream(in, Charset.forName("Shift_JIS"));

// 圧縮時に UTF-8 を指定
new ZipOutputStream(out, java.nio.charset.StandardCharsets.UTF_8);

よくある質問(FAQ)

QZip Slipとは何ですか?対策は必須ですか?
AZIP内のエントリ名に ../ を仕込み、展開先フォルダの外にファイルを書き込ませる攻撃です。対策は必須で、展開先パスを normalize() したうえでstartsWith(destRoot)想定フォルダ内か検証します。
Qリソースのclose漏れが心配です。
Atry-with-resourcestry (ZipInputStream zis = ...) { })を使えば、例外が起きてもストリームが自動でクローズされます。例外処理の基本はcatchブロック・例外処理を参照してください。
Q日本語のファイル名が文字化けします。
AZIPの文字コードと指定が合っていない可能性があります。ZipInputStream / ZipOutputStream のコンストラクタでCharsetShift_JISUTF-8)を明示してください。
Q空のフォルダもZIPに含めたいです。
AFiles.walk でディレクトリも対象にし、ディレクトリには末尾に / を付けたエントリ名(例 dir/)でputNextEntry すると、空フォルダもZIPに含められます。

まとめ

JavaでのZIP操作のポイントを整理します。

  • 解凍は ZipInputStream、圧縮は ZipOutputStream
  • リソースは try-with-resources で確実に閉じる
  • 解凍時はZip Slip対策(展開先がフォルダ内かの検証)が必須
  • フォルダの再帰圧縮は Files.walkrelativize でエントリ名を作る
  • 日本語ファイル名は Charset を明示して文字化けを防ぐ

関連として、ファイルを読み込む方法ファイルに書き込む方法catchブロック・例外処理もあわせて読むと、JavaのファイルI/Oに強くなれます。