1. はじめに
Rustのflate2でgzipファイルをデコードして読み込む時には、GzDecoderではなくMultiGzDecoderを使おうという記事。
gzipを開くコードとgzipの構造のざっくりした説明を記事で共有しておく。
Rustのバージョンは1.79.0。
flate2のドキュメントのイントロのそれぞれのstructのページに、
- GzDecoderはファイル内の最初のmemberのみ読み込む
- MultiGzDecoderはファイル内の全てのmemberを読み込む
的なことが書かれていたが、memberが何かわかっておらず、GzDecoderで複数memberを含むgzipファイルをデコードしようとして、失敗していた。(恥ずかしい、、)
そもそもgzipファイルを扱うのにその構造をさっぱりわかっていない、というのが不味かったように思う。調べたついでに記事にした。
2. サンプルコード
こちらのGithubリポジトリからクローン可。
gzipファイルを開いてバッファリングしながらデコードし、標準出力にテキストを書き出すコードを紹介する。
(大きいファイルの読み書きを想定。まずいところ、改善点があれば教えてください。)
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を読むのが一番早いし間違いないが、読むのが大変。
手元で試せるざっくりしたことだけ書いておく。
3-1. サンプルファイルの作成
サンプルファイルを用意する。
echo -e "11 12\n21 22" > test1.txt
echo -e "31 32\n41 42" > test2.txt
# 2つのテキストファイルを結合した後にgzip圧縮する。
cat test{1,2}.txt | gzip -c > test-single.txt.gz
# 2つのテキストファイルを別々に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
は異なるファイルだが、解凍すれば中身は同じ。
- 解凍してテキストを確認
% 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進数表示で確認
% 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 までが1つ目のmember。(test1.txt.gzの部分)
# 出力3行目の 1f 8b 08 08 からが2つ目の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
→ test1.txt.gzとtest2.txt.gzを繋ぎ合わせると確かにtest-multi.txt.gzになっている
3-2. RFC 1952の記載の確認
- 表記:1つの区切りが1バイトの大きさを表す。
+--------+
| 1バイト |
+--------+
- gzipファイルの構造(「# ・・・」はこの記事用に追記したもの)
# 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.
# それぞれの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の記載の付き合わせ
test1.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
【ヘッダー】
- 先頭の2バイト:1f 8b
"ID1"+"ID2"
ID1 = 1f, ID2 = 8b で、このファイルがgzipファイルであることを表す。 - 3バイト目:08
"CM"
Compression Method、 圧縮方法を表している。CM = 08 で "deflate"。 - 4バイト目:08
"FLG"
1バイト=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コード変換|進数変換 - 計算サイトで確認した。
【フッター】
- 末尾4バイト:0c 00 00 00
"ISIZE"
Input SIZE、圧縮前ファイルのサイズ(を2の32乗で割った余り)。
0x0000000c = 12- wc -c コマンドで確認
% wc -c test1.txt 12 test1.txt
- 末尾から5〜8バイト目:e8 e0 b9 57
"CRC32"
破損や改竄を検出するためのCRC32チェックサム。
4. サンプルコードの動作確認
展開対象のgzipファイルパスはmain()関数に直書きしている。
# main()関数1行目
let filename = "./test-multi.txt.gz";
- MultiGzDecoderを使ったサンプルコードで
./test-multi.txt.gz
を展開
% 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
→ ファイルの全て(2つのmember)がデコードされる。
- MultiGzDecoderをGzDecoderに置き換えて再実行
# 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
→ ファイルの途中まで(1つ目のmemberのみ)がデコードされる。
- GzDecoderを使った実装で、処理対象のファイルを
./test-multi.txt.gz
から./test-single.txt.gz
に変更して再実行
% 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
→ memberが1つだけなので、ファイルの全てがデコードされる。
5. まとめ
- gzipファイルは1つのmemberから成る場合と複数のmemberから成る場合がある。
- rustのflate2のGzDecoderは1つのmemberのみをデコードする。
- rustのflate2でgzipファイルを開く場合は、MultiGzDecoderを使用する。
- 自分が処理するファイルがどんなものなのかくらいは、知っておいた方が良い。
P.S.
こういうまとめ記事を書くのは初めてです。
色々ご指摘いただけると超ありがたいです。
A. 参考にしたサイト等
- gzip関連
RFC 1952
gzip(Wikipedia)
Gzipについて調べてみた
TAR32.DLL フォーマット説明ファイル
Go 言語と RFC から gzip の仕組みを紐解く
gzip圧縮されたデータの展開方法いろいろ
- flate2関係
Rustでfastq/fastq.gzを読み書きする
Rust-BioでGzip圧縮されたFASTAを読み込む
- rustのI/O関係
What is the difference between write_all and flush in io::Write trait?
Rustで高速な標準出力
Rustファイル操作勉強スレ