Base64のしくみを理解するためにRubyで実装してみる

Base64とは、A-Z, a-z, 0-9, +, / の64文字のみを用いてデータを表現するためのエンコード方式です。電子メールや Basic 認証なんかによく用いられているそうです。

さて、この Base64 という方式。プログラミングしていたらよく聞く単語だと思うんですけど、具体的にどんな処理をしているかまではちゃんと理解していませんでした。ので、今回中身の処理を理解するために Ruby で実装してみました。

class Base64
 class << self
   BASE64_TABLE = [*"A".."Z", *"a".."z", *"0".."9", "+", "/"]

   def encode(str)
     raise ArgumentError unless str.is_a?(String)

     # 引数の文字列を16進数→2進数に変換
     str_bin = str.unpack1("H*").each_char.map { |a| format("%04b", a.to_i) }

     # 2進数を6ビットごとに分割
     # 最後の値が6ビット未満だった場合は、不足分を0で詰める
     bin_per_6bit = str_bin.join.scan(/\d{1,6}/)
     if bin_per_6bit.last.size < 6
       last = bin_per_6bit.pop
       bin_per_6bit.push(last.ljust(6, "0"))
     end

     # 変換表に従って2進数をエンコードしていき、4文字ごとに分割する
     # 最後の値が4文字未満だった場合は、不足分を=で詰める
     encoded = bin_per_6bit.map { |bin| BASE64_TABLE[bin.to_i(2)] }.join.scan(/.{1,4}/)
     if encoded.last.size < 4
       last = encoded.pop
       encoded.push(last.ljust(4, "="))
     end
     encoded.join
   end

   def decode(str)
     raise ArgumentError unless str.is_a?(String)

     [str.delete("=").each_char.map { |c| "%06b" % BASE64_TABLE.find_index(c) }
         .join.scan(/\d{1,4}/).map { |s| s.to_i(2) }.join].pack("H*").chomp("\x00")
   end
 end
end

エンコーディングの流れ

順を追って説明すると以下の通りになります。結構細かい処理が続いてる。

1. 対象の文字列を 16 進数に変換する
2. 16 進数に変換した数字をそれぞれ 2 進数に変換する
3. 6 ビットごとに分割して、足りない部分は 0 で詰める
4. 変換表にしたがって各 6 ビットをエンコードしていく
5. 4文字ごとに分割して末尾が余る場合、"=" で詰める
6. 最後にすべての文字をつなげて完成

デコードはこれと逆の手順を踏めばいいので省略します。

例えば "ABCDEFG" という文字列をとってみると、以下のような変換が行われます。

ABCDEFG

41 42 43 44 45 46 47

0100 0001, 0100 0010, 0100 0011, 0100 0100, 0100 0101, 0100 0110, 0100 0111

010000 010100 001001 000011 010001 000100 010101 000110 010001 11

010000 010100 001001 000011 010001 000100 010101 000110 010001 110000

QUJDREVGRw

"QUJD", "REVG", "Rw"

"QUJD", "REVG", "Rw=="

QUJDREVGRw==

この処理を Ruby のメソッドで行おうとすると結構細かい処理が必要になりました。おかげでいろいろ勉強になった(笑)。

ちなみに base64 という標準ライブラリは Ruby にあります

こんな自前で実装しなくても base64 という標準ライブラリが Ruby にはちゃんと用意されているので、こんな長ったらしいコード書く必要はありません(笑)。

require "base64"
Base64.encode64("ABCDEFG")
Base64.decode64("QUJDREVGRw==")

こんな感じで使えます。ちなみに実装はどうなっているかというと、Array#pack と String#unpack を使っているだけですね。pack / unpack の引数にわたすテンプレートには Base64 変換を行う "m" が渡されています。これをやっとけば中で勝手に変換してくれます。

def encode64(bin)
 [bin].pack("m")
end

def decode64(str)
 str.unpack1("m")
end