【C++】gzip形式にて、ファイルを圧縮・展開する


更新

 C++を使用して、ファイルをgzip形式に圧縮する、または、gzip形式のファイルを展開する方法を説明します。

開発環境Visual Studio 2022
言語C++
ライブラリzlib 1.3
gzip-hpp 1.0.0
動作確認Windows 11
Windows 10
Windows 7
Ubuntu (g++)
FreeBSD (clang++)

※UNIX系OSは、付属のzlibにてgzip-hppを確認しました。

Windows環境にzlibを導入する

 zlibとは、C++にてgzipを扱うためのライブラリです。UNIX系OSには、標準で入っています。それは、「ls /usr/include/zlib.h」や「man zlib」と入力することで確認できます。

 Windows SDKには含まれていないので、別途導入する必要があります。以下、zlibをVisual Studio 2022にてビルドする手順です。

(1)
 https://zlib.net/からソース(zlib13.zip)をダウンロードして、展開します。

(2)
 ターミナルより、[Developer Command Prompt for VS 2022]を起動します(または、[スタート]-[すべてのアプリ]-[Visual Studio 2022]-[Developer Command Prompt for VS 2022])。仮に、ソースの展開先を「C:\temp\zlib13\zlib-1.3」、生成されるライブラリの出力先を「C:\temp\zlib13\zlib」とする場合、以下のコマンドを実行します。その際、-Aオプションは、組み込み先のソリューションプラットフォームに合わせます。x86の場合、「Win32」を指定します。


> cd C:\temp\zlib13\zlib-1.3
> mkdir build
> cd build
> cmake .. -G "Visual Studio 17 2022" -A x64 -D CMAKE_INSTALL_PREFIX:PATH="C:\temp\zlib13\zlib"
> cmake --build . --config Release --target install

(3)
 組み込み先のソリューションのプロジェクトフォルダーに、出力先の以下のファイルをコピーします。ここで、*.libファイルは名前変更可、*.dllファイルは名前変更不可です。

ファイル名備考
zconf.h
zlib.h
zlibstatic.libスタティックリンクの場合、ビルド時に必要
zlib.libダイナミックリンクの場合、ビルド時に必要
zlib.dllダイナミックリンクの場合、実行時に必要。
アプリケーションと同じ、または、パスが通ったフォルダーに配置

(4)
 プロジェクトのプロパティにて、[C/C++]-[全般]-[追加のインクルードディレクトリ]に、「$(ProjectDir)」が存在しない場合は追加します。また、[リンカー]-[入力]-[追加の依存ファイル]に、スタティックリンクの場合は「zlibstatic.lib」、ダイナミックリンクの場合は「zlib.lib」を追加します。これで、ソースに「#include <zlib.h>」を記述することで、zlibが使用可能となります。

gzip-hppを使う

 ここからは、zlib導入済みのWindows、UNIX系OS共通の話になります。zlibを直接呼び出してgzipを扱う方法は、多くのウェブページにて解説されていますが、難しいし面倒くさいです。そこで、zlibのラッパーである「gzip-hpp」(https://github.com/mapbox/gzip-hpp)を使用すると簡単になります。

 使用方法は、gzipフォルダーをプロジェクトフォルダーにコピーして、includeするだけです。ヘッダーファイルのみで動作します。


#include <gzip/compress.hpp>
#include <gzip/config.hpp>
#include <gzip/decompress.hpp>
#include <gzip/utils.hpp>
#include <gzip/version.hpp>

 サンプルソースを作ってみました。圧縮アプリケーションの例です。圧縮前データの先頭ポインターとサイズをgzip::compressに渡すと、圧縮後のstd::stringが戻ってきます。


#include <fstream>
#include <filesystem>

#include <gzip/compress.hpp>

using namespace std;

int main(int argc, char *argv[])
{
    // 最初の引数に、圧縮するファイルを指定
    if (argc < 2)
    {
        return 1;
    }

    size_t size = filesystem::file_size(argv[1]);
    char *pointer = new char[size];

    // 圧縮するファイルの読み込み
    ifstream ifs(argv[1], ios_base::binary);
    ifs.read(pointer, size);
    ifs.close();

    // 圧縮
    string compressed_data = gzip::compress(pointer, size);

    delete[] pointer;

    // gzipファイルの出力
    // ファイル名は、入力ファイルに拡張子.gzを付加する。
    ofstream ofs(argv[1] + string(".gz"), ios_base::binary);
    ofs << compressed_data;
    ofs.close();
}

 展開アプリケーションの例です。最初に、gzip::is_compressedにてgzip形式かどうかをチェックします。これは先頭2byteが0x1F 0x8Bかどうかを見ています。次に、gzipデータの先頭ポインターとサイズをgzip::decompressに渡すと、展開後のstd::stringが戻ってきます。


#include <fstream>
#include <filesystem>

#include <gzip/decompress.hpp>
#include <gzip/utils.hpp>

using namespace std;

int main(int argc, char *argv[])
{
    // 最初の引数に、gzipファイルを指定
    if (argc < 2)
    {
        return 1;
    }

    size_t size = filesystem::file_size(argv[1]);
    char *pointer = new char[size];

    // gzipファイルの読み込み
    ifstream ifs(argv[1], ios_base::binary);
    ifs.read(pointer, size);
    ifs.close();

    // gzip形式のチェック
    if (!gzip::is_compressed(pointer, size))
    {
        return 1;
    }

    // 展開
    string decompressed_data = gzip::decompress(pointer, size);

    delete[] pointer;

    // 展開したファイルの出力
    // ファイル名は、入力ファイルから拡張子.gzを削除する。
    // 拡張子.gz以外の場合は、同じファイル名で上書きする。
    filesystem::path pa = argv[1];

    if (pa.extension() == ".gz")
    {
        pa.replace_extension();
    }

    ofstream ofs(pa, ios_base::binary);
    ofs << decompressed_data;
    ofs.close();
}

 これらのサンプルソースは、ファイルサイズと拡張子の処理に<filesystem>を使用しているので、標準C++17(以降)でビルドしてください。Visual Studio 2022の場合、プロジェクトのプロパティにて、[全般]-[C++言語標準]に「ISO C++17標準(/std:c++17)」以降を選択します。

 UNIX系OSは、下記の例を参考にしてください。(サンプルソースをtest.cpp、実行ファイルをtestとする場合)


# g++の例
> g++ -std=c++17 -Wall -I. test.cpp -lz -o test

# clang++の例
> clang++ -std=c++17 -Wall -I. test.cpp -lz -o test

 gzipファイルフォーマットの公式仕様は、RFC1952(https://www.ietf.org/rfc/rfc1952.txt)です。これを読むと、gzipファイルは圧縮前のファイル名とタイムスタンプを格納することが可能ですが、必須ではありません。gzip-hppには、ファイル名やタイムスタンプのインターフェースが無いので、設定されないことになります。

 UbuntuやFreeBSDに入っているgzip/gunzipコマンドの動きを見ると、圧縮時にファイル名とタイムスタンプを格納していますが、展開時は、それを参照していないようです。いずれにしても、いろんなパターンで試しましたが、サンプルソースとgzip/gunzipコマンドの相互運用性には問題ありませんでした。

 最後に、RFC1952とzlibの実装(zlib-1.3\zutil.h)にて、異なる点を見つけました。それは、gzipファイルの先頭から10byte目のOS/ファイルシステム種別です。

RFC1952zlib
0x00FAT
0x01Amiga
0x02VMS
0x03Unix←(他に該当しないOSは全てUNIX)
0x04VM/CMS
0x05Atari TOS
0x06HPFS
0x07Macintosh
0x08Z-System
0x09CP/M
0x0ATOPS-20Windows
0x0BNTFS0x04,0x08と同じシリーズのIBMの何か
0x0CQDOS
0x0DAcorn RISCOS
0x0E
0x0F
0x10BeOS
0x11
0x12OS/400
0x130x07以外のAppleの何か
0xFFunknown

 このため、Windowsにて圧縮のサンプルソースを動かすと、0x0Bではなく、0x0Aが設定されます。