こんにちは、高木です。
今回も例によってPHPでC言語の前処理をする話題の続きです。C言語には#include指令があり、これによって別のソースファイルを取り込むことができます。また、PHPにはincludeおよびrequireという言語機能があり、やはりこれで別のスクリプトを取り込むことができます。今回はそれらと同様に別のソースファイルを取り込む機能を実装するのですが、C言語の#includeもPHPのincludeやrequireも使いません。
なお、前回、PHPの閉じタグに#line指令を埋め込むためのget_source_code関数を作成しました。今回はそのget_source_code関数を使いますので、前回の記事をまだ読まれていない方は先にそちらから読まれることをお勧めします。
C言語の#includeはともかく、PHPのincludeやrequireも使わないのはなぜでしょうか? それは、前回やったように#line指令を埋め込む必要があるからです。また、ソースファイルAからソースファイルBを取り込む場合、両者のエンコーディングが揃っているとは限りません。この問題を解消するには、get_source_code関数でいったん文字列化したソースコードに対して、mb_convert_encoding関数を使って文字列を揃えてあげる必要があります。これらの事情がなければ、単にrequireで取り込むだけでも問題ないかもしれません。
それでは早速ソースコードを見てみましょう。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php function import(string $path): bool { if (is_null($path) || !file_exists($path)) return false; try { $contents = "#line 1 \"$path\"\n"; $contents .= get_source_code($path); $contents = mb_convert_encoding($contents, 'UTF-8', 'auto'); eval("?>$contents"); return true; } catch (Throwable $e) { fprintf(STDERR, "$path:%d: error: %s\n", $e->getLine(), $e->getMessage()); } return false; } ?> |
ソースファイルを取り込む関数の名前はimportにしました。上のコードは簡略化するために、パスをダイレクトに指定する形を取っています。しかし、現実には#includeなどと同様、あらかじめ登録しておいたディレクトリから該当するソースファイルを探索する必要があります。これについては、いずれ機会があればご紹介したいと思います。
import関数の実装を見ると、get_source_code関数で文字列化したソースコードの直前にも#line指令を付加しているのがわかると思います。こうしておかないと、エラーメッセージ等で正しい行番号やファイル名が出力されなくなります。
文字列化したソースコードは、エンコーディングをUTF-8に揃えたあと、evalで評価しています。PHPの公式ドキュメントには、evalに関して次のような記述があります。
警告 eval() は非常に危険な言語構造です。 というのも、任意の PHP コードを実行できてしまうからです。 これを使うことはおすすめしません。 いろいろ検討した結果どうしても使わざるを得なくなった場合は、細心の注意を払って使いましょう。 ユーザーから受け取ったデータをそのまま渡してはいけません。 渡す前に、適切な検証が必要です。
今回は、まさにユーザーから受け取ったデータを(#line指令を付加して、エンコーディングを揃えただけで)そのままevalに渡しています。ですが、Webアプリケーションとはそもそも事情が異なります。もっとも、第三者が頒布しているソースコードを取り込んでビルドする際に、任意のコードが実行されてしまうという危険性をはらんでいることも事実です。ただ、それを言い出すとconfigureやMakefileなどでも同じことが言えてしまいます。ここはユーザー責任で運用する以外にありません。