ドラゴンクエストシリーズ第一作目に登場する「ふっかつのじゅもん」っぽいBase64亜種を書いてみました。
使い方
簡単に使えるようコマンドラインインターフェイスも用意しました。go install
でインストール可能です。
$ go install github.com/shogo82148/base64dq/cmd/base64dq@latest
base64dq
コマンドが使えるようになります。
Coreutilsのbase64
コマンドと同様に使えます。
$ echo 'こんにちは' | base64dq
づづきとづづさとづづきわづづきめづづきげうむ・・
$ echo 'づづきとづづさとづづきわづづきめづづきげうむ・・' | base64dq --decode
こんにちは
ふっかつのじゅもん
「ふっかつのじゅもん」はドラゴンクエストシリーズ第一作目で採用されたゲームのセーブ方式です。 ゲームを中断するときには、再開したときに同じ状態からゲームを始められるよう、ゲームの状態を保存しておく必要があります。 しかしドラクエIが発売されたのは1986年5月。 当時ドラクエIはファミコン向けに発売されたのですが、ファミコンにはフラッシュROMのような贅沢なハードウェアはついていません。 電源を落とすと簡単にデーターは失われてしまいます。
そこでゲームの状態を20文字の「ふっかつのじゅもん」にエンコードし、 再開時には「ふっかつのじゅもん」をプレイヤーに入力してもらう、というセーブ方式が編み出されました。
プレイヤーはゲームを中断するたびに「ふっかつのじゅもん」を書き写す必要がありました。 当時は液晶ディスプレイなどあるはずもなく、一般の家庭にあるのはブラウン管のアナログディスプレイです。 解像度が荒く読み取るのが大変なため、「ふっかつのじゅもん」を間違え散っていったプレイヤーも多くいたと聞いています(筆者はまだ生まれていないのでよく知らない)。
ふっかつのじゅもんとBase64の関係
今では有志による解読も進みふっかつのじゅもんのジェネレーターも開発されています。
DQ1 復活の呪文解析日記によると、「ふっかつのじゅもん」は64種類のひらがなで構成されているそうです。 64。実に切りのいい数字です。 1文字で6ビットの情報を表し、合計で120ビットのセーブデータを表現しています。
このエンコード方法はBase64とまったく同じですね! 「ふっかつのじゅもん」は、64種類のASCII文字の代わりに、64種類のひらがなを使ったBase64の亜種、と考えることができます。
ちなみにBase64がRFCに登場したのは1987年4月のRFC 989だそうです。 当時はBase64という言葉すらなく、printable encodingと呼ばれていたようです。 ドラクエIが発売されたのは1986年5月なので、Base64が一般に広まる前に「ふっかつのじゅもん」は世に公開されたわけですね。 すごい!
もちろん規格化されていないだけで「64種類の文字で情報をエンコードする」というアイディア自体はもっと昔からあったのでしょう。 でも似たようなものがゲームに使われていたのはおもしろいですね。
base64dq
そういうわけで「ふっかつのじゅもん」の影響を受けて作ったのが shogo82148/base64dq です。 64種類のASCII文字の代わりに、以下の64種類のひらがなを使います。
あいうえお
かきくけこ
さしすせそ
たちつてと
なにぬねの
はひふへほ
まみむめも
やゆよ
らりるれろ
わ
がぎぐげご
ざじずぜぞ
だぢづでど
ばびぶべぼ
・(パディング)
「ふっかつのじゅもん」にはパディングはないので、適当に「・」を選びました。
性能
空間効率
3バイトを4文字のひらがなにエンコードします。UTF-8の場合、ひらがなは3バイトなので、4×3=12バイト。 したがって、エンコードするとデーター量は4倍に増えます。 とても非効率です。
計算性能
base64dqはデフォルトで「ふっかつのじゅもん」っぽい文字を使いますが、この文字はカスタマイズ可能です。 もちろんASCII文字も指定できるので、以下のように通常のBase64エンコードにも使えます。
package main
import (
"fmt"
"github.com/shogo82148/base64dq"
)
func main() {
// デフォルトの文字
fmt.Println(base64dq.StdEncoding.EncodeToString([]byte("こんにちは")))
// づづきとづづさとづづきわづづきめづづきげ
// encoding/base64.StdEncoding相当
enc := base64dq.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/").WithPadding('=')
fmt.Println(enc.EncodeToString([]byte("こんにちは")))
// 44GT44KT44Gr44Gh44Gv
}
これを利用して、入出力の条件をそろえたうえで、標準ライブラリのencoding/base64との性能比較をしてみました。 末尾がBase64で終わっているのがbase64dqで通常のBase64を実現したもの、StdBase64で終わっているのが標準ライブラリのencoding/base64です。
goos: darwin
goarch: arm64
pkg: base64dq
BenchmarkEncodeToString-10 37768 30578 ns/op 267.90 MB/s
BenchmarkEncodeToString_Base64-10 37674 31834 ns/op 257.33 MB/s
BenchmarkEncodeToString_StdBase64-10 198698 6018 ns/op 1361.31 MB/s
BenchmarkDecodeString/2-10 40708496 29.16 ns/op 411.50 MB/s
BenchmarkDecodeString/4-10 21325150 56.58 ns/op 424.15 MB/s
BenchmarkDecodeString/8-10 11701461 102.2 ns/op 352.16 MB/s
BenchmarkDecodeString/64-10 1909441 629.5 ns/op 419.39 MB/s
BenchmarkDecodeString/8192-10 16758 72094 ns/op 454.57 MB/s
BenchmarkDecodeString_Base64/2-10 61730642 19.45 ns/op 205.62 MB/s
BenchmarkDecodeString_Base64/4-10 45182424 26.45 ns/op 302.45 MB/s
BenchmarkDecodeString_Base64/8-10 34497255 34.73 ns/op 345.54 MB/s
BenchmarkDecodeString_Base64/64-10 4936286 243.0 ns/op 362.08 MB/s
BenchmarkDecodeString_Base64/8192-10 48498 24746 ns/op 441.44 MB/s
BenchmarkDecodeString_StdBase64/2-10 56134659 21.49 ns/op 186.12 MB/s
BenchmarkDecodeString_StdBase64/4-10 52898973 21.89 ns/op 365.46 MB/s
BenchmarkDecodeString_StdBase64/8-10 46836578 25.48 ns/op 470.97 MB/s
BenchmarkDecodeString_StdBase64/64-10 15156817 79.14 ns/op 1111.96 MB/s
BenchmarkDecodeString_StdBase64/8192-10 231625 5229 ns/op 2089.07 MB/s
BenchmarkDecoder/2-10 2862613 432.3 ns/op 27.76 MB/s
BenchmarkDecoder/4-10 2617545 459.5 ns/op 52.23 MB/s
BenchmarkDecoder/8-10 2479767 484.1 ns/op 74.37 MB/s
BenchmarkDecoder/64-10 1000000 1058 ns/op 249.46 MB/s
BenchmarkDecoder/8192-10 16506 71903 ns/op 455.78 MB/s
BenchmarkDecoder_Base64/2-10 2906109 419.8 ns/op 9.53 MB/s
BenchmarkDecoder_Base64/4-10 2779658 431.5 ns/op 18.54 MB/s
BenchmarkDecoder_Base64/8-10 2681973 445.8 ns/op 26.92 MB/s
BenchmarkDecoder_Base64/64-10 1686495 689.1 ns/op 127.71 MB/s
BenchmarkDecoder_Base64/8192-10 39691 30254 ns/op 361.07 MB/s
BenchmarkDecoder_StdBase64/2-10 4166818 285.2 ns/op 14.02 MB/s
BenchmarkDecoder_StdBase64/4-10 4008444 301.1 ns/op 26.57 MB/s
BenchmarkDecoder_StdBase64/8-10 3961225 284.2 ns/op 42.22 MB/s
BenchmarkDecoder_StdBase64/64-10 3022238 402.5 ns/op 218.64 MB/s
BenchmarkDecoder_StdBase64/8192-10 106390 11288 ns/op 967.77 MB/s
PASS
ok base64dq 48.600s
バッファサイズにもよりますが、エンコード・デコードともにencoding/base64の1/2〜1/5倍といったところでしょうか。
応用
まったく役に立たないエンコード方式ですが、実は裏の利用目的があります。 「秘密の質問」です。
いまだに「母親の旧姓は?」と聞いてくるサイトがあるのですが、そんなサイトに正直に回答したくはありません。 ランダムな文字列で適当に埋めようとも考えるのですが、日本のサイトだと全角文字に絞っていることも多く、 普通のパスワードジェネレーターは使えません。
そこでbase64dqの出番です。以下のコマンドでランダムなひらがな文字列を生成できます。
$ head -c 15 /dev/random | base64dq
ほべちどでづはめちでがゆたろぎへびぶりし
そういうわけで、僕の母の旧姓は「ほべちどでづはめちでがゆたろぎへびぶりし」です。よろしくお願いします。
実装によるパディングの扱いの差について
実装の参考にencoding/base64のコードを読んで、実装によってパディングの扱いに差があることに気が付きました。 たとえば、encoding/base64はパディング文字のあとに英数字が現れることを禁止しています。
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// Error: illegal base64 data at input byte 8
fmt.Println(base64.StdEncoding.DecodeString("aGVsbG8=IHdvcmxkIQ=="))
}
しかしBase64の実装によっては、これを許可しているものもあります。
Coreutilsのbase64
コマンドは、途中にパディング文字が入ってもデコードに成功します。
$ echo 'aGVsbG8=IHdvcmxkIQ==' | base64 --decode
hello world!
この挙動の何がうれしいかというと、これが許可されていると「Base64エンコードされた結果」をデコードせずに結合できるんですね。
$ echo -n 'hello' | base64 >> foo.txt
$ echo -n ' world!' | base64 >> foo.txt
$ cat foo.txt | base64 --decode
hello world!
まあ、今となっては誰もこんな使い方しないのでしょうね。
まとめ
「ふっかつのじゅもん」っぽいBase64亜種を書いてみました。