Java 7 以降、FileSystems.newFileSystem(path)
で Zip ファイルを扱えます。
でも、 Java のバージョンと Zip ファイルの構造によっては、無限ループしたり ZipException
が発生したりします。
特に、いまだに Java 8 を使っている場合は要注意です。
ソースコード
参考:Java Zipファイルメモ(Hishidama's java zip Memo)
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class Main {
public static void main(String[] args) throws IOException {
Path path = Paths.get(args[0]);
try (FileSystem fileSystem = FileSystems.newFileSystem(path, null)) {
for (Path root : fileSystem.getRootDirectories()) {
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
});
}
}
}
}
Zip ファイルの中に .
や ./
が含まれている場合
バージョンによって挙動が異なります。
Java 7 ~ 12 では、 .
というファイルが含まれている場合に無限ループが発生します。
また、Java 9 ~ 12 では、 ./
というディレクトリが含まれている場合にも同様に無限ループが発生します。
/example.txt
/example.txt
/example.txt
/example.txt
/example.txt
・・・無限ループ・・・
Java 13 ~ 17 では、少し挙動が変わって無限再帰が発生します。
/example.txt
/./example.txt
/././example.txt
/./././example.txt
/././././example.txt
/./././././example.txt
・・・無限再帰・・・
最終的に、Java 18 で ZipException
が発生するように修正されました。
[JDK-8251329] (zipfs) Files.walkFileTree walks infinitely if zip has dir named "." inside - Java Bug System
Exception in thread "main" java.util.zip.ZipException: ZIP file can't be opened as a file system because an entry has a '.' or '..' element in its name
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.initCEN(ZipFileSystem.java:1106)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.<init>(ZipFileSystem.java:135)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystemProvider.newFileSystem(ZipFileSystemProvider.java:136)
at java.base/java.nio.file.FileSystems.newFileSystem(FileSystems.java:406)
at Main.main(Main.java:8)
この変更は、LTS である Java 11.0.14 と Java 17.0.2 にバックポートされています。
(今のところ、Java 8 にはバックポートされていません)
まとめると、このようになっています。
バージョン | サポート | 挙動 |
---|---|---|
Java 7 | LTS | 無限ループ |
Java 8 | LTS | 無限ループ |
Java 9 | non-LTS | 無限ループ |
Java 10 | non-LTS | 無限ループ |
Java 11 | LTS | 無限ループ (~ 11.0.13) ---- ZipException(11.0.14 以降) |
Java 12 | non-LTS | 無限ループ |
Java 13 | non-LTS | 無限再帰 |
Java 14 | non-LTS | 無限再帰 |
Java 15 | non-LTS | 無限再帰 |
Java 16 | non-LTS | 無限再帰 |
Java 17 | LTS | 無限再帰 (~ 17.0.1) ---- ZipException(17.0.2 以降) |
Java 18 以降 | non-LTS | ZipException |
Zip ファイルの中に /
ディレクトリが含まれている場合
バージョンとディストリビューションによって挙動が異なります。
Java 7 ~ 11 では、無限ループが発生します。
( .
や ./
が含まれている場合と同様)
/example.txt
/example.txt
/example.txt
/example.txt
/example.txt
・・・無限ループ・・・
最終的に、Java 12 で /
はスキップされるように修正されました。
[JDK-8197398] (zipfs) Files.walkFileTree walk indefinitelly while processing JAR file with "/" as a directory inside. - Java Bug System
結果、問題なく扱えるようになりました。
/example.txt
この修正は LTS である Java 11.0.2 にバックポートされています。
また、 Oracle JDK のみ 8u211 にバックポートされています。
(ただし、8u211 のリリースノート には記載されていませんでした)
確認した限り、同じ Java 8 でも OpenJDK にはバックポートされていませんでした。
- Adoptium OpenJDK (Eclipse Foundation)
- Corretto OpenJDK (Amazon)
- Zulu OpenJDK (Azul)
- Liberica OpenJDK (BellSoft)
- CentOS OpenJDK (RedHat)
また、openjdk/jdk8u にもコミットされていないようです。
まとめると、このようになっています。
バージョン | サポート | 挙動 |
---|---|---|
Java 7 | LTS | 無限ループ |
Java 8 (Oralce JDK) |
LTS | 無限ループ(~ 8u202) --- / をスキップ(8u211 以降) |
Java 8 (Open JDK) |
LTS | 無限ループ |
Java 9 | non-LTS | 無限ループ |
Java 10 | non-LTS | 無限ループ |
Java 11 | LTS | 無限ループ (~ 11.0.1) ---- / をスキップ(11.0.2 以降) |
Java 12 以降 | non-LTS |
/ をスキップ |
回避策
ZipFile
クラスを使った実装であれば、バージョンに依存せず正しく展開できます。
ただし、.
や ./
の扱いを間違えると脆弱性になるので注意が必要です。
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Main {
public static void main(String[] args) throws IOException {
try (ZipFile zip = new ZipFile(args[0], StandardCharsets.UTF_8)) {
Enumeration<? extends ZipEntry> entries = zip.entries();
while(entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
}
System.out.println(entry.getName());
}
}
}
}
メモ:問題となるファイルのつくり方
コマンドやツールによって作り方は異なりますが、Java だとこのようなコードで作れます。
try (OutputStream stream = Files.newOutputStream(Path.of("example.zip"))) {
try (ZipOutputStream zip = new ZipOutputStream(stream)) {
zip.putNextEntry(new ZipEntry("/"));
zip.putNextEntry(new ZipEntry("example.txt"));
zip.write("text".getBytes(StandardCharsets.UTF_8));
}
}