Module:IP: Difference between revisions

From the Croc Wiki, the Croc encyclopedia
Jump to navigationJump to search
10,187 bytes added ,  7 years ago
need to use __index to have a unique key which is really private - if outside code gets uniqueKey with getmetatable then they can set it in another table's __eq metamethod, thus foiling isSubnetObject
(use some functions from Module:IPblock to start implementation)
(need to use __index to have a unique key which is really private - if outside code gets uniqueKey with getmetatable then they can set it in another table's __eq metamethod, thus foiling isSubnetObject)
 
(49 intermediate revisions by 2 users not shown)
Line 3:
 
-- Load modules
require('Module:No globals')
local bit32 = require('bit32')
local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType
local checkTypeMulti = libraryUtil.checkTypeMulti
local makeCheckSelfFunction = libraryUtil.makeCheckSelfFunction
 
-- Constants
Line 11 ⟶ 15:
 
--------------------------------------------------------------------------------
-- Helper functions
-- Remove items from these notes when satisfied they are ok.
--------------------------------------------------------------------------------
 
local function makeValidationFunction(className, isObjectFunc)
-- IPAddress does not need mt:__le(obj) as Lua evaluates (a <= b) as (not (b < a))
-- Make a function for validating a specific object.
-- and that is correct for IP addresses.
return function (methodName, argIdx, arg)
 
if not isObjectFunc(arg) then
-- IPAddress obj:getIP() needs thought.
error(string.format(
-- At first, it looked like the representation of the IP (the numbers).
"bad argument #%d to '%s' (not a valid %s object)",
-- But perhaps it is intended to be a string as in the testcases.
argIdx, methodName, className
-- Is it really just a copy of the input string?
), 3)
-- What if the input is " 1.2.3.4 " (with leading and trailing spaces which
end
-- the code ignores)?
end
-- Also, tostring(ip) for an IPv6 address will return the normalized string
end
-- which might be different from the input. Example:
-- input "1:02:003:0004:0:0:0:0" is "1:2:3:4::" after normalization.
 
--------------------------------------------------------------------------------
-- RawIP class
-- Functions from Module:IPblock follow.
-- Numeric representation of an IPv4 or IPv6 address. Used internally.
-- TODO Massage for style consistent with this module.
-- A RawIP object is constructed by adding data to a Collection object and
-- then giving it a new metatable. This is to avoid the memory overhead of
-- copying the data to a new table.
--------------------------------------------------------------------------------
 
local functionRawIP collection()= {}
RawIP.__index = RawIP
-- 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,
remove = function (self, pos)
if self.n > 0 and (pos == nil or (0 < pos and pos <= self.n)) then
self.n = self.n - 1
return table.remove(self, pos)
end
end,
sort = function (self, comp)
table.sort(self, comp)
end,
}
end
 
do
local function ipv6_string(ip)
-- Collection class.
-- Return a string equivalent to the given IPv6 address.
-- This is a table used to hold items.
local z1, z2 -- indices of run of zeros to be displayed as "::"
local zstart, zcountCollection
do
for i = 1, 9 do
local mt = {
-- Find left-most occurrence of longest run of two or more zeros.
__index = {
if i < 9 and ip[i] == 0 then
add = function (self, item)
if zstart then
zcount self.n = zcountself.n + 1
self[self.n] = item
else
zstart = iend,
zcountjoin = 1function (self, sep)
return table.concat(self, sep)
end
else end,
}
if zcount and zcount > 1 then
}
if not z1 or zcount > z2 - z1 + 1 then
Collection = function ()
z1 = zstart
return setmetatable({n = 0}, mt)
z2 = zstart + zcount - 1
end
end
 
-- Constructors
function RawIP.newFromIPv4(ipStr)
-- Return a RawIP object if ipStr is a valid IPv4 string. 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 nil
end
octets[i] = num
else
return nil
end
else
return nil
end
end
zstartlocal parts = nilCollection()
zcountfor i = nil1, 3, 2 do
parts:add(octets[i] * 256 + octets[i+1])
end
return setmetatable(parts, RawIP)
end
return nil
end
 
local parts = collection()
function RawIP.newFromIPv6(ipStr)
for i = 1, 8 do
-- Return a RawIP object if ipStr is a valid IPv6 string. Otherwise,
if z1 and z1 <= i and i <= z2 then
-- return nil.
if i == z1 then
ipStr = ipStr:match('^%s*(.-)%s*$')
if z1 == 1 or z2 == 8 then
local _, n = ipStr:gsub(':', ':')
if z1 == 1 and z2 == 8 then
if n < 7 then
return '::'
ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n))
end
end
parts:add(':')
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
parts:addlocal num = tonumber(''s, 16)
if num and 0 <= num and num <= 65535 then
parts[i] = num
else
return nil
end
end
end
return setmetatable(parts, RawIP)
else
parts:add(string.format('%x', ip[i]))
end
return nil
end
return table.concat(parts, ':')
end
 
local function ip_stringRawIP.newFromIP(ipipStr)
-- Return a stringnew equivalentRawIP toobject givenfrom IPeither addressan (IPv4 string or an IPv6).
-- string. If ipStr is not a valid IPv4 or IPv6 string, then return
if ip.n == 2 then
-- IPv4nil.
return RawIP.newFromIPv4(ipStr) or RawIP.newFromIPv6(ipStr)
local parts = {}
end
for i = 1, 2 do
 
local w = ip[i]
-- Methods
local q = i == 1 and 1 or 3
function RawIP:getVersion()
parts[q] = math.floor(w / 256)
-- Return a string with the version of the IP protocol we are using.
parts[q+1] = w % 256
return self.n == 2 and V4 or V6
end
 
function RawIP:isIPv4()
-- Return true if this is an IPv4 representation, and false otherwise.
return self.n == 2
end
 
function RawIP:isIPv6()
-- Return true if this is an IPv6 representation, and false otherwise.
return self.n == 8
end
 
function RawIP:getAdjacent(previous)
-- Return a RawIP object for an adjacent IP address. If previous is true
-- then the previous IP is returned; otherwise the next IP is returned.
-- Will wraparound:
-- next 255.255.255.255 → 0.0.0.0
-- ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff → ::
-- previous 0.0.0.0 → 255.255.255.255
-- :: → ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
local result = Collection()
result.n = self.n
local carry = previous and 0xffff or 1
for i = self.n, 1, -1 do
local sum = self[i] + carry
if sum >= 0x10000 then
carry = previous and 0x10000 or 1
sum = sum - 0x10000
else
carry = previous and 0xffff or 0
end
result[i] = sum
end
return table.concatsetmetatable(partsresult, '.'RawIP)
end
return ipv6_string(ip)
end
 
local function ipv4_addressRawIP:getPrefix(data, ip_strbitLength)
-- Return a RawIP object for the prefix of the current IP Address with a
-- If ip_str is a valid IPv4 string, store its parts in data and
-- bit length of bitLength.
-- return true. Otherwise, return false.
local result = Collection()
-- This representation is for compatibility with IPv6 addresses.
result.n = self.n
local octets = collection()
for i = 1, self.n do
local s = ip_str:match('^%s*(.-)%s*$') .. '.'
if bitLength > 0 then
for item in s:gmatch('(.-)%.') do
if bitLength >= 16 then
octets:add(item)
result[i] = self[i]
bitLength = bitLength - 16
else
result[i] = bit32.replace(self[i], 0, 0, 16 - bitLength)
bitLength = 0
end
else
result[i] = 0
end
end
return setmetatable(result, RawIP)
end
 
if octets.n == 4 then
function RawIP:getHighestHost(bitLength)
for i, s in ipairs(octets) do
-- Return a RawIP object for the highest IP with the prefix of length
if s:match('^%d+$') then
-- bitLength. In other words, the network (the most-significant bits)
local num = tonumber(s)
-- is the same as the current IP's, but the host bits (the
if 0 <= num and num <= 255 then
-- least-significant bits) are all set to 1.
if num > 0 and s:match('^0') then
local bits = self.n * 16
-- A redundant leading zero is for an IP in octal.
local width
return false
if bitLength <= 0 then
end
octets[i]width = numbits
elseif bitLength >= bits then
width = 0
else
width = bits - bitLength
end
local result = Collection()
result.n = self.n
for i = self.n, 1, -1 do
if width > 0 then
if width >= 16 then
result[i] = 0xffff
width = width - 16
else
result[i] = bit32.replace(self[i], 0xffff, 0, width)
return false
width = 0
end
else
returnresult[i] false= self[i]
end
end
return setmetatable(result, RawIP)
data.parts = collection()
end
for i = 1, 3, 2 do
 
data.parts:add(octets[i] * 256 + octets[i+1])
function RawIP:_makeIPv6String()
-- Return an IPv6 string representation of the object. Behavior is
-- undefined if the current object is IPv4.
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 self[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()
return true
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', self[i]))
end
end
return parts:join(':')
end
return false
end
 
local function ipv6_addressRawIP:_makeIPv4String(data, ip_str)
-- IfReturn ip_stran is a valid IPv6IPv4 string, storerepresentation itsof partsthe inobject. dataBehavior andis
-- undefined if the current object is IPv6.
-- return true. Otherwise, return false.
local parts = Collection()
ip_str = ip_str:match('^%s*(.-)%s*$')
for i = 1, 2 do
local _, n = ip_str:gsub(':', ':')
local w = self[i]
if n < 7 then
parts:add(math.floor(w / 256))
ip_str, n = ip_str:gsub('::', string.rep(':', 9 - n))
parts:add(w % 256)
end
return parts:join('.')
end
 
local parts = collection()
function RawIP:__tostring()
for item in (ip_str .. ':'):gmatch('(.-):') do
-- Return a string equivalent to given IP address (IPv4 or IPv6).
parts:add(item)
if self.n == 2 then
return self:_makeIPv4String()
else
return self:_makeIPv6String()
end
end
 
if parts.n == 8 then
function RawIP:__lt(obj)
for i, s in ipairs(parts) do
if sself.n == ''obj.n then
parts[for i] = 01, self.n do
if self[i] ~= obj[i] then
else
return self[i] < obj[i]
local num = tonumber('0x' .. s)
end
if num and 0 <= num and num <= 65535 then
end
parts[i] = num
return false
else
end
return self.n < obj.n
end
 
function RawIP:__eq(obj)
if self.n == obj.n then
for i = 1, self.n do
if self[i] ~= obj[i] then
return false
end
end
return true
end
return false
data.parts = parts
return true
end
return false
end
 
--------------------------------------------------------------------------------
-- Initialize private methods available to IPAddress and Subnet
--------------------------------------------------------------------------------
 
-- Both IPAddress and Subnet need access to each others' private constructor
-- functions. IPAddress must be able to make Subnet objects from CIDR strings
-- and from RawIP objects, and Subnet must be able to make IPAddress objects
-- from IP strings and from RawIP objects. These constructors must all be
-- private to ensure correct error levels and to stop other modules from having
-- to worry about RawIP objects. Because they are private, they must be
-- initialized here.
local makeIPAddress, makeIPAddressFromRaw, makeSubnet, makeSubnetFromRaw
 
-- IPAddress and Subnet also need to be able to validate Subnet and IPAddress
-- objects that they are passed as input, so initialize those functions here
-- as well.
local validateIPAddress, validateSubnet
 
--------------------------------------------------------------------------------
-- IPAddress class
-- Represents a single ipv4IPv4 or ipv6IPv6 address.
--------------------------------------------------------------------------------
 
Line 188 ⟶ 345:
 
do
-- dataKey is a unique key to access objects' internal data. This is needed
-- Initialize metatable
-- to access the RawIP objects contained in other IPAddress objects so that
local mt = {}
-- they can be compared with the current object's RawIP object. This data
-- is not available to other classes or other modules.
local dataKey = {}
 
-- Private static methods
-- Constructor
local function IPAddress.newisIPAddressObject(ipval)
return type(val) == 'table' and val[dataKey] ~= nil
checkType('IPAddress.new', 1, ip, 'string')
end
 
validateIPAddress = makeValidationFunction('IPAddress', isIPAddressObject)
 
-- Metamethods that don't need upvalues
local function ipEquals(ip1, ip2)
return ip1[dataKey].rawIP == ip2[dataKey].rawIP
end
 
local function ipLessThan(ip1, ip2)
return ip1[dataKey].rawIP < ip2[dataKey].rawIP
end
 
local function concatIP(ip, val)
return tostring(ip) .. tostring(val)
end
 
local function ipToString(ip)
return ip:getIP()
end
 
-- Constructors
makeIPAddressFromRaw = function (rawIP)
-- Constructs a new IPAddress object from a rawIP object. This function
-- is for internal use; it is called by IPAddress.new and from other
-- IPAddress methods, and should be available to the Subnet class, but
-- should not be available to other modules.
assert(type(rawIP) == 'table', 'rawIP was type ' .. type(rawIP) .. '; expected type table')
 
-- Set up structure
local obj = setmetatable({}, mt)
local data = {}
data.rawIP = rawIP
 
-- A function to check whether methods are called with a valid self
-- parameter.
local checkSelf = makeCheckSelfFunction(
'IP',
'ipAddress',
obj,
'IPAddress object'
)
 
-- Public methods
function obj:getIP()
checkSelf(self, 'getIP')
return data.ip
return tostring(data.rawIP)
end
 
function obj:getVersion()
checkSelf(self, 'getVersion')
return data.version
return data.rawIP:getVersion()
end
 
function obj:isIPv4()
checkSelf(self, 'isIPv4')
return data.version == V4
return data.rawIP:isIPv4()
end
 
function obj:isIPv6()
checkSelf(self, 'isIPv6')
return data.version == V6
return data.rawIP:isIPv6()
end
 
function obj:isInSubnet(subnet)
checkSelf(self, 'isInSubnet')
return false
local tp = type(subnet)
if tp == 'string' then
subnet = makeSubnet(subnet)
elseif tp == 'table' then
validateSubnet('isInSubnet', 1, subnet)
else
checkTypeMulti('isInSubnet', 1, subnet, {'string', 'table'})
end
return subnet:containsIP(self)
end
 
function obj:getSubnet(bitLength)
checkSelf(self, 'getSubnet')
checkType('getSubnet', 1, bitLength, 'number')
return makeSubnetFromRaw(data.rawIP, bitLength)
end
 
function obj:getNextIP()
checkSelf(self, 'getNextIP')
return makeIPAddressFromRaw(data.rawIP:getAdjacent())
end
 
function obj:getPreviousIP()
checkSelf(self, 'getPreviousIP')
return makeIPAddressFromRaw(data.rawIP:getAdjacent(true))
end
 
-- Metamethods
-- This is ugly but can't see how else to do it.
return setmetatable(obj, {
function obj:getIPParts()
__eq = ipEquals,
return data.parts
__lt = ipLessThan,
end
__concat = concatIP,
 
__tostring = ipToString,
-- Set initial values
__index = function (self, key)
data.ip = ip
-- If any code knows the unique data key, allow it to access
if ipv4_address(data, ip) then
-- the data table.
data.version = V4
elseif if ipv6_address(data,key ip)== dataKey then
return data.version = V6
else
error('invalid IP', 2)
end
 
return obj
end
 
-- Metamethods
function mt:__eq(obj)
local lhs, rhs = self:getIPParts(), obj:getIPParts()
if lhs.n == rhs.n then
for i = 1, lhs.n do
if lhs[i] ~= rhs[i] then
return false
end
end,
__metatable = false, -- Don't allow access to the metatable
return true
end})
return false
end
 
makeIPAddress = function mt:__lt(objip)
local lhs, rhsrawIP = self:getIPParts(), obj:getIPPartsRawIP.newFromIP(ip)
if lhs.nnot == rhs.nrawIP then
error(string.format("'%s' is an invalid IP address", ip), 3)
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.nmakeIPAddressFromRaw(rawIP)
end
 
function mt:__tostringIPAddress.new(ip)
checkType('IPAddress.new', 1, ip, 'string')
return ip_string(self:getIPParts())
return makeIPAddress(ip)
end
end
Line 278 ⟶ 480:
--------------------------------------------------------------------------------
-- Subnet class
-- Represents a block of ipv4IPv4 or ipv6IPv6 addresses.
--------------------------------------------------------------------------------
 
Line 284 ⟶ 486:
 
do
-- uniqueKey is a unique, private key used to test whether a given object
-- Initialize metatable
-- is a Subnet object.
local mt = {}
local uniqueKey = setmetatable({}, {__metatable = false})
 
-- Private static methods
-- Constructor
local function Subnet.newisSubnetObject(optionsval)
-- Return true if val is a Subnet object, and false otherwise.
-- Set up structure
return getmetatable(val) == uniqueKey
local obj = setmetatable({}, mt)
end
local data = {}
 
-- Function to validate subnet objects.
-- Public methods
-- Params:
function obj:addIPs(...)
-- methodName (string) - the name of the method being validated
end
-- argIdx (number) - the position of the argument in the argument list
-- arg - the argument to be validated
validateSubnet = makeValidationFunction('Subnet', isSubnetObject)
 
-- Metamethods that don't need upvalues
function obj:addIPsFromString(s)
local function subnetEquals(subnet1, subnet2)
end
return subnet1:getCIDR() == subnet2:getCIDR()
end
 
local function concatenateSubnets(subnet1, subnet2)
return tostring(subnet1) .. tostring(subnet2)
end
 
local function subnetToString(subnet)
return subnet:getCIDR()
end
 
-- Constructors
makeSubnetFromRaw = function (rawIP, bitLength)
-- Set up structure
local obj = {}
local data = {
rawIP = rawIP,
bitLength = bitLength,
}
 
-- A function to check whether methods are called with a valid self
-- parameter.
local checkSelf = makeCheckSelfFunction(
'IP',
'subnet',
obj,
'Subnet object'
)
 
-- Public methods
function obj:getPrefix()
checkSelf(self, 'getPrefix')
return '1.2.3.0'
if not data.prefix then
data.prefix = makeIPAddressFromRaw(
data.rawIP:getPrefix(data.bitLength)
)
end
return data.prefix
end
 
function obj:getHighestIP()
checkSelf(self, 'getHighestIP')
return '1.2.3.255'
if not data.highestIP then
data.highestIP = makeIPAddressFromRaw(
data.rawIP:getHighestHost(data.bitLength)
)
end
return data.highestIP
end
 
function obj:getBitLength()
checkSelf(self, 'getBitLength')
return 24
return data.bitLength
end
 
function obj:getCIDR()
checkSelf(self, 'getCIDR')
return string.format('%s/%d', self:getPrefix(), self:getBitLength())
return string.format(
'%s/%d',
tostring(self:getPrefix()), self:getBitLength()
)
end
 
function obj:getVersion()
checkSelf(self, 'getVersion')
return data.version
return data.rawIP:getVersion()
end
 
function obj:isIPv4()
checkSelf(self, 'isIPv4')
return data.version == V4
return data.rawIP:isIPv4()
end
 
function obj:isIPv6()
checkSelf(self, 'isIPv6')
return data.version == V6
return data.rawIP:isIPv6()
end
 
function obj:containsIP(ip)
checkSelf(self, 'containsIP')
local tp = type(ip)
if tp == 'string' then
ip = makeIPAddress(ip)
elseif tp == 'table' then
validateIPAddress('containsIP', 1, ip)
else
checkTypeMulti('containsIP', 1, ip, {'string', 'table'})
end
if self:getVersion() == ip:getVersion() then
return self:getPrefix() <= ip and ip <= self:getHighestIP()
end
return false
end
 
function obj:overlapsSubnet(subnet)
checkSelf(self, 'overlapsSubnet')
local tp = type(subnet)
if tp == 'string' then
subnet = makeSubnet(subnet)
elseif tp == 'table' then
validateSubnet('overlapsSubnet', 1, subnet)
else
checkTypeMulti('overlapsSubnet', 1, subnet, {'string', 'table'})
end
if self:getVersion() == subnet:getVersion() then
return (
subnet:getHighestIP() >= self:getPrefix() and
subnet:getPrefix() <= self:getHighestIP()
)
end
return false
end
 
function obj:walk()
-- Set initial values
checkSelf(self, 'walk')
data.version = V4
local started
local current = self:getPrefix()
local highest = self:getHighestIP()
return function ()
if not started then
started = true
return current
end
if current < highest then
current = current:getNextIP()
return current
end
end
end
 
return setmetatable(obj, {
__index = function (self, key)
if key == uniqueKey then
return true
end
end,
__eq = subnetEquals,
__concat = concatenateSubnets,
__tostring = subnetToString,
__metatable = false,
})
end
 
makeSubnet = function (cidr)
-- Metamethods
-- Return a Subnet object from a CIDR string. If the CIDR string is
function mt:__eq(obj)
-- invalid, throw an error.
return false
local lhs, rhs = cidr: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 and (n == 0 or not rhs:find('^0')) then
-- The right-hand side is a number between 0 and 32 (for IPv4)
-- or 0 and 128 (for IPv6) and doesn't have any leading zeroes.
local base = RawIP.newFromIP(lhs)
if base then
-- The left-hand side is a valid IP address.
local prefix = base:getPrefix(n)
if base == prefix then
-- The left-hand side is the lowest IP in the subnet.
return makeSubnetFromRaw(prefix, n)
end
end
end
end
error(string.format("'%s' is an invalid CIDR string", cidr), 3)
end
 
function mt:__tostringSubnet.new(cidr)
checkType('Subnet.new', 1, cidr, 'string')
return self:getCIDR()
return makeSubnet(cidr)
end
end
Cookies help us deliver our services. By using our services, you agree to our use of cookies.

Navigation menu