こんにちは、高木です。
相変わらず今回もPHPでC言語の前処理をする話題です。前回、前々回と、#line指令を埋め込んだり、__FILE__をパス名にすり替えたりする処理を作ってきました。そこでは、話を簡単にするため、ASCII以外がパス名に含まれている場合や、Windowsでディレクトリの区切り文字が「\」の場合を考慮してきませんでした。今回は、そういったパス名にも対応していくことにします。
まずは方針を決めていくことにしましょう。
#line指令と__FILE__の代替の両方に使えるようにしないといけませんので、ディレクトリの区切り文字「\」は「/」に置換することにします。そうしないと、#line指令の中では「\」はそのままにしないといけないのに対して、__FILE__の代替では「\\」に置換しないといけません。これでは両方に共通して使える文字列を作ることができません。
次に、ASCII以外の文字が現れた場合について考えてみます。#line指令を埋め込むにせよ、__FILE__の代替にせよ、本来であれば元のソースファイルのエンコーディングを調べた上で、そのエンコーディングに合った文字列を使う必要があります。ただ、それだと処理時間がかかりますので、今回は国際文字名を使って埋め込むことにします。ただし、C99より前の規格にしか対応していない処理系では国際文字名は使えませんので、元のソースファイルのエンコーディングに合わせるしかないでしょう。
では、その方針でパス名をエンコーディングする関数を作ってみます。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?php function escape_path(string $path): string { $t = mb_convert_encoding($path, 'UTF-16LE', 'auto'); $r = ''; for ($i = 0, $n = strlen($t); $i < $n; $i += 2) { $lo = $t[$i]; $hi = $t[$i + 1]; $c = ord($lo) | ord($hi) << 8; if ($c > 0xff) $r .= sprintf("\\u%04x", $c); else if ($lo == "\\") $r .= '/'; else $r .= $lo; } return $r; } ?> |
元のパス名をいったんUTF-16LEに変換したあと、2バイトずつ取り出して処理しています。取り出した2バイトは上位、下位をつなげて16ビットの整数値にします。この16ビットの整数値が0xffより大きい場合は国際文字名に置換しています。また、「\」の場合は「/」に変換しています。それ以外の場合は、下位側だけを使っています。これで方針通りに変換できるようになりました。
若干問題点がないわけではありません。元々「\uXXXX」や「\UXXXXXXXX」のような国際文字名が含まれていた場合、先頭の「\」を「/」に置換されてしまい、文字列を破壊してしまいます。とはいえ、わざわざそんな非人間的な文字列を使うことは極めて稀なので、今回は目をつぶることにしましょう。必要なら「/u」および「/U」を「\u」や「\U」に置換してあげればOKです。