BitTorrentのメタ情報ファイルや、
トラッカーの応答メッセージで使われている
BEncode(bee-encodeと発音する)をRubyで実装してみた。

Rythonは、オリジナルのスクリプトに含まれているし、
Perl, OCamlによる実装はすでにあるらしい。&&
PHPには、若干仕様が異なるが、似たようなアルゴリズム
serialize, unserializeという組み込み関数がある。
...ということで、ひさしぶりにRuby.


ルールはシンプル。次の通り。

  • 文字列 length `:' data
  • 整数 'i' data 'e'
  • リスト 'l' ... 'e'
  • 辞書 'd' ... 'e'

例を幾つか上げると、
"4:SPAM"からは、':'以降の4bytes分のデータ"SPAM".
"i123e"は、'i'から次に現れる'e'までの数 123 がデータ部,
次にリスト構造。"li10ei20ei30ee"は、最初の'l'と最後の'e'
が対応していて、間には数値の要素が三つ[10,20,30]となる。
辞書もリストの応用で、違いは、辞書では連続する2つの要素
はキーと値のペアになって辞書(Rubyではハッシュ構造)に格納される。また、リストと辞書はそれぞれ入れ子にする事も出来る。

 require 'stringio'

 class UnknownCommandError < StandardError
 end

 class InvalidNumberFormatError < StandardError
 end

 def bdecode(stream, nesting=0)
   if stream.member? "getc"    # FIXME stream type ¤ÎȽÃÇ
     return _bdecode(stream, nesting)
   else
     return _bdecode(StringIO.new(stream), nesting)
   end
 end

 def _read_bytes(stream, terminator)
   tmp = Array.new
   loop do
     c = stream.getc
     if c && c.chr != terminator && c >= 0
       tmp.push(c.chr)
     else
       break
     end
   end
   return tmp.join('')
 end

 def _read_number(stream, terminator)
   num = _read_bytes(stream, terminator)
   if num.match(/^(0|-?[1-9][0-9]*)$/)
     return num.to_i
   else
     raise InvalidNumberFormatError 
   end
 end


 def _bdecode(stream, nesting=0)
   c = stream.getc
   case c.chr
   when 'd' # Dict
     tmp = Hash.new
     while key = _bdecode(stream, nesting+1) do
       val = _bdecode(stream, nesting+1)
       tmp[key] = val
     end
     return tmp     
   when 'l' # List
     tmp = Array.new
     while val = _bdecode(stream, nesting+1) do
       tmp.push(val)
     end 
     return tmp
   when 'i' # Integer
     return _read_number(stream, 'e')
   when '0' .. '9' # String
     stream.ungetc(c)
     len = _read_number(stream, ':')
     str = stream.read(len)
     return str
   when 'e', -1
     return nil
   else
     raise UnknownCommandError
   end
 end

 p bdecode("d4:name7:teamikl3:agei24ee")