--- [article] uuid = "89722cfb-7f4b-4e96-80bc-e0096e5eeef6" title = "PHPerKaigi 2023: ボツになったトークン問題 その 3" description = "来年の PHPerKaigi 2023 でデジタルサーカス株式会社から出題予定のトークン問題のうち、ボツになった問題を公開する (その 3)。" tags = [ "php", "phperkaigi", ] [[article.revisions]] date = "2023-01-10" remark = "公開" --- {#intro} # はじめに 2023 年 3 月 23 日から 25 日にかけて開催予定 (記事執筆時点) の [PHPerKaigi 2023](https://phperkaigi.jp/2023/) において、 昨年と同様に、弊社 [デジタルサーカス株式会社](https://www.dgcircus.com/) からトークン問題を出題予定である。 昨年のトークン問題の記事はこちら: [PHPerKaigi 2022 トークン問題の解説](/posts/2022-04-09/phperkaigi-2022-tokens/) すでに 2023 年用の問題は作成済みであるが、その制作過程の中でいくつかボツ問ができた。 せっかくなので、PHPerKaigi 開催を待つ間に紹介しようと思う。 10 月から 2 月まで、毎月 1 記事ずつ公開していく予定 (忘れていなければ → 忘れていたので 12 月公開予定だった記事を今書いている)。 * その 1 はこちら: [PHPerKaigi 2023: ボツになったトークン問題 その 1](/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/) * その 2 はこちら: [PHPerKaigi 2023: ボツになったトークン問題 その 2](/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2/) {#quiz} # 問題 注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。 ```php getPrevious()) printf('%c', $e->getLine() + 23); echo "\n"; } function f(int $i) { if ($i < 0) f(); try { match ($i) { 0 => 0 / 0, 15, 36 => 0 / 0, 14 => 0 / 0, 37 => 0 / 0, 6 => 0 / 0, 5 => 0 / 0, 22 => 0 / 0, 34, 35 => 0 / 0, 25 => 0 / 0, 17, 21 => 0 / 0, 24, 32 => 0 / 0, 33 => 0 / 0, 16 => 0 / 0, 18 => 0 / 0, 7 => 0 / 0, 2 => 0 / 0, 1, 20 => 0 / 0, 10, 28 => 0 / 0, 8, 12, 26 => 0 / 0, 4, 9, 13 => 0 / 0, 31 => 0 / 0, 29 => 0 / 0, 11 => 0 / 0, 3, 19, 23 => 0 / 0, 27 => 0 / 0, 30 => 0 / 0, }; } finally { f($i - 1); } } function g() { return __LINE__; } ``` "Catchline" と名付けた作品。実行するとトークン `#base64_decode('SGVsbG8sIFdvcmxkIQ==')` が得られる。 トークンは PHP の式になっていて、評価すると `Hello, World!` という文字列になる。PHPer チャレンジのトークンには空白を含められないという制約があるが、こういった形でトークンにすれば回避できる。 {#commentary} # 解説 {#summary} ## 概要 例外が発生した行数にデータをエンコードし、それを `catch` で捕まえて表示している。 {#chain-of-exceptions} ## 例外オブジェクトの連鎖 [`Exception`](https://www.php.net/class.Exception) や [`Error`](https://www.php.net/class.Error) には `$previous` というプロパティがあり、コンストラクタの第3引数から渡すことができる。主に 2つの用法がある: * エラーを処理している途中に起こった別のエラーに、元のエラー情報を含める * 内部エラーをラップして作られたエラーに、内部エラーの情報を含める このうち 1つ目のケースは、 `finally` 節の中でエラーを投げると PHP 処理系が勝手に `$previous` を設定してくれる。 ```php getMessage() . PHP_EOL; // => Error 2 echo $e->getPrevious()->getMessage() . PHP_EOL; // => Error 1 } ``` この知識を元に、トークンの出力部を解析してみる。 {#output} ## 出力部の解析 出力部をコメントや改行を追加して再掲する: ```php getPrevious()) { printf('%c', $e->getLine() + 23); } echo "\n"; } ``` 出力をおこなう `catch` 節を見てみると、 `Throwable::getPrevious()` を呼び出してエラーチェインを辿り、 `Throwable::getLine()` でエラーが発生した行数を取得している。その行数に `23` なるマジックナンバーを足し、フォーマット指定子 `%c` で出力している。 フォーマット指定子 `%c` は、整数を ASCII コード[^ras-syndrome] と見做して印字する。トークン `#base64_decode('SGVsbG8sIFdvcmxkIQ==')` の `b` であれば、ASCII コード `98` なので、75 行目で発生したエラー、 [^ras-syndrome]: RAS syndrome ```php 1, 20 => 0 / 0, ``` によって表現されている。エラーを起こす方法はいろいろと考えられるが、今回はゼロ除算を使った。 それでは、エラーチェインを作る箇所、関数 `f()` を見ていく。 {#data-construction} ## データ構成部の解析 `f()` の定義を再掲する (エラーオブジェクトの行数を利用しているので、一部分だけ抜き出すと値が変わることに注意): ```php function f(int $i) { if ($i < 0) f(); try { match ($i) { 0 => 0 / 0, // 12 行目 15, 36 => 0 / 0, 14 => 0 / 0, 37 => 0 / 0, // (略) 30 => 0 / 0, // 97 行目 }; } finally { f($i - 1); } } ``` 前述のように、 `finally` 節でエラーを投げると PHP 処理系が `$previous` を設定する。ここでは、エラーを繋げるために `f()` を再帰呼び出ししている。最初に `f()` を呼び出している箇所を確認すると、 ```php