1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ruby で Base65536 を実装する

Last updated at Posted at 2024-11-28

はじめに

バイナリを文字列に変換する際、Base64 がよく使用されます。

Base64 エンコード方式は、ASCII テキスト (または任意のバイナリーデータを受け入れるにはまだ不十分な ASCII のスーパーセット) しか扱えないメディア上で保存や送信を行う際に、バイナリーデータをエンコードするために一般的に使用されます。これにより、転送中にデータが変更されることなく、そのままの状態を確実に保持します。

Base64 - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN より

Base64 では ASCII 文字列に変換しますが、より多くの文字を利用すれば変換後の文字列の長さを短くできるのではと考えました。そこで調べてみると、世の中にはいろいろな Base エンコーディングがあることを知りました。

今回は、それらの中でも実装がシンプルな Base65536 を Ruby で実装してみました。

実装

Base64 が ASCII 文字列に変換するのに対して、Base65536 は Unicode 文字列に変換します1。変換の際、未割り当てのコードポイントや空白、制御文字などへの変換は避け、安全に使用できるよう工夫されています。

今回は以下の Python での実装を Ruby に移植してみました。

base65536.rb
module Base65536
  BLOCK_START = {
    0 => 13312,
    1 => 13568, 2 => 13824, 3 => 14080, 4 => 14336, 5 => 14592, 6 => 14848, 7 => 15104, 8 => 15360, 9 => 15616, 10 => 15872, 11 => 16128, 12 => 16384, 13 => 16640, 14 => 16896, 15 => 17152, 16 => 17408, 17 => 17664, 18 => 17920, 19 => 18176, 20 => 18432, 21 => 18688, 22 => 18944, 23 => 19200, 24 => 19456, 25 => 19968, 26 => 20224, 27 => 20480, 28 => 20736, 29 => 20992, 30 => 21248, 31 => 21504, 32 => 21760, 33 => 22016, 34 => 22272, 35 => 22528, 36 => 22784, 37 => 23040, 38 => 23296, 39 => 23552, 40 => 23808, 41 => 24064, 42 => 24320, 43 => 24576, 44 => 24832, 45 => 25088, 46 => 25344, 47 => 25600, 48 => 25856, 49 => 26112, 50 => 26368, 51 => 26624, 52 => 26880, 53 => 27136, 54 => 27392, 55 => 27648, 56 => 27904, 57 => 28160, 58 => 28416, 59 => 28672, 60 => 28928, 61 => 29184, 62 => 29440, 63 => 29696, 64 => 29952, 65 => 30208, 66 => 30464, 67 => 30720, 68 => 30976, 69 => 31232, 70 => 31488, 71 => 31744, 72 => 32000, 73 => 32256, 74 => 32512, 75 => 32768, 76 => 33024, 77 => 33280, 78 => 33536, 79 => 33792, 80 => 34048, 81 => 34304, 82 => 34560, 83 => 34816, 84 => 35072, 85 => 35328, 86 => 35584, 87 => 35840, 88 => 36096, 89 => 36352, 90 => 36608, 91 => 36864, 92 => 37120, 93 => 37376, 94 => 37632, 95 => 37888, 96 => 38144, 97 => 38400, 98 => 38656, 99 => 38912, 100 => 39168,
    101 => 39424, 102 => 39680, 103 => 39936, 104 => 40192, 105 => 40448, 106 => 41216, 107 => 41472, 108 => 41728, 109 => 42240, 110 => 67072, 111 => 73728, 112 => 73984, 113 => 74240, 114 => 77824, 115 => 78080, 116 => 78336, 117 => 78592, 118 => 82944, 119 => 83200, 120 => 92160, 121 => 92416, 122 => 131072, 123 => 131328, 124 => 131584, 125 => 131840, 126 => 132096, 127 => 132352, 128 => 132608, 129 => 132864, 130 => 133120, 131 => 133376, 132 => 133632, 133 => 133888, 134 => 134144, 135 => 134400, 136 => 134656, 137 => 134912, 138 => 135168, 139 => 135424, 140 => 135680, 141 => 135936, 142 => 136192, 143 => 136448, 144 => 136704, 145 => 136960, 146 => 137216, 147 => 137472, 148 => 137728, 149 => 137984, 150 => 138240, 151 => 138496, 152 => 138752, 153 => 139008, 154 => 139264, 155 => 139520, 156 => 139776, 157 => 140032, 158 => 140288, 159 => 140544, 160 => 140800, 161 => 141056, 162 => 141312, 163 => 141568, 164 => 141824, 165 => 142080, 166 => 142336, 167 => 142592, 168 => 142848, 169 => 143104, 170 => 143360, 171 => 143616, 172 => 143872, 173 => 144128, 174 => 144384, 175 => 144640, 176 => 144896, 177 => 145152, 178 => 145408, 179 => 145664, 180 => 145920, 181 => 146176, 182 => 146432, 183 => 146688, 184 => 146944, 185 => 147200, 186 => 147456, 187 => 147712, 188 => 147968, 189 => 148224, 190 => 148480, 191 => 148736, 192 => 148992, 193 => 149248, 194 => 149504, 195 => 149760, 196 => 150016, 197 => 150272, 198 => 150528, 199 => 150784, 200 => 151040,
    201 => 151296, 202 => 151552, 203 => 151808, 204 => 152064, 205 => 152320, 206 => 152576, 207 => 152832, 208 => 153088, 209 => 153344, 210 => 153600, 211 => 153856, 212 => 154112, 213 => 154368, 214 => 154624, 215 => 154880, 216 => 155136, 217 => 155392, 218 => 155648, 219 => 155904, 220 => 156160, 221 => 156416, 222 => 156672, 223 => 156928, 224 => 157184, 225 => 157440, 226 => 157696, 227 => 157952, 228 => 158208, 229 => 158464, 230 => 158720, 231 => 158976, 232 => 159232, 233 => 159488, 234 => 159744, 235 => 160000, 236 => 160256, 237 => 160512, 238 => 160768, 239 => 161024, 240 => 161280, 241 => 161536, 242 => 161792, 243 => 162048, 244 => 162304, 245 => 162560, 246 => 162816, 247 => 163072, 248 => 163328, 249 => 163584, 250 => 163840, 251 => 164096, 252 => 164352, 253 => 164608, 254 => 164864, 255 => 165120,
    -1 => 5376
  }.freeze
  B2 = BLOCK_START.invert.freeze

  class DecodeError < StandardError; end

  class << self
    def encode(str)
      bytes = str.bytes.lazy
      # 2 バイトごとに Unicode コードポイントにマッピングする。
      code_points = bytes.each_slice(2).map { |b1, b2| BLOCK_START[b2 || -1] + b1 }
      chars = code_points.map { _1.chr(Encoding::UTF_8) }

      chars.force.join
    end

    def decode(encoded)
      bytes =
        encoded.each_char.flat_map do |char|
          code_point = char.ord
          b1 = get_lower_8_bits(code_point)
          b2 = B2[code_point - b1]
          raise(DecodeError.new("Invalid Base65536 character: #{char}")) unless b2

          b2 == -1 ? b1 : [b1, b2]
        end

      bytes.pack('C*').force_encoding(Encoding::UTF_8)
    end

    private

    def get_lower_8_bits(n)
      n & 0xFF
    end
  end
end

# この Ruby ファイルを直接実装した際に単体テストを動かす。
# 他の Ruby ファイルで require した際は動かさない。
if __FILE__ == $PROGRAM_NAME
  require 'minitest'
  require 'minitest/autorun'

  class Base65536Test < Minitest::Test
    def test_encode
      assert_equal('驈ꍬ慯𔔠𓁯饬ᔡ', Base65536.encode('Hello, world!'))
    end

    def test_decode
      assert_equal('Hello, world!', Base65536.decode('驈ꍬ慯𔔠𓁯饬ᔡ'))
      error = assert_raises(Base65536::DecodeError) do
        Base65536.decode('驈ꍬ慯𔔠𓁯饬ᔡ🐹')
      end
      assert_equal('Invalid Base65536 character: 🐹', error.message)
    end
  end
end

試しに以下の画像2のバイナリをエンコードしてみます。

yoyo.jpg
jpg_file = Pathname(Dir.home).join('Downloads/yoyo.jpg')
jpg_file.size
#=> 292315

# バイナリ
binary = jpg_file.binread
# バイナリ → 16 進数文字列
hex_string = binary.unpack1('H*')
# バイナリ → Base64 文字列
base64_string = Base64.strict_encode64(binary)
# バイナリ → Base65536 文字列
base65536_string = Base65536.encode(binary)

# 文字列の文字数
[hex_string.length, base64_string.length, base65536_string.length]
#=> [584630, 389756, 146158]

# 文字列のバイト長
[hex_string.bytesize, base64_string.bytesize, base65536_string.bytesize]
#=> [584630, 389756, 518251]

Base65536 でエンコードするとバイト長は大きくなってしまいますが、文字数は減りますね。

バージョン情報

RUBY_VERSION
#=> "3.3.6"

Base64::VERSION
#=> "0.2.0"

参考

  1. もちろん ASCII 文字列と比べると可搬性は落ちます。例えば Unicode を符号化する方式ではない文字コード (e.g. Shift_JIS) だと使用できないなど。

  2. ホースカドライというヨーヨーの写真です。可愛いでしょ?🥰

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?