Zipの内部で使うアルゴリズムを独自のものに切り替える

Goのarchive/zipパッケージは、圧縮のアルゴリズムをいじることができます。もともとのzipのファイルフォーマットには、圧縮メソッドを識別する、2バイトのフラグを持っています。現在の仕様で登録されているフォーマットは以下の通りです。標準のDeflateが8番です。

```
        0 - The file is stored (no compression)
        1 - The file is Shrunk
        2 - The file is Reduced with compression factor 1
        3 - The file is Reduced with compression factor 2
        4 - The file is Reduced with compression factor 3
        5 - The file is Reduced with compression factor 4
        6 - The file is Imploded
        7 - Reserved for Tokenizing compression algorithm
        8 - The file is Deflated
        9 - Enhanced Deflating using Deflate64(tm)
       10 - PKWARE Data Compression Library Imploding (old IBM TERSE)
       11 - Reserved by PKWARE
       12 - File is compressed using BZIP2 algorithm
       13 - Reserved by PKWARE
       14 - LZMA
       15 - Reserved by PKWARE
       16 - IBM z/OS CMPSC Compression
       17 - Reserved by PKWARE
       18 - File is compressed using IBM TERSE (new)
       19 - IBM LZ77 z Architecture (PFS)
       96 - JPEG variant
       97 - WavPack compressed data
       98 - PPMd version I, Rev 1
       99 - AE-x encryption marker (see APPENDIX E)
```

# 圧縮メソッドの選択の仕方

圧縮するときに、ファイルごとにヘッダーを作って、それ経由でファイルを登録していきますが、そのヘッダーの中にMethodというuint16の変数があって、そこに数値を入れることで圧縮メソッドが選択できます。

ファイルごとなので、特定のファイルはDeflate、特定のファイルは無圧縮(zip.Store = 0)など、同じzipの中でもファイルによって使い分けることができます。epubファイルの場合、zipなのにmimetypeというファイルは無圧縮で保存しないとダメ、みたいな謎ルールがあってepubファイル作成に挑戦した人を悩ませたかもしれませんが、それはこのオプションの意味です。

```go
package main

import (
	"archive/zip"
	"os"
	"io"
)

func main() {
	out, _ := os.Create("output.zip")
	w := zip.NewWriter(out)
	// ファイルの数だけ、ヘッダーを作ってWriteする
	
	header := &zip.FileHeader{
		Name:   "test.bin",
		Method: zip.Deflate, // ここ！
	}
	writer, _ := w.CreateHeader(header)
	src, _ := os.Open("input.txt")
	io.Copy(writer, src)
	w.Close()
}
```

zipパッケージの中に圧縮メソッド集があって、この数値を見て圧縮したり展開する関数を切り替えています。

# 圧縮アルゴリズムの登録

Goのzipパッケージには圧縮アルゴリズムの登録のメソッドがあります。[サンプルコード](https://golang.org/pkg/archive/zip/#Writer.RegisterCompressor)では、Deflate（8)に、高圧縮なオプションを付与したDeflateのアルゴリズムを使う方法が紹介されています。

不特定多数と情報交換するのでなければ、別に独自のアルゴリズムでも問題はないでしょう。今回は高速圧縮・展開がうりのLZ4を使って見ます。他と被らなければ良いので、一番大きな数字の65535番をLZ4として登録します。今回は"github.com/pierrec/lz4"パッケージを使いました。

```go
	w := zip.NewWriter(out)
	// 65535番にLZ4を登録
	w.RegisterCompressor(65535, func(out io.Writer) (io.WriteCloser, error) {
		return lz4.NewWriter(out), nil
	})

	header := &zip.FileHeader{
		Name:   "test.bin",
		Method: 65535, // ここで指定
	}
```

これで圧縮時にLZ4が使われます。

展開側のプログラムにも別のアルゴリズムを登録します。今回は展開用です。展開時は各ファイルのヘッダーに登録された数字を見て自動的に展開するので、圧縮アルゴリズム以外の操作は不要です。なお、登録する関数は``io.ReadCloser``を返す必要がありますが、lz4.Readerは単なる``io.Reader``で、``Close()``メソッドを持っていません。``ioutil.NopCloser()``を使うと、ダミーの``Close()``メソッドを増やしてくれますので使えるようになります。便利！Goならわかるシステムプログラミングの3刷でちょっと追記したやつです。

```go
	inFile, _ := os.Open("out.zip")
	info, _ := inFile.Stat()
	r, _ := zip.NewReader(inFile), info.Size()))
	r.RegisterDecompressor(65535, func(in io.Reader) io.ReadCloser {
		return ioutil.NopCloser(lz4.NewReader(in))
	})
	// ファイル単位の操作はここ
	h := r.File[0]
	fr, _ := h.Open()
	content, _ := ioutil.ReadAll(fr)
```

これでLZ4を内部フォーマットにしたzipファイルの読み書きができるようになりました。

zipは64キロバイト以内のコメントをファイルごとや全体につけられます。JSONを入れるなどすればかなり柔軟にメタデータをデータに付与できます。生バイナリデータと、アノテーションなど、いろんな情報をシリアライズ化してまとめておくには、なかなか便利なやつです。.tar.gzとかとちがって、ファイル単位でランダムアクセスもできます。また、圧縮方式の情報が圧縮したデータ自身の外にあるので、Brotliのようなマジックナンバーが先頭についてない圧縮フォーマットにも対応できたりします。

また、macのリソースファイル方式みたいに、別のファイル名（先頭にピリオドをつけるとか、何かしらの拡張子をつけるとか）すれば、別のメタデータ的なファイルを突っ込むこともできますね。例えば、何かしらのスキーマが決まったJSONだとかのファイルを圧縮するときは、zstdのプリセット辞書を.presetみたいなファイル名で標準のdeflateで入れておいて、そこの部分を取り出してから展開することで、複数ファイルの合計のファイルサイズを削減したりも考えられます。というのも、zipはファイル単体で保存する方式なので、ファイル間での共通部分を見つけて圧縮みたいなのができないので、どうしても.tar.gzとかよりは圧縮率が下がってしまいます。そういう欠点を抑える、みたいなロジックを組んで見るのも楽しそうです。