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

Java 7 以降、`FileSystems.newFileSystem(path)` で Zip ファイルを扱えます。
でも、 **Java のバージョンと Zip ファイルの構造によっては、無限ループしたり `ZipException` が発生したりします。**

特に、いまだに Java 8 を使っている場合は要注意です。

# ソースコード

参考：[Java Zipファイルメモ(Hishidama's java zip Memo)](https://www.ne.jp/asahi/hishidama/home/tech/java/zip.html#h_ZipFileSystem_getRootDirectories)

```java
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](https://bugs.openjdk.org/browse/JDK-8251329)

```java
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）<br/>----<br/>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）<br/>----<br/>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](https://bugs.openjdk.org/browse/JDK-8197398)

結果、問題なく扱えるようになりました。
```
/example.txt
```

この修正は LTS である **Java 11.0.2 にバックポートされています。**
また、 **Oracle JDK のみ 8u211 にバックポートされています。** 
（ただし、[8u211 のリリースノート](https://www.oracle.com/java/technologies/javase/8u211-relnotes.html) には記載されていませんでした）

:::note warn
確認した限り、**同じ Java 8 でも OpenJDK にはバックポートされていませんでした。**
* Adoptium OpenJDK (Eclipse Foundation)
* Corretto OpenJDK (Amazon)
* Zulu OpenJDK (Azul)
* Liberica OpenJDK (BellSoft)
* CentOS OpenJDK (RedHat)

また、[openjdk/jdk8u](https://github.com/openjdk/jdk8u/) にもコミットされていないようです。
:::


まとめると、このようになっています。


|バージョン|サポート|挙動|
|:--|:--|:--|
|Java 7|LTS|無限ループ|
|Java 8<br/>**(Oralce JDK)**|LTS|無限ループ（～ 8u202）<br/>---<br/>`/` をスキップ（8u211 以降）|
|Java 8<br/>**(Open JDK)**|LTS|無限ループ|
|Java 9|non-LTS|無限ループ|
|Java 10|non-LTS|無限ループ|
|Java 11|LTS|無限ループ （～ 11.0.1）<br/>----<br/>`/` をスキップ（11.0.2 以降）|
|Java 12 以降|non-LTS|`/` をスキップ|

# 回避策

`ZipFile` クラスを使った実装であれば、バージョンに依存せず正しく展開できます。
ただし、`.` や `./` の扱いを間違えると脆弱性になるので注意が必要です。

```java
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 だとこのようなコードで作れます。

```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));
    }
}
```