データパックで Base64 パーサーを実装する

この記事は [Minecraft Command Advent Calendar 2023](https://qiita.com/advent-calendar/2023/mccmd) 20 日目の記事です。

## はじめに
同アドベントカレンダー ~~23 日~~ 25 日公開予定の ｢バニラでアップデート通知システムを作る｣ を書くに当たり、Base64 パーサーが必要になってしまったので (既にライブラリとしてどこかに転がっているかもしれませんが) 実際に自分で一から実装してみた話です。

この記事では、以下に関する知識を仮定します。
- コマンド ･ データパック ･ マクロの基礎
- storage の概念
- NBTPath による NBT 操作
- Base64 のふんわりとした知識 (文字列としてデータを表現できるくらいを知っていれば◯)

また、今回の開発は 1.20.4 で行います。
最後に、完全なコードは [GitHub](https://github.com/ChenCMD/MCCMD-Base64) に置いてあります。煮るなり焼くなり好きにしてください。

## Base64 の基礎的な話
Base64 とは何かについておさらいします。

> Base64は、データを64種類の印字可能な英数字のみを用いて、それ以外の文字を扱うことの出来ない通信環境にてマルチバイト文字やバイナリデータを扱うためのエンコード方式である。
> \<中略\>
> 具体的には、A、…、Z、a、…、z、0、…、9 の62種類の文字と、2種類の記号 (+、/)、さらにパディング (余った部分を詰める) のための記号として = が用いられる。
> [Base64 - Wikipedia](https://ja.wikipedia.org/wiki/Base64)

よく Base64 というと ｢ABCDEFG｣ <=> ｢QUJDREVGRw==｣ というような、｢文字列｣ と ｢64 種類で構成される文字列｣ の相互変換のことだと思っている人が多いと (勝手に) 思っていますが、より正確には RFC で規定されているのは ｢bit 列｣ と ｢64 (+ 1) 種類で構成される文字列｣ の変換です。
上記の通りだと Base64 パーサーとは bit 列への変換が目標に思えるかもしれませんが、今回の場合最終的には文字列が必要になるのでここでは Base64 パーサーに付随して ASCII エンコーダーの実装も行うことにします。

### ASCII とは
Base64 に比べるともしかしたら知らない人も居るかも知れないので、念のため簡単に説明すると 8bit を 1 文字としてエンコードする文字コードです。[^1]
ASCII の変換テーブルの中には ASCII 制御文字と呼ばれる改行やタブ、削除などの実際に文字としては観測できない文字も含まれています。
基本的[^2]にそれらの制御文字は先頭 3bit が `0000` で始まるような bit で表されます。(e.g. `00001010`)

[^1]: 先頭 bit は常に `0` であることに留意。
[^2]: 削除に当たる制御文字のみは例外で、`01111111` として表現されます。

### Base64 のデコード + ASCII のエンコードを手でやってみる
Base64 のパーサーを実装するに当たり、｢YXJtb3Jfc3RhbmQ=｣ という Base64 文字列を例に変換の流れを追ってみます。

#### 1. bit 列に変換する
まずは Base64 文字列を一文字ずつ bit 列に変換します。

文字と bit 列の対応はこんな感じです。`=` はパディング用の文字列なので無視して今回は構いません。
| 文字  |  bit 列  |       | 文字  |  bit 列  |       | 文字  |  bit 列  |       | 文字  |  bit 列  |
| :---: | :------: | :---: | :---: | :------: | :---: | :---: | :------: | :---: | :---: | :------: |
|  `A`  | `000000` |       |  `Q`  | `010000` |       |  `g`  | `100000` |       |  `w`  | `110000` |
|  `B`  | `000001` |       |  `R`  | `010001` |       |  `h`  | `100001` |       |  `x`  | `110001` |
|  `C`  | `000010` |       |  `S`  | `010010` |       |  `i`  | `100010` |       |  `y`  | `110010` |
|  `D`  | `000011` |       |  `T`  | `010011` |       |  `j`  | `100011` |       |  `z`  | `110011` |
|  `E`  | `000100` |       |  `U`  | `010100` |       |  `k`  | `100100` |       |  `0`  | `110100` |
|  `F`  | `000101` |       |  `V`  | `010101` |       |  `l`  | `100101` |       |  `1`  | `110101` |
|  `G`  | `000110` |       |  `W`  | `010110` |       |  `m`  | `100110` |       |  `2`  | `110110` |
|  `H`  | `000111` |       |  `X`  | `010111` |       |  `n`  | `100111` |       |  `3`  | `110111` |
|  `I`  | `001000` |       |  `Y`  | `011000` |       |  `o`  | `101000` |       |  `4`  | `111000` |
|  `J`  | `001001` |       |  `Z`  | `011001` |       |  `p`  | `101001` |       |  `5`  | `111001` |
|  `K`  | `001010` |       |  `a`  | `011010` |       |  `q`  | `101010` |       |  `6`  | `111010` |
|  `L`  | `001011` |       |  `b`  | `011011` |       |  `r`  | `101011` |       |  `7`  | `111011` |
|  `M`  | `001100` |       |  `c`  | `011100` |       |  `s`  | `101100` |       |  `8`  | `111100` |
|  `N`  | `001101` |       |  `d`  | `011101` |       |  `t`  | `101101` |       |  `9`  | `111101` |
|  `O`  | `001110` |       |  `e`  | `011110` |       |  `u`  | `101110` |       |  `+`  | `111110` |
|  `P`  | `001111` |       |  `f`  | `011111` |       |  `v`  | `101111` |       |  `/`  | `111111` |

上記の表に沿って ｢YXJtb3Jfc3RhbmQ=｣ を変換すると
1. `Y` => `011000`
2. `X` => `010111`
3. `J` => `001001`
4. `t` => `101101`
5. `b` => `011011`
6. `3` => `110111`
7. `J` => `001001`
8. `f` => `011111`
9. `c` => `011100`
10. `3` => `110111`
11. `R` => `010001`
12. `h` => `100001`
13. `b` => `011011`
14. `m` => `100110`
15. `Q` => `010000`

という 6bit x 15 の bit 列が得られました。

#### 2. 8bit ずつ分割する
得られた bit 列を ASCII の変換単位である 8bit ずつに分割します。この時、末尾の余った `0` のみの bit は要らないのでポイ捨てします。

その基準に沿って分割すると
1. `01100001` (61)
2. `01110010` (72)
3. `01101101` (6D)
4. `01101111` (6F)
5. `01110010` (72)
6. `01011111` (5F)
7. `01110011` (73)
8. `01110100` (74)
9. `01100001` (61)
10. `01101110` (6E)
11. `01100100` (64)

という 8bit x 11 の bit 列が得られました。(括弧内は 16 進数)

#### 3. 文字列に変換する
得られた bit 列を ASCII の変換テーブルに沿って変換します。
ASCII の変換テーブルはまぁまぁ大きいので省略します。見たい方は [ASCII - Wikipedia](https://ja.wikipedia.org/wiki/ASCII#ASCII%E5%8D%B0%E5%AD%97%E5%8F%AF%E8%83%BD%E6%96%87%E5%AD%97) を見てください。

ASCII の変換テーブルに沿って変換すると
1. `01100001` (61) => `a`
2. `01110010` (72) => `r`
3. `01101101` (6D) => `m`
4. `01101111` (6F) => `o`
5. `01110010` (72) => `r`
6. `01011111` (5F) => `_`
7. `01110011` (73) => `s`
8. `01110100` (74) => `t`
9. `01100001` (61) => `a`
10. `01101110` (6E) => `n`
11. `01100100` (64) => `d`

｢armor_stand｣ が得られました。やったね。


## 実装方針を立てる
手で Base64 をパースして大体の流れを掴んだところで実装方針を立てます。

まず、今回本質的に作りたいものは Base64 デコーダーと ASCII エンコーダーの 2 つです。この 2 つに関してはそれぞれで考えたいと思います。

### Base64 デコーダー
Base64 デコーダーとは、｢Base64 文字列｣ から ｢bit 列｣ への変換と考えた時、大まかに行うべき処理は次の通りになると思います。
1. 変換テーブルを定義する (一度のみ)
2. 文字列を 1 文字ごとに分割する
3. 変換テーブルを元に、各文字を 6bit に変換し bit リストに追加する

### ASCII エンコーダー
ASCII エンコーダーとは、｢bit 列｣ から ｢文字列｣ への変換と考えた時、大まかに行うべき処理は次の通りになると思います。
1. 変換テーブルを定義する (一度のみ)
2. bit リストの先頭 8bit を結合し 8bit 文字列に変換する
3. 変換テーブルを元に、8bit 文字列を文字に変換する
4. 無視する文字リストに含まれていなければ文字リストに追加する
5. 文字リストを文字列に結合する

#### 文字列結合について
文字列結合はマクロを使うことで実現可能になった技術ではありますが、とある理由により少し実装が面倒なためここではちょうど Minecraft Command Advent Calender 2023 14 日目の記事として [データパックで文字列結合](https://qiita.com/crafter1415/items/bb309c9442a980d9fcae)という記事と参考コードが上がっていますのでそちらを利用します。
文字列結合が面倒な理由や具体的な文字列結合の実装について気になる方は上記記事を読んでみてください。

## いざ実装
コード量はある程度控えたつもりですがそれでも多少多くなってしまったので、各関数の細かい解説は省きます。
ある程度の説明はコメントに書いたのでそちらを確認してください。
ちなみに、各関数の先頭に付いている特殊なコメントの書き方については、Minecraft Command Advent Calender 2023 1 日目の記事として [IMP-Doc のすゝめ](https://qiita.com/ChenCMD/items/1c3092e58a13cc6e6697)という記事を書いたのでそちらを読むとより理解できるようになると思います。

### 変換テーブルを定義する
まずは Base64 と ASCII それぞれの変換テーブルを Base64 は 文字から bit 列を、ASCII は bit 列から文字を取得できるような形で定義します。
また、今回の実装は 1.20.4 のためマクロが使えるのでマクロを利用したアクセスを想定して全てのキーを Compound 直下に定義します。

##### base64/functions/import_base64_table.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/0d1ddf8c-194b-d610-7164-816e1f3e373b.png)

<details>
<summary>base64/functions/import_base64_table.mcfunction</summary>

```mcfunction
#> base64:import_base64_table
# @within function base64:load

data modify storage base64:char_table r.A set value [B; 0b, 0b, 0b, 0b, 0b, 0b]
data modify storage base64:char_table r.B set value [B; 0b, 0b, 0b, 0b, 0b, 1b]
data modify storage base64:char_table r.C set value [B; 0b, 0b, 0b, 0b, 1b, 0b]
data modify storage base64:char_table r.D set value [B; 0b, 0b, 0b, 0b, 1b, 1b]
data modify storage base64:char_table r.E set value [B; 0b, 0b, 0b, 1b, 0b, 0b]
data modify storage base64:char_table r.F set value [B; 0b, 0b, 0b, 1b, 0b, 1b]
data modify storage base64:char_table r.G set value [B; 0b, 0b, 0b, 1b, 1b, 0b]
data modify storage base64:char_table r.H set value [B; 0b, 0b, 0b, 1b, 1b, 1b]
data modify storage base64:char_table r.I set value [B; 0b, 0b, 1b, 0b, 0b, 0b]
data modify storage base64:char_table r.J set value [B; 0b, 0b, 1b, 0b, 0b, 1b]
data modify storage base64:char_table r.K set value [B; 0b, 0b, 1b, 0b, 1b, 0b]
data modify storage base64:char_table r.L set value [B; 0b, 0b, 1b, 0b, 1b, 1b]
data modify storage base64:char_table r.M set value [B; 0b, 0b, 1b, 1b, 0b, 0b]
data modify storage base64:char_table r.N set value [B; 0b, 0b, 1b, 1b, 0b, 1b]
data modify storage base64:char_table r.O set value [B; 0b, 0b, 1b, 1b, 1b, 0b]
data modify storage base64:char_table r.P set value [B; 0b, 0b, 1b, 1b, 1b, 1b]
data modify storage base64:char_table r.Q set value [B; 0b, 1b, 0b, 0b, 0b, 0b]
data modify storage base64:char_table r.R set value [B; 0b, 1b, 0b, 0b, 0b, 1b]
data modify storage base64:char_table r.S set value [B; 0b, 1b, 0b, 0b, 1b, 0b]
data modify storage base64:char_table r.T set value [B; 0b, 1b, 0b, 0b, 1b, 1b]
data modify storage base64:char_table r.U set value [B; 0b, 1b, 0b, 1b, 0b, 0b]
data modify storage base64:char_table r.V set value [B; 0b, 1b, 0b, 1b, 0b, 1b]
data modify storage base64:char_table r.W set value [B; 0b, 1b, 0b, 1b, 1b, 0b]
data modify storage base64:char_table r.X set value [B; 0b, 1b, 0b, 1b, 1b, 1b]
data modify storage base64:char_table r.Y set value [B; 0b, 1b, 1b, 0b, 0b, 0b]
data modify storage base64:char_table r.Z set value [B; 0b, 1b, 1b, 0b, 0b, 1b]
data modify storage base64:char_table r.a set value [B; 0b, 1b, 1b, 0b, 1b, 0b]
data modify storage base64:char_table r.b set value [B; 0b, 1b, 1b, 0b, 1b, 1b]
data modify storage base64:char_table r.c set value [B; 0b, 1b, 1b, 1b, 0b, 0b]
data modify storage base64:char_table r.d set value [B; 0b, 1b, 1b, 1b, 0b, 1b]
data modify storage base64:char_table r.e set value [B; 0b, 1b, 1b, 1b, 1b, 0b]
data modify storage base64:char_table r.f set value [B; 0b, 1b, 1b, 1b, 1b, 1b]
data modify storage base64:char_table r.g set value [B; 1b, 0b, 0b, 0b, 0b, 0b]
data modify storage base64:char_table r.h set value [B; 1b, 0b, 0b, 0b, 0b, 1b]
data modify storage base64:char_table r.i set value [B; 1b, 0b, 0b, 0b, 1b, 0b]
data modify storage base64:char_table r.j set value [B; 1b, 0b, 0b, 0b, 1b, 1b]
data modify storage base64:char_table r.k set value [B; 1b, 0b, 0b, 1b, 0b, 0b]
data modify storage base64:char_table r.l set value [B; 1b, 0b, 0b, 1b, 0b, 1b]
data modify storage base64:char_table r.m set value [B; 1b, 0b, 0b, 1b, 1b, 0b]
data modify storage base64:char_table r.n set value [B; 1b, 0b, 0b, 1b, 1b, 1b]
data modify storage base64:char_table r.o set value [B; 1b, 0b, 1b, 0b, 0b, 0b]
data modify storage base64:char_table r.p set value [B; 1b, 0b, 1b, 0b, 0b, 1b]
data modify storage base64:char_table r.q set value [B; 1b, 0b, 1b, 0b, 1b, 0b]
data modify storage base64:char_table r.r set value [B; 1b, 0b, 1b, 0b, 1b, 1b]
data modify storage base64:char_table r.s set value [B; 1b, 0b, 1b, 1b, 0b, 0b]
data modify storage base64:char_table r.t set value [B; 1b, 0b, 1b, 1b, 0b, 1b]
data modify storage base64:char_table r.u set value [B; 1b, 0b, 1b, 1b, 1b, 0b]
data modify storage base64:char_table r.v set value [B; 1b, 0b, 1b, 1b, 1b, 1b]
data modify storage base64:char_table r.w set value [B; 1b, 1b, 0b, 0b, 0b, 0b]
data modify storage base64:char_table r.x set value [B; 1b, 1b, 0b, 0b, 0b, 1b]
data modify storage base64:char_table r.y set value [B; 1b, 1b, 0b, 0b, 1b, 0b]
data modify storage base64:char_table r.z set value [B; 1b, 1b, 0b, 0b, 1b, 1b]
data modify storage base64:char_table r.0 set value [B; 1b, 1b, 0b, 1b, 0b, 0b]
data modify storage base64:char_table r.1 set value [B; 1b, 1b, 0b, 1b, 0b, 1b]
data modify storage base64:char_table r.2 set value [B; 1b, 1b, 0b, 1b, 1b, 0b]
data modify storage base64:char_table r.3 set value [B; 1b, 1b, 0b, 1b, 1b, 1b]
data modify storage base64:char_table r.4 set value [B; 1b, 1b, 1b, 0b, 0b, 0b]
data modify storage base64:char_table r.5 set value [B; 1b, 1b, 1b, 0b, 0b, 1b]
data modify storage base64:char_table r.6 set value [B; 1b, 1b, 1b, 0b, 1b, 0b]
data modify storage base64:char_table r.7 set value [B; 1b, 1b, 1b, 0b, 1b, 1b]
data modify storage base64:char_table r.8 set value [B; 1b, 1b, 1b, 1b, 0b, 0b]
data modify storage base64:char_table r.9 set value [B; 1b, 1b, 1b, 1b, 0b, 1b]
data modify storage base64:char_table r.+ set value [B; 1b, 1b, 1b, 1b, 1b, 0b]
data modify storage base64:char_table r./ set value [B; 1b, 1b, 1b, 1b, 1b, 1b]
```

</details>

##### ascii/functions/import_ascii_table.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/620036b9-b5e4-9793-7be4-1dcb08cb9941.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/bcc079a9-28ee-d740-7f44-4fd7aed1e296.png)

<details>
<summary>ascii/functions/import_ascii_table.mcfunction</summary>

```mcfunction
#> ascii:import_ascii_table
# @within function ascii:load

data modify storage ascii:char_table _.00000000 set value ""
data modify storage ascii:char_table _.00000001 set value ""
data modify storage ascii:char_table _.00000010 set value ""
data modify storage ascii:char_table _.00000011 set value ""
data modify storage ascii:char_table _.00000100 set value ""
data modify storage ascii:char_table _.00000101 set value ""
data modify storage ascii:char_table _.00000110 set value ""
data modify storage ascii:char_table _.00000111 set value ""
data modify storage ascii:char_table _.00001000 set value ""
data modify storage ascii:char_table _.00001001 set value ""
data modify storage ascii:char_table _.00001010 set value ""
data modify storage ascii:char_table _.00001011 set value ""
data modify storage ascii:char_table _.00001100 set value ""
data modify storage ascii:char_table _.00001101 set value ""
data modify storage ascii:char_table _.00001110 set value ""
data modify storage ascii:char_table _.00001111 set value ""
data modify storage ascii:char_table _.00010000 set value ""
data modify storage ascii:char_table _.00010001 set value ""
data modify storage ascii:char_table _.00010010 set value ""
data modify storage ascii:char_table _.00010011 set value ""
data modify storage ascii:char_table _.00010100 set value ""
data modify storage ascii:char_table _.00010101 set value ""
data modify storage ascii:char_table _.00010110 set value ""
data modify storage ascii:char_table _.00010111 set value ""
data modify storage ascii:char_table _.00011000 set value ""
data modify storage ascii:char_table _.00011001 set value ""
data modify storage ascii:char_table _.00011010 set value ""
data modify storage ascii:char_table _.00011011 set value ""
data modify storage ascii:char_table _.00011100 set value ""
data modify storage ascii:char_table _.00011101 set value ""
data modify storage ascii:char_table _.00011110 set value ""
data modify storage ascii:char_table _.00011111 set value ""

data modify storage ascii:char_table _.00100000 set value " "
data modify storage ascii:char_table _.00100001 set value "!"
data modify storage ascii:char_table _.00100010 set value '"'
data modify storage ascii:char_table _.00100011 set value "#"
data modify storage ascii:char_table _.00100100 set value "$"
data modify storage ascii:char_table _.00100101 set value "%"
data modify storage ascii:char_table _.00100110 set value "&"
data modify storage ascii:char_table _.00100111 set value "'"
data modify storage ascii:char_table _.00101000 set value "("
data modify storage ascii:char_table _.00101001 set value ")"
data modify storage ascii:char_table _.00101010 set value "*"
data modify storage ascii:char_table _.00101011 set value "+"
data modify storage ascii:char_table _.00101100 set value ","
data modify storage ascii:char_table _.00101101 set value "-"
data modify storage ascii:char_table _.00101110 set value "."
data modify storage ascii:char_table _.00101111 set value "/"

data modify storage ascii:char_table _.00110000 set value "0"
data modify storage ascii:char_table _.00110001 set value "1"
data modify storage ascii:char_table _.00110010 set value "2"
data modify storage ascii:char_table _.00110011 set value "3"
data modify storage ascii:char_table _.00110100 set value "4"
data modify storage ascii:char_table _.00110101 set value "5"
data modify storage ascii:char_table _.00110110 set value "6"
data modify storage ascii:char_table _.00110111 set value "7"
data modify storage ascii:char_table _.00111000 set value "8"
data modify storage ascii:char_table _.00111001 set value "9"
data modify storage ascii:char_table _.00111010 set value ":"
data modify storage ascii:char_table _.00111011 set value ";"
data modify storage ascii:char_table _.00111100 set value "<"
data modify storage ascii:char_table _.00111101 set value "="
data modify storage ascii:char_table _.00111110 set value ">"
data modify storage ascii:char_table _.00111111 set value "?"

data modify storage ascii:char_table _.01000000 set value "@"
data modify storage ascii:char_table _.01000001 set value "A"
data modify storage ascii:char_table _.01000010 set value "B"
data modify storage ascii:char_table _.01000011 set value "C"
data modify storage ascii:char_table _.01000100 set value "D"
data modify storage ascii:char_table _.01000101 set value "E"
data modify storage ascii:char_table _.01000110 set value "F"
data modify storage ascii:char_table _.01000111 set value "G"
data modify storage ascii:char_table _.01001000 set value "H"
data modify storage ascii:char_table _.01001001 set value "I"
data modify storage ascii:char_table _.01001010 set value "J"
data modify storage ascii:char_table _.01001011 set value "K"
data modify storage ascii:char_table _.01001100 set value "L"
data modify storage ascii:char_table _.01001101 set value "M"
data modify storage ascii:char_table _.01001110 set value "N"
data modify storage ascii:char_table _.01001111 set value "O"

data modify storage ascii:char_table _.01010000 set value "P"
data modify storage ascii:char_table _.01010001 set value "Q"
data modify storage ascii:char_table _.01010010 set value "R"
data modify storage ascii:char_table _.01010011 set value "S"
data modify storage ascii:char_table _.01010100 set value "T"
data modify storage ascii:char_table _.01010101 set value "U"
data modify storage ascii:char_table _.01010110 set value "V"
data modify storage ascii:char_table _.01010111 set value "W"
data modify storage ascii:char_table _.01011000 set value "X"
data modify storage ascii:char_table _.01011001 set value "Y"
data modify storage ascii:char_table _.01011010 set value "Z"
data modify storage ascii:char_table _.01011011 set value "["
data modify storage ascii:char_table _.01011100 set value "\\"
data modify storage ascii:char_table _.01011101 set value "]"
data modify storage ascii:char_table _.01011110 set value "^"
data modify storage ascii:char_table _.01011111 set value "_"

data modify storage ascii:char_table _.01100000 set value "`"
data modify storage ascii:char_table _.01100001 set value "a"
data modify storage ascii:char_table _.01100010 set value "b"
data modify storage ascii:char_table _.01100011 set value "c"
data modify storage ascii:char_table _.01100100 set value "d"
data modify storage ascii:char_table _.01100101 set value "e"
data modify storage ascii:char_table _.01100110 set value "f"
data modify storage ascii:char_table _.01100111 set value "g"
data modify storage ascii:char_table _.01101000 set value "h"
data modify storage ascii:char_table _.01101001 set value "i"
data modify storage ascii:char_table _.01101010 set value "j"
data modify storage ascii:char_table _.01101011 set value "k"
data modify storage ascii:char_table _.01101100 set value "l"
data modify storage ascii:char_table _.01101101 set value "m"
data modify storage ascii:char_table _.01101110 set value "n"
data modify storage ascii:char_table _.01101111 set value "o"

data modify storage ascii:char_table _.01110000 set value "p"
data modify storage ascii:char_table _.01110001 set value "q"
data modify storage ascii:char_table _.01110010 set value "r"
data modify storage ascii:char_table _.01110011 set value "s"
data modify storage ascii:char_table _.01110100 set value "t"
data modify storage ascii:char_table _.01110101 set value "u"
data modify storage ascii:char_table _.01110110 set value "v"
data modify storage ascii:char_table _.01110111 set value "w"
data modify storage ascii:char_table _.01111000 set value "x"
data modify storage ascii:char_table _.01111001 set value "y"
data modify storage ascii:char_table _.01111010 set value "z"
data modify storage ascii:char_table _.01111011 set value "{"
data modify storage ascii:char_table _.01111100 set value "|"
data modify storage ascii:char_table _.01111101 set value "}"
data modify storage ascii:char_table _.01111110 set value "~"
data modify storage ascii:char_table _.01111111 set value ""
```

</details>

### Base64 デコーダーを実装する
全体としての関数のインターフェースは マクロ引数として Base64 文字列を受け取り、storage として Bit 列を返す形で実装します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/9d94b13a-3909-0b65-e5cb-e1a4d8cf08cb.png)

```mcfunction
#> base64:decode/m
# @input args
#   b64str: デコードするテキスト
# @output storage returns:base64
#   bitArray: byte[]
# @api
```

#### 文字列の分割
まずは受け取った文字列を変換テーブルを利用するために一文字ずつ分割します。

##### base64/functions/decode/m.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/73c69412-2f99-42e9-b499-fa2789684139.png)

<details>
<summary>base64/functions/decode/split/m.mcfunction</summary>

```diff
#> base64:decode/m
# @input args
#   b64str: デコードするテキスト
# @output storage returns:base64
#   bitArray: byte[]
# @api

+ #> Private
+ # @within base64:decode/**
+ #declare storage base64:decode
+ 
+ # Base64 文字列を一文字ずつ分割する
+ # args b64str => storage base64:decode chars
+ $data modify storage base64:decode str set value "$(b64str)"
+ data modify storage base64:decode chars set value []
+ function base64:decode/split
```

</details>

##### base64/functions/decode/split.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/37f0092b-4b6d-f3d7-9089-98611c1c4c6c.png)

<details>
<summary>base64/functions/decode/split/m.mcfunction</summary>

```mcfunction
#> base64:decode/split
# @within function
#   base64:decode/m
#   base64:decode/split

# 先頭文字を切り出して配列に追加する
data modify storage base64:decode chars append string storage base64:decode str 0 1

# str を二文字以降にする
data modify storage base64:decode str set string storage base64:decode str 1

# 要素が残ってたら再帰
execute unless data storage base64:decode {str:""} run function base64:decode/split
```

</details>


#### 各文字を Bit 列に変換する
分割した各文字を変換テーブル通して Bit 列に変換します。

##### base64/functions/decode/m.mcfunction
ついでに結果を返すようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/1be9ad46-1abd-dabf-807f-dffc86b83670.png)

<details>
<summary>base64/functions/decode/split/m.mcfunction</summary>

```diff
#> base64:decode/m
# @input args
#   b64str: デコードするテキスト
# @output storage returns:base64
#   bitArray: byte[]
# @api

#> Private
# @within base64:decode/**
#declare storage base64:decode

# Base64 文字列を一文字ずつ分割する
# args b64str => storage base64:decode chars
$data modify storage base64:decode str set value "$(b64str)"
data modify storage base64:decode chars set value []
function base64:decode/split

+ # 分割した各文字を変換テーブルに通して Bit 列に変換する
+ # storage base64:decode chars => storage base64:decode bitArray
+ data modify storage base64:decode bitArray set value []
+ function base64:decode/foreach_char
+ 
+ # storage base64:decode bitArray => storage returns:base64 bitArray
+ data modify storage returns:base64 bitArray set from storage base64:decode bitArray
+ 
+ # リセット
+ data remove storage base64:decode bitArray
```

</details>


##### base64/functions/decode/foreach_char.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/58c692d6-f481-f682-a279-6f612dc9ddd3.png)

<details>
<summary>base64/functions/decode/foreach_char.mcfunction</summary>

```mcfunction
#> base64:decode/foreach_char
# @within function
#   base64:decode/m
#   base64:decode/foreach_char

# 一文字目を pop する
data modify storage base64:decode args.elem set from storage base64:decode chars[0]
data remove storage base64:decode chars[0]

# 変換テーブルを利用して bit 列をリストに push する
function base64:decode/char_to_bit.m with storage base64:decode args

# 残りがあったら再帰
execute if data storage base64:decode chars[0] run function base64:decode/foreach_char
```

</details>

##### base64/functions/decode/char_to_bit.m.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/1d43c1dd-ffff-0347-dfab-9bdb20f1b287.png)

<details>
<summary>base64/functions/decode/char_to_bit.m.mcfunction</summary>

```mcfunction
#> base64:decode/char_to_bit.m
# @input args
#   elem: str
# @output storage base64:decode
#   bitArray
# @within function base64:decode/foreach_char

$data modify storage base64:decode bitArray append from storage base64:char_table r.$(elem)[]
```

</details>


### ASCII エンコーダーを実装する
全体としての関数のインターフェースは storage として Bit 列と無視する文字を受け取り、storage として文字を返す形で実装します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/aead18a1-27c7-6d60-cca6-5f4bf2095062.png)

<details>
<summary>ascii/functions/encode/_.mcfunction</summary>

```mcfunction
#> ascii:encode/_
# @input storage args:ascii
#   bitArray: byte[]
#   ignoreChars: string[]
# @output storage returns:ascii
#   string: string
# @api
```

</details>

#### ASCII 文字への変換
##### ascii/functions/encode/_.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/74ba1bff-cb4d-c1f2-1101-47f35f558d54.png)

<details>
<summary>ascii/functions/encode/_.mcfunction</summary>

```diff
#> ascii:encode/_
# @input storage args:ascii
#   bitArray: byte[]
#   ignoreChars: string[]
# @output storage returns:ascii
#   string: string
# @api

+ #> Private
+ # @within ascii:encode/*
+ #declare storage ascii:encode
+ 
+ # bit 列を文字列に変換する
+ # storage args:ascii (bitArray, ignoreChars) => storage ascii:encode chars
+ data modify storage ascii:encode chars set value []
+ function ascii:encode/rec
```

</details>

##### ascii/functions/encode/rec.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/2778fee7-1fe8-3f42-eace-5fbee14519e7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/fbc11532-0659-cdb7-4802-ffdc7ae77406.png)

<details>
<summary>ascii/functions/encode/rec.mcfunction</summary>

```mcfunction
#> ascii:encode/rec
# @output storage
#   ascii:encode chars: string[]
# @within function ascii:encode/*

# bit 配列の先頭 8 bit を結合した文字列を作成する
# storage args:ascii bitArray[0:4] => storage ascii:encode byteStr
data modify storage ascii:encode _.bit1 set from storage args:ascii bitArray[0]
data modify storage ascii:encode _.bit2 set from storage args:ascii bitArray[1]
data modify storage ascii:encode _.bit3 set from storage args:ascii bitArray[2]
data modify storage ascii:encode _.bit4 set from storage args:ascii bitArray[3]
data modify storage ascii:encode _.bit5 set from storage args:ascii bitArray[4]
data modify storage ascii:encode _.bit6 set from storage args:ascii bitArray[5]
data modify storage ascii:encode _.bit7 set from storage args:ascii bitArray[6]
data modify storage ascii:encode _.bit8 set from storage args:ascii bitArray[7]
function ascii:encode/concat_8bit with storage ascii:encode _
data remove storage ascii:encode _

# 8 bit 文字列から ASCII 文字を取得する
# storage ascii:encode byteStr => storage ascii:encode char
data modify storage ascii:encode _.byteStr set from storage ascii:encode byteStr
data remove storage ascii:encode byteStr
function ascii:encode/bit_to_char.m with storage ascii:encode _
data remove storage ascii:encode _

# 取得した ASCII 文字列が ignoreChars に含まれているかを確認する
data modify storage ascii:encode _.char set from storage ascii:encode char
execute if data storage ascii:encode _{char:"\\"} run data modify storage ascii:encode _.char set value "\\\\"
execute if data storage ascii:encode _{char:"\""} run data modify storage ascii:encode _.char set value "\\\""
function ascii:encode/is_contained_ignore_chars with storage ascii:encode _
data remove storage ascii:encode _

# 取得した ASCII 文字列が ignoreChars に含まれていなければ chars に追加する
execute if data storage ascii:encode {isContained: false} run data modify storage ascii:encode chars append from storage ascii:encode char
data remove storage ascii:encode char
data remove storage ascii:encode isContained

# bit 配列の先頭 8 要素を削除する
data remove storage args:ascii bitArray[7]
data remove storage args:ascii bitArray[6]
data remove storage args:ascii bitArray[5]
data remove storage args:ascii bitArray[4]
data remove storage args:ascii bitArray[3]
data remove storage args:ascii bitArray[2]
data remove storage args:ascii bitArray[1]
data remove storage args:ascii bitArray[0]

# まだ 8 bit 以上ある場合再帰する
execute if data storage args:ascii bitArray[7] run function ascii:encode/rec

```

</details>

##### ascii/functions/encode/concat_8bit.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/77773c9a-2742-ffb0-1185-124324fcc1ba.png)

<details>
<summary>ascii/functions/encode/concat_8bit.m.mcfunction</summary>

```mcfunction
#> ascii:encode/concat_8bit.m
# @input args
#   bit1: byte
#   bit2: byte
#   bit3: byte
#   bit4: byte
#   bit5: byte
#   bit6: byte
#   bit7: byte
#   bit8: byte
# @output storage ascii:encode
#   byteStr: string
# @within function ascii:encode/rec

$data modify storage ascii:encode byteStr set value '$(bit1)$(bit2)$(bit3)$(bit4)$(bit5)$(bit6)$(bit7)$(bit8)'
```

</details>

##### ascii/functions/encode/bit_to_char.m.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/2064e684-b423-b000-5d6d-6026fce6e9ac.png)

<details>
<summary>ascii/functions/encode/bit_to_char.m.mcfunction</summary>

```mcfunction
#> ascii:encode/bit_to_char.m
# @input args
#   byteStr: str
# @output storage ascii:encode
#   char: string | undefined
# @within function ascii:encode/rec

data remove storage ascii:encode char
$data modify storage ascii:encode char set from storage ascii:char_table _.$(byteStr)
```

</details>

##### ascii/functions/encode/is_contained_ignore_chars.m.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/849193a0-66f2-d93f-737b-31639cfe07d6.png)

<details>
<summary>ascii/functions/encode/is_contained_ignore_chars.m.mcfunction</summary>

```mcfunction
#> ascii:encode/is_contained_ignore_chars.m
# @input args
#   char: str
#     文字
# @output storage ascii:encode
#   isContained: boolean
#     ignoreChars に含まれているか否か
# @within function ascii:encode/rec

$execute store success storage ascii:encode isContained byte 1 if data storage args:ascii {ignoreChars:["$(char)"]}
```

</details>

#### 文字リストを文字列に結合する
##### ascii/functions/encode/_.mcfunction
文字列配列から文字列への結合について、は前述した記事のコードにそれを行う関数が `concat:concat_all` として提供されているのでそれを利用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/d3ef844d-99bc-935c-92df-24024971d76c.png)

<details>
<summary>ascii/functions/encode/_.mcfunction</summary>

```diff
#> ascii:encode/_
# @input storage args:ascii
#   bitArray: byte[]
#   ignoreChars: string[]
# @output storage returns:ascii
#   string: string
# @api

#> Private
# @within ascii:encode/*
#declare storage ascii:encode

# bit 列を文字列に変換する
# storage args:ascii (bitArray, ignoreChars) => storage ascii:encode chars
data modify storage ascii:encode chars set value []
function ascii:encode/rec

+ # 文字列を結合する
+ # storage ascii:encode chars => storage concat: result
+ data modify storage concat: args set from storage ascii:encode chars
+ function concat:concat_all
+ 
+ # storage concat: result => storage returns:ascii string
+ data modify storage returns:ascii string set from storage concat: result
+ 
+ # リセット
+ data remove storage args:ascii bitArray
+ data remove storage args:ascii ignoreChars
```

</details>

## 動作確認
利用順序としては `base64:decode/m` を呼び出した上で、その結果を `ascii:encode/_` で変換する形です。
下記のようなコードで利用することが出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/bb828b12-d51f-874f-14a7-bc3fb41e608b.png)

<details>
<summary>コード</summary>

```mcfunction
#> minecraft:test
# @private
# @user

# b64str => returns:base64 bitArray
tellraw @a [{"text":"<< YXJtb3Jfc3RhbmQ="}]
function base64:decode/m {b64str:"YXJtb3Jfc3RhbmQ="}

# returns:base64 bitArray => args:ascii bitArray
data modify storage args:ascii bitArray set from storage returns:base64 bitArray

# args:ascii bitArray => returns:ascii str
data modify storage args:ascii ignoreChars set value []
function ascii:encode/_

tellraw @a [{"text":">> "},{"storage":"returns:ascii","nbt":"string"}]
```

</details>

上記関数を実行してみると...

![2023-12-22_01.41.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/5f708f2d-c2f0-8522-7947-a58e13affcea.png)

成功です。手でパースしてみたときと同じように ｢armor_stand｣ が出力されました。

次に ignoreChars に `"_"` を指定して実行してみます...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/45da648e-38fc-aa3b-0e70-c13950d0a476.png)

<details>
<summary>コード</summary>

```diff
#> minecraft:test
# @private
# @user

# b64str => returns:base64 bitArray
tellraw @a [{"text":"<< YXJtb3Jfc3RhbmQ="}]
function base64:decode/m {b64str:"YXJtb3Jfc3RhbmQ="}

# returns:base64 bitArray => args:ascii bitArray
data modify storage args:ascii bitArray set from storage returns:base64 bitArray

# args:ascii bitArray => returns:ascii str
- data modify storage args:ascii ignoreChars set value []
+ data modify storage args:ascii ignoreChars set value ["_"]
function ascii:encode/_

tellraw @a [{"text":">> "},{"storage":"returns:ascii","nbt":"string"}]
```

</details>

![2023-12-22_01.45.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/0891490d-bbfc-201f-1474-7a39d7313410.png)

｢armorstand｣ が出力されました。いい感じですね。

## まとめ
この記事では 1.20.2 で新たに追加されたマクロ機能を利用してデータパックで Base64 パーサーを実装してみました。
マクロを利用したことでやや可読性が落ちてしまった感じがしますが、当初想定していたよりはかなりスッキリした実装に落ち着いた気がします。
ちなみに、Base64 パーサーをマイクラで実装して何に利用するんだと思う方も居るかも知れませんが、それは Minecraft Command Advent Calendar 2023 25 日目の記事で説明します。[^3]

[^3]: そもそも、元々この記事は 25 日目の記事の一部として記述する予定だったのですが、このスクロールバーを見て貰えれば分かるようにとんでもない記事の分量になってしまいそうだったので分離したという話があります。