1. はじめに: なぜ Java でアーカイブと圧縮が必要か
現代では、アーカイブや圧縮ファイルの取り扱いは一般的なタスクです。バックアップ、ファイル交換、ログ記録、大量データの保存など。Java はパッケージ java.util.zip を通じて、ZIP 形式のアーカイブや GZIP 圧縮ファイルを扱う標準機能を提供します。
Java でできること:
- ZIP アーカイブ(複数ファイルのコンテナ)の読み書き。
- GZIP ファイル(単一ファイルの圧縮)の読み書き。
- アーカイブの内容管理、マスクによるファイルフィルタリング。
- 圧縮レベルの制御。
- 展開時の安全性チェック(zip slip と zip bomb への対策)。
2. 主要クラス
ZipInputStream と ZipOutputStream
これは ZIP アーカイブを順次読み書きするためのストリームクラスです。使いどき: 個々のファイルへのランダムアクセスなしに、アーカイブを「その場で」読み書きしたい場合。
例: アーカイブの読み取り
import java.io.*;
import java.util.zip.*;
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
System.out.println("ファイル: " + entry.getName());
// entry の内容は zis.read(...) で読み取れます
zis.closeEntry();
}
}
例: アーカイブの作成
import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
ZipEntry entry = new ZipEntry("hello.txt");
zos.putNextEntry(entry);
zos.write("こんにちは、アーカイブ!".getBytes());
zos.closeEntry();
}
ZipFile
ZIP アーカイブの内容に対するランダムアクセス用のクラスです。ファイル一覧を素早く取得し、任意のファイルを名前で開いて内容を読み取れます。
例:
import java.util.zip.*;
import java.io.*;
ZipFile zipFile = new ZipFile("archive.zip");
zipFile.stream().forEach(entry -> System.out.println(entry.getName()));
ZipEntry entry = zipFile.getEntry("hello.txt");
try (InputStream is = zipFile.getInputStream(entry)) {
// ファイルの内容を読み込む
}
zipFile.close();
ZipFile を使う場面:
- ファイル一覧、メタデータ、サイズ、日付を素早く取得したい場合。
- アーカイブ全体を順次処理せずに、特定のファイルだけを読みたい場合。
ZipEntry
アーカイブ内の個々のファイルやフォルダを表すオブジェクト。名前、サイズ、日付、フラグ、圧縮レベルなどを持ちます。
import java.util.zip.ZipEntry;
ZipEntry entry = new ZipEntry("docs/readme.txt");
entry.setComment("ファイルの説明");
entry.setTime(System.currentTimeMillis());
圧縮レベル (Deflater)
アーカイブを作成する際、圧縮の度合いを制御できます(0 — 無圧縮 から 9 — 最高圧縮 まで):
import java.io.FileOutputStream;
import java.util.zip.Deflater;
import java.util.zip.ZipOutputStream;
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
zos.setLevel(Deflater.BEST_COMPRESSION); // または 0..9
// ...
}
- Deflater.NO_COMPRESSION (0)
- Deflater.BEST_SPEED (1)
- Deflater.BEST_COMPRESSION (9)
- Deflater.DEFAULT_COMPRESSION (-1)
原則: レベルが高いほど処理は遅いが、より強く圧縮されます。
3. 単一ファイルの圧縮
GZIP は単一ファイルを圧縮するためのフォーマットです(アーカイブではありません)。ログ、一時ファイル、ネットワーク伝送などに用いられます。
例: ファイルを圧縮する
import java.util.zip.*;
import java.io.*;
try (GZIPOutputStream gos = new GZIPOutputStream(new FileOutputStream("file.txt.gz"));
FileInputStream fis = new FileInputStream("file.txt")) {
fis.transferTo(gos);
}
例: ファイルを展開する
import java.util.zip.GZIPInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
try (GZIPInputStream gis = new GZIPInputStream(new FileInputStream("file.txt.gz"));
FileOutputStream fos = new FileOutputStream("file.txt")) {
gis.transferTo(fos);
}
覚えておきましょう: GZIP は 1 つのファイルだけを扱い、フォルダ構造や追加メタデータは保存しません。これに対して ZIP は、複数ファイルやフォルダ全体をまとめて格納でき、構造や各要素の情報を保持します。
4. ディレクトリの圧縮/展開、PathMatcher によるフィルタ
ディレクトリを ZIP に圧縮する
ファイルやサブフォルダを含むフォルダを圧縮するには、ファイルツリーを再帰的に走査し、各ファイルを正しい相対パスでアーカイブに追加します(ZIP の区切りは常に "/")。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
Path sourceDir = Paths.get("myfolder");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
Files.walk(sourceDir)
.filter(Files::isRegularFile)
.forEach(path -> {
String entryName = sourceDir.relativize(path).toString().replace("\\", "/");
try (InputStream is = Files.newInputStream(path)) {
zos.putNextEntry(new ZipEntry(entryName));
is.transferTo(zos);
zos.closeEntry();
} catch (IOException e) { e.printStackTrace(); }
});
}
アーカイブをディレクトリに展開する
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path outPath = Paths.get("output", entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
try (OutputStream os = Files.newOutputStream(outPath)) {
zis.transferTo(os);
}
}
zis.closeEntry();
}
}
マスクによるファイルフィルタ(PathMatcher)
圧縮/展開の対象をマスクでフィルタできます。例えば "*.txt" のみ:
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.PathMatcher;
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.txt");
Files.walk(sourceDir)
.filter(matcher::matches)
.forEach(/* ... */);
5. セキュリティ: Zip Slip、zip bomb、パス正規化の検証
Zip Slip(パスを悪用した攻撃)
問題: 攻撃者が、名前が "../../../../etc/passwd" のようなエントリを含むアーカイブを作ることがあります。検証せずに展開すると、システムファイルを上書きしてしまう可能性があります。
対策: ファイルを書き出す前にパスを正規化し、出力先ディレクトリの外に出ないことを確認します。
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
Path targetDir = Paths.get("output").toAbsolutePath();
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path outPath = targetDir.resolve(entry.getName()).normalize();
if (!outPath.startsWith(targetDir)) {
throw new IOException("Zip Slip: 出力先フォルダ外への書き込みが試行されました!");
}
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
try (OutputStream os = Files.newOutputStream(outPath)) {
zis.transferTo(os);
}
}
zis.closeEntry();
}
}
Zip bomb(圧縮爆弾)
問題: 展開後にギガバイト級になるファイルを含みながら、アーカイブ自体は数キロバイトという場合があります。これによりサーバーやディスクを「落とす」ことがあります。
対策: 展開する各ファイルの最大サイズと、全体の展開量に上限を設け、リミットを超えたら処理を中断します。
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
long maxSize = 100 * 1024 * 1024; // 100 MB
long totalUnzipped = 0;
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path outPath = Paths.get("output", entry.getName()).normalize();
Files.createDirectories(outPath.getParent());
long written = 0;
try (OutputStream os = Files.newOutputStream(outPath)) {
byte[] buf = new byte[8192];
int len;
while ((len = zis.read(buf)) > 0) {
os.write(buf, 0, len);
written += len;
totalUnzipped += len;
if (written > maxSize || totalUnzipped > maxSize) {
throw new IOException("Zip bomb detected!");
}
}
}
zis.closeEntry();
}
}
6. 実践: マスク対応の CLI ユーティリティ「zip/unzip」
マスク対応でファイルを圧縮・展開する簡単なコンソールユーティリティを書きましょう。
例: 圧縮
// java ZipUtil zip myfolder archive.zip *.txt
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public static void zip(String sourceDir, String zipFile, String glob) throws IOException {
Path src = Paths.get(sourceDir);
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
Files.walk(src)
.filter(Files::isRegularFile)
.filter(matcher::matches)
.forEach(path -> {
String entryName = src.relativize(path).toString().replace("\\", "/");
try (InputStream is = Files.newInputStream(path)) {
zos.putNextEntry(new ZipEntry(entryName));
is.transferTo(zos);
zos.closeEntry();
} catch (IOException e) { e.printStackTrace(); }
});
}
}
例: Zip Slip 対策つきの展開
// java ZipUtil unzip archive.zip output
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public static void unzip(String zipFile, String outDir) throws IOException {
Path targetDir = Paths.get(outDir).toAbsolutePath();
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path outPath = targetDir.resolve(entry.getName()).normalize();
if (!outPath.startsWith(targetDir)) {
throw new IOException("Zip Slip: 出力先フォルダ外への書き込みが試行されました!");
}
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
try (OutputStream os = Files.newOutputStream(outPath)) {
zis.transferTo(os);
}
}
zis.closeEntry();
}
}
}
実行例:
java ZipUtil zip myfolder archive.zip "*.txt"
java ZipUtil unzip archive.zip output
1 つ目の例では、コマンド java ZipUtil zip myfolder archive.zip "*.txt" が、フォルダ myfolder からすべての .txt ファイルを archive.zip に圧縮します。2 つ目の例では java ZipUtil unzip archive.zip output がアーカイブをフォルダ output に展開し、いずれのファイルも出力先ディレクトリの外に書き出されないことを検証します。これが Zip Slip に対する防御です。
GO TO FULL VERSION