LHA書庫を読む

新たなネタ(実際にはライブラリのネタのネタ)としてBoost.IostreamsでLHA書庫ファイルの読み書きをやってみます。
ZIPは誰かやってそうなのでLHAです。
ヘッダの仕様は、
http://homepage1.nifty.com/dangan/Content/Home.html
あたりを参考にしています。
圧縮結果のサンプルを得るために、まずはLZHファイルのパースから始めました。
ちょっと書いただけで、既にライブラリのネタが見つかりました。

  • 毎回書いているリトルエンディアンデコードはそろそろライブラリとして切り出そう
  • 構造体のバイナリ読み書きもなんとかしたい
  • non_blocking_adapterにnon_blocking_adapterをかぶせると無駄

ネタが見つかり次第脱線するので、気長にいきましょう。
以下が今日書いた書庫中のファイル名一覧を表示するプログラムです。

#include <boost/filesystem/path.hpp>
#include <boost/iostreams/detail/adapter/non_blocking_adapter.hpp>
#include <boost/iostreams/read.hpp>
#include <boost/iostreams/seek.hpp>
#include <boost/cstdint.hpp>
#include <boost/scoped_array.hpp>
#include <cstring>
#include <stdexcept>

inline boost::uint16_t decode_little16(const char* s)
{
    return static_cast<boost::uint16_t>(
        (static_cast<unsigned>(static_cast<unsigned char>(s[0]))     ) |
        (static_cast<unsigned>(static_cast<unsigned char>(s[1])) << 8)
    );
}

inline boost::uint32_t decode_little32(const char* s)
{
    return static_cast<boost::uint32_t>(
        (static_cast<boost::uint32_t>(static_cast<unsigned char>(s[0]))      ) |
        (static_cast<boost::uint32_t>(static_cast<unsigned char>(s[1])) <<  8) |
        (static_cast<boost::uint32_t>(static_cast<unsigned char>(s[2])) << 16) |
        (static_cast<boost::uint32_t>(static_cast<unsigned char>(s[3])) << 24)
    );
}

template<class Source>
inline boost::uint16_t read_little16(Source& src)
{
    char buf[2];
    boost::iostreams::non_blocking_adapter<Source> nb(src);
    if (boost::iostreams::read(nb, buf, 2) != 2)
        throw std::runtime_error("read_little16 error");
    return decode_little16(buf);
}

// まだSourceになってない
template<class Source>
class lzh_file_source
{
public:
    explicit lzh_file_source(const Source& src) : src_(src), size_(0)
    {
        // TODO: 書庫の前にあるデータを読み飛ばす

        if (!next_entry())
            throw std::runtime_error("bad LZH file");
    }

    bool next_entry()
    {
        boost::iostreams::non_blocking_adapter<Source> nb(src_);

        // 圧縮イメージはとりあえずスキップ
        if (size_ != 0)
            boost::iostreams::seek(nb, size_, std::ios_base::cur);

        // 基本ヘッダの読み込み
        char head[24];
        std::streamsize amt = boost::iostreams::read(nb, head, sizeof(head));
        if (amt < static_cast<std::streamsize>(sizeof(head)))
        {
            if ((amt <= 0) || (head[0] != '\0'))
                throw std::runtime_error("LZH end-mark not found");
            return false;
        }

        if (head[0] == '\0')
            return false;

        // とりあえずLevel 2のみ
        if (head[20] != '\x2')
            throw std::runtime_error("unsupported LZH header");

        is_directory_ = (std::memcmp(head+2, "-lhd-", 5) == 0);
        size_ = decode_little32(head+7);

        // 拡張ヘッダの読み込み
        std::string leaf;
        boost::filesystem::path branch;
        while (boost::uint16_t size = read_little16(src_))
        {
            if (size < 3)
                throw std::runtime_error("bad LZH extended header");
            size -= 2;

            boost::scoped_array<char> buf(new char[size]);
            const std::streamsize ssize = static_cast<std::streamsize>(size);
            if (boost::iostreams::read(nb, buf.get(), ssize) != ssize)
                throw std::runtime_error("bad LZH extended header");

            if (buf[0] == '\1')
                leaf.assign(buf.get()+1, size-1);
            else if (buf[0] == '\2')
                branch = parse_directory(buf.get()+1, size-1);
        }

        path_ = branch / leaf;

        return true;
    }

    boost::filesystem::path path() const
    {
        return path_;
    }

    bool is_directory() const
    {
        return is_directory_;
    }

private:
    Source src_;
    boost::filesystem::path path_;
    bool is_directory_;
    boost::uint32_t size_;

    // ディレクトリ拡張ヘッダのパース
    static boost::filesystem::path
    parse_directory(const char* s, boost::uint32_t n)
    {
        if (s[n-1] != '\xFF')
            throw std::runtime_error("bad LZH directory extended header");

        const char* cur = s;
        const char* end = s + n;
        boost::filesystem::path ph;

        while (cur != end)
        {
            const char* delim =
                static_cast<const char*>(std::memchr(cur, '\xFF', end-cur));

            ph /= std::string(cur, delim-cur);
            cur = ++delim;
        }

        return ph;
    }
};

#include <boost/iostreams/device/file.hpp>
#include <iostream>

namespace fs = boost::filesystem;
namespace io = boost::iostreams;

int main(int argc, char* argv[])
{
    try
    {
        if (argc != 2)
            return 1;

        fs::path::default_name_check(fs::no_check);

        typedef lzh_file_source<io::file_source> lzh_type;
        lzh_type lzh(io::file_source(argv[1], std::ios_base::binary));

        do
        {
            // 09/16: ディレクトリとファイルが逆になっていたのを修正
            if (lzh.is_directory())
                std::cout << lzh.path().native_directory_string() << '\n';
            else
                std::cout << lzh.path().native_file_string() << '\n';
        } while (lzh.next_entry());

        return 0;
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }
    return 1;
}

普段使ってるLhaplusが圧縮にLevel 0ヘッダを使っているのを知って、ちょっとショックでした。
Level 0/1も対応しないと駄目ですねぇ。