Windows環境で、カレントディレクトリのディレクトリ名末尾がン
とかの場合、__DIR__
定数やdirname(__FILE__)
の返却値から、現在のディレクトリが消滅し、親ディレクトリまでになってしまうという現象があります。
PHP 7.1ではWindowsのパス処理をUTF-8対応とすることで改善されているようです。
PHPのソースコードを眺めてみたところ、dirname
関数と思われる処理(よく知らないので推測です)で、バックスラッシュの前の1バイトをIsDBCSLeadByte
関数に渡し、FALSE
が返却されたらそのバックスラッシュは1バイト文字のバックスラッシュとして処理するような判定になっていました。
PHPソースコード - zend_dirname関数
PHPソースコード - IS_SLASH_Pマクロ(Win32)
IsDBCSLeadByte関数
しかし、IsDBCSLeadByte
がTRUE
を返す場合で2バイト文字の2バイト目の可能性があり、この場合、そのバックスラッシュを2バイト文字の2バイト目(ディレクトリの区切りではない)と見なしてスキップしてしまうようです。
この条件に該当するのは、2バイト文字の2バイト目が、0x81
-0x9F
など(2バイト文字の1バイト目と2バイト目どちらでもあり得る範囲)の場合と思われます。
ン
は0x8393
なので、この条件に該当します。
basename
関数では、mbrlen
関数を使用しているようで、こちらでは1バイト文字かつバックスラッシュという条件のようです。こちらではdirname
関数のような現象は起きません。basename(__FILE__)
とすれば正しくファイル名が返却されます。もしdirname
と同じような処理であれば、ファイル名にディレクトリ名が混入することになります。
PHPソースコード - php_basename関数
PHPソースコード - php_mblenマクロ(Win32)
mbrlen関数
英語力が皆無な自分にはバグ報告はなかなか厳しいですが、気が向けばやります。
現時点でこの現象を回避するにはいくつか方法が考えられます。
__DIR__
定数についてはいじれないので対処の方法は現実的にはありません。
- 日本語Windows環境などを使用しない。この現象はWindowsビルドのShift_JIS環境などで発生する現象です。
- 2バイト目が
0x81
-0x9F
などの2バイト文字をディレクトリ名の末尾に使用しない。 - 2バイト目が
0x81
-0x9F
などの2バイト文字をディレクトリ名の末尾に使用したディレクトリよりも下層で、2バイト目が0x81
-0x9F
などの2バイト文字をディレクトリ名の末尾に使用していないディレクトリで使用する。 - ディレクトリ区切りをスラッシュにして使用する。この現象は区切り文字がスラッシュの場合には影響ありません。バックスラッシュの場合だけ関係するのは、
0x5C
問題対策のためと思われます。 -
basename
関数の返却値を使用して、パス文字列からファイル名部分を除去する。 -
SplFileInfo::getPath()
を使用する。これは0x5C
問題に対応していないようなのであまり意味はありません。 - 自前でファイル名部分を除去する。
- Shift_JISをうまく処理してくれるライブラリを探して使用する。
- 自前で改修・ビルドして使用する。
実際にdirname
関数にかけて比較したところ、3600個の文字(使われていないコードも含まれていると思われる)がこの現象に該当しました。机上でのコード範囲の組み合わせの数と一致しています。
1バイト目が0x81
-0x9F
,0xE0
-0xFC
、
2バイト目が0x81
-0x9F
,0xE0
-0xFC
の組み合わせが該当します。つまり2バイト文字の1バイト目にあり得るコードが2バイト目にあるものがすべて該当します。
1バイト文字については、制御コード、スラッシュ、コロン、バックスラッシュは除外して確認しました。
テストコードを書いてみました。全パターンにすると出力が大変なことになるので、コード範囲の端だけにしています。
ComposerでPeridotとLeoをインストールして、peridot
コマンドで実行する前提のコードです。
<?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);
});
}
}
});
});