From dae1b45d1f3bc805161606ca5815dac210358db7 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 10 Jan 2023 01:50:37 +0900 Subject: new post: phperkaigi-2023-unused-token-quiz-3 --- .../phperkaigi-2023-unused-token-quiz-3.adoc | 279 +++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.adoc (limited to 'content/posts/2023-01-10') diff --git a/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.adoc b/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.adoc new file mode 100644 index 0000000..29a86a1 --- /dev/null +++ b/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.adoc @@ -0,0 +1,279 @@ += PHPerKaigi 2023: ボツになったトークン問題 その 3 +:tags: php, phperkaigi +:description: 来年の PHPerKaigi 2023 でデジタルサーカス株式会社から出題予定のトークン問題のうち、 \ + ボツになった問題を公開する (その 3)。 +:revision-1: 2023-01-10 公開 + + +== はじめに + +2023 年 3 月 23 日から 25 日にかけて開催予定 (記事執筆時点) の https://phperkaigi.jp/2023/[PHPerKaigi 2023] において、 +昨年と同様に、弊社 https://www.dgcircus.com/[デジタルサーカス株式会社] からトークン問題を出題予定である。 + +昨年のトークン問題の記事はこちら: link:/posts/2022-04-09/phperkaigi-2022-tokens/[PHPerKaigi 2022 トークン問題の解説] + +すでに 2023 年用の問題は作成済みであるが、その制作過程の中でいくつかボツ問ができた。せっかくなので、PHPerKaigi 開催を待つ間に紹介しようと思う。 + +10 月から 2 月まで、毎月 1 記事ずつ公開していく予定 (忘れていなければ → 忘れていたので 12 月公開予定だった記事を今書いている)。 + +* その 1 はこちら: link:/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/[PHPerKaigi 2023: ボツになったトークン問題 その 1] +* その 2 はこちら: link:/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2/[PHPerKaigi 2023: ボツになったトークン問題 その 2] + + +== 問題 + +注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。 + +[source,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 チャレンジのトークンには空白を含められないという制約があるが、こういった形でトークンにすれば回避できる。 + +== 解説 + +=== 概要 + +例外が発生した行数にデータをエンコードし、それを `catch` で捕まえて表示している。 + +=== 例外オブジェクトの連鎖 + +https://www.php.net/class.Exception[`Exception`] や https://www.php.net/class.Error[`Error`] には `$previous` というプロパティがあり、コンストラクタの第3引数から渡すことができる。主に 2つの用法がある: + +* エラーを処理している途中に起こった別のエラーに、元のエラー情報を含める +* 内部エラーをラップして作られたエラーに、内部エラーの情報を含める + +このうち 1つ目のケースは、 `finally` 節の中でエラーを投げると PHP 処理系が勝手に `$previous` を設定してくれる。 + +[source,php] +---- +getMessage() . PHP_EOL; + // => Error 2 + echo $e->getPrevious()->getMessage() . PHP_EOL; + // => Error 1 +} +---- + +この知識を元に、トークンの出力部を解析してみる。 + +=== 出力部の解析 + +出力部をコメントや改行を追加して再掲する: + +[source,php] +---- +getPrevious()) { + printf('%c', $e->getLine() + 23); + } + echo "\n"; +} +---- + +出力をおこなう `catch` 節を見てみると、 `Throwable::getPrevious()` を呼び出してエラーチェインを辿り、 `Throwable::getLine()` でエラーが発生した行数を取得している。その行数に `23` なるマジックナンバーを足し、フォーマット指定子 `%c` で出力している。 + +フォーマット指定子 `%c` は、整数を ASCII コードfootnote:[RAS syndrome] と見做して印字する。トークン `#base64_decode('SGVsbG8sIFdvcmxkIQ==')` の `b` であれば、ASCII コード `98` なので、75 行目で発生したエラー、 + +``` + 1, 20 => 0 / 0, +``` + +によって表現されている。エラーを起こす方法はいろいろと考えられるが、今回はゼロ除算を使った。 + +それでは、エラーチェインを作る箇所、関数 `f()` を見ていく。 + +=== データ構成部の解析 + +`f()` の定義を再掲する (エラーオブジェクトの行数を利用しているので、一部分だけ抜き出すと値が変わることに注意): + +[source,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()` を呼び出している箇所を確認すると、 + +[source,php] +---- +