iooiau.net

ファイル名が正しいかをそこそこちゃんと判定する

ユーザーがファイル名を入力できるようにした場合、そのファイル名が正しいかどうかをチェックして、もし正しくなければ知らせる機能をつけたい場合があります。
ファイル名が正しいかどうかをチェックするには、\ や / などのファイル名に使用できない文字がないかどうかを調べればいい、と思うかも知れませんが、実際はそう単純ではありません。

MSDN によるとファイル名には以下のルールがあります。

このことを踏まえて、ファイル名が正しいかどうかをチェックする処理を書いてみましょう。


// チェックの結果を表す定数です
typedef enum {
    FILENAME_OK,               // OK
    FILENAME_EMPTY,            // ファイル名が空
    FILENAME_INVALID_CHAR,     // 無効な文字
    FILENAME_INVALID_TRAILER,  // 無効な終端
    FILENAME_DEVICE_NAME,      // デバイスファイル
    FILENAME_BROKEN            // 2バイト文字が途中で切れている
} FileNameResult;

// ファイル名をチェックする関数です
FileNameResult CheckFileName(
    LPCTSTR pszFileName,    // チェックするファイル名へのポインタです
    bool fWildcard = false  // true の場合ワイルドカードを有効とみなします 
)
{
    // ファイル名が空でないか
    if (pszFileName == NULL || pszFileName[0] == '\0')
        return FILENAME_EMPTY;

    // デバイスファイルでないか
    int Length = lstrlen(pszFileName);
    if (Length == 3) {
        static const LPCTSTR pszDevices[] = {
            TEXT("CON"), TEXT("PRN"), TEXT("AUX"), TEXT("NUL")
        };
        for (int i = 0; i < sizeof(pszDevices) / sizeof(LPCTSTR); i++) {
            if (lstrcmpi(pszDevices[i], pszFileName) == 0)
                return FILENAME_DEVICE_NAME;
        }
    } else if (Length == 4) {
        TCHAR szName[5];

        for (int i = 1; i <= 9; i++) {
            wsprintf(szName, TEXT("COM%d"), i);
            if (lstrcmpi(szName, pszFileName) == 0)
                return FILENAME_DEVICE_NAME;
        }
        for (int i = 1 ; i <= 9; i++) {
            wsprintf(szName, TEXT("LPT%d"), i);
            if (lstrcmpi(szName, pszFileName)==0)
                return FILENAME_DEVICE_NAME;
        }
    }

#ifdef UNICODE
typedef WORD UTCHAR;
#else
typedef BYTE UTCHAR;
#endif

    LPCTSTR p = pszFileName;
    while (*p != _T('\0')) {
        TCHAR c = *p;

        // 使用できない文字ではないか
        if ((UTCHAR)c <= 31 || c == _T('<') || c == _T('>') || c == _T(':')
                || c == _T('"') || c == _T('/') || c == _T('\\') || c == _T('|')
                || (!fWildcard && (c == _T('*') || c == _T('?'))))
            return FILENAME_INVALID_CHAR;

        // 半角スペースか . で終了していないか
        if ((c == _T(' ') || c == _T('.')) && *(p + 1) == _T('\0'))
            return FILENAME_INVALID_TRAILER;

#ifndef UNICODE
        // 2バイト文字のチェック
        if (IsDBCSLeadByteEx(CP_ACP, c)) {
            p++;
            // 2バイト文字が途中で切れていないか
            if (*p == _T('\0'))
                return FILENAME_BROKEN;
        }
#endif
        p++;
    }

    return FILENAME_OK;
}

MSDN によれば、デバイスファイルに拡張子が付いたファイル名、例えば CON.txt なども「not recommended」とされています。
これは微妙な書き方なのでここではチェックしていませんが、気になる場合はこれもチェックするように書き換えてください。

ファイル名が正しいかどうかを判定するだけでも、意外と面倒なコーディングが必要ですね。