Posted at

Gzipについて調べてみた


概要


  • gzipについて暇だったので調べた

  • gzipに圧縮/解凍するやつを実装してみた


    • ただしzlibに依存している



暇だったので普段からお世話になっているgzipについて調べて少し実装するみたいなことをやってみました。何度繰り返された記事なのだろうか。

とはいっても仕様はRFCにて公開されているのでそれの猿真似です。

すこしハマるとこもあったので暇つぶしにQiitaに投稿してみようかなと。

なお、https://www.futomi.com/lecture/japanese/rfc1952.html のドキュメントを参考にさせていただきました。


ざっくり仕様について

Gzipファイルはbyte単位のいくつかのメンバにより構成されています。

それぞれbyte数が決まっていたり、決まっていなかったりします。

以下に順番にメンバを簡単に書きます。


  • ID1(IDentification 1), ID2(IDentification 2)


    • それぞれファイルがgzipであることを示すためのもの

    • ID1 = 0x1f, ID2 = 0x8b です

    • gzipファイルをhexdumpすると簡単に確認できます.


      • 下の00000000に続く2つの16進数が1f, 8b






$ zcat test.txt.gz
abc

$ hexdump -C test.txt.gz
00000000 1f 8b 08 08 28 2e 13 5d 00 03 74 65 73 74 2e 74 |....(..]..test.t|
00000010 78 74 00 4b 4c 4a e6 02 00 4e 81 88 47 04 00 00 |xt.KLJ...N..G...|
00000020 00


  • CM(Compression Method)


    • その名の通り圧縮方式

    • 現在使われているのは8(deflate)のみな感じ


      • 0~7は予約されている



    • 上で出したhexdumpの結果の3byte目も08であることがわかる



  • FLG


    • gzipファイルにどんな情報が含まれているかを示すフラグ

    • bit flagになっていて


      • bit 0 -> FTEXT

      • bit 1 -> FHCHR

      • bit 2 -> FEXTRA

      • bit 3 -> FNAME

      • bit 4 -> FCOMMENT

      • bit 5 ~7 -> 予約



    • いろいろあるんだけど、実際に使われているのはFNAME(ファイル名)?


      • gzipコマンドで指定できそうにもない

      • FNAMEはgzipでファイルを圧縮すると自動で入る
        * ちなみに標準入力を圧縮すると入らない





  • MTIME


    • modified time、ファイルの最終更新日をunit timeにしたもの

    • 4byteで構成され、秒単位になる

    • byteから直すときはそのまま直すのではなく、MTIMEのbyte列を逆順にしないといけない



      • 0x42 0xb8 0x12 0x5d だとしたら、 0x5d 0x12 0xb8 0x42 で読み取る必要がある

      • 不思議、そういうものなのだろうか





  • XFL(extra flag)


    • deflateの圧縮方式を示す


      • 2 -> 高圧縮

      • 4 -> 高速圧縮



    • なんだけど、gzipはdefaultで0になる


      • -9, -1 optionを入れると上記の数字が入る





  • OS


    • そのままOS

    • どのosがどの数字になるかは仕様書を読んでください

    • 雑な実装だと 255 = 不明 を入れるっぽい



  • XLEN(optional)


    • FLGにFEXTRAがセットされると入る

    • 拡張フィールドと呼ばれる付加情報のためのメンバのサイズを示す



  • 拡張フィールド(optional)


    • FLGにFEXTRAがセットされると入る

    • SI1, SI2, Dataで構成される


      • SI1, SI2はどの情報を示すかというID



    • 現在入っているのは SI1: 0x41 ('A') SI2: 0x70 ('P') Data: Apollo file type information

    • ほぼ使われていない



  • FNAME(optinal)


    • ファイル名が入る

    • 最終byteに0を入れる

    • gzipファイルをcatするとファイル名がでるのはこれが入っていたから



  • FCOMMENT(optional)


    • コメント



  • FHCRC(optional)


    • CRC16で計算されたチェックサム

    • 後でCRC32で計算したチェックサムいれるのにどうしてあるのかは謎



  • 圧縮ブロック


    • データ本体

    • deflateで圧縮されたものが入る



  • CRC32


    • CRC32で計算されたチェックサムが入る

    • これもMTIME同様にbyte順が逆転してるので、利用する場合は反転してあげる必要がある



  • ISIZE


    • 圧縮前のbyte数

    • これもまた逆転している



これがgzipのファイルフォーマット。


まとめやら

byte配列が逆転してたりとかして悩まされたりしたが、わりと単純だった(本体の圧縮アルゴリズムに触れてないからだけど)

これさえわかれば、gzipファイルをbyte配列で取り出して、1つずつ要素を眺めていけば問題なく扱える。

deflateでの圧縮もzlibとかを利用してあげれば大丈夫で、たいていの言語には実装されている。

ただ、zlibでそのまま利用するとheaderとか余計なものがついてくるので、排除したりする必要がある。

Java/scalaであれば java.util.zip.{Deflater, Inflater}を利用すればよい。

またheaderを排除するには、constructorのnowrapをtrueにすればよい

val inflater = new Inflater(true)

val deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true)

これで生の圧縮結果、もしくはheaderなしのbyte列でも解凍できる。

っていうことをscalaの練習がてら書いたのが https://github.com/takeru911/scala-gzip です。

gzipファイルを読み取れたので次は圧縮アルゴリズムを調べて実装する所存。