5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FileSystems.newFileSystem で Zip ファイルを扱える。ただし、バグったり例外が起きたりする場合があるので要注意。

Last updated at Posted at 2023-02-18

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));
    }
}
5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?