Windowsでファイル名の末尾ドットが無視される挙動の由来


Windows10において、`mydir`という名前のディレクトリ、`myfile.txt` というファイルが存在するとき、Javaの

[code]

    Files.exists(Paths.get("mydir.")); // 1個の末尾ドットが無視される？(2個以上はfalse)
    Files.exists(Paths.get("myfile.txt...")); // 1個以上の末尾ドットが無視される？
    
[/code]

が真となるのですが、なぜこのような挙動になるのでしょうか。  
また、正しく(つまり`mydir.`,`myfile.txt.`は存在しない、と)判断できるようにするためにはどうすべきでしょうか。

ちなみに、C#で試したところ

[code]

    Directory.Exists("mydir...."); // 1個以上
    File.Exists("myfile.txt.........."); // 1個以上
    
[/code]

が真となりました。(ディレクトリ存在の判断が異なるのも少し気になります…)

Java/C#ともCentOS7上では偽となっていました。

* * *

[以下補足情報]

Webサーバに対する[ディレクトリトラバーサル](https://ja.wikipedia.org/wiki/%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%83%88%E3%83%A9%E3%83%90%E3%83%BC%E3%82%B5%E3%83%AB)攻撃というのは、一般的には親方向にトラバースさせると思いますが、この挙動を利用すると子方向へのトラバース可能性も出てくるかと思います。

例えば、Java Servlet仕様([JSR 369: Java Servlet 4.0
Specification](https://www.jcp.org/en/jsr/detail?id=369) 10.5 Directory
Structure)によると `/WEB-INF` ディレクトリ以下への直接のリクエストは拒否されるべきですが、後ろにドットを付け、
<http://example.com/WEB-INF./web.xml> (あるいは小文字で <http://example.com/web-
inf./web.xml>
)とすることで該当ディレクトリ以下のファイルが漏洩する脆弱性が過去実際にあったようです([Tomcat](https://coderanch.com/t/547261/application-
servers/WEB-INF-web-xml-exposed),
[WildFly](https://jvndb.jvn.jp/ja/contents/2016/JVNDB-2016-001933.html))。




## Windowsでファイル名の末尾ドットが無視される挙動の由来

Windowsが当初使用していたFATファイルシステムの影響です。

[FATファイルシステムのディレクトリエントリ](https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system#Directory_entry)の構造としては、ファイル名８バイト、拡張子３バイトの固定長フィールドとなっており、それぞれの長さに満たない場合は空白で埋められる仕様です。このため、ファイル名と拡張子の間に`.`は格納されていません。  
例えば空白を`_`で表すと、ファイル名`README.TXT`であれば`README__TXT`と格納され、`README`であれば`README_____`となります。

このような事情があるため、Windows
APIでは暗黙の`.`を許容します。つまり`README.`、`README.*`、`*.*`などで検索を行うとファイル名`README`がヒットします。現在はNTFS等、他のファイルシステムが使われていますが、ファイルシステムに依らずWindows
APIは上記振る舞いを継承しています。

## ではどうすべきか

### 実は使うべきでない、使えない

Windowsのファイル名について説明するドキュメント[Naming Files, Paths, and
Namespaces](https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-
file)の[Naming Conventions](https://docs.microsoft.com/en-
us/windows/desktop/FileIO/naming-a-file#naming-conventions)では

>   * Do not end a file or directory name with a space or a period. Although
> the underlying file system may support such names, the Windows shell and
> user interface does not.
>

と、たとえファイルシステムとして`.`で終わるファイル名が認められているとしても、利用者としてはそのようなファイル名は使用するな、シェル（つまりファイルエクスプローラー）やアプリケーションがサポートしていない可能性がある、としています。

末尾ドットや末尾空白を無視するこの処理はファイルが見つからなかった際のフォールバック処理ではなく、ファイル操作前のパスの正規化の一環です（`./`や`../`と同じ扱いです）。  
例えば`README.`が存在する状況で`README.`を開こうとしても`README`が存在しないというエラーが発生します。

yukihaneさんの回答で触れられていた[`GetFullPathNameW`](https://docs.microsoft.com/en-
us/windows/desktop/api/fileapi/nf-fileapi-
getfullpathnamew)やより目的に近い[`PathCanonicalizeW`](https://docs.microsoft.com/en-
us/windows/desktop/api/shlwapi/nf-shlwapi-
pathcanonicalizew)においても同じことで、ファイルの存在とは無関係にパスの正規化として無条件に末尾`.`を削除します。

### それでも使いたい場合

質問文はファイルの存在確認だけを対象としています。存在確認だけに有効な方法としては、ディレクトリ内のファイル一覧を取得し、自分でファイル名一致の判定を行うことです。

ただし、前述の通り、ファイルが存在が確認できたとしてもアクセスできません。

## それでもそれでも使いたい場合

Windowsには正規化を行わない`\\\?\`プレフィックスというパス形式が存在します。例えばメモ帳であれば`\\\?\C:\Windows\notepad.exe`と表現されます。この形式の特徴は次の通りです。

  * 相対パスは認められません。常に絶対パスを使用する必要があります。
  * Unicodeバージョンではパス文字列の最大長が260文字から32,767文字へ拡張されます。
  * ディレクトリ区切りに`/`を使用できません。ディレクトリ区切りは`\`を使用する必要があります。
  * `..\`や`.\`は認められません。`..`や`.`を正当なファイル名として扱います。
  * 末尾`.`や末尾``を無視しません。正当なファイル名として扱います。

`\\\?\`プレフィックスを付けても次の動作は引き続き有効です。

  * 大文字・小文字を無視します。
  * short file nameを受け入れます。

この`\\\?\`プレフィックス形式を使用すれば、ファイル名の末尾ドットについて指定通り正しく扱うことができます。

ただし、これはあくまでWindows
APIでの話です。例えばC#では`\\\?\`形式のパスを認めていないため常に失敗します。C#で`\\\?\`形式を使用するためには直接Windows
APIを呼び出す必要があります。質問文で触れられているもう一つの言語Javaについては私は可否を知りません。




## Windowsでファイル名の末尾ドットが無視される挙動の由来

Windowsが当初使用していたFATファイルシステムの影響です。

[FATファイルシステムのディレクトリエントリ](https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system#Directory_entry)の構造としては、ファイル名８バイト、拡張子３バイトの固定長フィールドとなっており、それぞれの長さに満たない場合は空白で埋められる仕様です。このため、ファイル名と拡張子の間に`.`は格納されていません。  
例えば空白を`_`で表すと、ファイル名`README.TXT`であれば`README__TXT`と格納され、`README`であれば`README_____`となります。

このような事情があるため、Windows
APIでは暗黙の`.`を許容します。つまり`README.`、`README.*`、`*.*`などで検索を行うとファイル名`README`がヒットします。現在はNTFS等、他のファイルシステムが使われていますが、ファイルシステムに依らずWindows
APIは上記振る舞いを継承しています。

## ではどうすべきか

### 実は使うべきでない、使えない

Windowsのファイル名について説明するドキュメント[Naming Files, Paths, and
Namespaces](https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-
file)の[Naming Conventions](https://docs.microsoft.com/en-
us/windows/desktop/FileIO/naming-a-file#naming-conventions)では

>   * Do not end a file or directory name with a space or a period. Although
> the underlying file system may support such names, the Windows shell and
> user interface does not.
>

と、たとえファイルシステムとして`.`で終わるファイル名が認められているとしても、利用者としてはそのようなファイル名は使用するな、シェル（つまりファイルエクスプローラー）やアプリケーションがサポートしていない可能性がある、としています。

末尾ドットや末尾空白を無視するこの処理はファイルが見つからなかった際のフォールバック処理ではなく、ファイル操作前のパスの正規化の一環です（`./`や`../`と同じ扱いです）。  
例えば`README.`が存在する状況で`README.`を開こうとしても`README`が存在しないというエラーが発生します。

yukihaneさんの回答で触れられていた[`GetFullPathNameW`](https://docs.microsoft.com/en-
us/windows/desktop/api/fileapi/nf-fileapi-
getfullpathnamew)やより目的に近い[`PathCanonicalizeW`](https://docs.microsoft.com/en-
us/windows/desktop/api/shlwapi/nf-shlwapi-
pathcanonicalizew)においても同じことで、ファイルの存在とは無関係にパスの正規化として無条件に末尾`.`を削除します。

### それでも使いたい場合

質問文はファイルの存在確認だけを対象としています。存在確認だけに有効な方法としては、ディレクトリ内のファイル一覧を取得し、自分でファイル名一致の判定を行うことです。

ただし、前述の通り、ファイルが存在が確認できたとしてもアクセスできません。

## それでもそれでも使いたい場合

Windowsには正規化を行わない`\\\?\`プレフィックスというパス形式が存在します。例えばメモ帳であれば`\\\?\C:\Windows\notepad.exe`と表現されます。この形式の特徴は次の通りです。

  * 相対パスは認められません。常に絶対パスを使用する必要があります。
  * Unicodeバージョンではパス文字列の最大長が260文字から32,767文字へ拡張されます。
  * ディレクトリ区切りに`/`を使用できません。ディレクトリ区切りは`\`を使用する必要があります。
  * `..\`や`.\`は認められません。`..`や`.`を正当なファイル名として扱います。
  * 末尾`.`や末尾``を無視しません。正当なファイル名として扱います。

`\\\?\`プレフィックスを付けても次の動作は引き続き有効です。

  * 大文字・小文字を無視します。
  * short file nameを受け入れます。

この`\\\?\`プレフィックス形式を使用すれば、ファイル名の末尾ドットについて指定通り正しく扱うことができます。

ただし、これはあくまでWindows
APIでの話です。例えばC#では`\\\?\`形式のパスを認めていないため常に失敗します。C#で`\\\?\`形式を使用するためには直接Windows
APIを呼び出す必要があります。質問文で触れられているもう一つの言語Javaについては私は可否を知りません。

