class Epithet::Block58
Fixed-length base58 codec for a fixed-size block.
Obtain codecs via Block58::build, which selects the fastest variant for the block size, an unrolled decoder for 16-byte blocks, or the generic chunked decoder otherwise.
Constants
- Alphabet
-
= '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
Attributes
Public Class Methods
Source
# File lib/epithet/block58.rb, line 18 def self.build(block_size, ...) = (block_size == 16 ? Unrolled16 : self).new(block_size, ...)
Same as ::new but may select a tuned subclass for performance.
# File lib/epithet/block58.rb, line 24 def initialize(block_size, alphabet: Alphabet) raise ArgumentError, 'invalid block size' unless Integer === block_size && block_size > 0 @alphabet = alphabet.b.freeze raise ArgumentError, 'invalid alphabet length' unless @alphabet.bytesize == 58 raise ArgumentError, 'alphabet not strictly ascending' unless @alphabet.bytes.each_cons(2).all? { _2 > _1 } @size = ((block_size * 8) / Math.log2(58)).ceil @charsel = @alphabet.gsub(/[\^\-\\]/, '\\\\\&').freeze @blank = (@alphabet[0] * @size).freeze @lut = @alphabet.each_byte.with_index.with_object("\0" * 256) { |(val, idx), lut| lut.setbyte(val, idx) }.freeze @limit = 1 << (block_size * 8) @max = i2s(@limit - 1).freeze end
Create a codec for a block size in bytes.
The alphabet must be 58 distinct bytes in ascending order, so that lexicographic order agrees with numeric order.
Public Instance Methods
Source
# File lib/epithet/block58.rb, line 48 def i2s(int) raise ArgumentError, 'integer out of block range' unless Integer === int && int >= 0 && int < @limit # Using divmod+setbyte is faster than Integer#digits under YJIT, # and about equal in plain MRI. alphabet = @alphabet out = @blank.dup idx = @size - 1 n = int while idx >= 0 && n > 0 n, rem = n.divmod(58) out.setbyte(idx, alphabet.getbyte(rem)) idx -= 1 end out end
Encode an acceptable integer to fixed-length base58.
Source
# File lib/epithet/block58.rb, line 37 def inspect "#<#{self.class}:#{'%#016x' % (object_id << 1)} size=#{@size} alphabet=#{@alphabet}>" end
Source
# File lib/epithet/block58.rb, line 67 def s2i(str) # Chunking intermediate results into 64-bit integers is ~5x faster # under YJIT than Horner's scheme # # str.each_byte.inject(0) { _1 * 58 + @lut[_2] } # # at computing the inner product. lut = @lut size = @size pow = POW58 acc = 0 pos = 0 while pos < size n = size - pos n = 10 if n > 10 chunk = 0 i = 0 while i < n chunk = (chunk * 58) + lut.getbyte(str.getbyte(pos)) pos += 1 i += 1 end acc = (acc * pow[n]) + chunk end acc end
Decode a fixed-length base58 string to an integer. Assumes the input passes #valid?, behaviour undefined if it doesnβt.
Source
# File lib/epithet/block58.rb, line 43 def valid?(s) String === s && s.bytesize == @size && (s = s.b) <= @max && s.count(@charsel) == @size end
Return true if the string is in range with the right size and alphabet. The input is read as bytes, whatever its encoding.