module BERT class Decode attr_accessor :in include Types def self.impl 'Ruby' end def self.decode(string) io = StringIO.new(string) io.set_encoding('binary') if io.respond_to?(:set_encoding) new(io).read_any end def initialize(ins) @in = ins @peeked = "" end def read_any fail("Bad Magic") unless read_1 == MAGIC read_any_raw end def read_any_raw case peek_1 when ATOM then read_atom when SMALL_INT then read_small_int when INT then read_int when SMALL_BIGNUM then read_small_bignum when LARGE_BIGNUM then read_large_bignum when FLOAT then read_float when SMALL_TUPLE then read_small_tuple when LARGE_TUPLE then read_large_tuple when NIL then read_nil when STRING then read_erl_string when LIST then read_list when BIN then read_bin else fail("Unknown term tag: #{peek_1}") end end def read(length) if length < @peeked.length result = @peeked[0...length] @peeked = @peeked[length..-1] length = 0 else result = @peeked @peeked = '' length -= result.length end if length > 0 result << @in.read(length) end result end def peek(length) if length <= @peeked.length @peeked[0...length] else read_bytes = @in.read(length - @peeked.length) @peeked << read_bytes if read_bytes @peeked end end def peek_1 peek(1).unpack("C").first end def peek_2 peek(2).unpack("n").first end def read_1 read(1).unpack("C").first end def read_2 read(2).unpack("n").first end def read_4 read(4).unpack("N").first end def read_string(length) read(length) end def read_atom fail("Invalid Type, not an atom") unless read_1 == ATOM length = read_2 a = read_string(length) case a when "" Marshal.load("\004\b:\005") # Workaround for inability to do ''.to_sym else a.to_sym end end def read_small_int fail("Invalid Type, not a small int") unless read_1 == SMALL_INT read_1 end def read_int fail("Invalid Type, not an int") unless read_1 == INT value = read_4 negative = (value >> 31)[0] == 1 value = (value - (1 << 32)) if negative value end def read_small_bignum fail("Invalid Type, not a small bignum") unless read_1 == SMALL_BIGNUM size = read_1 sign = read_1 bytes = read_string(size).unpack("C" * size) added = bytes.zip((0..bytes.length).to_a).inject(0) do |result, byte_index| byte, index = *byte_index value = (byte * (256 ** index)) sign != 0 ? (result - value) : (result + value) end added end def read_large_bignum fail("Invalid Type, not a large bignum") unless read_1 == LARGE_BIGNUM size = read_4 sign = read_1 bytes = read_string(size).unpack("C" * size) added = bytes.zip((0..bytes.length).to_a).inject(0) do |result, byte_index| byte, index = *byte_index value = (byte * (256 ** index)) sign != 0 ? (result - value) : (result + value) end added end def read_float fail("Invalid Type, not a float") unless read_1 == FLOAT string_value = read_string(31) result = string_value.to_f end def read_small_tuple fail("Invalid Type, not a small tuple") unless read_1 == SMALL_TUPLE read_tuple(read_1) end def read_large_tuple fail("Invalid Type, not a small tuple") unless read_1 == LARGE_TUPLE read_tuple(read_4) end def read_tuple(arity) if arity > 0 tag = read_any_raw if tag == :bert read_complex_type(arity) else tuple = Tuple.new(arity) tuple[0] = tag (arity - 1).times { |i| tuple[i + 1] = read_any_raw } tuple end else Tuple.new end end def read_complex_type(arity) case read_any_raw when :nil nil when :true true when :false false when :time Time.at(read_any_raw * 1_000_000 + read_any_raw, read_any_raw) when :regex source = read_any_raw opts = read_any_raw options = 0 options |= Regexp::EXTENDED if opts.include?(:extended) options |= Regexp::IGNORECASE if opts.include?(:caseless) options |= Regexp::MULTILINE if opts.include?(:multiline) Regexp.new(source, options) when :dict read_dict else nil end end def read_dict type = read_1 fail("Invalid dict spec, not an erlang list") unless [LIST, NIL].include?(type) if type == LIST length = read_4 else length = 0 end hash = {} length.times do |i| pair = read_any_raw hash[pair[0]] = pair[1] end read_1 if type == LIST hash end def read_nil fail("Invalid Type, not a nil list") unless read_1 == NIL [] end def read_erl_string fail("Invalid Type, not an erlang string") unless read_1 == STRING length = read_2 read_string(length).unpack('C' * length) end def read_list fail("Invalid Type, not an erlang list") unless read_1 == LIST length = read_4 list = (0...length).map { |i| read_any_raw } read_1 list end def read_bin fail("Invalid Type, not an erlang binary") unless read_1 == BIN length = read_4 read_string(length) end def fail(str) raise str end end end