エンコード処理のインターフェース

clx ver 0.17.0 で,これまで個々に実装していた,文字列を何らかの形にエンコードする関数群のインターフェースを統一しました.具体的には,hexdump(), base64::encode(), base64::decode(), html::escape(), html::unescape(), uri::encode(), uri::decode() の 7 つ.そのときの,インターフェースの決め方のメモ.

これらのインターフェースを統一する際に,当初は std::transform() を利用する形を考えていました(std::transform() の関数オブジェクトを実装する形).ただ,いろいろやっているうちに,大まかに言って以下の 2 点の問題点が出てきたため std::transform() の関数オブジェクト用クラスと言う形はちょっと辛いと言う結論になりました.

  1. 1入力1出力とは限らない(複数の文字列が渡されて初めてエンコード可能になる場合がある).
  2. 終了時に特別な処理を必要とする場合がある.

特に問題となったのは後者でした(具体的には base64::encode() が問題になった).base64::encode() では,3文字のデータをエンコードして4文字の表示可能な文字を出力します.この際,入力データのバイト数が 3の倍数ではない場合,足りない部分を“=”と言う文字で補って出力するようになっているのですが,入力データが足りないかどうかはユーザからの入力が終了するまで分かりません.ユーザの入力が終了したかどうかは,例えばデストラクタなどをうまく使って判別する事はできそうですが,std::transform() の関数オブジェクト用クラスとして実装した場合,最後の(“=”の文字を含む)文字列を出力する機会が与えられません.

そう言う訳で,まず以下のような関数を定義し,先に挙げた 7つの関数はこの関数に適用できるようなクラスを作成すると言う形で統一してみました.

template <class InIter, class OutIter, class ConvPolicy>
OutIter convert(InIter first, InIter last, OutIter result, ConvPolicy f) {
    result = f.reset(result);
    for (; first != last; ++first) result = f(*first, result);
    result = f.finish(result);
    return result;
}

// std::string でよく使いそうな形
template <class Ch, class Tr, class ConvPolicy>
std::basic_string<Ch, Tr> convert(const std::basic_string<Ch, Tr>& src, ConvPolicy f) {
    std::basic_string<Ch, Tr> dest;
    std::insert_iterator<std::basic_string<Ch, Tr> > out(dest, dest.end());
    clx::convert(src.begin(), src.end(), out, f);
    return dest;
}

template <class CharT, class ConvPolicy>
std::basic_string<CharT> convert(const CharT* src, ConvPolicy f) {
    std::basic_string<CharT> tmp(src);
    std::basic_string<CharT> dest;
    std::insert_iterator<std::basic_string<CharT> > out(dest, dest.end());
    clx::convert(tmp.begin(), tmp.end(), out, f);
    return dest;
}

インターフェース的には std::transform() のような標準的な C++アルゴリズム用の関数とほぼ同じ形なのですが,以下のように第4引数に指定するクラスが要求するメンバ関数が異なります.

class convert_policy {
public:
    typedef char char_type;
    
    template <class OutIter>
    OutIter operator()(char_type c, OutIter out);
    
    template <class OutIter>
    OutIter reset(OutIter out);
    
    template <class OutIter>
    OutIter finish(OutIter out);
};

今回,インターフェース回りを統一したのは http://petitbanca.blogspot.com/2009/11/oauthtwitter.htmltwitter の OAuth 機能で利用するためには uri::encode() のソースコードを直接書き換えないといけないと言う話が出ていたからと言う事もあります.できるだけ拡張性は残しておきたかったのですが,そのために関数の引数が多くなりすぎるのは個人的にはあまり好きではありません.この辺りを苦慮した結果,カスタマイズしたい場合にはラッパ関数である uri::encode() を使用せずに,カスタマイズ用のクラスを作って(and/or インスタンスを生成して),大元となる convert() 関数とともに使ってもらうと言う形にしました.

例えば,uri::encode() が使用している URI エンコードのためのクラス uri_encoder は以下のような定義になっています(uri::encode() はデフォルトのコンストラクタでインスタンスを生成している).

template <
    class CharT,
    class Traits = std::char_traits<CharT>
>
class basic_uri_encoder {
public:
    typedef std::size_t size_type;
    typedef CharT char_type;
    typedef Traits traits;
    typedef std::basic_string<CharT, Traits> string_type;
    
    explicit basic_uri_encoder(bool lower = true) :
        f_(clx::is_any_of((const char_type*)LITERAL("!#$&'()*+,-./:;=?@_~"))),
        plus_(false), lower_(lower) {}
    
    explicit basic_uri_encoder(const string_type& symbols, bool space_to_plus, bool lower = true) :
        f_(clx::is_any_of(symbols)), plus_(space_to_plus), lower_(lower) {}
    
    explicit basic_uri_encoder(const char_type* symbols, bool space_to_plus, bool lower = true) :
        f_(clx::is_any_of(symbols)), plus_(space_to_plus), lower_(lower) {}
    
    ~basic_uri_encoder() throw() {}
    
    template <class OutIter>
    OutIter operator()(char_type c, OutIter out) {
        static const clx::classified_functor alnum = clx::is_alnum();
        static const size_type width = (sizeof(char_type) > 1) ? 4 : 2;
        
        if (alnum(c) || f_(c)) *out++ = c;
        else if (c == 0x20 && plus_) *out++ = LITERAL('+');
        else {
            std::basic_stringstream<char_type, traits> ss;
            ss << LITERAL('%');
            if (sizeof(char_type) > 1) ss << LITERAL('u');
            if (!lower_) ss << std::uppercase;
            ss << std::setw(width) << std::setfill((char_type)LITERAL('0')) << std::hex;
            ss << (static_cast<size_type>(c) & mpl::lower_mask<sizeof(char_type) * 8>::value);
            
            std::istreambuf_iterator<char_type> first(ss);
            std::istreambuf_iterator<char_type> last;
            out = std::copy(first, last, out);
        }
        
        return out;
    }
    
    template <class OutIter>
    OutIter reset(OutIter out) {
        return out;
    }
    
    template <class OutIter>
    OutIter finish(OutIter out) {
        return out;
    }
    
private:
    charset_functor<char_type> f_;
    bool plus_;
    bool lower_;
};

このクラスを用いて,以下を参考に OAuth の仕様を満たす URIエンコードするコードを記述すると以下のような形になるでしょうか(UTF-8 のチェック and/or 変換部分は割愛).

全てのパラメータ名と値は RFC3986 のパーセントエンコーディング(%xx)メカニズムでエスケープされます。非制限文字一覧 (RFC3986 section 2.3) にないものは、必ずエンコードしなければなりません(MUST)。非制限文字一覧にある文字はエンコードしてはいけません(MUST NOT)。エンコードした Hex 文字列は全て大文字にしなければなりません(MUST)。テキストの名前や値は UTF-8 オクテットとしてエンコードしてからパーセントエンコードしなければなりません(RFC3629)。(非制限文字は次の通り。)

unreserved = ALPHA, DIGIT, '-', '.', '_', '~'
xtra/OAuth1.0 - i-revo labs
#include <string>
#include <clx/uri.h>

int main(int argc, char* argv[]) {
    std::string src = ...; // 文字エンコードは UTF-8 でなければならない.
    
    /*
     * clx::uri_encoder は,英数字および第1引数で指定された文字
     * 以外を16進文字列 (%XX) に変換する.
     * 第2引数は,スペース(0x20) をプラス記号にエンコードするかどうか.
     * true の場合は + に,false の場合は %20 にエンコードする.
     * 第3引数は,エンコード後の 16進文字列が小文字かどうか.
     */
    clx::uri_encoder f("-._~", false, false);
    std::string dest = clx::covnert(src, f);
    
    ...
    return 0;
}

まだリファレンスを更新してないのですが,上記の convert() 関数に適用するクラスは現状では dec_encoder, hex_encoder, base64_encoder, base64_decoder, html_encoder, html_decoder, uri_encoder, uri_decoder を用意しています(10進/16進文字列からのデコード用クラスは未実装).