URI 用 path_type の試行

HTTP サーバで URI のパスを扱うために、 PerlMojo::Path を真似した path_class を何通りかの書き方で試しているところです。 今回は URI エスケープをデコードせずエンコードもしない単純版になっています。

#include <string>
#include <vector>
#include <list>
#include <utility>

class path_type {
    std::vector<std::string> mseg;
public:
    path_type () : mseg () {}
    path_type (std::string const& name) : mseg () { merge (name); }
    path_type (path_type const& path) : mseg (path.mseg) {}
    path_type (path_type&& path) : mseg (std::move (path.mseg)) {}

    path_type& operator= (path_type const& path)
    {
        if (this != &path)
            mseg.assign (path.mseg.begin (), path.mseg.end ());
        return *this;
    }

    path_type& operator= (path_type&& path)
    {
        if (this != &path)
            std::swap (mseg, path.mseg);
        return *this;
    }

    std::string to_string () const;
    std::string basename () const;
    std::string suffix () const;
    bool contains (std::string const& name) const;
    bool contains (path_type const& path) const;
    bool trailing_slash () const;
    path_type parent () const;
    path_type child (std::string const& name) const;
    path_type child (path_type const& part) const;
    path_type absolute (std::string const& basedirname) const;
    path_type absolute (path_type const& base) const;
    path_type relative (std::string const& basedirname) const;
    path_type relative (path_type const& base) const;
    path_type& merge (std::string const& name);
    path_type& merge (path_type const& path);
    path_type& trailing_slash (bool f);
    path_type& canonicalize ();
private:
    std::size_t count_dots (std::string const& s) const;
};

パスの内部表現では、 Path::Tiny のように文字列にする方法、 Path::Class のようにディレクトリ部分を文字列配列にしてベース名を独立して保持する方法、 Mojo::Path のように文字列で保持しつつ必要に応じてフラットな文字列配列に分割する方法等があります。 それらを一通り試してみてから、 ディレクトリとベース名を一緒にフラットな文字列配列に並べる方法を採用することにしました。 mseg 配列にディレクトリとベース名が一緒に並べます。 この配列の内容は、 Perl の File::Spec の splitpath と splitdir を両方おこなったものに類似したものにしてあります。 ルート・ディレクトリから始まる絶対パスは先頭が空文字列で始まり、相対パスは空でない文字列で始めます。 パスの末尾がスラッシュで終わるときは、 空文字列で終わります。 空でないベース名があるときは、 ベース名の文字列で終わります。 空の mseg はカレンド・ディレクトリを表すことにします。

to_string は、 Perl 風に説明すると、 mseg が空のときは . を、 そうでないときはスラッシュで join して返します。

//    mseg{}                    =>  "."
//    mseg{"", "a", "b", "c"};  =>  "/a/b/c"
//    mseg{"", "a", "b", ""};   =>  "/a/b/"
//    mseg{"a", "b", "c"};      =>  "a/b/c"
//    mseg{"", ""};             =>  "/"
//    mseg{"."};                =>  "."
//    mseg{".."};               =>  ".."
//    mseg{"a"};                =>  "a"
std::string
path_type::to_string () const
{
    if (mseg.empty ())
        return ".";
    std::string t = mseg.at (0);
    for (std::size_t i = 1; i < mseg.size (); ++i)
        t += "/" + mseg.at (i);
    return t;
}

mseg の末尾はベース名です。 ただし、 ドットだけからなるベース名はすべてディレクトリとみなし、 空のベース名を返します。

//    mseg{}                    =>  ""
//    mseg{"", "a", "b", "c"};  =>  "c"
//    mseg{"", "a", "b", ""};   =>  ""
//    mseg{"", ""};             =>  ""
//    mseg{"."};                =>  ""
//    mseg{".."};               =>  ""
//    mseg{"a"};                =>  "a"
std::string
path_type::basename () const
{
    if (mseg.empty () || count_dots (mseg.back ()) > 0)
        return "";
    return mseg.back ();
}

suffix はベース名のサフィックスを返します。 サフィックスはベース名の末尾につけるドットから始まる .html の部分のことです。 2015 年 9 月 26 日修正: ドットごと返すように変更しました。

//    mseg{".ignore"}         => ""
//    mseg{"index.md"}        => ".md"
//    mseg{"foo.bar.baz"}     => ".baz"
std::string
path_type::suffix () const
{
    if (mseg.empty () || count_dots (mseg.back ()) > 0)
        return "";
    std::string const& basename = mseg.back ();
    std::string::size_type i = basename.rfind ('.');
    if (i == 0 || i == std::string::npos)
        i = basename.size ();
    return basename.substr (i);
}

contains は、 this が path の配下にあるとき真を返します。

// path_type ("/foo/bar").contains ("/")        => true
// path_type ("/foo/bar").contains ("/foo")     => true
// path_type ("/foo/bar").contains ("/foo/bar") => true
// path_type ("/foo/bar").contains ("/f")       => false
// path_type ("/foo/bar").contains ("/bar")     => false
// path_type ("/foo/bar").contains ("/baz")     => false
bool
path_type::contains (std::string const& name) const
{
    path_type path (name);
    return contains (path);
}

bool
path_type::contains (path_type const& path) const
{
    std::size_t n1 = mseg.size ();
    if (n1 > 1 && mseg.back ().empty ())
        --n1;
    std::size_t n2 = path.mseg.size ();
    if (n2 > 1 && path.mseg.back ().empty ())
        --n2;
    if (n1 < n2)
        return false;
    for (std::size_t i = 0; i < n2; ++i)
        if (mseg.at (i) != path.mseg.at (i))
            return false;
    return true;
}

ディレクトリかどうかは末尾にスラッシュがついているかどうかで区別するようにしています。 末尾のスラッシュを mseg の末尾に空文字列をつけ加えたり、除去することで実現しています。 例外として、 この実装の mseg の仕組み上、 カレント・ディレクトリを空 mseg で表しているときはスラッシュをつけることができません。

//    path_type ("/foo/bar").trailing_slash () => false
//    path_type ("/foo/bar/").trailing_slash () => true
bool
path_type::trailing_slash () const
{
    return mseg.size () > 1 && mseg.back ().empty ();
}

//    path_type ("/foo/bar").trailing_slash (true) => path_type ("/foo/bar/")
//    path_type ("/foo/bar/").trailing_slash (false) => path_type ("/foo/bar")
path_type&
path_type::trailing_slash (bool slash_on)
{
    if (mseg.empty ())
        return *this;
    if (slash_on && ! trailing_slash ())
        mseg.push_back ("");
    else if (! slash_on && trailing_slash ())
        mseg.pop_back ();
    return *this;
}

ディレクトリを返す parent は、 this をコピーし、 末尾スラッシュを除去してから、 ../ を付け加えます。

//    path_type ("/foo/bar").parent () => path_type ("/foo/bar/../")
//    path_type ("/foo/bar").parent () => path_type ("/foo/bar/../")
path_type
path_type::parent () const
{
    path_type path (*this);
    path.trailing_slash (false);
    path.mseg.push_back ("..");
    path.mseg.push_back ("");
    return std::move (path);
}

merge は破壊的にパスを伸ばすのに使います。 文字列から this に追加するメンバ関数と、 path_type から追加するものの 2 種類があります。 どちらも、 Mojo::Path の同名のメソッド同様、 スラッシュで始まる絶対パスを merge すると、 絶対パスで置き換えます。 そうでないときはパスを伸ばします。 伸ばすとき、 スラッシュで終わってないパスに merge すると、ベース名を取り去ってから、 パスをつなぎます。 ただし、 パスが親ディレクトリで終わっているときは、 親ディレクトリを取り去らずにパスをつなぎます。 もちろん、 スラッシュで終わっているときは、 そのままパスをつなぎます。

//    path_type ("/foo/bar").merge ("/baz/yada") => path_type ("/baz/yada")
//    path_type ("/foo/bar").merge ("baz/yada")  => path_type ("/foo/baz/yada")
//    path_type ("/foo/bar/").merge ("baz/yada") => path_type ("/foo/bar/baz/yada")
path_type&
path_type::merge (std::string const& name)
{
    if (! name.empty () && name[0] == '/')
        mseg.clear ();
    std::string::size_type q = 0;
    bool pop_first = ! mseg.empty () && mseg.back () != "..";
    while (q < name.size ()) {
        std::string::size_type p1 = q;
        q = name.find ('/', q);
        if (q == std::string::npos)
            q = name.size ();
        std::string::size_type p2 = q;
        ++q;
        if (pop_first) {
            mseg.pop_back ();
            pop_first = false;
        }
        mseg.emplace_back (name.begin () + p1, name.begin () + p2);
    }
    if (! name.empty () && name.back () == '/')
        mseg.push_back ("");
    return *this;
}

path_type から merge するときも、 絶対パスのときは置き換え、 相対バスのときは追加をおこないます。 追加のとき、 末尾が親ディレクトリかスラッシュでないなら追加する前にベース名を取り除くのも同じです。

//    path_type ("/foo/bar").merge (path_type ("/baz/yada")) => path_type ("/baz/yada")
//    path_type ("/foo/bar").merge (path_type ("baz/yada"))  => path_type ("/foo/baz/yada")
//    path_type ("/foo/bar/").merge (path_type ("baz/yada")) => path_type ("/foo/bar/baz/yada")
path_type&
path_type::merge (path_type const& path)
{
    if (this == &path || path.mseg.empty ())
        return *this;
    if (path.mseg[0].empty ()) {
        mseg.assign (path.mseg.begin (), path.mseg.end ());
        return *this;
    }
    if (! mseg.empty () && mseg.back () != "..")
        mseg.pop_back ();
    mseg.insert (mseg.end (), path.mseg.begin (), path.mseg.end ());
    return *this;
}

child は this のコピーを作り、 末尾スラッシュをオンにしてから、 パスを追加・変更した新しいオブジェクトを作ります。

//    path_type ("/foo/bar").child ("/baz/yada") => path_type ("/baz/yada")
//    path_type ("/foo/bar").child ("baz/yada")  => path_type ("/foo/bar/baz/yada")
//    path_type ("/foo/bar/").child ("baz/yada") => path_type ("/foo/bar/baz/yada")
path_type
path_type::child (std::string const& name) const
{
    path_type path (*this);
    path.trailing_slash (true);
    path.merge (name);
    return std::move (path);
}

path_type
path_type::child (path_type const& part) const
{
    path_type path (*this);
    path.trailing_slash (true);
    path.merge (part);
    return std::move (path);
}

absolute は child の逆で、 base のコピーを作り、 末尾スラッシュをオンにしてから、 this を追加・変更したオブジェクトを作ります。

//    path_type ("/foo/bar").absolute ("/baz/yada/") => path_type ("/foo/bar")
//    path_type ("foo/bar").absolute ("/baz/yada/")  => path_type ("/baz/yada/foo/bar")
//    path_type ("foo/bar").absolute ("/baz/yada") => path_type ("/baz/yada/foo/bar")
path_type
path_type::absolute (std::string const& basedirname) const
{
    path_type path (basedirname);
    path.trailing_slash (true);
    path.merge (*this);
    return std::move (path);
}

path_type
path_type::absolute (path_type const& base) const
{
    path_type path (base);
    path.trailing_slash (true);
    path.merge (*this);
    return std::move (path);
}

相対パスを求める relative は Perl の File::Spec の abs2rel のコードを基にしています。 goal と base はディレクトリ部分のパス・セグメントのリストで、 base から goal への相対パスを求めてから、 ベース名を書き込みます。 原則として、 relative を使うときは、 あらかじめ this も base も両方とも canonicalize しておくべきですし、 両方とも絶対パスであるべきです。 そうでないときにも relative はパスを返しますが、 正しい相対パスになっている保証はありません。

// path_type ("/baz/yada/foo/bar").relative ("/baz/yada/") => path_type ("foo/bar")
path_type
path_type::relative (std::string const& basedirname) const
{
    path_type base (basedirname);
    return relative (base);
}

path_type
path_type::relative (path_type const& pathbase) const
{
    std::list<std::string> goal (mseg.begin (), mseg.end ());
    std::list<std::string> base (pathbase.mseg.begin (), pathbase.mseg.end ());
    if (mseg.size () > 1)
        goal.pop_back ();
    if (pathbase.mseg.size () > 1)
        base.pop_back ();
    std::list<std::string> roots;
    while (! goal.empty () && ! base.empty () && goal.front () == base.front ()) {
        roots.push_back ("");
        std::swap (roots.back (), goal.front ());
        goal.pop_front ();
        base.pop_front ();
    }
    path_type path;
    if (! goal.empty () || ! base.empty ()) {
        std::list<std::string> parents;
        while (! base.empty ()) {
            std::string segment;
            std::swap (segment, base.front ());
            base.pop_front ();
            if (segment != "..") {
                parents.push_front ("..");
                roots.push_back ("");
                std::swap (roots.back (), segment);
            }
            else if (! roots.empty ()) {
                if (! parents.empty () && parents.front () == "..") {
                    parents.pop_front ();
                    roots.pop_back ();
                }
                else {
                    parents.push_front ("");
                    std::swap (parents.front (), roots.back ());
                    roots.pop_back ();
                }
            }
        }
        path.mseg.assign (parents.begin (), parents.end ());
        path.mseg.insert (path.mseg.end (), goal.begin (), goal.end ());
    }
    if (mseg.size () > 1)
        path.mseg.push_back (mseg.back ());
    return path;
}

path_type では、 パスの正規化を自動でおこないません。 明示的に canonicalize メンバ関数を使う必要があります。 Mojo::Path では ... を . と同じ扱いをしてパスから除去するのに対して、 ここではドットだけからなるパス・セグメントのうちドットが 2 個でないものを除去します。 なお、 Mojo::Path は絶対パスのルート直下の .. を残しますが、 このコードではそれらも取り除きます。 canonicalize は this を破壊操作します。

// path_type ("/foo/.//.../bar/../baz").canonicalize () => path_type ("/foo/baz")
// path_type ("/foo/../bar/../../baz").canonicalize () => path_type ("/baz")
// path_type ("foo/../bar/../../baz").canonicalize () => path_type ("../baz")
path_type&
path_type::canonicalize ()
{
    bool tslash = trailing_slash ();
    std::vector<std::string> a;
    std::swap (a, mseg);
    for (std::vector<std::string>::iterator p = a.begin (); p != a.end (); ++p) {
        std::size_t dots = count_dots (*p);
        if (dots > 0 && dots != 2)
            ;
        else if (p != a.begin () && p->empty ())
            ;
        else if (dots != 2 || mseg.empty () || mseg.back () == "..") {
            mseg.push_back ("");
            std::swap (mseg.back (), *p);
        }
        else if (! mseg.back ().empty ())
            mseg.pop_back ();
    }
    if (! mseg.empty () && tslash)
        mseg.push_back ("");
    return *this;
}

count_dots メンバ関数は、 ドットだけが並んでいるパス・セグメントのときにドットの個数を数えて返します。ドット以外が含まれているときはゼロを返します。

std::size_t
path_type::count_dots (std::string const& s) const
{
    std::size_t n = 0;
    for (int const c : s) {
        if (c != '.')
            return 0;
        ++n;
    }
    return n;
}