From 674fe965550444db87edc7937ff6932e1a918d9d Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 27 Jun 2025 23:39:31 +0900 Subject: feat(meta): rename vhosts/ directory to services/ --- .../phperkaigi-2023-unused-token-quiz-3.dj | 289 +++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 services/blog/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.dj (limited to 'services/blog/content/posts/2023-01-10') diff --git a/services/blog/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.dj b/services/blog/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.dj new file mode 100644 index 00000000..9cbb15be --- /dev/null +++ b/services/blog/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.dj @@ -0,0 +1,289 @@ +--- +[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 行目で発生したエラー、 + +```php +1, 20 => 0 / 0, +``` + +によって表現されている。エラーを起こす方法はいろいろと考えられるが、今回はゼロ除算を使った。 + +それでは、エラーチェインを作る箇所、関数 `f()` を見ていく。 + +[^ras-syndrome]: RAS syndrome + +{#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 +