zip ファイルの展開 (zlib で zip にアクセスする (2) )

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 で確認しています。

zip2.cpp

関連エントリ
データ圧縮 zlib と gzip と zip (zlib で zip にアクセスする)