Windows + JavaのUNICODE ZIPへの圧縮対応状況をざっと確認

# 概要
マルチ プラットフォームに向け仕様改定があったはずなのに、未だに思ったほど普及が進んでいない様に思える、UNICODE ZIP。コロナの禍中で在宅ワークも増えて、ZIPファイルでの情報交換がより増えているはずで、UNICODE ZIPの必要性はより高まっていると思う。

ふとmacOS周りの事情やマルチプラット フォームのアーカイバが気になっり、
そもそも **JavaのUNICODE ZIPの対応状況はどうなのだろう？** と思い、軽く調べてみた。

# 結論
* Java 7 以降はUNICODE ZIPの圧縮に対応している。
  * JDK 6 で ビルドしたプログラムでもJRE 7 以降で実行すれば、UNICODE ZIPが作れる。  
    targetオプションなどで 7以上を対象にするのが望ましそう
* Java 6 以前は非対応。Apach Commons Compressで対応可能。
* WEBアプリでのZIPの文字化け対策は、適切にUNICODE ZIPをしっかり使うこと。
  * WEBアプリはマルチ プラットフォームがそもそもの本懐。
  * WEBアプリが今更MS932で圧縮なんてしてはいけない。  
    ( **馬鹿げている** と言っても過言ではないくらい、随分と昔に対応されているため)
  * UNICODE ZIPに対応していない、旧時代の圧縮解凍ソフトを使うのを辞める。
     * 解凍に対応：**Windows Explorer**, 7zip, Explzh, ...
     * 解凍に非対応：Lhasa, **Lhaplus**


※ 窓の杜さん、マジでいつまでもサイトTOPのオススメ ソフトに、
UNICODE ZIPに対応していないLhaplusを掲載するの辞めてください。。。
せっかくWindows 8からExplorerがUNICODE ZIPに対応しているのに、完全にスポイルされ続しているので。。。

# 検証
## 確認環境
* Windows 10
* ビルド
  * JDK 6, 7,
  * Open JDK 7, 14
* 実行
  * JRE 6, 7, 8
  * Open JDK 7, 14


## 確認方法
* Javaで圧縮するサンプル プログラムを作成。
* 各JDKに同梱されているJREなどでそれぞれ実行し、ファイル名に日本語を含むサンプル ファイルを圧縮。
* 7zipで開き、特性カラムを確認。
  * UNICODEの記載がある  
    ⇒ Language encoding flag (EFS) が立っている  
    ⇒ 正常なUNICODE ZIP
  * UNICODEの記載がない  
    ⇒ Language encoding flag (EFS) が立っていない  
    ⇒ 旧来のZIP  
    (OSの文字コードに依存するため、圧縮時と解凍時のOSのシステム文字コードが一致している必要がある)

![7zipで確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/f18e8954-0b87-e280-3459-a302899206fe.png)


## 参考資料
* UNICODE ZIPに対応した圧縮/解凍ソフトについて - Qiita
  https://qiita.com/libraplanet/items/97b1d1fbdc19b07bdf27
  ※ 手前味噌

* java.util.zip (Java Platform SE 6)
  https://docs.oracle.com/javase/jp/6/api/java/util/zip/package-summary.html
  ※ UNICODE ZIPへの言及無し

* java.util.zip (Java Platform SE 7 )
  https://docs.oracle.com/javase/jp/7/api/java/util/zip/package-summary.html
  > PKWARE ZIP File Format Specification の「APPENDIX D」- UTF-8 を使用して ZIP エントリファイル名およびコメントフィールドをエンコードする言語エンコーディングフラグ (EFS)。

* java.util.zip (Java Platform SE 8 )
  https://docs.oracle.com/javase/jp/8/docs/api/java/util/zip/package-summary.html
  > PKWARE ZIP File Format Specificationの「APPENDIX D」- UTF-8を使用してZIPエントリ・ファイル名およびコメント・フィールドをエンコードする言語エンコーディング・フラグ(EFS)。


## 圧縮結果表
閾値が割と見えたので、総当たりは行っていない。
(JDKを揃えるのが大変なので)

|ビルド<br/>(javac)|実行<br />(java)|Language<br /> encoding<br /> flag<br /> (EFS)|備考|
|:----------------|:----------------|:---|:---|
|JDK&#xA0;6       |JRE&#xA0;6       |なし |NG<br /> UTF-8のOSで圧縮しただけのZIP状態|
|JDK&#xA0;6       |JRE&#xA0;7       |あり |OK     |
|JDK&#xA0;6       |JRE&#xA0;8       |あり |OK     |
|JDK&#xA0;6       |Open JDK&#xA0;7  |あり |OK     |
|JDK&#xA0;6       |Open JDK&#xA0;14 |あり |OK     |
||
|JDK&#xA0;7       |JRE&#xA0;6       |-    |実行不可|
|JDK&#xA0;7       |JRE&#xA0;7       |あり |OK     |
|JDK&#xA0;7       |JRE&#xA0;8       |あり |OK     |
|JDK&#xA0;7       |Open JDK&#xA0;7  |あり |OK     |
|JDK&#xA0;7       |Open JDK&#xA0;14 |あり |OK     |
||
|Open JDK&#xA0;7  |JRE&#xA0;6       |-    |実行不可|
|Open JDK&#xA0;7  |JRE&#xA0;7       |あり |OK     |
|Open JDK&#xA0;7  |JRE&#xA0;8       |あり |OK     |
|Open JDK&#xA0;7  |Open JDK&#xA0;7  |あり |OK     |
|Open JDK&#xA0;7  |Open JDK&#xA0;14 |あり |OK     |
||
|Open JDK&#xA0;14 |JRE&#xA0;6       |-    |実行不可|
|Open JDK&#xA0;14 |JRE&#xA0;7       |-    |実行不可|
|Open JDK&#xA0;14 |JRE&#xA0;8       |-    |実行不可|
|Open JDK&#xA0;14 |Open JDK&#xA0;7  |-    |実行不可|
|Open JDK&#xA0;14 |Open JDK&#xA0;14 |あり |OK     |

JavaはASCII文字のファイル名の場合でも、EFSを付ける様子。
7Zipは非ASCII文字以外を含む場合のみのため、ちょっとした違い。
ただし、UNICODE ZIPはUTF-8を使用しており、ASCII部分には互換があるため、EFSの有無はあまり影響は無い。

## 圧縮結果表 (Linux)
ちょうど手元にUbuntu 20をインストールした仮想マシンがあったので、ついうっかりLinux版OpenJDKも軽く試してみた。

|ビルド<br/>(javac)|実行<br />(java)|Language<br /> encoding<br /> flag<br /> (EFS)|備考|
|:---------|:---------------|:---|:---|
|Windows<br>JDK&#xA0;6       |Linux<br>Open JDK&#xA0;7   |あり|OK|
|Windows<br>JDK&#xA0;6       |Linux<br>Open JDK&#xA0;8   |あり|OK|
|Windows<br>JDK&#xA0;6       |Linux<br>Open JDK&#xA0;14  |あり|OK|
||
|Windows<br>JDK&#xA0;7       |Linux<br>Open JDK&#xA0;7   |あり|OK|
|Windows<br>JDK&#xA0;7       |Linux<br>Open JDK&#xA0;8   |あり|OK|
|Windows<br>JDK&#xA0;7       |Linux<br>Open JDK&#xA0;14  |あり|OK|
||
|Windows<br>Open JDK&#xA0;14 |Linux<br>Open JDK&#xA0;7   |あり|OK|
|Windows<br>Open JDK&#xA0;14 |Linux<br>Open JDK&#xA0;8   |あり|OK|
|Windows<br>Open JDK&#xA0;14 |Linux<br>Open JDK&#xA0;14  |あり|OK|
||
|Linux<br>Open JDK&#xA0;7    |Windows<br>JRE&#xA0;6      |-   |実行不可|
|Linux<br>Open JDK&#xA0;7    |Windows<br>JRE&#xA0;7      |あり|OK|
|Linux<br>Open JDK&#xA0;7    |Windows<br>JRE&#xA0;8      |あり|OK|
|Linux<br>Open JDK&#xA0;7    |Windows<br>Open JDK&#xA0;7 |あり|OK|
|Linux<br>Open JDK&#xA0;7    |Windows<br>Open JDK&#xA0;14|あり|OK|
|Linux<br>Open JDK&#xA0;7    |Linux<br>Open JDK&#xA0;7   |あり|OK|
|Linux<br>Open JDK&#xA0;7    |Linux<br>Open JDK&#xA0;8   |あり|OK|
|Linux<br>Open JDK&#xA0;7    |Linux<br>Open JDK&#xA0;14  |あり|OK|
||
|Linux<br>Open JDK&#xA0;8    |Windows<br>JRE&#xA0;6      |-   |実行不可|
|Linux<br>Open JDK&#xA0;8    |Windows<br>JRE&#xA0;7      |-   |実行不可|
|Linux<br>Open JDK&#xA0;8    |Windows<br>JRE&#xA0;8      |あり|OK|
|Linux<br>Open JDK&#xA0;8    |Windows<br>Open JDK&#xA0;7 |-   |実行不可|
|Linux<br>Open JDK&#xA0;8    |Windows<br>Open JDK&#xA0;14|あり|OK|
|Linux<br>Open JDK&#xA0;8    |Linux<br>Open JDK&#xA0;7   |-   |実行不可|
|Linux<br>Open JDK&#xA0;8    |Linux<br>Open JDK&#xA0;8   |あり|OK|
|Linux<br>Open JDK&#xA0;8    |Linux<br>Open JDK&#xA0;14  |あり|OK|
||
|Linux<br>Open JDK&#xA0;14   |Windows<br>JRE&#xA0;6      |-   |実行不可|
|Linux<br>Open JDK&#xA0;14   |Windows<br>JRE&#xA0;7      |-   |実行不可|
|Linux<br>Open JDK&#xA0;14   |Windows<br>JRE&#xA0;8      |-   |実行不可|
|Linux<br>Open JDK&#xA0;14   |Windows<br>Open JDK&#xA0;7 |-   |実行不可|
|Linux<br>Open JDK&#xA0;14   |Windows<br>Open JDK&#xA0;14|あり|OK|
|Linux<br>Open JDK&#xA0;14   |Linux<br>Open JDK&#xA0;7   |-   |実行不可|
|Linux<br>Open JDK&#xA0;14   |Linux<br>Open JDK&#xA0;8   |-   |実行不可|
|Linux<br>Open JDK&#xA0;14   |Linux<br>Open JDK&#xA0;14  |あり|OK|

概ねWindows版と同様の想定通りの結果だったので、macのJavaでも同様になる期待が高い。


## 圧縮検証プログラムのサンプルコード
急ごしらえのため、雑コード

```java:java
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;

import java.util.zip.ZipOutputStream;
import java.util.zip.ZipEntry;

public class Main {
    private static void closeQuietly(final Closeable o) {
        try{
            if(o != null) {
                o.close();
            }
        } catch(Exception e){
        }
    }

    public static void main(String[] args) throws IOException {
        System.out.println("hoge");
        System.out.println("java version=" + System.getProperty("java.version"));
        System.out.println("args[0]=" + args[0]);
        System.out.println("args[1]=" + args[1]);
        debugZipArc(args[0], args[1]);
        System.exit(0);
    }

    private static void zipArc(final ZipOutputStream zoStream, final File obj, final String parentDirPath) throws IOException {
        final String currentName =  parentDirPath + obj.getName();
        if(obj.isFile()) {
            FileInputStream fiStream = null;
            try {
                byte buf[] = new byte[4096];
                int len;
                ZipEntry entry = new ZipEntry(currentName);
                fiStream = new FileInputStream(obj);
                zoStream.putNextEntry(entry);
                while((len = fiStream.read(buf)) > 0) {
                    zoStream.write(buf, 0, len);
                }
            } finally {
                closeQuietly(fiStream);
            }
        } else if(obj.isDirectory()) {
            for (File child : obj.listFiles()) {
                zipArc(zoStream, child, currentName + "/");
            }
        }
    }

    private static void debugZipArc(final String inDirPath, final String outFilePath) throws IOException {
        FileOutputStream foStream = null;
        ZipOutputStream zoStream = null;
        try {
            File obj;
            foStream = new FileOutputStream(outFilePath);
            zoStream = new ZipOutputStream(foStream);
            obj = new File(inDirPath);
            zipArc(zoStream, obj, "");
        } finally {
            closeQuietly(zoStream);
            closeQuietly(foStream);
        }
    }
}

```
## 補足① JRE6とサロゲート ペア
解凍ツールによっては任意の文字コードでファイル名を展開してくれるものもある。
JRE6ではUTF-8で格納はされている様だけど。``👺`` と ```𩸽``` が化けてしまっている。サロゲート ペアを処理する能力は持ち合わせていないのかな、と思われる。
(JavaVMの問題？)
![JRE6のZIPの文字化け.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/93fed68b-3ec4-0a4b-e98b-40150b4cf1af.png)


## 補足② UNICODE絵文字の表示能力
手元のマシンによっては、UNICODE絵文字が豆腐になるものもあった。同じWindows10、同じ7zipで豆腐になる、ならないがあり、またWinRARでも同様だった。なので、フォントのインストール状況の差異で表示だけの問題かと思われる。
(調査中)
![UNICODE絵文字の豆腐.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/9bfbab93-ad0f-7b23-8971-cfde4dcbc2db.png)

## 補足③ UNICODE対応
それまでシステム文字コードで処理していたのが、国際対応によってUNICODEに置き換わっているものは、ZIPに限らず色々ある。UNICODE ZIPに対応するということは、それらの歩みと同じであり、国際文字対応に於いて非常に意味のある対応である。

※ 日本語文字も国際文字である。

* 実行ファイル(ネイテイブ) ⇒ Win20000あたりから、内部文字コードがUNICODE (WCHAR)での処理に対応。
* 実行ファイル(VM) ⇒ Java、.NET CLRはUNICODE (UCS2)で処理。
* ID3Tag ⇒ 2.3以降でUNICODEに対応。
* APE Tag ⇒ APEv2でUNICODEに対応。

UNICODEに対応することでシステム文字コードに関わらず、一意の処理で国際文字を扱うことができる。

# 感想
Javaが割と真っ当にUNICODE ZIPに対応していたのは驚いた。そのため、それなのに未だにUNICODE ZIPが普及出来ていないのはなぜ？と、更に謎が深まった。りあえずJava 7以降であれば標準機能でUNICODE ZIPが使える様なので、マルチ プラットフォームなZIPユーティリティを作るならJavaが結構良さげに感じた。

また、OS側のZIP圧縮機能でUNICODE ZIPに対応しないのもなぜだろう。恐らくmacの方もwinやjavaなどと同様にシステム文字コードで圧縮しただけの、従来の圧縮をしてるだけの様子。しかし、そもそも **ZIPファイルはmacのファイル システム向けではない** 以上、クロス プラットフォームを無視できないはずなのに、macですらもUNICODE ZIPにしないのがホント謎。

最もmacの場合、ZIPファイルにリソース フォークぶっ込んできている段階で、ルール違反感も否めない。ファイル構成を仕様に盛り込んでる段階で、JARやOffice Open XMLみたいに、表向きだけでもZIPを名乗るの辞めたほうが良いじゃないの？とか思ったりしないでもない。

# 参考
* Application Note Archives - PKZIP & SecureZIP
  https://support.pkware.com/home/pkzip/developer-tools/appnote/application-note-archives

* java.util.zip (Java Platform SE 6)
  https://docs.oracle.com/javase/jp/6/api/java/util/zip/package-summary.html

* java.util.zip (Java Platform SE 7 )
  https://docs.oracle.com/javase/jp/7/api/java/util/zip/package-summary.html

* java.util.zip (Java Platform SE 8 )
  https://docs.oracle.com/javase/jp/8/docs/api/java/util/zip/package-summary.html

* OpenJDK
  https://openjdk.java.net/