Windows版 PHP(7.1前) での日本語パスの対応状況と暫定回避策

# 追記

この問題は、 PHP 7.1 で対応されることになりました。

参考: [Windows版 PHP 7.1 で日本語パス(パス文字列のエンコーディング)が対応されることによる影響](http://qiita.com/algo13/items/2810aacef69d6fb8fd0f)


# 現象

2016/04/24 現在(PHP 7.0.5, 5.6.20, 5.5.34)、
ディレクトリー名の末尾の文字の２バイト目が `0x5C`(`\`) の文字（表・予・申・能など）を含むパスの場合に、`fopen()`, `file_get_contents()` など、PHPからアクセスすることが出来ません。

```php
<?php
// '表' => \x95\x5c
// '表/file.txt'; => We can NEVER be accessed by PHP.(fopen, file, file_get_contents, arg ...)
fopen('表/file.txt', 'r'); // => false
fopen('表\\\\\\\\\\\\\\\\\\\\\\file.txt', 'r'); // => false
file('表/file.txt'); // => false
file_get_contents('表/file.txt'); // => false
```

```bat
C:\>php 表/test.php
Could not open input file: 表/test.php

C:\>
```

# 原因

パスの正規化(`sub\\\\\file.txt` => `sub\file.txt` など)を行う時に、
ディレクトリ名の末尾の`0x5C`(`\`)も `DIRECTORY_SEPARATOR` としてしまい連続した `DIRECTORY_SEPARATOR` と判断されるため。

[GitHub/php-src: tsrm_realpath_r()](https://github.com/php/php-src/blob/PHP-7.0.5/Zend/zend_virtual_cwd.c#L776)
[GitHub/php-src: IS_SLASH マクロ](https://github.com/php/php-src/blob/PHP-7.0.5/Zend/zend_virtual_cwd.h#L63)

```c
#define IS_SLASH(c)	((c) == '/' || (c) == '\\')
```

# 状況

[PHP Bug Tracking System](https://bugs.php.net/) では、類似する不具合の指摘は何件もあり、マルチバイトパスの問題として、Windows API をワイド文字版(`FindFirstFileA` => `FindFirstFileW` など)にして対応しようとしていたのか、何年(**10年以上**)も対応されない状況が続いていたようです。

- https://bugs.php.net/bug.php?id=30195
- https://bugs.php.net/bug.php?id=41199
- https://bugs.php.net/bug.php?id=50203
- https://bugs.php.net/bug.php?id=54028
- https://bugs.php.net/bug.php?id=54977
- https://bugs.php.net/bug.php?id=61315
- https://bugs.php.net/bug.php?id=63401
- https://bugs.php.net/bug.php?id=64506
- https://bugs.php.net/bug.php?id=64699
- https://bugs.php.net/bug.php?id=70903
- https://bugs.php.net/bug.php?id=71509

## Pull requests(#1854)

2016/4/18 に ワイド文字版の Windows API で対応した
[Pull requests(#1854)](https://github.com/php/php-src/pull/1854)
が発行(まだ未反映)されましたが、副作用として、ファイル名などで、自力でエスケープを入れて対応していた場合、動かなくなってしまうかもしれません。

```php
<?php
echo file_get_contents(mb_convert_encoding('可能\.txt', 'CP932', 'UTF-8'));
```

また、変更点も大きく、どのバージョンで採用されるかは不明です。  

> 選択肢の１つ(または意思表示)として、[Pull requests(#1883)](https://github.com/php/php-src/pull/1883) を発行しています。

**※Pull requests にコメントなり、いいねなりが増えれば、対応状況も変わるかもしれません。**

# 暫定的な回避策

PHP5.4 以上であれば、UTF-8 のパスで操作するエクステンションが公開されているようです。(未検証)

[GitHub: kenjiuno/php-wfio](https://github.com/kenjiuno/php-wfio)

あるいは、外部プログラムの引数としてなら、`tsrm_realpath_r()` は回避出来ます。
`dir /A:-D` とかで `is_file()` なども実装できそうですが、ここでは `file_get_contents()` を実装してみます。

```php
<?php
// Windows専用の file_get_contents
function file_get_contents_win($filename)
{
    $retval = false;
    if (ob_start()) {
        passthru('type '.escapeshellarg($filename).' 2>nul', $returnVar);
        if ($returnVar === 0) {
            $retval = ob_get_contents();
        }
        ob_end_clean();
    }
    return $retval;
}

// 「表」配下の「ン」配下の「可能.jpg」にアクセスしてみる
$filename = '表\\ン\\可能.jpg';
$cp932 = mb_convert_encoding($filename, 'CP932', 'UTF-8');
if ($filename !== mb_convert_encoding($cp932, 'UTF-8', 'CP932')) {
    echo 'ERR: file name encoding error.', PHP_EOL;
    die;
}
$contents = file_get_contents_win($cp932);
if ($contents === false) {
    echo 'ERR: file contents error.', PHP_EOL;
    die;
}
file_put_contents('put.jpg', $contents);
```