diff options
Diffstat (limited to 'content/posts')
| -rw-r--r-- | content/posts/2022-04-09/phperkaigi-2022-tokens.md | 323 |
1 files changed, 312 insertions, 11 deletions
diff --git a/content/posts/2022-04-09/phperkaigi-2022-tokens.md b/content/posts/2022-04-09/phperkaigi-2022-tokens.md index 6d110dc..4695bf7 100644 --- a/content/posts/2022-04-09/phperkaigi-2022-tokens.md +++ b/content/posts/2022-04-09/phperkaigi-2022-tokens.md @@ -6,16 +6,19 @@ tags: ["conference", "php", "phperkaigi"] aliases: ['/posts/phperkaigi-2022-tokens/'] --- +# 更新履歴 + +* 2022-04-09: 公開 +* 2022-04-16: 2問目、3問目の解説を追加、1問目に加筆 + # はじめに -本日開始された [PHPerKaigi 2022](https://phperkaigi.jp/2022/) の PHPer チャレンジにおいて、弊社デジタルサーカスの問題を 3問作成した。この記事では、これらの問題の解説をおこなう。 +本日開始された [PHPerKaigi 2022](https://phperkaigi.jp/2022/) の PHPer チャレンジにおいて、弊社 [デジタルサーカス株式会社](https://www.dgcircus.com/) の問題を 3問作成した。この記事では、これらの問題の解説をおこなう。 リポジトリはこちら: https://github.com/nsfisis/PHPerKaigi2022-tokens -注意: ネタバレ防止のため、2問目と 3問目はまだ解説を書いていない。 - # 第1問 brainf_ck.php @@ -103,6 +106,71 @@ $🐘([ まず目につくのは大量の絵文字だろう。 PHP は識別子に使用できる文字の範囲が広く、絵文字も使うことができる。 + +### プログラム全体 + +Brainf\*ck のインタプリタとプログラムになっている。 +Brainf\*ck とは、難解プログラミング言語のひとつであり、ここで説明するよりも Wikipedia の該当ページを読んだ方がよい。 + +https://ja.wikipedia.org/wiki/Brainfuck + +なお、brainf\*ck プログラムを普通の書き方で書くと、次のようになる。 + +``` ++ + + + + + + + + + +[ + > + + + + > + + + + + + > + + + + + + + + + + + + + > + + + + + + + + + + + < < < < - +] +> + + + + + . +- - . +> - - - . +> - - - . +- - . +- . +< . +> > - - . ++ + + + + + + . +< - - - - . +< . +> + + . +> - . +< . +``` + +実行結果はこちら: https://ideone.com/22VWmb + +それぞれの絵文字で表された関数が、各命令に対応している。 + +* `$👉`: `>` +* `$👈`: `<` +* `$👍`: `+` +* `$👎`: `-` +* `$📝`: `.` +* `$🤡`: `[` +* `$🎪`: `]` + +`,` (入力) に対応する関数はない (このプログラムでは使わないので用意していない)。 + +なお、`$🐘` はいわゆる main 関数であり、プログラムの実行部分である。 + + +### 絵文字の選択 + +おおよそ意味に合致するよう選んでいるが、`$🤡` と `$🎪` は弊社デジタルサーカスにちなんでいる。 +また、`$🐘` は PHP のマスコットの象に由来する。 + + +### strict_types + +`declare` 文の `strict_types` に指定できるのは、`0` か `1` の数値リテラルだが、 +`0x0` や `0b1` のような値も受け付ける。 +今回は、PHP 8.1 から追加された、`0O` または `0o` から始まる八進数リテラルを使った。 + + ### URL ソースコードのライセンスを示したこの部分だが、 @@ -114,6 +182,7 @@ https://creativecommons.org/publicdomain/zero/1.0/ 完全に合法な PHP のコードである。 `https:` 部分はラベル、`//` 以降は行コメントになっている。 + ### リテラルなしで数値を生成する ソースコード中に、ほとんど数値リテラルが書かれていないことにお気づきだろうか。 @@ -127,13 +196,17 @@ assert(3 === ![]+![]+![]); assert(10 === +(![].+!![])); ``` -`[]` に `!` を適用すると `TRUE` が返ってくる。それに `+` +`[]` に `!` を適用すると `true` が返ってくる。それに `+` を適用すると、`bool` から `int` ヘの型変換が走り、`1` が生成される。`10` はさらにトリッキーだ。まず `1` と `0` を作り、`.` で文字列として結合する (`'10'`)。これに `+` を適用すると、`string` から `int` への型変換が走り、`10` が生まれる (コード量に頓着しないなら、`1` を 10 個足し合わせてももちろん 10 が作れる)。 +また、`error_reporting` に指定しているのは `-1` である。 +これは、`!` によって文字列を `false` にし、`+` によって `false` を `0` にし、さらにビット反転して `-1` にしている。 + + ### `if` 文なしで条件分岐 三項演算子ないし `match` 式を使うことで、`if` を一切書かずに条件分岐ができる。 @@ -142,21 +215,249 @@ assert(10 === +(![].+!![])); ### `while`、`for` 文なしでループ -不動点コンビネータを使う (説明は省略)。ここでは、一般に Z -コンビネータとして知られるものを使った (`$z`)。 +不動点コンビネータを使って無名再帰する (詳しい説明は省略する。これらの単語で検索してほしい)。 +ここでは、一般に Z コンビネータとして知られるものを使った (`$z`)。 +実際のところ、`$🤡` や `$🎪`、`$🐘` は、一度 Scheme (Lisp の一種) で書いてから PHP に翻訳する形で記述した。 -### プログラム全体 +なお、PHP は末尾再帰の最適化をおこなわない (少なくとも今のところは) ので、 +あまりに長い brainf\*ck プログラムを書くとスタックオーバーフローする。 -Brainf\*ck。以上。 +# 第2問 riddle.php +ソースコードはこちら。実行には PHP 8.0 以上が必要なので注意。 -# 第2問 riddle.php +```php +<?php + +/********************************************************* + * This program displays a PHPer token. * + * Guess 'N'. * + * * + * Hints: * + * - N itself has no special meaning, e.g., 42, 8128, * + * it is selected at random. * + * - Each element of $token represents a single letter. * + * - One letter consists of 5x5 cells. * + * - Remember, the output is a complete PHPer token. * + * * + * License: * + * https://creativecommons.org/publicdomain/zero/1.0/ * + *********************************************************/ +const N = 0 /* Change it to your answer. */; +assert(0 <= N && N <= 0b11111_11111_11111_11111_11111); + +$token = [ + 0x14B499C, + 0x0BE34CC, 0x01C9C69, + 0x0ECA069, 0x01C2449, 0x0FDB166, 0x01C9C69, + 0x01C1C66, 0x0FC1C47, 0x01C1C66, + 0x10C5858, 0x1E4E3B8, 0x1A2F2F8, +]; +foreach ($token as $x) { + $x = $x ^ N; + + $x = sprintf('%025b', $x); + $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x); + $x = implode("\n", str_split($x, length: 5)); + echo "{$x}\n\n"; +} +``` + +さて、この問題はさきほどのように単純に実行しただけでは、謎のブロックが表示されるだけでトークンは得られない。 +トークンを得るためには、ソースコードを読み、定数 `N` を特定する必要がある。 + +ここでは、私の想定解を解説する。 + + +## 読解 + +まずはソースコードを読んでいく。 + +```php +$token = [ + // 略 +]; +``` + +数値からなる `$token` があり、各要素をループしている。 + +```php + $x = $x ^ N; +``` + +まずは排他的論理和 (xor) を取り、 + +```php + $x = sprintf('%025b', $x); +``` + +二進数に変換して、 + +```php + $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x); +``` + +0 を空白に、1 を `#` にし、 + +```php + $x = implode("\n", str_split($x, length: 5)); +``` + +5文字ごとに区切ったあと、改行で結合している。 + + +## ヒント + +次に、ソースコードに書いてあるヒントを読んでいく。 + +* `N` それ自体は、42 や 8128 といったような特別な意味を持たず、ランダムに決められている +* `$token` の各要素は、1文字を表す +* 1文字は 5x5 のセルからなる +* 出力されるのは、完全な PHPer トークンである + +ここで、PHPer トークンは必ず `#` 記号から始まることを思いだすと、 +`$token` の最初の数字 `0x14B499C` は、変換の結果 `#` になるのではないかと予想される (なお、このことは、リポジトリの README ファイルに追加ヒントとして書かれている)。 + + +## 解く + +ここまでわかれば、あと一歩で解ける。すなわち、`0x14B499C` が `#` に変換されるような `N` を見つければよい。 + +`N` は高々 + +```php +assert(0 <= N && N <= 0b11111_11111_11111_11111_11111); +``` + +なのでブルートフォースしてもよいが、ここではブルートフォースしない方法を紹介する。 + +```php +<?php + +$x = 0x14B499C; + +$x = $x ^ N; + +$x = sprintf('%025b', $x); +$x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x); +$x = implode("\n", str_split($x, length: 5)); + +assert($x === + " # # \n" . + "#####\n" . + " # # \n" . + "#####\n" . + " # # "); +``` + +この一連の変換に対する逆変換を考えると、次のようになる。 + +```php +<?php + +$x = + " # # \n" . + "#####\n" . + " # # \n" . + "#####\n" . + " # # "; + +$x = implode('', explode("\n", $x)); +$x = str_replace(search: [' ', '#'], replace: ['0', '1'], subject: $x); +$x = bindec($x); + +$n = $x ^ 0x14B499C; + +echo "N = $n\n"; +``` + +これを実行すると、`N` が得られる。 -前述のとおり、ネタバレ防止のため、2問目はまだ解説を書いていない。 # 第3問 toquine.php -前述のとおり、ネタバレ防止のため、3問目はまだ解説を書いていない。 +ソースコードはこちら。 + +```php +<?php + +// License: https://creativecommons.org/publicdomain/zero/1.0/ +// This is a quine-like program to generate a PHPer token. +// Execute it like this: php toquine.php | php | php | php | ... + +$s = <<<'Q' +<?cuc +// Yvprafr: uggcf://perngvirpbzzbaf.bet/choyvpqbznva/mreb/1.0/ +// Guvf vf n dhvar-yvxr cebtenz gb trarengr n CUCre gbxra. +// Rkrphgr vg yvxr guvf: cuc gbdhvar.cuc | cuc | cuc | cuc | ... +%f$f = %f; +$f = fge_ebg13($f); $kf = [ +%f, +]; +$g = ahyy.snyfr; sbe ($v = 0; $v <= vagqvi(__YVAR__-035,6); ++$v) vs (!vffrg($kf[$v])) oernx; ryfr +$g .= vzcybqr("\a", fge_fcyvg(fge_ercynpr(['0','1'], [' ','##'], fcevags(pue(37) . '025o', $kf[$v])), 012)) . "\a\a"; +$jf = neenl_znc(sa($j) => vzcybqr(', ', $j), neenl_puhax(neenl_znc(sa($k) => fcevags('0k' . pue(37) . '07K', $k), $kf), 10)); +cevags($f, $g, fge_ebg13("<<<'Q'\a{$f}\aQ"), vzcybqr(",\a", $jf)); +Q; +$s = str_rot13($s); $xs = [ +0x0AFABEA, 0x1F294A7, 0x1F2109F, 0x1F294A7, 0x0002800, 0x1F2109F, 0x0117041, 0x1F294A7, 0x1FAD6B5, 0x1F295B7, +0x010FC21, 0x1FAD6B5, 0x1151151, 0x010FC21, 0x1F294A7, 0x1F295B7, 0x1FAD6B5, 0x1F294A7, 0x1F295B7, 0x1F8C63F, +0x1F8C631, 0x1FAD6B5, 0x17AD6BD, 0x17AD6BD, 0x1F8C63F, 0x1F295B7, +]; +$t = null.false; for ($i = 0; $i <= intdiv(__LINE__-035,6); ++$i) if (!isset($xs[$i])) break; else +$t .= implode("\n", str_split(str_replace(['0','1'], [' ','##'], sprintf(chr(37) . '025b', $xs[$i])), 012)) . "\n\n"; +$ws = array_map(fn($w) => implode(', ', $w), array_chunk(array_map(fn($x) => sprintf('0x' . chr(37) . '07X', $x), $xs), 10)); +printf($s, $t, str_rot13("<<<'D'\n{$s}\nD"), implode(",\n", $ws)); +``` + +コメントにもあるとおり、次のようにして実行すれば答えがでてくる。 + +```shell-session +$ php toquine.php | php | php | php | ... +``` + +実際にはもう少しパイプで繋げなければならない。 + + +## 解説 + +### プログラム全体 + +コメントにもあるとおり、これは quine (風) のプログラムになっている。 +Quine とは、自分のソースコードをそっくりそのまま出力するようなプログラムのことである。 + +このプログラムは、実行すると自身とほとんど同じプログラムを出力する。 +異なるのはトークンになっている部分のみである。 + + +### トークン + +`$xs` がトークンに対応している。変換のロジックは `riddle.php` とほぼ同じなので省略する。 + + +### 状態保持 + +トークンの何文字目まで出力したかを、ソースコードを変えずに (quine なので) 覚えておく必要がある。 +このプログラムでは、トークンが出力されるとソースコードがだんだんと長くなっていくのを利用して、`__LINE__` から情報を取得している。 + + +### ROT 13 + +Quine は、素朴に書くとプログラムの一部が 2回記述されてしまう。 +これがあまり美しくないので、`toquine.php` では、ROT 13 変換を使って難読化した。 + +それにしてもなぜこんなものが標準ライブラリに……。 + + + +# おわりに + +解いていただいたみなさん、また、難易度調整につきあっていただいた社内のみなさん、ありがとうございました。 + +今回は直前に作りはじめたのもあり、3問だけかつ使い古されたネタばかりになってしまいましたが、 +来年は 5問、より面白い問題を持っていきます。 + +実はもう作りはじめているので、どうか来年もありますように……。 |
