This module validates the data in Module:Sensitive IP addresses/list.
-- This module validates the data in [[Module:Sensitive IP addresses/list]].
-- Load modules
local mSIPA_API = require('Module:Sensitive IP addresses/API')
local Subnet = require('Module:IP').Subnet
-- Constants
local DATA_MODULE = 'Module:Sensitive IP addresses/list'
local p = {}
local function makeErrorLogger()
-- Return an object for formatting errors.
return {
errors = {},
addError = function (self, msg, ...)
table.insert(self.errors, string.format(msg, ...))
end,
addEntryTypeError = function (self, entryIdx, field, actual, expected)
self:addError(
'The %s field in data entry #%d was type %s (should be string or nil)',
field, entryIdx, actual, expected
)
end,
hasErrors = function (self)
return #self.errors > 0
end,
makeReport = function (self)
if #self.errors < 1 then
return 'No errors found'
else
local ret = {'Found the following errors:'}
for i, msg in ipairs(self.errors) do
ret[#ret + 1] = string.format('* <strong class="error">%s</strong>', msg)
end
return table.concat(ret, '\n')
end
end,
}
end
local function loadData(logger)
-- Load the data table, logging any errors in the process.
-- Check whether the data module can be successfully loaded.
local success, data = pcall(mw.loadData, DATA_MODULE)
if not success then
logger:addError('%s could not be parsed by mw.loadData; check for [[mw:LUAREF#mw.loadData|invalid data]]', DATA_MODULE)
return nil
end
-- Check that the data table is a table.
if type(data) ~= 'table' then
logger:addError('%s returned a %s; table expected', DATA_MODULE, type(data))
end
return data
end
local function checkDataStructure(logger, data)
-- Check the structure of the individual entries in the data table.
for dataIndex, subtable in ipairs(data) do
-- Check that subtables are tables.
if type(subtable) ~= 'table' then
logger:addError('Data entry #%d is not a table', dataIndex)
end
-- Check that we have required string fields.
for _, field in ipairs{'name', 'id', 'description'} do
if type(subtable[field]) ~= 'string' then
logger:addError(
"Missing field '%s' in data entry #%d",
field,
dataIndex
)
elseif subtable[field] == '' then
logger:addError(
"Blank field '%s' in data entry #%d",
field,
dataIndex
)
end
end
-- Check that optional string fields are strings if they are present.
for _, field in ipairs{'notes'} do
local val = subtable[field]
if val ~= nil and type(val) ~= 'string' then
logger:addEntryTypeError(dataIndex, field, type(val), 'string or nil')
end
end
-- Check that the reason is valid if it is present.
if subtable.reason ~= nil then
if type(subtable.reason) ~= 'string' then
logger:addEntryTypeError(
dataIndex,
'reason',
type(subtable.reason),
'string or nil'
)
elseif not mSIPA_API._isValidSensitivityReason(subtable.reason) then
logger:addError(
"The reason field in data entry #%d was invalid (should be '%s')",
dataIndex,
mSIPA_API._getSensitivityReasons("', '", "', or '")
)
end
end
-- Check IP range tables.
for i, field in ipairs{'ipv4Ranges', 'ipv6Ranges'} do
local ranges = subtable[field]
if ranges ~= nil then
if type(ranges) ~= 'table' then
logger:addEntryTypeError(dataIndex, field, type(ranges), 'table or nil')
else
for j, range in ipairs(ranges) do
if type(range) ~= 'string' then
logger:addError(
'Range #%d in the %s field of entry #%d was type %s (expected string)',
j, field, type(range)
)
elseif range == '' then
logger:addError(
'Range #%d in the %s field of entry #%d was a blank string',
j, field
)
end
end
end
end
end
end
end
local function makeSubnet(cidr)
-- Make a subnet object from a CIDR string. Returns a subnet object, or nil
-- if there were any errors.
local success, obj = pcall(Subnet.new, cidr)
if success then
return obj
end
end
local function checkDuplicateIds(logger, data)
-- Check that there are no duplicate IDs in the data.
local ids = {}
for dataIndex, subtable in ipairs(data) do
if ids[subtable.id] then
logger:addError(
"Data entry #%d (%s) and data entry #%d (%s) have duplicate ID '%s'",
ids[subtable.id],
data[ids[subtable.id]].name,
dataIndex,
subtable.name,
subtable.id
)
else
ids[subtable.id] = dataIndex
end
end
end
local function checkRanges(logger, data)
-- Check the ranges in the data table to make sure they are all valid and
-- that they don't overlap with each other. This function assumes that the
-- structure of the data table is valid.
-- Make an array of subnet data for easy comparison
local ranges = {
ipv4 = {},
ipv6 = {},
}
for dataIndex, subtable in ipairs(data) do
for i, field in ipairs{'ipv4Ranges', 'ipv6Ranges'} do
local cidrs = subtable[field]
if cidrs then
for j, cidr in ipairs(cidrs) do
local subnet = makeSubnet(cidr)
if subnet then
local ipVersion = field == 'ipv4Ranges' and 'IPv4' or 'IPv6'
local rangeKey = ipVersion:lower()
if ipVersion == subnet:getVersion() then
table.insert(ranges[rangeKey], {
dataIndex = dataIndex,
field = field,
rangeIndex = j,
subnet = subnet,
name = subtable.name,
})
else
logger:addError(
"Found %s CIDR string '%s' in range #%d in the %s field of entry #%d (%s); should be %s",
subnet:getVersion(), cidr, j, field, dataIndex, subtable.name, ipVersion
)
end
else
logger:addError(
"Invalid CIDR string '%s' in range #%d in the %s field of entry #%d (%s)",
cidr, j, field, dataIndex, subtable.name
)
end
end
end
end
end
-- Check for overlapping subnets
local nComparisons = 0
for ipVersion, versionData in pairs(ranges) do
local lim = #versionData
for i = 1, lim - 1 do
local subnetData1 = versionData[i]
for j = i + 1, lim do
local subnetData2 = versionData[j]
nComparisons = nComparisons + 1
if subnetData1.subnet:overlapsSubnet(subnetData2.subnet) then
logger:addError(
"%s range #%d '%s' in data entry #%d (%s) overlaps range #%d '%s' in data entry #%d (%s)",
ipVersion == 'ipv4' and 'IPv4' or 'IPv6',
subnetData1.rangeIndex,
subnetData1.subnet:getCIDR(),
subnetData1.dataIndex,
subnetData1.name,
subnetData2.rangeIndex,
subnetData2.subnet:getCIDR(),
subnetData2.dataIndex,
subnetData2.name
)
end
end
end
end
mw.log(nComparisons .. ' subnet comparisons performed')
end
function p.main()
local logger = makeErrorLogger()
local data = loadData(logger)
if logger:hasErrors() then
return logger:makeReport()
end
checkDataStructure(logger, data)
if logger:hasErrors() then
return logger:makeReport()
end
checkDuplicateIds(logger, data)
checkRanges(logger, data)
return logger:makeReport()
end
return p