# = 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