From 6dedddc545e2f1930bdc2256784eb1551bd4231d Mon Sep 17 00:00:00 2001
From: nsfisis
emph strong
diff --git a/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes.md b/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes.md
index 70068754..fb8c8798 100644
--- a/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes.md
+++ b/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes.md
@@ -60,8 +60,7 @@ $ clang –std=c++17 hoge.cpp
別件で [cppreference.com の identifier のページ](https://en.cppreference.com/w/cpp/language/identifiers)を読んでいた時、次の文が目に止まった。
> * the identifiers that are keywords cannot be used for other purposes;
->
-> * The only place they can be used as non-keywords is in an attribute-token. (e.g. [[private]] is a valid attribute) (since C++11)
+> * The only place they can be used as non-keywords is in an attribute-token. (e.g. [[private]] is a valid attribute) (since C++11)
キーワードでも属性として指定する場合は非キーワードとして使えるらしい。
実際にやってみる。
diff --git a/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes/index.html b/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes/index.html
index 79f7ebdd..57412b03 100644
--- a/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes/index.html
+++ b/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes/index.html
@@ -72,40 +72,43 @@
タイトル落ち。まずはこのコードを見て欲しい。
コンパイラのバージョン
コンパイルコマンド (C++17 指定)
この記事から得られるものはこれ以上ないので以下は蛇足になる。
@@ -135,8 +138,9 @@
上のコードでは
C++17 の仕様も見てみる (正確には標準化前のドラフト)。
diff --git a/services/nuldoc/public/blog/posts/2021-10-02/python-unbound-local-error/index.html b/services/nuldoc/public/blog/posts/2021-10-02/python-unbound-local-error/index.html
index 26fb852f..03e01be0 100644
--- a/services/nuldoc/public/blog/posts/2021-10-02/python-unbound-local-error/index.html
+++ b/services/nuldoc/public/blog/posts/2021-10-02/python-unbound-local-error/index.html
@@ -75,13 +75,14 @@
Python でクロージャを作ろうと、次のようなコードを書いた。
関数
当初の意図を表現するには、次のように書けばよい。
それぞれの処理系がどのような値を返すかだが、stack overflow に良い質問と回答があった。
@@ -96,16 +97,17 @@
@@ -121,10 +123,11 @@
+ puts "Hello, World!"puts "Hello, World!"
+
+ #include <iostream>
-
-[[alignas]] [[alignof]] [[and]] [[and_eq]] [[asm]] [[auto]] [[bitand]]
-[[bitor]] [[bool]] [[break]] [[case]] [[catch]] [[char]] [[char16_t]]
-[[char32_t]] [[class]] [[compl]] [[const]] [[const_cast]] [[constexpr]]
-[[continue]] [[decltype]] [[default]] [[delete]] [[do]] [[double]]
-[[dynamic_cast]] [[else]] [[enum]] [[explicit]] [[export]] [[extern]] [[false]]
-[[final]] [[float]] [[for]] [[friend]] [[goto]] [[if]] [[inline]] [[int]]
-[[long]] [[mutable]] [[namespace]] [[new]] [[noexcept]] [[not]] [[not_eq]]
-[[nullptr]] [[operator]] [[or]] [[or_eq]] [[override]] [[private]]
-[[protected]] [[public]] [[register]] [[reinterpret_cast]] [[return]] [[short]]
-[[signed]] [[sizeof]] [[static]] [[static_assert]] [[static_cast]] [[struct]]
-[[switch]] [[template]] [[this]] [[thread_local]] [[throw]] [[true]] [[try]]
-[[typedef]] [[typeid]] [[typename]] [[union]] [[unsigned]]
-[[virtual]] [[void]] [[volatile]] [[wchar_t]] [[while]] [[xor]] [[xor_eq]]
-// [[using]]
-int main() {
- std::cout << "Hello, World!" << std::endl;
-}#include <iostream>
+
+[[alignas]] [[alignof]] [[and]] [[and_eq]] [[asm]] [[auto]] [[bitand]]
+[[bitor]] [[bool]] [[break]] [[case]] [[catch]] [[char]] [[char16_t]]
+[[char32_t]] [[class]] [[compl]] [[const]] [[const_cast]] [[constexpr]]
+[[continue]] [[decltype]] [[default]] [[delete]] [[do]] [[double]]
+[[dynamic_cast]] [[else]] [[enum]] [[explicit]] [[export]] [[extern]] [[false]]
+[[final]] [[float]] [[for]] [[friend]] [[goto]] [[if]] [[inline]] [[int]]
+[[long]] [[mutable]] [[namespace]] [[new]] [[noexcept]] [[not]] [[not_eq]]
+[[nullptr]] [[operator]] [[or]] [[or_eq]] [[override]] [[private]]
+[[protected]] [[public]] [[register]] [[reinterpret_cast]] [[return]] [[short]]
+[[signed]] [[sizeof]] [[static]] [[static_assert]] [[static_cast]] [[struct]]
+[[switch]] [[template]] [[this]] [[thread_local]] [[throw]] [[true]] [[try]]
+[[typedef]] [[typeid]] [[typename]] [[union]] [[unsigned]]
+[[virtual]] [[void]] [[volatile]] [[wchar_t]] [[while]] [[xor]] [[xor_eq]]
+// [[using]]
+int main() {
+ std::cout << "Hello, World!" << std::endl;
+}
+
+ $ clang++ –version Apple clang version 11.0.0
-(clang-1100.0.33.8) Target: x86_64-apple-darwin19.6.0 Thread model:
-posix InstalledDir:
-/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin$ clang++ –version Apple clang version 11.0.0
+(clang-1100.0.33.8) Target: x86_64-apple-darwin19.6.0 Thread model:
+posix InstalledDir:
+/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
+
+ $ clang –std=c++17 hoge.cpp$ clang –std=c++17 hoge.cpp
+[[using]] をコメントアウトしているが、これは using キーワードのみ属性構文の中で意味を持つからであり、このコメントアウトを外すとコンパイルに失敗する。
+ // using の例
-[[using foo: attr1, attr2]] int x; // [[foo::attr1, foo::attr2]] の糖衣構文// using の例
+[[using foo: attr1, attr2]] int x; // [[foo::attr1, foo::attr2]] の糖衣構文
+
+ def f():
- x = 0
- def g():
- x += 1
- g()
-
-f()def f():
+ x = 0
+ def g():
+ x += 1
+ g()
+
+f()
+g から 関数 f のスコープ内で定義された変数 x を参照し、それに 1 を足そうとしている。 これを実行すると x += 1 の箇所でエラーが発生する。
@@ -95,27 +96,29 @@
local変数 x が代入前に参照された、とある。これは、f の x を参照するのではなく、新しく別の変数を g 内に作ってしまっているため。前述のコードを宣言と代入を便宜上分けて書き直すと次のようになる。var を変数宣言のための構文として擬似的に利用している。
+ # 注: var は正しい Python の文法ではない。上記参照のこと
-def f():
- var x # f の local変数 'x' を宣言
- x = 0 # x に 0 を代入
- def g(): # f の内部関数 g を定義
- var x # g の local変数 'x' を宣言
- # たまたま f にも同じ名前の変数があるが、それとは別の変数
- x += 1 # x に 1 を加算 (x = x + 1 の糖衣構文)
- # 加算する前の値を参照しようとするが、まだ代入されていないためエラー
- g()# 注: var は正しい Python の文法ではない。上記参照のこと
+def f():
+ var x # f の local変数 'x' を宣言
+ x = 0 # x に 0 を代入
+ def g(): # f の内部関数 g を定義
+ var x # g の local変数 'x' を宣言
+ # たまたま f にも同じ名前の変数があるが、それとは別の変数
+ x += 1 # x に 1 を加算 (x = x + 1 の糖衣構文)
+ # 加算する前の値を参照しようとするが、まだ代入されていないためエラー
+ g()
+
+ def f():
- x = 0
- def g():
- nonlocal x ## (*)
- x += 1
- g()def f():
+ x = 0
+ def g():
+ nonlocal x ## (*)
+ x += 1
+ g()
+(*) のように、nonlocal を追加する。これにより一つ外側のスコープ (g の一つ外側 = f) で定義されている x を探しに行くようになる。
diff --git a/services/nuldoc/public/blog/posts/2021-10-02/ruby-detect-running-implementation/index.html b/services/nuldoc/public/blog/posts/2021-10-02/ruby-detect-running-implementation/index.html
index b2e7a02a..db82851d 100644
--- a/services/nuldoc/public/blog/posts/2021-10-02/ruby-detect-running-implementation/index.html
+++ b/services/nuldoc/public/blog/posts/2021-10-02/ruby-detect-running-implementation/index.html
@@ -81,12 +81,13 @@
上記ページの例から引用する:
+ $ ruby-1.9.1 -ve 'p RUBY_ENGINE'
-ruby 1.9.1p0 (2009-03-04 revision 22762) [x86_64-linux]
-"ruby"
-$ jruby -ve 'p RUBY_ENGINE'
-jruby 1.2.0 (ruby 1.8.6 patchlevel 287) (2009-03-16 rev 9419) [i386-java]
-"jruby"$ ruby-1.9.1 -ve 'p RUBY_ENGINE'
+ruby 1.9.1p0 (2009-03-04 revision 22762) [x86_64-linux]
+"ruby"
+$ jruby -ve 'p RUBY_ENGINE'
+jruby 1.2.0 (ruby 1.8.6 patchlevel 287) (2009-03-16 rev 9419) [i386-java]
+"jruby"
+
+ | RUBY_ENGINE | Implementation |
-|:-----------:|:------------------|
-| <undefined> | MRI < 1.9 |
-| 'ruby' | MRI >= 1.9 or REE |
-| 'jruby' | JRuby |
-| 'macruby' | MacRuby |
-| 'rbx' | Rubinius |
-| 'maglev' | MagLev |
-| 'ironruby' | IronRuby |
-| 'cardinal' | Cardinal || RUBY_ENGINE | Implementation |
+|:-----------:|:------------------|
+| <undefined> | MRI < 1.9 |
+| 'ruby' | MRI >= 1.9 or REE |
+| 'jruby' | JRuby |
+| 'macruby' | MacRuby |
+| 'rbx' | Rubinius |
+| 'maglev' | MagLev |
+| 'ironruby' | IronRuby |
+| 'cardinal' | Cardinal |
+
+ /*
- * Ruby engine.
- */
-#define MRUBY_RUBY_ENGINE "mruby"
diff --git a/services/nuldoc/public/blog/posts/2021-10-02/ruby-then-keyword-and-case-in/index.html b/services/nuldoc/public/blog/posts/2021-10-02/ruby-then-keyword-and-case-in/index.html
index 9035fa18..0592f0a6 100644
--- a/services/nuldoc/public/blog/posts/2021-10-02/ruby-then-keyword-and-case-in/index.html
+++ b/services/nuldoc/public/blog/posts/2021-10-02/ruby-then-keyword-and-case-in/index.html
@@ -103,36 +103,38 @@
使われることは稀だが、Ruby では /*
+ * Ruby engine.
+ */
+#define MRUBY_RUBY_ENGINE "mruby"
+then がキーワードになっている。次のように使う:
if cond then
- puts "Y"
-else
- puts "N"
-end
+ if cond then
+ puts "Y"
+else
+ puts "N"
+end
+
このキーワードが現れうる場所はいくつかあり、if、unless、rescue、case 構文がそれに当たる。 上記のように、何か条件を書いた後 then を置き、式がそこで終了していることを示すマーカーとして機能する。
# Example:
-
-if x then
- a
-end
-
-unless x then
- a
-end
-
-begin
- a
-rescue then
- b
-end
-
-case x
-when p then
- a
-end
+ # Example:
+
+if x then
+ a
+end
+
+unless x then
+ a
+end
+
+begin
+ a
+rescue then
+ b
+end
+
+case x
+when p then
+ a
+end
+
then を書くことはない。なぜか。次のコードを実行してみるとわかる。
if true puts 'Hello, World!' end
+ if true puts 'Hello, World!' end
+
次のような構文エラーが出力される。
20:1: syntax error, unexpected local variable or method, expecting `then' or ';' or '\n'
-if true puts 'Hello, World!' end
- ^~~~
-20:1: syntax error, unexpected `end', expecting end-of-input
-...f true puts 'Hello, World!' end
+ 20:1: syntax error, unexpected local variable or method, expecting `then' or ';' or '\n'
+if true puts 'Hello, World!' end
+ ^~~~
+20:1: syntax error, unexpected `end', expecting end-of-input
+...f true puts 'Hello, World!' end
+
二つ目のメッセージは無視して一つ目を読むと、then か ; か改行が来るはずのところ変数だかメソッドだかが現れたことによりエラーとなっているようだ。
@@ -160,8 +164,9 @@
ポイントは改行が then (や ;) の代わりとなることである。true の後に改行を入れてみる。
if true
-puts 'Hello, World!' end
+ if true
+puts 'Hello, World!' end
+
無事 Hello, World! と出力されるようになった。
@@ -173,22 +178,25 @@
なぜ then や ; や改行 (以下 「then 等」) が必要なのだろうか。次の例を見てほしい:
if a b end
+ if a b end
+
then も ; も改行もないのでエラーになるが、これは条件式がどこまで続いているのかわからないためだ。この例は二通りに解釈できる。
# a という変数かメソッドの評価結果が truthy なら b という変数かメソッドを評価
-if a then
-b
-end
+ # a という変数かメソッドの評価結果が truthy なら b という変数かメソッドを評価
+if a then
+b
+end
+
# a というメソッドに b という変数かメソッドの評価結果を渡して呼び出し、
-# その結果が truthy なら何もしない
-if a(b) then
-end
+ # a というメソッドに b という変数かメソッドの評価結果を渡して呼び出し、
+# その結果が truthy なら何もしない
+if a(b) then
+end
+
then 等はこの曖昧性を排除するためにあり、条件式は if から then 等までの間にある、ということを明確にする。 C系の if 後に来る (/) や、Python の :、Rust/Go/Swift などの { も同じ役割を持つ。
@@ -209,39 +217,41 @@
p_case_body : keyword_in
-{
- SET_LEX_STATE(EXPR_BEG|EXPR_LABEL);
- p->command_start = FALSE;
- $<ctxt>1 = p->ctxt;
- p->ctxt.in_kwarg = 1;
- $<tbl>$ = push_pvtbl(p);
-}
-{
- $<tbl>$ = push_pktbl(p);
-}
-p_top_expr then
-{
- pop_pktbl(p, $<tbl>3);
- pop_pvtbl(p, $<tbl>2);
- p->ctxt.in_kwarg = $<ctxt>1.in_kwarg;
-}
-compstmt
-p_cases
-{
- /*%%%*/
- $$ = NEW_IN($4, $7, $8, &@$);
- /*% %*/
- /*% ripper: in!($4, $7, escape_Qundef($8)) %*/
-}
-;
+ p_case_body : keyword_in
+{
+ SET_LEX_STATE(EXPR_BEG|EXPR_LABEL);
+ p->command_start = FALSE;
+ $<ctxt>1 = p->ctxt;
+ p->ctxt.in_kwarg = 1;
+ $<tbl>$ = push_pvtbl(p);
+}
+{
+ $<tbl>$ = push_pktbl(p);
+}
+p_top_expr then
+{
+ pop_pktbl(p, $<tbl>3);
+ pop_pvtbl(p, $<tbl>2);
+ p->ctxt.in_kwarg = $<ctxt>1.in_kwarg;
+}
+compstmt
+p_cases
+{
+ /*%%%*/
+ $$ = NEW_IN($4, $7, $8, &@$);
+ /*% %*/
+ /*% ripper: in!($4, $7, escape_Qundef($8)) %*/
+}
+;
+
簡略版:
p_case_body : keyword_in p_top_expr then compstmt p_cases
-;
+ p_case_body : keyword_in p_top_expr then compstmt p_cases
+;
+
ここで、keyword_in は文字通り in、p_top_expr はいわゆるパターン、then は then キーワードのことではなく、この記事で then 等と呼んでいるもの、つまり then キーワード、;、改行のいずれかである。
@@ -250,36 +260,38 @@
これにより、case - when による従来の構文と同じように、then 等をパターンの後ろに挿入すればよいことがわかった。つまり次の3通りのいずれかになる:
case x
-in 1 then a
-in 2 then b
-in 3 then c
-end
-
-case x
-in 1
- a
-in 2
- b
-in 3
- c
-end
-
-case x
-in 1; a
-in 2; b
-in 3; c
-end
+ case x
+in 1 then a
+in 2 then b
+in 3 then c
+end
+
+case x
+in 1
+ a
+in 2
+ b
+in 3
+ c
+end
+
+case x
+in 1; a
+in 2; b
+in 3; c
+end
+
ところで、p_top_expr には if による guard clause が書けるので、その場合は if - then と似たような見た目になる。
case x
-in 0 then a
-in n if n < 0 then b
-in n then c
-end
+ case x
+in 0 then a
+in n if n < 0 then b
+in n then c
+end
+
#![allow(non_camel_case_types)]
-#![allow(dead_code)]
-
-struct bool;
-struct char;
-struct i8;
-struct i16;
-struct i32;
-struct i64;
-struct i128;
-struct isize;
-struct u8;
-struct u16;
-struct u32;
-struct u64;
-struct u128;
-struct usize;
-struct f32;
-struct f64;
-struct str;
+ #![allow(non_camel_case_types)]
+#![allow(dead_code)]
+
+struct bool;
+struct char;
+struct i8;
+struct i16;
+struct i32;
+struct i64;
+struct i128;
+struct isize;
+struct u8;
+struct u16;
+struct u32;
+struct u64;
+struct u128;
+struct usize;
+struct f32;
+struct f64;
+struct str;
+
では、普段単に bool と書いたとき、この bool は一体どこから来ているのか。rustc のソースを追ってみた。
@@ -134,23 +135,25 @@
rustc はセルフホストされている (= rustc 自身が Rust で書かれている) ので、bool や char などで適当に検索をかけてもノイズが多すぎて話にならない。しかし、お誂え向きなことに i128/u128 というコンパイラ自身が使うことがなさそうな型が存在するのでこれを使って git grep してみる。
$ git grep "\bi128\b" | wc # i128
-165 1069 15790
-
-$ git grep "\bu128\b" | wc # u128
-293 2127 26667
-
-$ git grep "\bbool\b" | wc # cf. bool の結果
-3563 23577 294659
+ $ git grep "\bi128\b" | wc # i128
+165 1069 15790
+
+$ git grep "\bu128\b" | wc # u128
+293 2127 26667
+
+$ git grep "\bbool\b" | wc # cf. bool の結果
+3563 23577 294659
+
165 程度であれば探すことができそうだ。今回は、クレート名を見ておおよその当たりをつけた。
$ git grep "\bi128\b"
-...
-rustc_resolve/src/lib.rs: table.insert(sym::i128, Int(IntTy::I128));
-...
+ $ git grep "\bi128\b"
+...
+rustc_resolve/src/lib.rs: table.insert(sym::i128, Int(IntTy::I128));
+...
+
rustc_resolve というのはいかにも名前解決を担いそうなクレート名である。該当箇所を見てみる。
@@ -159,72 +162,75 @@
/// Interns the names of the primitive types.
-///
-/// All other types are defined somewhere and possibly imported, but the primitive ones need
-/// special handling, since they have no place of origin.
-struct PrimitiveTypeTable {
- primitive_types: FxHashMap<Symbol, PrimTy>,
-}
-
-impl PrimitiveTypeTable {
- fn new() -> PrimitiveTypeTable {
- let mut table = FxHashMap::default();
-
- table.insert(sym::bool, Bool);
- table.insert(sym::char, Char);
- table.insert(sym::f32, Float(FloatTy::F32));
- table.insert(sym::f64, Float(FloatTy::F64));
- table.insert(sym::isize, Int(IntTy::Isize));
- table.insert(sym::i8, Int(IntTy::I8));
- table.insert(sym::i16, Int(IntTy::I16));
- table.insert(sym::i32, Int(IntTy::I32));
- table.insert(sym::i64, Int(IntTy::I64));
- table.insert(sym::i128, Int(IntTy::I128));
- table.insert(sym::str, Str);
- table.insert(sym::usize, Uint(UintTy::Usize));
- table.insert(sym::u8, Uint(UintTy::U8));
- table.insert(sym::u16, Uint(UintTy::U16));
- table.insert(sym::u32, Uint(UintTy::U32));
- table.insert(sym::u64, Uint(UintTy::U64));
- table.insert(sym::u128, Uint(UintTy::U128));
- Self { primitive_types: table }
- }
-}
+ /// Interns the names of the primitive types.
+///
+/// All other types are defined somewhere and possibly imported, but the primitive ones need
+/// special handling, since they have no place of origin.
+struct PrimitiveTypeTable {
+ primitive_types: FxHashMap<Symbol, PrimTy>,
+}
+
+impl PrimitiveTypeTable {
+ fn new() -> PrimitiveTypeTable {
+ let mut table = FxHashMap::default();
+
+ table.insert(sym::bool, Bool);
+ table.insert(sym::char, Char);
+ table.insert(sym::f32, Float(FloatTy::F32));
+ table.insert(sym::f64, Float(FloatTy::F64));
+ table.insert(sym::isize, Int(IntTy::Isize));
+ table.insert(sym::i8, Int(IntTy::I8));
+ table.insert(sym::i16, Int(IntTy::I16));
+ table.insert(sym::i32, Int(IntTy::I32));
+ table.insert(sym::i64, Int(IntTy::I64));
+ table.insert(sym::i128, Int(IntTy::I128));
+ table.insert(sym::str, Str);
+ table.insert(sym::usize, Uint(UintTy::Usize));
+ table.insert(sym::u8, Uint(UintTy::U8));
+ table.insert(sym::u16, Uint(UintTy::U16));
+ table.insert(sym::u32, Uint(UintTy::U32));
+ table.insert(sym::u64, Uint(UintTy::U64));
+ table.insert(sym::u128, Uint(UintTy::U128));
+ Self { primitive_types: table }
+ }
+}
+
これは初めに列挙したプリミティブ型の一覧と一致している。doc comment にも、
All other types are defined somewhere and possibly imported, but the
-primitive ones need special handling, since they have no place of
-origin.
+ All other types are defined somewhere and possibly imported, but the
+primitive ones need special handling, since they have no place of
+origin.
+
とある。次はこの struct の使用箇所を追う。追うと言っても使われている箇所は次の一箇所しかない。なお説明に不要な箇所は大きく削っている。
/// This resolves the identifier `ident` in the namespace `ns` in the current lexical scope.
-/// (略)
-fn resolve_ident_in_lexical_scope(
- &mut self,
- mut ident: Ident,
- ns: Namespace,
- // (略)
-) -> Option<LexicalScopeBinding<'a>> {
- // (略)
-
- if ns == TypeNS {
- if let Some(prim_ty) = self.primitive_type_table.primitive_types.get(&ident.name) {
- let binding =
- (Res::PrimTy(*prim_ty), ty::Visibility::Public, DUMMY_SP, ExpnId::root())
- .to_name_binding(self.arenas);
- return Some(LexicalScopeBinding::Item(binding));
- }
- }
-
- None
-}
+ /// This resolves the identifier `ident` in the namespace `ns` in the current lexical scope.
+/// (略)
+fn resolve_ident_in_lexical_scope(
+ &mut self,
+ mut ident: Ident,
+ ns: Namespace,
+ // (略)
+) -> Option<LexicalScopeBinding<'a>> {
+ // (略)
+
+ if ns == TypeNS {
+ if let Some(prim_ty) = self.primitive_type_table.primitive_types.get(&ident.name) {
+ let binding =
+ (Res::PrimTy(*prim_ty), ty::Visibility::Public, DUMMY_SP, ExpnId::root())
+ .to_name_binding(self.arenas);
+ return Some(LexicalScopeBinding::Item(binding));
+ }
+ }
+
+ None
+}
+
関数名や doc comment が示している通り、この関数は識別子 (identifier, ident) を現在のレキシカルスコープ内で解決 (resolve) する。if ns == TypeNS のブロック内では、primitive_type_table (上記の PrimitiveTypeTable::new() で作られた変数) に含まれている識別子 (bool、i32 など) かどうか判定し、そうであればそれに紐づけられたプリミティブ型を返している。
@@ -239,13 +245,14 @@
動作がわかったところで、例として次のコードを考える。
#![allow(non_camel_case_types)]
-
-struct bool;
-
-fn main() {
- let _: bool = bool;
-}
+ #![allow(non_camel_case_types)]
+
+struct bool;
+
+fn main() {
+ let _: bool = bool;
+}
+
ここで main() の bool は struct bool として解決される。なぜなら、プリミティブ型の判定をする前に bool という名前の別の型が見つかるからだ。
diff --git a/services/nuldoc/public/blog/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre/index.html b/services/nuldoc/public/blog/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre/index.html
index cafadae1..275086e6 100644
--- a/services/nuldoc/public/blog/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre/index.html
+++ b/services/nuldoc/public/blog/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre/index.html
@@ -146,8 +146,9 @@
{"BufAdd", EVENT_BUFADD},
-{"BufCreate", EVENT_BUFADD},
+ {"BufAdd", EVENT_BUFADD},
+{"BufCreate", EVENT_BUFADD},
+
https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L95-L97 @@ -156,9 +157,10 @@
{"BufRead", EVENT_BUFREADPOST},
-{"BufReadCmd", EVENT_BUFREADCMD},
-{"BufReadPost", EVENT_BUFREADPOST},
+ {"BufRead", EVENT_BUFREADPOST},
+{"BufReadCmd", EVENT_BUFREADCMD},
+{"BufReadPost", EVENT_BUFREADPOST},
+
https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L103-L105 @@ -167,9 +169,10 @@
{"BufWrite", EVENT_BUFWRITEPRE},
-{"BufWritePost", EVENT_BUFWRITEPOST},
-{"BufWritePre", EVENT_BUFWRITEPRE},
+ {"BufWrite", EVENT_BUFWRITEPRE},
+{"BufWritePost", EVENT_BUFWRITEPOST},
+{"BufWritePre", EVENT_BUFWRITEPRE},
+
aliases = {
- BufCreate = 'BufAdd',
- BufRead = 'BufReadPost',
- BufWrite = 'BufWritePre',
- FileEncoding = 'EncodingChanged',
-},
+ aliases = {
+ BufCreate = 'BufAdd',
+ BufRead = 'BufReadPost',
+ BufWrite = 'BufWritePre',
+ FileEncoding = 'EncodingChanged',
+},
+
ところで、上では取り上げなかった FileEncoding だが、これは :help FileEncoding にしっかりと書いてある。
@@ -198,9 +202,10 @@
*FileEncoding*
-FileEncoding Obsolete. It still works and is equivalent
- to |EncodingChanged|.
+ *FileEncoding*
+FileEncoding Obsolete. It still works and is equivalent
+ to |EncodingChanged|.
+
" License: Public Domain
-
-command! -bar -range=%
- \ Reverse
- \ keeppatterns <line1>,<line2>g/^/m<line1>-1
+ " License: Public Domain
+
+command! -bar -range=%
+ \ Reverse
+ \ keeppatterns <line1>,<line2>g/^/m<line1>-1
+
:g/^/m0 は全ての行を入れ替えるが、:N,Mg/^/mN-1 とすることで N行目から M行目を処理範囲とするよう拡張できる。手でこれを入力するわけにはいかないので、次のようなコマンドを用意する。
command! -bar -range=%
- \ Reverse
- \ <line1>,<line2>g/^/m<line1>-1
+ command! -bar -range=%
+ \ Reverse
+ \ <line1>,<line2>g/^/m<line1>-1
+
これは望みの動作をするが、実際に実行してみると全行がハイライトされてしまう。次節で詳細を述べる。
@@ -200,13 +202,14 @@
前述した :Reverse コマンドの定義を少し変えて、次のようにする:
function! s:reverse_lines(from, to) abort
- execute printf("%d,%dg/^/m%d", a:from, a:to, a:from - 1)
-endfunction
-
-command! -bar -range=%
- \ Reverse
- \ call <SID>reverse_lines(<line1>, <line2>)
+ function! s:reverse_lines(from, to) abort
+ execute printf("%d,%dg/^/m%d", a:from, a:to, a:from - 1)
+endfunction
+
+command! -bar -range=%
+ \ Reverse
+ \ call <SID>reverse_lines(<line1>, <line2>)
+
実行しているコマンドが変わったわけではないが、関数呼び出しを経由するようにした。これだけで前述の問題が解決する。 @@ -253,9 +256,10 @@
command! -bar -range=%
- \ Reverse
- \ keeppatterns <line1>,<line2>g/^/m<line1>-1
+ command! -bar -range=%
+ \ Reverse
+ \ keeppatterns <line1>,<line2>g/^/m<line1>-1
+
まさにこのための Exコマンド、:keeppatterns が存在する。:keeppatterns {command} のように使い、読んで字の如く、後ろに続く Exコマンドを「現在の検索パターンを保ったまま」実行する。はるかに分かりやすく意図を表現できる。
diff --git a/services/nuldoc/public/blog/posts/2022-04-09/phperkaigi-2022-tokens/index.html b/services/nuldoc/public/blog/posts/2022-04-09/phperkaigi-2022-tokens/index.html
index b3637d00..c5404c5e 100644
--- a/services/nuldoc/public/blog/posts/2022-04-09/phperkaigi-2022-tokens/index.html
+++ b/services/nuldoc/public/blog/posts/2022-04-09/phperkaigi-2022-tokens/index.html
@@ -166,75 +166,76 @@
<?php
-
-declare(strict_types=0O1);
-
-namespace Dgcircus\PHPerKaigi\Y2022;
-
-/**
- * @todo
- * Run this program to acquire a PHPer token.
- */
-
-https://creativecommons.org/publicdomain/zero/1.0/
-
-\error_reporting(~+!'We are hiring!');
-
-$z = fn($f) => (fn($x) => $f(fn(...$xs) => $x($x)(...$xs)))(fn($x) => $f(fn(...$xs) => $x($x)(...$xs)));
-$id = \spl_object_id(...);
-$put = fn($c) => \printf('%c', $c);
-$mm = fn($p, $n) => new \ArrayObject(\array_fill(+!![], $n, $p));
-
-$👉 = fn($m, $p, $b, $e, $mp, $pc) => [++$mp, ++$pc];
-$👈 = fn($m, $p, $b, $e, $mp, $pc) => [--$mp, ++$pc];
-$👍 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, ++$m[$mp]];
-$👎 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, --$m[$mp]];
-$📝 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, $put($m[$mp])];
-$🤡 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
- +!![] => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
- $b => $loop(++$pc, ++$n),
- $e => $n === +!![] ? ++$pc : $loop(++$pc, --$n),
- default => $loop(++$pc, $n),
- })($pc, -![])],
- default => [$mp, ++$pc],
-};
-$🎪 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
- +!![] => [$mp, ++$pc],
- default => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
- $e => $loop(--$pc, ++$n),
- $b => $n === +!![] ? $pc+![] : $loop(--$pc, --$n),
- default => $loop(--$pc, $n),
- })($pc, -![])],
-};
-$🐘 = fn($p) => $z(fn($loop) => fn($m, $p, $b, $e, $mp, $pc) =>
- isset($p[$pc]) && $loop($m, $p, $b, $e, ...($p[$pc]($m, $p, $b, $e, $mp, $pc)))
-)($mm(+!![], +(![].![])), $p, $id($🤡), $id($🎪), +!![], +!![]);
-
-$🐘([
- $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
- $🤡,
- $👉, $👍, $👍, $👍,
- $👉, $👍, $👍, $👍, $👍, $👍,
- $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
- $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
- $👈, $👈, $👈, $👈, $👎,
- $🎪,
- $👉, $👍, $👍, $👍, $👍, $👍, $📝,
- $👎, $👎, $📝,
- $👉, $👎, $👎, $👎, $📝,
- $👉, $👎, $👎, $👎, $📝,
- $👎, $👎, $📝,
- $👎, $📝,
- $👈, $📝,
- $👉, $👉, $👎, $👎, $📝,
- $👍, $👍, $👍, $👍, $👍, $👍, $👍, $📝,
- $👈, $👎, $👎, $👎, $👎, $📝,
- $👈, $📝,
- $👉, $👍, $👍, $📝,
- $👉, $👎, $📝,
- $👈, $📝,
-]);
+ <?php
+
+declare(strict_types=0O1);
+
+namespace Dgcircus\PHPerKaigi\Y2022;
+
+/**
+ * @todo
+ * Run this program to acquire a PHPer token.
+ */
+
+https://creativecommons.org/publicdomain/zero/1.0/
+
+\error_reporting(~+!'We are hiring!');
+
+$z = fn($f) => (fn($x) => $f(fn(...$xs) => $x($x)(...$xs)))(fn($x) => $f(fn(...$xs) => $x($x)(...$xs)));
+$id = \spl_object_id(...);
+$put = fn($c) => \printf('%c', $c);
+$mm = fn($p, $n) => new \ArrayObject(\array_fill(+!![], $n, $p));
+
+$👉 = fn($m, $p, $b, $e, $mp, $pc) => [++$mp, ++$pc];
+$👈 = fn($m, $p, $b, $e, $mp, $pc) => [--$mp, ++$pc];
+$👍 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, ++$m[$mp]];
+$👎 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, --$m[$mp]];
+$📝 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, $put($m[$mp])];
+$🤡 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
+ +!![] => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
+ $b => $loop(++$pc, ++$n),
+ $e => $n === +!![] ? ++$pc : $loop(++$pc, --$n),
+ default => $loop(++$pc, $n),
+ })($pc, -![])],
+ default => [$mp, ++$pc],
+};
+$🎪 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) {
+ +!![] => [$mp, ++$pc],
+ default => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) {
+ $e => $loop(--$pc, ++$n),
+ $b => $n === +!![] ? $pc+![] : $loop(--$pc, --$n),
+ default => $loop(--$pc, $n),
+ })($pc, -![])],
+};
+$🐘 = fn($p) => $z(fn($loop) => fn($m, $p, $b, $e, $mp, $pc) =>
+ isset($p[$pc]) && $loop($m, $p, $b, $e, ...($p[$pc]($m, $p, $b, $e, $mp, $pc)))
+)($mm(+!![], +(![].![])), $p, $id($🤡), $id($🎪), +!![], +!![]);
+
+$🐘([
+ $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $🤡,
+ $👉, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍,
+ $👈, $👈, $👈, $👈, $👎,
+ $🎪,
+ $👉, $👍, $👍, $👍, $👍, $👍, $📝,
+ $👎, $👎, $📝,
+ $👉, $👎, $👎, $👎, $📝,
+ $👉, $👎, $👎, $👎, $📝,
+ $👎, $👎, $📝,
+ $👎, $📝,
+ $👈, $📝,
+ $👉, $👉, $👎, $👎, $📝,
+ $👍, $👍, $👍, $👍, $👍, $👍, $👍, $📝,
+ $👈, $👎, $👎, $👎, $👎, $📝,
+ $👈, $📝,
+ $👉, $👍, $👍, $📝,
+ $👉, $👎, $📝,
+ $👈, $📝,
+]);
+
この問題は、単に適切なバージョンの PHP で動かせばトークンが得られる。 @@ -259,28 +260,29 @@ なお、brainf*ck プログラムを普通の書き方で書くと、次のようになる。
+ + + + + + + + + +
-[
- > + + +
- > + + + + +
- > + + + + + + + + + + + +
- > + + + + + + + + + +
- < < < < -
-]
-> + + + + + .
-- - .
-> - - - .
-> - - - .
-- - .
-- .
-< .
-> > - - .
-+ + + + + + + .
-< - - - - .
-< .
-> + + .
-> - .
-< .
+ + + + + + + + + + +
+[
+ > + + +
+ > + + + + +
+ > + + + + + + + + + + + +
+ > + + + + + + + + + +
+ < < < < -
+]
+> + + + + + .
+- - .
+> - - - .
+> - - - .
+- - .
+- .
+< .
+> > - - .
++ + + + + + + .
+< - - - - .
+< .
+> + + .
+> - .
+< .
+
実行結果はこちら: https://ideone.com/22VWmb @@ -336,7 +338,8 @@ ソースコードのライセンスを示したこの部分だが、
https://creativecommons.org/publicdomain/zero/1.0/
+ https://creativecommons.org/publicdomain/zero/1.0/
+
完全に合法な PHP のコードである。 https: 部分はラベル、// 以降は行コメントになっている。
@@ -348,11 +351,12 @@
ソースコード中に、ほとんど数値リテラルが書かれていないことにお気づきだろうか。PHP では、型変換を利用することで任意の整数を作り出すことができる。
assert(0 === +!![]);
-assert(1 === +![]);
-assert(2 === ![]+![]);
-assert(3 === ![]+![]+![]);
-assert(10 === +(![].+!![]));
+ assert(0 === +!![]);
+assert(1 === +![]);
+assert(2 === ![]+![]);
+assert(3 === ![]+![]+![]);
+assert(10 === +(![].+!![]));
+
[] に ! を適用すると true が返ってくる。それに + を適用すると、bool から int ヘの型変換が走り、1 が生成される。10 はさらにトリッキーだ。まず 1 と 0 を作り、. で文字列として結合する ('10')。これに + を適用すると、string から int への型変換が走り、10 が生まれる (コード量に頓着しないなら、1 を 10 個足し合わせてももちろん 10 が作れる)。
@@ -390,40 +394,41 @@
<?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";
-}
+ <?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 を特定する必要がある。
@@ -437,33 +442,38 @@
まずはソースコードを読んでいく。
$token = [
- // 略
-];
+ $token = [
+ // 略
+];
+
数値からなる $token があり、各要素をループしている。
$x = $x ^ N;
+ $x = $x ^ N;
+
まずは排他的論理和 (xor) を取り、
$x = sprintf('%025b', $x);
+ $x = sprintf('%025b', $x);
+
二進数に変換して、
$x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+ $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
+
0 を空白に、1 を # にし、
$x = implode("\n", str_split($x, length: 5));
+ $x = implode("\n", str_split($x, length: 5));
+
5文字ごとに区切ったあと、改行で結合している。
@@ -501,49 +511,52 @@
N は高々
assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
+ assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
+
なのでブルートフォースしてもよいが、ここではブルートフォースしない方法を紹介する。
<?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
+
+$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
-
-$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";
+ <?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 が得られる。
@@ -559,41 +572,43 @@
<?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));
+ <?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));
+
コメントにもあるとおり、次のようにして実行すれば答えがでてくる。
$ php toquine.php | php | php | php | ...
+ $ php toquine.php | php | php | php | ...
+
実際にはもう少しパイプで繋げなければならない。 diff --git a/services/nuldoc/public/blog/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/index.html b/services/nuldoc/public/blog/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/index.html index 67b601f7..480907c4 100644 --- a/services/nuldoc/public/blog/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/index.html +++ b/services/nuldoc/public/blog/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/index.html @@ -81,7 +81,8 @@ こんなものを作った。
$ term-banner 'Hello, World!' 'こんにちは、' '世界!'
+ $ term-banner 'Hello, World!' 'こんにちは、' '世界!'
+
diff --git a/services/nuldoc/public/blog/posts/2022-08-27/php-conference-okinawa-code-golf/index.html b/services/nuldoc/public/blog/posts/2022-08-27/php-conference-okinawa-code-golf/index.html
index c9c27912..8a4ab91f 100644
--- a/services/nuldoc/public/blog/posts/2022-08-27/php-conference-okinawa-code-golf/index.html
+++ b/services/nuldoc/public/blog/posts/2022-08-27/php-conference-okinawa-code-golf/index.html
@@ -144,7 +144,8 @@
書いたものがこちら:
[<?php $n=$argv[1];foreach([1e4,5e3,2e3,1e3,500,100,50,10,5,1]as$x)for(;$n>=$x;$n-=$x)$r[]=$x;echo implode(', ',$r??[]);?>]
+ [<?php $n=$argv[1];foreach([1e4,5e3,2e3,1e3,500,100,50,10,5,1]as$x)for(;$n>=$x;$n-=$x)$r[]=$x;echo implode(', ',$r??[]);?>]
+
しめて 123 バイトとなった (末尾改行を含めずにカウント)。 @@ -153,15 +154,16 @@ こちらは改行とスペースを追加したバージョン:
[<?php
-
-$n = $argv[1];
-foreach ([1e4, 5e3, 2e3, 1e3, 500, 100, 50, 10, 5, 1] as $x)
- for (; $n >= $x; $n -= $x)
- $r[] = $x;
-echo implode(', ', $r ?? []);
-
-?>]
+ [<?php
+
+$n = $argv[1];
+foreach ([1e4, 5e3, 2e3, 1e3, 500, 100, 50, 10, 5, 1] as $x)
+ for (; $n >= $x; $n -= $x)
+ $r[] = $x;
+echo implode(', ', $r ?? []);
+
+?>]
+
#\
-i\
-n\
-c\
-l\
-u\
-d\
-e\
-<\
-s\
-t\
-d\
-i\
-o\
-.\
-h\
->\
-/*
-*/
-i\
-n\
-t\
-/*
-*/
-m\
-a\
-i\
-n(
-){
-f\
-o\
-r(
-i\
-n\
-t\
-/*
-*/
-i=
-1;
-i<
-1\
-0\
-0;
-i\
-+\
-+)
-if
-(i
-%\
-15
-==
-0)
-p\
-r\
-i\
-n\
-t\
-f(
-"\
-F\
-i\
-z\
-z\
-B\
-u\
-z\
-z\
-%\
-c\
-",
-10
-);
-
-/* あとは同じように普通のプログラムを変形するだけなので省略 */
+ #\
+i\
+n\
+c\
+l\
+u\
+d\
+e\
+<\
+s\
+t\
+d\
+i\
+o\
+.\
+h\
+>\
+/*
+*/
+i\
+n\
+t\
+/*
+*/
+m\
+a\
+i\
+n(
+){
+f\
+o\
+r(
+i\
+n\
+t\
+/*
+*/
+i=
+1;
+i<
+1\
+0\
+0;
+i\
++\
++)
+if
+(i
+%\
+15
+==
+0)
+p\
+r\
+i\
+n\
+t\
+f(
+"\
+F\
+i\
+z\
+z\
+B\
+u\
+z\
+z\
+%\
+c\
+",
+10
+);
+
+/* あとは同じように普通のプログラムを変形するだけなので省略 */
+
バックスラッシュを使った行継続がトークンを区切らない、というのがポイントだ。
@@ -273,10 +274,11 @@
また、2文字だと文字列がまともに書けないのも辛い。'' だけで2文字使うので、「1文字の文字列リテラル」というものを書くことができない。PHP では文字列リテラル中に生の改行が書けるので
$a
-='
-a'
-;;
+ $a
+='
+a'
+;;
+
とすると $a は "\na" になるのだが、余計な改行が入ってしまう。
@@ -293,11 +295,12 @@
まずは普通に書くとしよう。
<?php
-
-for ($i = 1; $i < 100; $i++) {
- echo (($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n";
-}
+ <?php
+
+for ($i = 1; $i < 100; $i++) {
+ echo (($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n";
+}
+
素直に書いた fizzbuzz とは言い難いが、このくらいは普通だということにしておかないと、この先がやっていられないので許してほしい。
@@ -309,14 +312,15 @@
for は、3文字もある長いキーワードである。こんなものは使えない。array_ 系の関数を使って、適当に置き換えるとしよう。
<?php
-
-$s = range(1, 100);
-array_walk(
-$s,
-fn($i) =>
-printf((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"),
-);
+ <?php
+
+$s = range(1, 100);
+array_walk(
+$s,
+fn($i) =>
+printf((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"),
+);
+
array_walk や range、printf といった for よりも長いトークンが現れてしまったが、これは次節で直すことにする。なお、echo は文 (statement) であり式 (expression) ではないので、式である printf に置き換えた。
@@ -328,18 +332,19 @@
range、array_walk、printf は長すぎるのでどうにかせねばならない。ここで、PHP の可変関数を使う。可変関数とは、関数名が文字列として入った変数を経由して、関数を呼び出す機能である。
<?php
-
-$r = 'range';
-$w = 'array_walk';
-$p = 'printf';
-
-$s = $r(1, 100);
-$w(
-$s,
-fn($i) =>
-$p((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"),
-);
+ <?php
+
+$r = 'range';
+$w = 'array_walk';
+$p = 'printf';
+
+$s = $r(1, 100);
+$w(
+$s,
+fn($i) =>
+$p((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"),
+);
+
これで関数を呼び出している所は短くなった。では、$r や $w や $p、また 'Fizz' や 'Buzz' はどうやって 1 行 2 文字に収めるのか。次のテクニックへ移ろう。
@@ -361,26 +366,28 @@
というルールがない場合、「未定義の定数が評価された場合、その定数の名前が値になる」という PHP 7.x までの仕様が利用できる。例えば、 Fizz という文字列が欲しければ、次のようにする。
$f
-=F
-.i
-.z
-.z
-;;
+ $f
+=F
+.i
+.z
+.z
+;;
+
こうして簡単に文字列を作れる。なお、この仕様は 7.x 時点でも警告を受けるので、@ 演算子を使って抑制してやるとよい。
$f
-=@
-F.
-@i
-.#
-@z
-.#
-@z
-;;
+ $f
+=@
+F.
+@i
+.#
+@z
+.#
+@z
+;;
+
むしろ、このことがわかっていたからこそ PHP 8.x での動作を要件に課したところがある。
@@ -395,66 +402,70 @@
ずばり、文字列同士のビット演算を使う。PHP では、文字列同士でビット演算 (&、|、^) をした場合、文字列の各バイトごとに指定したビット演算がなされ、それを結合したものが演算結果となる。
$a = "12345";
-$b = "world";
-
-// $a ^ $b は次のコードと同じ
-$result = '';
-for ($i = 0; $i < min(strlen($a), strlen($b)); $i++) {
-$result .= $a[$i] ^ $b[$i];
-}
-
-echo $result;
-// => F]AXQ
+ $a = "12345";
+$b = "world";
+
+// $a ^ $b は次のコードと同じ
+$result = '';
+for ($i = 0; $i < min(strlen($a), strlen($b)); $i++) {
+$result .= $a[$i] ^ $b[$i];
+}
+
+echo $result;
+// => F]AXQ
+
これを踏まえ、次のコードを見てみよう。
$x = "x\nOm\n";
-$y = "\nk!\no";
-$r = $x ^ $y;
-echo "$r\n";
+ $x = "x\nOm\n";
+$y = "\nk!\no";
+$r = $x ^ $y;
+echo "$r\n";
+
実行すると、range が表示される。さて、PHP では文字列リテラル中に生の改行を直接書いてもよいのだった (「主な障害」の節を参照のこと)。書きかえてみよう。
$x
-='x
-Om
-';
-$y
-='
-k!
-o'
-;
-
-$r = $x ^ $y;
-echo "$r\n";
+ $x
+='x
+Om
+';
+$y
+='
+k!
+o'
+;
+
+$r = $x ^ $y;
+echo "$r\n";
+
さらに # を使って適当に調整すると、次のようになる。
$x
-=#
-'x
-Om
-';
-$y
-='
-k!
-o'
-;#
-$r
-=#
-$x
-^#
-$y
-;#
-
-echo "$r\n";
+ $x
+=#
+'x
+Om
+';
+$y
+='
+k!
+o'
+;#
+$r
+=#
+$x
+^#
+$y
+;#
+
+echo "$r\n";
+
1行あたり2文字で、range という文字列を生成することに成功した。他の必要な文字列にも、同様の処理をほどこす。
@@ -470,155 +481,156 @@
完成したものがこちら。
<?php
-
-$x
-=#
-'i
-S'
-;;
-$y
-='
-b!
-';
-$c
-=#
-$x
-^#
-$y
-;#
-$x
-=#
-'x
-Om
-';
-$y
-='
-k!
-o'
-;#
-$r
-=#
-$x
-^#
-$y
-;#
-$x
-=#
-'k
-Sk
-~}
-Ma
-';
-$y
-='
-x!
-s!
-k!
-';
-$w
-=#
-$x
-^#
-$y
-;#
-$x
-=#
-'z
-Hd
-G'
-;#
-$y
-='
-x!
-~!
-';
-$p
-=#
-$x
-^#
-$y
-;#
-$x
-=#
-'L
-[p
-';
-$y
-='
-c!
-';
-$f
-=#
-$x
-^#
-$y
-;#
-$x
-=#
-'H
-[p
-';
-$y
-='
-_!
-';
-$b
-=#
-$x
-^#
-$y
-;#
-$b
-[1
-]=
-$c
-(#
-13
-*9
-);
-$s
-=#
-$r
-(1
-,(
-10
-**
-2)
-);
-$w
-(#
-$s
-,#
-fn
-(#
-$i
-)#
-=>
-$p
-((
-(#
-$i
-%3
-?#
-''
-:#
-$f
-).
-(#
-$i
-%5
-?#
-''
-:#
-$b
-)?
-:#
-$i
-)#
-.'
-')
-);
+ <?php
+
+$x
+=#
+'i
+S'
+;;
+$y
+='
+b!
+';
+$c
+=#
+$x
+^#
+$y
+;#
+$x
+=#
+'x
+Om
+';
+$y
+='
+k!
+o'
+;#
+$r
+=#
+$x
+^#
+$y
+;#
+$x
+=#
+'k
+Sk
+~}
+Ma
+';
+$y
+='
+x!
+s!
+k!
+';
+$w
+=#
+$x
+^#
+$y
+;#
+$x
+=#
+'z
+Hd
+G'
+;#
+$y
+='
+x!
+~!
+';
+$p
+=#
+$x
+^#
+$y
+;#
+$x
+=#
+'L
+[p
+';
+$y
+='
+c!
+';
+$f
+=#
+$x
+^#
+$y
+;#
+$x
+=#
+'H
+[p
+';
+$y
+='
+_!
+';
+$b
+=#
+$x
+^#
+$y
+;#
+$b
+[1
+]=
+$c
+(#
+13
+*9
+);
+$s
+=#
+$r
+(1
+,(
+10
+**
+2)
+);
+$w
+(#
+$s
+,#
+fn
+(#
+$i
+)#
+=>
+$p
+((
+(#
+$i
+%3
+?#
+''
+:#
+$f
+).
+(#
+$i
+%5
+?#
+''
+:#
+$b
+)?
+:#
+$i
+)#
+.'
+')
+);
+
shell_exec 関数と等価である。さて、PHP ではバックスラッシュによる行継続が使えないと書いたが、シェルでは使える (当然だが、呼び出されるシェルに依存する。Bash なら大丈夫だろう。知らんけど)。
<?php
-
-printf(`
-e\
-c\
-h\
-o\
-\
-1\
-2\
-3\
-`);
+ <?php
+
+printf(`
+e\
+c\
+h\
+o\
+\
+1\
+2\
+3\
+`);
+
なお、ここでは簡単のため出力に printf をそのまま使っているが、実際には printf という文字列を合成して可変関数で呼び出す。
@@ -669,56 +682,59 @@
もうこれ以上は不可能だと思っていたのだが、この記事の執筆中に解決する方法を思いついたので載せておく。
<?php
-
-$c = 'chr';
-
-${
-'_
-'}
-=#
-$c
-(#
-32
-).
-$c
-(#
-92
-);
-
-printf(`
-e\
-c\
-h\
-o\
-${
-'_
-'}
-1\
-2\
-3\
-`);
+ <?php
+
+$c = 'chr';
+
+${
+'_
+'}
+=#
+$c
+(#
+32
+).
+$c
+(#
+92
+);
+
+printf(`
+e\
+c\
+h\
+o\
+${
+'_
+'}
+1\
+2\
+3\
+`);
+
先程と同じく、chr や printf を生成する部分は長くなるので省いた。
${
-'_
-'}
+ ${
+'_
+'}
+
は変数で、中にはスペースとエスケープが入っている (chr(32) . chr(92))。シェルに渡されている文字列は次のようになる。
e\
-c\
-h\
-o\
-\
-1\
-2\
-3\
+ e\
+c\
+h\
+o\
+\
+1\
+2\
+3\
+
これは、前掲したコマンドと同じだ。かくして、スペースを陽に書かずにシェルをおおよそ自由に扱えるようになった。Fizzbuzz のワンライナーくらいすぐ書けるだろうから、あとはなんとかなるだろう (試してないけど)。 @@ -730,9 +746,10 @@ ちなみに、PHP 8.2 からは、この記法で Warning が出るようになるようだ。
${
-'_
-'}
+ ${
+'_
+'}
+
最新版で警告が出るというのも美しくないので、私としては本編の解法を推す。 diff --git a/services/nuldoc/public/blog/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/index.html b/services/nuldoc/public/blog/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/index.html index 2c82b081..4cccc732 100644 --- a/services/nuldoc/public/blog/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/index.html +++ b/services/nuldoc/public/blog/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/index.html @@ -102,27 +102,28 @@ 注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。
<?php
-
-$π = $argv[1] ?? null;
-if ($π === null) {
- exit('No input.');
-}
-$π = trim($π);
-if (!is_numeric($π)) {
- exit('Invalid input.');
-}
-
-$s = implode(array_map(chr(...), str_split($π, 2)));
-
-preg_match('/(\x23.+?) /', $s, $m);
-$t = $m[1] ?? '';
-
-if (md5($t) === '056e831a4146bf123e8ea16613303d2e') {
- echo "Token: {$t}\n";
-} else {
- echo "Failed.\n";
-}
+ <?php
+
+$π = $argv[1] ?? null;
+if ($π === null) {
+ exit('No input.');
+}
+$π = trim($π);
+if (!is_numeric($π)) {
+ exit('Invalid input.');
+}
+
+$s = implode(array_map(chr(...), str_split($π, 2)));
+
+preg_match('/(\x23.+?) /', $s, $m);
+$t = $m[1] ?? '';
+
+if (md5($t) === '056e831a4146bf123e8ea16613303d2e') {
+ echo "Token: {$t}\n";
+} else {
+ echo "Failed.\n";
+}
+
$argv[1] を参照している。それを $π なる変数に代入しているので、円周率を渡してみる。
$ php Q.php 3.14
-Failed.
+ $ php Q.php 3.14
+Failed.
+
失敗してしまった。精度を上げてみる。
$ php Q.php 3.1415
-Failed.
+ $ php Q.php 3.1415
+Failed.
+
だめだった。これを成功するまで繰り返す。 @@ -148,8 +151,9 @@ 最初にトークンが得られるのは、小数点以下 16 桁目まで入力したときで、こうなる。
$ php Q.php 3.1415926535897932
-Token: #YO
+ $ php Q.php 3.1415926535897932
+Token: #YO
+
めでたくトークン「#YO」が手に入った。
@@ -161,20 +165,22 @@
短いので頭から追っていく。
$π = $argv[1] ?? null;
-if ($π === null) {
- exit('No input.');
-}
-$π = trim($π);
-if (!is_numeric($π)) {
- exit('Invalid input.');
-}
+ $π = $argv[1] ?? null;
+if ($π === null) {
+ exit('No input.');
+}
+$π = trim($π);
+if (!is_numeric($π)) {
+ exit('Invalid input.');
+}
+
入力のバリデーション部分。数値のみ受け付ける。
$s = implode(array_map(chr(...), str_split($π, 2)));
+ $s = implode(array_map(chr(...), str_split($π, 2)));
+
$π を 2 文字ごとに区切り (str_split)、数値を ASCII コードと見做して文字に変換 (chr) して結合 (implode) している。
@@ -183,14 +189,16 @@
例えば、$π が '656667' だったとすると、65、66、67 に対応した 'A'、'B'、'C' へと変換され、'ABC' になる。
$π = '656667';
-$s = implode(array_map(chr(...), str_split($π, 2)));
-echo $s;
-// => ABC
+ $π = '656667';
+$s = implode(array_map(chr(...), str_split($π, 2)));
+echo $s;
+// => ABC
+
preg_match('/(\x23.+?) /', $s, $m);
-$t = $m[1] ?? '';
+ preg_match('/(\x23.+?) /', $s, $m);
+$t = $m[1] ?? '';
+
正規表現でマッチングしている。\x23 は # と同じであることに留意すると、この正規表現は「# から始まる 2 以上の長さ (含 #) の文字列で、最初に現れるスペースまで」にマッチする。つまりこれは、PHPerKaigi におけるトークンである。
@@ -199,11 +207,12 @@
なお、# を直接書いていないのは、/#.+?) / と書くと、#.+?) という意図せぬトークンが登録されてしまうからである。
if (md5($t) === '056e831a4146bf123e8ea16613303d2e') {
- echo "Token: {$t}\n";
-} else {
- echo "Failed.\n";
-}
+ if (md5($t) === '056e831a4146bf123e8ea16613303d2e') {
+ echo "Token: {$t}\n";
+} else {
+ echo "Failed.\n";
+}
+
最後にトークンのハッシュ値を見て、想定解かどうかを確認する。 diff --git a/services/nuldoc/public/blog/posts/2022-10-28/setup-server-for-this-site/index.html b/services/nuldoc/public/blog/posts/2022-10-28/setup-server-for-this-site/index.html index 79d11c10..9fd8d6c2 100644 --- a/services/nuldoc/public/blog/posts/2022-10-28/setup-server-for-this-site/index.html +++ b/services/nuldoc/public/blog/posts/2022-10-28/setup-server-for-this-site/index.html @@ -176,8 +176,9 @@ ローカルマシンで鍵を生成する。
$ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/teika.key
-$ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/github2teika.key
+ $ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/teika.key
+$ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/github2teika.key
+
teika.key はローカルからサーバへの接続用、github2teika.key は、GitHub Actions からサーバへのデプロイ用。
@@ -189,12 +190,13 @@
.ssh/config に設定しておく。
Host teika
- HostName **********
- User **********
- Port **********
- IdentityFile ~/.ssh/teika.key
- IdentitiesOnly yes
+ Host teika
+ HostName **********
+ User **********
+ Port **********
+ IdentityFile ~/.ssh/teika.key
+ IdentitiesOnly yes
+
sudo グループに追加して sudo できるようにし、su で切り替え。
$ sudo adduser **********
-$ sudo adduser ********** sudo
-$ su **********
-$ cd
+ $ sudo adduser **********
+$ sudo adduser ********** sudo
+$ su **********
+$ cd
+
$ sudo hostname teika
+ $ sudo hostname teika
+
$ mkdir ~/.ssh
-$ chmod 700 ~/.ssh
-$ vi ~/.ssh/authorized_keys
+ $ mkdir ~/.ssh
+$ chmod 700 ~/.ssh
+$ vi ~/.ssh/authorized_keys
+
authorized_keys には、ローカルで生成した ~/.ssh/teika.key.pub と ~/.ssh/github2teika.key.pub の内容をコピーする。
@@ -241,8 +246,9 @@
SSH の設定を変更し、少しでも安全にしておく。
$ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
-$ sudo vi /etc/ssh/sshd_config
+ $ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
+$ sudo vi /etc/ssh/sshd_config
+
$ sudo systemctl restart sshd
-$ sudo systemctl status sshd
+ $ sudo systemctl restart sshd
+$ sudo systemctl status sshd
+
$ ssh teika
+ $ ssh teika
+
$ sudo ufw deny ssh
-$ sudo ufw allow *******
-$ sudo ufw enable
-$ sudo ufw reload
-$ sudo ufw status
+ $ sudo ufw deny ssh
+$ sudo ufw allow *******
+$ sudo ufw enable
+$ sudo ufw reload
+$ sudo ufw status
+
ここでもう一度 SSH の接続確認を挟む。 @@ -294,41 +303,46 @@ GitHub に置いてある private リポジトリをサーバから clone したいので、SSH 鍵を生成して置いておく。
$ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/github.key
-$ cat ~/.ssh/github.key.pub
+ $ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/github.key
+$ cat ~/.ssh/github.key.pub
+
GitHub の設定画面 から、この公開鍵を追加する。
$ vi ~/.ssh/config
+ $ vi ~/.ssh/config
+
設定はこう。
Host github.com
- HostName github.com
- User git
- Port 22
- IdentityFile ~/.ssh/github.key
- IdentitiesOnly yes
+ Host github.com
+ HostName github.com
+ User git
+ Port 22
+ IdentityFile ~/.ssh/github.key
+ IdentitiesOnly yes
+
最後に接続できるか確認しておく。
$ ssh -T github.com
+ $ ssh -T github.com
+
$ sudo apt update
-$ sudo apt upgrade
-$ sudo apt update
-$ sudo apt upgrade
-$ sudo apt autoremove
+ $ sudo apt update
+$ sudo apt upgrade
+$ sudo apt update
+$ sudo apt upgrade
+$ sudo apt autoremove
+
$ sudo apt install docker docker-compose git make
+ $ sudo apt install docker docker-compose git make
+
$ sudo adduser ********** docker
+ $ sudo adduser ********** docker
+
$ sudo ufw allow 80/tcp
-$ sudo ufw allow 443/tcp
-$ sudo ufw reload
-$ sudo ufw status
+ $ sudo ufw allow 80/tcp
+$ sudo ufw allow 443/tcp
+$ sudo ufw reload
+$ sudo ufw status
+
$ cd
-$ git clone git@github.com:nsfisis/nsfisis.dev.git
-$ cd nsfisis.dev
-$ git submodule update --init
+ $ cd
+$ git clone git@github.com:nsfisis/nsfisis.dev.git
+$ cd nsfisis.dev
+$ git submodule update --init
+
$ docker-compose up -d acme-challenge
-$ make setup
+ $ docker-compose up -d acme-challenge
+$ make setup
+
$ make serve
+ $ make serve
+
<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+ <?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+
“And Then There Were None” (そして誰もいなくなった) と名付けた作品。変則 quine (自分自身と同じソースコードを出力するプログラム) になっている。 @@ -126,46 +127,49 @@ 実行してみると、次のような出力が得られる。
#
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+ #
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+
1 行目を除き、先ほどのコードとほぼ同じものが出てきた。もう一度実行してみる。
#
-W
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
-<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+ #
+W
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+
今度は 2 行目が書き換えられた。すべての行が変化するまで繰り返すと次のようになる。
#
-W
-E
-L
-O
-V
-E
-P
-H
-P
+ #
+W
+E
+L
+O
+V
+E
+P
+H
+P
+
トークン「#WELOVEPHP」が手に入った。 @@ -180,7 +184,8 @@ Vim で開くと次のようになる (1 行目を抜粋)。
<?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s='<200b><?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+ <?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s='<200b><?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?>
+
<200b> と表示されているのは、Unicode の U+200b で、ゼロ幅スペースである。
@@ -202,13 +207,15 @@
続いて、トークンへの変換ロジックを解析する。注目すべきはこの部分だ。以下、ゼロ幅スペースは Vim での表示に合わせて <200b> と記載する。
fn($s)=>chr(strlen($s)/3)
+ fn($s)=>chr(strlen($s)/3)
+
PHP の strlen() は文字列のバイト数を返す。1 行目の $s は以下の内容となっており、
$s='<200b><?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>'
+ $s='<200b><?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>'
+
このソースコードは UTF-8 で書かれているので、105 バイトになる。それを 3 で割ると 35 となり、これは # の ASCII コードと一致する。他の行も、同様にしてゼロ幅スペースを詰めることで文字列長を調整し、トークンをエンコードしている。
diff --git a/services/nuldoc/public/blog/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3/index.html b/services/nuldoc/public/blog/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3/index.html
index 3c9f1e60..4937deb0 100644
--- a/services/nuldoc/public/blog/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3/index.html
+++ b/services/nuldoc/public/blog/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3/index.html
@@ -121,118 +121,119 @@
注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。
<?php
-try {
- f(g() / __LINE__);
-} catch (Throwable $e) {
- while ($e = $e->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__;
-}
+ <?php
+try {
+ f(g() / __LINE__);
+} catch (Throwable $e) {
+ while ($e = $e->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==') が得られる。
@@ -266,20 +267,21 @@
このうち 1つ目のケースは、 finally 節の中でエラーを投げると PHP 処理系が勝手に $previous を設定してくれる。
<?php
-
-try {
- try {
- throw new Exception("Error 1");
- } finally {
- throw new Exception("Error 2");
- }
-} catch (Exception $e) {
- echo $e->getMessage() . PHP_EOL;
- // => Error 2
- echo $e->getPrevious()->getMessage() . PHP_EOL;
- // => Error 1
-}
+ <?php
+
+try {
+ try {
+ throw new Exception("Error 1");
+ } finally {
+ throw new Exception("Error 2");
+ }
+} catch (Exception $e) {
+ echo $e->getMessage() . PHP_EOL;
+ // => Error 2
+ echo $e->getPrevious()->getMessage() . PHP_EOL;
+ // => Error 1
+}
+
この知識を元に、トークンの出力部を解析してみる。 @@ -291,15 +293,16 @@ 出力部をコメントや改行を追加して再掲する:
<?php
-try {
- f(g() / __LINE__);
-} catch (Throwable $e) {
- while ($e = $e->getPrevious()) {
- printf('%c', $e->getLine() + 23);
- }
- echo "\n";
-}
+ <?php
+try {
+ f(g() / __LINE__);
+} catch (Throwable $e) {
+ while ($e = $e->getPrevious()) {
+ printf('%c', $e->getLine() + 23);
+ }
+ echo "\n";
+}
+
出力をおこなう catch 節を見てみると、 Throwable::getPrevious() を呼び出してエラーチェインを辿り、 Throwable::getLine() でエラーが発生した行数を取得している。その行数に 23 なるマジックナンバーを足し、フォーマット指定子 %c で出力している。
@@ -308,7 +311,8 @@
フォーマット指定子 %c は、整数を ASCII コード[1] と見做して印字する。トークン #base64_decode('SGVsbG8sIFdvcmxkIQ==') の b であれば、ASCII コード 98 なので、75 行目で発生したエラー、
1, 20 => 0 / 0,
+ 1, 20 => 0 / 0,
+
によって表現されている。エラーを起こす方法はいろいろと考えられるが、今回はゼロ除算を使った。
@@ -323,39 +327,42 @@
f() の定義を再掲する (エラーオブジェクトの行数を利用しているので、一部分だけ抜き出すと値が変わることに注意):
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);
- }
-}
+ 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
-try {
- f(g() / __LINE__); // 3 行目
+ <?php
+try {
+ f(g() / __LINE__); // 3 行目
+
function g() {
- return __LINE__; // 111 行目
-}
+ function g() {
+ return __LINE__; // 111 行目
+}
+
f() には 111 / 3 で 37 が渡されることがわかる。そこから 1 ずつ減らして再帰呼び出ししていき、0 より小さくなったら f() を引数なしで呼び出す。引数の数が足りないと呼び出しに失敗するので、再帰はここで止まる。
diff --git a/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder.md b/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder.md
index 6f4fb3c8..2fd69590 100644
--- a/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder.md
+++ b/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder.md
@@ -189,19 +189,19 @@ IHDR chunk は最初に配置される chunk である。次のようなデー
1. 画像の幅 (符号なし 4 バイト整数)
1. 画像の高さ (符号なし 4 バイト整数)
1. ビット深度 (符号なし 1 バイト整数)
- * 1 色に使うビット数。1 ピクセルに 24 bit 使う truecolor 画像では 8 になる
+ * 1 色に使うビット数。1 ピクセルに 24 bit 使う truecolor 画像では 8 になる
1. 色タイプ (符号なし 1 バイト整数)
- * 0: グレースケール
- * 2: Truecolor (今回はこれに決め打ち)
- * 3: パレットのインデックス
- * 4: グレースケール + アルファ
- * 6: Truecolor + アルファ
+ * 0: グレースケール
+ * 2: Truecolor (今回はこれに決め打ち)
+ * 3: パレットのインデックス
+ * 4: グレースケール + アルファ
+ * 6: Truecolor + アルファ
1. 圧縮方式 (符号なし 1 バイト整数)
- * PNG の仕様書に 0 しか定義されていないので 0 で固定
+ * PNG の仕様書に 0 しか定義されていないので 0 で固定
1. フィルタ方式 (符号なし 1 バイト整数)
- * PNG の仕様書に 0 しか定義されていないので 0 で固定
+ * PNG の仕様書に 0 しか定義されていないので 0 で固定
1. インターレース方式 (符号なし 1 バイト整数)
- * 今回はインターレースしないので 0
+ * 今回はインターレースしないので 0
今回ほとんどのデータは決め打ちするので、データに応じて変わるのは width と height だけになる。コードは次のようになる。
diff --git a/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html b/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html
index 7ab1e9b5..07518f23 100644
--- a/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html
+++ b/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html
@@ -135,44 +135,45 @@
以下のソースコードをベースにする。今回 PNG のデコーダは扱わないので、読み込みには Go の標準ライブラリ image/png を用いる。
package main
-
-import (
- "image"
- _ "image/png"
- "io"
- "os"
-)
-
-func main() {
- inFile, err := os.Open("input.png")
- if err != nil {
- panic(err)
- }
- defer inFile.Close()
-
- img, _, err := image.Decode(inFile)
- if err != nil {
- panic(err)
- }
-
- outFile, err := os.Create("output.png")
- if err != nil {
- panic(err)
- }
- defer outFile.Close()
-
- writePng(outFile, img)
-}
-
-func writePng(w io.Writer, img image.Image) {
- width := uint32(img.Bounds().Dx())
- height := uint32(img.Bounds().Dy())
- writeSignature(w)
- writeChunkIhdr(w, width, height)
- writeChunkIdat(w, width, height, img)
- writeChunkIend(w)
-}
+ package main
+
+import (
+ "image"
+ _ "image/png"
+ "io"
+ "os"
+)
+
+func main() {
+ inFile, err := os.Open("input.png")
+ if err != nil {
+ panic(err)
+ }
+ defer inFile.Close()
+
+ img, _, err := image.Decode(inFile)
+ if err != nil {
+ panic(err)
+ }
+
+ outFile, err := os.Create("output.png")
+ if err != nil {
+ panic(err)
+ }
+ defer outFile.Close()
+
+ writePng(outFile, img)
+}
+
+func writePng(w io.Writer, img image.Image) {
+ width := uint32(img.Bounds().Dx())
+ height := uint32(img.Bounds().Dy())
+ writeSignature(w)
+ writeChunkIhdr(w, width, height)
+ writeChunkIdat(w, width, height, img)
+ writeChunkIend(w)
+}
+
以降は、writeSignature や writeChunkIhdr などを実装していく。
@@ -215,21 +216,22 @@
writeSignature の実装はこちら:
import "encoding/binary"
-
-func writeSignature(w io.Writer) {
- sig := [8]uint8{
- 0x89,
- 0x50, // P
- 0x4E, // N
- 0x47, // G
- 0x0D, // CR
- 0x0A, // LF
- 0x1A, // EOF (^Z)
- 0x0A, // LF
- }
- binary.Write(w, binary.BigEndian, sig)
-}
+ import "encoding/binary"
+
+func writeSignature(w io.Writer) {
+ sig := [8]uint8{
+ 0x89,
+ 0x50, // P
+ 0x4E, // N
+ 0x47, // G
+ 0x0D, // CR
+ 0x0A, // LF
+ 0x1A, // EOF (^Z)
+ 0x0A, // LF
+ }
+ binary.Write(w, binary.BigEndian, sig)
+}
+
encoding/binary パッケージの binary.Write を使い、固定の 8 バイトを書き込む。
@@ -258,55 +260,57 @@
CRC (Cyclic Redundancy Check) は誤り検出符号の一種。Go 言語では hash/crc32 パッケージにあるが、今回はこれも自前で実装する。PNG の仕様書に C 言語のサンプルコードが載っている ( D. Sample CRC implementation ) ので、これを Go に移植する。
var (
- crcTable [256]uint32
- crcTableComputed bool
-)
-
-func makeCrcTable() {
- for n := 0; n < 256; n++ {
- c := uint32(n)
- for k := 0; k < 8; k++ {
- if (c & 1) != 0 {
- c = 0xEDB88320 ^ (c >> 1)
- } else {
- c = c >> 1
- }
- }
- crcTable[n] = c
- }
- crcTableComputed = true
-}
-
-func updateCrc(crc uint32, buf []byte) uint32 {
- if !crcTableComputed {
- makeCrcTable()
- }
-
- c := crc
- for n := 0; n < len(buf); n++ {
- c = crcTable[(c^uint32(buf[n]))&0xFF] ^ (c >> 8)
- }
- return c
-}
-
-func crc(buf []byte) uint32 {
- return updateCrc(0xFFFFFFFF, buf) ^ 0xFFFFFFFF
-}
+ var (
+ crcTable [256]uint32
+ crcTableComputed bool
+)
+
+func makeCrcTable() {
+ for n := 0; n < 256; n++ {
+ c := uint32(n)
+ for k := 0; k < 8; k++ {
+ if (c & 1) != 0 {
+ c = 0xEDB88320 ^ (c >> 1)
+ } else {
+ c = c >> 1
+ }
+ }
+ crcTable[n] = c
+ }
+ crcTableComputed = true
+}
+
+func updateCrc(crc uint32, buf []byte) uint32 {
+ if !crcTableComputed {
+ makeCrcTable()
+ }
+
+ c := crc
+ for n := 0; n < len(buf); n++ {
+ c = crcTable[(c^uint32(buf[n]))&0xFF] ^ (c >> 8)
+ }
+ return c
+}
+
+func crc(buf []byte) uint32 {
+ return updateCrc(0xFFFFFFFF, buf) ^ 0xFFFFFFFF
+}
+
できた crc 関数を使って、chunk 一般を書き込む関数も用意しておこう。
func writeChunk(w io.Writer, chunkType string, data []byte) {
- typeAndData := make([]byte, 0, len(chunkType)+len(data))
- typeAndData = append(typeAndData, []byte(chunkType)...)
- typeAndData = append(typeAndData, data...)
-
- binary.Write(w, binary.BigEndian, uint32(len(data)))
- binary.Write(w, binary.BigEndian, typeAndData)
- binary.Write(w, binary.BigEndian, crc(typeAndData))
-}
+ func writeChunk(w io.Writer, chunkType string, data []byte) {
+ typeAndData := make([]byte, 0, len(chunkType)+len(data))
+ typeAndData = append(typeAndData, []byte(chunkType)...)
+ typeAndData = append(typeAndData, data...)
+
+ binary.Write(w, binary.BigEndian, uint32(len(data)))
+ binary.Write(w, binary.BigEndian, typeAndData)
+ binary.Write(w, binary.BigEndian, crc(typeAndData))
+}
+
仕様どおり、chunkType と data から CRC を計算し、data の長さと合わせて書き込んでいる。PNG では基本的に big endian を使うことに注意する。
@@ -384,20 +388,21 @@
今回ほとんどのデータは決め打ちするので、データに応じて変わるのは width と height だけになる。コードは次のようになる。
import "bytes"
-
-func writeChunkIhdr(w io.Writer, width, height uint32) {
- var buf bytes.Buffer
- binary.Write(&buf, binary.BigEndian, width)
- binary.Write(&buf, binary.BigEndian, height)
- binary.Write(&buf, binary.BigEndian, uint8(8))
- binary.Write(&buf, binary.BigEndian, uint8(2))
- binary.Write(&buf, binary.BigEndian, uint8(0))
- binary.Write(&buf, binary.BigEndian, uint8(0))
- binary.Write(&buf, binary.BigEndian, uint8(0))
-
- writeChunk(w, "IHDR", buf.Bytes())
-}
+ import "bytes"
+
+func writeChunkIhdr(w io.Writer, width, height uint32) {
+ var buf bytes.Buffer
+ binary.Write(&buf, binary.BigEndian, width)
+ binary.Write(&buf, binary.BigEndian, height)
+ binary.Write(&buf, binary.BigEndian, uint8(8))
+ binary.Write(&buf, binary.BigEndian, uint8(2))
+ binary.Write(&buf, binary.BigEndian, uint8(0))
+ binary.Write(&buf, binary.BigEndian, uint8(0))
+ binary.Write(&buf, binary.BigEndian, uint8(0))
+
+ writeChunk(w, "IHDR", buf.Bytes())
+}
+
const adler32Base = 65521
-
-func updateAdler32(adler uint32, buf []byte) uint32 {
- s1 := adler & 0xFFFF
- s2 := (adler >> 16) & 0xFFFF
-
- for n := 0; n < len(buf); n++ {
- s1 = (s1 + uint32(buf[n])) % adler32Base
- s2 = (s2 + s1) % adler32Base
- }
- return (s2 << 16) + s1
-}
-
-func adler32(buf []byte) uint32 {
- return updateAdler32(1, buf)
-}
+ const adler32Base = 65521
+
+func updateAdler32(adler uint32, buf []byte) uint32 {
+ s1 := adler & 0xFFFF
+ s2 := (adler >> 16) & 0xFFFF
+
+ for n := 0; n < len(buf); n++ {
+ s1 = (s1 + uint32(buf[n])) % adler32Base
+ s2 = (s2 + s1) % adler32Base
+ }
+ return (s2 << 16) + s1
+}
+
+func adler32(buf []byte) uint32 {
+ return updateAdler32(1, buf)
+}
+
「データ」の部分には圧縮したデータが入るのだが、真面目に deflate アルゴリズムを実装する必要はない。Zlib には無圧縮のデータブロックを格納することができるので、これを使う。本来は、データの圧縮効率の悪いランダムなデータをそのまま格納するためのものだが、今回は deflate の実装をサボるために使う。 @@ -472,30 +478,31 @@ 実際にこの手抜き zlib を実装したものがこちら:
func encodeZlib(data []byte) []byte {
- var buf bytes.Buffer
-
- binary.Write(&buf, binary.BigEndian, uint8(0x78))
- binary.Write(&buf, binary.BigEndian, uint8(0x01))
- blockSize := 65535
- isFinalBlock := false
- for i := 0; !isFinalBlock; i++ {
- var block []byte
- if len(data) <= (i+1)*blockSize {
- block = data[i*blockSize:]
- isFinalBlock = true
- } else {
- block = data[i*blockSize : (i+1)*blockSize]
- }
- binary.Write(&buf, binary.BigEndian, isFinalBlock)
- binary.Write(&buf, binary.LittleEndian, uint16(len(block)))
- binary.Write(&buf, binary.LittleEndian, uint16(^len(block)))
- binary.Write(&buf, binary.LittleEndian, block)
- }
- binary.Write(&buf, binary.BigEndian, adler32(data))
-
- return buf.Bytes()
-}
+ func encodeZlib(data []byte) []byte {
+ var buf bytes.Buffer
+
+ binary.Write(&buf, binary.BigEndian, uint8(0x78))
+ binary.Write(&buf, binary.BigEndian, uint8(0x01))
+ blockSize := 65535
+ isFinalBlock := false
+ for i := 0; !isFinalBlock; i++ {
+ var block []byte
+ if len(data) <= (i+1)*blockSize {
+ block = data[i*blockSize:]
+ isFinalBlock = true
+ } else {
+ block = data[i*blockSize : (i+1)*blockSize]
+ }
+ binary.Write(&buf, binary.BigEndian, isFinalBlock)
+ binary.Write(&buf, binary.LittleEndian, uint16(len(block)))
+ binary.Write(&buf, binary.LittleEndian, uint16(^len(block)))
+ binary.Write(&buf, binary.LittleEndian, block)
+ }
+ binary.Write(&buf, binary.BigEndian, adler32(data))
+
+ return buf.Bytes()
+}
+
encodeZlib も使って実際に実装したものがこちら:
func writeChunkIdat(w io.Writer, width, height uint32, img image.Image) {
- var pixels bytes.Buffer
- for y := uint32(0); y < height; y++ {
- binary.Write(&pixels, binary.BigEndian, uint8(0))
- for x := uint32(0); x < width; x++ {
- r, g, b, _ := img.At(int(x), int(y)).RGBA()
- binary.Write(&pixels, binary.BigEndian, uint8(r))
- binary.Write(&pixels, binary.BigEndian, uint8(g))
- binary.Write(&pixels, binary.BigEndian, uint8(b))
- }
- }
-
- writeChunk(w, "IDAT", encodeZlib(pixels.Bytes()))
-}
+ func writeChunkIdat(w io.Writer, width, height uint32, img image.Image) {
+ var pixels bytes.Buffer
+ for y := uint32(0); y < height; y++ {
+ binary.Write(&pixels, binary.BigEndian, uint8(0))
+ for x := uint32(0); x < width; x++ {
+ r, g, b, _ := img.At(int(x), int(y)).RGBA()
+ binary.Write(&pixels, binary.BigEndian, uint8(r))
+ binary.Write(&pixels, binary.BigEndian, uint8(g))
+ binary.Write(&pixels, binary.BigEndian, uint8(b))
+ }
+ }
+
+ writeChunk(w, "IDAT", encodeZlib(pixels.Bytes()))
+}
+
IEND くらいなので実装は簡単:
func writeChunkIend(w io.Writer) {
- writeChunk(w, "IEND", nil)
-}
+ func writeChunkIend(w io.Writer) {
+ writeChunk(w, "IEND", nil)
+}
+
package main
-
-import (
- "bytes"
- "encoding/binary"
- "image"
- _ "image/png"
- "io"
- "os"
-)
-
-func main() {
- inFile, err := os.Open("input.png")
- if err != nil {
- panic(err)
- }
- defer inFile.Close()
-
- img, _, err := image.Decode(inFile)
- if err != nil {
- panic(err)
- }
-
- outFile, err := os.Create("output.png")
- if err != nil {
- panic(err)
- }
- defer outFile.Close()
-
- writePng(outFile, img)
-}
-
-func writePng(w io.Writer, img image.Image) {
- width := uint32(img.Bounds().Dx())
- height := uint32(img.Bounds().Dy())
- writeSignature(w)
- writeChunkIhdr(w, width, height)
- writeChunkIdat(w, width, height, img)
- writeChunkIend(w)
-}
-
-func writeSignature(w io.Writer) {
- sig := [8]uint8{
- 0x89,
- 0x50, // P
- 0x4E, // N
- 0x47, // G
- 0x0D, // CR
- 0x0A, // LF
- 0x1A, // EOF (^Z)
- 0x0A, // LF
- }
- binary.Write(w, binary.BigEndian, sig)
-}
-
-func writeChunkIhdr(w io.Writer, width, height uint32) {
- var buf bytes.Buffer
- binary.Write(&buf, binary.BigEndian, width)
- binary.Write(&buf, binary.BigEndian, height)
- binary.Write(&buf, binary.BigEndian, uint8(8))
- binary.Write(&buf, binary.BigEndian, uint8(2))
- binary.Write(&buf, binary.BigEndian, uint8(0))
- binary.Write(&buf, binary.BigEndian, uint8(0))
- binary.Write(&buf, binary.BigEndian, uint8(0))
-
- writeChunk(w, "IHDR", buf.Bytes())
-}
-
-func writeChunkIdat(w io.Writer, width, height uint32, img image.Image) {
- var pixels bytes.Buffer
- for y := uint32(0); y < height; y++ {
- binary.Write(&pixels, binary.BigEndian, uint8(0))
- for x := uint32(0); x < width; x++ {
- r, g, b, _ := img.At(int(x), int(y)).RGBA()
- binary.Write(&pixels, binary.BigEndian, uint8(r))
- binary.Write(&pixels, binary.BigEndian, uint8(g))
- binary.Write(&pixels, binary.BigEndian, uint8(b))
- }
- }
-
- writeChunk(w, "IDAT", encodeZlib(pixels.Bytes()))
-}
-
-func encodeZlib(data []byte) []byte {
- var buf bytes.Buffer
-
- binary.Write(&buf, binary.BigEndian, uint8(0x78))
- binary.Write(&buf, binary.BigEndian, uint8(0x01))
- blockSize := 65535
- isFinalBlock := false
- for i := 0; !isFinalBlock; i++ {
- var block []byte
- if len(data) <= (i+1)*blockSize {
- block = data[i*blockSize:]
- isFinalBlock = true
- } else {
- block = data[i*blockSize : (i+1)*blockSize]
- }
- binary.Write(&buf, binary.BigEndian, isFinalBlock)
- binary.Write(&buf, binary.LittleEndian, uint16(len(block)))
- binary.Write(&buf, binary.LittleEndian, uint16(^len(block)))
- binary.Write(&buf, binary.LittleEndian, block)
- }
- binary.Write(&buf, binary.BigEndian, adler32(data))
-
- return buf.Bytes()
-}
-
-func writeChunkIend(w io.Writer) {
- writeChunk(w, "IEND", nil)
-}
-
-func writeChunk(w io.Writer, chunkType string, data []byte) {
- typeAndData := make([]byte, 0, len(chunkType)+len(data))
- typeAndData = append(typeAndData, []byte(chunkType)...)
- typeAndData = append(typeAndData, data...)
-
- binary.Write(w, binary.BigEndian, uint32(len(data)))
- binary.Write(w, binary.BigEndian, typeAndData)
- binary.Write(w, binary.BigEndian, crc(typeAndData))
-}
-
-var (
- crcTable [256]uint32
- crcTableComputed bool
-)
-
-func makeCrcTable() {
- for n := 0; n < 256; n++ {
- c := uint32(n)
- for k := 0; k < 8; k++ {
- if (c & 1) != 0 {
- c = 0xEDB88320 ^ (c >> 1)
- } else {
- c = c >> 1
- }
- }
- crcTable[n] = c
- }
- crcTableComputed = true
-}
-
-func updateCrc(crc uint32, buf []byte) uint32 {
- if !crcTableComputed {
- makeCrcTable()
- }
-
- c := crc
- for n := 0; n < len(buf); n++ {
- c = crcTable[(c^uint32(buf[n]))&0xFF] ^ (c >> 8)
- }
- return c
-}
-
-func crc(buf []byte) uint32 {
- return updateCrc(0xFFFFFFFF, buf) ^ 0xFFFFFFFF
-}
-
-const adler32Base = 65521
-
-func updateAdler32(adler uint32, buf []byte) uint32 {
- s1 := adler & 0xFFFF
- s2 := (adler >> 16) & 0xFFFF
-
- for n := 0; n < len(buf); n++ {
- s1 = (s1 + uint32(buf[n])) % adler32Base
- s2 = (s2 + s1) % adler32Base
- }
- return (s2 << 16) + s1
-}
-
-func adler32(buf []byte) uint32 {
- return updateAdler32(1, buf)
-}
+ package main
+
+import (
+ "bytes"
+ "encoding/binary"
+ "image"
+ _ "image/png"
+ "io"
+ "os"
+)
+
+func main() {
+ inFile, err := os.Open("input.png")
+ if err != nil {
+ panic(err)
+ }
+ defer inFile.Close()
+
+ img, _, err := image.Decode(inFile)
+ if err != nil {
+ panic(err)
+ }
+
+ outFile, err := os.Create("output.png")
+ if err != nil {
+ panic(err)
+ }
+ defer outFile.Close()
+
+ writePng(outFile, img)
+}
+
+func writePng(w io.Writer, img image.Image) {
+ width := uint32(img.Bounds().Dx())
+ height := uint32(img.Bounds().Dy())
+ writeSignature(w)
+ writeChunkIhdr(w, width, height)
+ writeChunkIdat(w, width, height, img)
+ writeChunkIend(w)
+}
+
+func writeSignature(w io.Writer) {
+ sig := [8]uint8{
+ 0x89,
+ 0x50, // P
+ 0x4E, // N
+ 0x47, // G
+ 0x0D, // CR
+ 0x0A, // LF
+ 0x1A, // EOF (^Z)
+ 0x0A, // LF
+ }
+ binary.Write(w, binary.BigEndian, sig)
+}
+
+func writeChunkIhdr(w io.Writer, width, height uint32) {
+ var buf bytes.Buffer
+ binary.Write(&buf, binary.BigEndian, width)
+ binary.Write(&buf, binary.BigEndian, height)
+ binary.Write(&buf, binary.BigEndian, uint8(8))
+ binary.Write(&buf, binary.BigEndian, uint8(2))
+ binary.Write(&buf, binary.BigEndian, uint8(0))
+ binary.Write(&buf, binary.BigEndian, uint8(0))
+ binary.Write(&buf, binary.BigEndian, uint8(0))
+
+ writeChunk(w, "IHDR", buf.Bytes())
+}
+
+func writeChunkIdat(w io.Writer, width, height uint32, img image.Image) {
+ var pixels bytes.Buffer
+ for y := uint32(0); y < height; y++ {
+ binary.Write(&pixels, binary.BigEndian, uint8(0))
+ for x := uint32(0); x < width; x++ {
+ r, g, b, _ := img.At(int(x), int(y)).RGBA()
+ binary.Write(&pixels, binary.BigEndian, uint8(r))
+ binary.Write(&pixels, binary.BigEndian, uint8(g))
+ binary.Write(&pixels, binary.BigEndian, uint8(b))
+ }
+ }
+
+ writeChunk(w, "IDAT", encodeZlib(pixels.Bytes()))
+}
+
+func encodeZlib(data []byte) []byte {
+ var buf bytes.Buffer
+
+ binary.Write(&buf, binary.BigEndian, uint8(0x78))
+ binary.Write(&buf, binary.BigEndian, uint8(0x01))
+ blockSize := 65535
+ isFinalBlock := false
+ for i := 0; !isFinalBlock; i++ {
+ var block []byte
+ if len(data) <= (i+1)*blockSize {
+ block = data[i*blockSize:]
+ isFinalBlock = true
+ } else {
+ block = data[i*blockSize : (i+1)*blockSize]
+ }
+ binary.Write(&buf, binary.BigEndian, isFinalBlock)
+ binary.Write(&buf, binary.LittleEndian, uint16(len(block)))
+ binary.Write(&buf, binary.LittleEndian, uint16(^len(block)))
+ binary.Write(&buf, binary.LittleEndian, block)
+ }
+ binary.Write(&buf, binary.BigEndian, adler32(data))
+
+ return buf.Bytes()
+}
+
+func writeChunkIend(w io.Writer) {
+ writeChunk(w, "IEND", nil)
+}
+
+func writeChunk(w io.Writer, chunkType string, data []byte) {
+ typeAndData := make([]byte, 0, len(chunkType)+len(data))
+ typeAndData = append(typeAndData, []byte(chunkType)...)
+ typeAndData = append(typeAndData, data...)
+
+ binary.Write(w, binary.BigEndian, uint32(len(data)))
+ binary.Write(w, binary.BigEndian, typeAndData)
+ binary.Write(w, binary.BigEndian, crc(typeAndData))
+}
+
+var (
+ crcTable [256]uint32
+ crcTableComputed bool
+)
+
+func makeCrcTable() {
+ for n := 0; n < 256; n++ {
+ c := uint32(n)
+ for k := 0; k < 8; k++ {
+ if (c & 1) != 0 {
+ c = 0xEDB88320 ^ (c >> 1)
+ } else {
+ c = c >> 1
+ }
+ }
+ crcTable[n] = c
+ }
+ crcTableComputed = true
+}
+
+func updateCrc(crc uint32, buf []byte) uint32 {
+ if !crcTableComputed {
+ makeCrcTable()
+ }
+
+ c := crc
+ for n := 0; n < len(buf); n++ {
+ c = crcTable[(c^uint32(buf[n]))&0xFF] ^ (c >> 8)
+ }
+ return c
+}
+
+func crc(buf []byte) uint32 {
+ return updateCrc(0xFFFFFFFF, buf) ^ 0xFFFFFFFF
+}
+
+const adler32Base = 65521
+
+func updateAdler32(adler uint32, buf []byte) uint32 {
+ s1 := adler & 0xFFFF
+ s2 := (adler >> 16) & 0xFFFF
+
+ for n := 0; n < len(buf); n++ {
+ s1 = (s1 + uint32(buf[n])) % adler32Base
+ s2 = (s2 + s1) % adler32Base
+ }
+ return (s2 << 16) + s1
+}
+
+func adler32(buf []byte) uint32 {
+ return updateAdler32(1, buf)
+}
+
import { readFile } from 'node:fs/promises';
-import PHPWasm from './php-wasm.mjs'
-
-const code = await readFile('/dev/stdin', { encoding: 'utf-8' });
-
-const { ccall } = await PHPWasm();
-const result = ccall(
- 'php_wasm_run',
- 'number', ['string'],
- [code],
-);
-console.log(`exit code: ${result}`);
+ import { readFile } from 'node:fs/promises';
+import PHPWasm from './php-wasm.mjs'
+
+const code = await readFile('/dev/stdin', { encoding: 'utf-8' });
+
+const { ccall } = await PHPWasm();
+const result = ccall(
+ 'php_wasm_run',
+ 'number', ['string'],
+ [code],
+);
+console.log(`exit code: ${result}`);
+
標準入力から与えたコードを WebAssembly にコンパイルされた PHP 処理系の上で実行している。このような php-wasm.mjs (とそこから呼び出される php-wasm.wasm) を作成する。
@@ -167,30 +168,31 @@
先ほどのコードでも使っていたエントリポイントである php_wasm_run を用意する。
#include <stdio.h>
-#include <emscripten.h>
-#include <Zend/zend_execute.h>
-#include <sapi/embed/php_embed.h>
-
-int EMSCRIPTEN_KEEPALIVE php_wasm_run(const char* code) {
- zend_result result;
-
- int argc = 1;
- char* argv[] = { "php.wasm", NULL };
-
- PHP_EMBED_START_BLOCK(argc, argv);
-
- result = zend_eval_string_ex(code, NULL, "php.wasm code", 1);
-
- PHP_EMBED_END_BLOCK();
-
- fprintf(stdout, "\n");
- fflush(stdout);
- fprintf(stderr, "\n");
- fflush(stderr);
-
- return result == SUCCESS ? 0 : 1;
-}
+ #include <stdio.h>
+#include <emscripten.h>
+#include <Zend/zend_execute.h>
+#include <sapi/embed/php_embed.h>
+
+int EMSCRIPTEN_KEEPALIVE php_wasm_run(const char* code) {
+ zend_result result;
+
+ int argc = 1;
+ char* argv[] = { "php.wasm", NULL };
+
+ PHP_EMBED_START_BLOCK(argc, argv);
+
+ result = zend_eval_string_ex(code, NULL, "php.wasm code", 1);
+
+ PHP_EMBED_END_BLOCK();
+
+ fprintf(stdout, "\n");
+ fflush(stdout);
+ fprintf(stderr, "\n");
+ fflush(stderr);
+
+ return result == SUCCESS ? 0 : 1;
+}
+
ほとんどはただの PHP の公開 API を使ったコードだが、Emscripten 向けの注意点が 2点ある。
@@ -213,15 +215,16 @@
デフォルトの出力方法は index.mjs の中で PHPWasm() を呼ぶとき、stdout・stderr というオプションを渡せば変更できる。
const { ccall } = await PHPWasm({
- stdout: (c) => {
- if (c === null) {
- // flush the standard output.
- } else {
- // output c to the standard output.
- }
- },
-});
+ const { ccall } = await PHPWasm({
+ stdout: (c) => {
+ if (c === null) {
+ // flush the standard output.
+ } else {
+ // output c to the standard output.
+ }
+ },
+});
+
c は null か 1バイト符号つき整数を取り、null が flush 要求を意味する。
@@ -241,49 +244,52 @@
まずは Emscripten 公式が提供している Docker イメージ を使って、PHP 処理系と先ほど示した C 言語のソースコードを WebAssembly にコンパイルする。
FROM emscripten/emsdk:3.1.46 AS wasm-builder
+ FROM emscripten/emsdk:3.1.46 AS wasm-builder
+
次に、 php/php-src から PHP 処理系のソースコードを取得し、ビルドに必要な apt パッケージを取ってくる。有効にする拡張を増やしたいなら、ここでインストールするパッケージも増やすことになるだろう。
RUN git clone --depth=1 --branch=php-8.2.10 https://github.com/php/php-src
-
-RUN apt-get update && \
- apt-get install -y --no-install-recommends \
- autoconf \
- bison \
- pkg-config \
- re2c \
- && \
- :
+ RUN git clone --depth=1 --branch=php-8.2.10 https://github.com/php/php-src
+
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ autoconf \
+ bison \
+ pkg-config \
+ re2c \
+ && \
+ :
+
続けて、Emscripten のツールチェインを用いて PHP 処理系をビルドする。
RUN cd php-src && \
- ./buildconf --force && \
- emconfigure ./configure \
- --disable-all \
- --disable-mbregex \
- --disable-fiber-asm \
- --disable-cli \
- --disable-cgi \
- --disable-phpdbg \
- --enable-embed=static \
- --enable-mbstring \
- --without-iconv \
- --without-libxml \
- --without-pcre-jit \
- --without-pdo-sqlite \
- --without-sqlite3 \
- && \
- EMCC_CFLAGS='-s ERROR_ON_UNDEFINED_SYMBOLS=0' emmake make -j$(nproc) && \
- mv libs/libphp.a .. && \
- make clean && \
- git clean -fd && \
- :
+ RUN cd php-src && \
+ ./buildconf --force && \
+ emconfigure ./configure \
+ --disable-all \
+ --disable-mbregex \
+ --disable-fiber-asm \
+ --disable-cli \
+ --disable-cgi \
+ --disable-phpdbg \
+ --enable-embed=static \
+ --enable-mbstring \
+ --without-iconv \
+ --without-libxml \
+ --without-pcre-jit \
+ --without-pdo-sqlite \
+ --without-sqlite3 \
+ && \
+ EMCC_CFLAGS='-s ERROR_ON_UNDEFINED_SYMBOLS=0' emmake make -j$(nproc) && \
+ mv libs/libphp.a .. && \
+ make clean && \
+ git clean -fd && \
+ :
+
ここまでと比べると少し複雑なので、それぞれ詳しく見ていこう。
@@ -307,22 +313,23 @@
さて、PHP 処理系をライブラリ化できたので、次に先ほど載せた C のソースコードをビルドしていこう。Dockerfile と同じ場所に php-wasm.c という名前で保存し、次のようにする。
COPY php-wasm.c /src/
-
-RUN cd php-src && \
- emcc \
- -c \
- -o php-wasm.o \
- -I . \
- -I TSRM \
- -I Zend \
- -I main \
- ../php-wasm.c \
- && \
- mv php-wasm.o .. && \
- make clean && \
- git clean -fd && \
- :
+ COPY php-wasm.c /src/
+
+RUN cd php-src && \
+ emcc \
+ -c \
+ -o php-wasm.o \
+ -I . \
+ -I TSRM \
+ -I Zend \
+ -I main \
+ ../php-wasm.c \
+ && \
+ mv php-wasm.o .. && \
+ make clean && \
+ git clean -fd && \
+ :
+
emcc は cc (C コンパイラ/リンカ) の Emscripten 版で、-c は「コンパイル」の意。-o や -I は普通の C コンパイラと同様、出力ファイルの指定とインクルードパスの指定である。
@@ -331,18 +338,19 @@
libphp.a と php-wasm.o が手に入ったので、これらをリンクして WebAssembly のバイナリとそのラッパである JavaScript ファイルを生成する。これにも emcc コマンドを使う。
RUN emcc \
- -s ENVIRONMENT=node \
- -s ERROR_ON_UNDEFINED_SYMBOLS=0 \
- -s EXPORTED_RUNTIME_METHODS='["ccall"]' \
- -s EXPORT_ES6=1 \
- -s INITIAL_MEMORY=16777216 \
- -s INVOKE_RUN=0 \
- -s MODULARIZE=1 \
- -o php-wasm.js \
- php-wasm.o \
- libphp.a \
- ;
+ RUN emcc \
+ -s ENVIRONMENT=node \
+ -s ERROR_ON_UNDEFINED_SYMBOLS=0 \
+ -s EXPORTED_RUNTIME_METHODS='["ccall"]' \
+ -s EXPORT_ES6=1 \
+ -s INITIAL_MEMORY=16777216 \
+ -s INVOKE_RUN=0 \
+ -s MODULARIZE=1 \
+ -o php-wasm.js \
+ php-wasm.o \
+ libphp.a \
+ ;
+
それぞれのフラグについて解説する。
@@ -375,14 +383,15 @@
といっても、Node.js はビルトインで WebAssembly をサポートしているので、ほとんどやることはない。先ほど掲載した JavaScript のコードは、Dockerfile と同じディレクトリに index.mjs で配置すること。
FROM node:20.7
-
-WORKDIR /app
-COPY --from=wasm-builder /src/php-wasm.js /app/php-wasm.mjs
-COPY --from=wasm-builder /src/php-wasm.wasm /app/php-wasm.wasm
-COPY index.mjs /app/
-
-ENTRYPOINT ["node", "index.mjs"]
+ FROM node:20.7
+
+WORKDIR /app
+COPY --from=wasm-builder /src/php-wasm.js /app/php-wasm.mjs
+COPY --from=wasm-builder /src/php-wasm.wasm /app/php-wasm.wasm
+COPY index.mjs /app/
+
+ENTRYPOINT ["node", "index.mjs"]
+
Dockerfile、php-wasm.c、index.mjs を用意したら、Docker コンテナをビルドして実行する。
$ docker build -t php-wasm .
-$ echo 'echo "Hello, World!", PHP_EOL;' | docker run --rm -i php-wasm
-Hello, World!
-
-
-exit code: 0
+ $ docker build -t php-wasm .
+$ echo 'echo "Hello, World!", PHP_EOL;' | docker run --rm -i php-wasm
+Hello, World!
+
+
+exit code: 0
+
namespace 宣言を挿入したい。具体的には、トップレベルの名前空間が MyNamespace であり、ファイル src/Foo/Bar/Baz.php を開いたときに、そのファイルが空であるなら、次のようなテンプレートが自動的に挿入されてほしい。
<?php
-
-namespace MyNamespace\Foo\Bar;
+ <?php
+
+namespace MyNamespace\Foo\Bar;
+
$ nvim --version
-NVIM v0.9.2
-Build type: Release
-LuaJIT 2.1.1693350652
+ $ nvim --version
+NVIM v0.9.2
+Build type: Release
+LuaJIT 2.1.1693350652
+
今回は Lua で処理を記述したため、Vim では動作しない。以下の説明でも Neovim に絞って述べる。また、パス区切りがスラッシュである前提で記述したため、Windows には対応していない。
@@ -135,13 +137,14 @@
ファイルタイプは読み込んだあとに変更されることもあるので、ftplugin は複数回実行されうる。二重読み込みを防ぐために、did_ftplugin_<FILE_TYPE>_after というバッファローカル変数を定義しておくのが慣習となっている。
if vim.b.did_ftplugin_php_after then
- return
-end
-
--- ここに実際の処理を書く
-
-vim.b.did_ftplugin_php_after = true
+ if vim.b.did_ftplugin_php_after then
+ return
+end
+
+-- ここに実際の処理を書く
+
+vim.b.did_ftplugin_php_after = true
+
if vim.b.did_ftplugin_php_after then
- return
-end
-
--- base_dir を起点としてディレクトリを上向きに辿っていき、composer.json を探す
--- :help vim.fs.find()
-local function find_composer_json(base_dir)
- return vim.fs.find('composer.json', {
- path = base_dir,
- upward = true,
- -- ホームディレクトリまで到達したら探索を打ち切る
- stop = vim.loop.os_homedir(),
- type = 'file',
- })[1]
-end
-
--- JSON ファイルを読み込み、デコードして返す
--- :help readblob()
--- :help vim.json.decode
--- :help luaref-pcall()
-local function load_json(file_path)
- -- readblob() は Vim script では Blob オブジェクトを返すが、Lua から呼ぶと string に変換される
- local ok_read, content = pcall(vim.fn.readblob, file_path)
- if not ok_read then
- return nil
- end
- local ok_decode, obj = pcall(vim.json.decode, content)
- if not ok_decode then
- return nil
- end
- return obj
-end
-
--- 対象ファイルの置かれたディレクトリを基に namespace 宣言を生成する
--- :help nvim_buf_get_name()
--- :help vim.fs.dirname()
-local function generate_namespace_declaration()
- -- composer.json を探し、トップレベルの名前空間とディレクトリを特定する
- local current_dir = vim.fs.dirname(vim.api.nvim_buf_get_name(0))
- local path_to_composer_json = find_composer_json(current_dir)
- if not path_to_composer_json then
- return nil -- failed to locate composer.json
- end
- local composer_json = load_json(path_to_composer_json)
- if not composer_json then
- return nil -- failed to load composer.json
- end
- -- autoload.psr-4 を探し、型が期待される型と一致するかどうか調べる
- local psr4 = vim.tbl_get(composer_json, 'autoload', 'psr-4')
- if not psr4 then
- return nil -- autoload.psr-4 section is absent
- end
- if vim.tbl_count(psr4) ~= 1 then
- return nil -- psr-4 section is ambiguous
- end
- local psr4_namespace, psr4_dir
- for k, v in pairs(psr4) do
- psr4_namespace = k
- psr4_dir = v
- end
- if type(psr4_dir) == 'table' then
- if #psr4_dir == 1 then
- psr4_dir = psr4_dir[1]
- else
- return nil -- psr-4 section is ambiguous
- end
- end
- if type(psr4_namespace) ~= 'string' or type(psr4_dir) ~= 'string' then
- return nil -- psr-4 section is invalid
- end
- -- 末尾のスラッシュとバックスラッシュを取り除いておく
- if psr4_namespace:sub(-1, -1) == '\\' then
- psr4_namespace = psr4_namespace:sub(0, -2)
- end
- if psr4_dir:sub(-1, -1) == '/' then
- psr4_dir = psr4_dir:sub(0, -2)
- end
-
- -- 対象ファイルが置かれたディレクトリとトップレベルのディレクトリを比較し、その差分を名前空間とする
- local namespace_root_dir = vim.fs.dirname(path_to_composer_json) .. '/' .. psr4_dir
- if not vim.startswith(current_dir, namespace_root_dir) then
- return nil
- end
- local current_path_suffix = current_dir:sub(#namespace_root_dir + 1)
- local namespace = psr4_namespace .. current_path_suffix:gsub('/', '\\')
- return ("namespace %s;"):format(namespace)
-end
-
-local function generate_template()
- local lines = {
- '<?php',
- '',
- 'declare(strict_types=1);',
- '',
- }
- local namespace_decl = generate_namespace_declaration()
- if namespace_decl then
- lines[#lines + 1] = namespace_decl
- lines[#lines + 1] = ''
- end
- lines[#lines + 1] = ''
- return lines
-end
-
-if vim.fn.line('$') == 1 and vim.fn.getline(1) == '' then
- -- 対象ファイルが空なら、テンプレートを挿入してカーソルを末尾に移動させる
- -- :help setline()
- -- :help cursor()
- vim.fn.setline(1, generate_template())
- vim.fn.cursor('$', 0)
-end
-
-vim.b.did_ftplugin_php_after = true
+ if vim.b.did_ftplugin_php_after then
+ return
+end
+
+-- base_dir を起点としてディレクトリを上向きに辿っていき、composer.json を探す
+-- :help vim.fs.find()
+local function find_composer_json(base_dir)
+ return vim.fs.find('composer.json', {
+ path = base_dir,
+ upward = true,
+ -- ホームディレクトリまで到達したら探索を打ち切る
+ stop = vim.loop.os_homedir(),
+ type = 'file',
+ })[1]
+end
+
+-- JSON ファイルを読み込み、デコードして返す
+-- :help readblob()
+-- :help vim.json.decode
+-- :help luaref-pcall()
+local function load_json(file_path)
+ -- readblob() は Vim script では Blob オブジェクトを返すが、Lua から呼ぶと string に変換される
+ local ok_read, content = pcall(vim.fn.readblob, file_path)
+ if not ok_read then
+ return nil
+ end
+ local ok_decode, obj = pcall(vim.json.decode, content)
+ if not ok_decode then
+ return nil
+ end
+ return obj
+end
+
+-- 対象ファイルの置かれたディレクトリを基に namespace 宣言を生成する
+-- :help nvim_buf_get_name()
+-- :help vim.fs.dirname()
+local function generate_namespace_declaration()
+ -- composer.json を探し、トップレベルの名前空間とディレクトリを特定する
+ local current_dir = vim.fs.dirname(vim.api.nvim_buf_get_name(0))
+ local path_to_composer_json = find_composer_json(current_dir)
+ if not path_to_composer_json then
+ return nil -- failed to locate composer.json
+ end
+ local composer_json = load_json(path_to_composer_json)
+ if not composer_json then
+ return nil -- failed to load composer.json
+ end
+ -- autoload.psr-4 を探し、型が期待される型と一致するかどうか調べる
+ local psr4 = vim.tbl_get(composer_json, 'autoload', 'psr-4')
+ if not psr4 then
+ return nil -- autoload.psr-4 section is absent
+ end
+ if vim.tbl_count(psr4) ~= 1 then
+ return nil -- psr-4 section is ambiguous
+ end
+ local psr4_namespace, psr4_dir
+ for k, v in pairs(psr4) do
+ psr4_namespace = k
+ psr4_dir = v
+ end
+ if type(psr4_dir) == 'table' then
+ if #psr4_dir == 1 then
+ psr4_dir = psr4_dir[1]
+ else
+ return nil -- psr-4 section is ambiguous
+ end
+ end
+ if type(psr4_namespace) ~= 'string' or type(psr4_dir) ~= 'string' then
+ return nil -- psr-4 section is invalid
+ end
+ -- 末尾のスラッシュとバックスラッシュを取り除いておく
+ if psr4_namespace:sub(-1, -1) == '\\' then
+ psr4_namespace = psr4_namespace:sub(0, -2)
+ end
+ if psr4_dir:sub(-1, -1) == '/' then
+ psr4_dir = psr4_dir:sub(0, -2)
+ end
+
+ -- 対象ファイルが置かれたディレクトリとトップレベルのディレクトリを比較し、その差分を名前空間とする
+ local namespace_root_dir = vim.fs.dirname(path_to_composer_json) .. '/' .. psr4_dir
+ if not vim.startswith(current_dir, namespace_root_dir) then
+ return nil
+ end
+ local current_path_suffix = current_dir:sub(#namespace_root_dir + 1)
+ local namespace = psr4_namespace .. current_path_suffix:gsub('/', '\\')
+ return ("namespace %s;"):format(namespace)
+end
+
+local function generate_template()
+ local lines = {
+ '<?php',
+ '',
+ 'declare(strict_types=1);',
+ '',
+ }
+ local namespace_decl = generate_namespace_declaration()
+ if namespace_decl then
+ lines[#lines + 1] = namespace_decl
+ lines[#lines + 1] = ''
+ end
+ lines[#lines + 1] = ''
+ return lines
+end
+
+if vim.fn.line('$') == 1 and vim.fn.getline(1) == '' then
+ -- 対象ファイルが空なら、テンプレートを挿入してカーソルを末尾に移動させる
+ -- :help setline()
+ -- :help cursor()
+ vim.fn.setline(1, generate_template())
+ vim.fn.cursor('$', 0)
+end
+
+vim.b.did_ftplugin_php_after = true
+
$ sudo apt install wireguard
+ $ sudo apt install wireguard
+
次に、WireGuard で使用する鍵を生成する。
$ wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub
-$ sudo chmod 600 /etc/wireguard/server.{key,pub}
+ $ wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub
+$ sudo chmod 600 /etc/wireguard/server.{key,pub}
+
# クライアント 1 の場合
-[Interface]
-Address = 10.10.1.2/32
-PrivateKey = <クライアント 1 の秘密鍵>
-
-[Peer]
-PublicKey = <サーバの公開鍵>
-AllowedIPs = <サーバの外部 IP アドレス>/32
-Endpoint = <サーバの外部 IP アドレス>:51820
+ # クライアント 1 の場合
+[Interface]
+Address = 10.10.1.2/32
+PrivateKey = <クライアント 1 の秘密鍵>
+
+[Peer]
+PublicKey = <サーバの公開鍵>
+AllowedIPs = <サーバの外部 IP アドレス>/32
+Endpoint = <サーバの外部 IP アドレス>:51820
+
# クライアント 2 の場合
-[Interface]
-Address = 10.10.1.3/32
-PrivateKey = <クライアント 2 の秘密鍵>
-
-[Peer]
-PublicKey = <サーバの公開鍵>
-AllowedIPs = <サーバの外部 IP アドレス>/32
-Endpoint = <サーバの外部 IP アドレス>:51820
+ # クライアント 2 の場合
+[Interface]
+Address = 10.10.1.3/32
+PrivateKey = <クライアント 2 の秘密鍵>
+
+[Peer]
+PublicKey = <サーバの公開鍵>
+AllowedIPs = <サーバの外部 IP アドレス>/32
+Endpoint = <サーバの外部 IP アドレス>:51820
+
PrivateKey や PublicKey は鍵ファイルのパスではなく中身を書くことに注意。
@@ -166,29 +170,32 @@
一度サーバへ戻り、WireGuard の設定ファイルを書く。
$ sudo vim /etc/wireguard/wg0.conf
+ $ sudo vim /etc/wireguard/wg0.conf
+
[Interface]
-Address = 10.10.1.1/32
-SaveConfig = true
-PrivateKey = <サーバの秘密鍵>
-ListenPort = 51820
-
-[Peer]
-PublicKey = <クライアント 1 の公開鍵>
-AllowedIPs = 10.10.1.2/32
-
-[Peer]
-PublicKey = <クライアント 2 の公開鍵>
-AllowedIPs = 10.10.1.3/32
+ [Interface]
+Address = 10.10.1.1/32
+SaveConfig = true
+PrivateKey = <サーバの秘密鍵>
+ListenPort = 51820
+
+[Peer]
+PublicKey = <クライアント 1 の公開鍵>
+AllowedIPs = 10.10.1.2/32
+
+[Peer]
+PublicKey = <クライアント 2 の公開鍵>
+AllowedIPs = 10.10.1.3/32
+
次に、WireGuard のサービスを起動する。
$ sudo systemctl enable wg-quick@wg0
-$ sudo systemctl start wg-quick@wg0
+ $ sudo systemctl enable wg-quick@wg0
+$ sudo systemctl start wg-quick@wg0
+
wg0 を通る通信を許可する。
$ sudo ufw allow 51820/udp
-$ sudo ufw allow in on wg0
-$ sudo ufw allow out on wg0
+ $ sudo ufw allow 51820/udp
+$ sudo ufw allow in on wg0
+$ sudo ufw allow out on wg0
+
次に、80 や 443 などの必要なポートについて、wg0 を経由してのアクセスのみ許可する。
$ sudo ufw allow in on wg0 to any port 80 proto tcp
-$ sudo ufw allow in on wg0 to any port 443 proto tcp
+ $ sudo ufw allow in on wg0 to any port 80 proto tcp
+$ sudo ufw allow in on wg0 to any port 443 proto tcp
+
最後に、ufw を有効にする。
$ sudo ufw status
-$ sudo ufw enable
+ $ sudo ufw status
+$ sudo ufw enable
+
hello-world:
- stage: test
- image: alpine:latest
- script:
- - 'echo "Hello, World!"'
- rules:
- - if: '$CI_MERGE_REQUEST_IID'
- when: always
+ hello-world:
+ stage: test
+ image: alpine:latest
+ script:
+ - 'echo "Hello, World!"'
+ rules:
+ - if: '$CI_MERGE_REQUEST_IID'
+ when: always
+
ここで、script に指定したコマンドが失敗する (exit status が 0 以外になる) と、即座に実行が停止され、ジョブは失敗する。
@@ -132,14 +133,15 @@
では、次のようなケースだとどうなるか。
hello-world:
- stage: test
- image: alpine:latest
- script:
- - 'exit 1 | exit 0'
- rules:
- - if: '$CI_MERGE_REQUEST_IID'
- when: always
+ hello-world:
+ stage: test
+ image: alpine:latest
+ script:
+ - 'exit 1 | exit 0'
+ rules:
+ - if: '$CI_MERGE_REQUEST_IID'
+ when: always
+
失敗するコマンドをパイプに接続した。通常 Bash では、パイプの最後のコマンドの exit code が全体の exit code になる。
@@ -151,10 +153,11 @@
前述したようなケースにおいて、途中で失敗したときに全体を失敗させるには、pipefail オプションを有効にする。
# On にする
-set -o pipefail
-# Off にする
-set +o pipefail
+ # On にする
+set -o pipefail
+# Off にする
+set +o pipefail
+
こうすると、パイプ全体が失敗するようになる。この設定は、デフォルトだと off になっている。 @@ -167,14 +170,15 @@ 次のような GitLab CI/CD ジョブが失敗してしまった。
hoge:
- stage: test
- image: alpine:latest
- script:
- - 'cat hoge.txt | grep piyo | sed -e "s/foo/bar/g"'
- rules:
- - if: '$CI_MERGE_REQUEST_IID'
- when: always
+ hoge:
+ stage: test
+ image: alpine:latest
+ script:
+ - 'cat hoge.txt | grep piyo | sed -e "s/foo/bar/g"'
+ rules:
+ - if: '$CI_MERGE_REQUEST_IID'
+ when: always
+
grep コマンドは、パターンにマッチする行が一行もなかったとき、exit code 1 を返す。よって、pipefail が on になっていると、このジョブは失敗する。現在の pipefail がどうなっているか確かめるため set +o で全オプションを出力させたところ、pipefail が on になっていた。
@@ -183,20 +187,21 @@
しかし、先述したように Bash における pipefail のデフォルト値は off のはずだ。実際に、ローカルで alpine:latest を動かしてみたところ、
$ docker run --rm alpine:latest sh -c "set +o"
-set +o errexit
-set +o noglob
-set +o ignoreeof
-set +o monitor
-set +o noexec
-set +o xtrace
-set +o verbose
-set +o noclobber
-set +o allexport
-set +o notify
-set +o nounset
-set +o vi
-set +o pipefail
+ $ docker run --rm alpine:latest sh -c "set +o"
+set +o errexit
+set +o noglob
+set +o ignoreeof
+set +o monitor
+set +o noexec
+set +o xtrace
+set +o verbose
+set +o noclobber
+set +o allexport
+set +o notify
+set +o nounset
+set +o vi
+set +o pipefail
+
確かに pipefail は無効になっている。
@@ -211,9 +216,10 @@
.gitlab-ci.yml で明示的には書いていないので、GitLab Runner (GitLab CI/CD のスクリプトを実行するプログラム) が勝手に追加しているに違いない。そう仮説を立てて GitLab Runner のリポジトリ を調査したところ、 ソースコード中の以下の箇所 で set -o pipefail していることが判明した (コメントは筆者による)。
// pipefail オプションが存在しない環境にも対応するため、
-// 先に set -o でオプション一覧を表示させたあと、set -o pipefail している
-buf.WriteString("if set -o | grep pipefail > /dev/null; then set -o pipefail; fi; set -o errexit\n")
+ // pipefail オプションが存在しない環境にも対応するため、
+// 先に set -o でオプション一覧を表示させたあと、set -o pipefail している
+buf.WriteString("if set -o | grep pipefail > /dev/null; then set -o pipefail; fi; set -o errexit\n")
+
pipefail が on になっていては困る場所だけ off にしてやればよい。
hoge:
- stage: test
- image: alpine:latest
- script:
-+ - 'set +o pipefail'
- - 'cat hoge.txt | grep piyo | sed -e "s/foo/bar/g"'
-+ - 'set -o pipefail' # この例の場合、ここで終わりなので戻さなくてもよい
- rules:
- - if: '$CI_MERGE_REQUEST_IID'
- when: always
+ hoge:
+ stage: test
+ image: alpine:latest
+ script:
++ - 'set +o pipefail'
+ - 'cat hoge.txt | grep piyo | sed -e "s/foo/bar/g"'
++ - 'set -o pipefail' # この例の場合、ここで終わりなので戻さなくてもよい
+ rules:
+ - if: '$CI_MERGE_REQUEST_IID'
+ when: always
+
_composer 関数を定義しているファイルの冒頭にも書かれている。
# - @todo We don't complete custom commands (including script aliases). This is
-# easy to do in the general case, but it probably requires some clever caching
-# to avoid introducing a noticeable lag to every completion operation, due to
-# the way command resolution works and the fact that discovering custom
-# commands requires making slow calls to Composer
+ # - @todo We don't complete custom commands (including script aliases). This is
+# easy to do in the general case, but it probably requires some clever caching
+# to avoid introducing a noticeable lag to every completion operation, due to
+# the way command resolution works and the fact that discovering custom
+# commands requires making slow calls to Composer
+
~/.zshrc にすべて書く前提だが、autoload を設定するなどすれば別ファイルに分離できる (詳細な手順は割愛)。
compdef _my_composer composer composer.phar
+ compdef _my_composer composer composer.phar
+
compdef は Zsh が用意している関数で、第一引数に補完関数の名前、第二引数以降に補完を適用するコマンド名を並べる。この場合は、composer コマンドや composer.phar コマンドに対して _my_composer を使って補完をおこなうよう定義している。
@@ -147,9 +149,10 @@
次に _my_composer を定義する。基本的にはデフォルトの composer コマンドの補完関数 (つまり _composer 関数) を使い、それが何も返さなかった場合に限り、Zsh のファイル・ディレクトリ補完へフォールバックする。
function _my_composer() {
- _composer "$@" || _files "$@"
-}
+ function _my_composer() {
+ _composer "$@" || _files "$@"
+}
+
_composer コマンドは何も補完候補がなかったとき非ゼロな exit status で終了するので、そうであったなら _files を呼び出す。_files は、Zsh がデフォルトで用意しているファイル・ディレクトリの補完をおこなう関数である。
diff --git a/services/nuldoc/public/blog/posts/2024-07-19/reparojson-fix-only-json-formatter/index.html b/services/nuldoc/public/blog/posts/2024-07-19/reparojson-fix-only-json-formatter/index.html
index 50bbf5ee..bf60bb52 100644
--- a/services/nuldoc/public/blog/posts/2024-07-19/reparojson-fix-only-json-formatter/index.html
+++ b/services/nuldoc/public/blog/posts/2024-07-19/reparojson-fix-only-json-formatter/index.html
@@ -106,17 +106,18 @@
次のように動作する。
$ echo '[ 1 2 ]' | reparojson
-[ 1, 2 ]
-
-$ echo '[ 1, 2, ]' | reparojson
-[ 1, 2 ]
-
-$ echo '{ "foo": 1 "bar": 2 }' | reparojson
-{ "foo": 1, "bar": 2 }
-
-$ echo '{ "foo": 1, "bar": 2, }' | reparojson
-{ "foo": 1, "bar": 2 }
+ $ echo '[ 1 2 ]' | reparojson
+[ 1, 2 ]
+
+$ echo '[ 1, 2, ]' | reparojson
+[ 1, 2 ]
+
+$ echo '{ "foo": 1 "bar": 2 }' | reparojson
+{ "foo": 1, "bar": 2 }
+
+$ echo '{ "foo": 1, "bar": 2, }' | reparojson
+{ "foo": 1, "bar": 2 }
+
バージョン 0.1.1 時点で修正対象の文法エラーは次のとおり: @@ -148,33 +149,34 @@ ここでは、 nvim-lspconfig と efm-langserver を用いた設定例を紹介する。
local lspconfig = require('lspconfig')
-
- lspconfig.efm.setup({
- init_options = { documentFormatting = true },
- settings = {
- rootMarkers = {".git/"},
- languages = {
- json = {
- {
- formatCommand = "reparojson -q",
- formatStdin = true,
- },
- },
- },
- }
- })
-
- vim.api.nvim_create_autocmd('LspAttach', {
- callback = function(e)
- vim.api.nvim_create_autocmd('BufWritePre', {
- buffer = e.buf,
- callback = function()
- vim.lsp.buf.format({ async = false })
- end
- })
- end,
- })
+ local lspconfig = require('lspconfig')
+
+ lspconfig.efm.setup({
+ init_options = { documentFormatting = true },
+ settings = {
+ rootMarkers = {".git/"},
+ languages = {
+ json = {
+ {
+ formatCommand = "reparojson -q",
+ formatStdin = true,
+ },
+ },
+ },
+ }
+ })
+
+ vim.api.nvim_create_autocmd('LspAttach', {
+ callback = function(e)
+ vim.api.nvim_create_autocmd('BufWritePre', {
+ buffer = e.buf,
+ callback = function()
+ vim.lsp.buf.format({ async = false })
+ end
+ })
+ end,
+ })
+
ほとんどは nvim-lspconfig と efm-langserver を使う際のボイラープレートだが、formatCommand で -q フラグを指定していることに注意してほしい。このツールは、デフォルトでは JSON が修正された場合 exit code 1 で終了する。これは、入力が最初から正しかった場合と修正して正しくなった場合を区別するためだが、異常終了してしまうと置き換えが発生しない。そのため、-q フラグを指定して、修正されたときも exit code 0 で終了するようにしている。
@@ -186,28 +188,31 @@
このツールが威力を発揮するのは、行の入れ換え時である。次のような JSON があり、
{
- "a": true,
- "b": false
- }
+ {
+ "a": true,
+ "b": false
+ }
+
2行目と3行目を入れ換えて以下のように編集した。
{
- "b": false
- "a": true,
- }
+ {
+ "b": false
+ "a": true,
+ }
+
これは不正な JSON だが、このツールを通せば次のようになる。
{
- "b": false,
- "a": true
- }
+ {
+ "b": false,
+ "a": true
+ }
+
もちろん、このような操作を文法を壊さずにおこなう Vim プラグインは存在する。しかし、単なる行の入れ換えであれば ddp の3ストロークでおこなうことができ、専用のキーバインドを覚える必要もない。このツールを用いることで、より Vimmer-friendly な JSON 編集が可能となる。
diff --git a/services/nuldoc/public/blog/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range/index.html b/services/nuldoc/public/blog/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range/index.html
index 536926b5..6222f888 100644
--- a/services/nuldoc/public/blog/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range/index.html
+++ b/services/nuldoc/public/blog/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range/index.html
@@ -90,19 +90,20 @@
Go には、標準ライブラリにテンプレートライブラリ text/template がある。この text/template における制御構造、with と range は次のように使われる。
# {{ .Title }}
-
-# User
-
-{{ with .User }}
- {{ .Name }} ({{ .ID }})
-{{ end }}
-
-# Items
-
-{{ range .Items }}
- - {{ . }}
-{{ end }}
+ # {{ .Title }}
+
+# User
+
+{{ with .User }}
+ {{ .Name }} ({{ .ID }})
+{{ end }}
+
+# Items
+
+{{ range .Items }}
+ - {{ . }}
+{{ end }}
+
text/template の . は、現在の操作対象を表す特殊なオブジェクトである。
@@ -114,18 +115,19 @@
つまりこのテンプレートは、次のような構造をレンダリングしている (Execute() の第2引数)。
tmpl.Execute(out, Params{
- Title: "foo",
- User: User{
- ID: 123,
- Name: "john",
- },
- Items: []string{
- "hoge",
- "piyo",
- "fuga",
- },
-})
+ tmpl.Execute(out, Params{
+ Title: "foo",
+ User: User{
+ ID: 123,
+ Name: "john",
+ },
+ Items: []string{
+ "hoge",
+ "piyo",
+ "fuga",
+ },
+})
+
with や range の中で、その外側で使われていたトップレベルのオブジェクトを参照することだ。
{{ with .User }}
- ここから .Title を参照するには?
-{{ end }}
-
-{{ range .Items }}
- ここから .User を参照するには?
-{{ end }}
+ {{ with .User }}
+ ここから .Title を参照するには?
+{{ end }}
+
+{{ range .Items }}
+ ここから .User を参照するには?
+{{ end }}
+
with や range は、. を自身の対象オブジェクトに変更するので、単に {{ with .User }} の中で .Title と書いても、それは User の Title プロパティを参照しているとみなされる。
@@ -149,7 +152,8 @@
text/template では変数が使えるので、テンプレートの先頭で
{{ $params := . }}
+ {{ $params := . }}
+
とでもしておけば実現は可能である。
@@ -164,13 +168,14 @@
常にトップレベルを指す特殊変数 $ を使えばよい。
{{ with .User }}
- {{ $.Title }}
-{{ end }}
-
-{{ range .Items }}
- {{ $.User.Name }}
-{{ end }}
+ {{ with .User }}
+ {{ $.Title }}
+{{ end }}
+
+{{ range .Items }}
+ {{ $.User.Name }}
+{{ end }}
+
$ は、テンプレートが実行されるときに渡されたオブジェクトを指す。これを使えば現在の . に関係なくトップレベルを参照できる。
diff --git a/services/nuldoc/public/blog/posts/2024-12-04/cohackpp-report/index.html b/services/nuldoc/public/blog/posts/2024-12-04/cohackpp-report/index.html
index 8201978a..050c47cd 100644
--- a/services/nuldoc/public/blog/posts/2024-12-04/cohackpp-report/index.html
+++ b/services/nuldoc/public/blog/posts/2024-12-04/cohackpp-report/index.html
@@ -157,129 +157,130 @@
https://github.com/nsfisis/cohackpp/blob/main/congrats.php
<?php
-$s=<<<'Q'
-<?php
-%
-$s=<<<'Q'
-@$c=[`];
-$m="";for($k=0;$k<min(13,intdiv(__LINE__-119,80)+1);$k++){$C=str_replace("\n","",
-$c[$k]);$f=!0;foreach(str_split(base64_decode($C))as$l){$L=ord($l);$m.=str_repeat
-($f?"#":chr(32),$L&127);$f=!$f;if($L&128){$m.="\n";$f=!0;continue;}}}print(
-str_replace([chr(96),chr(37),chr(64)],[implode("\n",array_map(fn($C)=>"'".trim(
-chunk_split(str_replace("\n","",$C),80,"\n"))."',",$c)),"\n{$m}","{$s}\nQ;\n"],$s));
-Q;
-$c=['0AFOgQFOgQFOgQFOgQFOgQFEAgiBAUIECIEBQwQHgQE8AQYFBoEBOgQGBAaBAToEBwQFgQE6BQYFBIEB
-OwQHBASBATwEBgUDgQE8BQYEA4EBPQQGBAOBAT0FBgEFgQERBhsIBAQMgQERKQQFC4EBESkFAQ6BAREp
-FIEBESkUgQERKRSBAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B
-AU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAQ4EPIEBDQY7gQENBjuBAQ0GO4EBDQU8gQENBTyBAQwG
-PIEBDAY8gQEMBjyBAQwGPIEBDAY8gQEMBjyBAQwHO4EBDAc7gQENBzqBAQ0IOYEBDgg4gQEOCiUCD4EB
-DwwdBw+BARAQDhEPgQERLg+BARItD4EBFCsPgQEXJRKBARsbGIEBToEBToEBToEBToEBToHQ',
-'0AFOgQFOgQFOgQEPASMFFoEBDwMhBRaBAQ4FIAUWgQEOBSAFFoEBDQUhBRaBAQ0FIQUWgQEMBSIFFoEB
-DAUiBRaBAQsFIwUWgQELBQgBGgUWgQEKBQkDGAUWgQEKBQgGBCoDgQEJBQkFBSoDgQEFAQMECQYFKgOB
-AQQDAQUJBQYqA4EBBAgJBQcqA4EBAwkJBRkFFoEBBAcJBRoFFoEBBQYIBRsFFoEBBgYHBRsFFoEBBwYF
-BRwFFoEBCAYDBR0FFoEBCQYCBR0FFoEBCgseBRaBAQsJHwUWgQEMByAFFoEBDAcFAxgFFoEBDQUFBBgF
-FoEBDQQGBRYGFoEBDAUHBAcmBYEBCwUIBQYmBYEBCwQKBAYmBYEBCgQLBQUmBYEBCQUMBS+BAQgFDAYv
-gQEDHC+BAQMdLoEBAx0ugQEDHi2BAQMJBAUIBC2BARAFCAQtgQEQBQgELYEBEAUJAS+BARAFESEHgQEQ
-BREhB4EBBwEIBQYBCiEHgQEHBAUFBAQJIQeBAQcEBQUEBAkEGAUHgQEGBQUFBAUIBBgFB4EBBgUFBQUE
-CAQYBQeBAQYFBQUFBAgEGAUHgQEGBAYFBQUHBBgFB4EBBgQGBQYEBwQYBQeBAQUFBgUGBQYEGAUHgQEF
-BQYFBwQGBBgFB4EBBQQHBQcEBgQYBQeBAQUEBwUHBQUEGAUHgQEEBQcFBwUFBBgFB4EBBAUHBQgEBQQY
-BQeBAQQECAUIBAUEGAUHgQEDBQgFCAEIBBgFB4EBAwUIBREEGAUHgQECBQkFEQQYBQeBAQIFCQURIQeB
-AQQCCgURIQeBARAFESEHgQEQBREhB4EBEAURIQeBARAFEQQYBQeBARAFEQQYBQeBARAFEQQYBQeBARAF
-EQQYBQeBAU6BAU6BAU6B0A==',
-'0AFOgQFOgQFOgQEOAjEBDIEBDgUqBwqBAQ0FJwwJgQENBSETCIEBDQURAQcXDIEBDQURGxCBAQ0FERcU
-gQENBBIOBAUUgQEMBRIGDAUUgQEMBRIFDQUUgQEMBRIFDgQUgQEMBRIFDgQUgQEMBBMFDgQUgQELBRMF
-DgQUgQELBRMFDgUTgQELBRMFDgUTgQEDGQcFDgUTgQEDGwUoA4EBAxsFKAOBAQMbBSgDgQEDGwUoA4EB
-CgUKBQUFDwUSgQEKBAsEBgUQBBKBAQkFCwQGBRAFEYEBCQULBAYFEAURgQEJBQoFBgUQBRGBAQkFCgUG
-BREFEIEBCQQLBQYFEQUQgQEIBQsFBgUSBQkBBYEBCAULBQYFEgUJAwOBAQgFCwUGBQoFBAUIBAKBAQgF
-CwQHBQQLBAYHAwOBAQgECwUHFAUGBQQDgQEHBQsFAxgFBwQEA4EBBwULBQMVCQ4DgQEHBQsFAw8QDQOB
-AQcEDAUDCRcLBIEBBgULBQUCHwgFgQEGBQsFKAQHgQEGBQsFM4EBBgULBTOBAQYFCgUKIgiBAQUHCQUK
-IgiBAQUICAUKIgiBAQUKBgUKIgiBAQULBAULBRgFCIEBBA0DBQsFGAUIgQEEBQIHAgULBRgFCIEBBgMD
-DAwFGAUIgQENCwwFGAUIgQEOCgwFGAUIgQEQBw0FGAUIgQERBwwFGAUIgQERCAsiCIEBEAoKIgiBARAL
-CSIIgQEPDQgiCIEBDwUCBwcFGAUIgQEOBgMHBgUYBQiBAQ0GBQcFBRgFCIEBDAcGBgUFGAUIgQELBwgE
-BgUYBQiBAQoHCgMGBRgFCIEBCQcMAQcFGAUIgQEIBxUFGAUIgQEHCBUiCIEBBggWIgiBAQQJFyIIgQEF
-BxgiCIEBBQUaBRgFCIEBBgMbBRgFCIEBJAUYBQiBAU6BAU6BAU6B0A==',
-'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQEZBi+BARkGL4EBGgUvgQEaBS+BARoFL4EBGgUvgQEaBS+BARoF
-L4EBGgUvgQEaBRkBFYEBGgUZAxOBARoFDwEIBRKBARoFCwUIBxCBARoFBgoHCg6BARoVCQkNgQEJJgsJ
-C4EBCSYMCQqBAQohEgkIgQEKGxoIB4EBChUhCQWBARkFIwkEgQEZBSUGBYEBGQUmBAaBARkFKAIGgQEZ
-BTCBARkFMIEBGQUwgQEZBTCBARkFMIEBGQUwgQEZBTCBARkFCRAXgQEZBQQYFIEBGSMSgQEZJBGBARkS
-CAwPgQEXDhIJDoEBFQwYCA2BARMMGwcNgQESDB0HDIEBEA4eBgyBAQ8IAwQfBguBAQ4IBAQfBguBAQ0H
-BgQgBQuBAQwHBwQgBQuBAQsHCAUfBQuBAQoGCgUfBQuBAQoGCgUfBQuBAQkGCwUfBQuBAQkFDAUeBguB
-AQgGDAUeBguBAQgGDAUdBwuBAQgGDAUdBgyBAQgGDAUcBwyBAQkFDAUbBw2BAQkGCwUZCQ2BAQkHCgUY
-CQ6BAQoIBwYVCw+BAQsIBQcSDRCBAQwSChURgQENEQoTE4EBDhAKERWBARANDA4XgQESCwwLGoEBFQYO
-BSCBAU6BAU6BAU6BAU6BAU6BAU6B0A==',
-'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQEuBhqBAS4FG4EBLgUbgQEuBRuBARICGQYbgQEPBRkGG4EBDgYZ
-BRyBAQ8FGQUcgQEPBhgFHIEBDwYXBhyBARAFFwUdgQEQBRcFHYEBEAYNERqBAREFChcXgQERBQccFYEB
-EQYEIBOBARIFAg4DExGBARIRBwYEChCBARIOCgUHCQ+BARMKDQUJCA6BARIJDgYKCA2BAREIEAUNBwyB
-ARAJEAUOBgyBAQ8KDwYPBguBAQ4MDgUQBwqBAQ4MDgURBgqBAQ0GAgYMBRMFCoEBDAYEBQwFEwYJgQEM
-BgQFCwYTBgmBAQsGBQYKBRUFCYEBCgYHBQkGFQYIgQEKBQgGCAYVBgiBAQkGCQUIBRcFCIEBCQUKBgYG
-FwUIgQEJBQsFBgUYBQiBAQgFDAYEBhgFCIEBCAUNBQMGGQUIgQEIBQ0GAgYZBQiBAQcFDwwaBQiBAQcF
-DwwaBQiBAQcFEAobBQiBAQcFEAoaBgiBAQcFEQgbBgiBAQcFEgYcBgiBAQcFEQgbBQmBAQcFEAoZBgmB
-AQcFEAoZBgmBAQcFDwwXBgqBAQcFDg4VBwqBAQcGDAcCBxQGC4EBCAULBwQEFQcLgQEIBggIBgIVBwyB
-AQgIBAkHARUHDYEBCRMcCQ2BAQoRHAkOgQELDhwKD4EBDAwbChGBAQ4HGwwSgQEsDxOBASgRFYEBKQ4X
-gQEpDBmBASoIHIEBKwMggQFOgQFOgQFOgQFOgQFOgQFOgQFOgdA=',
-'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQE1DwqBASkbCoEBHiYKgQETMQqBAQc9CoEBBjQU
-gQEGIQQKGYEBBhcOBxyBAQcOFAcegQEHBhsHH4EBJwYhgQEmBiKBASUGFgEMgQEkBhUDDIEBIwYWBAuB
-ASMGFwQKgQEiBg8DBgQKgQEhBg8EBwQJgQEhBhAEBwQIgQEgBhEFBgQIgQEgBRMEBwQHgQEfBhQEBgUG
-gQEfBhQEBwQGgQEfBRYEBgIIgQEeBhYEEIEBHgYXBA+BAR4FGAMQgQEeBSuBAR0GK4EBHQYrgQEdBiuB
-AR0GK4EBHQYrgQEdBiuBAR0GK4EBHQYrgQEdBiuBAR4FK4EBHgYqgQEeBiqBAR4HKYEBHwYpgQEfByiB
-ASAHJ4EBIAcngQEhByaBASEJJIEBIgkjgQEjCiGBASQLH4EBJQwdgQEmDxmBASgTE4EBKhMRgQErEhGB
-AS4PEYEBMAwSgQE0CBKBAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B0A==',
-'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQEXATaBARQENoEBEgY2gQESBzWBARMGNYEBEwc0gQEUBjSB
-ARQGNIEBFQYzgQEVBiABEoEBFgYeAxGBARYGHAYQgQEWBxoHEIEBFwYYCg+BARcHFQsQgQEYBhMLEoEB
-GAYRCxSBARkGDgsWgQEZBgwLGIEBGgYJCxqBARoHBwocgQEbBgUKHoEBGwcCCiCBARwRIYEBHA8jgQEd
-DCWBAR0KJ4EBHAoogQEbCSqBARoILIEBGQgtgQEXCC+BARYIMIEBFQgxgQEVBzKBARQHM4EBEwc0gQES
-BzWBARIGNoEBEQY3gQERBjeBAREFOIEBEAY4gQEQBjiBARAGOIEBEAY4gQEQBjiBARAGOIEBEAY4gQEQ
-BjiBARAHN4EBEAc3gQERBzaBAREINYEBEgkzgQETCiEDDYEBEw0WCw2BARQtDYEBFisNgQEXKg2BARon
-DYEBHSARgQEjDxyBAU6BAU6BAU6BAU6BAU6BAU6BAU6B0A==',
-'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQEXBDOBARcLLIEBFxQjgQEXIRaBARchFoEBGh4WgQEiFhaBASsN
-FoEBNgEXgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQEkDR2BAR4WGoEBGR0YgQEVIxaBARApFYEB
-DRcLCxSBAQ4QFAkTgQEODBoIEoEBDgkeBxKBAQ4GIgcRgQEPAiYGEYEBNwcQgQE4BhCBATgGEIEBOAYQ
-gQE4BhCBATkFEIEBOQUQgQE5BRCBATgGEIEBOAYQgQE4BhCBATgGEIEBNwcQgQE3BhGBATcGEYEBNgcR
-gQE1BxKBATUHEoEBNAcTgQEzCBOBATIIFIEBMQgVgQEvCRaBAS4JF4EBLAoYgQEqCxmBAScMG4EBJQ0c
-gQEhDx6BARwTH4EBGBQigQEZESSBARoNJ4EBGgoqgQEbBS6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B0A==',
-'0AFOgQFOgQFOgQFOgQFOgQFDAgmBAUEECYEBQgQIgQE7AQYFB4EBOQQGBAeBATkEBwQGgQE5BQYFBYEB
-OgQHBAWBATsEBgUEgQE7BQYEBIEBPAQGBASBATwFBgEGgQEQBhsIBAQNgQEQKQQFDIEBECkFAQ+BARAp
-FYEBECkVgQEQKRWBAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B
-AU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAQ0EPYEBDAY8gQEMBjyBAQwGPIEBDAU9gQEMBT2BAQsG
-PYEBCwY9gQELBj2BAQsGPYEBCwY9gQELBj2BAQsHPIEBCwc8gQEMBzuBAQwIOoEBDQg5gQENCiUCEIEB
-DgwdBxCBAQ8QDhEQgQEQLhCBAREtEIEBEysQgQEWJROBARobGYEBToEBToEBToEBToEBToHQ',
-'0AFOgQFOgQFOgQFOgQFCAwmBAUEECYEBQQUIgQE5AwYFB4EBOAQHBAeBASkCDgQGBQaBASYGDQUGBAaB
-ASYGDgQHBAWBASYGDgUGBAWBAScFDwQHBASBAScGDwQGAwWBAScGDwUNgQEoBRAEDYEBKAUQAw6BASgG
-IIEBKQUQAw2BASkFDAcNgQEpBgYMDYEBCgMdFw2BAQsVAh8NgQELNQ6BAQsxEoEBCysYgQELJh2BARkI
-CwUdgQEsBhyBAS0FHIEBLQUcgQEuBRuBAS4FG4EBLwUagQEvBRqBATAFGYEBMAYYgQExBRiBATEGF4EB
-MgUXgQEyBhaBATMGFYEBNAUVgQE0BhSBATUGE4EBEAIWCgMHEoEBEAYSFRGBAQ8GExURgQEPBRQUEoEB
-DgYaDROBAQ4GIwQTgQEOBTuBAQ0GO4EBDQY7gQENBTyBAQ0FPIEBDQU8gQENBTyBAQ0FPIEBDQU8gQEN
-BjuBAQ0GO4EBDgY6gQEOBzmBAQ4IOIEBDwg3gQEQCh4BFYEBEQwVBxWBARInFYEBEyYVgQEVJBWBARgh
-FYEBGxoZgQFOgQFOgQFOgQFOgdA=',
-'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQEJBz6BAQkGP4EBCQY/gQEJBigDFIEB
-CQYlBhSBAQkGJQcTgQEJBiYHEoEBCQYnBhKBAQkGJwcRgQEJBigGEYEBCQYoBxCBAQkGKQYQgQEJBioG
-D4EBCQYqBg+BAQkGKgcOgQEJBisGDoEBCgUrBg6BAQoFLAYNgQEKBSwGDYEBCgUtBgyBAQoFLQYMgQEK
-BS0GDIEBCgUuBguBAQoFLgYLgQEKBS4GC4EBCgYtBguBAQoGLgYKgQEKBi4GCoEBCgYuBgqBAQoGLwYJ
-gQELBS8GCYEBCwUvBgmBAQsFLwYJgQELBhMBGgYJgQELBhMCGgYIgQELBhMDGQYIgQEMBRMEGAYIgQEM
-BhEGFwYIgQEMBhEGFwYIgQEMBhAGGQUIgQENBg8GGQUIgQENBg8GGQUIgQENBg4GGgILgQEOBg0GJ4EB
-DgcLBiiBAQ4HCgcogQEPBwgHKYEBEAcGCCmBARAKAQkqgQEREiuBARIRK4EBEhAsgQETDi2BARULLoEB
-FwcwgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgdA=',
-'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQElBiOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUjgQEmBSOBASYF
-I4EBJgUQBQ6BAQ4IEAUGDw6BAQ4yDoEBDjIOgQEOMg6BAQ4rFYEBGhIigQEmBSOBASYFI4EBJgUjgQEm
-BSOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUjgQEmBRQCDYEBDQMWBQwKDYEBDQ8JHA2BAQ4zDYEBDjMN
-gQEOMg6BARAnF4EBJQYjgQEmBSOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUj
-gQEmBSOBAR0EBQUjgQEXFCOBARQZIYEBEh4egQERIRyBARAJDBAZgQEPBxARF4EBDgYSExWBAQ4FEwYD
-CxSBAQ4FEwYFCxKBAQ0FFAYHChGBAQ0FFAYJCg+BAQ0FFAYKCg6BAQ0GEwYMCQ2BAQ4FEwYNCQyBAQ4G
-EQYQBg2BAQ4HDwcRBQ2BAQ8IDAgSAw6BAQ8bFAEPgQERGCWBARIWJoEBFBIogQEXDSqBAU6BAU6BAU6B
-AU6BAU6BAU6B0A==',
-'0AFOgQFOgQFOgQFOgQFOgQFOgQEpBh+BASkGH4EBKQYfgQEqBR+BASoFH4EBKgUfgQEqBR+BASoFH4EB
-KgUfgQEqBR+BARkuB4EBBkEHgQEHQAeBAQdAB4EBB0AHgQEHDBcFH4EBKgUfgQEqBR+BASoFH4EBKgUf
-gQEqBR+BASoFH4EBKgUfgQEgDx+BAR4RH4EBHBMfgQEbFB+BARoIBAkfgQEZBwkGH4EBGQYLBh6BARgG
-DQUegQEYBQ4GHYEBFwYPBR2BARcFEAUdgQEXBRAFHYEBFwUQBhyBARcFEAYcgQEXBQ8HHIEBFwUPBxyB
-ARcGDgccgQEXBg0IHIEBGAYMBx2BARgHCggdgQEZCAYKHYEBGhcdgQEbFh2BARwVHYEBHgsBBh6BASAG
-BAYegQEpBh+BASkGH4EBKAYggQEnByCBASYHIYEBJQcigQEkCCKBASIJI4EBIAokgQEeCyWBARwLJ4EB
-GQ0ogQEWDiqBARcMK4EBGAktgQEZBTCBARoCMoEBToEBToEBToEBToEBToEBToHQ',];
-$m="";for($k=0;$k<min(13,intdiv(__LINE__-119,80)+1);$k++){$C=str_replace("\n","",
-$c[$k]);$f=!0;foreach(str_split(base64_decode($C))as$l){$L=ord($l);$m.=str_repeat
-($f?"#":chr(32),$L&127);$f=!$f;if($L&128){$m.="\n";$f=!0;continue;}}}print(
-str_replace([chr(96),chr(37),chr(64)],[implode("\n",array_map(fn($C)=>"'".trim(
-chunk_split(str_replace("\n","",$C),80,"\n"))."',",$c)),"\n{$m}","{$s}\nQ;\n"],$s));
+ <?php
+$s=<<<'Q'
+<?php
+%
+$s=<<<'Q'
+@$c=[`];
+$m="";for($k=0;$k<min(13,intdiv(__LINE__-119,80)+1);$k++){$C=str_replace("\n","",
+$c[$k]);$f=!0;foreach(str_split(base64_decode($C))as$l){$L=ord($l);$m.=str_repeat
+($f?"#":chr(32),$L&127);$f=!$f;if($L&128){$m.="\n";$f=!0;continue;}}}print(
+str_replace([chr(96),chr(37),chr(64)],[implode("\n",array_map(fn($C)=>"'".trim(
+chunk_split(str_replace("\n","",$C),80,"\n"))."',",$c)),"\n{$m}","{$s}\nQ;\n"],$s));
+Q;
+$c=['0AFOgQFOgQFOgQFOgQFOgQFEAgiBAUIECIEBQwQHgQE8AQYFBoEBOgQGBAaBAToEBwQFgQE6BQYFBIEB
+OwQHBASBATwEBgUDgQE8BQYEA4EBPQQGBAOBAT0FBgEFgQERBhsIBAQMgQERKQQFC4EBESkFAQ6BAREp
+FIEBESkUgQERKRSBAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B
+AU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAQ4EPIEBDQY7gQENBjuBAQ0GO4EBDQU8gQENBTyBAQwG
+PIEBDAY8gQEMBjyBAQwGPIEBDAY8gQEMBjyBAQwHO4EBDAc7gQENBzqBAQ0IOYEBDgg4gQEOCiUCD4EB
+DwwdBw+BARAQDhEPgQERLg+BARItD4EBFCsPgQEXJRKBARsbGIEBToEBToEBToEBToEBToHQ',
+'0AFOgQFOgQFOgQEPASMFFoEBDwMhBRaBAQ4FIAUWgQEOBSAFFoEBDQUhBRaBAQ0FIQUWgQEMBSIFFoEB
+DAUiBRaBAQsFIwUWgQELBQgBGgUWgQEKBQkDGAUWgQEKBQgGBCoDgQEJBQkFBSoDgQEFAQMECQYFKgOB
+AQQDAQUJBQYqA4EBBAgJBQcqA4EBAwkJBRkFFoEBBAcJBRoFFoEBBQYIBRsFFoEBBgYHBRsFFoEBBwYF
+BRwFFoEBCAYDBR0FFoEBCQYCBR0FFoEBCgseBRaBAQsJHwUWgQEMByAFFoEBDAcFAxgFFoEBDQUFBBgF
+FoEBDQQGBRYGFoEBDAUHBAcmBYEBCwUIBQYmBYEBCwQKBAYmBYEBCgQLBQUmBYEBCQUMBS+BAQgFDAYv
+gQEDHC+BAQMdLoEBAx0ugQEDHi2BAQMJBAUIBC2BARAFCAQtgQEQBQgELYEBEAUJAS+BARAFESEHgQEQ
+BREhB4EBBwEIBQYBCiEHgQEHBAUFBAQJIQeBAQcEBQUEBAkEGAUHgQEGBQUFBAUIBBgFB4EBBgUFBQUE
+CAQYBQeBAQYFBQUFBAgEGAUHgQEGBAYFBQUHBBgFB4EBBgQGBQYEBwQYBQeBAQUFBgUGBQYEGAUHgQEF
+BQYFBwQGBBgFB4EBBQQHBQcEBgQYBQeBAQUEBwUHBQUEGAUHgQEEBQcFBwUFBBgFB4EBBAUHBQgEBQQY
+BQeBAQQECAUIBAUEGAUHgQEDBQgFCAEIBBgFB4EBAwUIBREEGAUHgQECBQkFEQQYBQeBAQIFCQURIQeB
+AQQCCgURIQeBARAFESEHgQEQBREhB4EBEAURIQeBARAFEQQYBQeBARAFEQQYBQeBARAFEQQYBQeBARAF
+EQQYBQeBAU6BAU6BAU6B0A==',
+'0AFOgQFOgQFOgQEOAjEBDIEBDgUqBwqBAQ0FJwwJgQENBSETCIEBDQURAQcXDIEBDQURGxCBAQ0FERcU
+gQENBBIOBAUUgQEMBRIGDAUUgQEMBRIFDQUUgQEMBRIFDgQUgQEMBRIFDgQUgQEMBBMFDgQUgQELBRMF
+DgQUgQELBRMFDgUTgQELBRMFDgUTgQEDGQcFDgUTgQEDGwUoA4EBAxsFKAOBAQMbBSgDgQEDGwUoA4EB
+CgUKBQUFDwUSgQEKBAsEBgUQBBKBAQkFCwQGBRAFEYEBCQULBAYFEAURgQEJBQoFBgUQBRGBAQkFCgUG
+BREFEIEBCQQLBQYFEQUQgQEIBQsFBgUSBQkBBYEBCAULBQYFEgUJAwOBAQgFCwUGBQoFBAUIBAKBAQgF
+CwQHBQQLBAYHAwOBAQgECwUHFAUGBQQDgQEHBQsFAxgFBwQEA4EBBwULBQMVCQ4DgQEHBQsFAw8QDQOB
+AQcEDAUDCRcLBIEBBgULBQUCHwgFgQEGBQsFKAQHgQEGBQsFM4EBBgULBTOBAQYFCgUKIgiBAQUHCQUK
+IgiBAQUICAUKIgiBAQUKBgUKIgiBAQULBAULBRgFCIEBBA0DBQsFGAUIgQEEBQIHAgULBRgFCIEBBgMD
+DAwFGAUIgQENCwwFGAUIgQEOCgwFGAUIgQEQBw0FGAUIgQERBwwFGAUIgQERCAsiCIEBEAoKIgiBARAL
+CSIIgQEPDQgiCIEBDwUCBwcFGAUIgQEOBgMHBgUYBQiBAQ0GBQcFBRgFCIEBDAcGBgUFGAUIgQELBwgE
+BgUYBQiBAQoHCgMGBRgFCIEBCQcMAQcFGAUIgQEIBxUFGAUIgQEHCBUiCIEBBggWIgiBAQQJFyIIgQEF
+BxgiCIEBBQUaBRgFCIEBBgMbBRgFCIEBJAUYBQiBAU6BAU6BAU6B0A==',
+'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQEZBi+BARkGL4EBGgUvgQEaBS+BARoFL4EBGgUvgQEaBS+BARoF
+L4EBGgUvgQEaBRkBFYEBGgUZAxOBARoFDwEIBRKBARoFCwUIBxCBARoFBgoHCg6BARoVCQkNgQEJJgsJ
+C4EBCSYMCQqBAQohEgkIgQEKGxoIB4EBChUhCQWBARkFIwkEgQEZBSUGBYEBGQUmBAaBARkFKAIGgQEZ
+BTCBARkFMIEBGQUwgQEZBTCBARkFMIEBGQUwgQEZBTCBARkFCRAXgQEZBQQYFIEBGSMSgQEZJBGBARkS
+CAwPgQEXDhIJDoEBFQwYCA2BARMMGwcNgQESDB0HDIEBEA4eBgyBAQ8IAwQfBguBAQ4IBAQfBguBAQ0H
+BgQgBQuBAQwHBwQgBQuBAQsHCAUfBQuBAQoGCgUfBQuBAQoGCgUfBQuBAQkGCwUfBQuBAQkFDAUeBguB
+AQgGDAUeBguBAQgGDAUdBwuBAQgGDAUdBgyBAQgGDAUcBwyBAQkFDAUbBw2BAQkGCwUZCQ2BAQkHCgUY
+CQ6BAQoIBwYVCw+BAQsIBQcSDRCBAQwSChURgQENEQoTE4EBDhAKERWBARANDA4XgQESCwwLGoEBFQYO
+BSCBAU6BAU6BAU6BAU6BAU6BAU6B0A==',
+'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQEuBhqBAS4FG4EBLgUbgQEuBRuBARICGQYbgQEPBRkGG4EBDgYZ
+BRyBAQ8FGQUcgQEPBhgFHIEBDwYXBhyBARAFFwUdgQEQBRcFHYEBEAYNERqBAREFChcXgQERBQccFYEB
+EQYEIBOBARIFAg4DExGBARIRBwYEChCBARIOCgUHCQ+BARMKDQUJCA6BARIJDgYKCA2BAREIEAUNBwyB
+ARAJEAUOBgyBAQ8KDwYPBguBAQ4MDgUQBwqBAQ4MDgURBgqBAQ0GAgYMBRMFCoEBDAYEBQwFEwYJgQEM
+BgQFCwYTBgmBAQsGBQYKBRUFCYEBCgYHBQkGFQYIgQEKBQgGCAYVBgiBAQkGCQUIBRcFCIEBCQUKBgYG
+FwUIgQEJBQsFBgUYBQiBAQgFDAYEBhgFCIEBCAUNBQMGGQUIgQEIBQ0GAgYZBQiBAQcFDwwaBQiBAQcF
+DwwaBQiBAQcFEAobBQiBAQcFEAoaBgiBAQcFEQgbBgiBAQcFEgYcBgiBAQcFEQgbBQmBAQcFEAoZBgmB
+AQcFEAoZBgmBAQcFDwwXBgqBAQcFDg4VBwqBAQcGDAcCBxQGC4EBCAULBwQEFQcLgQEIBggIBgIVBwyB
+AQgIBAkHARUHDYEBCRMcCQ2BAQoRHAkOgQELDhwKD4EBDAwbChGBAQ4HGwwSgQEsDxOBASgRFYEBKQ4X
+gQEpDBmBASoIHIEBKwMggQFOgQFOgQFOgQFOgQFOgQFOgQFOgdA=',
+'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQE1DwqBASkbCoEBHiYKgQETMQqBAQc9CoEBBjQU
+gQEGIQQKGYEBBhcOBxyBAQcOFAcegQEHBhsHH4EBJwYhgQEmBiKBASUGFgEMgQEkBhUDDIEBIwYWBAuB
+ASMGFwQKgQEiBg8DBgQKgQEhBg8EBwQJgQEhBhAEBwQIgQEgBhEFBgQIgQEgBRMEBwQHgQEfBhQEBgUG
+gQEfBhQEBwQGgQEfBRYEBgIIgQEeBhYEEIEBHgYXBA+BAR4FGAMQgQEeBSuBAR0GK4EBHQYrgQEdBiuB
+AR0GK4EBHQYrgQEdBiuBAR0GK4EBHQYrgQEdBiuBAR4FK4EBHgYqgQEeBiqBAR4HKYEBHwYpgQEfByiB
+ASAHJ4EBIAcngQEhByaBASEJJIEBIgkjgQEjCiGBASQLH4EBJQwdgQEmDxmBASgTE4EBKhMRgQErEhGB
+AS4PEYEBMAwSgQE0CBKBAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B0A==',
+'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQEXATaBARQENoEBEgY2gQESBzWBARMGNYEBEwc0gQEUBjSB
+ARQGNIEBFQYzgQEVBiABEoEBFgYeAxGBARYGHAYQgQEWBxoHEIEBFwYYCg+BARcHFQsQgQEYBhMLEoEB
+GAYRCxSBARkGDgsWgQEZBgwLGIEBGgYJCxqBARoHBwocgQEbBgUKHoEBGwcCCiCBARwRIYEBHA8jgQEd
+DCWBAR0KJ4EBHAoogQEbCSqBARoILIEBGQgtgQEXCC+BARYIMIEBFQgxgQEVBzKBARQHM4EBEwc0gQES
+BzWBARIGNoEBEQY3gQERBjeBAREFOIEBEAY4gQEQBjiBARAGOIEBEAY4gQEQBjiBARAGOIEBEAY4gQEQ
+BjiBARAHN4EBEAc3gQERBzaBAREINYEBEgkzgQETCiEDDYEBEw0WCw2BARQtDYEBFisNgQEXKg2BARon
+DYEBHSARgQEjDxyBAU6BAU6BAU6BAU6BAU6BAU6BAU6B0A==',
+'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQEXBDOBARcLLIEBFxQjgQEXIRaBARchFoEBGh4WgQEiFhaBASsN
+FoEBNgEXgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQEkDR2BAR4WGoEBGR0YgQEVIxaBARApFYEB
+DRcLCxSBAQ4QFAkTgQEODBoIEoEBDgkeBxKBAQ4GIgcRgQEPAiYGEYEBNwcQgQE4BhCBATgGEIEBOAYQ
+gQE4BhCBATkFEIEBOQUQgQE5BRCBATgGEIEBOAYQgQE4BhCBATgGEIEBNwcQgQE3BhGBATcGEYEBNgcR
+gQE1BxKBATUHEoEBNAcTgQEzCBOBATIIFIEBMQgVgQEvCRaBAS4JF4EBLAoYgQEqCxmBAScMG4EBJQ0c
+gQEhDx6BARwTH4EBGBQigQEZESSBARoNJ4EBGgoqgQEbBS6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B0A==',
+'0AFOgQFOgQFOgQFOgQFOgQFDAgmBAUEECYEBQgQIgQE7AQYFB4EBOQQGBAeBATkEBwQGgQE5BQYFBYEB
+OgQHBAWBATsEBgUEgQE7BQYEBIEBPAQGBASBATwFBgEGgQEQBhsIBAQNgQEQKQQFDIEBECkFAQ+BARAp
+FYEBECkVgQEQKRWBAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B
+AU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAQ0EPYEBDAY8gQEMBjyBAQwGPIEBDAU9gQEMBT2BAQsG
+PYEBCwY9gQELBj2BAQsGPYEBCwY9gQELBj2BAQsHPIEBCwc8gQEMBzuBAQwIOoEBDQg5gQENCiUCEIEB
+DgwdBxCBAQ8QDhEQgQEQLhCBAREtEIEBEysQgQEWJROBARobGYEBToEBToEBToEBToEBToHQ',
+'0AFOgQFOgQFOgQFOgQFCAwmBAUEECYEBQQUIgQE5AwYFB4EBOAQHBAeBASkCDgQGBQaBASYGDQUGBAaB
+ASYGDgQHBAWBASYGDgUGBAWBAScFDwQHBASBAScGDwQGAwWBAScGDwUNgQEoBRAEDYEBKAUQAw6BASgG
+IIEBKQUQAw2BASkFDAcNgQEpBgYMDYEBCgMdFw2BAQsVAh8NgQELNQ6BAQsxEoEBCysYgQELJh2BARkI
+CwUdgQEsBhyBAS0FHIEBLQUcgQEuBRuBAS4FG4EBLwUagQEvBRqBATAFGYEBMAYYgQExBRiBATEGF4EB
+MgUXgQEyBhaBATMGFYEBNAUVgQE0BhSBATUGE4EBEAIWCgMHEoEBEAYSFRGBAQ8GExURgQEPBRQUEoEB
+DgYaDROBAQ4GIwQTgQEOBTuBAQ0GO4EBDQY7gQENBTyBAQ0FPIEBDQU8gQENBTyBAQ0FPIEBDQU8gQEN
+BjuBAQ0GO4EBDgY6gQEOBzmBAQ4IOIEBDwg3gQEQCh4BFYEBEQwVBxWBARInFYEBEyYVgQEVJBWBARgh
+FYEBGxoZgQFOgQFOgQFOgQFOgdA=',
+'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQEJBz6BAQkGP4EBCQY/gQEJBigDFIEB
+CQYlBhSBAQkGJQcTgQEJBiYHEoEBCQYnBhKBAQkGJwcRgQEJBigGEYEBCQYoBxCBAQkGKQYQgQEJBioG
+D4EBCQYqBg+BAQkGKgcOgQEJBisGDoEBCgUrBg6BAQoFLAYNgQEKBSwGDYEBCgUtBgyBAQoFLQYMgQEK
+BS0GDIEBCgUuBguBAQoFLgYLgQEKBS4GC4EBCgYtBguBAQoGLgYKgQEKBi4GCoEBCgYuBgqBAQoGLwYJ
+gQELBS8GCYEBCwUvBgmBAQsFLwYJgQELBhMBGgYJgQELBhMCGgYIgQELBhMDGQYIgQEMBRMEGAYIgQEM
+BhEGFwYIgQEMBhEGFwYIgQEMBhAGGQUIgQENBg8GGQUIgQENBg8GGQUIgQENBg4GGgILgQEOBg0GJ4EB
+DgcLBiiBAQ4HCgcogQEPBwgHKYEBEAcGCCmBARAKAQkqgQEREiuBARIRK4EBEhAsgQETDi2BARULLoEB
+FwcwgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgdA=',
+'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQElBiOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUjgQEmBSOBASYF
+I4EBJgUQBQ6BAQ4IEAUGDw6BAQ4yDoEBDjIOgQEOMg6BAQ4rFYEBGhIigQEmBSOBASYFI4EBJgUjgQEm
+BSOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUjgQEmBRQCDYEBDQMWBQwKDYEBDQ8JHA2BAQ4zDYEBDjMN
+gQEOMg6BARAnF4EBJQYjgQEmBSOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUj
+gQEmBSOBAR0EBQUjgQEXFCOBARQZIYEBEh4egQERIRyBARAJDBAZgQEPBxARF4EBDgYSExWBAQ4FEwYD
+CxSBAQ4FEwYFCxKBAQ0FFAYHChGBAQ0FFAYJCg+BAQ0FFAYKCg6BAQ0GEwYMCQ2BAQ4FEwYNCQyBAQ4G
+EQYQBg2BAQ4HDwcRBQ2BAQ8IDAgSAw6BAQ8bFAEPgQERGCWBARIWJoEBFBIogQEXDSqBAU6BAU6BAU6B
+AU6BAU6BAU6B0A==',
+'0AFOgQFOgQFOgQFOgQFOgQFOgQEpBh+BASkGH4EBKQYfgQEqBR+BASoFH4EBKgUfgQEqBR+BASoFH4EB
+KgUfgQEqBR+BARkuB4EBBkEHgQEHQAeBAQdAB4EBB0AHgQEHDBcFH4EBKgUfgQEqBR+BASoFH4EBKgUf
+gQEqBR+BASoFH4EBKgUfgQEgDx+BAR4RH4EBHBMfgQEbFB+BARoIBAkfgQEZBwkGH4EBGQYLBh6BARgG
+DQUegQEYBQ4GHYEBFwYPBR2BARcFEAUdgQEXBRAFHYEBFwUQBhyBARcFEAYcgQEXBQ8HHIEBFwUPBxyB
+ARcGDgccgQEXBg0IHIEBGAYMBx2BARgHCggdgQEZCAYKHYEBGhcdgQEbFh2BARwVHYEBHgsBBh6BASAG
+BAYegQEpBh+BASkGH4EBKAYggQEnByCBASYHIYEBJQcigQEkCCKBASIJI4EBIAokgQEeCyWBARwLJ4EB
+GQ0ogQEWDiqBARcMK4EBGAktgQEZBTCBARoCMoEBToEBToEBToEBToEBToEBToHQ',];
+$m="";for($k=0;$k<min(13,intdiv(__LINE__-119,80)+1);$k++){$C=str_replace("\n","",
+$c[$k]);$f=!0;foreach(str_split(base64_decode($C))as$l){$L=ord($l);$m.=str_repeat
+($f?"#":chr(32),$L&127);$f=!$f;if($L&128){$m.="\n";$f=!0;continue;}}}print(
+str_replace([chr(96),chr(37),chr(64)],[implode("\n",array_map(fn($C)=>"'".trim(
+chunk_split(str_replace("\n","",$C),80,"\n"))."',",$c)),"\n{$m}","{$s}\nQ;\n"],$s));
+
$ echo "#iwillblog" | php Q1.png >/dev/null
+ $ echo "#iwillblog" | php Q1.png >/dev/null
+
無事に実行できていれば「#ModernPHPisStaticallyTypedLanguage」というトークンが得られる。 @@ -181,7 +182,8 @@ まずは素直に画像として見てみよう。全体は QR コードになっている。適当な QR コードリーダで読み込むと、次のようなテキストが表示されるはずだ。
Guess password. $ echo "password" | php Q1.png >/dev/null
+ Guess password. $ echo "password" | php Q1.png >/dev/null
+
メッセージは、この画像の実行方法とこの問題でやるべきこと (パスワードの推測) を示している。 @@ -196,8 +198,9 @@ 不正なパスワードを使って実行してみると、次のようなエラーメッセージが表示される。
$ echo "foo" | php Q1.png >/dev/null
-401 Unauthorized
+ $ echo "foo" | php Q1.png >/dev/null
+401 Unauthorized
+
すでに 「解き方」の節 で示したように、パスワードである PHPer トークンは「#iwillblog」である。これを与えて実行すると正解のトークンが得られる。
@@ -267,23 +270,25 @@
strings コマンドを使うと、隠されたデータを簡単に閲覧できる。
IHDR
--HHc
-<PLTE
-IDATx
-IEND
-<?php
-error_reporting(-1);
-$b = unpack('C*', file_get_contents(__FILE__));
-$w = $b[20]+2;
-$h = $b[24]+2;
-// (以下略)
+ IHDR
+-HHc
+<PLTE
+IDATx
+IEND
+<?php
+error_reporting(-1);
+$b = unpack('C*', file_get_contents(__FILE__));
+$w = $b[20]+2;
+$h = $b[24]+2;
+// (以下略)
+
IHDR や IEND が PNG 画像の一部で、<?php からが実際のプログラムになっている。もちろんこれを PHP プログラムとして動かすと、PHP タグより前にある PNG 画像としてのデータはそのまま標準出力へと出力されてしまう。それを防ぐため、QR コードを読み込んだときの実行方法
Guess password. $ echo "password" | php Q1.png >/dev/null
+ Guess password. $ echo "password" | php Q1.png >/dev/null
+
には標準出力を捨てるよう >/dev/null と指定されている。
@@ -298,107 +303,108 @@
画像の正体がわかったところで、画像に隠されていた PHP プログラムについて見ていこう。先ほどは一部しか記載しなかったので、全体を載せる。なお、ある程度ゴルフしながら書いたので、空白こそ残しているものの可読性は非常に低いことと思う。
<?php
-error_reporting(-1);
-$b = unpack('C*', file_get_contents(__FILE__));
-$w = $b[20]+2;
-$h = $b[24]+2;
-$cs = [];
-for ($y = 0; $y < $h; $y++)
- for ($x = 0; $x < $w; $x++)
- $cs[$y*$w + $x] = ($x*$y === 0 || $x === $w-1 || $y === $h-1)
- ? 0
- : $b[122+($y-1)*($w-1)+$x-1];
-$i = stream_isatty(STDIN)
- ? []
- : array_map(ord(...), str_split(trim((string) fgets(STDIN))));
-$m = [];
-$pc = 1*$w+1;
-$dp = 0;
-$cc = 1;
-$c0 = 1;
-$b = 0;
-$ns = 0;
-$o = '';
-while (true) {
- $ns++;
- if ($ns > 1e5) {
- echo "infinite loop detected\n";
- break;
- $c1 = $cs[$pc];
- $y = (6 + intdiv($c1-2, 3) - intdiv($c0-2, 3)) % 6;
- $x = (3 + $c1%3 - $c0%3) % 3;
- match (($c0 !== 1) * ($c1 !== 1) * ($y*3 + $x)) {
- 1 => $m[] = $b,
- 2 => array_pop($m),
- 3 => $m[] = array_pop($m) + array_pop($m),
- 4 => $m[] = (fn($x, $y) => $y - $x)(array_pop($m), array_pop($m)),
- 5 => $m[] = array_pop($m) * array_pop($m),
- 8 => $m[] = array_pop($m) === 0 ? 1 : 0,
- 11 => $cc *= pow(-1, array_pop($m)),
- 12 => $m[] = $m[count($m)-1],
- 13 => $m = (fn($n, $d, $m, $l) => [
- ...array_slice($m, 0, $l-$d),
- ...array_reverse([
- ...array_reverse(array_slice($m, $l-$d, $d-$n)),
- ...array_reverse(array_slice($m, $l-$n)),
- ]),
- ])(array_pop($m), array_pop($m), $m, count($m)),
- 15 => !empty($i) and $m[] = array_shift($i),
- 16 => $o .= sprintf('%d', array_pop($m)),
- 17 => $o .= sprintf('%c', array_pop($m)),
- default => 'nop',
- };
- $c0 = $c1;
- for ($j = 0; $j < 8; $j++) {
- $v = [];
- if ($c1 === 1) {
- $x = $pc % $w;
- $y = intdiv($pc, $w);
- $e = [($y+1)*$w-1, ($h-1)*$w+$x, $y*$w, $x][$dp];
- $z = [1, $w, -1, -$w][$dp];
- for ($ep = $pc; $ep !== $e; $ep += $z)
- if ($cs[$ep] !== 1) break;
- $ep -= $z;
- $pc = $ep;
- } else {
- $q = [$pc];
- $ep = $pc;
- while (!empty($q)) {
- $qq = array_pop($q);
- $v[$qq] = true;
- foreach ([$qq+1, $qq+$w, $qq-1, $qq-$w] as $qp) {
- if ($cs[$qp] !== $c1) continue;
- if (isset($v[$qp])) continue;
- $q[] = $qp;
- $qx = $qp % $w;
- $qy = intdiv($qp, $w);
- $x = $ep % $w;
- $y = intdiv($ep, $w);
- if (
- ($dp === 0 && ($x < $qx || ($x === $qx && ($y<=>$qy) === $cc)))
- || ($dp === 1 && ($y < $qy || ($y === $qy && ($qx<=>$x) === $cc)))
- || ($dp === 2 && ($qx < $x || ($qx === $x && ($qy<=>$y) === $cc)))
- || ($dp === 3 && ($qy < $y || ($qy === $y && ($x<=>$qx) === $cc)))
- )
- $ep = $qp;
- }
- }
- }
- $np = $ep + [1, $w, -1, -$w][$dp];
- if ($cs[$np] !== 0) {
- $b = count(array_keys($v));
- $pc = $np;
- break;
- }
- if ($j === 7) break 2;
- if ($j % 2 === 0) $cc = -$cc;
- if ($j % 2 === 1) $dp = ($dp+1) % 4;
-// The original Piet image is wrong: it outputs 403 error for invalid passwords.
-// Failure of authentication should be notified by 401, not 403.
-// I noticed that one month before PHPerKaigi, but I could not read or write (paint)
-// Piet any longer at that time.
-fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o));
+ <?php
+error_reporting(-1);
+$b = unpack('C*', file_get_contents(__FILE__));
+$w = $b[20]+2;
+$h = $b[24]+2;
+$cs = [];
+for ($y = 0; $y < $h; $y++)
+ for ($x = 0; $x < $w; $x++)
+ $cs[$y*$w + $x] = ($x*$y === 0 || $x === $w-1 || $y === $h-1)
+ ? 0
+ : $b[122+($y-1)*($w-1)+$x-1];
+$i = stream_isatty(STDIN)
+ ? []
+ : array_map(ord(...), str_split(trim((string) fgets(STDIN))));
+$m = [];
+$pc = 1*$w+1;
+$dp = 0;
+$cc = 1;
+$c0 = 1;
+$b = 0;
+$ns = 0;
+$o = '';
+while (true) {
+ $ns++;
+ if ($ns > 1e5) {
+ echo "infinite loop detected\n";
+ break;
+ $c1 = $cs[$pc];
+ $y = (6 + intdiv($c1-2, 3) - intdiv($c0-2, 3)) % 6;
+ $x = (3 + $c1%3 - $c0%3) % 3;
+ match (($c0 !== 1) * ($c1 !== 1) * ($y*3 + $x)) {
+ 1 => $m[] = $b,
+ 2 => array_pop($m),
+ 3 => $m[] = array_pop($m) + array_pop($m),
+ 4 => $m[] = (fn($x, $y) => $y - $x)(array_pop($m), array_pop($m)),
+ 5 => $m[] = array_pop($m) * array_pop($m),
+ 8 => $m[] = array_pop($m) === 0 ? 1 : 0,
+ 11 => $cc *= pow(-1, array_pop($m)),
+ 12 => $m[] = $m[count($m)-1],
+ 13 => $m = (fn($n, $d, $m, $l) => [
+ ...array_slice($m, 0, $l-$d),
+ ...array_reverse([
+ ...array_reverse(array_slice($m, $l-$d, $d-$n)),
+ ...array_reverse(array_slice($m, $l-$n)),
+ ]),
+ ])(array_pop($m), array_pop($m), $m, count($m)),
+ 15 => !empty($i) and $m[] = array_shift($i),
+ 16 => $o .= sprintf('%d', array_pop($m)),
+ 17 => $o .= sprintf('%c', array_pop($m)),
+ default => 'nop',
+ };
+ $c0 = $c1;
+ for ($j = 0; $j < 8; $j++) {
+ $v = [];
+ if ($c1 === 1) {
+ $x = $pc % $w;
+ $y = intdiv($pc, $w);
+ $e = [($y+1)*$w-1, ($h-1)*$w+$x, $y*$w, $x][$dp];
+ $z = [1, $w, -1, -$w][$dp];
+ for ($ep = $pc; $ep !== $e; $ep += $z)
+ if ($cs[$ep] !== 1) break;
+ $ep -= $z;
+ $pc = $ep;
+ } else {
+ $q = [$pc];
+ $ep = $pc;
+ while (!empty($q)) {
+ $qq = array_pop($q);
+ $v[$qq] = true;
+ foreach ([$qq+1, $qq+$w, $qq-1, $qq-$w] as $qp) {
+ if ($cs[$qp] !== $c1) continue;
+ if (isset($v[$qp])) continue;
+ $q[] = $qp;
+ $qx = $qp % $w;
+ $qy = intdiv($qp, $w);
+ $x = $ep % $w;
+ $y = intdiv($ep, $w);
+ if (
+ ($dp === 0 && ($x < $qx || ($x === $qx && ($y<=>$qy) === $cc)))
+ || ($dp === 1 && ($y < $qy || ($y === $qy && ($qx<=>$x) === $cc)))
+ || ($dp === 2 && ($qx < $x || ($qx === $x && ($qy<=>$y) === $cc)))
+ || ($dp === 3 && ($qy < $y || ($qy === $y && ($x<=>$qx) === $cc)))
+ )
+ $ep = $qp;
+ }
+ }
+ }
+ $np = $ep + [1, $w, -1, -$w][$dp];
+ if ($cs[$np] !== 0) {
+ $b = count(array_keys($v));
+ $pc = $np;
+ break;
+ }
+ if ($j === 7) break 2;
+ if ($j % 2 === 0) $cc = -$cc;
+ if ($j % 2 === 1) $dp = ($dp+1) % 4;
+// The original Piet image is wrong: it outputs 403 error for invalid passwords.
+// Failure of authentication should be notified by 401, not 403.
+// I noticed that one month before PHPerKaigi, but I could not read or write (paint)
+// Piet any longer at that time.
+fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o));
+
これは一体なんなのか。ずばり、難解プログラミング言語の一つ Piet のインタプリタである。Piet はピエト・モンドリアン (『赤・青・黄のコンポジション』などで知られる抽象画家) の作品にインスピレーションを受けて作られた、画像をソースコードとするプログラミング言語である。インタプリタは画像の各ピクセルの上を進みながら、色等に応じて特定の処理をおこなっていく。ここでは詳しい言語仕様については解説しないので、気になる方は Wikipedia の記事「Piet」 などを参照してほしい。 @@ -407,7 +413,8 @@ プログラムの冒頭にあるこの箇所
$b = unpack('C*', file_get_contents(__FILE__));
+ $b = unpack('C*', file_get_contents(__FILE__));
+
で __FILE__ つまりこの画像ファイルを読み込んでいる。先ほど Piet は画像をソースコードにしていると説明した。そう、今回の問題の画像ファイル Q1.png は、PHP 製 Piet インタプリタであると同時に、Piet のソースコード画像でもあるのだ。QR コード中央のカラフルな部分が Piet の命令になっている。
@@ -459,11 +466,12 @@
ところで、先ほど掲載した Piet のインタプリタのソースコード末尾には次のような箇所がある。
// The original Piet image is wrong: it outputs 403 error for invalid passwords.
-// Failure of authentication should be notified by 401, not 403.
-// I noticed that one month before PHPerKaigi, but I could not read or write (paint)
-// Piet any longer at that time.
-fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o));
+ // The original Piet image is wrong: it outputs 403 error for invalid passwords.
+// Failure of authentication should be notified by 401, not 403.
+// I noticed that one month before PHPerKaigi, but I could not read or write (paint)
+// Piet any longer at that time.
+fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o));
+
コメントにも書かれているが、この Piet のソースコード画像には誤りがあった。本来 HTTP のステータスコードを真似るのなら、認証の失敗には 401 を返さなければならない。しかし、Piet のソースは 403 を返すように書いてしまっていた。そのことに私が気付いたのは PHPerKaigi 2023 が開催されるひと月前で、その時点で私はこの Piet のソースコードを (ちょうどこの記事でそうなっているのと同じように) 読解できなくなっていた。さらに悪いことに、正しいメッセージ「401 Unauthorized」は元の「403 Forbidden」よりも3文字長い。3文字出力が長くなるということは、それだけ Piet で塗るべきピクセルが増えることを意味する。もはや3文字追加で出力するだけの余白はこの画像に残されていなかった (と思う。腕ききの Piet プログラマならできるかもしれないので挑戦してみてほしい)。
diff --git a/services/nuldoc/public/blog/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2/index.html b/services/nuldoc/public/blog/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2/index.html
index 496d5af3..305c976a 100644
--- a/services/nuldoc/public/blog/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2/index.html
+++ b/services/nuldoc/public/blog/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2/index.html
@@ -128,14 +128,15 @@
YAML 1.1 では、<< という文字列をキーに指定することで、マップをマージすることができた。
x: &base
- a: 123
-# => { "x": { "a": 123 } }
-
-y:
- <<: *base
- b: 456
-# => { "y": { "a": 123, "b": 456 } }
+ x: &base
+ a: 123
+# => { "x": { "a": 123 } }
+
+y:
+ <<: *base
+ b: 456
+# => { "y": { "a": 123, "b": 456 } }
+
1.2 からはこれができなくなる。
diff --git a/services/nuldoc/public/blog/posts/2025-03-27/zip-function-like-command-paste-command/index.html b/services/nuldoc/public/blog/posts/2025-03-27/zip-function-like-command-paste-command/index.html
index 75e953c2..3560fe20 100644
--- a/services/nuldoc/public/blog/posts/2025-03-27/zip-function-like-command-paste-command/index.html
+++ b/services/nuldoc/public/blog/posts/2025-03-27/zip-function-like-command-paste-command/index.html
@@ -80,28 +80,31 @@
a.txt
a1
-a2
-a3
+ a1
+a2
+a3
+
b.txt
b1
-b2
-b3
+ b1
+b2
+b3
+
ab.txt
a1
-b1
-a2
-b2
-a3
-b3
+ a1
+b1
+a2
+b2
+a3
+b3
+
ちょうど Python や Haskell などにある zip 関数のような動きをさせたい。
@@ -113,8 +116,9 @@
記事タイトルに書いたように、paste コマンドを使うと実現できる。
$ paste -d '\
-' a.txt b.txt > ab.txt
+ $ paste -d '\
+' a.txt b.txt > ab.txt
+
paste コマンドは複数のファイルを引数に取り、それらを1行ずつ消費しながら -d で指定した文字で区切って出力する。-d は区切り文字の指定で、デフォルトだとタブ区切りになる。
@@ -123,22 +127,24 @@
ファイル名には - を指定でき、その場合は標準入力から読み込んで出力する。このとき paste - - のように複数回 - を指定すると、指定した回数の行ごとに連結することができる。例えば ab.txt だとこうなる。
$ paste - - < ab.txt
-a1 b1
-a2 b2
-a3 b3
+ $ paste - - < ab.txt
+a1 b1
+a2 b2
+a3 b3
+
これは標準入力を使うとき特有の挙動で、単に同じファイル名を指定してもこうはならない。
$ paste ab.txt ab.txt
-a1 a1
-b1 b1
-a2 a2
-b2 b2
-a3 a3
-b3 b3
+ $ paste ab.txt ab.txt
+a1 a1
+b1 b1
+a2 a2
+b2 b2
+a3 a3
+b3 b3
+
ときどき便利。
diff --git a/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html b/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html
index 52aef34c..74a1f48d 100644
--- a/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html
+++ b/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html
@@ -119,7 +119,7 @@
- 今回頂いたのは審査員賞の一つ eto award (公式の賞の名前に合わせて敬称略) で、“Most Ruby-on-Ruby” Award (『最もRuby on Ruby賞』) として受賞した (IOCCC と同じく、それぞれの賞に個別の名前が付く)。
+ 今回頂いたのは審査員賞の一つ eto award (公式の賞の名前に合わせて敬称略) で、”Most Ruby-on-Ruby” Award (『最もRuby on Ruby賞』) として受賞した (IOCCC と同じく、それぞれの賞に個別の名前が付く)。
ソースコード等はこちら: https://github.com/tric/trick2025/tree/main/10-nsfisis
@@ -161,7 +161,8 @@
表示している。つまり、Ruby プログラムにルビを振った作品である。例えば、先頭の2行目の
順に使ったテクニックを解説していく。
@@ -172,11 +173,12 @@
改めて quine について説明する。Quine とは、自身のソースコードを出力するようなプログラムである。Ruby では様々な方法で quine を書くことができるが、この作品で使っている基本形は以下のようなものである。
変数
補足: 変数名がやたら短いのは、このあとの振り仮名データの量を削減するため。
@@ -216,20 +219,21 @@
トークン種別に応じた色付けは CSS でおこなっている。出力する HTML のクラス名に
トークン種別の列挙にはそれなりに文字数を使ってしまうのだが、今回の TRICK のレギュレーションでは
トークンの種類 (
例えば
これは後で気付いたのだが、Ruby は多倍長整数が扱えるので
GNU patch を Homebrew などの手段でインストールし、BSD patch よりも優先されるパスに配置すれば問題が解消する。
diff --git a/services/nuldoc/public/blog/posts/2025-05-05/make-tiny-self-hosted-c-compiler/index.html b/services/nuldoc/public/blog/posts/2025-05-05/make-tiny-self-hosted-c-compiler/index.html
index 29af749d..e2e8bb11 100644
--- a/services/nuldoc/public/blog/posts/2025-05-05/make-tiny-self-hosted-c-compiler/index.html
+++ b/services/nuldoc/public/blog/posts/2025-05-05/make-tiny-self-hosted-c-compiler/index.html
@@ -359,9 +359,10 @@
compilerbook では整数一つのパース・コード生成から始めるが、今回は以下のようなソースをパースしてコード生成するところからスタートすることにした。
この時点で、作品紹介
require は次のような HTML で構成されている。
+ <ruby class="IDENTIFIER">require<rp class="">(</rp><rt class="">リクワイア</rt><rp class="">)</rp></ruby><ruby class="IDENTIFIER">require<rp class="">(</rp><rt class="">リクワイア</rt><rp class="">)</rp></ruby>
+
+ eval $s=<<'EOS'
-print "eval $s=<<'EOS'\n"
-print $s
-print "EOS\n"
-EOSeval $s=<<'EOS'
+print "eval $s=<<'EOS'\n"
+print $s
+print "EOS\n"
+EOS
+$s に 2 行目、3 行目、4 行目が入っており、それに加えて 1 行目と 5 行目を出力すれば元のソースコードが得られる。実際には $s を加工してシンタックスハイライトや振り仮名を振ることになる。
@@ -191,23 +193,24 @@
トークナイズには Ruby 3.4 からデフォルトのパーサになった Prism を利用している。Prism.lex() を使うとトークナイズができるので、トークンに付いているソースコード位置の情報を使いつつ元のソースコードを復元する。
+ y = 1 # 現在の行
-x = 0 # 現在の列
-Prism.lex($s).value[..-2].each {|t, *|
- l = t.location
- r = l.start_line # トークンの開始行
- if y < r # 改行が必要なら
- p "\n" * (r - y) # 改行を挿入して
- x = 0 # 列の先頭へ戻る
- end
- c = l.start_column # トークンの開始列
- if x < c # 空白が必要なら
- p " " * (c - x) # 空白を挿入
- end
- p ruby(t) # トークン本体を出力
- y = l.end_line # 現在行を更新
- x = l.end_column # 現在列を更新
-}y = 1 # 現在の行
+x = 0 # 現在の列
+Prism.lex($s).value[..-2].each {|t, *|
+ l = t.location
+ r = l.start_line # トークンの開始行
+ if y < r # 改行が必要なら
+ p "\n" * (r - y) # 改行を挿入して
+ x = 0 # 列の先頭へ戻る
+ end
+ c = l.start_column # トークンの開始列
+ if x < c # 空白が必要なら
+ p " " * (c - x) # 空白を挿入
+ end
+ p ruby(t) # トークン本体を出力
+ y = l.end_line # 現在行を更新
+ x = l.end_column # 現在列を更新
+}
+Prism::Token#type を指定しておいて、index.html でそれぞれのクラスにスタイルを当てた。
+ <style>
- /* ... */
-
- .COMMENT {
- color: #777;
- font-style: italic;
- }
-
- .CONSTANT, .GLOBAL_VARIABLE, .INSTANCE_VARIABLE, .IDENTIFIER {
- color: #088;
- }
-
- /* ... */
- </style> <style>
+ /* ... */
+
+ .COMMENT {
+ color: #777;
+ font-style: italic;
+ }
+
+ .CONSTANT, .GLOBAL_VARIABLE, .INSTANCE_VARIABLE, .IDENTIFIER {
+ color: #088;
+ }
+
+ /* ... */
+ </style>
+index.html にサイズ制限がなかったので好きに色を付けることができた。
@@ -241,27 +245,28 @@
それぞれの英単語や記号に対応した振り仮名のデータは、プログラム中に埋め込まれている。
+ def rt(t)
- r = {
- :"&&" => "1136",
- :"=" => "04199275",
- :"||" => "623147",
- :$s => "41750825",
- :* => "111775",
- # ...
- type: "310455",
- utf_8: "70923803920853080440",
- value: "48746992",
- x: "08351525",
- y: "7904",
- }
- kana(
- r[:"#{t.type}"] ||
- r[s = :"#{t.value.downcase}"] ||
- s.end_with?(":") && r[:"#{s[..-2]}"] ||
- nil
- )
-enddef rt(t)
+ r = {
+ :"&&" => "1136",
+ :"=" => "04199275",
+ :"||" => "623147",
+ :$s => "41750825",
+ :* => "111775",
+ # ...
+ type: "310455",
+ utf_8: "70923803920853080440",
+ value: "48746992",
+ x: "08351525",
+ y: "7904",
+ }
+ kana(
+ r[:"#{t.type}"] ||
+ r[s = :"#{t.value.downcase}"] ||
+ s.end_with?(":") && r[:"#{s[..-2]}"] ||
+ nil
+ )
+end
+t.type) またはトークンの文字列表現そのもの (t.value.downcase) を使ってテーブルを引いて振り仮名へ変換している。このテーブルのキー部分そのものにも振り仮名を振るために、トークンが : で終わっていれば : を取り除いて振り仮名を得ている (例: "value:" → "value" → "48746992")。
@@ -270,25 +275,27 @@
このテーブルはサイズ制限を突破するために圧縮されており、kana() 関数で展開される。
+ def kana(s)
- s
- &.scan(/.{2}/)
- &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
- &.*("")
-enddef kana(s)
+ s
+ &.scan(/.{2}/)
+ &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
+ &.*("")
+end
+value に対応する振り仮名データ "48746992" であれば、次のような変換を経て振り仮名へと展開される。
+ s
- # => "48746992"
- &.scan(/.{2}/)
- # => ["48", "74", "69", "92"]
- &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
- # => ["バ", "リ", "ュ", "ー"]
- &.*("")
- # => "バリュー" s
+ # => "48746992"
+ &.scan(/.{2}/)
+ # => ["48", "74", "69", "92"]
+ &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
+ # => ["バ", "リ", "ュ", "ー"]
+ &.*("")
+ # => "バリュー"
+"48746992" のようなデータは単に 48746992 と書けばよかった。kana() 関数が多少長くはなるが、振り仮名データの数 x 2 バイト分サイズが減るのでこちらの方が短くなる。サイズ制限の都合で振り仮名を振るのを諦めた記号もあったのでもったいない。
diff --git a/services/nuldoc/public/blog/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos/index.html b/services/nuldoc/public/blog/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos/index.html
index 7fb14431..70d84caa 100644
--- a/services/nuldoc/public/blog/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos/index.html
+++ b/services/nuldoc/public/blog/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos/index.html
@@ -126,8 +126,9 @@
ワークアラウンドとして、macOS にも GNU patch をインストールしてしまうという方法がある。例:
+ $ brew install gpatch
-$ echo 'PATH="/opt/homebrew/opt/gpatch/libexec/gnubin:$PATH"' >> ~/.zshrc$ brew install gpatch
+$ echo 'PATH="/opt/homebrew/opt/gpatch/libexec/gnubin:$PATH"' >> ~/.zshrc
+
+ int main() {
- return 42;
-}int main() {
+ return 42;
+}
+struct Token、struct Parser、struct AstNode、struct CodeGen といった主要なデータ構造が定義され、この後もほぼ同じソース設計のまま進めている。
@@ -408,23 +409,24 @@
一日の終わりには、次のようなプログラムのテストが通るようになった。
+ int printf();
-
-int main() {
- int i;
- for (i = 1; i <= 100; i = i + 1) {
- if (i % 15 == 0) {
- printf("FizzBuzz\n");
- } else if (i % 3 == 0) {
- printf("Fizz\n");
- } else if (i % 5 == 0) {
- printf("Buzz\n");
- } else {
- printf("%d\n", i);
- }
- }
- return 0;
-}int printf();
+
+int main() {
+ int i;
+ for (i = 1; i <= 100; i = i + 1) {
+ if (i % 15 == 0) {
+ printf("FizzBuzz\n");
+ } else if (i % 3 == 0) {
+ printf("Fizz\n");
+ } else if (i % 5 == 0) {
+ printf("Buzz\n");
+ } else {
+ printf("%d\n", i);
+ }
+ }
+ return 0;
+}
+
gen_expr(g, ast->expr1, GEN_RVAL);
- } else {
- gen_expr(g, ast->expr1, GEN_RVAL);
-- gen_lval2rval(ast->expr1->ty);
-+ gen_lval2rval(ast->expr1->ty->to);
- }
- }
+ gen_expr(g, ast->expr1, GEN_RVAL);
+ } else {
+ gen_expr(g, ast->expr1, GEN_RVAL);
+- gen_lval2rval(ast->expr1->ty);
++ gen_lval2rval(ast->expr1->ty->to);
+ }
+ }
+
メモリアドレスから参照先の値を得る際、その型によってロードする命令の種類を変える必要があるのだが、その切替をポインタ型でおこなっていた。正しくは、そのポインタ型が指す型を元にして切り替えなければならない。
@@ -534,16 +537,17 @@
一体どこが異なるのか。hexdump の差分がこちら。
$ diff -u <(hexdump -C p4dcc2) <(hexdump -C p4dcc3)
-@@ -5090,7 +5090,7 @@
- 00015db0 72 72 61 79 5f 65 6e 74 72 79 00 66 72 61 6d 65 |rray_entry.frame|
- 00015dc0 5f 64 75 6d 6d 79 00 5f 5f 66 72 61 6d 65 5f 64 |_dummy.__frame_d|
- 00015dd0 75 6d 6d 79 5f 69 6e 69 74 5f 61 72 72 61 79 5f |ummy_init_array_|
--00015de0 65 6e 74 72 79 00 63 63 6d 69 42 49 59 6b 2e 6f |entry.ccmiBIYk.o|
-+00015de0 65 6e 74 72 79 00 63 63 53 71 64 47 76 57 2e 6f |entry.ccSqdGvW.o|
- 00015df0 00 66 61 74 61 6c 5f 65 72 72 6f 72 00 72 65 61 |.fatal_error.rea|
- 00015e00 64 5f 61 6c 6c 00 74 6f 6b 65 6e 69 7a 65 00 74 |d_all.tokenize.t|
- 00015e10 79 70 65 5f 6e 65 77 00 74 79 70 65 5f 6e 65 77 |ype_new.type_new|
+ $ diff -u <(hexdump -C p4dcc2) <(hexdump -C p4dcc3)
+@@ -5090,7 +5090,7 @@
+ 00015db0 72 72 61 79 5f 65 6e 74 72 79 00 66 72 61 6d 65 |rray_entry.frame|
+ 00015dc0 5f 64 75 6d 6d 79 00 5f 5f 66 72 61 6d 65 5f 64 |_dummy.__frame_d|
+ 00015dd0 75 6d 6d 79 5f 69 6e 69 74 5f 61 72 72 61 79 5f |ummy_init_array_|
+-00015de0 65 6e 74 72 79 00 63 63 6d 69 42 49 59 6b 2e 6f |entry.ccmiBIYk.o|
++00015de0 65 6e 74 72 79 00 63 63 53 71 64 47 76 57 2e 6f |entry.ccSqdGvW.o|
+ 00015df0 00 66 61 74 61 6c 5f 65 72 72 6f 72 00 72 65 61 |.fatal_error.rea|
+ 00015e00 64 5f 61 6c 6c 00 74 6f 6b 65 6e 69 7a 65 00 74 |d_all.tokenize.t|
+ 00015e10 79 70 65 5f 6e 65 77 00 74 79 70 65 5f 6e 65 77 |ype_new.type_new|
+
fatal_error、read_all、tokenize type_new はいずれも main.c で定義された関数の名前である。このことから考えると、これは GCC が埋め込んだシンボルテーブルである可能性が高い。わずかに異なっている 6バイトは、ランダム生成された何かのように見える。
diff --git a/services/nuldoc/public/blog/posts/2025-10-31/representing-single-value-with-half-open-float-interval/index.html b/services/nuldoc/public/blog/posts/2025-10-31/representing-single-value-with-half-open-float-interval/index.html
index 10500010..deff05d3 100644
--- a/services/nuldoc/public/blog/posts/2025-10-31/representing-single-value-with-half-open-float-interval/index.html
+++ b/services/nuldoc/public/blog/posts/2025-10-31/representing-single-value-with-half-open-float-interval/index.html
@@ -152,8 +152,9 @@
1 と p のビット列での表現を見てみよう。
1 = 0011111111110000000000000000000000000000000000000000000000000000
-p = 0011111111110000000000000000000000000000000000000000000000000001
+ 1 = 0011111111110000000000000000000000000000000000000000000000000000
+p = 0011111111110000000000000000000000000000000000000000000000000001
+
p が 1 よりも一つ分だけ大きいのがわかるだろうか (ここでは binary64 の具体的な表現について言及していないのでそうなる保証はないのだが、あくまで雰囲気として)。
@@ -182,37 +183,39 @@
binary64 を 64 bit の整数に変換できるなら、他の言語でもほとんど同じ方法で実装できるはずだ。
public static function nextUp(float $x): float
- {
- // NaN (Not a Number) なら NaN を返す。
- if (is_nan($x)) {
- return NAN;
- }
- // 正の無限大なら正の無限大を返す。
- if (is_infinite($x) && $x > 0) {
- return INF;
- }
- // 0 なら minValue() を返す (後述)。
- if ($x === 0.0) {
- return self::minValue();
- }
- // binary64 を 64 bit 整数に変換する。
- $u = self::floatToInt($x);
- // 正なら整数に +1 して binary64 に戻す。
- // 負なら整数に -1 して binary64 に戻す。
- return $x > 0.0 ? self::intToFloat($u + 1) : self::intToFloat($u - 1);
- }
+ public static function nextUp(float $x): float
+ {
+ // NaN (Not a Number) なら NaN を返す。
+ if (is_nan($x)) {
+ return NAN;
+ }
+ // 正の無限大なら正の無限大を返す。
+ if (is_infinite($x) && $x > 0) {
+ return INF;
+ }
+ // 0 なら minValue() を返す (後述)。
+ if ($x === 0.0) {
+ return self::minValue();
+ }
+ // binary64 を 64 bit 整数に変換する。
+ $u = self::floatToInt($x);
+ // 正なら整数に +1 して binary64 に戻す。
+ // 負なら整数に -1 して binary64 に戻す。
+ return $x > 0.0 ? self::intToFloat($u + 1) : self::intToFloat($u - 1);
+ }
+
0 のときに返している minValue() は次のような値である。
public static function minValue(): float
- {
- // 整数の 1 を binary64 と解釈した値を返す。
- // binary64 で表せる最小の正の非正規化数。
- return self::intToFloat(1);
- }
+ public static function minValue(): float
+ {
+ // 整数の 1 を binary64 と解釈した値を返す。
+ // binary64 で表せる最小の正の非正規化数。
+ return self::intToFloat(1);
+ }
+
print$a+=$\=y/8B/0/+y/0469ADO-R//.$/,","for<>
+ print$a+=$\=y/8B/0/+y/0469ADO-R//.$/,","for<>
+
Hole 1 については同一言語・同一スコアの回答が複数あるので詳細は省略する。 @@ -150,10 +151,11 @@ 最終スコアを見ると 4 位タイ (107 byte) が多く、3 位以上の回答と明確にアルゴリズムの差があるのでここから解説をスタートしようと思う。
s=gets
-?A.upto(?Z){(b,),m=s.scan(/(?=(.\B.))/).tally.max_by{_2}
-m>1&&(s.gsub!b,it;$*<<it+?:+b)}
-puts$**?,,s
+ s=gets
+?A.upto(?Z){(b,),m=s.scan(/(?=(.\B.))/).tally.max_by{_2}
+m>1&&(s.gsub!b,it;$*<<it+?:+b)}
+puts$**?,,s
+
変数名などの細かい差異を除けば他の 107 byte 回答と同じだが、 String#scan に渡す正規表現にこれを採用していたのは私だけだったのではないだろうか。 /(?=(\S\S))/ や /(?=(\w\w))/ と比べて短くはならないので意味はない。
@@ -168,9 +170,10 @@
Enumerable#max_by で最頻値を取ってきた後は、多重代入を使って必要な値を取り出している。
x = [["la"], 3]
-(b,),m = x
-# => b = "la", m = 3
+ x = [["la"], 3]
+(b,),m = x
+# => b = "la", m = 3
+
置換テーブルのデータは $* へと追加しているが、これは Ruby の特殊変数で、本来は Object::ARGV を指す。ここでは単に最初から空配列で初期化されている便利な入れ物として用いている。
@@ -185,10 +188,11 @@
回答 A をぐっと睨むと、m>1&&(...) の括弧を削りたくなる。しかしそれには m>1&& がどうしても邪魔になる。というわけで終了条件を工夫することでなんとか m を排除できないかを考えた。それがこちら。
s=gets
-?A.upto(?Z){(b,),=(?_+s).scan(/(?=(.\B.))/).tally.max_by{_2}
-$*<<it+?:+b if s.gsub!b,it}
-puts$**?,,s
+ s=gets
+?A.upto(?Z){(b,),=(?_+s).scan(/(?=(.\B.))/).tally.max_by{_2}
+$*<<it+?:+b if s.gsub!b,it}
+puts$**?,,s
+
s の先頭に番兵 _ を置くことで、bi-gram の出現頻度がすべて 1 になったとき、b へと代入される値が「_ + (s の先頭の文字)」になる。これを String#gsub! で置き換えようとすると、そのような文字列は s 中にないので置換が発生しない。String#gsub! は置換が起きなかったとき nil を返すので、これを使って条件分岐ができる。&& だと優先度の関係から String#gsub! の括弧が省略できないが、後置 if なら省略できる。
@@ -203,10 +207,11 @@
Kernel#gets は、入力を特殊変数 $_ へ代入する。これは Perl 由来の挙動で、Ruby にはいくつか $_ を参照するものがある。これを使って変数 s を置き換えると次のようになる。
gets
-?A.upto(?Z){(b,),="_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}
-$*<<it+?:+b if$_.gsub!b,it}
-puts$**?,,$_
+ gets
+?A.upto(?Z){(b,),="_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}
+$*<<it+?:+b if$_.gsub!b,it}
+puts$**?,,$_
+
これで 1 byte 縮む。
@@ -218,9 +223,10 @@
回答 C を眺めると、b への代入に文字を費やしすぎている。これを String#gsub! の第一引数に直接書いてはどうか。更に、直前のマッチしたパターンを指す特殊変数 $& を使えば、変数 b を排除できる。それがこちら。
gets
-?A.upto(?Z){$*<<it+?:+$&if$_.gsub!"_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}[0][0],it}
-puts$**?,,$_
+ gets
+?A.upto(?Z){$*<<it+?:+$&if$_.gsub!"_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}[0][0],it}
+puts$**?,,$_
+
これにより 2 bytes も一気に縮まった。 @@ -232,9 +238,10 @@ 回答 D を提出したことで tompng 氏のスコアを越え、氏のコードを閲覧できるようになった。そこから少し変更したものが、mame 氏と (変数名などの些事を除いて) 同じ以下のコードである。
s=gets
-?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;s.gsub!b,it)}
-puts$**?,,s
+ s=gets
+?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;s.gsub!b,it)}
+puts$**?,,s
+
ここまでとは大きく異なる戦略で終了条件を判定している。使われているのはパターンマッチで、in がマッチの有無を true / false で返すことを利用している。or を用いて、最頻値の出現回数が 1 でないなら置換処理を継続する。
@@ -243,10 +250,11 @@
パターンマッチの利用については途中何度か検討したが、1 でないときに処理を実行するという方針で実装しようとしてしまい、上手く短縮できなかった。
# これは 106 byte
-s=gets
-?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],2..and($*<<it+?:+b;s.gsub!b,it)}
-puts$**?,,s
+ # これは 106 byte
+s=gets
+?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],2..and($*<<it+?:+b;s.gsub!b,it)}
+puts$**?,,s
+
ruby で -p を付けると、以下のようなコードを書いたかのように動作する。
while gets
- ... # 記載したコードの処理
- puts $_
-end
+ while gets
+ ... # 記載したコードの処理
+ puts $_
+end
+
また、Kernel#gsub という $_ = $_.gsub(...) と同様の処理をおこなうメソッドが生えてくる。今回は String#gsub! も使うので、shebang の分を回収できれば短縮になりそうだ。
@@ -278,18 +287,20 @@
というわけで、実はこれまでも shebang での短縮は何度か試していた。しかし、いずれも 1 byte 増えたり変化しなかったりで成果を上げられずにいた。回答 E についても同様に、以下のようなコードを作っていた。
#!ruby -p
-?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;gsub b,it)}
-puts$**?,
+ #!ruby -p
+?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;gsub b,it)}
+puts$**?,
+
しかしこれは 103 byte で縮められない。にっくきは gsub と b の間のスペースである。せっかく s.gsub! を gsub にしたのに、後ろが記号でなくなったことでスペースが生じている。といって、括弧を付けるのも上手くはいかない。
# これも同じく 103 byte
-#!ruby -p
-?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b&&gsub(b,it)}
-puts$**?,
+ # これも同じく 103 byte
+#!ruby -p
+?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b&&gsub(b,it)}
+puts$**?,
+
外側の括弧を移動させてくれば
このように wget に適切なオプションを渡すことで、指定したページから遷移可能なページを再帰的に辿っていき、サイト内の全ページをファイルへ落とすことができる。今回のサイトにはページ遷移では辿り着けないページがあったが (管理画面など)、運用が停止している今そういったページはアーカイブしなくてもよい。
@@ -144,16 +145,17 @@
あとはこのファイルを適当にサーブしてやればよい。
gsub と b の間のスペースを消せるが、; を && にせねばならず失敗する。この問題を解決したのが最終回答の 102 byte コードである。
@@ -298,9 +309,10 @@
最終回答 (102 byte)
+ #!ruby -p
-?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b%gsub(b,it)}
-puts$**?,#!ruby -p
+?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b%gsub(b,it)}
+puts$**?,
+String#% は文字列のフォーマット処理をおこなう演算子だが、ここでは特にフォーマット目的で呼んでいるわけではない。ここで重要なのは、この演算子が特に副作用を持たず、どんな型でも右辺に取れることである。b の中身にフォーマット指定子はない (% などの記号が入力されないことが問題文から分かる) ので、誤って動作を壊してしまうおそれもない。
diff --git a/services/nuldoc/public/blog/posts/2025-12-06/archive-dynamic-site-with-wget/index.html b/services/nuldoc/public/blog/posts/2025-12-06/archive-dynamic-site-with-wget/index.html
index f3c04fc0..27bf970e 100644
--- a/services/nuldoc/public/blog/posts/2025-12-06/archive-dynamic-site-with-wget/index.html
+++ b/services/nuldoc/public/blog/posts/2025-12-06/archive-dynamic-site-with-wget/index.html
@@ -101,41 +101,42 @@
今回使用したスクリプトはこちら: https://github.com/nsfisis/phperkaigi-2024-albatross-archive/blob/cc837f6d2109555e2392016e8f6820fb5fd46dd6/archive.sh
+ # 指定した URL からスタートしてリンクを辿りながら全ファイルをファイルに書き出す
-#
-# --mirror リンクを再帰的に辿ってダウンロードする
-# --page-requisites CSS や画像等も含めて HTML から参照されている全ファイルをダウンロードする
-# --convert-links リンクを相対リンクへ変換する
-# --adjust-extension URL に拡張子が無くてもいい感じに推測する
-# --no-parent 親ディレクトリは見に行かない
-# --no-wait=1 リクエスト間で 1 秒待機する
-# -P ./archive/ 指定したディレクトリに保存する
-wget \
- --mirror \
- --page-requisites \
- --convert-links \
- --adjust-extension \
- --no-parent \
- --wait=1 \
- -P ./archive/ \
- https://t.nil.ninja/phperkaigi/2024/golf/
-
-# ディレクトリ構造を調整する
-mv ./archive/t.nil.ninja/phperkaigi/2024/golf/* ./archive
-rmdir ./archive/t.nil.ninja/phperkaigi/2024/golf/
-rmdir ./archive/t.nil.ninja/phperkaigi/2024/
-rmdir ./archive/t.nil.ninja/phperkaigi/
-rmdir ./archive/t.nil.ninja/
-
-mkdir -p ./archive/api/quizzes/{1,2,3}
-
-# 動的な API エンドポイントを叩いて結果を JSON ファイルとして保存する
-wget -O ./archive/api/quizzes/1/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/1/chart
-wget -O ./archive/api/quizzes/2/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/2/chart
-wget -O ./archive/api/quizzes/3/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/3/chart
-
-# 上記 API を叩いている箇所を、落としてきた静的ファイルを参照するように変更する
-sed -i -e 's#/chart`#/chart.json`#' ./archive/assets/chart.js# 指定した URL からスタートしてリンクを辿りながら全ファイルをファイルに書き出す
+#
+# --mirror リンクを再帰的に辿ってダウンロードする
+# --page-requisites CSS や画像等も含めて HTML から参照されている全ファイルをダウンロードする
+# --convert-links リンクを相対リンクへ変換する
+# --adjust-extension URL に拡張子が無くてもいい感じに推測する
+# --no-parent 親ディレクトリは見に行かない
+# --no-wait=1 リクエスト間で 1 秒待機する
+# -P ./archive/ 指定したディレクトリに保存する
+wget \
+ --mirror \
+ --page-requisites \
+ --convert-links \
+ --adjust-extension \
+ --no-parent \
+ --wait=1 \
+ -P ./archive/ \
+ https://t.nil.ninja/phperkaigi/2024/golf/
+
+# ディレクトリ構造を調整する
+mv ./archive/t.nil.ninja/phperkaigi/2024/golf/* ./archive
+rmdir ./archive/t.nil.ninja/phperkaigi/2024/golf/
+rmdir ./archive/t.nil.ninja/phperkaigi/2024/
+rmdir ./archive/t.nil.ninja/phperkaigi/
+rmdir ./archive/t.nil.ninja/
+
+mkdir -p ./archive/api/quizzes/{1,2,3}
+
+# 動的な API エンドポイントを叩いて結果を JSON ファイルとして保存する
+wget -O ./archive/api/quizzes/1/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/1/chart
+wget -O ./archive/api/quizzes/2/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/2/chart
+wget -O ./archive/api/quizzes/3/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/3/chart
+
+# 上記 API を叩いている箇所を、落としてきた静的ファイルを参照するように変更する
+sed -i -e 's#/chart`#/chart.json`#' ./archive/assets/chart.js
+
+ server {
- listen 80 default;
- listen [::]:80;
-
- charset UTF-8;
-
- location /phperkaigi/2024/golf/ {
- alias /archive/;
- }
-}server {
+ listen 80 default;
+ listen [::]:80;
+
+ charset UTF-8;
+
+ location /phperkaigi/2024/golf/ {
+ alias /archive/;
+ }
+}
+