PHP
Windows
0x5C
ダメ文字
5c問題

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

More than 1 year has passed since last update.

追記

この問題は、 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年以上)も対応されない状況が続いていたようです。

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 のパスで操作するエクステンションが公開されているようです。(未検証)

GitHub: kenjiuno/php-wfio

あるいは、外部プログラムの引数としてなら、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);