# Validate hand-edited TOML as well as UI changes, before generating nft syntax. { lib }: data: let fail = message: throw "ports.toml: ${message}"; lanIP = value: builtins.isString value && builtins.match "10[.]0[.]0[.]([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])" value != null; normalize = entry: let name = entry.name or ""; proto = entry.protocol or ""; single = entry ? port; range = entry ? ports; spec = if single then toString entry.port else entry.ports or ""; validSpec = builtins.isString spec && builtins.match "[0-9]+(-[0-9]+)?" spec != null; bounds = map lib.toInt (lib.splitString "-" spec); first = builtins.head bounds; last = lib.last bounds; dest = entry.dest or data.dest_default; in if !builtins.isString name || builtins.match "[A-Za-z0-9][A-Za-z0-9 _().-]{0,79}" name == null then fail "invalid forward name" else if !(lib.elem proto [ "tcp" "udp" "both" ]) then fail "${name}: invalid protocol" else if single == range || !validSpec then fail "${name}: use one port or one port range" else if (single && !builtins.isInt entry.port) || first < 1 || last > 65535 || (range && first >= last) then fail "${name}: invalid port bounds" else if !lanIP dest then fail "${name}: destination must be a LAN IPv4 address" else { inherit name first last dest; protos = if proto == "both" then [ "tcp" "udp" ] else [ proto ]; }; entries = map normalize (data.forward or [ ]); names = map (e: e.name) entries; overlaps = a: b: a.first <= b.last && b.first <= a.last && lib.intersectLists a.protos b.protos != [ ]; noOverlaps = list: list == [ ] || ( lib.all (b: !overlaps (builtins.head list) b) (builtins.tail list) && noOverlaps (builtins.tail list)); reachable = port: lib.any (e: lib.elem "tcp" e.protos && e.first <= port && port <= e.last && e.dest == "10.0.0.1") entries; in if (data.dest_default or "") != "10.0.0.1" then fail "dest_default must remain 10.0.0.1" else if builtins.length names != builtins.length (lib.unique names) then fail "duplicate names" else if !noOverlaps entries then fail "overlapping protocol/port ranges" else if !lib.all reachable [ 22 443 ] then fail "TCP 22 and 443 must still reach the router" else builtins.deepSeq entries data