Browse files

Initial import

  • Loading branch information...
0 parents commit 0dcead5804b022027d9b3fe481616a95f86a173d @candlerb candlerb committed Jan 26, 2010
Showing with 1,059 additions and 0 deletions.
  1. +11 −0 .gitignore
  2. +99 −0 README
  3. +37 −0 Rakefile
  4. +1 −0 lib/ip.rb
  5. +341 −0 lib/ip/base.rb
  6. +19 −0 lib/ip/cpal.rb
  7. +31 −0 lib/ip/socket.rb
  8. +33 −0 test/ip_cpal_test.rb
  9. +483 −0 test/ip_test.rb
  10. +4 −0 test/test_helper.rb
11 .gitignore
@@ -0,0 +1,11 @@
+*~
+*.bak
+*.orig
+*.rej
+*.log
+doc/rdoc
+coverage
+.svn
+.irbrc
+rerun.txt
+tmp
99 README
@@ -0,0 +1,99 @@
+== ruby-ip library
+
+This is a library for manipulating IP addresses. Feature overview:
+
+* Create from string and to string
+
+ require 'ip'
+ ip = IP.new("192.0.2.53/24")
+ ip.to_s # "192.0.2.53/24"
+ ip.to_i # 3221226037
+ ip.to_hex # "c0000235"
+ ip.to_addr # "192.0.2.53"
+ ip.pfxlen # 24
+
+* Qualify IP address with "routing context" (VRF)
+
+ ip = IP.new("192.0.2.53/24@cust1")
+ ip.to_s # "192.0.2.53/24@cust1"
+ ip.to_addrlen # "192.0.2.53/24"
+ ip.ctx # "cust1"
+
+* Clean implementation of IP::V4 and IP::V6 as subclasses of IP
+
+ ip1 = IP.new("192.0.2.53/24") #<IP::V4 192.0.2.53/24>
+ ip2 = IP.new("2001:db8:be00::/48") #<IP::V6 2001:db8:be00::/48>
+ ip1.is_a?(IP::V4) # true
+
+* Create directly from integers or hex
+
+ ip = IP::V4.new(3221226037, 24, "cust1")
+ ip = IP::V4.new("c0000235", 24, "cust1")
+
+* Netmask manipulation
+
+ ip = IP.new("192.0.2.53/24")
+ ip.network #<IP::V4 192.0.2.0/24>
+ ip.network(1) #<IP::V4 192.0.2.1/24>
+ ip.broadcast #<IP::V4 192.0.2.255/24>
+ ip.broadcast(-1) #<IP::V4 192.0.2.254/24>
+ ip.mask # 255
+ ip.size # 256
+ ip.netmask.to_s # "255.255.255.0"
+ ip.wildmask.to_s # "0.0.0.255"
+
+* Address masking
+
+ ip = IP.new("192.0.2.53/24")
+ ip.offset? # true
+ ip.offset # 53
+ ip.mask!
+ ip.to_s # "192.0.2.0/24"
+ ip.offset? # false
+
+* Simple IP arithmetic
+
+ ip = IP.new("192.0.2.53/24")
+ ip + 4 #<IP::V4 192.0.2.57/24>
+ ip | 7 #<IP::V4 192.0.2.55/24>
+ ip ^ 7 #<IP::V4 192.0.2.50/24>
+ ~ip #<IP::V4 63.255.253.202/24>
+
+* Convert to and from a compact Array representation
+
+ ip1 = IP.new("192.0.2.53/24@cust1")
+ ip1.to_a # ["v4", 3221226037, 24, "cust1"]
+
+ ip2 = IP.new(["v4", 3221226037, 24, "cust1"])
+ ip1 == ip2 # true
+
+* Hex array representation, useful when talking to languages which don't
+ have Bignum support
+
+ ip1 = IP.new("2001:db8:be00::/48@cust1")
+ ip1.to_ah # ["v6", "20010db8be0000000000000000000000", 48, "cust1"]
+
+ ip2 = IP.new(["v6", "20010db8be0000000000000000000000", 48, "cust1"])
+ ip1 == ip2 # true
+
+* Addresses are Comparable, sortable, and can be used as Hash keys
+
+== Why not IPAddr?
+
+Ruby bundles an IPAddr class (ipaddr.rb). However there are a number of
+serious problems with this library.
+
+1. Given an IP address with a netmask or prefix (e.g. 192.0.2.0/24) it's
+ very hard to get access to the netmask part. It involves digging around
+ instance variables.
+
+2. It's impossible to deal with an off-base address with prefix, because
+ IPAddr forcibly masks it to the base. e.g. 192.0.2.53/24 is stored as
+ 192.0.2.0/24
+
+3. IPAddr uses calls to the socket library to validate IP addresses, and
+ this can trigger spurious DNS lookups when given an invalid IP address.
+ ruby-ip does not depend on the socket library at all, unless you
+ require 'ip/socket' to have access to the Socket::AF_INET and
+ Socket::AF_INET6 constants.
+
37 Rakefile
@@ -0,0 +1,37 @@
+require 'rake/clean'
+
+#### TESTING ####
+require 'rake/testtask'
+task :default => :test
+
+Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.test_files = FileList['test/*_test.rb']
+ t.verbose = true
+end
+
+#### COVERAGE ####
+begin
+ require 'rcov/rcovtask'
+
+ Rcov::RcovTask.new do |t|
+ t.libs << "test"
+ t.test_files = FileList['test/*_test.rb']
+ t.verbose = true
+ t.rcov_opts << '--exclude "gems/*"'
+ end
+rescue LoadError
+end
+
+#### DOCUMENTATION ####
+require 'rake/rdoctask'
+Rake::RDocTask.new { |rdoc|
+ rdoc.rdoc_dir = 'doc/rdoc'
+ rdoc.template = ENV['template'] if ENV['template']
+ rdoc.title = "Ruby-IP Documentation"
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.options << '--charset' << 'utf-8'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+}
+
1 lib/ip.rb
@@ -0,0 +1 @@
+require 'ip/base'
341 lib/ip/base.rb
@@ -0,0 +1,341 @@
+class IP
+ PROTO_TO_CLASS = {}
+
+ class << self
+ alias :orig_new :new
+ # Examples:
+ # IP.new("1.2.3.4")
+ # IP.new("1.2.3.4/28")
+ # IP.new("1.2.3.4/28@routing_context")
+ #
+ # Array form (inverse of to_a and to_ah):
+ # IP.new(["v4", 0x01020304])
+ # IP.new(["v4", 0x01020304, 28])
+ # IP.new(["v4", 0x01020304, 28, "routing_context"])
+ # IP.new(["v4", "01020304", 28, "routing_context"])
+ #
+ # Note that this returns an instance of IP::V4 or IP::V6. IP is the
+ # base class of both of those, but cannot be instantiated itself.
+ def new(src)
+ case src
+ when String
+ parse(src) || (raise ArgumentError, "invalid address")
+ when Array
+ (PROTO_TO_CLASS[src[0]] || (raise ArgumentError, "invalid protocol")).new(*src[1..-1])
+ when IP
+ src.dup
+ else
+ raise ArgumentError, "invalid address"
+ end
+ end
+
+ # Parse a string as an IP address - return a V4/V6 object or nil
+ def parse(str)
+ V4.parse(str) || V6.parse(str)
+ end
+ end
+
+ # Length of prefix (network portion) of address
+ attr_reader :pfxlen
+
+ # Routing Context indicates the scope of this address (e.g. virtual router)
+ attr_accessor :ctx
+
+ # Examples:
+ # IP::V4.new(0x01020304)
+ # IP::V4.new("01020304")
+ # IP::V4.new(0x01020304, 28)
+ # IP::V4.new(0x01020304, 28, "routing_context")
+ def initialize(addr, pfxlen=nil, ctx=nil)
+ @addr = addr.is_a?(String) ? addr.to_i(16) : addr.to_i
+ @pfxlen = pfxlen || self.class::ADDR_BITS
+ @ctx = ctx
+ end
+
+ # Return the protocol in string form, "v4" or "v6"
+ def proto
+ self.class::PROTO
+ end
+
+ # Return the string representation of the address, x.x.x.x[/pfxlen][@ctx]
+ def to_s
+ ctx ? "#{to_addrlen}@#{ctx}" : to_addrlen
+ end
+
+ # Return the string representation of the IP address and prefix, or
+ # just the IP address if it's a single address
+ def to_addrlen
+ pfxlen == self.class::ADDR_BITS ? to_addr : "#{to_addr}/#{pfxlen}"
+ end
+
+ # Return the address as an Integer
+ def to_i
+ @addr
+ end
+
+ # Return the address as a hexadecimal string (8 or 32 digits)
+ def to_hex
+ @addr.to_s(16).rjust(self.class::ADDR_BITS>>2,"0")
+ end
+
+ # Return an array representation of the address, with 3 or 4 elements
+ # depending on whether there is a routing context set.
+ # ["v4", 16909060, 28]
+ # ["v4", 16909060, 28, "context"]
+ # (Removing the last element makes them Comparable, as nil.<=> doesn't exist)
+ def to_a
+ @ctx ? [self.class::PROTO, @addr, @pfxlen, @ctx] :
+ [self.class::PROTO, @addr, @pfxlen]
+ end
+
+ # Return an array representation of the address, with 3 or 4 elements
+ # depending on whether there is a routing context set, using hexadecimal.
+ # ["v4", "01020304", 28]
+ # ["v4", "01020304", 28, "context"]
+ def to_ah
+ @ctx ? [self.class::PROTO, to_hex, @pfxlen, @ctx] :
+ [self.class::PROTO, to_hex, @pfxlen]
+ end
+
+ # Change the prefix length. If nil, the maximum is used (32 or 128)
+ def pfxlen=(pfxlen)
+ @pfxlen = pfxlen || self.class::ADDR_BITS
+ @mask = nil
+ end
+
+ # Return the mask for this pfxlen as an integer. For example,
+ # a V4 /24 address has a mask of 255 (0x000000ff)
+ def mask
+ @mask ||= (1 << (self.class::ADDR_BITS - @pfxlen)) - 1
+ end
+
+ # Return a new IP object at the base of the subnet, with an optional
+ # offset applied.
+ # IP.new("1.2.3.4/24").network => #<IP::V4 1.2.3.0/24>
+ # IP.new("1.2.3.4/24").network(7) => #<IP::V4 1.2.3.7/24>
+ def network(offset=0)
+ self.class.new((@addr & ~mask) + offset, @pfxlen, @ctx)
+ end
+
+ # Return a new IP object at the top of the subnet, with an optional
+ # offset applied.
+ # IP.new("1.2.3.4/24").broadcast => #<IP::V4 1.2.3.255/24>
+ # IP.new("1.2.3.4/24").broadcast(-1) => #<IP::V4 1.2.3.254/24>
+ def broadcast(offset=0)
+ self.class.new((@addr | mask) + offset, @pfxlen, @ctx)
+ end
+
+ # Return a new IP object representing the netmask
+ # IP.new("1.2.3.4/24").netmask => #<IP::V4 255.255.255.0>
+ def netmask
+ self.class.new(self.class::MASK & ~mask)
+ end
+
+ # Return a new IP object representing the wildmask (inverse netmask)
+ # IP.new("1.2.3.4/24").netmask => #<IP::V4 0.0.0.255>
+ def wildmask
+ self.class.new(mask)
+ end
+
+ # Masks the address such that it is the base of the subnet
+ # IP.new("1.2.3.4/24").mask! => #<IP::V4 1.2.3.0/24>
+ def mask!
+ @addr &= ~mask
+ self
+ end
+
+ # Returns true if this is not the base address of the subnet implied
+ # from the prefix length (e.g. 1.2.3.4/24 is offset, because the base
+ # is 1.2.3.0/24)
+ def offset?
+ @addr != (@addr & ~mask)
+ end
+
+ # Returns offset from base of subnet to this address
+ # IP.new("1.2.3.4/24").offset => 4
+ def offset
+ @addr - (@addr & ~mask)
+ end
+
+ # If the address is not on the base, turn it into a single IP.
+ # IP.new("1.2.3.4/24").reset_pfxlen! => <IP::V4 1.2.3.4>
+ # IP.new("1.2.3.0/24").reset_pfxlen! => <IP::V4 1.2.3.0/24>
+ def reset_pfxlen!
+ self.pfxlen = nil if offset?
+ self
+ end
+
+ def to_range
+ a1 = @addr & ~mask
+ a2 = a1 | mask
+ (a1..a2)
+ end
+
+ # The number of IP addresses in subnet
+ # IP.new("1.2.3.4/24").size => 256
+ def size
+ mask + 1
+ end
+
+ def +(other)
+ self.class.new(@addr + other.to_int, @pfxlen, @ctx)
+ end
+
+ def -(other)
+ self.class.new(@addr - other.to_int, @pfxlen, @ctx)
+ end
+
+ def &(other)
+ self.class.new(@addr & other.to_int, @pfxlen, @ctx)
+ end
+
+ def |(other)
+ self.class.new(@addr | other.to_int, @pfxlen, @ctx)
+ end
+
+ def ^(other)
+ self.class.new(@addr ^ other.to_int, @pfxlen, @ctx)
+ end
+
+ def ~
+ self.class.new(~@addr & self.class::MASK, @pfxlen, @ctx)
+ end
+
+ def succ!
+ @addr += 1
+ self
+ end
+
+ def inspect
+ res = "#<#{self.class} #{to_s}>"
+ end
+
+ def ipv4_mapped?
+ false
+ end
+
+ def ipv4_compat?
+ false
+ end
+
+ def native
+ self
+ end
+
+ def hash
+ to_a.hash
+ end
+
+ def eql?(other)
+ to_a.eql?(other.to_a)
+ end
+
+ def <=>(other)
+ to_a <=> other.to_a
+ end
+ include Comparable
+
+ class V4 < IP
+ class << self; alias :new :orig_new; end
+ PROTO = "v4".freeze
+ PROTO_TO_CLASS[PROTO] = self
+ ADDR_BITS = 32
+ MASK = (1 << ADDR_BITS) - 1
+
+ # Parse a string; return an V4 instance if it's a valid IPv4 address,
+ # nil otherwise
+ def self.parse(str)
+ if str =~ /\A(\d+)\.(\d+)\.(\d+)\.(\d+)(?:\/(\d+))?(?:@(.*))?\z/
+ pfxlen = ($5 || ADDR_BITS).to_i
+ return nil if pfxlen > 32
+ addrs = [$1.to_i, $2.to_i, $3.to_i, $4.to_i]
+ return nil if addrs.find { |n| n>255 }
+ addr = (((((addrs[0] << 8) | addrs[1]) << 8) | addrs[2]) << 8) | addrs[3]
+ new(addr, pfxlen, $6)
+ end
+ end
+
+ # Return just the address part as a String in dotted decimal form
+ def to_addr
+ sprintf("%d.%d.%d.%d",
+ (@addr>>24)&0xff, (@addr>>16)&0xff, (@addr>>8)&0xff, @addr&0xff)
+ end
+ end
+
+ class V6 < IP
+ class << self; alias :new :orig_new; end
+ PROTO = "v6".freeze
+ PROTO_TO_CLASS[PROTO] = self
+ ADDR_BITS = 128
+ MASK = (1 << ADDR_BITS) - 1
+
+ # Parse a string; return an V6 instance if it's a valid IPv6 address,
+ # nil otherwise
+ #--
+ # FIXME: allow larger variations of mapped addrs like 0:0:0:0:ffff:1.2.3.4
+ #++
+ def self.parse(str)
+ case str
+ when /\A\[?::(ffff:)?(\d+\.\d+\.\d+\.\d+)\]?(?:\/(\d+))?(?:@(.*))?\z/i
+ mapped = $1
+ pfxlen = ($3 || 128).to_i
+ v4 = (V4.parse($2) || return).to_i
+ v4 |= 0xffff00000000 if mapped
+ new(v4, pfxlen, $4)
+ when /\A\[?([0-9a-f:]+)\]?(?:\/(\d+))?(?:@(.*))?\z/i
+ addr = $1
+ pfxlen = ($2 || 128).to_i
+ ctx = $3
+ return nil if pfxlen > 128
+ if addr =~ /\A(.*?)::(.*)\z/
+ left, right = $1, $2
+ l = left.split(':')
+ r = right.split(':')
+ rest = 8 - l.length - r.length
+ return nil if rest < 0
+ else
+ l = addr.split(':')
+ r = []
+ rest = 0
+ return nil if l.length != 8
+ end
+ out = ""
+ l.each { |quad| return nil if quad.length>4; out << quad.rjust(4,"0") }
+ rest.times { out << "0000" }
+ r.each { |quad| return nil if quad.length>4; out << quad.rjust(4,"0") }
+ new(out, pfxlen, ctx)
+ else
+ nil
+ end
+ end
+
+ # Return just the address part as a String in compact decimal form
+ def to_addr
+ if ipv4_compat?
+ "::#{native.to_addr}"
+ elsif ipv4_mapped?
+ "::ffff:#{native.to_addr}"
+ else
+ res = to_hex.scan(/..../).join(':')
+ res.gsub!(/\b0{1,3}/,'')
+ res.gsub!(/(\A|:)(0:)+/,'::')
+ res.gsub!(/::0\z/, '::')
+ res
+ end
+ end
+
+ def ipv4_mapped?
+ (@addr >> 32) == 0xffff
+ end
+
+ def ipv4_compat?
+ @addr > 1 && (@addr >> 32) == 0
+ end
+
+ # Convert an IPv6 mapped/compat address to a V4 native address
+ def native
+ return self unless (ipv4_mapped? || ipv4_compat?) && (@pfxlen >= 96)
+ V4.new(@addr & V4::MASK, @pfxlen - 96, @ctx)
+ end
+ end
+end
19 lib/ip/cpal.rb
@@ -0,0 +1,19 @@
+class IP
+ # Create an instance from an alternative array format:
+ # [context, protocol, address, prefix_length]
+ def self.from_cpal(cpal)
+ new([cpal[1], cpal[2], cpal[3], cpal[0]])
+ end
+
+ # Return an alternative 4-element array format with the routing context
+ # as the first element. Useful for grouping by context.
+ # cpal = [context, proto, address, prefix_length]
+ def to_cpal
+ [@ctx, self.class::PROTO, @addr, @pfxlen]
+ end
+
+ # As cpal but with a hex string for the address part
+ def to_cphl
+ [@ctx, self.class::PROTO, to_hex, @pfxlen]
+ end
+end
31 lib/ip/socket.rb
@@ -0,0 +1,31 @@
+require 'socket'
+
+class IP
+ # Return the address family, Socket::AF_INET or Socket::AF_INET6
+ def af
+ self.class::AF
+ end
+
+ # Convert to a packed sockaddr structure
+ def to_sockaddr(port=0)
+ Socket.pack_sockaddr_in(port, to_addr)
+ end
+
+ class V4
+ AF = Socket::AF_INET
+ PROTO_TO_CLASS[AF] = self
+
+ # Avoid the string conversion when building sockaddr. Unfortunately this
+ # fails 32-bit machines with 1.8.6 for addrs >= 0x80000000. There is
+ # also no corresponding Socket.pack_sockaddr_in6 we could use for V6.
+
+ #def to_sockaddr(port=0)
+ # Socket.pack_sockaddr_in(port, to_i)
+ #end
+ end
+
+ class V6
+ AF = Socket::AF_INET6
+ PROTO_TO_CLASS[AF] = self
+ end
+end
33 test/ip_cpal_test.rb
@@ -0,0 +1,33 @@
+require 'test_helper'
+require 'ip/base'
+require 'ip/cpal'
+
+class IPTestRPAL < Test::Unit::TestCase
+ context "v4" do
+ setup do
+ @addr = IP.new("1.2.3.4/24@foo")
+ end
+
+ should "build from cpal" do
+ res = IP.from_cpal(["bar", "v4", 0x01020304, 26])
+ assert_equal IP::V4, res.class
+ assert_equal "1.2.3.4/26@bar", res.to_s
+ end
+
+ should "have to_cpal" do
+ assert_equal ["foo","v4", 0x01020304, 24], @addr.to_cpal
+ end
+
+ should "have to_cphl" do
+ assert_equal ["foo","v4", "01020304", 24], @addr.to_cphl
+ end
+ end
+
+ context "v6" do
+ should "build from cpal" do
+ res = IP.from_cpal(["bar", "v6", 0xdeadbeef000000000000000000000123, 48])
+ assert_equal IP::V6, res.class
+ assert_equal "dead:beef::123/48@bar", res.to_s
+ end
+ end
+end
483 test/ip_test.rb
@@ -0,0 +1,483 @@
+require 'test_helper'
+require 'ip/base'
+
+class IPTest < Test::Unit::TestCase
+ context "v4" do
+ should "build from string" do
+ res = IP.new("1.2.3.4/26")
+ assert_equal IP::V4, res.class
+ assert_equal "1.2.3.4/26", res.to_s
+ assert_equal "1.2.3.4/26", res.to_addrlen
+ assert_equal 0x01020304, res.to_i
+ assert_equal 26, res.pfxlen
+ assert_nil res.ctx
+ end
+
+ should "build from string with ctx" do
+ res = IP.new("1.2.3.4/26@nat")
+ assert_equal IP::V4, res.class
+ assert_equal "1.2.3.4/26@nat", res.to_s
+ assert_equal "1.2.3.4/26", res.to_addrlen
+ assert_equal 0x01020304, res.to_i
+ assert_equal 26, res.pfxlen
+ assert_equal "nat", res.ctx
+ end
+
+ should "build from array" do
+ res = IP.new(["v4", 0x01020304])
+ assert_equal IP::V4, res.class
+ assert_equal "1.2.3.4", res.to_s
+ assert_equal 32, res.pfxlen
+ end
+
+ should "build from array with pfxlen" do
+ res = IP.new(["v4", 0x01020304, 26])
+ assert_equal IP::V4, res.class
+ assert_equal "1.2.3.4/26", res.to_s
+ end
+
+ should "build from array with pfxlen and ctx" do
+ res = IP.new(["v4", 0x01020304, 26, "bar"])
+ assert_equal IP::V4, res.class
+ assert_equal "1.2.3.4/26@bar", res.to_s
+ end
+
+ should "build from array with hex, pfxlen and ctx" do
+ res = IP.new(["v4", "01020304", 26, "bar"])
+ assert_equal IP::V4, res.class
+ assert_equal "1.2.3.4/26@bar", res.to_s
+ end
+
+ should "build direct from integer" do
+ res = IP::V4.new(0x01020304)
+ assert_equal "1.2.3.4", res.to_s
+ assert_equal 32, res.pfxlen
+ assert_nil res.ctx
+ end
+
+ should "build direct from integer, pfxlen, ctx" do
+ res = IP::V4.new(0x01020304, 24, "foo")
+ assert_equal "1.2.3.4/24@foo", res.to_s
+ end
+
+ should "build from another IP" do
+ s1 = IP.new("1.2.3.4/24@foo")
+ s2 = IP.new(s1)
+ assert_equal s1, s2
+ assert_not_equal s1.object_id, s2.object_id
+ end
+
+ context "address not on subnet boundary" do
+ setup do
+ @addr = IP.new("1.2.3.4/24@foo")
+ end
+
+ should "have to_s" do
+ assert_equal "1.2.3.4/24@foo", @addr.to_s
+ end
+
+ should "have to_addrlen" do
+ assert_equal "1.2.3.4/24", @addr.to_addrlen
+ end
+
+ should "have to_addr" do
+ assert_equal "1.2.3.4", @addr.to_addr
+ end
+
+ should "have to_i" do
+ assert_equal 0x01020304, @addr.to_i
+ end
+
+ should "have to_a" do
+ assert_equal ["v4", 0x01020304, 24, "foo"], @addr.to_a
+ end
+
+ should "have to_ah" do
+ assert_equal ["v4", "01020304", 24, "foo"], @addr.to_ah
+ end
+
+ should "have to_hex" do
+ assert_equal "01020304", @addr.to_hex
+ end
+
+ should "have inspect" do
+ assert_equal "#<IP::V4 1.2.3.4/24@foo>", @addr.inspect
+ end
+
+ should "have pfxlen" do
+ assert_equal 24, @addr.pfxlen
+ end
+
+ should "have to_range" do
+ assert_equal((0x01020300 .. 0x010203ff), @addr.to_range)
+ end
+
+ should "have size" do
+ assert_equal 256, @addr.size
+ end
+
+ should "have ctx reader" do
+ assert_equal "foo", @addr.ctx
+ end
+
+ should "have ctx writer" do
+ @addr.ctx = "bar"
+ assert_equal "bar", @addr.ctx
+ end
+
+ should "have network" do
+ assert_equal "1.2.3.0/24@foo", @addr.network.to_s
+ assert_equal "1.2.3.9/24@foo", @addr.network(9).to_s
+ end
+
+ should "have broadcast" do
+ assert_equal "1.2.3.255/24@foo", @addr.broadcast.to_s
+ assert_equal "1.2.3.252/24@foo", @addr.broadcast(-3).to_s
+ end
+
+ should "have mask" do
+ assert_equal 0x000000ff, @addr.mask
+ end
+
+ should "have netmask" do
+ assert_equal "255.255.255.0", @addr.netmask.to_s
+ end
+
+ should "have wildmask" do
+ assert_equal "0.0.0.255", @addr.wildmask.to_s
+ end
+
+ should "perform mask!" do
+ res = @addr.mask!
+ assert_equal "1.2.3.0/24@foo", res.to_s
+ assert_equal "1.2.3.0/24@foo", @addr.to_s # mutates object
+ end
+
+ should "have offset" do
+ assert @addr.offset?
+ assert_equal 4, @addr.offset
+ @addr.reset_pfxlen!
+ assert_equal "1.2.3.4@foo", @addr.to_s
+ end
+
+ should "have +" do
+ assert_equal ["v4", 0x01020309, 24, "foo"], (@addr + 5).to_a
+ assert_equal ["v4", 0x01020304, 24, "foo"], @addr.to_a
+ end
+
+ should "have -" do
+ assert_equal ["v4", 0x010202ff, 24, "foo"], (@addr - 5).to_a
+ assert_equal ["v4", 0x01020304, 24, "foo"], @addr.to_a
+ end
+
+ should "have &" do
+ assert_equal ["v4", 0x00000304, 24, "foo"], (@addr & 0xffff).to_a
+ assert_equal ["v4", 0x01020304, 24, "foo"], @addr.to_a
+ end
+
+ should "have |" do
+ assert_equal ["v4", 0x01020307, 24, "foo"], (@addr | 7).to_a
+ assert_equal ["v4", 0x01020304, 24, "foo"], @addr.to_a
+ end
+
+ should "have ^" do
+ assert_equal ["v4", 0x010203fb, 24, "foo"], (@addr ^ 255).to_a
+ assert_equal ["v4", 0x01020304, 24, "foo"], @addr.to_a
+ end
+
+ should "have ~" do
+ assert_equal ["v4", 0xfefdfcfb, 24, "foo"], (~@addr).to_a
+ assert_equal ["v4", 0x01020304, 24, "foo"], @addr.to_a
+ end
+
+ should "have pfxlen writer" do
+ assert_equal 0xffffff00, @addr.netmask.to_i
+ @addr.pfxlen = 29
+ assert_equal 0xfffffff8, @addr.netmask.to_i
+ assert_equal "1.2.3.4/29@foo", @addr.to_s
+ end
+
+ should "have native" do
+ assert_equal @addr, @addr.native
+ end
+ end
+
+ context "address on subnet boundary" do
+ setup do
+ @addr = IP.new("1.2.3.4/30")
+ end
+
+ should "have inspect" do
+ assert_equal "#<IP::V4 1.2.3.4/30>", @addr.inspect
+ end
+
+ should "perform mask!" do
+ @addr.mask!
+ assert_equal "1.2.3.4/30", @addr.to_s
+ end
+
+ should "have offset" do
+ assert !@addr.offset?
+ assert_equal 0, @addr.offset
+ end
+ end
+
+ context "single IP" do
+ setup do
+ @addr = IP.new("1.2.3.4")
+ end
+
+ should "have inspect" do
+ assert_equal "#<IP::V4 1.2.3.4>", @addr.inspect
+ end
+
+ should "have pfxlen" do
+ assert_equal 32, @addr.pfxlen
+ end
+
+ should "have size" do
+ assert_equal 1, @addr.size
+ end
+
+ should "have offset" do
+ assert !@addr.offset?
+ assert_equal 0, @addr.offset
+ end
+ end
+ end
+
+ context "v6 normal" do
+ should "build from string" do
+ res = IP.new("dead:beef::123/48")
+ assert_equal IP::V6, res.class
+ assert_equal "dead:beef::123/48", res.to_s
+ assert_equal "dead:beef::123/48", res.to_addrlen
+ assert_equal 0xdeadbeef000000000000000000000123, res.to_i
+ assert_equal 48, res.pfxlen
+ assert_nil res.ctx
+ end
+
+ should "build from string with ctx" do
+ res = IP.new("dead:beef::123/48@nat")
+ assert_equal IP::V6, res.class
+ assert_equal "dead:beef::123/48@nat", res.to_s
+ assert_equal "dead:beef::123/48", res.to_addrlen
+ assert_equal 0xdeadbeef000000000000000000000123, res.to_i
+ assert_equal 48, res.pfxlen
+ assert_equal "nat", res.ctx
+ end
+
+ should "build from array" do
+ res = IP.new(["v6", 0xdeadbeef000000000000000000000123])
+ assert_equal IP::V6, res.class
+ assert_equal "dead:beef::123", res.to_s
+ assert_equal 128, res.pfxlen
+ assert_nil res.ctx
+ end
+
+ should "build from array with pfxlen" do
+ res = IP.new(["v6", 0xdeadbeef000000000000000000000123, 48])
+ assert_equal IP::V6, res.class
+ assert_equal "dead:beef::123/48", res.to_s
+ end
+
+ should "build from array with pfxlen and ctx" do
+ res = IP.new(["v6", 0xdeadbeef000000000000000000000123, 48, "bar"])
+ assert_equal IP::V6, res.class
+ assert_equal "dead:beef::123/48@bar", res.to_s
+ end
+
+ should "build from array with hex, pfxlen and ctx" do
+ res = IP.new(["v6", "deadbeef000000000000000000000123", 48, "bar"])
+ assert_equal IP::V6, res.class
+ assert_equal "dead:beef::123/48@bar", res.to_s
+ end
+
+ should "build direct from integer" do
+ res = IP::V6.new(0xdeadbeef000000000000000000000123)
+ assert_equal "dead:beef::123", res.to_s
+ assert_equal 128, res.pfxlen
+ assert_nil res.ctx
+ end
+
+ should "build direct from integer, pfxlen, ctx" do
+ res = IP::V6.new(0xdeadbeef000000000000000000000123, 24, "foo")
+ assert_equal "dead:beef::123/24@foo", res.to_s
+ assert_equal 24, res.pfxlen
+ assert_equal "foo", res.ctx
+ end
+
+ should "have native" do
+ res = IP::V6.new(0xdeadbeef000000000000000000000123, 24, "foo")
+ assert_equal res, res.native
+ end
+ end
+
+ context "v6 ::0" do
+ setup do
+ @addr = IP::V6.new(0)
+ end
+
+ should "format" do
+ assert_equal "::", @addr.to_s
+ end
+
+ should "have native" do
+ assert_equal @addr, @addr.native
+ end
+ end
+
+ context "v6 ::1" do
+ setup do
+ @addr = IP::V6.new(1)
+ end
+
+ should "format" do
+ assert_equal "::1", @addr.to_s
+ end
+
+ should "have native" do
+ assert_equal @addr, @addr.native
+ end
+ end
+
+ context "v6 compat" do
+ setup do
+ @addr = IP.new("::1.2.3.4/120@xxx")
+ end
+
+ should "parse" do
+ assert_equal 0x01020304, @addr.to_i
+ assert_equal "xxx", @addr.ctx
+ end
+
+ should "format" do
+ assert_equal "::1.2.3.4/120@xxx", @addr.to_s
+ end
+
+ should "have native" do
+ a2 = @addr.native
+ assert_equal "1.2.3.4/24@xxx", a2.to_s
+ end
+ end
+
+ context "v6 mapped" do
+ setup do
+ @addr = IP.new("::ffff:1.2.3.4/120@xxx")
+ end
+
+ should "parse" do
+ assert_equal 0xffff01020304, @addr.to_i
+ assert_equal "xxx", @addr.ctx
+ end
+
+ should "format" do
+ assert_equal "::ffff:1.2.3.4/120@xxx", @addr.to_s
+ end
+
+ should "have native" do
+ a2 = @addr.native
+ assert_equal "1.2.3.4/24@xxx", a2.to_s
+ end
+ end
+
+ context "comparing" do
+ setup do
+ @a1 = IP.new("1.2.3.4")
+ end
+
+ should "compare equal with same" do
+ assert_equal @a1, IP.new("1.2.3.4")
+ end
+
+ should "order v4 before v6" do
+ assert @a1 < IP.new("::1")
+ end
+
+ should "compare < >" do
+ @a2 = IP.new("2.3.4.5")
+ assert @a1 < @a2
+ assert @a2 > @a1
+ end
+
+ should "use prefix as tiebreaker" do
+ @a2 = IP.new("1.2.3.4/30")
+ assert @a1 > @a2 # @a1 has /32 prefix length
+ end
+
+ should "use ctx as tiebreaker" do
+ @a2 = IP.new("1.2.3.4@bar")
+ @a3 = IP.new("1.2.3.4@foo")
+ assert @a1 < @a2 # @a1 has no ctx
+ assert @a2 < @a3
+ end
+ end
+
+ context "hashing" do
+ should "implement eql?" do
+ assert IP.new("1.2.3.96@tst").eql?(IP.new("1.2.3.96@tst"))
+ end
+
+ should "implement hash" do
+ assert_equal IP.new("1.2.3.96@tst").hash, IP.new("1.2.3.96@tst").hash
+ end
+
+ should "be able to use IP as hash key" do
+ @hash = {IP.new("1.2.3.96@tst")=>1, IP.new("1.2.3.111@tst")=>2}
+ assert_equal 1, @hash[IP.new("1.2.3.96@tst")]
+ assert_equal 2, @hash[IP.new("1.2.3.111@tst")]
+ end
+ end
+
+ PARSE_TESTS = [
+ # ipv4
+ [["v4", 0x01020304, 32], "1.2.3.4"],
+ [["v4", 0xffffffff, 32], "255.255.255.255"],
+ [nil, "255.255.255.256"],
+ [["v4", 0x01020304, 29], "1.2.3.4/29"],
+ [nil, "1.2.3.4/33"],
+ # ipv6
+ [nil, "Abc"],
+ [["v6", 0x0abc0000000000000000000000000000, 128], "Abc::"],
+ [["v6", 0x0abc0000000000000000000000000000, 128], "[Abc::]"],
+ [["v6", 0x00000000000000000000000000000abc, 128], "::Abc"],
+ [nil, "Abcde::"],
+ [["v6", 0x12340000000000000000000000000005, 128], "1234::5"],
+ [["v6", 0x11122223333444455556666777788889, 128], "1112:2223:3334:4445:5556:6667:7778:8889"],
+ [["v6", 0x11122223333400005556666777788889, 128], "1112:2223:3334::5556:6667:7778:8889"],
+ [nil, "1112:2223:3334:4445:5556:6667:7778"],
+ [["v6", 0x00000000000000000000000000000001, 96], "::1/96"],
+ [["v6", 0x00000000000000000000000000000001, 96], "[::1]/96"],
+ [nil, "[::1]/129"],
+ [["v6", 0xc0a80001, 128], "::192.168.0.1"],
+ [["v6", 0x01020304, 120], "::1.2.3.4/120"],
+ [["v6", 0xffff01020304, 126], "::ffff:1.2.3.4/126"],
+ [["v6", 0xffff01020304, 126], "::ffff:1.2.3.4/126"],
+ [["v6", 0xffff01020304, 120], "[::ffff:1.2.3.4]/120"],
+ ]
+
+ FORMAT_TESTS = [
+ ["1.2.3.4", ["v4",0x01020304,32]],
+ ["1.2.3.4/29", ["v4",0x01020304,29]],
+# ["::4d2", ["v6",0x000000000000000000000000000004d2,128]],
+# ["abc::1/96", ["v6",0x0abc0000000000000000000000000001,96]],
+ ]
+
+ should "parse addresses" do
+ PARSE_TESTS.each do |exp,src|
+ res = IP.parse(src)
+ assert_equal exp, exp ? res.to_a : res, "Testing #{src.inspect}"
+ end
+ end
+
+ should "parse with routing context" do
+ res = IP.parse("1.2.3.4/28@foo")
+ assert_equal ["v4", 0x01020304, 28, "foo"], res.to_a
+ end
+
+ should "format addresses" do
+ FORMAT_TESTS.each do |exp,src|
+ assert_equal exp, IP.new(src).to_s, "Testing #{src.inspect}"
+ end
+ end
+end
4 test/test_helper.rb
@@ -0,0 +1,4 @@
+require 'rubygems'
+require 'shoulda'
+require 'test/unit'
+#require 'mocha'

0 comments on commit 0dcead5

Please sign in to comment.