#
#       $Id: RR.rb,v 1.19 2006/07/28 07:33:36 bluemonk Exp $    
#

require 'net/dns/names/names'
require 'net/dns/rr/types'
require 'net/dns/rr/classes'


%w[a ns mx cname txt soa ptr aaaa mr srv].each do |file|
  require "net/dns/rr/#{file}"
end

module Net # :nodoc:
  module DNS 
    
    # =Name
    #
    # Net::DNS::RR - DNS Resource Record class
    #
    # =Synopsis
    # 
    #   require 'net/dns/rr'
    #
    # =Description
    # 
    # The Net::DNS::RR is the base class for DNS Resource 
    # Record (RR) objects. A RR is a pack of data that represents
    # resources for a DNS zone. The form in which this data is 
    # shows can be drawed as follow:
    #
    #   "name  ttl  class  type  data"
    # 
    # The +name+ is the name of the resource, like an canonical
    # name for an +A+ record (internet ip address). The +ttl+ is the
    # time to live, expressed in seconds. +type+ and +class+ are 
    # respectively the type of resource (+A+ for ip addresses, +NS+
    # for nameservers, and so on) and the class, which is almost 
    # always +IN+, the Internet class. At the end, +data+ is the
    # value associated to the name for that particular type of 
    # resource record. An example:
    #
    #   # A record for IP address
    #   "www.example.com  86400  IN  A  172.16.100.1"
    #   
    #   # NS record for name server
    #   "www.example.com  86400  IN  NS  ns.example.com"
    #
    # A new RR object can be created in 2 ways: passing a string
    # such the ones above, or specifying each field as the pair
    # of an hash. See the Net::DNS::RR.new method for details.
    #
    # =Error classes
    #
    # Some error classes has been defined for the Net::DNS::RR class,
    # which are listed here to keep a light and browsable main documentation.
    # We have:
    #
    # * RRArgumentError: Generic argument error for class Net::DNS::RR
    # * RRDataError: Error in parsing binary data, maybe from a malformed packet
    #
    # =Copyright
    # 
    # Copyright (c) 2006 Marco Ceresa
    #
    # All rights reserved. This program is free software; you may redistribute 
    # it and/or modify it under the same terms as Ruby itself.
    #
    class RR
      include Net::DNS::Names
      
      # Regexp matching an RR string
      RR_REGEXP = Regexp.new("^\\s*(\\S+)\\s*(\\d+)?\\s+(" +
                               Net::DNS::RR::Classes.regexp + 
                               "|CLASS\\d+)?\\s*(" +
                               Net::DNS::RR::Types.regexp + 
                               "|TYPE\\d+)?\\s*(.*)$", Regexp::IGNORECASE, 'n')

      # Dimension of the sum of class, type, TTL and rdlength fields in a
      # RR portion of the packet, in bytes
      RRFIXEDSZ = 10

      # Name of the RR
      attr_reader :name
      # TTL time (in seconds) of the RR
      attr_reader :ttl
      # Data belonging to that appropriate class, 
      # not to be used (use real accessors instead)
      attr_reader :rdata
      
      # Create a new instance of Net::DNS::RR class, or an instance of
      # any of the subclass of the appropriate type.
      # 
      # Argument can be a string or an hash. With a sting, we can pass
      # a RR resource record in the canonical format:
      #
      #   a     = Net::DNS::RR.new("foo.example.com. 86400 A 10.1.2.3")
      #   mx    = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
      #   cname = Net::DNS::RR.new("www.example.com 300 IN CNAME www1.example.com")
      #   txt   = Net::DNS::RR.new('baz.example.com 3600 HS TXT "text record"')
      #
      # Incidentally, +a+, +mx+, +cname+ and +txt+ objects will be instances of
      # respectively Net::DNS::RR::A, Net::DNS::RR::MX, Net::DNS::RR::CNAME and 
      # Net::DNS::RR::TXT classes.
      #
      # The name and RR data are required; all other informations are optional.  
      # If omitted, the +TTL+ defaults to 10800, +type+ default to +A+ and the RR class
      # defaults to +IN+.  Omitting the optional fields is useful for creating the 
      # empty RDATA sections required for certain dynamic update operations.
      # All names must be fully qualified.  The trailing dot (.) is optional.
      #
      # The preferred method is however passing an hash with keys and values:
      #
      #   rr = Net::DNS::RR.new(
      #                 :name    => "foo.example.com",
      #                 :ttl     => 86400,
      #                 :cls     => "IN",
      #                 :type    => "A",
      #                 :address => "10.1.2.3"
      #         )
      #
      #   rr = Net::DNS::RR.new(
      #                 :name => "foo.example.com",
      #                 :rdata => "10.1.2.3"
      #         )
      #
      # Name and data are required; all the others fields are optionals like 
      # we've seen before. The data field can be specified either with the 
      # right name of the resource (+:address+ in the example above) or with
      # the generic key +:rdata+. Consult documentation to find the exact name
      # for the resource in each subclass.
      # 
      def initialize(arg)
        case arg
        when String
          instance = new_from_string(arg)
        when Hash
          instance = new_from_hash(arg)
        else
          raise RRArgumentError, "Invalid argument, must be a RR string or an hash of values"
        end

        if @type.to_s == "ANY"
          @cls = Net::DNS::RR::Classes.new("IN")
        end

        build_pack
        set_type

        instance
      end

      # Return a new RR object of the correct type (like Net::DNS::RR::A
      # if the type is A) from a binary string, usually obtained from
      # network stream.
      #
      # This method is used when parsing a binary packet by the Packet
      # class.
      # 
      def RR.parse(data)
        o = allocate
        obj,offset = o.send(:new_from_binary, data, 0)
        return obj
      end
      
      # Same as RR.parse, but takes an entire packet binary data to 
      # perform name expansion. Default when analizing a packet
      # just received from a network stream.
      #
      # Return an instance of appropriate class and the offset 
      # pointing at the end of the data parsed.
      #
      def RR.parse_packet(data,offset)
        o = allocate
        o.send(:new_from_binary,data,offset)
      end

      # Return the RR object in binary data format, suitable 
      # for using in network streams, with names compressed. 
      # Must pass as arguments the offset inside the packet
      # and an hash of compressed names.
      #
      # This method is to be used in other classes and is 
      # not intended for user space programs.
      #
      # TO FIX in one of the future releases
      #
      def comp_data(offset,compnames)
        type,cls = @type.to_i, @cls.to_i
        str,offset,names = dn_comp(@name,offset,compnames)
        str += [type,cls,@ttl,@rdlength].pack("n2 N n")
        offset += Net::DNS::RRFIXEDSZ
        return str,offset,names
      end
      
      # Return the RR object in binary data format, suitable 
      # for using in network streams.
      #
      #   raw_data = rr.data
      #   puts "RR is #{raw_data.size} bytes long"
      #
      def data
        type,cls = @type.to_i, @cls.to_i
        str = pack_name(@name)
        return str + [type,cls,@ttl,@rdlength].pack("n2 N n") + get_data
      end
      
      # Canonical inspect method    
      #
      #   mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
      #     #=> example.com.            7200    IN      MX      10 mailhost.example.com.
      #
      def inspect
        data = get_inspect 
        # Returns the preformatted string
        if @name.size < 24
          [@name, @ttl.to_s, @cls.to_s, @type.to_s, 
            data].pack("A24 A8 A8 A8 A*")
        else
          to_a.join("   ")
        end
      end

      # Returns the RR in a string format.
      #
      #   mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
      #   mx.to_s
      #     #=> "example.com.            7200    IN      MX      10 mailhost.example.com."
      #
      def to_s
        "#{self.inspect}"
      end

      # Returns an array with all the fields for the RR record.
      #
      #   mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
      #   mx.to_a
      #     #=> ["example.com.",7200,"IN","MX","10 mailhost.example.com."]
      #
      def to_a
        [@name,@ttl,@cls.to_s,@type.to_s,get_inspect]
      end
      
      # Type accessor
      def type
        @type.to_s
      end
      
      # Class accessor
      def cls
        @cls.to_s
      end
      
      private
      
      #---
      # New RR with argument in string form
      #---
      def new_from_string(rrstring)

        unless rrstring =~ RR_REGEXP
          raise RRArgumentError, 
          "Format error for RR string (maybe CLASS and TYPE not valid?)"
        end

        # Name of RR - mandatory
        begin
          @name = $1.downcase
        rescue NoMethodError
          raise RRArgumentError, "Missing name field in RR string #{rrstring}"
        end
        
        # Time to live for RR, default 3 hours
        @ttl = $2 ? $2.to_i : 10800
        
        # RR class, default to IN
        @cls = Net::DNS::RR::Classes.new $3
        
        # RR type, default to A
        @type = Net::DNS::RR::Types.new $4
        
        # All the rest is data 
        @rdata = $5 ? $5.strip : ""  
        
        if self.class == Net::DNS::RR
          (eval "Net::DNS::RR::#@type").new(rrstring)
        else
          subclass_new_from_string(@rdata)
          self.class
        end
      end
      
      def new_from_hash(args)
        
        # Name field is mandatory   
        unless args.has_key? :name 
          raise RRArgumentError, "RR argument error: need at least RR name"
        end

        @name  = args[:name].downcase
        @ttl   = args[:ttl] ? args[:ttl].to_i : 10800 # Default 3 hours
        @type  = Net::DNS::RR::Types.new args[:type]
        @cls  = Net::DNS::RR::Classes.new args[:cls]
        
        @rdata = args[:rdata] ? args[:rdata].strip : ""
        @rdlength = args[:rdlength] || @rdata.size

        if self.class == Net::DNS::RR
          (eval "Net::DNS::RR::#@type").new(args)
        else
          hash = args - [:name,:ttl,:type,:cls]
          if hash.has_key? :rdata
            subclass_new_from_string(hash[:rdata])
          else
            subclass_new_from_hash(hash)
          end
          self.class
        end
      end # new_from_hash

      def new_from_binary(data,offset)
        if self.class == Net::DNS::RR
          temp = dn_expand(data,offset)[1]
          type = Net::DNS::RR::Types.new data.unpack("@#{temp} n")[0]
          (eval "Net::DNS::RR::#{type}").parse_packet(data,offset)
        else
          @name,offset = dn_expand(data,offset)
          rrtype,cls,@ttl,@rdlength = data.unpack("@#{offset} n2 N n")
          @type = Net::DNS::RR::Types.new rrtype
          @cls = Net::DNS::RR::Classes.new cls
          offset += RRFIXEDSZ
          offset = subclass_new_from_binary(data,offset)
          build_pack
          set_type
          return [self,offset]
        end
#      rescue StandardError => err
#        raise RRDataError, "Caught exception, maybe packet malformed: #{err}"
      end
      
      # Methods to be overridden by subclasses
      def subclass_new_from_array(arr)
      end
      def subclass_new_from_string(str)
      end
      def subclass_new_from_hash(hash)
      end
      def subclass_new_from_binary(data,offset)
      end
      def build_pack
      end
      def set_type
      end
      def get_inspect
        @rdata
      end
      def get_data
        @rdata
      end

      # NEW new method :)
      def self.new(*args)
        o = allocate
        obj = o.send(:initialize,*args)
        if self == Net::DNS::RR
          return obj
        else
          return o
        end
      end
            
    end # class RR
    
  end # module DNS
end # module Net

class RRArgumentError < ArgumentError # :nodoc:
end
class RRDataError < StandardError # :nodoc:
end

module ExtendHash # :nodoc:
  
  # Performs a sort of group difference 
  # operation on hashes or arrays
  # 
  #   a = {:a=>1,:b=>2,:c=>3}
  #   b = {:a=>1,:b=>2}
  #   c = [:a,:c]
  #   a-b #=> {:c=>3}
  #   a-c #=> {:b=>2}
  #
  def -(oth)
    case oth
    when Hash
      delete_if {|k,v| oth.has_key? k}
    when Array
      delete_if {|k,v| oth.include? k}
    end
  end
end

class Hash # :nodoc:
  include ExtendHash
end
