Heliodor 2019/07/21 10:04

ZIPファイル その2

ZIPファイルについて続きです。
長いです。

それは Libre Office の Calc(Excel互換アプリ)で作成した .XLSX ファイルを開こうとした時の事です。

参考
Libre Office
https://ja.libreoffice.org/


.XLSX ファイルの中身はただの .ZIP ファイルなので、普通に .ZIP として開くことができます。
……のハズなのですが、自分が最初に作ったプログラムではこれがエラーになって開けなかったんです。

で、調べてみたら「ローカルファイルヘッダ―」にデータサイズが記載されてなかったんです。なんのこっちゃ?って感じだと思うので、以下 ZIP ファイルフォーマットについての話です。


まず簡単にZIPファイルの構造を書いておきます。

ZIPファイルの先頭には ZIPファイル全体に対するヘッダがあるのかと思いきや、そうではなく、格納している個々のファイルデータに対するヘッダから始まります。


・ローカルファイルヘッダ (30バイト)
・ファイル名(nバイト)
・拡張データ(省略可能)(nバイト)
・ファイルデータ(nバイト)
・データデスクリプタ(省略可能)(12 or 16バイト)


このブロックがファイルの個数だけひたすら繰り返されます。
それが終わると、今度は「中央ディレクトリヘッダ」がやはりファイルの個数だけ繰り返し続きます。


・中央ディレクトリヘッダ (46バイト)
・ファイル名(nバイト)
・拡張データ(省略可能)(nバイト)
・コメント(省略可能)(nバイト)


最後に、ZIP全体の情報が入ります。


・中央ディレクトリ終端レコード (22バイト)
・ZIPに対するコメント(省略可能)(nバイト)


そう、ZIPファイル全体の情報は、ファイル先頭ではなく末尾にあるのです!
さてここで、単純にZIPファイル内のファイル名をリスト化することを考えてみます。普通に考えれば次のようになります。


まずファイル先頭に読み取り位置を移動してから、以下の手順を繰り返します。

・最初の4バイトが "PK0304" ならば「ローカルファイルヘッダ」なので、それを読む
・ファイル名を読む
・拡張データがあれば、それをスキップ
・ファイルデータをスキップ
・データデスクリプタ があれば、それをスキップ


これを、「中央ディレクトリヘッダ」にたどり着くまで繰り返せば、自動的にすべてのファイル名が分かるはずです。
ところで、「ファイルデータをスキップ」するためには、当然ファイルデータが何バイトあるのか?を知らなくてはなりません。
で、普通それは「ローカルファイルヘッダ」に書いてあります。

ところが、場合によっては「ローカルファイルヘッダ」にサイズが記載されておらず(圧縮前と圧縮後のデータサイズの値が 0 になっている)、その代わりに圧縮データの後に「データデスクリプタ」がくっついていて、そこにサイズが書いてあることがあります。

「データデスクリプタ」を見つけるには、ファイルデータ部分をスキップしないといけません。
ファイルデータをスキップするためには、データが何バイトあるのかを知らないといけません。
データのバイト数は「データデスクリプタ」に書いてあります。


無限ループです。


この「データデスクリプタ」はオプション的な扱いで、あってもなくてもOKです。
これがあるかどうかは「ローカルファイルヘッダ」のフラグ "general purpose bit flag" の Bit3 を見る(0x0008との論理積)と分かります。
この値が1ならばファイルデータの後ろには「データデスクリプタ」がくっついていて、そこにデータサイズが記録されています。
「データデスクリプタ」が存在するという事は、「ローカルファイルヘッダ」にはデータサイズが記載されていない可能性があるという事です。
この場合、ファイルを先頭から辿ることはできないので、逆に末尾から辿っていきます。

ファイルの末尾には「中央ディレクトリ終端レコード」があります。
まずファイルの末尾までシークして、そこから 22 バイト戻れば「中央ディレクトリ終端レコード」が読み取れるはずです。
これを読むと、ファイル先頭から最初の「中央ディレクトリヘッダ」までのオフセットバイト数がわかるので、それを使って「中央ディレクトリヘッダ」に移動します。

実はこの「中央ディレクトリヘッダ」には、「ローカルファイルヘッダ」とほとんど同じ内容が重複して書いてあります。
そして「データデスクリプタ」の有無とは無関係に、「中央ディレクトリヘッダ」のデータサイズの項目にはちゃんとデータサイズが書いてあります。


……ところがですねえ、これでうまくいくとは限らないんですよ。
いま、サラっと「まずファイルの末尾までシークして、そこから 22 バイト戻れば中央ディレクトリ終端レコードが読み取れるはず」なんて書きましたが、実は「中央ディレクトリ終端レコード」の後ろに「ZIPファイルに対するコメント」が付いている場合があります。
コメントの文字列バイト数は「中央ディレクトリ終端レコード」に書いてあるので、その場合はフィアル終端からの逆シークができなくなります(ファイル終端からコメントバイト数+22バイト戻れば「中央ディレクトリ終端レコード」にたどり着くが、そもそもコメントバイト数は「中央ディレクトリ終端レコード」に書いてあるという矛盾)

これ、本気で解析しようと思ったら次のようにします。

  1. ファイル終端に移動
  2. ファイル先端に向かって1バイトづつ移動しながら識別子 "PK0506" と一致するバイト列を探す
  3. 一致した場合は、そこが「中央ディレクトリ終端レコード」であると仮定してレコードを読む
  4. 「中央ディレクトリ終端レコード」にはコメントのバイト数が入っているので、
      レコードの位置、コメントサイズ、全体のファイルサイズの関係に矛盾が無ければOK。具体的にはレコード位置+レコードサイズ(22バイト)+コメントサイズ=全体のファイルサイズ になっていれば良い。
    (※コメントは文字列であるとは限らず、単なるバイナリデータでもよいため偶然一致してしまう可能性も十分ある。極端な場合だと、コメントとして別のZIPファイルのバイナリが丸ごと入っている可能性もある。その場合はコメントとして入っているZIPの終端レコードなのか、それとも今開いているファイルに対する終端レコードなのか区別できない。が、そういうひねくれた使い方はここでは考えない)

  5. 矛盾があった場合、偶然一致してしまっただけなので (2) に戻り、さらに探す

そんなわけで、「データデスクリプタ―」が存在し、かつ全体コメントがあるZIPファイルは、その中身を確定的に知ることができない(矛盾が無いようなファイル構造を自分で調べないといけない)、という事になります。
(ここで問題にしているのは「ZIP全体」に対するコメントのことであって、「ZIP内の各ファイル」に対するコメントは全く問題ありません)


なんて面倒なんでしょう。
まあ、ZIPファイルにコメントが付いていること自体めったにない事ですが。


さらに「データデスクリプタ(※)」自体が遺物というか、ランダムシークできなかった頃の名残なので、今更そんなヘッダがくっついていることもないだろうと。
……そうんなふうに考えていた時期が私にもありました。

(※ストリーミングやテープメディアなど、逆シークできない(しにくい)環境での圧縮を想定したもの。圧縮しながらデータを送り出す場合、圧縮が完了してからでないとデータサイズがわからない。圧縮後のサイズが分かっても、既に書き込んでしまったローカルファイルヘッダを編集することができないので、データデスクリプタという形で情報を追加する)


ところがあったんですよ。そんな亡霊のような「データデスクリプタ」が。
それが冒頭でお話しした Libre Office Calc の .XLSX ファイルでした……。




ZIPファイルの構造について、くわしい情報はこちらをどうぞ。
だいぶお世話になりました

ZIP (ファイルフォーマット)
https://ja.wikipedia.org/wiki/ZIP_(ファイルフォーマット)


ZIP書庫ファイル フォーマット
http://www.tvg.ne.jp/menyukko/cauldron/dtzipformat.html

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

記事のタグから探す

月別アーカイブ

限定特典から探す

記事を検索