PHPのdirname関数でWindows環境のShift_JISパス文字列のディレクトリ名の末尾に特定のコードが入るとディレクトリが欠落する現象

Windows環境で、カレントディレクトリのディレクトリ名末尾が`ン`とかの場合、`__DIR__`定数や`dirname(__FILE__)`の返却値から、現在のディレクトリが消滅し、親ディレクトリまでになってしまうという現象があります。
PHP 7.1ではWindowsのパス処理をUTF-8対応とすることで改善されているようです。

PHPのソースコードを眺めてみたところ、`dirname`関数と思われる処理(よく知らないので推測です)で、バックスラッシュの前の1バイトを`IsDBCSLeadByte`関数に渡し、`FALSE`が返却されたらそのバックスラッシュは1バイト文字のバックスラッシュとして処理するような判定になっていました。
[PHPソースコード - zend_dirname関数](https://github.com/php/php-src/blob/bc49f8b4383a930cc280a43518e9181ef8fde49c/Zend/zend_compile.c#L1833)
[PHPソースコード - IS_SLASH_Pマクロ(Win32)](https://github.com/php/php-src/blob/563659822de1e22d835df94e13cc66a34e7261b2/Zend/zend_virtual_cwd.h#L64)
[IsDBCSLeadByte関数](https://msdn.microsoft.com/ja-jp/library/cc448045.aspx)

しかし、`IsDBCSLeadByte`が`TRUE`を返す場合で2バイト文字の2バイト目の可能性があり、この場合、そのバックスラッシュを2バイト文字の2バイト目(ディレクトリの区切りではない)と見なしてスキップしてしまうようです。
この条件に該当するのは、2バイト文字の2バイト目が、`0x81`-`0x9F`など(2バイト文字の1バイト目と2バイト目どちらでもあり得る範囲)の場合と思われます。
`ン`は`0x8393`なので、この条件に該当します。

`basename`関数では、`mbrlen`関数を使用しているようで、こちらでは**1バイト文字**かつ**バックスラッシュ**という条件のようです。こちらでは`dirname`関数のような現象は起きません。`basename(__FILE__)`とすれば正しくファイル名が返却されます。もし`dirname`と同じような処理であれば、ファイル名にディレクトリ名が混入することになります。
[PHPソースコード - php_basename関数](https://github.com/php/php-src/blob/f65486f13d04f5cc0d484117fefeea6e128f3771/ext/standard/string.c#L1555)
[PHPソースコード - php_mblenマクロ(Win32)](https://github.com/php/php-src/blob/71c19800258ee3a9548af9a5e64ab0a62d1b1d8e/ext/standard/php_string.h#L160)
[mbrlen関数](https://msdn.microsoft.com/ja-jp/library/tt1kc7c1.aspx)

英語力が皆無な自分にはバグ報告はなかなか厳しいですが、気が向けばやります。
現時点でこの現象を回避するにはいくつか方法が考えられます。
`__DIR__`定数についてはいじれないので対処の方法は現実的にはありません。

1. 日本語Windows環境などを使用しない。この現象はWindowsビルドのShift_JIS環境などで発生する現象です。
2. 2バイト目が`0x81`-`0x9F`などの2バイト文字をディレクトリ名の末尾に使用しない。
3. 2バイト目が`0x81`-`0x9F`などの2バイト文字をディレクトリ名の末尾に使用したディレクトリよりも下層で、2バイト目が`0x81`-`0x9F`などの2バイト文字をディレクトリ名の末尾に使用していないディレクトリで使用する。
4. ディレクトリ区切りをスラッシュにして使用する。この現象は区切り文字がスラッシュの場合には影響ありません。バックスラッシュの場合だけ関係するのは、`0x5C`問題対策のためと思われます。
5. `basename`関数の返却値を使用して、パス文字列からファイル名部分を除去する。
6. `SplFileInfo::getPath()`を使用する。これは`0x5C`問題に対応していないようなのであまり意味はありません。
7. 自前でファイル名部分を除去する。
8. Shift_JISをうまく処理してくれるライブラリを探して使用する。
9. 自前で改修・ビルドして使用する。

実際に`dirname`関数にかけて比較したところ、3600個の文字(使われていないコードも含まれていると思われる)がこの現象に該当しました。机上でのコード範囲の組み合わせの数と一致しています。
1バイト目が`0x81`-`0x9F`,`0xE0`-`0xFC`、
2バイト目が`0x81`-`0x9F`,`0xE0`-`0xFC`
の組み合わせが該当します。つまり2バイト文字の1バイト目にあり得るコードが2バイト目にあるものがすべて該当します。
1バイト文字については、制御コード、スラッシュ、コロン、バックスラッシュは除外して確認しました。

* * *

テストコードを書いてみました。全パターンにすると出力が大変なことになるので、コード範囲の端だけにしています。
[Composer](https://getcomposer.org/)で[Peridot](http://peridot-php.github.io/)と[Leo](http://peridot-php.github.io/leo/)をインストールして、`peridot`コマンドで実行する前提のコードです。

```php
<?php
require 'vendor/autoload.php';

describe('dirname: Shift_JIS character + backslash + file name', function() {

    describe('Shift_JIS: 1 byte (edge)', function() {
        foreach ([0x20, 0x2E, 0x30, 0x39, 0x3B, 0x5B, 0x5D, 0x7E, 0xA1, 0xDF] as $single) {
            $char = sprintf("%c", $single);

            it('0x'.strtoupper(bin2hex($char)), function() use ($char) {
                $expect = 'a'.$char;
                $path = $expect.'\\'.'b.c';
                if (PHP_VERSION_ID >= 70100) { // PHP 7.1以降
                    $expect = mb_convert_encoding($expect, 'UTF-8', 'SJIS-win');
                    $path = mb_convert_encoding($path, 'UTF-8', 'SJIS-win');
                }
                expect(dirname($path))->to->equal($expect);
            });

        }
    });

    describe('Shift_JIS: 2 bytes (edge)', function() {
        $last_range = [0x40, 0x7E, 0x80, 0x81, 0x9F, 0xE0, 0xFC];
        foreach ([0x81, 0x9F, 0xE0, 0xFC] as $first) {
            foreach ($last_range as $last) {
                $char = sprintf("%c%c", $first, $last);

                it('0x'.strtoupper(bin2hex($char)), function() use ($char) {
                    $expect = 'a'.$char;
                    $path = $expect.'\\'.'b.c';
                    if (PHP_VERSION_ID >= 70100) { // PHP 7.1以降
                        $expect = mb_convert_encoding($expect, 'UTF-8', 'SJIS-win');
                        $path = mb_convert_encoding($path, 'UTF-8', 'SJIS-win');
                    }
                    expect(dirname($path))->to->equal($expect);
                });

            }
        }
    });

});
```