ZIPファイルのファイル名の問題を修復するツールを作った

## ZIPファイルは非互換性の塊

複数のファイルやフォルダを1個にまとめて、更にサイズの削減までできてしまうZIPファイル。さらに、パスワードを使った暗号化にも対応しています。特にWindows環境の人にとっては、ソフトの配布・メールへの書類の添付などに欠かせない存在になっています。（macOS・Linuxだとtarballの方が一般的だと思います）

最近問題になっている[PPAP](https://ja.wikipedia.org/wiki/PPAP_%28%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%29)にも欠かせない存在です。

しかしながら、このZIPファイルには、**OS・言語・作成ツールによって、日本語等（非ASCII文字）のエンコーディング方法が異なるというとんでもない問題があります。WindowsやmacOSで日本語等を含んだ名前のファイルから普通にZIPファイルを作成した場合、別のOS（Windowsの場合は別の言語でも）で解凍すると、ファイル名が化けます。**

それこそPPAPする場合、受信者が非日本人・マカーだと破綻します。

**一応、規格上では、特定のフラグを立ててZIPファイルに埋め込めば、解凍・表示時にUTF-8として扱うというようになっていますが、Windows7以前では無視されていました。さらに、Unicode正規化については何も決められていないので、macOSのFinderでは、普通でない正規化形式のファイル名をしれっと紛れ込ませてきます。さらに、Windowsで一般的な方法や、macOSの `zip` コマンドでは、このUTF-8フラグに対応していません。**

### 詳細

Windowsの「送る」→「圧縮 (zip 形式) フォルダー」を使う場合は、Shift-JISでファイル名が保存されます。（日本語の場合）macOSの場合、ターミナルの`zip`コマンドは、UTF-8使用フラグが立たないのでWindowsでは正しく取り扱われません。Finderで作成した場合、濁点・半濁点が「付く前の文字」+「濁点or半濁点」のよいうに分解されて保存されます。Windows・Linuxでは再合成されないので、一見何も問題がないように見えても、実際にはファイル選択時等に支障が出ます。

LinuxのZIPコマンドはお行儀がよく、一般的な正規化のUTF-8でファイル名を保存してくれる上、フラグもきちんと立ててくれます。また、PowerShellの`Compress-Archive`コマンドレットも、同じくお行儀がよいです。

WindowsのサードパーティZIP作成ソフトも行儀の悪いものだらけです。一昔前に全盛期を誇っていたLhaplusでは当然Shift-JISのみ対応（解凍時にもUTF-8フラグの取扱なし）、7-Zipでも[隠しオプション的な扱いをされています。](https://proengineer.internous.co.jp/content/columnfeature/6276#section202)（デフォルトだとShift-JIS、）Windows7への忖度が全ての原因です。

**なお、ZIPの規格では、UTF-8使用フラグがついていなければ、ASCII文字以外の使用は認められません。**

## つくったもの

<https://github.com/tats-u/zifu>

<https://crates.io/crates/zifu>

**ZIFU**という名前のツールです。**ZI**P **F**ile names to **U**TF-8の略です。

現在ではコマンドラインツールの形態を取っていますが、いずれ本当に実行を習慣化してほしい~~情弱の~~人達をターゲットにできるように、GUI版も作りたいなーと思っています。

## ライブラリの選定・開発

以下のようなライブラリが必要でしたが、使えるようなものはありませんでした。

- ZIPファイル中の構造体をローレベルで扱うライブラリ
- macOS独自のUnicode正規化を直すライブラリ
- OEMコードページをUTF-8と相互変換するライブラリ（国際化対応に必要）
- 現在のロケールに対応するANSI/OEMコードページを取得するライブラリ（macOS・Linux用）

それぞれ、以下のようなクレートを作成して公開しました。

### ZIPファイルの構造体を作成するライブラリ

<https://github.com/tats-u/rust-zip-structs/>

<https://crates.io/crates/zip_structs>

既存のライブラリは、ファイル名が指すバイト配列を変更するという要求に応えられませんでした。

最初はプログラム本体のクレートに同梱していましたが、別クレートに分離しました。

[公式の仕様書](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT)・[日本語の解説資料](https://gist.github.com/ysakasin/2edf8d3bf55c6ebf63f82851e302b030#general-purpose-bit-flag)等を参照しつつ、`hexyl`で実際のZIPファイルを解析したりもしました。

### macOS独自のUnicode正規化を直すライブラリ

<https://github.com/tats-u/rust-hfs-nfd/>

<https://crates.io/crates/hfs_nfd>

このUnicode正規化は非標準のものです。標準のNFDとは一部結果が食い違うため、[公式のテーブル](https://developer.apple.com/library/archive/technotes/tn/tn1150table.html)を取得して自動でRustコードを生成するスクリプトをPythonで書きました。

### OEMコードページをUTF-8と相互変換するライブラリ

<https://github.com/tats-u/rust-oem-cp/>

<https://crates.io/crates/oem_cp>

アジア以外の言語のエンコーディングは、ANSIコードページとOEMコードページの2つに分かれています。

ターミナルやZIPファイルではOEMコードページの方が使われます。ANSIの方はライブラリがりますが、OEMの方はなかったので、仕方なく作りました。

多くのコードページは、<https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/>に対応表がありましたが、一部欠落していたため、それらは[ICUに入っていた対応表](https://github.com/unicode-org/icu/tree/main/icu4c/source/data/mappings)を仕方なく使用しました。

ロケール文字列中のトークン（`ja`・`jp`など）をキーとしたハッシュ木を構築して、辿っています。

こちらも自動生成スクリプト使用。

### 現在のロケールに対応するANSI/OEMコードページを取得するライブラリ

<https://github.com/tats-u/locale-name-code-page>

<https://crates.io/crates/locale_name_code_page>

WindowsではコードページはWindows APIの`GetACP`・`GetOEMCP`で取得できますが、macOS・LinuxではUTF-8が当たり前なので、そうは行きません。

Microsoftが過去に公開していた対応表の魚拓（Wayback Machine）が残っていたので、それからリストを作成しました。
こちらも自動生成スクリプト使用。

## 実装について

ZIPファイルの大半は、「ローカルファイルヘッダ」「中央ディレクトリ」「中央ディレクトリ終端」の3種類の構造体からなります。ファイル名の情報は前者2つに格納されています。この2つには共通して以下のような情報があります。

- ファイル名のバイト数
- ファイル名がUTF-8で格納されているかどうか
- ファイル名

「ファイル名がUTF-8で格納されているかどうか」が格納されているのは、「汎用目的フラグ」と呼ばれる2バイトの整数です。これをリトルエンディアンで見て、LSBから11ビット目が1になっていれば、UTF-8で格納されています。これは、リトルエンディアンで取得した数値に`1 << 11 = 0x0800`をANDして0でない、という条件に置き換えられます。

このフラグが立っているならば、Apple独自のUnicode正規化を行っていないかチェックし、ユーザの同意の元修正します。
立っていない場合、ASCII以外の文字が入っていないかどうかチェックします。入っている場合は、システム言語に対応したエンコーディング（日本語ならShift-JIS）、または与えられたエンコーディングを使用し、UTF-8への変換を試みます。

ファイル名の変更をしたとして、今度はファイルへの保存も一苦労です。変更前後でファイル名の長さが変わり、構造体はファイル名等の分可変長ですので、これらの変更内容だけサクッと書き換え・・・といったことはできません。

構造体の内容は（中身のファイルの内容以外だけでも）保持しておき、新しいファイルを作って逐次書き込まなければなりません。
上書きをする動作の場合は、同じディレクトリに一時ファイルを作る→元のファイルを削除→一時ファイルをリネーム　という手段を取りました。

## インストール

[リポジトリのReleases](https://github.com/tats-u/zifu/releases/)からZIP・debファイルをダウンロードして適当に配置（ZIP）・インストール（deb）してください。

ZIPファイルはCPUのアーキテクチャ・OS毎に分かれています。主なものは以下の2つです。

- `x86_64-windows`・`x86_64-linux`: Windows・Linuxの場合、通常はこれを選んでください。
- `universal-macos`: Intel CPU・Apple Silicon両対応（のはず）です。


その他は特殊なケース用（ARM版のWindows・Linuxなど）です。

また、Rustには[crates.io](https://crates.io/)という非常にありがたい公式リポジトリが存在します。Rust環境があれば、以下のコマンドでインストール完了させることもできます。

```bash
cargo install zifu
```

Rustのインストールは、[rustup経由](https://www.rust-lang.org/ja/tools/install)を推奨します。

## 使い方

以下のコマンドで本ツールの適用が必要かどうかがわかります。

```bash
zifu -c 【ZIPファイルのパス】
```

ファイル名一覧は次のコマンドで確認できます。

```bash
zifu -l 【ZIPファイルのパス】
```

Windows 7以前以外の全環境で扱えるように変換するには、以下のコマンドで可能です。

```bash
zifu 【ZIPファイルのパス】 【出力先のパス】
```

上書きする場合は、代わりに以下のコマンドを入力します。

```bash
zifu -i 【ZIPファイルのパス】
```

## 免責事項

MITライセンスのため、本ツールを適用して再入手不可のZIPファイルが壊れたなどの責任は負いかねます。また、テストコード等である程度の動作の確認して万全を期していますが、完全な動作保証はありません。特に、ZIP64非対応のため、4GB近くのZIPファイルを扱う際には十分注意してください。
問題がありましたら、IssueやPRをお願いします。