Index: ipOps.lua
===================================================================
--- ipOps.lua	(revision 29646)
+++ ipOps.lua	(working copy)
@@ -3,17 +3,17 @@
 --
 -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
 
-local bin = require "bin"
-local bit = require "bit"
-local stdnse = require "stdnse"
-local string = require "string"
-local table = require "table"
 local type     = type
 local table    = table
 local string   = string
 local ipairs   = ipairs
+local pairs    = pairs
 local tonumber = tonumber
+local tostring = tostring
+local unpack   = unpack
 
+local stdnse   = require "stdnse"
+local bit      = require "bit"
 
 _ENV = stdnse.module("ipOps", stdnse.seeall)
 
@@ -127,7 +127,7 @@
 --
 -- Note: IPv6 addresses are not supported. Currently, numbers in NSE are
 -- limited to 10^14, and consequently not all IPv6 addresses can be
--- represented. Consider using <code>ip_to_str</code> for IPv6 addresses.
+-- represented.
 -- @param ip  String representing an IPv4 address.  Shortened notation is
 -- permitted.
 -- @usage
@@ -212,8 +212,7 @@
 
 
 ---
--- Compares two IP addresses. When comparing addresses from different families,
--- IPv4 addresses will sort before IPv6 addresses.
+-- Compares two IP addresses (of either type).
 -- @param left   String representing an IPv4 or IPv6 address.  Shortened
 -- notation is permitted.
 -- @param op     A comparison operator which may be one of the following
@@ -233,36 +232,49 @@
     return nil, "Error in ipOps.compare_ip: Expected IP address as a string."
   end
 
+  if     ( left:match( ":" ) and not right:match( ":" ) ) then right = expand_ip( '::ffff:' .. right )
+  elseif ( not left:match( ":" ) and right:match( ":" ) ) then left  = expand_ip( '::ffff:' .. left )
+  end
+
+  if op == "lt" or op == "le" then
+    left, right = right, left
+  elseif op ~= "eq" and op ~= "ne" and op ~= "ge" and op ~= "gt" then
+    return nil, "Error in ipOps.compare_ip: Invalid Operator."
+  end
+
   local err ={}
-  left, err[#err+1] = ip_to_str( left )
-  right, err[#err+1] = ip_to_str( right )
+  left, err[#err+1] = ip_to_bin( left )
+  right, err[#err+1] = ip_to_bin( right )
   if #err > 0 then
     return nil, table.concat( err, " " )
   end
 
-  if #left > #right then
-    left = bin.pack( "CA", 0x06, left )
-    right = bin.pack( "CA", 0x04, right )
-  elseif #right > #left then
-    right = bin.pack( "CA", 0x06, right )
-    left = bin.pack( "CA", 0x04, left )
+  if string.len( left ) ~= string.len( right ) then
+      -- shouldn't happen...
+      return nil, "Error in ipOps.compare_ip: Binary IP addresses were of different lengths."
   end
 
-  if ( op == "eq" ) then
-    return ( left == right )
-  elseif ( op == "ne" ) then
+  -- equal?
+  if ( op == "eq" or op == "le" or op == "ge" ) and left == right then
+    return true
+  elseif op == "eq" then
+    return false
+  elseif op == "ne" then
     return ( left ~= right )
-  elseif ( op == "le" ) then
-    return ( left <= right )
-  elseif ( op == "ge" ) then
-    return ( left >= right )
-  elseif ( op == "lt" ) then
-    return ( left < right )
-  elseif ( op == "gt" ) then
-    return ( left > right )
   end
 
-  return nil, "Error in ipOps.compare_ip: Invalid Operator."
+  -- starting from the leftmost bit, subtract the bit in right from the bit in left
+  local compare
+  for i = 1, string.len( left ), 1 do
+    compare = tonumber( string.sub( left, i, i ) ) - tonumber( string.sub( right, i, i ) )
+    if compare == 1 then
+      return true
+    elseif compare == -1 then
+      return false
+    end
+  end
+  return false
+
 end
 
 
@@ -288,8 +300,8 @@
   if err then return nil, err end
   ip, err = expand_ip( ip )
   if err then return nil, err end
-  if ( ip:match( ":" ) and not first:match( ":" ) ) or ( not ip:match( ":" ) and first:match( ":" ) ) then
-    return nil, "Error in ipOps.ip_in_range: IP address is of a different address family to Range."
+  if     ( first:match( ":" ) and not ip:match( ":" ) ) then ip    = expand_ip( '::ffff:' .. ip )
+  elseif ( not first:match( ":" ) and ip:match( ":" ) ) then first = expand_ip( '::ffff:' .. first )
   end
 
   err = {}
@@ -318,15 +330,12 @@
 -- the IPv4 portion is shortened and does not contain a dot, in which case the
 -- address will be treated as IPv6.
 -- @param ip  String representing an IPv4 or IPv6 address in shortened or full notation.
--- @param family String representing the address family to expand to. Only
--- affects IPv4 addresses when "inet6" is provided, causing the function to
--- return an IPv4-mapped IPv6 address.
 -- @usage
 -- local ip = ipOps.expand_ip( "2001::" )
 -- @return    String representing a fully expanded IPv4 or IPv6 address (or
 -- <code>nil</code> in case of an error).
 -- @return String error message in case of an error.
-expand_ip = function( ip, family )
+expand_ip = function( ip )
   local err
 
   if type( ip ) ~= "string" or ip == "" then
@@ -349,20 +358,9 @@
     while #octets < 4 do
       octets[#octets+1] = "0"
     end
-    if family == "inet6" then
-      return ( table.concat( { 0,0,0,0,0,"ffff",
-        stdnse.tohex( 256*octets[1]+octets[2] ),
-        stdnse.tohex( 256*octets[3]+octets[4] ) 
-        }, ":" ) )
-    else
       return ( table.concat( octets, "." ) )
     end
-  end
 
-  if family ~= nil and family ~= "inet6" then
-    return nil, "Error in ipOps.expand_ip: Cannot convert IPv6 address to IPv4"
-  end
-
   if ip:match( "[^%.:%x]" ) then
     return nil, ( err4:gsub( "IPv4", "IPv6" ) )
   end
@@ -445,6 +443,9 @@
     first, prefix = range:match( "([%x%d:%.]+)/(%d+)" )
   elseif range:match( "-" ) then
     first, last = range:match( "([%x%d:%.]+)%s*%-%s*([%x%d:%.]+)" )
+  else
+    first = range
+    last  = range
   end
 
   local err = {}
@@ -460,8 +461,8 @@
   end
 
   if first and last then
-    if ( first:match( ":" ) and not last:match( ":" ) ) or ( not first:match( ":" ) and last:match( ":" ) ) then
-      return nil, nil, "Error in ipOps.get_ips_from_range: First IP address is of a different address family to last IP address."
+    if     ( first:match( ":" ) and not last:match( ":" ) ) then last  = expand_ip( '::ffff:' .. last )
+    elseif ( not first:match( ":" ) and last:match( ":" ) ) then first = expand_ip( '::ffff:' .. first )
     end
     return first, last
   else
@@ -473,6 +474,233 @@
 
 
 ---
+-- Converts a first and last address into a table of ranges.
+-- @param first  String representing a IPv4 or IPv6 address.
+-- @param last   String representing a IPv4 or IPv6 address.
+-- @usage
+-- ranges = ipOps.get_ranges_from_ips( "192.168.0.0", "192.168.255.255" )
+-- @return       Table containing the list of ranges in CIDR format
+-- (or <code>nil</code> in case of an error).
+-- @return       String error message in case of an error.
+get_ranges_from_ips = function( first, last )
+
+  if first == last then return { first } end
+
+  local err = {}
+  first, err[#err+1] = expand_ip( first )
+  last,  err[#err+1] = expand_ip( last )
+  if #err > 0 then
+    return nil, table.concat( err, " " )
+  end
+
+  if first == last then return { first } end
+
+  if compare_ip( first, "gt", last ) then first, last = last, first end
+
+  if     ( first:match( ":" ) and not last:match( ":" ) ) then last  = expand_ip( '::ffff:' .. last )
+  elseif ( not first:match( ":" ) and last:match( ":" ) ) then first = expand_ip( '::ffff:' .. first )
+  end
+
+  local maxPrefix = 32
+  if first:match( ":" ) then maxPrefix = 128 end
+
+  -- find the best net address
+  local netPrefix = maxPrefix
+  for cidr = maxPrefix-1, 1, -1 do
+    local netIP = get_net_ip(first, cidr)
+    if compare_ip( first, "eq", netIP ) then netPrefix = cidr else break end
+  end
+
+  -- find the best prefix
+  local prefix = maxPrefix
+  local lastIP
+  for cidr = netPrefix, maxPrefix, 1 do
+    lastIP = get_last_ip(first, cidr)
+    if compare_ip( last, "ge", lastIP ) then prefix = cidr break end  -- (no else)
+  end
+
+  -- divide and conquer
+  local range = first .. '/' .. tostring(prefix)
+  if prefix == maxPrefix then range = first end
+  if compare_ip( last, "eq", lastIP ) then return { range } end
+
+  local ip_blocks = {}
+  ip_blocks, err = get_ranges_from_ips( get_next_ip(lastIP), last )  -- re-run to get the next set of IP blocks
+  if ip_blocks then
+    table.insert(ip_blocks, 1, range)
+    return ip_blocks
+  else
+    return nil, err
+  end
+
+end
+
+
+
+---
+-- Returns a table (possibly combined) with both ranges A and B.
+-- @param rangeA  String representing a range of IPv4 or IPv6 addresses in either
+-- CIDR or first-last notation.  Can also be a single IP address.
+-- @param rangeB  String representing a range of IPv4 or IPv6 addresses in either
+-- CIDR or first-last notation.  Can also be a single IP address.
+-- @usage
+-- ranges = ipOps.add_range_to_range( "192.168.0.0/24", "192.168.1.0/24" )
+-- @return       Table containing the list of ranges, which could be the exact
+--  same addresses (if A is completely detached from B), or <code>nil</code> in
+--  case of an error.
+-- @return       String error message in case of an error.
+add_range_to_range = function( rangeA, rangeB )
+
+  -- ip to range shortcut
+  local isIPA, isIPB = not rangeA:match( "[/-]" ), not rangeB:match( "[/-]" )
+  if     isIPA and isIPB then return { rangeA, rangeB }  -- (kind of absurd, but...)
+  elseif isIPA then
+    local inrange, err = ip_in_range( rangeA, rangeB )
+    if err then return nil, err end
+
+    if inrange then return { rangeB }
+    else            return { rangeA, rangeB }
+	end
+  elseif isIPB then
+    local inrange, err = ip_in_range( rangeB, rangeA )
+    if err then return nil, err end
+
+    if inrange then return { rangeA }
+    else            return { rangeA, rangeB }
+	end
+  end
+
+  local firstA, lastA, firstB, lastB
+  local err = {}
+  firstA, lastA, err[#err+1] = get_ips_from_range( rangeA )
+  firstB, lastB, err[#err+1] = get_ips_from_range( rangeB )
+  if #err > 0 then
+    return nil, table.concat( err, " " )
+  end
+
+  local inrangeAF, inrangeAL, inrangeBF, inrangeBL, nextedgeA, nextedgeB
+  inrangeAF, err[#err+1] = ip_in_range( firstA, rangeB )
+  inrangeAL, err[#err+1] = ip_in_range( lastA,  rangeB )
+  inrangeBF, err[#err+1] = ip_in_range( firstB, rangeA )
+  inrangeBL, err[#err+1] = ip_in_range( lastB,  rangeA )
+  nextedgeA, err[#err+1] = compare_ip( get_next_ip(lastA), "eq", firstB )
+  nextedgeB, err[#err+1] = compare_ip( get_next_ip(lastB), "eq", firstA )
+  if #err > 0 then
+    return nil, table.concat( err, " " )
+  end
+
+  -- B fully contains A; B wins
+  if     inrangeAF and inrangeAL then return { rangeB }
+  -- A fully contains B; A wins
+  elseif inrangeBF and inrangeBL then return { rangeA }
+  -- A/B don't even touch each other; separate ranges
+  elseif not inrangeBF and not inrangeBL then return { rangeA, rangeB }
+  -- A is on the left edge of B
+  elseif nextedgeA               then return get_ranges_from_ips(firstA, lastB)
+  -- B is on the left edge of A
+  elseif nextedgeB               then return get_ranges_from_ips(firstB, lastA)
+  -- Venn diagram; A on left side
+  elseif inrangeAL and inrangeBF then return get_ranges_from_ips(firstA, lastB)
+  -- Venn diagram; B on left side
+  elseif inrangeAF and inrangeBL then return get_ranges_from_ips(firstB, lastA)
+  end
+
+  return nil, nil  -- we should never get here...
+end
+
+
+
+---
+-- Subtracts IPs in range B from range A (A minus B).
+-- @param rangeA  String representing a range of IPv4 or IPv6 addresses in either
+-- CIDR or first-last notation.  Can also be a single IP address.
+-- @param rangeB  String representing a range of IPv4 or IPv6 addresses in either
+-- CIDR or first-last notation.  Can also be a single IP address.
+-- @usage
+-- ranges = ipOps.subtract_range_from_range( "192.168.0.0/24", "192.168.128.0/22" )
+-- @return       Table containing the list of ranges, which could be an empty
+--  table (if B wholely contains A), or <code>nil</code> in case of an error.
+-- @return       String error message in case of an error.
+subtract_range_from_range = function( rangeA, rangeB )
+
+  local firstA, lastA, firstB, lastB
+  local err = {}
+  firstA, lastA, err[#err+1] = get_ips_from_range( rangeA )
+  firstB, lastB, err[#err+1] = get_ips_from_range( rangeB )
+  if #err > 0 then
+    return nil, table.concat( err, " " )
+  end
+
+  local inrangeAF, inrangeAL, inrangeBF, inrangeBL, isedgeF, isedgeL
+  inrangeAF, err[#err+1] = ip_in_range( firstA, rangeB )
+  inrangeAL, err[#err+1] = ip_in_range( lastA,  rangeB )
+  inrangeBF, err[#err+1] = ip_in_range( firstB, rangeA )
+  inrangeBL, err[#err+1] = ip_in_range( lastB,  rangeA )
+  isedgeF,   err[#err+1] = compare_ip( firstA, "eq", firstB )
+  isedgeL,   err[#err+1] = compare_ip( lastA,  "eq", lastB  )
+  if #err > 0 then
+    return nil, table.concat( err, " " )
+  end
+
+  -- B fully contains A; B wins
+  if     inrangeAF and inrangeAL then return { }
+  -- B isn't even in A; A wins
+  elseif not inrangeBF and not inrangeBL then return { rangeA }
+  -- Venn diagram; A on left side
+  elseif inrangeAL and inrangeBF then return get_ranges_from_ips(firstA, get_prev_ip(firstB))
+  -- Venn diagram; B on left side
+  elseif inrangeAF and inrangeBL then return get_ranges_from_ips(get_next_ip(lastB), lastA)
+  -- A fully contains B; B is a hole within A
+  elseif inrangeBF and inrangeBL then
+    local blocksL, blocksR
+    if not isedgeF then blocksL, err[#err+1] = get_ranges_from_ips(firstA, get_prev_ip(firstB)) end
+    if not isedgeL then blocksR, err[#err+1] = get_ranges_from_ips(get_next_ip(lastB), lastA) end
+    if #err > 0 then
+      return nil, table.concat( err, " " )
+    end
+
+    if isedgeF then return blocksL end
+    if isedgeL then return blocksR end
+    return { unpack(blocksL), unpack(blocksR) }
+  end
+
+  return nil, nil  -- we should never get here...
+end
+
+---
+-- Calculates the network IP address given an IP address in
+-- the range and prefix length for that range.
+-- @param ip      String representing an IPv4 or IPv6 address.  Shortened
+-- notation is permitted.
+-- @param prefix  Number or a string representing a decimal number corresponding
+-- to a prefix length.
+-- @usage
+-- net = ipOps.get_net_ip( "192.168.85.3", 26 )
+-- @return        String representing the network IP address of the range denoted
+-- by the supplied parameters (or <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+get_net_ip = function( ip, prefix )
+
+  local first, err = ip_to_bin( ip )
+  if err then return nil, err end
+
+  prefix = tonumber( prefix )
+  if not prefix or ( prefix < 0 ) or ( prefix > # first  ) then
+    return nil, "Error in ipOps.get_net_ip: Invalid prefix length."
+  end
+
+  local netbits  = first:sub(1, prefix)
+  local hostbits = ('0'):rep(#first - prefix)
+
+  local net
+  net, err = bin_to_ip( netbits .. hostbits )
+  if err then return nil, err end
+  return net
+end
+
+
+
+---
 -- Calculates the last IP address of a range of addresses given an IP address in
 -- the range and prefix length for that range.
 -- @param ip      String representing an IPv4 or IPv6 address.  Shortened
@@ -503,41 +731,74 @@
 
 end
 
+
+
 ---
--- Converts an IP address into an opaque string.
--- @param ip  String representing an IPv4 or IPv6 address.
--- @param family (optional) Address family to convert to. "ipv6" converts IPv4
--- addresses to IPv4-mapped IPv6.
+-- Calculates the previous IP (IP-1) given an IP address.
+-- @param ip      String representing an IPv4 or IPv6 address.  Shortened
+-- notation is permitted.
 -- @usage
--- opaque = ipOps.ip_to_str( "192.168.3.4" )
--- @return 4- or 16-byte string representing IP address (or <code>nil</code>
+-- next = ipOps.get_prev_ip( "192.168.85.3" )
+-- @return        String representing the previous IP address (or <code>nil</code>
 -- in case of an error).
--- @return String error message in case of an error
-ip_to_str = function( ip, family )
-  local err
+-- @return String error message in case of an error.
+get_prev_ip = function( ip )
 
-  ip, err = expand_ip( ip, family )
+  local bin_ip, err = ip_to_bin( ip )
   if err then return nil, err end
 
-  local t = {}
+  for p = #bin_ip, 1, -1 do
+    -- hit a one; bit can be subtracted and we can stop
+    if bin_ip:sub(p, p) == '1' then
+      bin_ip = bin_ip:replace_char('0', p)
+      break
+    -- hit a zero; bit will underflow to one and we need to continue
+    else
+      bin_ip = bin_ip:replace_char('1', p)
+    end
+  end
 
-  if not ip:match( ":" ) then
-    -- ipv4 string
-    for octet in string.gmatch( ip, "%d+" ) do
-      t[#t+1] = bin.pack( ">C", tonumber(octet) )
-    end
+  local prevIP
+  prevIP, err = bin_to_ip( bin_ip )
+  if err then return nil, err end
+  return prevIP
+end
+
+
+
+---
+-- Calculates the next IP (IP+1) given an IP address.
+-- @param ip      String representing an IPv4 or IPv6 address.  Shortened
+-- notation is permitted.
+-- @usage
+-- next = ipOps.get_next_ip( "192.168.85.3" )
+-- @return        String representing the next IP address (or <code>nil</code>
+-- in case of an error).
+-- @return String error message in case of an error.
+get_next_ip = function( ip )
+
+  local bin_ip, err = ip_to_bin( ip )
+  if err then return nil, err end
+
+  for p = #bin_ip, 1, -1 do
+    -- hit a zero; will fill bit and we can stop
+    if bin_ip:sub(p, p) == '0' then
+      bin_ip = bin_ip:replace_char('1', p)
+      break
+    -- hit a one; bit will overflow to zero and we need to continue
   else
-    -- ipv6 string
-    for hdt in string.gmatch( ip, "%x+" ) do
-      t[#t+1] = bin.pack( ">S", tonumber(hdt, 16) )
+      bin_ip = bin_ip:replace_char('0', p)
     end
   end
 
-
-  return table.concat( t )
+  local nextIP
+  nextIP, err = bin_to_ip( bin_ip )
+  if err then return nil, err end
+  return nextIP
 end
 
 
+
 ---
 -- Converts an IP address into a string representing the address as binary
 -- digits.
@@ -653,4 +914,57 @@
 
 end
 
+---
+-- Count the number of individual IPs in a set of ranges/IPs.
+-- @param ...  Any number of strings representing a range or IP address.
+-- Non-IP/domain strings count as 1.  Tables will be unpacked.  Anything else
+-- will simply be skipped over.
+-- @usage
+-- bin_string = ipOps.count_ips( "192.168.2.0/24", "4.4.4.4", ips, ipOps.get_ranges_from_ips(first, last) )
+-- @return     Number representing the total count of (valid) IPs
+count_ips = function( ... )
+  local ips = {...}
+
+  local count = 0
+  for _,range in ipairs(ips) do
+    if type(range) == "string" then
+      local ip, prefix
+      if range:match( "/" ) then
+        ip, prefix = range:match( "([%x%d:\.]+)/(%d+)" )
+        prefix = tonumber(prefix)
+
+        local maxPrefix = 32
+        if ip:match( ":" ) then maxPrefix = 128 end
+
+        if prefix ~= nil then count = count + 2 ^ (maxPrefix-prefix)
+        else                  count = count+1
+        end
+      elseif range:match( "-" ) then
+        -- let get_ranges_from_ips handle it
+        local first, last = range:match( "([%x%d:\.]+)%s*\-%s*([%x%d:\.]+)" )
+        local ranges = get_ranges_from_ips(first, last)
+
+        if ranges ~= nil then table.push( ips, ranges )
+        else                  count = count+1
+        end
+
+      else
+        count = count+1
+      end
+    elseif type(range) == "table" then
+      table.push( ips, range )
+    end
+  end
+
+  return count
+end
+
+string.replace_char = function(str, r, pos)
+  return str:sub(1, pos-1) .. r .. str:sub(pos+1)
+end
+
+table.push = function(tbl, itbl)
+  for _,i in pairs(itbl) do table.insert(tbl, i) end
+end
+
 return _ENV;
