Module: Oxygene::Base32
- Defined in:
- lib/oxygene/base32.rb
Overview
Fast, optimized implementation of RFC 4648 Base32 for encoding and decoding binary data.
Decoding accepts either lowercase or uppercase input and accepts = padding if present. Encoder only
creates Base32 output using lowercase alphabet and without padding, since that's what it used in
DASL/ATProto CIDs.
Based on the code of the base32 gem by Samantha Tesla (MIT).
Class Method Summary collapse
-
.decode(data, start_offset = 0, prefix = "") ⇒ String
Decodes Base32-encoded data into the original byte string.
-
.encode(data, start_offset = 0, prefix = "") ⇒ String
Encodes a binary string into Base32 (lowercase and without padding).
Class Method Details
.decode(data, start_offset = 0, prefix = "") ⇒ String
Decodes Base32-encoded data into the original byte string.
For compatibility, the decoder accepts both lowercase or uppercase inputs and trailing
= padding if present. If you want to reject uppercase characters or padding, you need
to perform such checks manually.
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/oxygene/base32.rb', line 129 def self.decode(data, start_offset = 0, prefix = "") total_size = data.bytesize raise ArgumentError, "Start offset can't be negative" if start_offset < 0 raise ArgumentError, "Start offset is larger than the length of data" if start_offset > total_size encoded_end = total_size if encoded_end > start_offset && data.getbyte(encoded_end - 1) == 61 # '=' # Only decode until the beginning of padding encoded_end -= 1 while encoded_end > start_offset && data.getbyte(encoded_end - 1) == 61 padding_size = total_size - encoded_end # Full blocks are 8 5-bit Base32 characters decoded into 5 8-bit bytes. # Here, we check if in the last incomplete block the padding (if included) # covers the whole rest of the block. # # Because how 5-bit characters map into 8-bit bytes, only a final incomplete # block of 2, 4, 5 or 7 characters makes sense - 1, 3 and 6 don't include enough # bits to create another byte. Which is why padding_size can't be 7 here. # # "n & 7" is the same as "n % 8", just slightly faster. unpadded_remainder = (encoded_end - start_offset) & 7 expected_padding = (8 - unpadded_remainder) & 7 if ((total_size - start_offset) & 7) != 0 || padding_size != expected_padding || padding_size > 6 raise DecodeError, "Invalid Base32 padding" end end encoded_size = encoded_end - start_offset remainder = encoded_size & 7 # See above for why only these are allowed unless remainder == 0 || remainder == 2 || remainder == 4 || remainder == 5 || remainder == 7 raise DecodeError, "Invalid Base32 length" end output = prefix.dup.force_encoding(Encoding::BINARY) offset = start_offset full_block_end = encoded_end - remainder table = BASE32_DECODE_TABLE # Process full blocks of 8 5-bit Base32 characters and decode them into 5 8-bit # bytes. The characters are first mapped into indexes of 0...32, and then combined # into a single 40-bit number, which is sliced into 5 bytes. # # The BASE32_DECODE_TABLE table is a table mapping character indexes 0...256 to # the 0...32 Base32 value. Using this table, a character's index can be looked up in # O(1) time using its .ord code. The special value 255 means that there is no Base32 # character with a given ASCII code and that an exception should be raised. while offset < full_block_end v0 = table[data.getbyte(offset)] v1 = table[data.getbyte(offset + 1)] v2 = table[data.getbyte(offset + 2)] v3 = table[data.getbyte(offset + 3)] v4 = table[data.getbyte(offset + 4)] v5 = table[data.getbyte(offset + 5)] v6 = table[data.getbyte(offset + 6)] v7 = table[data.getbyte(offset + 7)] # Combine the bits of the 8 index values using OR. If any of the 8 characters # has any bits above the 6th (in practice it could only be 255), the combined # value will also have the bits set so it will be higher than 31. invalid_value = v0 | v1 | v2 | v3 | v4 | v5 | v6 | v7 invalid_character!(data, offset, offset + 8, table) if invalid_value > 31 value = (v0 << 35) | (v1 << 30) | (v2 << 25) | (v3 << 20) | (v4 << 15) | (v5 << 10) | (v6 << 5) | v7 output << ((value >> 32) & 255) output << ((value >> 24) & 255) output << ((value >> 16) & 255) output << ((value >> 8) & 255) output << (value & 255) offset += 8 end # Finish the output for the final incomplete block of less than 40 bits: case remainder when 2 v0 = table[data.getbyte(offset)] v1 = table[data.getbyte(offset + 1)] invalid_character!(data, offset, encoded_end, table) if (v0 | v1) > 31 value = (v0 << 5) | v1 raise DecodeError, "Invalid Base32 trailing bits" unless (value & 3) == 0 output << (value >> 2) when 4 v0 = table[data.getbyte(offset)] v1 = table[data.getbyte(offset + 1)] v2 = table[data.getbyte(offset + 2)] v3 = table[data.getbyte(offset + 3)] invalid_character!(data, offset, encoded_end, table) if (v0 | v1 | v2 | v3) > 31 value = (v0 << 15) | (v1 << 10) | (v2 << 5) | v3 raise DecodeError, "Invalid Base32 trailing bits" unless (value & 15) == 0 output << ((value >> 12) & 255) output << ((value >> 4) & 255) when 5 v0 = table[data.getbyte(offset)] v1 = table[data.getbyte(offset + 1)] v2 = table[data.getbyte(offset + 2)] v3 = table[data.getbyte(offset + 3)] v4 = table[data.getbyte(offset + 4)] invalid_character!(data, offset, encoded_end, table) if (v0 | v1 | v2 | v3 | v4) > 31 value = (v0 << 20) | (v1 << 15) | (v2 << 10) | (v3 << 5) | v4 raise DecodeError, "Invalid Base32 trailing bits" unless (value & 1) == 0 output << ((value >> 17) & 255) output << ((value >> 9) & 255) output << ((value >> 1) & 255) when 7 v0 = table[data.getbyte(offset)] v1 = table[data.getbyte(offset + 1)] v2 = table[data.getbyte(offset + 2)] v3 = table[data.getbyte(offset + 3)] v4 = table[data.getbyte(offset + 4)] v5 = table[data.getbyte(offset + 5)] v6 = table[data.getbyte(offset + 6)] invalid_character!(data, offset, encoded_end, table) if (v0 | v1 | v2 | v3 | v4 | v5 | v6) > 31 value = (v0 << 30) | (v1 << 25) | (v2 << 20) | (v3 << 15) | (v4 << 10) | (v5 << 5) | v6 raise DecodeError, "Invalid Base32 trailing bits" unless (value & 7) == 0 output << ((value >> 27) & 255) output << ((value >> 19) & 255) output << ((value >> 11) & 255) output << ((value >> 3) & 255) end output end |
.encode(data, start_offset = 0, prefix = "") ⇒ String
Encodes a binary string into Base32 (lowercase and without padding).
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/oxygene/base32.rb', line 48 def self.encode(data, start_offset = 0, prefix = "") total_size = data.bytesize raise ArgumentError, "Start offset can't be negative" if start_offset < 0 raise ArgumentError, "Start offset is larger than the length of data" if start_offset > total_size encoded_size = total_size - start_offset output = prefix.dup offset = start_offset full_block_end = total_size - (encoded_size % 5) # A single base32 character is one of the 32 values in the BASE32_ALPHABET string # above, i.e. 5 bits. The BASE32_ENCODE_TABLE array stores a (flattened) 32x32 table # of all possible combinations of two-character pairs (10 bits). # # Instead of taking the 40 bits of a 5-byte slice of the original string, slicing # it into 8 5-bit pieces and looking up 8 separate base32 characters, we process # 10 bits at a time here, looking up 4 two-character pairs. This allows us to do # only half the amount of array lookups, shifts and bitwise ands. while offset < full_block_end value = (data.getbyte(offset) << 32) | (data.getbyte(offset + 1) << 24) | (data.getbyte(offset + 2) << 16) | (data.getbyte(offset + 3) << 8) | (data.getbyte(offset + 4)) output << BASE32_ENCODE_TABLE[(value >> 30) & 1023] output << BASE32_ENCODE_TABLE[(value >> 20) & 1023] output << BASE32_ENCODE_TABLE[(value >> 10) & 1023] output << BASE32_ENCODE_TABLE[(value) & 1023] offset += 5 end # Finish the output for the final incomplete block of less than 40 bits: case total_size - offset when 1 output << BASE32_ENCODE_TABLE[data.getbyte(offset) << 2] when 2 value = (data.getbyte(offset) << 8) | data.getbyte(offset + 1) output << BASE32_ENCODE_TABLE[value >> 6] output << BASE32_ENCODE_TABLE[(value & 63) << 4] when 3 value = (data.getbyte(offset) << 16) | (data.getbyte(offset + 1) << 8) | (data.getbyte(offset + 2)) output << BASE32_ENCODE_TABLE[value >> 14] output << BASE32_ENCODE_TABLE[(value >> 4) & 1023] output << BASE32_ALPHABET.getbyte((value & 15) << 1) when 4 value = (data.getbyte(offset) << 24) | (data.getbyte(offset + 1) << 16) | (data.getbyte(offset + 2) << 8) | (data.getbyte(offset + 3)) output << BASE32_ENCODE_TABLE[value >> 22] output << BASE32_ENCODE_TABLE[(value >> 12) & 1023] output << BASE32_ENCODE_TABLE[(value >> 2) & 1023] output << BASE32_ALPHABET.getbyte((value & 3) << 3) end output end |