追記
この問題は、 PHP 7.1 で対応されることになりました。
参考: Windows版 PHP 7.1 で日本語パス(パス文字列のエンコーディング)が対応されることによる影響
現象
2016/04/24 現在(PHP 7.0.5, 5.6.20, 5.5.34)、
ディレクトリー名の末尾の文字の2バイト目が 0x5C
(\
) の文字(表・予・申・能など)を含むパスの場合に、fopen()
, file_get_contents()
など、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
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()
GitHub/php-src: IS_SLASH マクロ
#define IS_SLASH(c) ((c) == '/' || (c) == '\\')
状況
PHP Bug Tracking System では、類似する不具合の指摘は何件もあり、マルチバイトパスの問題として、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)
が発行(まだ未反映)されましたが、副作用として、ファイル名などで、自力でエスケープを入れて対応していた場合、動かなくなってしまうかもしれません。
<?php
echo file_get_contents(mb_convert_encoding('可能\.txt', 'CP932', 'UTF-8'));
また、変更点も大きく、どのバージョンで採用されるかは不明です。
選択肢の1つ(または意思表示)として、Pull requests(#1883) を発行しています。
※Pull requests にコメントなり、いいねなりが増えれば、対応状況も変わるかもしれません。
暫定的な回避策
PHP5.4 以上であれば、UTF-8 のパスで操作するエクステンションが公開されているようです。(未検証)
あるいは、外部プログラムの引数としてなら、tsrm_realpath_r()
は回避出来ます。
dir /A:-D
とかで is_file()
なども実装できそうですが、ここでは file_get_contents()
を実装してみます。
<?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);