require 'stringio' module BEncode class Parser attr_reader :stream, :encoding def initialize(stream) @stream = if stream.kind_of?(IO) || stream.kind_of?(StringIO) stream elsif stream.respond_to? :string StringIO.new stream.string elsif stream.respond_to? :to_s StringIO.new stream.to_s end @encoding = @stream.external_encoding || Encoding::default_external end def parse! case peek when ?i then parse_integer! when ?l then parse_list! when ?d then parse_dict! when ?0 .. ?9 then parse_string! end end def eos? stream.eof? end private def parse_integer! stream.getc num = stream.gets("e") or raise BEncode::DecodeError num.chop.to_i end def parse_list! stream.getc ary = [] ary.push(parse!) until peek == ?e stream.getc ary end def parse_dict! stream.getc hsh = {} until peek == ?e key = parse! unless key.is_a? String or key.is_a? Integer raise BEncode::DecodeError, "key must be a string or number" end val = parse! hsh.store(key.to_s, val) end stream.getc hsh end def parse_string! num = stream.gets(":") or raise BEncode::DecodeError, "invalid string length (no colon)" begin length = num.chop.to_i return "" if length == 0 # Workaround for Rubinius bug str = stream.read(length).force_encoding(encoding) rescue raise BEncode::DecodeError, "invalid string length" end str end def peek c = stream.getc stream.ungetc(c) c end end end