😺

Win32 API PathMatchSepc(Ex) のバグ具合と交代処理

2024/01/06に公開

c++17 以降 filesystem::directory_iterator / std::filesystem::recursive_directory_iterator を用いれば、ディレクトリの全ファイル列挙が簡単にできます。

が、ワイルドカード指定でのファイル名マッチはc++標準には無さそうで、別途用意する必要があります。ファイル名はファイルシステムによって文字コード範囲の違いや大小文字の同一視等があり、できれば標準でほしいのですが。

手っ取り早く、Windows API を使えれば楽なのですが、その API の PathMatchSepc / PathMatchSepcEx はかなり前から酷いバグ持ちです。

https://all.undo.jp/asr/doc/Ver7/11.html

によると Windows8 の頃かららしいですが、マッチするはずの文字がマッチしないのです。
ASCII範囲は大丈夫ですが、よくカタカナがマッチしていない印象。
※実は自前ツールで数年気づかずにコレを使ってたことがある...

で、具体的にどれくらいマッチしないのか気になってたので試してみました。

https://github.com/tenk-a/samples/blob/CheckPathMatchSpec/CheckPathMatchSpec/CheckPathMatchSpec.cpp

ascii(0x7F以下)以外の2バイトUNICODE範囲で、文字ごとのファイル名を生成して、 でマッチするかを総当り。
結果、

Char:63358, File:63358, FindFirst:63358, PathMatchSpecEx:63358-48478=14880

大文字小文字同一視があるため、実際のファイル数は 62411 ファイルでした。
差の 947 文字は同一視された文字数ですが 2->1でなく複数が1字になることもあり。

上記マッチしなかった文字のうち、同一視文字がどの程度含まれてるかは不明ですが、全て含まれてたとしても 14000弱、思いの外多かったです。
ファイル名検索として、アテにできません。
(0x10000 以上の UNICODE 文字は含めていないのでさらに非マッチ増えるかもで)

交代処理

よくあるワイルドカード処理を Windows に合わせて調整。

bool is_sep(uint32_t c) { return c == '/' || c == '\\'; }

uint32_t to_lower(uint32_t c) { return (c < 0xffff) ? uint32_t(CharLowerW((LPWSTR)c)) : c; }

uint32_t get_ch(wchar_t const*& s) {
    uint32_t c = *s;
    if (c) {
        ++s;
        if (c >= 0xD800 && c <= 0xDBFF && *s)
            c = (c << 16) | uint32_t(*s++);
    }
    return c;
}

bool fn_match(wchar_t const* ptn, wchar_t const* tgt) {
    wchar_t const* tgt2 = tgt;
    uint32_t tc = get_ch(tgt2);
    switch (*ptn) {
    case L'\0': return tc == L'\0';
    case L'\\':
    case L'/': return is_sep(tc) && fn_match(ptn + 1, tgt2);
    case L'?': return tc && !is_sep(tc) && fn_match(ptn + 1, tgt2);
    case L'*': return fn_match(ptn + 1, tgt) || (tc && !is_sep(tc) && fn_match(ptn, tgt2));
    default:
        uint32_t pc = get_ch(ptn);
        pc = to_lower(pc);
        tc = to_lower(tc);
        return (pc == tc) && fn_match(ptn, tgt2);
    }
}

小文字化に Windows API の CharLowerW を使用。
これ、自前で組もうとするとUNICODEでの大小変換テーブル必要で、というかWindowsのファイルシステムでの大小変換と同じものを用意する必要があり、かなり面倒です。

一応、先のプログラムのファイル生成除いて PathMatchSpecExW を fn_match に置き換えたモノで実行したところ、

https://github.com/tenk-a/samples/blob/CheckPathMatchSpec/CheckPathMatchSpec/check_fn_match.cpp

Char:63358, FindFirst:63358, fn_match:63358-63358=0

で、UNICODE文字のマッチとしては機能してそうです。(ワイルドカード処理としての確認にはあまりなってませんが)

その他

チェックプログラムでも使ってますが、ディレクトリ再帰しないのなら、ファイル検索API FindFirstFileEx FindNextFile でワイルドカードを使うのが実行時効率はよいでしょうね。

ディレクトリ再帰をするなら、各ディレクトリ階層エントリ全部取得してディレクトリ以外を fn_match で選別、サブディレクトリに潜って再帰。

c++17 以降なら std::filesystem::recursive_directory_iterator で回して、filename 部分を fn_natch で選別。

    namespace  fs = std::filesystem;
    for (auto& it : fs::recursive_directory_iterator(".")) {
        if (fn_match(L"*.cpp", it.path().filename().c_str())) {
            printf("%s\n", (char const*)it.path().u8string().c_str());
        }
    }

楽に書けますが、L"文字列" を使わないといけないのが玉に瑕。

Windows 以外を考慮したり、UTF-8 char 環境なら、

bool fn_match(char const* ptn, fs::path::value_type const* tgt);

template<typename PC,typename TC>
bool fn_match(PC const* ptn, TC const* tgt);

を実装したほうがよいかもです。

Discussion