Rustのflate2でgzipファイルをデコードするときはMultiGzDecoderを使おう

## 1. はじめに

Rustのflate2でgzipファイルをデコードして読み込む時には、[GzDecoder](https://docs.rs/flate2/latest/flate2/read/struct.GzDecoder.html)ではなく[MultiGzDecoder](https://docs.rs/flate2/latest/flate2/read/struct.MultiGzDecoder.html#)を使おうという記事。

gzipを開くコードとgzipの構造のざっくりした説明を記事で共有しておく。

Rustのバージョンは1.79.0。

---

[flate2のドキュメントのイントロ](https://docs.rs/flate2/latest/flate2/index.html#about-multi-member-gzip-files)のそれぞれのstructのページに、

- GzDecoderはファイル内の最初のmemberのみ読み込む
- MultiGzDecoderはファイル内の全てのmemberを読み込む

的なことが書かれていたが、memberが何かわかっておらず、GzDecoderで複数memberを含むgzipファイルをデコードしようとして、失敗していた。（恥ずかしい、、）

そもそもgzipファイルを扱うのにその構造をさっぱりわかっていない、というのが不味かったように思う。調べたついでに記事にした。

## 2. サンプルコード

[こちらのGithubリポジトリ](https://github.com/k94-ishi/flate2-Multi-GzDecoder-test)からクローン可。  
gzipファイルを開いてバッファリングしながらデコードし、標準出力にテキストを書き出すコードを紹介する。
（大きいファイルの読み書きを想定。まずいところ、改善点があれば教えてください。）

``` rust
use std::fs::File;
use std::io::{stdout, BufRead, BufReader, BufWriter, Write};
use std::error::Error;
use flate2::read::MultiGzDecoder;


// gzipファイルのパスを受け取る。
// ファイルのオープンに失敗した時にはファイル名とエラー内容を表示。
fn open_reading_gzip(filename: &str) -> BufReader<MultiGzDecoder<File>> {
    let file = File::open(filename).unwrap_or_else(|err| {
        panic!("Cannnot open file '{}', Error: {}", filename, err);
    });
    let decoder = MultiGzDecoder::new(file);
    BufReader::new(decoder)
}

fn main() -> Result<(), Box<dyn Error>> {
    // 処理対象のファイルパスを直書きしている。
    let filename = "./test-multi.txt.gz";
    // 読み込むgzipファイルを開き、バッファリングして読み込むためのBufReaderを準備
    let reader = open_reading_gzip(filename);
    // バッファリングして標準出力に書き出すためのBufwriterを準備
    let out = stdout();
    let mut writer = BufWriter::new(out.lock());
    // ファイルを行ごとに読み出す。
    let mut counter_lines: u64 = 0;
    for line in reader.lines() {
        counter_lines += 1;
        // 行の取り出しに失敗した時にはファイル名、その行が何行目か、エラー内容を表示する。
        let line = line.unwrap_or_else(|err|{
            panic!("Cannnot reading the {}th line of {}, Error: {}", counter_lines, filename, err);
        });
        // 標準出力に書き出し
        writer.write_all((line + "\n").as_bytes())?;
    }
    writer.flush()?;
    Ok(())
}
```

## 3. gzipについて

[RFC 1952](https://datatracker.ietf.org/doc/html/rfc1952)を読むのが一番早いし間違いないが、読むのが大変。
手元で試せるざっくりしたことだけ書いておく。

### 3-1. サンプルファイルの作成

サンプルファイルを用意する。

``` zsh
echo -e "11 12\n21 22" > test1.txt 
echo -e "31 32\n41 42" > test2.txt
# ２つのテキストファイルを結合した後にgzip圧縮する。
cat test{1,2}.txt | gzip -c > test-single.txt.gz 
# ２つのテキストファイルを別々にgzip圧縮した後に、それぞれのgzipファイルを結合する。
gzip -k test{1,2}.txt
cat test{1,2}.txt.gz > test-multi.txt.gz
```

用意した`test-multi.txt.gz`と`test-single.txt.gz`は異なるファイルだが、解凍すれば中身は同じ。

- 解凍してテキストを確認

``` zsh
% gzcat test-single.txt.gz  # bashの場合はzcat
11 12
21 22
31 32
41 42
% gzcat test-multi.txt.gz  # bashの場合はzcat
11 12
21 22
31 32
41 42
```

- バイナリファイルとして16進数表示で確認

``` zsh
% od -tx1 test-single.txt.gz
0000000    1f  8b  08  00  9d  79  9c  66  00  03  33  34  54  30  34  e2
0000020    32  32  54  30  32  e2  32  36  54  30  36  e2  32  31  54  30
0000040    31  e2  02  00  a3  93  dc  4a  18  00  00  00                
0000054
% od -tx1 test-multi.txt.gz
0000000    1f  8b  08  08  8e  5f  9b  66  00  03  74  65  73  74  31  2e
0000020    74  78  74  00  33  34  54  30  34  e2  32  32  54  30  32  e2
0000040    02  00  e8  e0  b9  57  0c  00  00  00  1f  8b  08  08  54  61
# 出力3行目の 0c  00  00  00 までが１つ目のmember。（test1.txt.gzの部分）
# 出力3行目の 1f  8b  08  08 からが２つ目のmember。（test2.txt.gzの部分）
0000060    9b  66  00  03  74  65  73  74  32  2e  74  78  74  00  33  36
0000100    54  30  36  e2  32  31  54  30  31  e2  02  00  5e  c9  a0  47
0000120    0c  00  00  00                                                
0000124
# test1.txt.gzとtest2.txt.gzもついでに確認
% od -tx1 test1.txt.gz      
0000000    1f  8b  08  08  8e  5f  9b  66  00  03  74  65  73  74  31  2e
0000020    74  78  74  00  33  34  54  30  34  e2  32  32  54  30  32  e2
0000040    02  00  e8  e0  b9  57  0c  00  00  00                        
0000052
% od -tx1 test2.txt.gz      
0000000    1f  8b  08  08  54  61  9b  66  00  03  74  65  73  74  32  2e
0000020    74  78  74  00  33  36  54  30  36  e2  32  31  54  30  31  e2
0000040    02  00  5e  c9  a0  47  0c  00  00  00                        
0000052
```

&rarr; test1.txt.gzとtest2.txt.gzを繋ぎ合わせると確かにtest-multi.txt.gzになっている

### 3-2. [RFC 1952](https://datatracker.ietf.org/doc/html/rfc1952)の記載の確認

- 表記：１つの区切りが1バイトの大きさを表す。

``` text
	 +--------+
	 | 1バイト  |
	 +--------+
```

- gzipファイルの構造（「# ・・・」はこの記事用に追記したもの）

``` text
	# gzipファイルは圧縮されたデータセット"members"が連続する構造になっている。
	
      A gzip file consists of a series of "members" (compressed data
      sets).  The format of each member is specified in the following
      section.  The members simply appear one after another in the file,
      with no additional information before, between, or after them.
```

``` text
	# それぞれのmemberは以下の構造を持っている。

      Each member has the following structure:

	# ヘッダー
         +---+---+---+---+---+---+---+---+---+---+
         |ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more-->)
         +---+---+---+---+---+---+---+---+---+---+
           
      (if FLG.FNAME set)

         +=========================================+
         |...original file name, zero-terminated...| (more-->)
         +=========================================+
         
    # もし指定があれば色々なメタ情報がさらに続くが、
    # 今確認しているサンプルファイルのヘッダーは上記のみ。

	# 圧縮された本体部分のデータ
         +=======================+
         |...compressed blocks...| (more-->)
         +=======================+

	# フッター
           0   1   2   3   4   5   6   7
         +---+---+---+---+---+---+---+---+
         |     CRC32     |     ISIZE     |
         +---+---+---+---+---+---+---+---+
```

### 3-3. サンプルファイルと[RFC 1952](https://datatracker.ietf.org/doc/html/rfc1952)の記載の付き合わせ

`test1.txt.gz`の中身で確認していく。

``` zsh
% od -tx1 test1.txt.gz      
0000000    1f  8b  08  08  8e  5f  9b  66  00  03  74  65  73  74  31  2e
0000020    74  78  74  00  33  34  54  30  34  e2  32  32  54  30  32  e2
0000040    02  00  e8  e0  b9  57  0c  00  00  00                        
0000052
```

【ヘッダー】

- 先頭の2バイト：1f  8b
  "ID1"＋"ID2"  
  ID1 = 1f, ID2 = 8b で、このファイルがgzipファイルであることを表す。
- 3バイト目：08
  "CM"
  Compression Method、 圧縮方法を表している。CM = 08 で "deflate"。
- 4バイト目：08
  "FLG"  
  １バイト=8bitの内5bitを使用して、圧縮前ファイルの各種情報を持つ。FLG = 08 は「圧縮前のファイル名情報を持っている、というフラグのみON」の状態。
  また、`test-single.txt.gz`は標準入力からgzip圧縮を行ったので、このフラグがOFFとなっており、FLG = 00 となっている。
- 5 〜 8バイト目：8e  5f  9b  66
  "MTIME"  
  Modification TIME、最終更新日時。標準入力などから圧縮した場合、圧縮日時。
- 9バイト目：00
  "XFL"  
  eXtra FLags、拡張フラグ。
- 10バイト目：03
  "OS"  
  圧縮を行った場所のファイルシステムの種類。OS = 03 でUnix。
- 11〜20バイト目：74  65  73  74  31  2e  74  78  74  00
  "original file name, zero-terminated"  
  圧縮前のファイル名、末尾に 00 が付く。
  "test.txt"を16進数表示すると"74  65  73  74  31  2e  74  78  74"となる。
  [ASCIIコード変換｜進数変換 - 計算サイト](https://www.calc-site.com/bases/ascii)で確認した。

【フッター】

- 末尾4バイト：0c  00  00  00
  "ISIZE"
  Input SIZE、圧縮前ファイルのサイズ（を2の32乗で割った余り）。
  0x0000000c = 12
	- wc -c コマンドで確認
	``` zsh
	% wc -c test1.txt   
	      12 test1.txt
	```
- 末尾から5〜8バイト目：e8  e0  b9  57
  "CRC32"
  破損や改竄を検出するためのCRC32チェックサム。

## 4. サンプルコードの動作確認

展開対象のgzipファイルパスはmain()関数に直書きしている。

``` rust
	# main()関数１行目
    let filename = "./test-multi.txt.gz";
```

- MultiGzDecoderを使ったサンプルコードで`./test-multi.txt.gz`を展開

``` zsh
% cargo run
   Compiling gzip_test v0.1.0 (/...path.../gzip_test)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/gzip_test`
11 12
21 22
31 32
41 42
```

&rarr; ファイルの全て（２つのmember）がデコードされる。

- MultiGzDecoderをGzDecoderに置き換えて再実行

``` zsh
# GNU版のsedコマンドをgsedとして使っている。
% gsed -i "s/MultiGzDecoder/GzDecoder/g" src/main.rs
% cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/gzip_test`
11 12
21 22 
```

&rarr; ファイルの途中まで（１つ目のmemberのみ）がデコードされる。

- GzDecoderを使った実装で、処理対象のファイルを`./test-multi.txt.gz`から`./test-single.txt.gz`に変更して再実行

``` zsh
% gsed -i "s/test-multi\.txt\.gz/test-single.txt.gz/g" src/main.rs
% cargo run
   Compiling gzip_test v0.1.0 (/Users/keiichi/Work/test/gzip_test)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/gzip_test`
11 12
21 22
31 32
41 42
```

&rarr; memberが１つだけなので、ファイルの全てがデコードされる。

## 5. まとめ

- gzipファイルは１つのmemberから成る場合と複数のmemberから成る場合がある。
- rustのflate2のGzDecoderは１つのmemberのみをデコードする。
- rustのflate2でgzipファイルを開く場合は、MultiGzDecoderを使用する。
- 自分が処理するファイルがどんなものなのかくらいは、知っておいた方が良い。

P.S.
こういうまとめ記事を書くのは初めてです。
色々ご指摘いただけると超ありがたいです。

## A. 参考にしたサイト等

- gzip関連

[RFC 1952](https://datatracker.ietf.org/doc/html/rfc1952)
[gzip（Wikipedia）](https://ja.wikipedia.org/wiki/Gzip)
[Gzipについて調べてみた](https://qiita.com/takeru0911/items/903ac8b5d94660640af6)
[TAR32.DLL フォーマット説明ファイル](http://openlab.ring.gr.jp/tsuneo/soft/tar32_2/tar32_2/sdk/TAR_FMT.TXT)
[Go 言語と RFC から gzip の仕組みを紐解く](https://blog.8tak4.com/post/169064070956/principle-of-gzip-golang)
[gzip圧縮されたデータの展開方法いろいろ](https://qiita.com/mpyw/items/eb6ef5e444c2250361b5)

- flate2関係

[Rustでfastq/fastq.gzを読み書きする](https://illumination-k.dev/techblog/post/198bf005-b169-4992-8ad5-667f594d84e0)
[Rust-BioでGzip圧縮されたFASTAを読み込む](https://menseki.hatenablog.jp/entry/2021/09/08/000000)

- rustのI/O関係

[What is the difference between write_all and flush in io::Write trait?](https://stackoverflow.com/questions/73620289/what-is-the-difference-between-write-all-and-flush-in-iowrite-trait)
[Rustで高速な標準出力](https://keens.github.io/blog/2017/10/05/rustdekousokunahyoujunshutsuryoku/)
[Rustファイル操作勉強スレ](https://zenn.dev/r0227n/scraps/2b9de10d44cd06)