Module:IP: Difference between revisions
(test new IPAddress class structure to make the objects more memory-efficient) |
(try different format with underscores instead of private data) |
||
Line 163: | Line 163: | ||
end |
end |
||
return ipv6String(ip) |
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 |
end |
||
Line 237: | Line 171: | ||
local IPAddress = {} |
local IPAddress = {} |
||
IPAddress.__index = IPAddress |
|||
do |
do |
||
-- Private static methods |
|||
local dataKey = {} -- A unique key to access objects' internal data. |
|||
local function parseIPv4(ipStr) |
|||
-- If ipStr is a valid IPv4 string, return a collection of its parts. |
|||
-- Static private methods |
|||
-- Otherwise, return nil. |
|||
local function ipEquals(ip1, ip2) |
|||
-- This representation is for compatibility with IPv6 addresses. |
|||
local lhs = ip1[dataKey].parts |
|||
local |
local octets = collection() |
||
local s = ipStr:match('^%s*(.-)%s*$') .. '.' |
|||
if lhs.n == rhs.n then |
|||
for item in s:gmatch('(.-)%.') do |
|||
octets:add(item) |
|||
if lhs[i] ~= rhs[i] then |
|||
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 |
return false |
||
end |
end |
||
end |
end |
||
local parts = collection() |
|||
return true |
|||
for i = 1, 3, 2 do |
|||
parts:add(octets[i] * 256 + octets[i+1]) |
|||
end |
|||
return parts |
|||
end |
end |
||
return |
return nil |
||
end |
end |
||
local function |
local function parseIPv6(ipStr) |
||
-- If ipStr is a valid IPv6 string, return a collection of its parts. |
|||
local lhs = ip1[dataKey].parts |
|||
-- Otherwise, return nil. |
|||
local rhs = ip2[dataKey].parts |
|||
ipStr = ipStr:match('^%s*(.-)%s*$') |
|||
if lhs.n == rhs.n then |
|||
local _, n = ipStr:gsub(':', ':') |
|||
if n < 7 then |
|||
ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n)) |
|||
return lhs[i] < rhs[i] |
|||
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 |
||
end |
end |
||
return |
return parts |
||
end |
end |
||
return |
return nil |
||
end |
|||
local function concatIPs(ip1, ip2) |
|||
return tostring(ip1) .. tostring(ip2) |
|||
end |
|||
local function ipToString(ip) |
|||
return ipString(ip[dataKey].parts) |
|||
end |
end |
||
Line 281: | Line 243: | ||
function IPAddress.new(ip) |
function IPAddress.new(ip) |
||
checkType('IPAddress.new', 1, ip, 'string') |
checkType('IPAddress.new', 1, ip, 'string') |
||
local self = setmetatable({}, IPAddress) |
|||
-- Set up structure |
|||
local obj = {} |
|||
local data = {} |
|||
-- Set initial values |
-- Set initial values |
||
self._parts = parseIPv4(ip) or parseIPv6(ip) |
|||
if not (ipv4Address(data, ip) or ipv6Address(data, ip)) then |
|||
if not self._parts then |
|||
error('invalid IP', 2) |
error('invalid IP', 2) |
||
end |
end |
||
self._version = self._parts.n == 2 and V4 or V6 |
|||
return self |
|||
-- Metamethods |
|||
return setmetatable(obj, { |
|||
__eq = ipEquals, |
|||
__lt = ipLessThan, |
|||
__concat = concatIPs, |
|||
__tostring = ipToString, |
|||
__index = function (self, key) |
|||
if key == dataKey then |
|||
return data |
|||
else |
|||
return IPAddress[key] |
|||
end |
|||
end, |
|||
}) |
|||
end |
end |
||
-- Public methods |
-- Public methods |
||
function IPAddress:getIP() |
function IPAddress:getIP() |
||
return ipString(self |
return ipString(self._parts) |
||
end |
end |
||
function IPAddress:getVersion() |
function IPAddress:getVersion() |
||
return self |
return self._version |
||
end |
end |
||
function IPAddress:getHighestIP(bitLength) |
function IPAddress:getHighestIP(bitLength) |
||
return IPAddress.new(uniqueId, setHostBits(self |
return IPAddress.new(uniqueId, setHostBits(self._parts, bitLength)) |
||
end |
end |
||
function IPAddress:getPrefix(bitLength) |
function IPAddress:getPrefix(bitLength) |
||
return IPAddress.new(uniqueId, copyPrefix(self |
return IPAddress.new(uniqueId, copyPrefix(self._parts, bitLength)) |
||
end |
end |
||
function IPAddress:isIPv4() |
function IPAddress:isIPv4() |
||
return self |
return self._version == V4 |
||
end |
end |
||
function IPAddress:isIPv6() |
function IPAddress:isIPv6() |
||
return self |
return self._version == V6 |
||
end |
end |
||
Line 344: | Line 291: | ||
function IPAddress:getNextIP() |
function IPAddress:getNextIP() |
||
return IPAddress.new(uniqueId, copyChanged(self |
return IPAddress.new(uniqueId, copyChanged(self._parts)) |
||
end |
end |
||
function IPAddress:getPreviousIP() |
function IPAddress:getPreviousIP() |
||
return IPAddress.new(uniqueId, copyChanged(self |
return IPAddress.new(uniqueId, copyChanged(self._parts, true)) |
||
end |
end |
||
function IPAddress:__eq(obj) |
|||
local lhs = self._parts |
|||
local rhs = obj._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 |
|||
function IPAddress:__lt(ip1, ip2) |
|||
local lhs = self._parts |
|||
local rhs = obj._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 |
|||
function IPAddress:__concat(obj) |
|||
return tostring(self) .. tostring(obj) |
|||
end |
|||
function IPAddress:__tostring() |
|||
return ipString(self._parts) |
|||
end |
|||
end |
end |
||
Revision as of 01:26, July 18, 2016
This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is protected from editing. |
This Lua module is used in system messages. Changes to it can cause immediate changes to the Wikipedia user interface. To avoid major disruption, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Please discuss changes on the talk page before implementing them. |
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
--------------------------------------------------------------------------------
-- IPAddress class
-- Represents a single IPv4 or IPv6 address.
--------------------------------------------------------------------------------
local IPAddress = {}
IPAddress.__index = IPAddress
do
-- Private static methods
local function parseIPv4(ipStr)
-- If ipStr is a valid IPv4 string, return a collection of its parts.
-- Otherwise, return nil.
-- 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
local parts = collection()
for i = 1, 3, 2 do
parts:add(octets[i] * 256 + octets[i+1])
end
return parts
end
return nil
end
local function parseIPv6(ipStr)
-- If ipStr is a valid IPv6 string, return a collection of its parts.
-- Otherwise, return nil.
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
return parts
end
return nil
end
-- Constructor
function IPAddress.new(ip)
checkType('IPAddress.new', 1, ip, 'string')
local self = setmetatable({}, 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
return self
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
function IPAddress:__eq(obj)
local lhs = self._parts
local rhs = obj._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
function IPAddress:__lt(ip1, ip2)
local lhs = self._parts
local rhs = obj._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
function IPAddress:__concat(obj)
return tostring(self) .. tostring(obj)
end
function IPAddress:__tostring()
return ipString(self._parts)
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,
}