はじめに
Lhaplus といえば、PC をセットアップしたら最初に入れる定番の圧縮・解凍ソフトです。ただ、最後の更新は 2015 年で、困る場面が増えてきました。
- RAR が解凍できない:今の RAR は RAR5 が主流ですが、Lhaplus は対応していません。
- 文字化け:海外製の ZIP や Mac で作られた ZIP、古い LZH のファイル名が化けます。
- WinRAR を併用すると:シェアウェアなので、ライセンス購入の案内が出ます。
欲しいのは「ダブルクリックしたら解凍する」だけ。そのうえで、なんでも開けて、文字化けせず、Mac の ZIP もきれいに開けるもの。なければ作るか、ということで作ったのが smarcivaZIP です。
- ソースコード:https://github.com/comnote-max/smarcivaZIP (MIT License)
- 公式ページ:https://smarciva.com/library/smarcivazip/
- Microsoft Store:https://apps.microsoft.com/detail/9PKJ277XTVBF
この記事では、どんな OSS を使って、Windows のどこでハマったかを中心に書きます。
全体の構成
| 役割 | 使ったもの |
|---|---|
| アプリ本体 | C# / .NET 9 / WPF(ランタイム同梱で発行) |
| 圧縮・解凍エンジン | 7-Zip の 7z.dll(LGPL)を COM 経由で呼ぶ |
| 右クリックメニュー(インストーラ版) | レジストリ(HKCU\Software\Classes)だけで実装 |
| 右クリックメニュー(ストア版) | C++ の IExplorerCommand 実装 DLL + MSIX |
| インストーラ | Inno Setup 6(管理者権限なしのユーザー単位インストール) |
| ストア用パッケージ | Windows SDK の makeappx / makepri
|
| テスト | xUnit |
| CI / 配布 | GitHub Actions、GitHub Releases、Cloudflare R2 |
形式ごとの処理は全部 7-Zip に任せ、アプリ側は「文字化け対策」「安全対策」「Windows との統合」に集中しています。
1. 7z.dll を C# から使う
7-Zip の 7z.dll は、CreateObject という関数で IInArchive / IOutArchive などの COM インターフェイスを返します。C# 側でこれらのインターフェイスを [ComImport] で定義し、NativeLibrary で DLL を読み込んで呼び出します。RAR5、Zstandard、XZ、ISO など 100 種類以上の形式が、これだけで扱えます。
ハマりどころ:COM オブジェクトのスレッド
WPF の UI スレッドは STA です。そこで作った COM オブジェクトを別スレッドから触ると、QueryInterface に失敗して落ちます。
なので、7z.dll の COM オブジェクトは、生成から破棄まで専用スレッドで扱うようにしました(ComWorker)。UI からは「このスレッドで実行して」と処理を渡すだけにしています。
// 作るのも使うのも捨てるのも、すべて同じ専用スレッド
result = _archiveWorker.Invoke(() =>
service.Compress(inputs, options, progress, cancellationToken));
2. 文字化け対策
ここが一番やりたかったところです。
ZIP:文字コードの自動判定
ZIP にはファイル名の文字コードを記録する決まった場所がありません(UTF-8 フラグが立っていれば UTF-8 ですが、古い ZIP はほぼ立っていません)。そこで、ファイル名のバイト列を Shift_JIS、UTF-8、各国のコードページで試しに読み、どれが自然かを点数にして判定しています。
- 自信があるとき:そのまま解凍(ダブルクリックで即解凍という体感を守るため)
- 自信がないとき(確信度 0.8 未満):ファイル名の一覧を見ながら文字コードを選べる画面を出す
LZH:7z.dll がシステムのコードページで読んでしまう
LZH は 7z.dll でも開けますが、ファイル名を OS のコードページで読むため、英語版 Windows や CI 上では日本語名が化けます。
そこで、LZH のヘッダ(レベル 0/1/2)を自前で読んで、ファイル名の生のバイト列を取り出し、CP932 として読み直すようにしました。注意点が2つあります。
- パスの区切りに
0xFFが使われるので、文字コードとして読む前に分割する - 日本語名の中にある
\(0x5C)は、Shift_JIS の2バイト目かもしれないので、読んだ後で区切りとして扱う
Mac で作られた ZIP
-
__MACOSX/、._*、.DS_Storeを自動で除外 - 濁点が分解されたファイル名(NFD、「が」が「か」+「゛」になる)を NFC に正規化
3. 安全対策
解凍ソフトは、怪しいファイルを最初に触るソフトでもあるので、ここは手を抜けません。
-
Zip Slip:
../や絶対パスで展開先の外に書こうとするエントリを止める - Zip Bomb:展開後のサイズや圧縮率が異常な書庫を止める
-
Mark of the Web:ネットから落とした書庫の
Zone.Identifier(代替データストリーム)を、解凍したファイルにも引き継ぐ。これで Office や SmartScreen の保護がそのまま効きます - 260 文字を超えるパスにも対応
4. 右クリックメニュー(インストーラ版)
シェル拡張 DLL を使わず、レジストリだけでカスケードメニューを作っています。エクスプローラーに自分のコードを読み込ませないので、不具合があってもエクスプローラーごと落ちることがありません。
HKCU\Software\Classes\*\shell\smarcivaZIP
MUIVerb = smarcivaZIP
SubCommands = "" ← 空文字にすると shell サブキーがサブメニューになる
\shell\00compress-zip
MUIVerb = ZIP に圧縮
MultiSelectModel = Document
\command = "...\SmarcivaZip.exe" --compress zip "%1"
ハマりどころ:複数選択すると、ファイルの数だけプロセスが起動する
MultiSelectModel=Document でも、Windows は選んだファイル1つにつき1回コマンドを起動します。10 個選んで「ZIP に圧縮」すると、何もしなければ ZIP が 10 個できます。
そこで、短時間に起動したプロセス同士で選択を束ねる仕組み(SelectionCoalescer)を作りました。最初に起動したプロセスが少し待って、後から来たプロセスのパスを受け取り、1つの書庫にまとめます。後から来たプロセスは何もせずに終了します。
書庫か、それ以外か
デスクトップのアイコンにドロップしたときや、関連付けからの起動では、全部が書庫なら解凍、1つでも普通のファイルが混ざれば全体を圧縮します(Lhaplus と同じ考え方)。
5. Windows 11 の「最初の右クリックメニュー」に出す(ストア版)
レジストリ方式のメニューは、Windows 11 では「その他のオプションを確認」の中に入ってしまいます。最初の右クリックメニューに出すには、IExplorerCommand を実装した COM サーバーを、パッケージ(MSIX)の定義で登録する必要があります。
<desktop4:Extension Category="windows.fileExplorerContextMenus">
<desktop4:FileExplorerContextMenus>
<desktop5:ItemType Type="*">
<desktop5:Verb Id="smarcivaZIP" Clsid="F25B0869-E77F-4627-AAA1-A00B930517F8" />
</desktop5:ItemType>
<desktop5:ItemType Type="Directory">
<desktop5:Verb Id="smarcivaZIP" Clsid="F25B0869-E77F-4627-AAA1-A00B930517F8" />
</desktop5:ItemType>
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="smarcivaZIP context menu">
<com:Class Id="F25B0869-E77F-4627-AAA1-A00B930517F8"
Path="SmarcivaZip.ShellExt.dll" ThreadingModel="STA" />
</com:SurrogateServer>
</com:ComServer>
</com:Extension>
工夫した点は次の3つです。
-
メニュー部品は C++ で小さく作る:
SurrogateServerにしたので、エクスプローラーとは別のdllhost.exeで動きます。CRT は静的リンクにして、依存を無くしました。 -
メニューの中身はアプリが書き出すファイルから読む:設定画面で選んだ圧縮形式や表示言語は、アプリが起動時に
LocalState\menu.tsvへ書き出します。DLL はそれを読んで並べるだけです。翻訳を DLL 側に持たなくて済みます。 -
選択したパスはファイルで渡す:数百個のファイルを選ぶとコマンドライン(32,767 文字)に収まらないので、パスの一覧を一時ファイルに書いて
--paths-fromで渡します。こちらは最初から全部そろっているので、先ほどの「束ねる」処理も要りません。
ストア版は runFullTrust(制限付き機能)を使うので、Partner Center で用途の説明が必要でした。
6. 既定のアプリ問題:アプリからは変えられない
一番「仕様だから仕方ない」と言いたくなったところです。
Windows 10 以降、「どのアプリで開くか」は利用者が選んだ記録(UserChoice)で決まり、ハッシュで保護されていて、アプリからは書き換えられません。そのため、関連付けを登録しても、本人が一度選ぶまでは Windows が「どのアプリで開きますか?」と聞いてきます。
保護を回避するやり方もありますが、それはやらず、正攻法でできることを積み上げました。
-
RegisteredApplicationsとCapabilitiesを登録して、Windows の「既定のアプリ」に smarcivaZIP のページを作る - インストールの最後と設定画面から
ms-settings:defaultapps?registeredAppUser=smarcivaZIPを開き、そのページに直接案内する - 設定の保存時に
AssocQueryStringで「実際には何で開くか」を Windows に問い合わせ、チェックを入れたのにまだ切り替わっていない形式を一覧で知らせる -
アンインストール済みのアプリを指したまま残っている
UserChoiceだけを取り除く(入っているアプリを選んでいる場合は触らない)。これをやらないと、昔入れていた別の解凍ソフトの「残骸」のせいで、毎回アプリを聞かれ続けます
7. インストーラ(Inno Setup)
-
PrivilegesRequired=lowestで、管理者権限なしにユーザー単位でインストール - 右クリックメニューと関連付けの登録は、インストール後にアプリ自身(
--register --quiet)をExecAsOriginalUserで呼んで行う - アンインストールでは、
smarcivaZIP.で始まる登録をレジストリから総当たりで探して消す。途中でチェックを外した拡張子も残さず、ほかのアプリの関連付けには触りません
8. 20 言語対応
画面の文字列は .resx で 20 言語分を持っています。ストア版では、ファイルの種類名やアプリの説明を .resx から .resw に変換し、makepri で resources.pri にまとめて、マニフェストから ms-resource: で参照しています。
9. CI と配布
- GitHub Actions:テスト、インストーラ(x64 / ARM64)、MSIX バンドルのビルド
- タグを打つと、リリースの下書きと
SHA256SUMS.txtを自動で作る - 更新のお知らせ:週に1回、GitHub の Releases API で最新版の番号だけを確認して知らせる(自動でダウンロードや入れ替えはしない。ストア版はストアが更新するので確認しない)
- 配布:GitHub Releases の配信が日本から遅いことがあったので、インストーラは Cloudflare R2 からも配っています(中身は同じで、ハッシュで確認しています)
使った OSS・ツール
-
7-Zip(
7z.dll、LGPL。RAR の展開部分には unRAR の制限があります) - .NET / WPF
- Inno Setup
- xUnit
- Windows SDK(
makeappx、makepri)
おわりに
「ダブルクリックしたら解凍する」だけのソフトでも、文字化け、Windows のシェル統合、既定のアプリの決まりなど、作ってみると Windows の細かいところに次々とぶつかりました。
ソースは全部公開しているので、気になるところがあれば自由に読んだり改造したりしてください。Issue や PR、感想も大歓迎です。