# Thanks to Daniel Martin and all the other lads at ruby-lang for # helping me out. # bencode is a Ruby implementation of the Bencode data serialization # format used in the BitTorrent protocol. # # == Synopsis # # "foobar".bencode #=> "6:foobar" # 42.bencode #=> "i42e" # [1, 2, 3].bencode #=> "li1ei2ei3ee" # # == Authors # # * Daniel Schierbeck # # == Contributors # # * Daniel Martin # * Phrogz # # == Copyright # # Bencode is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # Bencode is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU General Public License for more details. class Object # Raises an exception. Subclasses of Object must themselves # define meaningful #bencode methods. def bencode raise BencodeError, self.class end end class Integer # Bencodes the Integer object. Bencoded integers are represented # as +ixe+, where +x+ is the integer with an optional # hyphen prepended, indicating negativity. # # 42.bencode #=> "i42e" # -7.bencode #=> "i-7e" def bencode "i#{self}e" end end class String # Bencodes the String object. Bencoded strings are represented # as +x:y+, where +y+ is the string and x is the length of the # string. # # "foo".bencode #=> "3:foo" # "".bencode #=> "0:" # def bencode "#{length}:#{self}" end # Bdecodes the String object and returns the data serialized # through bencoding. # # "li1ei2ei3ee".bdecode #=> [1, 2, 3] # def bdecode Bencode.load(self) end # Tests whether the String object is a valid bencoded string. def bencoded? bdecode rescue BdecodeError false else true end end class Array # Bencodes the Array object. Bencoded arrays are represented as # +lxe+, where x is zero or more bencoded objects. # # [1, "foo"].bencode #=> "li1e3:fooe" # def bencode "l#{map{|obj| obj.bencode}.join('') }e" end end class Hash # Bencodes the Hash object. Bencoded hashes are represented as # +dxe+, where x is zero or a power of two bencoded objects. # each key is immediately followed by its associated value. # All keys must be strings. The keys of the bencoded hash will # be in lexicographical order. def bencode pairs = map{|key, val| [key.to_str.bencode, val.bencode] } pairs.sort!{|a, b| a.first <=> b.first } "d#{pairs.join('')}e" end end class IO def self.bdecode(*args) new(*args).bdecode end def self.bencode(*args) new(*args).bencode end def bdecode read.chomp.bdecode end def bencode read.chomp.bdecode end end class BencodeError < StandardError def initialize(object_class = nil) # :nodoc: @object_class = object_class end def to_s # :nodoc: "could not bencode #{@object_class}" end end class BdecodeError < StandardError attr_reader :pos def initialize(pos = nil) # :nodoc: @pos = pos end def to_s # :nodoc: if pos.nil? "syntax error" else "syntax error near position #{pos}" end end end module Bencode class << self def dump(obj) obj.bencode end def load(str) require 'strscan' scanner = StringScanner.new(str) obj = parse(scanner) raise BdecodeError unless scanner.eos? return obj end def load_file(path) load(File.open(path).read) end def parse(scanner) # :nodoc: case token = scanner.scan(/[ild]|\d+:|\s/) when nil raise BdecodeError, scanner.pos when "i" number = scanner.scan(/0|(?:-?[1-9][0-9]*)/) unless number and scanner.scan(/e/) raise BdecodeError, scanner.pos end return number.to_i when "l" ary = [] until scanner.peek(1) == "e" ary.push(parse(scanner)) end scanner.pos += 1 return ary when "d" hsh = {} until scanner.peek(1) == "e" key, value = parse(scanner), parse(scanner) unless key.is_a? String raise BdecodeError, "error at #{scanner.pos}: " + "key must be a string" end hsh.store(key, value) end scanner.pos += 1 return hsh when /\d+:/ length = token.chop.to_i str = scanner.peek(length) scanner.pos += length return str when /\s/ nil else raise BdecodeError, scanner.pos end end private :parse end end