13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Rust] zip ファイルを読む

Posted at

zip ファイルは, 「ヘッダー+圧縮したファイル本体」という組がファイルの数だけ繰り返されたものに最後に全体のフッター (セントラルディレクトリ) がくっついているという構造をしています. 詳細については wikipedia が異様に詳しいですし, こちらの gist がとても役に立ちます. 公式の仕様 (英語) もあります.

ここでは一番シンプルな場合 (暗号化なし, 圧縮アルゴリズムは deflate のみ) に限って Rust で zip ファイルを読むコードをつくってみます1.

zip ファイルの構造を読み取る

zip ファイルのパスが与えられたときに, その中に保持されているファイル名を出力する関数をつくってみます. これは次のような形になっているはずです.

use std::path::Path;
use std::fs::File;
use std::io::{Read, BufReader, Seek, SeekFrom, Result};

fn parse<P: AsRef<Path>>( fp: P ) -> Result<()> {
    let file = File::open(fp)?;
    let mut reader = BufReader::new(file);
    
    // この部分をつくる
    ...
    
    Ok(())
}

ファイルヘッダー

ファイルヘッダーは常に "PK\003\004" から始まります. 従ってファイル冒頭 4 byte を見れば zip ファイルかどうかが判別できるようになっている訳です. なおヘッダーはリトルエンディアンです.

const LOCAL_FILE_HEADER_SIGNATURE: [u8;4] = [ 0x50, 0x4b, 0x03, 0x04 ];

let mut buf = [0u8; 4];
reader.read(&mut buf)?;
debug_assert_eq!( buf, LOCAL_FILE_HEADER_SIGNATURE );

次の 2 byte はそのファイルを展開するのに必要な PKZIP の最小バージョン (を 10 倍した値) が入っています. その次の 2 byte は暗号化されているか等の情報を保持しています. どちらもいまは必要ないので読み飛ばしてしまいましょう.

reader.seek(SeekFrom::Current(4))?; // 4 byte 読み飛ばす

次の 2 byte は圧縮アルゴリズムを指定します. 0 なら非圧縮, 8 なら deflate です. 他にもありますが省略.

let comp_method = {
    let mut buf = [0u8; 2];
    reader.read(&mut buf)?;
    u16::from_le_bytes(buf)
};

続いてファイル変更日時 (4 byte), 誤り検出符号 (CRC-32, 4 byte) が入っていますがこれも省略. その次の 4 byte が圧縮後のファイル本体のデータサイズ, その次が圧縮前のデータサイズです. どちらも 4 byte (つまり u32) だから普通の zip ファイルは 4 GB より大きなファイルを格納できないんですね.

reader.seek(SeekFrom::Current(8))?; 

let comp_size = {
    let mut buf = [0u8; 4];
    reader.read(&mut buf)?;
    u32::from_le_bytes(buf)
};
let uncomp_size = {
    let mut buf = [0u8; 4];
    reader.read(&mut buf)?;
    u32::from_le_bytes(buf)
};

次の 2 byte はファイル名の長さ, その次の 2 byte は拡張フィールドの長さです. その次に, いま読み取った長さのファイル名および拡張フィールドが続きます.

let file_name_length = {
    let mut buf = [0u8; 2];
    reader.read(&mut buf)?;
    u16::from_le_bytes(buf) as usize
};
let extra_field_length = {
    let mut buf = [0u8; 2];
    reader.read(&mut buf)?;
    u16::from_le_bytes(buf) as usize
};

let file_name = {
    let mut buf = vec![0u8; file_name_length];
    reader.read_exact(&mut buf)?;
    String::from_utf8(buf).unwrap()
};

let extra_field = {
    let mut buf = vec![0u8; extra_field_length];
    reader.read_exact(&mut buf)?;
    buf
};

これでファイルヘッダーは終了です. 欲しい情報を出力するなりなんなりしてください.

println!("{}", file_name);

ファイル本体とセントラルディレクトリ

とりあえずファイル名にしか興味がないのであれば, ファイル本体は省略できますね.

reader.seek(SeekFrom::Current(comp_size as i64))?;

そうしたら次のファイルのファイルヘッダーが始まるので上の手続きを繰り返せばよいです. または, すべてのファイルが終了すると次はセントラルディレクトリが始まります. セントラルディレクトリは冒頭 4 byte が "PK\001\002" になっているので, これを見れば区別できます. なので次の 4 byte が "PK\001\002" になるまで以上の処理を反復するというコードになるでしょう.

ファイルの中身を読み取る

ファイルの中身が欲しい場合, それが圧縮されているかどうかで処理が変わります. まず上ではファイル本体を読み飛ばしてしまいましたが, その部分をちゃんとバッファーに読み込むようにします.

let content = {
    let mut buf = vec![ 0u8, comp_size as usize ];
    reader.read(&mut buf)?;
    buf
};

次に comp_method の値を見ます. これが 0 なら圧縮されていないので, 読み込んだままの content がファイルの実体です. comp_method が 8 なら deflate アルゴリズムで圧縮されているので, 解凍します. 例えば inflate クレートを用いるならば

use inflate::inflate_bytes;

let content = if comp_method == 8 {
    inflate_bytes(&content).unwrap()
} else {
    content
};

で完了です.

  1. 普通に zip ファイルをつくるとこうなると思います (Windows でエクスプローラからつくった場合や Linux で zip コマンドをオプションなしで使った場合).

13
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?