gzip は cat で連結できる

gzip は cat で連結できることを最近知った。

なんと gunzip(gzip(A) + gzip(B)) = gunzip(gzip(A+B)) になる。

$ echo "apple" > apple.txt
$ echo "banana" > banana.txt

# 個別に gzip ファイルを作って cat で連結する
$ gzip apple.txt
$ gzip banana.txt
$ cat apple.txt.gz banana.txt.gz > concat.txt.gz

# 展開すると 個別のファイルを連結したもの と同一になる
$ gunzip concat.txt.gz
$ cat concat.txt
apple
banana

なぜ連結できるかというと、gzip ファイルは gzip メンバ(圧縮したデータの集合)の繰り返しの構造になっているため。RFC 1952 GZIP file format specification version 4.3

これを利用すれば、特定のバイナリを 1000 バイトごとに分割して個別に gzip 圧縮し、

# ls コマンドをコピーして 1000 バイトごとにファイル分割する
$ cp /usr/bin/ls .
$ split -b 1000 ./ls
$ ls
ls   xaf  xal  xar  xax  xbd  xbj  xbp  xbv  xcb  xch  xcn  xct  xcz  xdf  xdl  xdr  xdx  xed
xaa  xag  xam  xas  xay  xbe  xbk  xbq  xbw  xcc  xci  xco  xcu  xda  xdg  xdm  xds  xdy  xee
xab  xah  xan  xat  xaz  xbf  xbl  xbr  xbx  xcd  xcj  xcp  xcv  xdb  xdh  xdn  xdt  xdz  xef
xac  xai  xao  xau  xba  xbg  xbm  xbs  xby  xce  xck  xcq  xcw  xdc  xdi  xdo  xdu  xea
xad  xaj  xap  xav  xbb  xbh  xbn  xbt  xbz  xcf  xcl  xcr  xcx  xdd  xdj  xdp  xdv  xeb
xae  xak  xaq  xaw  xbc  xbi  xbo  xbu  xca  xcg  xcm  xcs  xcy  xde  xdk  xdq  xdw  xec

$ gzip x*
$ ls
ls      xai.gz  xar.gz  xba.gz  xbj.gz  xbs.gz  xcb.gz  xck.gz  xct.gz  xdc.gz  xdl.gz  xdu.gz  xed.gz
xaa.gz  xaj.gz  xas.gz  xbb.gz  xbk.gz  xbt.gz  xcc.gz  xcl.gz  xcu.gz  xdd.gz  xdm.gz  xdv.gz  xee.gz
xab.gz  xak.gz  xat.gz  xbc.gz  xbl.gz  xbu.gz  xcd.gz  xcm.gz  xcv.gz  xde.gz  xdn.gz  xdw.gz  xef.gz
xac.gz  xal.gz  xau.gz  xbd.gz  xbm.gz  xbv.gz  xce.gz  xcn.gz  xcw.gz  xdf.gz  xdo.gz  xdx.gz
xad.gz  xam.gz  xav.gz  xbe.gz  xbn.gz  xbw.gz  xcf.gz  xco.gz  xcx.gz  xdg.gz  xdp.gz  xdy.gz
xae.gz  xan.gz  xaw.gz  xbf.gz  xbo.gz  xbx.gz  xcg.gz  xcp.gz  xcy.gz  xdh.gz  xdq.gz  xdz.gz
xaf.gz  xao.gz  xax.gz  xbg.gz  xbp.gz  xby.gz  xch.gz  xcq.gz  xcz.gz  xdi.gz  xdr.gz  xea.gz
xag.gz  xap.gz  xay.gz  xbh.gz  xbq.gz  xbz.gz  xci.gz  xcr.gz  xda.gz  xdj.gz  xds.gz  xeb.gz
xah.gz  xaq.gz  xaz.gz  xbi.gz  xbr.gz  xca.gz  xcj.gz  xcs.gz  xdb.gz  xdk.gz  xdt.gz  xec.gz

全体を連結したあとに解凍して特定のバイナリを完全に復元することだってできる!

$ cat x* > ls.gz
$ gunzip ls.gz
gzip: ls already exists; do you wish to overwrite (y or n)? y
$ chmod u+x ./ls
$ ./ls
ls    xai.gz    xar.gz  xba.gz  xbj.gz  xbs.gz  xcb.gz  xck.gz  xct.gz  xdc.gz  xdl.gz  xdu.gz  xed.gz
xaa.gz xaj.gz   xas.gz  xbb.gz  xbk.gz  xbt.gz  xcc.gz  xcl.gz  xcu.gz  xdd.gz  xdm.gz  xdv.gz  xee.gz
xab.gz xak.gz   xat.gz  xbc.gz  xbl.gz  xbu.gz  xcd.gz  xcm.gz  xcv.gz  xde.gz  xdn.gz  xdw.gz  xef.gz
xac.gz xal.gz   xau.gz  xbd.gz  xbm.gz  xbv.gz  xce.gz  xcn.gz  xcw.gz  xdf.gz  xdo.gz  xdx.gz
xad.gz xam.gz   xav.gz  xbe.gz  xbn.gz  xbw.gz  xcf.gz  xco.gz  xcx.gz  xdg.gz  xdp.gz  xdy.gz
xae.gz xan.gz   xaw.gz  xbf.gz  xbo.gz  xbx.gz  xcg.gz  xcp.gz  xcy.gz  xdh.gz  xdq.gz  xdz.gz
xaf.gz xao.gz   xax.gz  xbg.gz  xbp.gz  xby.gz  xch.gz  xcq.gz  xcz.gz  xdi.gz  xdr.gz  xea.gz
xag.gz xap.gz   xay.gz  xbh.gz  xbq.gz  xbz.gz  xci.gz  xcr.gz  xda.gz  xdj.gz  xds.gz  xeb.gz
xah.gz xaq.gz   xaz.gz  xbi.gz  xbr.gz  xca.gz  xcj.gz  xcs.gz  xdb.gz  xdk.gz  xdt.gz  xec.gz

(いやこの例になんの意味があるかと言ったら意味はないんですが、たまにメール添付サイズの容量の都合で似たことを人為的にやってる人がいたりしますよね。Unix ライクな行動だったんですね。)

ただし注意点としては gzip メンバ(実データではないメタデータ的なもの)の数が増えるので gzip(A) + gzip(B) = gzip(A+B) ではない。要は全体を cat してから gzip したほうが圧縮効率が上がる。

$ echo "apple" > apple.txt
$ echo "banana" > banana.txt

# 個別に gzip ファイルを作って cat で連結する(72バイト)
$ cat apple.txt.gz banana.txt.gz > gzip+gzip.gz
$ stat --format "%s" gzip+gzip.gz
72

# ファイルを cat で連結してから gzip ファイルを作る(40バイト)
$ cat apple.txt banana.txt > gzip.txt
$ gzip gzip.txt
$ stat --format "%s" gzip.txt.gz
40

なんか古臭い gzip の仕組みを知ったからといってどうなるんだという感はあるが、この gzip の仕組みを利用して tar.gz を Seekable にしようという GitHub - google/crfs: CRFS: Container Registry Filesystem やそれをベースに拡張した stargz-snapshotter/estargz.md at main · containerd/stargz-snapshotter · GitHub が考案されていて、コンテナ起動の高速化技術である lazy-pulling で利用されている。意外なところで渋い知識が役に立つことがあるんだな(自分は逆に辿ったけど)と思った。