# = bencode.rb - Bencode Library # # Bencode is a Ruby implementation of the Bencode data serialization # format used in the BitTorrent protocol. # # == Synopsis # # Bencoding (pronounced bee-encode) is a simple protocol, consisting of # only 4 value types. # # === Integers # # An integer is encoded as an _i_ followed by the numeral itself, followed # by an _e_. Leading zeros are not allowed. Negative values are prefixed # with a minus sign. # # 42.bencode #=> "i42e" # -2.bencode #=> "i-2e" # 0.bencode #=> "i0e" # # === Strings # # Strings are sequences of zero or more bytes. They are encoded as # <length>:<contents>, where _length_ is the length of _contents_. _length_ # must be non-negative. # # "".bencode #=> "0:" # "foo".bencode #=> "3:foo" # # === Lists # # Lists are encoded as _l_ followed by the elements, followed by _e_. # There is no element seperator. # # [1, 2, 3].bencode #=> "li1ei2ei3ee" # # === Dictionaries # # Dictionaries are encoded as _d_ followed by a sequence of key-value pairs, followed by _e_. # Each value must be immediately preceded by a key. Keys must be strings, and must appear in # lexicographical order. # # {"foo" => 3, "bar" => 1, "baz" => 2}.bencode # #=> "d3:bari1e3:bazi2e3:fooi3ee" # # # == Authors # # * Daniel Schierbeck # # == Contributors # # * Daniel Martin # * Phrogz # * Julien Pervillé # # == 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, "object cannot be bencoded" 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" rescue BencodeError raise BencodeError, "list items must be bencodable" 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 = sort.map{|key, val| [key.to_str.bencode, val.bencode] } "d#{pairs.join('')}e" rescue NoMethodError => error if error.name == :to_str raise BencodeError, "dictionary keys must be strings" else raise end end end class IO def self.bdecode(filename) open(filename, 'r').bdecode end def self.bencode(filename) open(filename, 'r').bencode end def bdecode read.chomp.bdecode end def bencode read.chomp.bencode end end class BencodeError < StandardError; end class BdecodeError < StandardError; end module Bencode class << self # Bencodes +obj+ def dump(obj) obj.bencode end # Bdecodes +str+ def load(str) require 'strscan' scanner = StringScanner.new(str) obj = parse(scanner) raise BdecodeError unless scanner.eos? return obj end # Bdecodes the file located at +path+ 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 when "i" number = scanner.scan(/0|(?:-?[1-9][0-9]*)/) raise BdecodeError unless number and scanner.scan(/e/) 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) raise BdecodeError, "key must be a string" unless key.is_a? String 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 end end private :parse end end