## bencoding.rb -- parse and generate bencoded values.
## Copyright 2004 William Morgan.
##
## This file is part of RubyTorrent. RubyTorrent is free software;
## you can redistribute it and/or modify it under the terms of version
## 2 of the GNU General Public License as published by the Free
## Software Foundation.
##
## RubyTorrent 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 (in the file COPYING) for more details.

require 'uri'
require 'digest/sha1'
 
module RubyTorrent

## we mess in the users' namespaces in this file. there's no good way
## around it. i don't think it's too egregious though.

class BEncodingError < StandardError; end

class BStream
  include Enumerable

  @@classes = []

  def initialize(s)
    @s = s
  end

  def self.register_bencoded_class(c)
    @@classes.push c
  end

  def each
    happy = true
    begin
      happy = false
      c = @s.getc
      @@classes.each do |klass|
        if klass.bencoded? c
          o = klass.parse_bencoding(c, @s)
          happy = true
          yield o
          break
        end
      end unless c.nil?
      unless happy
        @s.ungetc c unless c.nil?
      end
    end while happy
    self
  end
end

end

class String
  def to_bencoding
    self.length.to_s + ":" + self.to_s
  end

  def self.bencoded?(c)
    (?0 .. ?9).include? c
  end

  def self.parse_bencoding(c, s)
    lens = c.chr
    while ((x = s.getc) != ?:)
      unless (?0 .. ?9).include? x
        s.ungetc x
        raise RubyTorrent::BEncodingError, "invalid bencoded string length #{lens} + #{x}" 
      end
      lens += x.chr
    end
    raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless lens.length <= 20
    len = lens.to_i
    raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless len >= 0
    (len > 0 ? s.read(len) : "")
  end

  RubyTorrent::BStream.register_bencoded_class self
end

class Integer
  def to_bencoding
    "i" + self.to_s + "e"
  end

  def self.bencoded?(c)
    c == ?i
  end

  def self.parse_bencoding(c, s)
    ints = ""
    while ((x = s.getc.chr) != 'e')
      raise RubyTorrent::BEncodingError, "invalid bencoded integer #{x.inspect}" unless x =~ /\d|-/
      ints += x
    end
    raise RubyTorrent::BEncodingError, "invalid integer #{ints} (too long)" unless ints.length <= 20
    int = ints.to_i
    raise RubyTorrent::BEncodingError, %{can't parse bencoded integer "#{ints}"} if (int == 0) && (ints !~ /^0$/) #'
    int
  end

  RubyTorrent::BStream.register_bencoded_class self
end

class Time
  def to_bencoding
    self.to_i.to_bencoding
  end
end

module URI
  def to_bencoding
    self.to_s.to_bencoding
  end
end

class Array
  def to_bencoding
    "l" + self.map { |e| e.to_bencoding }.join + "e"
  end

  def self.bencoded?(c)
    c == ?l
  end

  def self.parse_bencoding(c, s)
    ret = RubyTorrent::BStream.new(s).map { |x| x }
    raise RubyTorrent::BEncodingError, "missing list terminator" unless s.getc == ?e
    ret
  end

  RubyTorrent::BStream.register_bencoded_class self
end

class Hash
  def to_bencoding
    "d" + keys.sort.map do |k|
      v = self[k]
      if v.nil?
        nil
      else
        [k.to_bencoding, v.to_bencoding].join
      end
    end.compact.join + "e"
  end

  def self.bencoded?(c)
    c == ?d
  end

  def self.parse_bencoding(c, s)
    ret = {}
    key = nil
    RubyTorrent::BStream.new(s).each do |x|
      if key == nil
        key = x
      else
        ret[key] = x
        key = nil
      end
    end

    raise RubyTorrent::BEncodingError, "no dictionary terminator" unless s.getc == ?e
    ret
  end

  RubyTorrent::BStream.register_bencoded_class self
end
