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