Base64を実装して理解する

# Base64とは

あらゆるデータ：テキストデータ、バイナリデータ（画像、音声など)から**64文字**をを用いてエンコードする方式です。

```
- [A-Z]: 26文字
- [a-z]: 26文字
- [0-9]: 10文字
- [+ /]: 2文字

ABCDEFGHIJKLMNOPQRSTUVWXYZ 
abcdefghijklmnopqrstuvwxyz
0123456789
+/
```
Base64が誕生した理由は上記以外の文字を扱うことの出来ない通信環境にて**マルチバイト文字（漢字など）**や**バイナリデータ**を扱うためです。**7ビットのデータしか扱うことの出来ない電子メール**にて広く利用されています。

バイナリデータなどをやりとりするには、文字列に変換する必要があるので、文字列を変換した時に**4文字**グループごとにまとめ、足りない部分は`=`で埋めるため、厳密に言えば`=`も含めて**65文字**が使用されています。

URLや正規表現の中でBase64を用いると`+`や`/`は特別な意味を持つので、`+`は`-`、`/`は`_`に変換する場合があります。


# Base64のアルゴリズム

#### エンコード

変換したいデータを2進数で表し、6ビットずつ区切り、6ビットに足りない場合は`0`で埋めます。
例えば文字`a`は次のように変換します。
`a => 01100001 => 011000　　01`最後は2ビットしかないので、`0`で埋めると`011000　010000`になります。
変換テーブルを使用してインデックスから文字を取得し、４文字グループを作ります。
![base64.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524261/5d8c35c8-4e4f-74ef-86bc-4d55cb2e884a.png)
４文字に足りない場合は`=`で埋めます。
`011000(24) 010000(16) => 　Y Q`、長さは２しかないので、`=`で埋めると`YQ==`になります。

#### デコード
エンコードされた文から`=`を取り除き、上記の変換テーブルを使用して文字からインデックスを取得し、2進数で表します。
`YQ== => YQ => Y Q => 24 16 => 011000 010000 => 011000010000`
8ビットごとに[ASCIIコード](https://e-words.jp/p/r-ascii.html)を用いて変換し、余った`0`は捨てます。
`011000010000 => 01100001 0000 => 01100001(97) => a`


# Rubyで実装してみます
```ruby
class MyBase64
  # 変換テーブル
  ConvertTable = [*'A'..'Z', *'a'..'z', *'0'..'9', '+', '/']
  class << self
    def encode(str)
      # 文字列をバイナリに変換
      # rjustは８桁の文字列を作るため足りない部分を０で左側に詰めます。
      # https://docs.ruby-lang.org/ja/latest/method/String/i/rjust.html
      binary_str = str.bytes.map { |byte| byte.to_s(2).rjust(8, '0') }.join
      # ６ビットに足りない部分を0で埋める
      binary_str += '0' while binary_str.size % 6 != 0
      # 6ビットごとに分割
      result = binary_str.scan(/.{6}/).map { |chunk| ConvertTable[chunk.to_i(2)] }.join
      # 長さは4より少ないときは=で埋める
      result += '=' while result.size % 4 != 0
      result
    end

    def decode(str)
      # =を除去
      str.gsub!(/[=]/, '')
      # 変換テーブルを逆順にする（文字からインデックスを取得）
      binary_str = str.chars.map { |ch| ConvertTable.index(ch).to_s(2).rjust(6, '0') }.join
      # 余った0を除去
      binary_str.chop! while binary_str.size % 8 != 0
      # 8ビットごとに分割、ACSIIコードに戻す
      binary_str.scan(/.{8}/).map { |chunk| chunk.to_i(2).chr }.join
    end

    def encode_urlsafe(str)
      encode(str).tr('+/', '-_')
    end

    def decode_urlsafe(str)
      decode(str.tr('-_', '+/'))
    end
  end
end


# 標準ライブラリと動作検証
require 'base64'
input = gets.chop #fはkjfはskfはsdkhf2y3r８１９２３８９２yfはs

a = MyBase64.encode(input)         #ZuOBr2tqZuOBr3NrZuOBr3Nka2hmMnkzcu+8mO+8ke+8me+8ku+8k++8mO+8me+8knlm44Gvcw==
b = Base64.strict_encode64(input)  #ZuOBr2tqZuOBr3NrZuOBr3Nka2hmMnkzcu+8mO+8ke+8me+8ku+8k++8mO+8me+8knlm44Gvcw==
c = MyBase64.encode_urlsafe(input) #ZuOBr2tqZuOBr3NrZuOBr3Nka2hmMnkzcu-8mO-8ke-8me-8ku-8k--8mO-8me-8knlm44Gvcw==
d = Base64.urlsafe_encode64(input) #ZuOBr2tqZuOBr3NrZuOBr3Nka2hmMnkzcu-8mO-8ke-8me-8ku-8k--8mO-8me-8knlm44Gvcw==
puts a, b, c, d

e = MyBase64.decode(a)         #fはkjfはskfはsdkhf2y3r８１９２３８９２yfはs
f = Base64.strict_decode64(b)  #fはkjfはskfはsdkhf2y3r８１９２３８９２yfはs
g = MyBase64.decode_urlsafe(c) #fはkjfはskfはsdkhf2y3r８１９２３８９２yfはs
h = Base64.urlsafe_decode64(d) #fはkjfはskfはsdkhf2y3r８１９２３８９２yfはs
puts e, f, g, h
```
***
参考文献：
https://ja.wikipedia.org/wiki/Base64