無圧縮zipを作ってみる(1)

 zip形式は時代のニーズに応えるとともに様々な拡張が施されてきたため、最新の仕様に準拠するとそれなりに複雑なコードになりますが、zipのもっとも初期の形態、つまりアーカイブを行っただけの至極シンプルな形態であれば、そのファイルフォーマットはすこぶる単純です。

 zipの基本は、「ヘッダ+内容」を次々に連結していくところにあります。ヘッダ+内容でのみ構成される一般的なファイルを単細胞生物とすると、zipファイルは多細胞生物といったところでしょうか。

zipファイルの基本的な構成
zipファイルの基本形はヘッダ+データの繰り返し

 zipファイルのヘッダは一定のサイズを持った構造体で構成されます。これをC言語で記述すると以下のような感じとなります。

struct ZipHeader
{
	unsigned int signature;
	unsigned short needver;
	unsigned short option;
	unsigned short comptype;
	unsigned short filetime;
	unsigned short filedate;
	unsigned int crc32;
	unsigned int compsize;
	unsigned int uncompsize;
	unsigned short fnamelen;
	unsigned short extralen;
};

 signatureはヘッダの内容が何を意味するのかを数値で指定します。「ヘッダの後には一般的なアーカイブデータが続きます」と宣言する場合は、この値に0x04034B50を代入します。ちなみにバイナリエディタで開くとここは「PK0304」と表示されます。zipヘッダ宣言はいずれも「PK+16進数数値4桁」がくるので、zipファイルを読み込むときはこれをもとにヘッダの種類を識別していくことになります。

 needverは、アーカイブに収録されているデータを抜き取るために最低限必要なzipのバージョンを指定します。今回は圧縮しない最も初期のタイプなので、zipバージョンは1.0となるため、この値には「10」を代入します。

 optionでは、暗号化されているかなどのアーカイブファイルに関する情報をビットフラグで指定します。今回は指定するオプションは全くないため、0を代入します。

 comptypeでは、アーカイブデータを圧縮するのに用いたアルゴリズムを数値で指定します。無圧縮の場合は0になります。

 filetime/filedateには、それぞれ元となるデータの日付・時刻を代入します。元のデータがディスクに保存されているファイルなら、ファイルの作成日を、メモリーから直接アーカイブ化するのであれば、アーカイブした日付を入れるのが一般的です。

 日付や時刻は以下のMS-DOS形式が用いられます。

MS-DOS日付形式(数値はビット)
0 - 4日(1~31)
5 - 8月(1 = 1 月、2 = 2 月……)
9 - 15年(1980年からの経過年数)
MS-DOS時刻形式(数値はビット)
0 - 4秒を2で割った値(0~29)
5 - 10分(0~59)
11 - 15時(24時間制で0~23)

 MS-DOS形式の日付に変換する関数例は以下のようになります。なお、ウィンドウズのAPI関数から読み込んだファイルから取得する日付に対してはFileTimeToDosDateTime()を使った方が手っ取り早いでしょう。

// 日付を取得
unsigned short GetDosDate(int year, int month, int day)
{
	return (unsigned short)(
		((unsigned)(year - 1980) << 9) |
		((unsigned)month << 5) |
		(unsigned)day);
}

// 時刻を取得
unsigned short GetDosTime(int hour, int minute, int second)
{
	return (unsigned short)(
		((unsigned)hour << 11) |
		((unsigned)minute << 5) |
		((unsigned)second >> 1));
}

crc32では、圧縮前のデータをCRC32アルゴリズムで数値化したデータを入れます。このアルゴリズムを用いると、元のデータが1バイト違うだけでも全く異なる計算結果になるため、この値を元に、オリジナルのデータと同一であるかを調べるためによく使われます。zip形式で用いられるCRC32の計算アルゴリズムは下記の通りとなります。

static unsigned int table[256];

// 計算の元となるテーブルをあらかじめ作成
void InitCRC32()
{
	unsigned int poly = 0xEDB88320, u, i, j;

	for(i = 0; i < 256; i++){
		u = i;

		for(j = 0; j < 8; j++){
			if(u & 0x1){
				u = (u >> 1) ^ poly;
			}else{
				u >>= 1;
			}
		}

		table[i] = u;
	}
}

// crc32_startでは、前のバッファブロックのCRC32値をここに指定する。
// バッファが単一であるのなら、個々に引数は指定しなくてよい(0xFFFFFFFFが初期値)。
unsigned int GetCRC32(unsigned char *buffer, unsigned int bufferlen, unsigned int crc32_start)
{
	unsigned int result = crc32_start;
	for(unsigned int i = 0; i < bufferlen; i++){
		result = (result >> 8) ^ table[buffer[i] ^ (result & 0xFF)];
	}
	return ~result;
}

compsize/uncompsizeでは、圧縮前と圧縮後のサイズをバイト数で指定します。今回は圧縮はしないのでともにデータのサイズをそのまま格納します。

fnamelenには、元のファイル名のサイズを入力します。ファイル名は原則として半角英数字しか使えません。漢字などが文字として認識されるのはOS次第であり、通常、海外のウィンドウズや別のOSでは日本語によるファイル名は文字化けしてしまいます。Unicodeでしか使えないような文字が含まれる場合は、オプションフラグの11番目にビットフラグを立てることで、UTF-8文字列を利用することが認められています。なお、ファイル名にNULL文字を含めてはいけません。ファイル名自体はzipヘッダに続けて記録します

extralenでは、zipアーカイブに拡張データがあるときの、そのデータのサイズを指定します。今回の無圧縮ファイルでは拡張するデータはないので0を入力します。ちなみに、拡張データがある場合は、ファイル名データの後ろに記録することになります。