# = 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 # :, 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, 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 = sort.map{|key, val| [key.to_str.bencode, val.bencode]} "d#{pairs.join('')}e" 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 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; 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