見出し画像

あの言語で2進UTF-8文字列のデコーダを作ってみた

最近、言葉を UTF-8 の2進数で表してポストする人が増えているようである。

そこで、デコーダを書いてみた。

早速コードを掲載

すき大石泉大石泉大石泉すきすきすきすきすきすきすきすき大石泉大石泉すき大石泉すき大石泉すき大石泉すき大石泉大石泉大石泉大石泉すき?すきいずみん大石泉大石泉すきすきすきすきすきすきすきすきいずみん愛してるすきすきすきすきすきすき大石泉好きイズミン愛してるすき大石泉すきすき愛してるいずみん愛してる好き大石泉好きイズミン愛してるいずみん大石泉大石泉好き愛してる愛してる愛してる愛してる愛してるイズミン愛してる愛してる愛してるいずみん大石泉イズミン大石泉大石泉大石泉好きいずみん大石泉大石泉好き愛してる愛してる愛してる愛してる愛してるイズミン愛してる愛してる愛してるいずみん大石泉イズミン大石泉大石泉大石泉すき大石泉大石泉いずみん好き愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してるいずみん大石泉大石泉大石泉好き大石泉大石泉大石泉大石泉大石泉大石泉大石泉大石泉大石泉いずみん愛してる愛してるすき愛してるイズミン愛してる愛してる愛してる愛してる愛してるいずみん大石泉大石泉大石泉好き好き好き好きいずみん愛してるイズミン愛してる愛してる愛してる愛してる愛してるいずみん大石泉大石泉大石泉大石泉大石泉大石泉大石泉大石泉すきすきすきすきすきいずみん愛してる愛してるすきすきすきすきすき大石泉大石泉好きイズミン愛してる愛してるすきすき愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してるすきすきすきすきすきすきすきすきすきすきすき大石泉大石泉イズミン大石泉大石泉大石泉大石泉大石泉大石泉すきすきすきすきいずみん愛してるイズミン愛してるいずみん大石泉大石泉好き愛してるイズミン大石泉すき愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる好き大石泉大石泉大石泉大石泉イズミンイズミン大石泉大石泉大石泉大石泉大石泉いずみん愛してる愛してるいずみん愛してる愛してるいずみん好き大石泉大石泉大石泉イズミン大石泉大石泉大石泉大石泉大石泉いずみん大石泉大石泉大石泉大石泉大石泉いずみん大石泉大石泉すきすき愛してる愛してる好きイズミン大石泉大石泉いずみん愛してる愛してるすき大石泉大石泉好きイズミン愛してるいずみん愛してるすき大石泉好きイズミン愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してるいずみん大石泉大石泉イズミン大石泉大石泉いずみん愛してる愛してる愛してる愛してる好きいずみん大石泉大石泉イズミン大石泉大石泉大石泉大石泉大石泉いずみん大石泉大石泉大石泉大石泉大石泉大石泉すきすきすきすきすきすきすきすきいずみん愛してる愛してる好き好き好き好き好き好き好き好き大石泉大石泉好きイズミン愛してる愛してるすき!いずみん好きイズミン大石泉大石泉すきすきすきすきすきいずみん愛してる愛してるすきすきすきすきすきすきすきすきすきすきすき大石泉大石泉好きイズミン愛してる愛してる愛してる愛してるイズミン愛してる愛してる愛してる愛してる愛してるイズミン大石泉大石泉大石泉大石泉大石泉イズミン愛してる愛してる愛してる愛してる愛してるイズミン大石泉大石泉大石泉大石泉大石泉いずみん愛してる愛してる愛してる愛してる愛してるすきすきすきすきすきすきすきすき愛してる愛してるすき大石泉大石泉大石泉大石泉大石泉大石泉大石泉大石泉イズミン愛してる愛してる愛してる愛してる愛してる愛してる好きいずみん大石泉イズミン大石泉大石泉大石泉大石泉大石泉いずみん大石泉大石泉好きいずみん愛してるイズミン愛してる愛してる愛してる愛してる愛してるいずみん大石泉大石泉大石泉大石泉大石泉大石泉すき!いずみん好きイズミン愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してる愛してるすき大石泉大石泉大石泉すきすきすきすきすきすきすきすき大石泉イズミン大石泉大石泉大石泉大石泉大石泉イズミンイズミン大石泉大石泉大石泉大石泉大石泉イズミン愛してる愛してるすき?すきイズミン

これは何?

大石泉すき言語 で実装した、UTF-8 の文字列を表す2進文字列を入力として受け取り、それが表す文字列を出力するデコーダである。

入力例

11100101 10100100 10100111 11100111 10011111 10110011 11100110 10110011 10001001 11100011 10000001 10011001 11100011 10000001 10001101

出力例

大石泉すき

仕様

  • 半角の「0」で0のビットを、「1」で1のビットを表す

  • 「0」「1」以外の文字は無視する。無視する文字には全角の「0」「1」も含まれる

  • 1バイトのうち、上位のビットを先に、下位のビットを後に入力する

  • 1バイトは必ず8ビットで表す。最上位ビットが「0」であっても省略はできない (「0」「1」以外の文字は無視するので、区別ができず、省略するとその後のデータがズレてしまう)

  • UTF-8 以外の形式には対応しない。データ列が UTF-8 として不正な場合の動作は保証しない

  • 入力の最後 (EOF) まで処理を行う。最後に中途半端なデータが残っている場合、その分の出力は行わない

大石泉すき言語の処理系の詳細

大石泉すき言語は、Brainf*ck の各命令を1対1でほかの文字列に置き換えた派生言語である。
Brainf*ck の処理系には、配列の要素数・要素のサイズ・入出力の扱いなどに差がみられる。
そこで、大石泉すき言語の処理系の仕様の詳細をみておく。

執筆時点において、大石泉すき言語の処理系は Web版C# 版 の2種類が公開されている。

配列の要素数

配列の要素数は、Web版・C# 版ともに 512 である。
配列の範囲外 (一番最初の要素の前や、一番最後の要素の次) にポインタを移動させようとすると、ラップアラウンドはせず、そのまま範囲外になる。
ポインタを範囲外に移動させるだけではエラーにならないが、ポインタが範囲外にある状態で要素の操作 (インクリメント・デクリメント・入出力) を行おうとすると、異常動作の原因となる。
C# 版では、範囲外の要素の操作を行おうとすると例外が発生して強制終了となるが、Web版では強制終了しない。
Web版の範囲外の要素の初期値は undefined であり、そのままインクリメントやデクリメントを行うと NaN になる。
範囲外の要素に値を読み込む操作を行うことで、数値が入り、その要素はインクリメントやデクリメントなども可能になる。

配列の要素のサイズ

C# 版の配列の要素は int 型 であり、サイズは32ビットである。
Web版の配列の要素は Number であり、サイズは実質54ビットである。

いずれも符号付きであり、オーバーフローを利用するのは現実的ではない。
Web版では、そもそも無限に時間をかけてもオーバーフローさせることはできない (表現できる範囲を超えると、そこで値の増加・減少が止まる) だろう。

入出力の扱い

入出力は、UTF-16 の文字 (コードユニット) 単位で行う。
「大」などのマルチバイト文字も、1回の入出力命令で入出力できる。
「𡘙」などの U+10000 以上の文字は、2回に分けて入出力する。(サロゲートペア)

Web版では、プログラムから入力を要求すると、prompt により入力を受け付ける。
0文字だけ入力すると、正の文字数を入力するまで繰り返しダイアログが出る。
複数の文字を入力すると、それらを使い切るまでダイアログを出さずにプログラムに渡していく。
ダイアログをキャンセルすると、エラーが発生し、プログラムの実行が強制終了される。(prompt から返される null のプロパティを参照しようとするため)
よって、Web版ではプログラムが EOF を受け取ることはない。

C# 版では、Console.Read メソッド を用いて入力を受け取る。
このメソッドは EOF に到達すると -1 を返し、これがそのまま配列に入る。
また、Shift_JIS に無い文字は、うまく出力できないようである。

今回のプログラムをどうやって作ったか

さて、残念ながら自分の頭の良さは大石泉すき言語を直接書くには足りない。
そこで、自分が今回のプログラムをどうやって作ったかをここに記す。

C# でロジックを書く

まず、C# で変換のロジックを実装した。
このとき、この後 Brainf*ck に移植することをある程度考えて作った。
また、Shift_JIS に無い文字でも出力できるよう、C#でコンソール出力時の文字エンコーディングを制御する を参考に文字コードを UTF-8 に設定した。

まず、UTF-8 では各文字を表す最初のバイトの上位ビットに何個「1」が連続しているかでその文字のバイト数を表すので、これを読み取る。
4バイトであれば、サロゲートペアでの表現となるため、そのための情報を設定する。

各文字の2バイト目以降では、まず上位2ビットを読み飛ばし、残りの6ビットを文字を表す数値に追加する。
数値にビットを追加した際、それがサロゲートペアの最初のコードユニットに格納する最後のビットであれば、そのコードユニットを出力し、2番目のコードユニット用の情報を設定する。

各文字を表すバイト列の読み取りが完了したら、文字を出力し、次の文字の最初のバイトを読み取るモードにする。

using System;
using System.Text;

class BinUtf8Decode
{
	public static void Main(string[] args)
	{
		Console.OutputEncoding = Encoding.UTF8;
		bool initialMode = true;
		bool skipFlag = false;
		int bytesLeft = 0;
		int bitsLeft = 8;
		int spBitsLeft = 0;
		int output = 0;
		int input;
		while ((input = Console.Read()) != -1)
		{
			if (input != '0' && input != '1') continue;
			if (initialMode) {
				if (input == '1') {
					bytesLeft++;
				} else {
					if (bytesLeft == 4) {
						spBitsLeft = 11;
						output = 0x1b;
					}
					if (bytesLeft == 0) bytesLeft++;
					initialMode = false;
				}
				bitsLeft--;
			} else {
				if (bitsLeft == 0) {
					bitsLeft = 8;
					skipFlag = true;
				} else {
					if (skipFlag) {
						skipFlag = false;
					} else {
						output = output << 1;
						if (input == '1') output++;
						if (spBitsLeft > 0) {
							spBitsLeft--;
							if (spBitsLeft == 0) {
								output -= 0x40;
								Console.Write((char)output);
								output = 0x37;
							}
						}
					}
				}
				bitsLeft--;
				if (bitsLeft == 0) {
					bytesLeft--;
					if (bytesLeft == 0) {
						Console.Write((char)output);
						output = 0;
						initialMode = true;
						bitsLeft = 8;
					}
				}
			}
		}
	}
}

C# から Brainf*ck に移植する

C# のコードでロジックの動作確認が取れたら、このロジックを Brainf*ck に移植した。

Brainf*ck のコードでは、命令を表す文字以外は無視されるので、コメントを書くことができる。
ただし、「コメント」という文法は無いため、うっかり「コメント」内で命令を表す文字を使わないように気をつけなければならない。
たとえば、負の数を表現する際、命令を表す「-」は使わず、「マイナス」と書いた。

プログラムの最初の部分に、メモリの変数への割り当てを書いた。
変数を並べるだけでなく、条件分岐を行った後にポインタの位置を合わせるための領域も用意した。

その後、行う処理、想定するポインタの位置、条件分岐で実行する条件、などのコメントを書きながら実装を行った。

initialMode
skipFlag
spBitsLeft
bitsLeft
sync_0_a
sync_1_a
sync_1_b
sync_1_c
sync_1_d
sync_0_b
bytesLeft
output
input
input_cmp_work1 / output_work1
input_cmp_work2 / input_valid_flag / output_work2

// 初期値を設定する (initialMode = 1; bitsLeft = 8; sync_1_* = 1)
// ポインタを input に置く
+>>>++++++++>>+>+>+>+>>>>
// 入力を読み取る
,+
[
  // 入力に1足した値がinputに入っている
  // 入力がマイナス1(EOF)の場合は0になって終了
  // input_cmp_work1 = 49; input_valid_flag = 2; ポインタ → input_cmp_work1
  // input_cmp_work2 を使用し、6*8に1を足して49を作る
  >>++++++++[<++++++>-]<+>++<
  // input から input_cmp_work1 を引く
  [<->-]
  // input が 0 でなければ、input_valid_flag から 1 を引く
  <[>>-<<<<<]
  // ポインタが input または sync_0_b にあるので、sync_0_b に統一する
  <<<[>]
  // input から 1 を引き、0 でなければ、input_valid_flag から 1 を引く
  >>>-[>>-<<<<<]
  // ポインタが input または sync_0_b にあるので、sync_0_b に統一する
  <<<[>]
  // input に 1 を足す (入力が 0 なら 0、1 なら 1 にする)
  >>>+
  // input_valid_flag をチェックし、0 でなければ文字の処理を行う
  >>[
    // input_valid_flag を 0 にする
    -
    // initialMode で分岐する
    <<<<<<<<<<<<<<[
      // initialMode が 1 のとき
      // bitsLeft から 1 を引く
      >>>-
      // input で分岐する
      >>>>>>>>>[
        // input が 1 のとき
        // bytesLeft に 1 を足す
        <<+
        // ポインタを sync_0_b に移す → 次の移動で sync_0_a に移る
        <
      ]<<<<<[
        // input が 0 のとき (ポインタは sync_1_c にある)
        // bytesLeft が 4 かを判定する
        // 4 であればポインタは sync_1_a に、4 でなければポインタは sync_0_a に移る
        >>>----[<]<<<<<
        // bytesLeft が 4であれば、以下の処理を行う
        [
          // output_work1 を用いて、output を 0x1b にする
          >>>>>>>>+++++[<<+++++>>-]<<++
          // spBitsLeft を 11 にする
          <<<<<<<<<+++++++++++
          // ポインタを sync_0_a に移す
          >>
        ]
        // ポインタを bytesLeft に移し、値を復元する
        >>>>>>++++
        // ポインタを bytesLeft が 0 であれば sync_0_b に、0 でなければ sync_1_d に移す
        [<]<
        // bytesLeft が 0 でなければ、bytesLeft から 1 を引き、ポインタを sync_0_b に移す
        [>>-<]
        // bytesLeft に 1 を足す
        // これにより、bytesLeft が 0 だったら 1 を足し、0 でなかったらそのまま、となる
        >+
        // initialMode を 0 にする
        <<<<<<<<<<-
        // ポインタを sync_0_a に移す
        >>>>
      ]
      // ポインタは sync_0_a にある → 次の移動で sync_0_b に移る
    ]>>>>>[
      // initialMode が 0 のとき (ポインタは sync_1_a にある)
      // bitsLeft で分岐する
      <<[
        // bitsLeft が 0 でないとき
        // skipFlag で分岐する
        <<[
          // skipFlag が 1 のとき
          // skipFlag を 0 にする
          -
          // ポインタを sync_0_a に移す → 次の移動で sync_0_b に移る
          >>>
        ]>>>>>[
          // skipFlag が 0 のとき (ポインタは sync_1_b にある)
          // output_work1 を用いて output を 2 倍にする
          // カーソルは output_work1 に移る
          >>>>>[>>++<<-]>>[<<+>>-]
          // input が 1 なら、output に 1 を足す (input は 0 になる)
          <[<+>-]
          // ポインタを sbBitsLeft が 0 ならsync_0_a に、0 でなければ sync_1_b に移す
          <<<<<<<<<<[>>]>>
          // sbBitsLeft が 0 でなければ、以下を行う
          [
            // sbBitsLeft から 1 を引く
            <<<<-
            // ポインタを sbBitsLeft が 0 なら sync_1_c に、0 でなければ sync_0_b に移す
            [>>]>>>>>
            // sbBitsLeft が 0 であれば、以下を行う
            [
              // output_work1 を用いて、output から 0x40 を引く
              >>>>>>++++++++[<<-------->>-]
              // output を出力し、0 にする
              <<.[-]
              // output_work1 を用いて、output を 0x37 にする
              >>+++++[<<+++++++++++>>-]
              // ポインタを sync_0_b に移す
              <<<<
            ]
            // ポインタを sync_0_b から sync_0_a に移す
            <<<<<
          ]
          // ポインタを sync_0_a から sync_0_b に移す
          >>>>>
        ]
        // ポインタを sync_0_b から sync_0_a に移す → 次の移動で sync_0_b に移る
        <<<<<
      ]>>>>>[
        // bitsLeft が 0 のとき (ポインタは sync_1_d にある)
        // bitsLeft を 8 にする
        <<<<<++++++++
        // skipFlag を 1 にする
        <<+
        // ポインタを sync_0_b に移す
        >>>>>>>>
      ]
      // ポインタを sync_0_b から移し、bitsLeft から 1 を引く
      <<<<<<-
      // ポインタを bitsLeft が 0 なら sync_1_d に、0 でないなら sync_0_b に移す
      [>]>>>>>
      // bitsLeft が 0 なら、以下を実行する
      [
        // bytesLeft から 1 を引く
        >>-
        // ポインタを bytesLeft が 0 なら sync_1_a に、0 でないなら sync_0_a に移す
        [<]<<<<<
        // bytesLeft が 0 なら、以下を実行する
        [
          // output を出力し、0 にする
          >>>>>>.[-]
          // initialMode を 1 にする
          <<<<<<<<<<<+
          // bitsLeft を 8 にする
          >>>++++++++
          // ポインタを sync_0_a に移す
          >
        ]
        // ポインタを sync_0_b に移す
        >>>>>
      ]
      // ポインタは sync_0_b にある
    ]
    // ポインタを sync_0_b から input_valid_flag に移す
    >>>>>
  ]
  // ポインタを input_valid_flag から input に移し、次の入力を読み取る
  <<,+
]

Brainf*ck から大石泉すき言語に変換する

最後に、Brainf*ck から大石泉すき言語への変換を行った。

変換は、Brainf*ck の命令でない文字を消した後、命令の各文字を変換することで行った。

以下の CyberChef の Recipe により、変換を行うことができる。
Find / Replace, 8 more - CyberChef

おわりに

2進UTF-8文字列のデコードを行う大石泉すき言語のプログラムを書くことができた。
きちんと整理して実装していけば、思ったより難しくなかった。

大石泉すき。

今回のプログラムはこちらにも置いたので、改造などしたいかたはどうぞ。(MITライセンス)
mikecat/ois_bin_utf8: 大石泉すき言語で2進UTF-8文字列の変換

エンコーダは……気が向いたら書くかも……?


いいなと思ったら応援しよう!

コメント

ログイン または 会員登録 するとコメントできます。
あの言語で2進UTF-8文字列のデコーダを作ってみた|みけCAT
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1