lib/multibases/base_x.rb
# frozen_string_literal: true
require 'multibases/byte_array'
require 'multibases/ord_table'
module Multibases
class BaseX
def inspect
"[Multibases::Base#{@table.base} " \
"alphabet=\"#{@table.alphabet}\"" \
"#{@table.strict? ? ' strict' : ''}" \
']'
end
class Table < IndexedOrdTable
def self.from(alphabet, **opts)
raise ArgumentError, 'Alphabet too long' if alphabet.length >= 255
alphabet = alphabet.bytes if alphabet.respond_to?(:bytes)
alphabet.map!(&:ord)
new(alphabet, **opts)
end
end
def initialize(alphabet, strict: false, encoding: nil)
@table = Table.from(alphabet, strict: strict, encoding: encoding)
end
##
# Encode +plain+ to an encoded string
#
# @param plain [String, Array] plain string or byte array
# @return [EncodedByteArray] encoded byte array
#
def encode(plain)
return EncodedByteArray::EMPTY if plain.empty?
plain = plain.bytes unless plain.is_a?(Array)
expected_length = @table.encoded_length(plain)
# Find leading zeroes
zeroes_count = [
0,
plain.find_index { |b| b.ord != 0 } || plain.length
].max
plain = plain.drop(zeroes_count)
expected_length = @table.encoded_length(plain) unless @table.pad_to_power?
# Encode number into destination base as byte array
output = []
plain_big_number = plain.inject { |a, b| (a << 8) + b.ord }
while plain_big_number >= @table.base
mod = plain_big_number % @table.base
output.unshift(@table.ord_at(mod))
plain_big_number = (plain_big_number - mod) / @table.base
end
output.unshift(@table.ord_at(plain_big_number))
# Prepend the leading zeroes
@table.encoded_zeroes_length(zeroes_count).times do
output.unshift(@table.zero)
end
# Padding at the front (to match expected length). Because of the
if @table.pad_to_power?
(expected_length - output.length).times do
output.unshift(@table.zero)
end
end
EncodedByteArray.new(output, encoding: @table.encoding)
end
##
# Decode +encoded+ to a byte array
#
# @param encoded [String, Array, ByteArray] encoded string or byte array
# @return [DecodedByteArray] decoded byte array
#
def decode(encoded)
return DecodedByteArray::EMPTY if encoded.empty?
unless encoded.is_a?(Array)
encoded = encoded.force_encoding(@table.encoding).bytes
end
unless decodable?(encoded)
raise ArgumentError, "'#{encoded}' contains unknown characters'"
end
# Find leading zeroes
zeroes_count = [
0,
encoded.find_index { |b| b.ord != @table.zero } || encoded.length
].max
encoded = encoded.drop(zeroes_count)
# Decode number from encoding base to base 10
encoded_big_number = 0
encoded.reverse.each_with_index do |char, i|
table_i = @table.index(char)
encoded_big_number += @table.base**i * table_i
end
# Build the output by reversing the bytes. Because the encoding is "lost"
# the result might not be correct just yet. This is up to the caller to
# fix. The algorithm **can not know** what the encoding was.
output = 1.upto((Math.log2(encoded_big_number) / 8).ceil).collect do
encoded_big_number, character_byte = encoded_big_number.divmod 256
character_byte
end.reverse
# Prepend the leading zeroes
@table.decoded_zeroes_length(zeroes_count).times do
output.unshift(0x00)
end
DecodedByteArray.new(output)
end
def decodable?(encoded)
(encoded.uniq - @table.tr_ords).length.zero?
end
end
end