Module:Sensitive IP addresses/list/validate

-- 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('* %s ', 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 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