バイナリエディタを駆使して壊れたzipからファイルを救出する

# ukyoは激怒した。ukyoにはMsEdgeがわからぬ。けれどもzipに対しては、人一倍に敏感であった

```
$ unzip MsEdge.Win10.VMware.zip
Archive:  MsEdge.Win10.VMware.zip
warning [MsEdge.Win10.VMware.zip]:  1031667094 extra bytes at beginning or within zipfile
  (attempting to process anyway)
error [MsEdge.Win10.VMware.zip]:  start of central directory not found;
  zipfile corrupt.
  (please check that you have transferred or created the zipfile in the
  appropriate BINARY mode and that you have compiled UnZip properly)
```

なにやら[Virtual Machine (VM), Windows Virtual PC & BrowserStack : Microsoft Edge Dev](https://dev.windows.com/en-us/microsoft-edge/tools/vms/mac/)よりダウンロードしたzipアーカイブが展開できない、エラーをみるとどうやらメタデータが壊れているようなので[zipの仕様書](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT)を見ながらファイルを救出することにした。

# zipに格納された各ファイルのオフセットとサイズを取得する

zipの構造については仕様書にも書かれていますし、いろいろなサイトで解説されてますので詳しくは解説しません。以下のサイト等を参考にして下さい。

* [ZIP書庫ファイル フォーマット - 略して仮。](http://www.tvg.ne.jp/menyukko/cauldron/dtzipformat.html)
* [ZIP101 an archive walkthrough - Imgur](http://imgur.com/BXuOFqT)

通常zipはファイル末尾にあるメタデータからデータを展開していきます。が、それが壊れているようなので、前から読み込んでいきます。リトルエンディアンなのでバイナリエディタの表示とは逆になっていることに注意して下さい。

![zip1.png](https://qiita-image-store.s3.amazonaws.com/0/380/afeb63df-3014-ca23-7b12-290bbf8cae60.png)

ファイル先頭から、以下のような情報が読み取れます。

1. `0x04034B50` local file headerのシグニチャです
2. `0xFFFFFFFF` ファイルサイズを表しますが、この場合は4GBで収まらないファイルということになります。その場合は、拡張フィールドにサイズが書いてあります
3. `0x0014` 拡張フィールドのバイト長
4. `0x0001` 拡張フィールドには複数の要素が格納されている可能性があります。これは要素の種類を表します、この場合は64bitで表されたファイルのサイズが入っています
5. `0x0010` 要素のサイズ
6. `0x000000013D7DF66C` ファイルの圧縮後のサイズ

MsEdge-Win10-VMware/MsEdge-Win10-VMware-disk1.vmdkのオフセットが`0x64`byte、サイズが`0x000000013D7DF66C`byteだと判明しました。次のファイルのlocal file headerの先頭は`0x000000013D7DF6D0`byteにあるはずなのでそこまで飛びます。

![zip2.png](https://qiita-image-store.s3.amazonaws.com/0/380/36982ee2-643b-a4e0-460c-87e0c2ea94c2.png)

同様に読み込んでいきます。1画面に収まる範囲にあるので半透明な範囲で覆ってみました。ファイル名がEdge-Win10-VMware/MsEdge-Win10-VMware.mfでlocal file headerの先頭からのオフセットは`0x48`byteはサイズはサイズは`0x7A`byteです。

次のファイルも同様に読み込んでいきます。これは省略。3つ目のファイルを読み終わって次のファイルを読み込もうとしたときシグニチャが`0x04034B50`でなくて`0x02014B50`なことに気づきます。これはcentral directory headerのそれ、local file headerのオフセット等のメタデータで、つまりこれ以上ファイルはないということにもなります。

<img width="617" alt="スクリーンショット 2015-12-07 20.00.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/380/5796f1d3-8648-b288-5c46-e03479453820.png">

合計3つのファイルがありました。まとめると以下のようになります。

ファイル名 | local file header先頭からのオフセット(byte) | 圧縮されたファイルのサイズ(byte)
---|---:|---:
MsEdge-Win10-VMware/MsEdge-Win10-VMware-disk1.vmdk | `0x64` | `0x013D7DF66C`
MsEdge-Win10-VMware/MsEdge-Win10-VMware.mf | `0x48` | `0x7A`
MsEdge-Win10-VMware/MsEdge-Win10-VMware.ovf | `0x49` | `0x056E`

# 展開する

各ファイルのオフセットとサイズが取れたので各ファイルを展開していきます。jsで書きます。別に言語はなんでもいいんですが、慣れているのでこれで。jsだと浮動小数点のアレがあるんですが、今回のだとセーフですかね。

```js
var fs = require('fs');
var zlib = require('zlib');

var start;
var end;

function unzipFile(name, start, end) {
  var rs = fs.createReadStream('MsEdge.Win10.VMware.zip', {start: start, end: end});
  var inflateStream = zlib.createInflateRaw();
  var ws = fs.createWriteStream(name);
  rs.pipe(inflateStream).pipe(ws);
}

start = 0x6E;
end = start + 0x013D7DF66C;
unzipFile('MsEdge-Win10-VMware-disk1.vmdk', start, end);

start = end + 0x48;
end = start + 0x7A;
unzipFile('MsEdge-Win10-VMware.mf', start, end);

start = end + 0x49;
end = start + 0x056E;
unzipFile('MsEdge-Win10-VMware.ovf', start, end);
```

適当にdeflateで圧縮されていると決めつけましたが、zipの圧縮アルゴリズムはdeflate以外も使えます。ただ、deflateじゃないのを見たことがないので意識する必要はないかと思います。

# その他、救出する方法を考えてみる

zipはストリームで出力するためにlocal file header内のcompressed size(とuncomressed size, crc32)の部分に`0x00000000`を入れておいて、圧縮されたファイルのバイト列のおしりにdata descriptorとして書くことができます。ただ、それだと前からファイルのサイズが取得できないので詰みます。そんなときどうしたらよいか考えてみます。

## ディレクトリ名で検索する

今回の例だとMsEdge-Win10-VMware。案外単一ディレクトリに入っているzipが多い印象があります。ディレクトリ名で検索してちょっと戻ってシグニチャを探すのはありだと思います。

## central directory headerが生存しているか確認する

今回の例だとcentral directory headerの先頭バイトが見つからないみたいなエラーだったのでそれ自体は生き残っているかもしれません。実際生き残っていましたし。残っていればそこから普通にオフセットを計算すればOKですね。

## local file headerらしきものを探す

これは割りと力技です。local file headerのシグニチャを一個ずつ検索していき、直前の12byte(data descriptorのはず)を解析します。compressed sizeが正しいものなら一個前のlocal file headerの末尾にたどり着くはずです。まぁ、ここまで来たらそういうプログラムを作るべきですね。

# `zip -F`

実は`zip -F in --out out`でzipファイルを修復することができます。最初からそれ使えよ、というかんじですが、これは終わった後に知ったことです。ただ、これやってもなぜか修復できなかったので結局手動でやるしかなかったのですが。

# 参考

* [Virtual Machine (VM), Windows Virtual PC & BrowserStack : Microsoft Edge Dev](https://dev.windows.com/en-us/microsoft-edge/tools/vms/mac/)
* [https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT)
* [ZIP書庫ファイル フォーマット - 略して仮。](http://www.tvg.ne.jp/menyukko/cauldron/dtzipformat.html)
* [ZIP101 an archive walkthrough - Imgur](http://imgur.com/BXuOFqT)
* [太宰治 走れメロス](http://www.aozora.gr.jp/cards/000035/files/1567_14913.html)