Module:IP: Difference between revisions

From the Croc Wiki, the Croc encyclopedia
Jump to navigationJump to search
Content added Content deleted
(try different format with underscores instead of private data)
(try original format again)
Line 165: Line 165:
end
end


local function ipv4Address(data, ipStr)
--------------------------------------------------------------------------------
-- If ipStr is a valid IPv4 string, store its parts in data and
-- IPAddress class
-- return true. Otherwise, return false.
-- Represents a single IPv4 or IPv6 address.
-- This representation is for compatibility with IPv6 addresses.
--------------------------------------------------------------------------------
local octets = collection()

local s = ipStr:match('^%s*(.-)%s*$') .. '.'
local IPAddress = {}
for item in s:gmatch('(.-)%.') do
IPAddress.__index = IPAddress
octets:add(item)

end
do
if octets.n == 4 then
-- Private static methods
for i, s in ipairs(octets) do
local function parseIPv4(ipStr)
if s:match('^%d+$') then
-- If ipStr is a valid IPv4 string, return a collection of its parts.
local num = tonumber(s)
-- Otherwise, return nil.
if 0 <= num and num <= 255 then
-- This representation is for compatibility with IPv6 addresses.
if num > 0 and s:match('^0') then
local octets = collection()
-- A redundant leading zero is for an IP in octal.
local s = ipStr:match('^%s*(.-)%s*$') .. '.'
for item in s:gmatch('(.-)%.') do
octets:add(item)
end
if octets.n == 4 then
for i, s in ipairs(octets) do
if s:match('^%d+$') then
local num = tonumber(s)
if 0 <= num and num <= 255 then
if num > 0 and s:match('^0') then
-- A redundant leading zero is for an IP in octal.
return false
end
octets[i] = num
else
return false
return false
end
end
octets[i] = num
else
else
return false
return false
end
end
else
return false
end
end
local parts = collection()
for i = 1, 3, 2 do
parts:add(octets[i] * 256 + octets[i+1])
end
return parts
end
end
data.parts = collection()
return nil
for i = 1, 3, 2 do
data.parts:add(octets[i] * 256 + octets[i+1])
end
return true
end
end
return false
end


local function parseIPv6(ipStr)
local function ipv6Address(data, ipStr)
-- If ipStr is a valid IPv6 string, return a collection of its parts.
-- If ipStr is a valid IPv6 string, store its parts in data and
-- Otherwise, return nil.
-- return true. Otherwise, return false.
ipStr = ipStr:match('^%s*(.-)%s*$')
ipStr = ipStr:match('^%s*(.-)%s*$')
local _, n = ipStr:gsub(':', ':')
local _, n = ipStr:gsub(':', ':')
if n < 7 then
if n < 7 then
ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n))
ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n))
end
end
local parts = collection()
local parts = collection()
for item in (ipStr .. ':'):gmatch('(.-):') do
for item in (ipStr .. ':'):gmatch('(.-):') do
parts:add(item)
parts:add(item)
end
end
if parts.n == 8 then
if parts.n == 8 then
for i, s in ipairs(parts) do
for i, s in ipairs(parts) do
if s == '' then
if s == '' then
parts[i] = 0
parts[i] = 0
else
local num = tonumber('0x' .. s)
if num and 0 <= num and num <= 65535 then
parts[i] = num
else
else
return false
local num = tonumber('0x' .. s)
if num and 0 <= num and num <= 65535 then
parts[i] = num
else
return false
end
end
end
end
end
return parts
end
end
data.parts = parts
return nil
return true
end
end
return false
end


--------------------------------------------------------------------------------
-- Constructor
function IPAddress.new(ip)
-- IPAddress class
-- Represents a single IPv4 or IPv6 address.
checkType('IPAddress.new', 1, ip, 'string')
--------------------------------------------------------------------------------
local self = setmetatable({}, IPAddress)


local IPAddress = {}
-- Set initial values
self._parts = parseIPv4(ip) or parseIPv6(ip)
if not self._parts then
error('invalid IP', 2)
end
self._version = self._parts.n == 2 and V4 or V6


do
return self
local dataKey = {} -- A unique key to access objects' internal data.
end

-- Public methods
function IPAddress:getIP()
return ipString(self._parts)
end

function IPAddress:getVersion()
return self._version
end

function IPAddress:getHighestIP(bitLength)
return IPAddress.new(uniqueId, setHostBits(self._parts, bitLength))
end

function IPAddress:getPrefix(bitLength)
return IPAddress.new(uniqueId, copyPrefix(self._parts, bitLength))
end

function IPAddress:isIPv4()
return self._version == V4
end

function IPAddress:isIPv6()
return self._version == V6
end

function IPAddress:isInSubnet(subnet)
-- TODO Consider alternative of checking:
-- (ipFirst <= self and self <= ipLast)
if self:getVersion() == subnet:getVersion() then
local prefix = self:getPrefix(subnet:getBitLength())
return prefix == subnet:getPrefix()
end
return false
end

function IPAddress:getNextIP()
return IPAddress.new(uniqueId, copyChanged(self._parts))
end

function IPAddress:getPreviousIP()
return IPAddress.new(uniqueId, copyChanged(self._parts, true))
end


-- Static private methods
function IPAddress:__eq(obj)
local function ipEquals(ip1, ip2)
local lhs = self._parts
local rhs = obj._parts
local lhs = ip1[dataKey].parts
local rhs = ip2[dataKey].parts
if lhs.n == rhs.n then
if lhs.n == rhs.n then
for i = 1, lhs.n do
for i = 1, lhs.n do
Line 312: Line 256:
end
end


function IPAddress:__lt(ip1, ip2)
local function ipLessThan(ip1, ip2)
local lhs = self._parts
local lhs = ip1[dataKey].parts
local rhs = obj._parts
local rhs = ip2[dataKey].parts
if lhs.n == rhs.n then
if lhs.n == rhs.n then
for i = 1, lhs.n do
for i = 1, lhs.n do
Line 326: Line 270:
end
end


-- Constructor
function IPAddress:__concat(obj)
function IPAddress.new(ip)
return tostring(self) .. tostring(obj)
checkType('IPAddress.new', 1, ip, 'string')
end


-- Set up structure
function IPAddress:__tostring()
local obj = {}
return ipString(self._parts)
local data = {}

-- Set initial values
if not (ipv4Address(data, ip) or ipv6Address(data, ip)) then
error('invalid IP', 2)
end
data.version = data.parts.n == 2 and V4 or V6

-- Public methods
function obj:getIP()
return ipString(data.parts)
end

function obj:getVersion()
return data.version
end

function obj:getHighestIP(bitLength)
return IPAddress.new(uniqueId, setHostBits(data.parts, bitLength))
end

function obj:getPrefix(bitLength)
return IPAddress.new(uniqueId, copyPrefix(data.parts, bitLength))
end

function obj:isIPv4()
return data.version == V4
end

function obj:isIPv6()
return data.version == V6
end

function obj:isInSubnet(subnet)
-- TODO Consider alternative of checking:
-- (ipFirst <= self and self <= ipLast)
if self:getVersion() == subnet:getVersion() then
local prefix = self:getPrefix(subnet:getBitLength())
return prefix == subnet:getPrefix()
end
return false
end

function obj:getNextIP()
return IPAddress.new(uniqueId, copyChanged(data.parts))
end

function obj:getPreviousIP()
return IPAddress.new(uniqueId, copyChanged(data.parts, true))
end

-- Metamethods
return setmetatable(obj, {
__eq = ipEquals,
__lt = ipLessThan,
__index = function (self, key)
if key == dataKey then
return data
end
end,
__concat = function (self, obj)
return tostring(self) .. tostring(obj)
end,
__tostring = function (self)
return ipString(self:getIPParts())
end,
})
end
end
end
end

Revision as of 01:33, July 18, 2016

Module:IP is a library for working with IP addresses and subnets. It can handle both IPv4 and IPv6. The library exports four classes, IPAddress, Subnet, IPv4Collection, and IPv6Collection.

Loading the library

local IP = require('Module:IP')
local IPAddress = IP.IPAddress
local Subnet = IP.Subnet

IPAddress

The IPAddress class is used to work with single IP addresses. To create a new IPAddress object:

local ipAddress = IPAddress.new(ipString)

The ipString variable can be a valid IPv4 or IPv6 address.

Examples:

local ipv4Address = IPAddress.new('1.2.3.4')
local ipv6Address = IPAddress.new('2001:db8::ff00:12:3456')

IPAddress objects can be compared with relational operators:

-- Equality
IPAddress.new('1.2.3.4') == IPAddress.new('1.2.3.4') -- true
IPAddress.new('1.2.3.4') == IPAddress.new('1.2.3.5') -- false

-- Less than / greater than
IPAddress.new('1.2.3.4') < IPAddress.new('1.2.3.5')  -- true
IPAddress.new('1.2.3.4') > IPAddress.new('1.2.3.5')  -- false
IPAddress.new('1.2.3.4') <= IPAddress.new('1.2.3.5') -- true
IPAddress.new('1.2.3.4') <= IPAddress.new('1.2.3.4') -- true

You can use tostring on them (this is equivalent to using getIP):

tostring(IPAddress.new('1.2.3.4'))                -- "1.2.3.4"
tostring(IPAddress.new('2001:db8::ff00:12:3456')) -- "2001:db8::ff00:12:3456"

-- Expanded IPv6 addresses are abbreviated:
tostring(IPAddress.new('2001:db8:0:0:0:0:0:0'))   -- "2001:db8::"

You can also concatenate them:

IPAddress.new('1.2.3.4') .. ' foo'                   -- "1.2.3.4 foo"
IPAddress.new('1.2.3.4') .. IPAddress.new('5.6.7.8') -- "1.2.3.45.6.7.8"

IPAddress objects have several methods, outlined below.

getIP

ipAddress:getIP()

Returns a string representation of the IP address. IPv6 addresses are abbreviated if possible.

Examples:

IPAddress.new('1.2.3.4'):getIP()                -- "1.2.3.4"
IPAddress.new('2001:db8::ff00:12:3456'):getIP() -- "2001:db8::ff00:12:3456"
IPAddress.new('2001:db8:0:0:0:0:0:0'):getIP()   -- "2001:db8::"

getVersion

ipAddress:getVersion()

Returns the version of the IP protocol being used. This is "IPv4" for IPv4 addresses, and "IPv6" for IPv6 addresses.

Examples:

IPAddress.new('1.2.3.4'):getVersion()                -- "IPv4"
IPAddress.new('2001:db8::ff00:12:3456'):getVersion() -- "IPv6"

isIPv4

ipAddress:isIPv4()

Returns true if the IP address is an IPv4 address, and false otherwise.

Examples:

IPAddress.new('1.2.3.4'):isIPv4()                -- true
IPAddress.new('2001:db8::ff00:12:3456'):isIPv4() -- false

isIPv6

ipAddress:isIPv6()

Returns true if the IP address is an IPv6 address, and false otherwise.

Examples:

IPAddress.new('1.2.3.4'):isIPv6()                -- false
IPAddress.new('2001:db8::ff00:12:3456'):isIPv6() -- true

isInSubnet

ipAddress:isInSubnet(subnet)

Returns true if the IP address is in the subnet subnet, and false otherwise. subnet may be a Subnet object or a CIDR string.

Examples:

IPAddress.new('1.2.3.4'):isInSubnet('1.2.3.0/24')                             -- true
IPAddress.new('1.2.3.4'):isInSubnet('1.2.4.0/24')                             -- false
IPAddress.new('1.2.3.4'):isInSubnet(Subnet.new('1.2.3.0/24'))                 -- true
IPAddress.new('2001:db8::ff00:12:3456'):isInSubnet('2001:db8::ff00:12:0/112') -- true

getSubnet

ipAddress:getSubnet(bitLength)

Returns a Subnet object for the subnet with a bit length of bitLength which contains the current IP. The bitLength parameter must be an integer between 0 and 32 for IPv4 addresses, or an integer between 0 and 128 for IPv6 addresses.

Examples:

IPAddress.new('1.2.3.4'):getSubnet(24) -- Equivalent to Subnet.new('1.2.3.0/24')

getNextIP

ipAddress:getNextIP()

Returns a new IPAddress object equivalent to the current IP address incremented by one. The IPv4 address "255.255.255.255" rolls around to "0.0.0.0", and the IPv6 address "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" rolls around to "::".

Examples:

IPAddress.new('1.2.3.4'):getNextIP()                -- Equivalent to IPAddress.new('1.2.3.5')
IPAddress.new('2001:db8::ff00:12:3456'):getNextIP() -- Equivalent to IPAddress.new('2001:db8::ff00:12:3457')
IPAddress.new('255.255.255.255'):getNextIP()        -- Equivalent to IPAddress.new('0.0.0.0')

getPreviousIP

ipAddress:getPreviousIP()

Returns a new IPAddress object equivalent to the current IP address decremented by one. The IPv4 address "0.0.0.0" rolls around to "255.255.255.255", and the IPv6 address "::" rolls around to "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff".

Examples:

IPAddress.new('1.2.3.4'):getPreviousIP()                -- Equivalent to IPAddress.new('1.2.3.3')
IPAddress.new('2001:db8::ff00:12:3456'):getPreviousIP() -- Equivalent to IPAddress.new('2001:db8::ff00:12:3455')
IPAddress.new('0.0.0.0'):getPreviousIP()                -- Equivalent to IPAddress.new('255.255.255.255')

Subnet

The Subnet class is used to work with subnetworks of IPv4 or IPv6 addresses. To create a new Subnet object:

local subnet = Subnet.new(cidrString)

cidrString must be a valid IPv4 or IPv6 CIDR string.

Subnet objects can be compared for equality:

Subnet.new('1.2.3.0/24') == Subnet.new('1.2.3.0/24')                           -- true
Subnet.new('1.2.3.0/24') == Subnet.new('1.2.3.0/25')                           -- false
Subnet.new('1.2.3.0/24') == Subnet.new('2001:db8::ff00:12:0/112')              -- false
Subnet.new('2001:db8::ff00:12:0/112') == Subnet.new('2001:db8::ff00:12:0/112') -- true
Subnet.new('2001:db8:0:0:0:0:0:0/112') == Subnet.new('2001:db8::/112')         -- true

You can use tostring on them (this is equivalent to getCIDR):

tostring(Subnet.new('1.2.3.0/24'))               -- "1.2.3.0/24"
tostring(Subnet.new('2001:db8::ff00:12:0/112'))  -- "2001:db8::ff00:12:0/112"
tostring(Subnet.new('2001:db8:0:0:0:0:0:0/112')) -- "2001:db8::/112"

You can also concatenate them:

Subnet.new('1.2.3.0/24') .. ' foo'                   -- "1.2.3.0/24 foo"
Subnet.new('1.2.3.0/24') .. Subnet.new('4.5.6.0/24') -- "1.2.3.0/244.5.6.0/24"

Subnet objects have several methods, outlined below.

getPrefix

subnet:getPrefix()

Returns an IPAddress object for the lowest IP address in the subnet.

Examples:

Subnet.new('1.2.3.0/24'):getPrefix()              -- Equivalent to IPAddress.new('1.2.3.0')
Subnet.new('2001:db8::ff00:12:0/112'):getPrefix() -- Equivalent to IPAddress.new('2001:db8::ff00:12:0')

getHighestIP

subnet:getHighestIP()

Returns an IPAddress object for the highest IP address in the subnet.

Examples:

Subnet.new('1.2.3.0/24'):getHighestIP()              -- Equivalent to IPAddress.new('1.2.3.255')
Subnet.new('2001:db8::ff00:12:0/112'):getHighestIP() -- Equivalent to IPAddress.new('2001:db8::ff00:12:ffff')

getBitLength

subnet:getBitLength()

Returns the bit length of the subnet. This is an integer between 0 and 32 for IPv4 addresses, or an integer between 0 and 128 for IPv6 addresses.

Examples:

Subnet.new('1.2.3.0/24'):getBitLength()              -- 24
Subnet.new('2001:db8::ff00:12:0/112'):getBitLength() -- 112

getCIDR

subnet:getCIDR()

Returns a CIDR string representation of the subnet.

Examples:

Subnet.new('1.2.3.0/24'):getCIDR()               -- "1.2.3.0/24"
Subnet.new('2001:db8::ff00:12:0/112'):getCIDR()  -- "2001:db8::ff00:12:0/112"
Subnet.new('2001:db8:0:0:0:0:0:0/112'):getCIDR() -- "2001:db8::/112"

getVersion

subnet:getVersion()

Returns the version of the IP protocol being used. This is "IPv4" for IPv4 addresses, and "IPv6" for IPv6 addresses.

Examples:

Subnet.new('1.2.3.0/24'):getVersion()              -- "IPv4"
Subnet.new('2001:db8::ff00:12:0/112'):getVersion() -- "IPv6"

isIPv4

subnet:isIPv4()

Returns true if the subnet is using IPv4, and false otherwise.

Examples:

Subnet.new('1.2.3.0/24'):isIPv4()              -- true
Subnet.new('2001:db8::ff00:12:0/112'):isIPv4() -- false

isIPv6

subnet:isIPv6()

Returns true if the subnet is using IPv6, and false otherwise.

Examples:

Subnet.new('1.2.3.0/24'):isIPv6()              -- false
Subnet.new('2001:db8::ff00:12:0/112'):isIPv6() -- true

containsIP

subnet:containsIP(ip)

Returns true if the subnet contains the IP address ip, and false otherwise. ip can be an IP address string, or an IPAddress object.

Examples:

Subnet.new('1.2.3.0/24'):containsIP('1.2.3.4')                             -- true
Subnet.new('1.2.3.0/24'):containsIP('1.2.4.4')                             -- false
Subnet.new('1.2.3.0/24'):containsIP(IPAddress.new('1.2.3.4'))              -- true
Subnet.new('2001:db8::ff00:12:0/112'):containsIP('2001:db8::ff00:12:3456') -- true

overlapsSubnet

subnet:overlapsSubnet(subnet)

Returns true if the current subnet overlaps with subnet, and false otherwise. subnet can be a CIDR string or a subnet object.

Examples:

Subnet.new('1.2.3.0/24'):overlapsSubnet('1.2.0.0/16')                         -- true
Subnet.new('1.2.3.0/24'):overlapsSubnet('1.2.12.0/22')                        -- false
Subnet.new('1.2.3.0/24'):overlapsSubnet(Subnet.new('1.2.0.0/16'))             -- true
Subnet.new('2001:db8::ff00:12:0/112'):overlapsSubnet('2001:db8::ff00:0:0/96') -- true

walk

subnet:walk()

The walk method iterates over all of the IPAddress objects in the subnet.

Examples:

for ipAddress in Subnet.new('192.168.0.0/30'):walk() do
	mw.log(tostring(ipAddress))
end
-- 192.168.0.0
-- 192.168.0.1
-- 192.168.0.2
-- 192.168.0.3

IPv4Collection

The IPv4Collection class is used to work with several different IPv4 addresses and IPv4 subnets. To create a new IPv4Collection object:

local collection = IPv4Collection.new()

IPv4Collection objects have several methods, outlined below.

getVersion

collection:getVersion()

Returns the string "IPv4".

addIP

collection:addIP(ip)

Adds an IP to the collection. The IP can be either a string or an IPAddress object.

Examples:

collection:addIP('1.2.3.4')
collection:addIP(IPAddress.new('1.2.3.4'))

This method is chainable:

collection:addIP('1.2.3.4'):addIP('5.6.7.8')

addSubnet

collection:addSubnet(subnet)

Adds a subnet to the collection. The subnet can be either a CIDR string or a Subnet object.

Examples:

collection:addSubnet('1.2.3.0/24')
collection:addSubnet(Subnet.new('1.2.3.0/24'))

This method is chainable:

collection:addSubnet('1.2.0.0/24'):addSubnet('1.2.1.0/24')

addFromString

collection:addFromString(str)

Extracts any IPv4 addresses and IPv4 CIDR subnets from str and adds them to the collection. Any text that is not an IPv4 address or CIDR subnet is ignored.

Examples:

collection:addFromString('Add some IPs and subnets: 1.2.3.4 1.2.3.5 2001:0::f foo 1.2.4.0/24')

This method is chainable:

collection:addFromString('foo 1.2.3.4'):addFromString('bar 5.6.7.8')

containsIP

collection:containsIP(ip)

Returns true if the collection contains the specified IP; otherwise returns false. The ip parameter can be a string or an IPAddress object.

Examples:

collection:containsIP('1.2.3.4')
collection:containsIP(IPAddress.new('1.2.3.4'))

getRanges

collection:getRanges()

Returns a sorted array of IP pairs equivalent to the collection. Each IP pair is an array representing a contiguous range of IP addresses from pair[1] to pair[2] inclusive. pair[1] and pair[2] are IPAddress objects.

Examples:

collection:addSubnet('1.2.0.0/24')
collection:addSubnet('1.2.1.0/24')
collection:addSubnet('1.2.10.0/24')
mw.logObject(collection:getRanges())
-- Logs the following:
-- table#1 {
--   table#2 {
--     1.2.0.0,
--     1.2.1.255,
--   },
--   table#3 {
--     1.2.10.0,
--     1.2.10.255,
--   },
-- }

overlapsSubnet

collection:overlapsSubnet(subnet)

Returns true, obj if subnet overlaps this collection, where obj is the first IPAddress or Subnet object overlapping the subnet. Otherwise, returns false. subnet can be a CIDR string or a Subnet object.

Examples:

collection:addIP('1.2.3.4')
collection:overlapsSubnet('1.2.3.0/24') -- true, IPAddress.new('1.2.3.4')
collection:overlapsSubnet('1.2.4.0/24') -- false

IPv6Collection

The IPv6Collection class is used to work with several different IPv6 addresses and IPv6 subnets. IPv6Collection objects are directly analogous to IPv4Collection objects: they contain the same methods and work the same way, but all IP addresses and subnets added to it must be IPv6, not IPv4.

To create a new IPv6Collection object:

local collection = IPv6Collection.new()



-- IP library
-- This library contains classes for working with IP addresses and IP ranges.

-- Load modules
local bit32 = require('bit32')
local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType

-- Constants
local V4 = 'IPv4'
local V6 = 'IPv6'

------------------------------------------------------------------------
-- Functions from Module:IPblock follow.
-- TODO Massage following for style consistent with this module.
--------------------------------------------------------------------------------

local function collection()
	-- Return a table to hold items.
	return {
		n = 0,
		add = function (self, item)
			self.n = self.n + 1
			self[self.n] = item
		end,
		join = function (self, sep)
			return table.concat(self, sep)
		end,
		sort = function (self, comp)
			table.sort(self, comp)
		end,
	}
end

local function copyChanged(parts, down)
	-- Return a copy of IPv4 or IPv6 parts, incremented or decremented.
	-- Will wraparound:
	--   increment 255.255.255.255 → 0.0.0.0
	--             ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff → ::
	--   decrement 0.0.0.0 → 255.255.255.255
	--             :: → ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
	local result = { n = parts.n }
	local carry = down and 0xffff or 1
	for i = parts.n, 1, -1 do
		local sum = parts[i] + carry
		if sum >= 0x10000 then
			carry = down and 0x10000 or 1
			sum = sum - 0x10000
		else
			carry = down and 0xffff or 0
		end
		result[i] = sum
	end
	return result
end

local function copyPrefix(parts, length)
	-- Return a copy of IPv4 or IPv6 parts, masked to length.
	local result = { n = parts.n }
	for i = 1, parts.n do
		if length > 0 then
			if length >= 16 then
				result[i] = parts[i]
				length = length - 16
			else
				result[i] = bit32.band(parts[i],
					bit32.arshift(0xffff8000, length - 1))
				length = 0
			end
		else
			result[i] = 0
		end
	end
	return result
end

local function setHostBits(parts, length)
	-- Return a copy of IPv4 or IPv6 parts, with the least-significant bits
	-- (host bits) set to 1.
	-- The most-significant length bits identify the network.
	local bits = parts.n * 16
	local width
	if length <= 0 then
		width = bits
	elseif length >= bits then
		width = 0
	else
		width = bits - length
	end
	local result = { n = parts.n }
	for i = parts.n, 1, -1 do
		if width > 0 then
			if width >= 16 then
				result[i] = 0xffff
				width = width - 16
			else
				result[i] = bit32.replace(parts[i], 0xffff, width - 1, width)
				width = 0
			end
		else
			result[i] = parts[i]
		end
	end
	return result
end

local function ipv6String(ip)
	-- Return a string equivalent to the given IPv6 address.
	local z1, z2  -- indices of run of zeros to be displayed as "::"
	local zstart, zcount
	for i = 1, 9 do
		-- Find left-most occurrence of longest run of two or more zeros.
		if i < 9 and ip[i] == 0 then
			if zstart then
				zcount = zcount + 1
			else
				zstart = i
				zcount = 1
			end
		else
			if zcount and zcount > 1 then
				if not z1 or zcount > z2 - z1 + 1 then
					z1 = zstart
					z2 = zstart + zcount - 1
				end
			end
			zstart = nil
			zcount = nil
		end
	end
	local parts = collection()
	for i = 1, 8 do
		if z1 and z1 <= i and i <= z2 then
			if i == z1 then
				if z1 == 1 or z2 == 8 then
					if z1 == 1 and z2 == 8 then
						return '::'
					end
					parts:add(':')
				else
					parts:add('')
				end
			end
		else
			parts:add(string.format('%x', ip[i]))
		end
	end
	return table.concat(parts, ':')
end

local function ipString(ip)
	-- Return a string equivalent to given IP address (IPv4 or IPv6).
	if ip.n == 2 then
		-- IPv4.
		local parts = {}
		for i = 1, 2 do
			local w = ip[i]
			local q = i == 1 and 1 or 3
			parts[q] = math.floor(w / 256)
			parts[q+1] = w % 256
		end
		return table.concat(parts, '.')
	end
	return ipv6String(ip)
end

local function ipv4Address(data, ipStr)
	-- If ipStr is a valid IPv4 string, store its parts in data and
	-- return true. Otherwise, return false.
	-- This representation is for compatibility with IPv6 addresses.
	local octets = collection()
	local s = ipStr:match('^%s*(.-)%s*$') .. '.'
	for item in s:gmatch('(.-)%.') do
		octets:add(item)
	end
	if octets.n == 4 then
		for i, s in ipairs(octets) do
			if s:match('^%d+$') then
				local num = tonumber(s)
				if 0 <= num and num <= 255 then
					if num > 0 and s:match('^0') then
						-- A redundant leading zero is for an IP in octal.
						return false
					end
					octets[i] = num
				else
					return false
				end
			else
				return false
			end
		end
		data.parts = collection()
		for i = 1, 3, 2 do
			data.parts:add(octets[i] * 256 + octets[i+1])
		end
		return true
	end
	return false
end

local function ipv6Address(data, ipStr)
	-- If ipStr is a valid IPv6 string, store its parts in data and
	-- return true. Otherwise, return false.
	ipStr = ipStr:match('^%s*(.-)%s*$')
	local _, n = ipStr:gsub(':', ':')
	if n < 7 then
		ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n))
	end
	local parts = collection()
	for item in (ipStr .. ':'):gmatch('(.-):') do
		parts:add(item)
	end
	if parts.n == 8 then
		for i, s in ipairs(parts) do
			if s == '' then
				parts[i] = 0
			else
				local num = tonumber('0x' .. s)
				if num and 0 <= num and num <= 65535 then
					parts[i] = num
				else
					return false
				end
			end
		end
		data.parts = parts
		return true
	end
	return false
end

--------------------------------------------------------------------------------
-- IPAddress class
-- Represents a single IPv4 or IPv6 address.
--------------------------------------------------------------------------------

local IPAddress = {}

do
	local dataKey = {} -- A unique key to access objects' internal data.

	-- Static private methods
	local function ipEquals(ip1, ip2)
		local lhs = ip1[dataKey].parts
		local rhs = ip2[dataKey].parts
		if lhs.n == rhs.n then
			for i = 1, lhs.n do
				if lhs[i] ~= rhs[i] then
					return false
				end
			end
			return true
		end
		return false
	end

	local function ipLessThan(ip1, ip2)
		local lhs = ip1[dataKey].parts
		local rhs = ip2[dataKey].parts
		if lhs.n == rhs.n then
			for i = 1, lhs.n do
				if lhs[i] ~= rhs[i] then
					return lhs[i] < rhs[i]
				end
			end
			return false
		end
		return lhs.n < rhs.n
	end

	-- Constructor
	function IPAddress.new(ip)
		checkType('IPAddress.new', 1, ip, 'string')

		-- Set up structure
		local obj = {}
		local data = {}

		-- Set initial values
		if not (ipv4Address(data, ip) or ipv6Address(data, ip)) then
			error('invalid IP', 2)
		end
		data.version = data.parts.n == 2 and V4 or V6

		-- Public methods
		function obj:getIP()
			return ipString(data.parts)
		end

		function obj:getVersion()
			return data.version
		end

		function obj:getHighestIP(bitLength)
			return IPAddress.new(uniqueId, setHostBits(data.parts, bitLength))
		end

		function obj:getPrefix(bitLength)
			return IPAddress.new(uniqueId, copyPrefix(data.parts, bitLength))
		end

		function obj:isIPv4()
			return data.version == V4
		end

		function obj:isIPv6()
			return data.version == V6
		end

		function obj:isInSubnet(subnet)
			-- TODO Consider alternative of checking:
			--   (ipFirst <= self and self <= ipLast)
			if self:getVersion() == subnet:getVersion() then
				local prefix = self:getPrefix(subnet:getBitLength())
				return prefix == subnet:getPrefix()
			end
			return false
		end

		function obj:getNextIP()
			return IPAddress.new(uniqueId, copyChanged(data.parts))
		end

		function obj:getPreviousIP()
			return IPAddress.new(uniqueId, copyChanged(data.parts, true))
		end

		-- Metamethods
		return setmetatable(obj, {
			__eq = ipEquals,
			__lt = ipLessThan,
			__index = function (self, key)
				if key == dataKey then
					return data
				end
			end,
			__concat = function (self, obj)
				return tostring(self) .. tostring(obj)
			end,
			__tostring = function (self)
				return ipString(self:getIPParts())
			end,
		})
	end
end

local function makeSubnet(data, cidrStr)
	-- If cidrStr is a valid IPv4 or IPv6 CIDR specification, store its parts
	-- in data and return true. Otherwise, return false.
	local lhs, rhs = cidrStr:match('^%s*(.-)/(%d+)%s*$')
	if lhs then
		local bits = lhs:find(':', 1, true) and 128 or 32
		local n = tonumber(rhs)
		if n and n <= bits then
			local base = IPAddress.new(lhs)
			local prefix = base:getPrefix(n)
			if base == prefix then
				data.parts = base:getIPParts()
				data.bitLength = n
				data.prefix = prefix
				data.highestIP = base:getHighestIP(n)
				return true
			end
		end
	end
	return false
end

--------------------------------------------------------------------------------
-- Subnet class
-- Represents a block of IPv4 or IPv6 addresses.
--------------------------------------------------------------------------------

local Subnet = {}

do
	-- Initialize metatable
	local mt = {}

	-- Constructor
	function Subnet.new(cidr)
		-- Set up structure
		local obj = setmetatable({}, mt)
		local data = {}

		-- Public methods
		function obj:getPrefix()
			return data.prefix
		end

		function obj:getHighestIP()
			return data.highestIP
		end

		function obj:getBitLength()
			return data.bitLength
		end

		function obj:getCIDR()
			return string.format('%s/%d', self:getPrefix(), self:getBitLength())
		end

		function obj:getVersion()
			return data.version
		end

		function obj:isIPv4()
			return data.version == V4
		end

		function obj:isIPv6()
			return data.version == V6
		end

		function obj:containsIP(ip)
			-- TODO See ip:isInSubnet(subnet); use this technique there?
			if self:getVersion() == ip:getVersion() then
				return self:getPrefix() <= ip and ip <= self:getHighestIP()
			end
			return false
		end

		function obj:overlapsSubnet(subnet)
			if self:getVersion() == subnet:getVersion() then
				return (
					subnet:getHighestIP() >= self:getPrefix() and
					subnet:getPrefix() <= self:getHighestIP()
				)
			end
			return false
		end

		-- Set initial values
		checkType('Subnet.new', 1, cidr, 'string')
		if not makeSubnet(data, cidr) then
			error('invalid CIDR', 2)
		end
		data.version = data.parts.n == 2 and V4 or V6

		return obj
	end

	-- Metamethods
	function mt:__eq(obj)
		return self:getCIDR() == obj:getCIDR()
	end

	function mt:__tostring()
		return self:getCIDR()
	end
end

return {
	IPAddress = IPAddress,
	Subnet = Subnet,
}