zip のアクセスはファイル単位なので比較的容易で、
圧縮解凍も zlib を使うことが出来ます。
apk や ipa, jar など様々なところで利用されています。
前回に続き、実際に zip ファイルを展開してみます。
zip ヘッダやファイルの情報はリトルエンディアンで格納されていますが、
アライメントが考慮されていないのでバイトアクセスが必要になります。
今後多用するのでメモリアクセス命令を作っておきます。
typedef unsigned short UI16; typedef unsigned int UI32; namespace le { inline UI32 Load2( const unsigned char* ptr ) { return (ptr[0]) | (ptr[1]<< 8); } inline UI32 Load4( const unsigned char* ptr ) { return (ptr[0]) | (ptr[1]<< 8) | (ptr[2]<<16) | (ptr[3]<<24); } inline void Store2( unsigned char* ptr, UI32 value ) { ptr[0]= static_cast( value ); ptr[1]= static_cast ( value >> 8); } inline void Store4( unsigned char* ptr, UI32 value ) { ptr[0]= static_cast ( value ); ptr[1]= static_cast ( value >> 8 ); ptr[2]= static_cast ( value >>16 ); ptr[3]= static_cast ( value >>24 ); } }
これを用いて、unaligned access 用のデータタイプを定義します。
struct UI16LE { unsigned char Memory[2]; UI32 Get() const { return le::Load2( Memory ); } void Set( UI32 value ) { le::Store2( Memory, value ); } }; struct UI32LE { unsigned char Memory[4]; UI32 Get() const { return le::Load4( Memory ); } void Set( UI32 value ) { le::Store4( Memory, value ); } };
UI16LE, UI32LE はそれぞれ UI16 (unsigned short), UI32 (unsigned int) と
対になっており、2byte, 4byte の符号なし整数型です。
アライメントを考慮する必要がなく、かつプラットフォーム非依存で
LittleEndian で読み書きします。
これで zip のヘッダ構造を定義できるようになりました。
下記は読み出しに必要な最小限で、32bit ヘッダのみとなっています。
enum { ZIP_SIGNATURE_LOCAL32 = 0x04034b50, ZIP_SIGNATURE_CENTRALDIR32 = 0x02014b50, ZIP_SIGNATURE_CENTRALDIR32_EOR = 0x06054b50, }; struct T_ZIP_LOCAL32 { UI32LE Signature; // PK0304 = 0x04034b50 UI16LE Extract; UI16LE BitFlag; UI16LE Method; UI16LE Time; UI16LE Date; UI32LE CRC32; UI32LE CompressedSize; UI32LE UncompressedSize; UI16LE NameLength; UI16LE ExtraLength; }; struct T_ZIP_CENTRALDIR32 { UI32LE Signature; // PK0102 = 0x02014b50 UI16LE Version; UI16LE Extract; UI16LE BitFlag; UI16LE Method; UI16LE Time; UI16LE Date; UI32LE CRC32; UI32LE CompressedSize; UI32LE UncompressedSize; UI16LE NameLength; UI16LE ExtraLength; UI16LE CommentLength; UI16LE DiskNumberStart; UI16LE InternalAttributes; UI32LE ExternalAttributes; UI32LE LocalHeaderOffset; public: const char* GetNamePointer() const { return reinterpret_cast( reinterpret_cast ( this ) + sizeof(T_ZIP_CENTRALDIR32) ); } const bool IsDirectory() const { unsigned int name_length= NameLength.Get(); return name_length && GetNamePointer()[ name_length-1 ] == '/'; } const T_ZIP_CENTRALDIR32* Next() const { return reinterpret_cast ( reinterpret_cast ( this ) + sizeof(T_ZIP_CENTRALDIR32) + NameLength.Get() + ExtraLength.Get() + CommentLength.Get() ); } }; struct T_ZIP_CENTRALDIR32_EOR { UI32LE Signature; // PK0506 = 0x06054b50 UI16LE NumberOfThisDisk; UI16LE StartDisk; UI16LE TotalEntriesDisk; UI16LE TotalEntries; UI32LE CentralDirectorySize; UI32LE CentralDirectoryOffset; UI16LE CommentLength; };
前回の手順通り、まずは EOR (End of Central Directory record) を検索します。
const int EOR_LOCATOR_SIZE= 512; unsigned char Locator[EOR_LOCATOR_SIZE]; file.Seek( -EOR_LOCATOR_SIZE, SEEK_END ); file.Read( Locator, EOR_LOCATOR_SIZE ); const unsigned char* ptr= Locator; const unsigned char* end_ptr= ptr + EOR_LOCATOR_SIZE - sizeof(T_ZIP_CENTRALDIR32_EOR) + sizeof(UI32LE); for(; ptr < end_ptr ; ptr++ ){ if( *ptr == 'P' && le::Load4( ptr ) == ZIP_SIGNATURE_CENTRALDIR32_EOR ){ // found } }
↑上のコードはファイルの終端から 512byte を読み込んで、EOR の Signature
"PK0506" (0x06054b50) を検索しています。
T_ZIP_CENTRALDIR32_EOR の CommentLength の分だけ追加情報を挿入できるので、
必ずしもこの範囲に存在するとは限りません。
見つからない場合にさらに読み進める処理が必要かもしれません。
EOR がわかれば CentralDirectoryOffset, CentralDirectorySize を使って
Central Directory を読み込むことが出来ます。
ファイル名検索に何度も利用するので、最初にメモリに読み込んでおきます。
EOR 読み込みと合わせて class 化します。
class ZipDirectory { unsigned int DirectoryEntries; Buffer DirectoryImage; public: const T_ZIP_CENTRALDIR32* Begin() const { return DirectoryImage.Address(); } bool IsEnd( const T_ZIP_CENTRALDIR32* dir_ptr ) const { return reinterpret_cast (dir_ptr) >= reinterpret_cast (DirectoryImage.Address ()) + DirectoryImage.Size(); } static const unsigned char* FindEOR( const unsigned char* ptr, size_t size ) { const unsigned char* end_ptr= ptr + size - sizeof(T_ZIP_CENTRALDIR32_EOR) + sizeof(UI32LE); for(; ptr < end_ptr ; ptr++ ){ if( *ptr == 'P' && le::Load4( ptr ) == ZIP_SIGNATURE_CENTRALDIR32_EOR ){ return ptr; } } return NULL; } bool Load( const char* zip_file_name ) { File file; if( !file.Open( zip_file_name ) ){ return false; } const int EOR_LOCATOR_SIZE= 512; unsigned char Locator[EOR_LOCATOR_SIZE]; file.Seek( -EOR_LOCATOR_SIZE, SEEK_END ); size_t read_size= file.Read( Locator, EOR_LOCATOR_SIZE ); const unsigned char* ptr= FindEOR( Locator, read_size ); if( !ptr ){ file.Close(); return false; } const T_ZIP_CENTRALDIR32_EOR* eor= reinterpret_cast ( ptr ); unsigned int dir_size= eor->CentralDirectorySize.Get(); DirectoryEntries= eor->TotalEntries.Get(); DirectoryImage.Alloc( dir_size ); file.Seek( eor->CentralDirectoryOffset.Get(), SEEK_SET ); file.Read( DirectoryImage.Address (), dir_size ); file.Close(); return true; } };
これで zip ファイル内のファイル情報にアクセスできるようになりました。
下記は ZipDirectory を使ってファイル名一覧を取り出すサンプルです。
T_ZIP_CENTRALDIR32 内のファイル名が 0 終端になっていない点に注意。
void file_lsit( const char* zip_file_name ) { ZipDirectory directory; directory.Load( zip_file_name ); const int NAME_BUFFER_SIZE= 512; char name_buffer[ NAME_BUFFER_SIZE ]; const T_ZIP_CENTRALDIR32* dir_ptr= directory.Begin(); for(; !directory.IsEnd( dir_ptr ) ; dir_ptr= dir_ptr->Next() ){ unsigned int name_length= dir_ptr->NameLength.Get(); assert( name_length < NAME_BUFFER_SIZE ); // ファイル名の取り出し memcpy( name_buffer, dir_ptr->GetNamePointer(), name_length ); name_buffer[name_length]= '\0'; printf( "%8d %8d %s\n", uncompressed_size, compressed_size, name_buffer ); } }
ZipDirectory の中に出てくる Buffer は汎用的なメモリ確保を行っています。
class Buffer { void* Memory; size_t MemorySize; Buffer( const Buffer& ){} Buffer& operator=( const Buffer& src ){ return *this; } public: Buffer() : Memory( NULL ), MemorySize( 0 ){} ~Buffer() { Release(); } void Release() { if( Memory ){ free( Memory ); Memory= NULL; } } void Alloc( size_t size ) { Release(); Memory= malloc( size ); MemorySize= size; } void Shrink( size_t size ) { assert( size <= MemorySize ); MemorySize= size; } size_t Size() const { return MemorySize; } unsigned long SizeLong() const { return static_cast( MemorySize ); } template T* Address() const { return reinterpret_cast ( Memory ); } };
同様に File 型も必要に応じて作ります。
例えば stdio なら下記の通り。
class File { FILE* Fp; public: File() : Fp( NULL ){} ~File() { Close(); } bool Open( const char* file_name ) { #if _WINDOWS fopen_s( &Fp, file_name, "rb" ); #else Fp= fopen( file_name, "rb" ); #endif return Fp != NULL; } bool Create( const char* file_name ) { #if _WINDOWS fopen_s( &Fp, file_name, "wb" ); #else Fp= fopen( file_name, "wb" ); #endif return Fp != NULL; } void Close() { if( Fp ){ fclose( Fp ); Fp= NULL; } } size_t Read( void* buffer, size_t size ) { return fread( buffer, 1, size, Fp ); } size_t Write( void* buffer, size_t size ) { return fwrite( buffer, 1, size, Fp ); } void Seek( long long offset, int origin ) { #if _WINDOWS _fseeki64( Fp, offset, origin ); #else fseek( Fp, offset, origin ); #endif } };
ファイル情報がとれたので、あとはファイルの内容を読み出すだけです。
下記 UnzipFile() は、zip 内のファイルを解凍することが出来ます。
bool UnzipFile( const T_ZIP_CENTRALDIR32* entry, const char* zip_file_name, const char* extract_file_name ) { File zip_file; if( !zip_file.Open( zip_file_name ) ){ return false; } // Local Header の読み込み T_ZIP_LOCAL32 LocalHeader; zip_file.Seek( entry->LocalHeaderOffset.Get(), SEEK_SET ); zip_file.Read( &LocalHeader, sizeof(T_ZIP_LOCAL32) ); assert( LocalHeader.Signature.Get() == ZIP_SIGNATURE_LOCAL32 ); // Offset の算出 unsigned int local_offset= sizeof(T_ZIP_LOCAL32) + LocalHeader.NameLength.Get() + LocalHeader.ExtraLength.Get(); unsigned int uncompressed_size= entry->UncompressedSize.Get(); unsigned int compressed_size= entry->CompressedSize.Get(); // データ本体の読み込み Buffer src_buffer; src_buffer.Alloc( compressed_size ); zip_file.Seek( entry->LocalHeaderOffset.Get() + local_offset, SEEK_SET ); zip_file.Read( src_buffer.Address(), compressed_size ); Buffer dest_buffer; dest_buffer.Alloc( uncompressed_size ); switch( entry->Method.Get() ){ case 0: // 非圧縮 assert( uncompressed_size == compressed_size ); memcpy( dest_buffer.Address (), src_buffer.Address (), uncompressed_size ); break; case 8: // 圧縮されている場合 if( !zlib_uncompress_raw( dest_buffer, src_buffer ) ){ return false; } break; default: assert( 0 ); break; } // CRC の確認 if( crc32( 0, dest_buffer.Address (), uncompressed_size ) != entry->CRC32.Get() ){ return false; } // 書き込み File file; if( !file.Create( extract_file_name ) ){ return false; } file.Write( dest_buffer.Address (), dest_buffer.Size() ); file.Close(); return true; }
UnzipFile() では、Central Directory (T_ZIP_CENTRALDIR32) の情報を元に
まず Local Header (T_ZIP_LOCAL32) を読み込んでいます。
T_ZIP_LOCAL32 の NameLength と ExtraLength から実際のデータ位置が求まるので、
あらためてデータ本体を読み込みます。
データが圧縮されている場合は、前回解説したとおり zlib の raw format と
みなして展開しています。
size_t zlib_uncompress_raw( Buffer& dest, const Buffer& source ) { z_stream stream; memset( &stream, 0, sizeof(z_stream) ); stream.next_in= source.Address(); stream.avail_in= source.SizeLong(); stream.next_out= dest.Address (); stream.avail_out= dest.SizeLong(); // raw format の指定 if( inflateInit2( &stream, -MAX_WBITS ) != Z_OK ){ return 0; } int err= inflate( &stream, Z_FINISH ); if( err != Z_STREAM_END && err != Z_OK ){ return 0; } err= inflateEnd( &stream ); assert( dest.Size() == stream.total_out ); return stream.total_out; }
ここまでのコードで zip 内ファイルを取り出すことが可能です。
ただし展開パスにディレクトリが含まれている場合は書き込みに失敗するので、
もう少しだけ手を加えてみます。
下記の MakePath() は階層に対応した mkdir() です。(終端に '/' が必要)
static bool MakeDirectory( const char* path ) { #if _WINDOWS return _mkdir( path ) == 0; #else return mkdir( path, 0755 ) == 0; #endif } static void MakePath( const char* extract_file_name ) { size_t length= strlen( extract_file_name ); Buffer path_buffer; path_buffer.Alloc( length + 1 ); char* str= path_buffer.Address(); const char* ptr= extract_file_name; for(; *ptr ;){ if( *ptr == '/' ){ *str= '\0'; MakeDirectory( path_buffer.Address () ); } *str++= *ptr++; } }
同じパスを何度も mkdir するのは非効率なので、前回と同じパスは省きます。
でもあまり効果が無いかもしれません。
struct PathCache { Buffer PrevPath; bool IsNewPath( const char* path ) { const char* last_path= NULL; for( const char* ptr= path ; *ptr ; ptr++ ){ if( *ptr == '/' ){ last_path= ptr + 1; } } if( last_path ){ size_t size= last_path - path; if( PrevPath.Size() == size + 1 ){ if( memcmp( PrevPath.Address(), path, size ) == 0 ){ return false; } } PrevPath.Alloc( size + 1 ); memcpy( PrevPath.Address (), path, size ); PrevPath.Address ()[size]= '\0'; return true; } return false; } const char* GetPath() const { return PrevPath.Address (); } };
zip の展開ができるようになりました。
下記 Unzip() は階層付きで zip に含まれる全ファイルを展開することが出来ます。
void Unzip( const char* zip_file_name ) { ZipDirectory directory; directory.Load( zip_file_name ); PathCache path_cache; const T_ZIP_CENTRALDIR32* dir_ptr= directory.Begin(); for(; !directory.IsEnd( dir_ptr ) ; dir_ptr= dir_ptr->Next() ){ assert( dir_ptr->Signature.Get() == ZIP_SIGNATURE_CENTRALDIR32 ); unsigned int name_length= dir_ptr->NameLength.Get(); const int NAME_BUFFER_SIZE= 512; char name_buffer[ NAME_BUFFER_SIZE ]; assert( name_length < NAME_BUFFER_SIZE ); memcpy( name_buffer, dir_ptr->GetNamePointer(), name_length ); name_buffer[name_length]= '\0'; if( !dir_ptr->IsDirectory() ){ if( path_cache.IsNewPath( name_buffer ) ){ MakePath( path_cache.GetPath() ); } UnzipFile( dir_ptr, zip_file_name, name_buffer ); } } }
int main( int argc, char** argv ) { for(; --argc ; Unzip( *++argv ) ); return 0; }
最終的なコードはこちら。Windows と Linux で確認しています。