From 35f0f3b0a5a194361d65fe67564bd9b77fa13471 Mon Sep 17 00:00:00 2001 From: Nikkuss Date: Fri, 20 Feb 2026 20:07:52 +0000 Subject: [PATCH] init --- flake.lock | 26 ++ flake.nix | 53 ++++ lib/cidr.nix | 664 +++++++++++++++++++++++++++++++++++++++ lib/default.nix | 42 +++ lib/internal.nix | 56 ++++ lib/ip.nix | 392 +++++++++++++++++++++++ lib/iterate.nix | 389 +++++++++++++++++++++++ lib/validate.nix | 371 ++++++++++++++++++++++ tests/cidr-tests.nix | 315 +++++++++++++++++++ tests/default.nix | 43 +++ tests/ip-tests.nix | 205 ++++++++++++ tests/iterate-tests.nix | 195 ++++++++++++ tests/validate-tests.nix | 266 ++++++++++++++++ 13 files changed, 3017 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 lib/cidr.nix create mode 100644 lib/default.nix create mode 100644 lib/internal.nix create mode 100644 lib/ip.nix create mode 100644 lib/iterate.nix create mode 100644 lib/validate.nix create mode 100644 tests/cidr-tests.nix create mode 100644 tests/default.nix create mode 100644 tests/ip-tests.nix create mode 100644 tests/iterate-tests.nix create mode 100644 tests/validate-tests.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8371c64 --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs-lib": { + "locked": { + "lastModified": 1769909678, + "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "72716169fe93074c333e8d0173151350670b824c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2c14efb --- /dev/null +++ b/flake.nix @@ -0,0 +1,53 @@ +{ + description = "Pure Nix library for IPv4 operations"; + + inputs = { + nixpkgs-lib.url = "github:nix-community/nixpkgs.lib"; + }; + + outputs = + { self, nixpkgs-lib }: + let + # Import our library with nixpkgs.lib + ipLib = import ./lib { lib = nixpkgs-lib.lib; }; + + # Systems for checks and packages + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = f: nixpkgs-lib.lib.genAttrs systems f; + + in + { + # Primary library output + lib = ipLib; + + overlays = { + default = final: prev: { + lib = prev.lib.extend self.overlays.lib; + }; + + lib = final: prev: { + iputils = ipLib; + }; + }; + + # Tests + checks = forAllSystems ( + system: + let + lib = nixpkgs-lib.lib; + runTests = import ./tests { + inherit lib system; + ipLib = ipLib; + }; + in + { + tests = runTests; + } + ); + }; +} diff --git a/lib/cidr.nix b/lib/cidr.nix new file mode 100644 index 0000000..185a573 --- /dev/null +++ b/lib/cidr.nix @@ -0,0 +1,664 @@ +{ + lib, + internal, + ip, +}: + +let + inherit (builtins) + elemAt + filter + map + split + ; + inherit (lib) toInt; + inherit (internal) pow2 intRange; + +in +rec { + /** + Parse a CIDR string into { address; prefixLen }. + Also accepts attrsets (returns as-is). + + # Type + + ``` + parse :: (String | { address :: Int; prefixLen :: Int; }) -> { address :: Int; prefixLen :: Int; } + ``` + + # Example + + ```nix + parse "192.168.1.0/24" + => { address = 3232235776; prefixLen = 24; } + ``` + */ + parse = + cidr: + if builtins.isAttrs cidr then + cidr + else + let + parts = filter builtins.isString (split "/" cidr); + addr = ip.parse (elemAt parts 0); + prefix = toInt (elemAt parts 1); + in + { + address = addr; + prefixLen = prefix; + }; + + /** + Create a CIDR from IP and prefix length. + Returns { address; prefixLen }. + + # Type + + ``` + make :: (String | Int) -> Int -> { address :: Int; prefixLen :: Int; } + ``` + + # Example + + ```nix + make "192.168.1.0" 24 + => { address = 3232235776; prefixLen = 24; } + ``` + */ + make = addr: prefixLen: { + address = ip.parse addr; + inherit prefixLen; + }; + + /** + Format a CIDR as string. + + # Type + + ``` + format :: (String | { address :: Int; prefixLen :: Int; }) -> String + ``` + + # Example + + ```nix + format { address = 3232235776; prefixLen = 24; } + => "192.168.1.0/24" + ``` + */ + format = + cidr: + let + c = parse cidr; + in + "${ip.format c.address}/${toString c.prefixLen}"; + + /** + Calculate netmask from prefix length (as integer). + + # Type + + ``` + netmask :: Int -> Int + ``` + + # Example + + ```nix + netmask 24 + => 4294967040 + ``` + */ + netmask = + prefixLen: + if prefixLen == 0 then + 0 + else + let + # Create mask with prefixLen 1s followed by (32-prefixLen) 0s + hostBits = 32 - prefixLen; + in + 4294967295 - (pow2 hostBits - 1); + + /** + Calculate netmask from prefix length (as string). + + # Type + + ``` + netmaskStr :: Int -> String + ``` + + # Example + + ```nix + netmaskStr 24 + => "255.255.255.0" + ``` + */ + netmaskStr = prefixLen: ip.format (netmask prefixLen); + + /** + Calculate wildcard mask (inverse of netmask). + + # Type + + ``` + wildcard :: Int -> Int + ``` + + # Example + + ```nix + wildcard 24 + => 255 + ``` + */ + wildcard = + prefixLen: + let + hostBits = 32 - prefixLen; + in + pow2 hostBits - 1; + + /** + Calculate wildcard mask as string. + + # Type + + ``` + wildcardStr :: Int -> String + ``` + + # Example + + ```nix + wildcardStr 24 + => "0.0.0.255" + ``` + */ + wildcardStr = prefixLen: ip.format (wildcard prefixLen); + + /** + Get network address of a CIDR (as integer). + + # Type + + ``` + network :: (String | { address :: Int; prefixLen :: Int; }) -> Int + ``` + + # Example + + ```nix + network "192.168.1.50/24" + => 3232235776 + ``` + */ + network = + cidr: + let + c = parse cidr; + mask = netmask c.prefixLen; + in + builtins.bitAnd c.address mask; + + /** + Get network address of a CIDR (as string). + + # Type + + ``` + networkStr :: (String | { address :: Int; prefixLen :: Int; }) -> String + ``` + + # Example + + ```nix + networkStr "192.168.1.50/24" + => "192.168.1.0" + ``` + */ + networkStr = cidr: ip.format (network cidr); + + /** + Get broadcast address of a CIDR (as integer). + + # Type + + ``` + broadcast :: (String | { address :: Int; prefixLen :: Int; }) -> Int + ``` + + # Example + + ```nix + broadcast "192.168.1.0/24" + => 3232236031 + ``` + */ + broadcast = + cidr: + let + c = parse cidr; + wild = wildcard c.prefixLen; + net = network cidr; + in + builtins.bitOr net wild; + + /** + Get broadcast address of a CIDR (as string). + + # Type + + ``` + broadcastStr :: (String | { address :: Int; prefixLen :: Int; }) -> String + ``` + + # Example + + ```nix + broadcastStr "192.168.1.0/24" + => "192.168.1.255" + ``` + */ + broadcastStr = cidr: ip.format (broadcast cidr); + + /** + Get first usable host address (as integer). + For /31 and /32, returns network address. + + # Type + + ``` + firstHost :: (String | { address :: Int; prefixLen :: Int; }) -> Int + ``` + + # Example + + ```nix + firstHost "192.168.1.0/24" + => 3232235777 + ``` + */ + firstHost = + cidr: + let + c = parse cidr; + net = network cidr; + in + if c.prefixLen >= 31 then net else net + 1; + + /** + Get first usable host address (as string). + + # Type + + ``` + firstHostStr :: (String | { address :: Int; prefixLen :: Int; }) -> String + ``` + + # Example + + ```nix + firstHostStr "192.168.1.0/24" + => "192.168.1.1" + ``` + */ + firstHostStr = cidr: ip.format (firstHost cidr); + + /** + Get last usable host address (as integer). + For /31 and /32, returns broadcast address. + + # Type + + ``` + lastHost :: (String | { address :: Int; prefixLen :: Int; }) -> Int + ``` + + # Example + + ```nix + lastHost "192.168.1.0/24" + => 3232236030 + ``` + */ + lastHost = + cidr: + let + c = parse cidr; + bcast = broadcast cidr; + in + if c.prefixLen >= 31 then bcast else bcast - 1; + + /** + Get last usable host address (as string). + + # Type + + ``` + lastHostStr :: (String | { address :: Int; prefixLen :: Int; }) -> String + ``` + + # Example + + ```nix + lastHostStr "192.168.1.0/24" + => "192.168.1.254" + ``` + */ + lastHostStr = cidr: ip.format (lastHost cidr); + + /** + Get total number of addresses in CIDR. + + # Type + + ``` + size :: (String | { address :: Int; prefixLen :: Int; }) -> Int + ``` + + # Example + + ```nix + size "192.168.1.0/24" + => 256 + ``` + */ + size = + cidr: + let + c = parse cidr; + in + pow2 (32 - c.prefixLen); + + /** + Get number of usable host addresses. + Excludes network and broadcast for normal subnets. + /31 returns 2, /32 returns 1 (special cases per RFC 3021). + + # Type + + ``` + hostCount :: (String | { address :: Int; prefixLen :: Int; }) -> Int + ``` + + # Example + + ```nix + hostCount "192.168.1.0/24" + => 254 + ``` + */ + hostCount = + cidr: + let + c = parse cidr; + total = size cidr; + in + if c.prefixLen == 32 then + 1 + else if c.prefixLen == 31 then + 2 + else if total <= 2 then + 0 + else + total - 2; + + /** + Check if an IP address is within a CIDR. + + # Type + + ``` + contains :: (String | { address :: Int; prefixLen :: Int; }) -> (String | Int) -> Bool + ``` + + # Example + + ```nix + contains "10.0.0.0/8" "10.1.2.3" + => true + ``` + */ + contains = + cidr: addr: + let + ipInt = ip.parse addr; + net = network cidr; + bcast = broadcast cidr; + in + ipInt >= net && ipInt <= bcast; + + /** + Check if cidr2 is a subnet of cidr1. + + # Type + + ``` + isSubnetOf :: (String | { address :: Int; prefixLen :: Int; }) -> (String | { address :: Int; prefixLen :: Int; }) -> Bool + ``` + + # Example + + ```nix + isSubnetOf "10.0.0.0/8" "10.1.0.0/16" + => true + ``` + */ + isSubnetOf = + cidr1: cidr2: + let + c1 = parse cidr1; + c2 = parse cidr2; + net1 = network cidr1; + bcast1 = broadcast cidr1; + net2 = network cidr2; + bcast2 = broadcast cidr2; + in + c2.prefixLen >= c1.prefixLen && net2 >= net1 && bcast2 <= bcast1; + + /** + Get the Nth host in a CIDR (0-indexed from network address). + + # Type + + ``` + host :: (String | { address :: Int; prefixLen :: Int; }) -> Int -> Int + ``` + + # Example + + ```nix + host "192.168.1.0/24" 5 + => 3232235781 + ``` + */ + host = cidr: n: (network cidr) + n; + + /** + Get the Nth host in a CIDR (as string). + + # Type + + ``` + hostStr :: (String | { address :: Int; prefixLen :: Int; }) -> Int -> String + ``` + + # Example + + ```nix + hostStr "192.168.1.0/24" 5 + => "192.168.1.5" + ``` + */ + hostStr = cidr: n: ip.format (host cidr n); + + /** + Subdivide a CIDR into smaller subnets. + Returns the Nth subnet with the new prefix length. + + # Type + + ``` + subnet :: (String | { address :: Int; prefixLen :: Int; }) -> Int -> Int -> { address :: Int; prefixLen :: Int; } + ``` + + # Example + + ```nix + subnet "192.168.0.0/24" 26 0 + => { address = 3232235520; prefixLen = 26; } + ``` + */ + subnet = + cidr: newPrefix: n: + let + net = network cidr; + subnetSize = pow2 (32 - newPrefix); + subnetStart = net + (n * subnetSize); + in + { + address = subnetStart; + prefixLen = newPrefix; + }; + + /** + Get all subnets when subdividing. + + # Type + + ``` + subnets :: (String | { address :: Int; prefixLen :: Int; }) -> Int -> [{ address :: Int; prefixLen :: Int; }] + ``` + + # Example + + ```nix + subnets "192.168.0.0/24" 26 + => [ { address = 3232235520; prefixLen = 26; } ... ] # 4 subnets + ``` + */ + subnets = + cidr: newPrefix: + let + c = parse cidr; + numSubnets = pow2 (newPrefix - c.prefixLen); + in + map (n: subnet cidr newPrefix n) (intRange 0 (numSubnets - 1)); + + /** + Get all subnets as strings. + + # Type + + ``` + subnetsStr :: (String | { address :: Int; prefixLen :: Int; }) -> Int -> [String] + ``` + + # Example + + ```nix + subnetsStr "192.168.0.0/24" 26 + => [ "192.168.0.0/26" "192.168.0.64/26" "192.168.0.128/26" "192.168.0.192/26" ] + ``` + */ + subnetsStr = cidr: newPrefix: map format (subnets cidr newPrefix); + + /** + Check if two CIDRs overlap. + + # Type + + ``` + overlaps :: (String | { address :: Int; prefixLen :: Int; }) -> (String | { address :: Int; prefixLen :: Int; }) -> Bool + ``` + + # Example + + ```nix + overlaps "192.168.1.0/24" "192.168.1.128/25" + => true + ``` + */ + overlaps = + cidr1: cidr2: + let + net1 = network cidr1; + bcast1 = broadcast cidr1; + net2 = network cidr2; + bcast2 = broadcast cidr2; + in + !(bcast1 < net2 || bcast2 < net1); + + /** + Get prefix length from number of required hosts. + + # Type + + ``` + prefixForHosts :: Int -> Int + ``` + + # Example + + ```nix + prefixForHosts 100 + => 25 + ``` + */ + prefixForHosts = + hosts: + let + findPrefix = + prefix: + if prefix < 0 then + 0 + else if + hostCount { + address = 0; + prefixLen = prefix; + } >= hosts + then + prefix + else + findPrefix (prefix - 1); + in + findPrefix 32; + + /** + Get prefix length from number of required addresses. + + # Type + + ``` + prefixForSize :: Int -> Int + ``` + + # Example + + ```nix + prefixForSize 256 + => 24 + ``` + */ + prefixForSize = + addresses: + let + findPrefix = + prefix: + if prefix < 0 then + 0 + else if + size { + address = 0; + prefixLen = prefix; + } >= addresses + then + prefix + else + findPrefix (prefix - 1); + in + findPrefix 32; +} diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..47ddc8e --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,42 @@ +{ lib }: +let + # Internal helpers + internal = import ./internal.nix { inherit lib; }; + + # IP address operations + ip = import ./ip.nix { inherit lib internal; }; + + # CIDR/subnet operations + cidr = import ./cidr.nix { inherit lib internal ip; }; + + # Validation functions + validate = import ./validate.nix { + inherit + lib + internal + ip + cidr + ; + }; + + # Range/iteration functions + iterate = import ./iterate.nix { + inherit + lib + internal + ip + cidr + ; + }; + +in +{ + inherit + ip + cidr + validate + iterate + ; + + _internal = internal; +} diff --git a/lib/internal.nix b/lib/internal.nix new file mode 100644 index 0000000..8f4d349 --- /dev/null +++ b/lib/internal.nix @@ -0,0 +1,56 @@ +{ lib }: +let + inherit (builtins) elemAt genList; + inherit (lib) mod; +in +rec { + # Power of 2 calculation + # pow2 n returns 2^n + pow2 = + n: + if n == 0 then + 1 + else if n == 1 then + 2 + else + let + half = n / 2; + halfPow = pow2 half; + in + halfPow * halfPow * (if mod n 2 == 1 then 2 else 1); + + # Convert list of 4 octets to 32-bit integer + # [192 168 1 1] -> 3232235777 + octetsToInt = + octets: + let + a = elemAt octets 0; + b = elemAt octets 1; + c = elemAt octets 2; + d = elemAt octets 3; + in + a * 16777216 + b * 65536 + c * 256 + d; + + # Convert 32-bit integer to list of 4 octets + # 3232235777 -> [192 168 1 1] + intToOctets = + ip: + let + a = ip / 16777216; + remainder1 = mod ip 16777216; + b = remainder1 / 65536; + remainder2 = mod remainder1 65536; + c = remainder2 / 256; + d = mod remainder2 256; + in + [ + a + b + c + d + ]; + + # Generate a list of integers from start to end (inclusive) + # More efficient than recursive approach for large ranges + intRange = start: end: if end < start then [ ] else genList (i: start + i) (end - start + 1); +} diff --git a/lib/ip.nix b/lib/ip.nix new file mode 100644 index 0000000..a63d515 --- /dev/null +++ b/lib/ip.nix @@ -0,0 +1,392 @@ +{ lib, internal }: + +let + inherit (builtins) + filter + map + split + ; + inherit (lib) concatStringsSep toInt; + inherit (internal) octetsToInt intToOctets; +in +rec { + /** + Parse an IP address string to a 32-bit integer. + Also accepts integers (returns as-is). + + # Type + + ``` + parse :: (String | Int) -> Int + ``` + + # Example + + ```nix + parse "192.168.1.1" + => 3232235777 + ``` + */ + parse = + ip: + if builtins.isInt ip then + ip + else + let + parts = filter builtins.isString (split "\\." ip); + octets = map toInt parts; + in + octetsToInt octets; + + /** + Format a 32-bit integer as an IP address string. + Also accepts strings (returns as-is after validation). + + # Type + + ``` + format :: (Int | String) -> String + ``` + + # Example + + ```nix + format 3232235777 + => "192.168.1.1" + ``` + */ + format = + ip: if builtins.isString ip then ip else concatStringsSep "." (map toString (intToOctets ip)); + + /** + Convert IP to list of octets. + + # Type + + ``` + toOctets :: (String | Int) -> [Int] + ``` + + # Example + + ```nix + toOctets "192.168.1.1" + => [192 168 1 1] + ``` + */ + toOctets = ip: intToOctets (parse ip); + + /** + Convert list of octets to integer. + + # Type + + ``` + fromOctets :: [Int] -> Int + ``` + + # Example + + ```nix + fromOctets [192 168 1 1] + => 3232235777 + ``` + */ + fromOctets = octetsToInt; + + /** + Add offset to an IP address. + Returns integer; use format for string. + + # Type + + ``` + add :: (String | Int) -> Int -> Int + ``` + + # Example + + ```nix + add "192.168.1.1" 10 + => 3232235787 + ``` + */ + add = ip: offset: (parse ip) + offset; + + /** + Add offset to an IP address, return as string. + + # Type + + ``` + addStr :: (String | Int) -> Int -> String + ``` + + # Example + + ```nix + addStr "192.168.1.1" 10 + => "192.168.1.11" + ``` + */ + addStr = ip: offset: format (add ip offset); + + /** + Subtract offset from an IP address. + Returns integer; use format for string. + + # Type + + ``` + subtract :: (String | Int) -> Int -> Int + ``` + + # Example + + ```nix + subtract "192.168.1.10" 5 + => 3232235781 + ``` + */ + subtract = ip: offset: (parse ip) - offset; + + /** + Subtract offset from an IP address, return as string. + + # Type + + ``` + subtractStr :: (String | Int) -> Int -> String + ``` + + # Example + + ```nix + subtractStr "192.168.1.10" 5 + => "192.168.1.5" + ``` + */ + subtractStr = ip: offset: format (subtract ip offset); + + /** + Calculate difference between two IPs (ip1 - ip2). + + # Type + + ``` + diff :: (String | Int) -> (String | Int) -> Int + ``` + + # Example + + ```nix + diff "192.168.1.10" "192.168.1.1" + => 9 + ``` + */ + diff = ip1: ip2: (parse ip1) - (parse ip2); + + /** + Compare two IP addresses. + Returns -1 if ip1 < ip2, 0 if equal, 1 if ip1 > ip2. + + # Type + + ``` + compare :: (String | Int) -> (String | Int) -> Int + ``` + + # Example + + ```nix + compare "192.168.1.1" "192.168.1.10" + => -1 + ``` + */ + compare = + ip1: ip2: + let + a = parse ip1; + b = parse ip2; + in + if a < b then + -1 + else if a > b then + 1 + else + 0; + + /** + Less than comparison. + + # Type + + ``` + lt :: (String | Int) -> (String | Int) -> Bool + ``` + + # Example + + ```nix + lt "192.168.1.1" "192.168.1.10" + => true + ``` + */ + lt = ip1: ip2: (parse ip1) < (parse ip2); + + /** + Less than or equal comparison. + + # Type + + ``` + lte :: (String | Int) -> (String | Int) -> Bool + ``` + + # Example + + ```nix + lte "192.168.1.1" "192.168.1.1" + => true + ``` + */ + lte = ip1: ip2: (parse ip1) <= (parse ip2); + + /** + Greater than comparison. + + # Type + + ``` + gt :: (String | Int) -> (String | Int) -> Bool + ``` + + # Example + + ```nix + gt "192.168.1.10" "192.168.1.1" + => true + ``` + */ + gt = ip1: ip2: (parse ip1) > (parse ip2); + + /** + Greater than or equal comparison. + + # Type + + ``` + gte :: (String | Int) -> (String | Int) -> Bool + ``` + + # Example + + ```nix + gte "192.168.1.1" "192.168.1.1" + => true + ``` + */ + gte = ip1: ip2: (parse ip1) >= (parse ip2); + + /** + Equality comparison. + + # Type + + ``` + eq :: (String | Int) -> (String | Int) -> Bool + ``` + + # Example + + ```nix + eq "192.168.1.1" 3232235777 + => true + ``` + */ + eq = ip1: ip2: (parse ip1) == (parse ip2); + + /** + Get minimum of two IPs (returns integer). + + # Type + + ``` + min :: (String | Int) -> (String | Int) -> Int + ``` + + # Example + + ```nix + min "192.168.1.10" "192.168.1.1" + => 3232235777 + ``` + */ + min = + ip1: ip2: + let + a = parse ip1; + b = parse ip2; + in + if a < b then a else b; + + /** + Get minimum of two IPs (returns string). + + # Type + + ``` + minStr :: (String | Int) -> (String | Int) -> String + ``` + + # Example + + ```nix + minStr "192.168.1.10" "192.168.1.1" + => "192.168.1.1" + ``` + */ + minStr = ip1: ip2: format (min ip1 ip2); + + /** + Get maximum of two IPs (returns integer). + + # Type + + ``` + max :: (String | Int) -> (String | Int) -> Int + ``` + + # Example + + ```nix + max "192.168.1.1" "192.168.1.10" + => 3232235786 + ``` + */ + max = + ip1: ip2: + let + a = parse ip1; + b = parse ip2; + in + if a > b then a else b; + + /** + Get maximum of two IPs (returns string). + + # Type + + ``` + maxStr :: (String | Int) -> (String | Int) -> String + ``` + + # Example + + ```nix + maxStr "192.168.1.1" "192.168.1.10" + => "192.168.1.10" + ``` + */ + maxStr = ip1: ip2: format (max ip1 ip2); +} diff --git a/lib/iterate.nix b/lib/iterate.nix new file mode 100644 index 0000000..efd83d5 --- /dev/null +++ b/lib/iterate.nix @@ -0,0 +1,389 @@ +{ + lib, + internal, + ip, + cidr, +}: +let + inherit (builtins) genList; + inherit (lib) map; +in +rec { + /** + Generate a list of IP integers from start to end (inclusive). + + # Type + + ``` + range :: (String | Int) -> (String | Int) -> [Int] + ``` + + # Example + + ```nix + range "192.168.1.1" "192.168.1.3" + => [ 3232235777 3232235778 3232235779 ] + ``` + */ + range = + start: end: + let + startInt = ip.parse start; + endInt = ip.parse end; + count = endInt - startInt + 1; + in + if endInt < startInt then [ ] else genList (i: startInt + i) count; + + /** + Generate a list of IP strings from start to end (inclusive). + + # Type + + ``` + rangeStr :: (String | Int) -> (String | Int) -> [String] + ``` + + # Example + + ```nix + rangeStr "192.168.1.1" "192.168.1.3" + => [ "192.168.1.1" "192.168.1.2" "192.168.1.3" ] + ``` + */ + rangeStr = start: end: map ip.format (range start end); + + /** + Get all addresses in a CIDR (as integers). + + # Type + + ``` + cidrAddresses :: (String | { address :: Int; prefixLen :: Int; }) -> [Int] + ``` + + # Example + + ```nix + cidrAddresses "192.168.1.0/30" + => [ 3232235776 3232235777 3232235778 3232235779 ] + ``` + */ + cidrAddresses = + cidrStr: + let + net = cidr.network cidrStr; + bcast = cidr.broadcast cidrStr; + in + range net bcast; + + /** + Get all addresses in a CIDR (as strings). + + # Type + + ``` + cidrAddressesStr :: (String | { address :: Int; prefixLen :: Int; }) -> [String] + ``` + + # Example + + ```nix + cidrAddressesStr "192.168.1.0/30" + => [ "192.168.1.0" "192.168.1.1" "192.168.1.2" "192.168.1.3" ] + ``` + */ + cidrAddressesStr = cidrStr: map ip.format (cidrAddresses cidrStr); + + /** + Get all usable host addresses in a CIDR (as integers). + Excludes network and broadcast for /30 and larger. + + # Type + + ``` + cidrHosts :: (String | { address :: Int; prefixLen :: Int; }) -> [Int] + ``` + + # Example + + ```nix + cidrHosts "192.168.1.0/30" + => [ 3232235777 3232235778 ] + ``` + */ + cidrHosts = + cidrStr: + let + c = cidr.parse cidrStr; + first = cidr.firstHost cidrStr; + last = cidr.lastHost cidrStr; + in + if c.prefixLen >= 31 then range first last else range first last; + + /** + Get all usable host addresses in a CIDR (as strings). + + # Type + + ``` + cidrHostsStr :: (String | { address :: Int; prefixLen :: Int; }) -> [String] + ``` + + # Example + + ```nix + cidrHostsStr "192.168.1.0/30" + => [ "192.168.1.1" "192.168.1.2" ] + ``` + */ + cidrHostsStr = cidrStr: map ip.format (cidrHosts cidrStr); + + /** + Check if an IP is within a range (inclusive). + + # Type + + ``` + inRange :: (String | Int) -> (String | Int) -> (String | Int) -> Bool + ``` + + # Example + + ```nix + inRange "192.168.1.1" "192.168.1.10" "192.168.1.5" + => true + ``` + */ + inRange = + start: end: addr: + let + startInt = ip.parse start; + endInt = ip.parse end; + addrInt = ip.parse addr; + in + addrInt >= startInt && addrInt <= endInt; + + /** + Count IPs in a range (inclusive). + + # Type + + ``` + countRange :: (String | Int) -> (String | Int) -> Int + ``` + + # Example + + ```nix + countRange "192.168.1.1" "192.168.1.10" + => 10 + ``` + */ + countRange = + start: end: + let + startInt = ip.parse start; + endInt = ip.parse end; + in + if endInt < startInt then 0 else endInt - startInt + 1; + + /** + Map a function over an IP range. + More memory-efficient than generating the full list first. + + # Type + + ``` + mapRange :: (Int -> a) -> (String | Int) -> (String | Int) -> [a] + ``` + + # Example + + ```nix + mapRange (i: i * 2) "0.0.0.1" "0.0.0.3" + => [ 2 4 6 ] + ``` + */ + mapRange = + f: start: end: + let + startInt = ip.parse start; + endInt = ip.parse end; + count = endInt - startInt + 1; + in + if endInt < startInt then [ ] else genList (i: f (startInt + i)) count; + + /** + Map a function over a CIDR's addresses. + + # Type + + ``` + mapCidr :: (Int -> a) -> (String | { address :: Int; prefixLen :: Int; }) -> [a] + ``` + + # Example + + ```nix + mapCidr ip.format "192.168.1.0/30" + => [ "192.168.1.0" "192.168.1.1" "192.168.1.2" "192.168.1.3" ] + ``` + */ + mapCidr = + f: cidrStr: + let + net = cidr.network cidrStr; + bcast = cidr.broadcast cidrStr; + in + mapRange f net bcast; + + /** + Map a function over a CIDR's usable hosts. + + # Type + + ``` + mapCidrHosts :: (Int -> a) -> (String | { address :: Int; prefixLen :: Int; }) -> [a] + ``` + + # Example + + ```nix + mapCidrHosts ip.format "192.168.1.0/30" + => [ "192.168.1.1" "192.168.1.2" ] + ``` + */ + mapCidrHosts = + f: cidrStr: + let + first = cidr.firstHost cidrStr; + last = cidr.lastHost cidrStr; + in + mapRange f first last; + + /** + Filter IPs in a range. + + # Type + + ``` + filterRange :: (Int -> Bool) -> (String | Int) -> (String | Int) -> [Int] + ``` + + # Example + + ```nix + filterRange (i: lib.mod i 2 == 0) "0.0.0.1" "0.0.0.5" + => [ 2 4 ] + ``` + */ + filterRange = + pred: start: end: + let + startInt = ip.parse start; + endInt = ip.parse end; + count = endInt - startInt + 1; + allIps = if endInt < startInt then [ ] else genList (i: startInt + i) count; + in + builtins.filter pred allIps; + + /** + Find first IP in range matching predicate. + + # Type + + ``` + findInRange :: (Int -> Bool) -> (String | Int) -> (String | Int) -> (Int | Null) + ``` + + # Example + + ```nix + findInRange (i: lib.mod i 2 == 0) "0.0.0.1" "0.0.0.5" + => 2 + ``` + */ + findInRange = + pred: start: end: + let + startInt = ip.parse start; + endInt = ip.parse end; + find = + current: + if current > endInt then + null + else if pred current then + current + else + find (current + 1); + in + if endInt < startInt then null else find startInt; + + /** + Find first IP in range matching predicate (as string). + + # Type + + ``` + findInRangeStr :: (Int -> Bool) -> (String | Int) -> (String | Int) -> (String | Null) + ``` + + # Example + + ```nix + findInRangeStr (i: lib.mod i 2 == 0) "0.0.0.1" "0.0.0.5" + => "0.0.0.2" + ``` + */ + findInRangeStr = + pred: start: end: + let + result = findInRange pred start end; + in + if result == null then null else ip.format result; + + /** + Take first n IPs from a range. + + # Type + + ``` + takeRange :: Int -> (String | Int) -> (String | Int) -> [Int] + ``` + + # Example + + ```nix + takeRange 2 "192.168.1.1" "192.168.1.10" + => [ 3232235777 3232235778 ] + ``` + */ + takeRange = + n: start: end: + let + startInt = ip.parse start; + endInt = ip.parse end; + actualEnd = if startInt + n - 1 < endInt then startInt + n - 1 else endInt; + in + if endInt < startInt || n <= 0 then [ ] else range startInt actualEnd; + + /** + Take first n IPs from a range (as strings). + + # Type + + ``` + takeRangeStr :: Int -> (String | Int) -> (String | Int) -> [String] + ``` + + # Example + + ```nix + takeRangeStr 2 "192.168.1.1" "192.168.1.10" + => [ "192.168.1.1" "192.168.1.2" ] + ``` + */ + takeRangeStr = + n: start: end: + map ip.format (takeRange n start end); +} diff --git a/lib/validate.nix b/lib/validate.nix new file mode 100644 index 0000000..39e4ce0 --- /dev/null +++ b/lib/validate.nix @@ -0,0 +1,371 @@ +{ + lib, + internal, + ip, + cidr, +}: +let + inherit (builtins) + elemAt + filter + length + match + split + tryEval + ; + inherit (lib) all toInt; +in +rec { + /** + Check if a string is a valid IPv4 address. + + # Type + + ``` + isValidIp :: Any -> Bool + ``` + + # Example + + ```nix + isValidIp "192.168.1.1" + => true + ``` + */ + isValidIp = + str: + if !builtins.isString str then + false + else + let + # Match basic format: digits.digits.digits.digits + m = match "([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)" str; + in + if m == null then + false + else + let + octets = map toInt m; + validOctet = o: o >= 0 && o <= 255; + in + all validOctet octets; + + /** + Check if a string is valid CIDR notation. + + # Type + + ``` + isValidCidr :: Any -> Bool + ``` + + # Example + + ```nix + isValidCidr "192.168.1.0/24" + => true + ``` + */ + isValidCidr = + str: + if !builtins.isString str then + false + else + let + parts = filter builtins.isString (split "/" str); + in + if length parts != 2 then + false + else + let + ipPart = elemAt parts 0; + prefixPart = elemAt parts 1; + prefixMatch = match "[0-9]+" prefixPart; + in + if !isValidIp ipPart then + false + else if prefixMatch == null then + false + else + let + prefix = toInt prefixPart; + in + prefix >= 0 && prefix <= 32; + + /** + Try to parse an IP, return null on failure. + + # Type + + ``` + tryParseIp :: String -> (Int | Null) + ``` + + # Example + + ```nix + tryParseIp "192.168.1.1" + => 3232235777 + ``` + */ + tryParseIp = + str: + let + result = tryEval (if isValidIp str then ip.parse str else throw "invalid"); + in + if result.success then result.value else null; + + /** + Try to parse a CIDR, return null on failure. + + # Type + + ``` + tryParseCidr :: String -> ({ address :: Int; prefixLen :: Int; } | Null) + ``` + + # Example + + ```nix + tryParseCidr "192.168.1.0/24" + => { address = 3232235776; prefixLen = 24; } + ``` + */ + tryParseCidr = + str: + let + result = tryEval (if isValidCidr str then cidr.parse str else throw "invalid"); + in + if result.success then result.value else null; + + /** + Check if IP is in RFC 1918 private address space. + Covers 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. + + # Type + + ``` + isPrivate :: (String | Int) -> Bool + ``` + + # Example + + ```nix + isPrivate "192.168.1.1" + => true + ``` + */ + isPrivate = + addr: + let + ipInt = ip.parse addr; + in + cidr.contains "10.0.0.0/8" ipInt + || cidr.contains "172.16.0.0/12" ipInt + || cidr.contains "192.168.0.0/16" ipInt; + + /** + Check if IP is loopback (127.0.0.0/8). + + # Type + + ``` + isLoopback :: (String | Int) -> Bool + ``` + + # Example + + ```nix + isLoopback "127.0.0.1" + => true + ``` + */ + isLoopback = addr: cidr.contains "127.0.0.0/8" (ip.parse addr); + + /** + Check if IP is link-local (169.254.0.0/16). + + # Type + + ``` + isLinkLocal :: (String | Int) -> Bool + ``` + + # Example + + ```nix + isLinkLocal "169.254.1.1" + => true + ``` + */ + isLinkLocal = addr: cidr.contains "169.254.0.0/16" (ip.parse addr); + + /** + Check if IP is multicast (224.0.0.0/4). + + # Type + + ``` + isMulticast :: (String | Int) -> Bool + ``` + + # Example + + ```nix + isMulticast "224.0.0.1" + => true + ``` + */ + isMulticast = addr: cidr.contains "224.0.0.0/4" (ip.parse addr); + + /** + Check if IP is broadcast (255.255.255.255). + + # Type + + ``` + isBroadcast :: (String | Int) -> Bool + ``` + + # Example + + ```nix + isBroadcast "255.255.255.255" + => true + ``` + */ + isBroadcast = addr: (ip.parse addr) == 4294967295; + + /** + Check if IP is unspecified (0.0.0.0). + + # Type + + ``` + isUnspecified :: (String | Int) -> Bool + ``` + + # Example + + ```nix + isUnspecified "0.0.0.0" + => true + ``` + */ + isUnspecified = addr: (ip.parse addr) == 0; + + /** + Check if IP is documentation range. + Covers 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24. + + # Type + + ``` + isDocumentation :: (String | Int) -> Bool + ``` + + # Example + + ```nix + isDocumentation "192.0.2.1" + => true + ``` + */ + isDocumentation = + addr: + let + ipInt = ip.parse addr; + in + cidr.contains "192.0.2.0/24" ipInt + || cidr.contains "198.51.100.0/24" ipInt + || cidr.contains "203.0.113.0/24" ipInt; + + /** + Check if IP is reserved for future use (240.0.0.0/4, except broadcast). + + # Type + + ``` + isReserved :: (String | Int) -> Bool + ``` + + # Example + + ```nix + isReserved "240.0.0.1" + => true + ``` + */ + isReserved = + addr: + let + ipInt = ip.parse addr; + in + cidr.contains "240.0.0.0/4" ipInt && ipInt != 4294967295; + + /** + Check if IP is globally routable (not private, loopback, link-local, etc.). + + # Type + + ``` + isGlobalUnicast :: (String | Int) -> Bool + ``` + + # Example + + ```nix + isGlobalUnicast "8.8.8.8" + => true + ``` + */ + isGlobalUnicast = + addr: + !isPrivate addr + && !isLoopback addr + && !isLinkLocal addr + && !isMulticast addr + && !isBroadcast addr + && !isUnspecified addr + && !isDocumentation addr + && !isReserved addr; + + /** + Classify an IP address into its type. + Returns one of: "loopback", "private", "link-local", "multicast", + "broadcast", "unspecified", "documentation", "reserved", "global-unicast". + + # Type + + ``` + classify :: (String | Int) -> String + ``` + + # Example + + ```nix + classify "192.168.1.1" + => "private" + ``` + */ + classify = + addr: + if isLoopback addr then + "loopback" + else if isPrivate addr then + "private" + else if isLinkLocal addr then + "link-local" + else if isMulticast addr then + "multicast" + else if isBroadcast addr then + "broadcast" + else if isUnspecified addr then + "unspecified" + else if isDocumentation addr then + "documentation" + else if isReserved addr then + "reserved" + else + "global-unicast"; +} diff --git a/tests/cidr-tests.nix b/tests/cidr-tests.nix new file mode 100644 index 0000000..24c09e3 --- /dev/null +++ b/tests/cidr-tests.nix @@ -0,0 +1,315 @@ +{ + lib, + cidr, +}: + +let + inherit (lib) runTests; + +in +runTests { + testParseBasic = { + expr = cidr.parse "192.168.1.0/24"; + expected = { + address = 3232235776; + prefixLen = 24; + }; + }; + + testParseSlash32 = { + expr = cidr.parse "10.0.0.1/32"; + expected = { + address = 167772161; + prefixLen = 32; + }; + }; + + testParseSlash0 = { + expr = cidr.parse "0.0.0.0/0"; + expected = { + address = 0; + prefixLen = 0; + }; + }; + + testMakeBasic = { + expr = cidr.make "192.168.1.0" 24; + expected = { + address = 3232235776; + prefixLen = 24; + }; + }; + + testFormatBasic = { + expr = cidr.format { + address = 3232235776; + prefixLen = 24; + }; + expected = "192.168.1.0/24"; + }; + + testNetmask24 = { + expr = cidr.netmaskStr 24; + expected = "255.255.255.0"; + }; + + testNetmask16 = { + expr = cidr.netmaskStr 16; + expected = "255.255.0.0"; + }; + + testNetmask8 = { + expr = cidr.netmaskStr 8; + expected = "255.0.0.0"; + }; + + testNetmask32 = { + expr = cidr.netmaskStr 32; + expected = "255.255.255.255"; + }; + + testNetmask0 = { + expr = cidr.netmaskStr 0; + expected = "0.0.0.0"; + }; + + testNetmask25 = { + expr = cidr.netmaskStr 25; + expected = "255.255.255.128"; + }; + + testWildcard24 = { + expr = cidr.wildcardStr 24; + expected = "0.0.0.255"; + }; + + testWildcard16 = { + expr = cidr.wildcardStr 16; + expected = "0.0.255.255"; + }; + + testNetworkBasic = { + expr = cidr.networkStr "192.168.1.50/24"; + expected = "192.168.1.0"; + }; + + testNetworkAlreadyNetwork = { + expr = cidr.networkStr "192.168.1.0/24"; + expected = "192.168.1.0"; + }; + + testNetworkSlash16 = { + expr = cidr.networkStr "172.16.45.67/16"; + expected = "172.16.0.0"; + }; + + testBroadcastBasic = { + expr = cidr.broadcastStr "192.168.1.0/24"; + expected = "192.168.1.255"; + }; + + testBroadcastSlash16 = { + expr = cidr.broadcastStr "172.16.0.0/16"; + expected = "172.16.255.255"; + }; + + testBroadcastSlash30 = { + expr = cidr.broadcastStr "192.168.1.0/30"; + expected = "192.168.1.3"; + }; + + testFirstHostBasic = { + expr = cidr.firstHostStr "192.168.1.0/24"; + expected = "192.168.1.1"; + }; + + testLastHostBasic = { + expr = cidr.lastHostStr "192.168.1.0/24"; + expected = "192.168.1.254"; + }; + + testFirstHostSlash31 = { + expr = cidr.firstHostStr "192.168.1.0/31"; + expected = "192.168.1.0"; + }; + + testLastHostSlash31 = { + expr = cidr.lastHostStr "192.168.1.0/31"; + expected = "192.168.1.1"; + }; + + testFirstHostSlash32 = { + expr = cidr.firstHostStr "192.168.1.1/32"; + expected = "192.168.1.1"; + }; + + testSize24 = { + expr = cidr.size "192.168.1.0/24"; + expected = 256; + }; + + testSize16 = { + expr = cidr.size "172.16.0.0/16"; + expected = 65536; + }; + + testSize32 = { + expr = cidr.size "192.168.1.1/32"; + expected = 1; + }; + + testSize30 = { + expr = cidr.size "192.168.1.0/30"; + expected = 4; + }; + + testHostCount24 = { + expr = cidr.hostCount "192.168.1.0/24"; + expected = 254; + }; + + testHostCount30 = { + expr = cidr.hostCount "192.168.1.0/30"; + expected = 2; + }; + + testHostCount31 = { + expr = cidr.hostCount "192.168.1.0/31"; + expected = 2; + }; + + testHostCount32 = { + expr = cidr.hostCount "192.168.1.1/32"; + expected = 1; + }; + + testContainsTrue = { + expr = cidr.contains "10.0.0.0/8" "10.1.2.3"; + expected = true; + }; + + testContainsFalse = { + expr = cidr.contains "10.0.0.0/8" "192.168.1.1"; + expected = false; + }; + + testContainsNetwork = { + expr = cidr.contains "192.168.1.0/24" "192.168.1.0"; + expected = true; + }; + + testContainsBroadcast = { + expr = cidr.contains "192.168.1.0/24" "192.168.1.255"; + expected = true; + }; + + testContainsEdgeFalse = { + expr = cidr.contains "192.168.1.0/24" "192.168.2.0"; + expected = false; + }; + + testIsSubnetOfTrue = { + expr = cidr.isSubnetOf "10.0.0.0/8" "10.1.0.0/16"; + expected = true; + }; + + testIsSubnetOfFalse = { + expr = cidr.isSubnetOf "10.0.0.0/8" "192.168.0.0/16"; + expected = false; + }; + + testIsSubnetOfSame = { + expr = cidr.isSubnetOf "10.0.0.0/8" "10.0.0.0/8"; + expected = true; + }; + + testIsSubnetOfLarger = { + expr = cidr.isSubnetOf "10.1.0.0/16" "10.0.0.0/8"; + expected = false; + }; + + testHostBasic = { + expr = cidr.hostStr "192.168.1.0/24" 5; + expected = "192.168.1.5"; + }; + + testHostZero = { + expr = cidr.hostStr "192.168.1.0/24" 0; + expected = "192.168.1.0"; + }; + + testHostLast = { + expr = cidr.hostStr "192.168.1.0/24" 255; + expected = "192.168.1.255"; + }; + + testSubnetFirst = { + expr = cidr.format (cidr.subnet "192.168.0.0/24" 26 0); + expected = "192.168.0.0/26"; + }; + + testSubnetSecond = { + expr = cidr.format (cidr.subnet "192.168.0.0/24" 26 1); + expected = "192.168.0.64/26"; + }; + + testSubnetLast = { + expr = cidr.format (cidr.subnet "192.168.0.0/24" 26 3); + expected = "192.168.0.192/26"; + }; + + testSubnetsCount = { + expr = builtins.length (cidr.subnets "192.168.0.0/24" 26); + expected = 4; + }; + + testSubnetsStr = { + expr = cidr.subnetsStr "192.168.0.0/24" 26; + expected = [ + "192.168.0.0/26" + "192.168.0.64/26" + "192.168.0.128/26" + "192.168.0.192/26" + ]; + }; + + testOverlapsTrue = { + expr = cidr.overlaps "192.168.1.0/24" "192.168.0.0/16"; + expected = true; + }; + + testOverlapsFalse = { + expr = cidr.overlaps "192.168.1.0/24" "10.0.0.0/8"; + expected = false; + }; + + testOverlapsAdjacent = { + expr = cidr.overlaps "192.168.1.0/24" "192.168.2.0/24"; + expected = false; + }; + + testPrefixForHosts100 = { + expr = cidr.prefixForHosts 100; + expected = 25; + }; + + testPrefixForHosts254 = { + expr = cidr.prefixForHosts 254; + expected = 24; + }; + + testPrefixForHosts1 = { + expr = cidr.prefixForHosts 1; + expected = 32; + }; + + testPrefixForSize256 = { + expr = cidr.prefixForSize 256; + expected = 24; + }; + + testPrefixForSize1 = { + expr = cidr.prefixForSize 1; + expected = 32; + }; +} diff --git a/tests/default.nix b/tests/default.nix new file mode 100644 index 0000000..668d77a --- /dev/null +++ b/tests/default.nix @@ -0,0 +1,43 @@ +{ + lib, + ipLib, + system, +}: + +let + ipTests = import ./ip-tests.nix { + inherit lib; + ip = ipLib.ip; + }; + + cidrTests = import ./cidr-tests.nix { + inherit lib; + cidr = ipLib.cidr; + }; + + validateTests = import ./validate-tests.nix { + inherit lib; + validate = ipLib.validate; + }; + + iterateTests = import ./iterate-tests.nix { + inherit lib; + iterate = ipLib.iterate; + ip = ipLib.ip; + }; + + allTests = ipTests ++ cidrTests ++ validateTests ++ iterateTests; + +in +if allTests == [ ] then + derivation { + name = "nix-ip-utils-tests"; + inherit system; + builder = "/bin/sh"; + args = [ + "-c" + "echo 'passed!' > $out" + ]; + } +else + throw "Tests failed: ${builtins.toJSON allTests}" diff --git a/tests/ip-tests.nix b/tests/ip-tests.nix new file mode 100644 index 0000000..dcd46ee --- /dev/null +++ b/tests/ip-tests.nix @@ -0,0 +1,205 @@ +{ lib, ip }: +let + inherit (lib) runTests; +in +runTests { + testParseBasic = { + expr = ip.parse "192.168.1.1"; + expected = 3232235777; + }; + + testParseZeros = { + expr = ip.parse "0.0.0.0"; + expected = 0; + }; + + testParseMax = { + expr = ip.parse "255.255.255.255"; + expected = 4294967295; + }; + + testParseLoopback = { + expr = ip.parse "127.0.0.1"; + expected = 2130706433; + }; + + testParseTen = { + expr = ip.parse "10.0.0.1"; + expected = 167772161; + }; + + testParsePassthroughInt = { + expr = ip.parse 3232235777; + expected = 3232235777; + }; + + testFormatBasic = { + expr = ip.format 3232235777; + expected = "192.168.1.1"; + }; + + testFormatZeros = { + expr = ip.format 0; + expected = "0.0.0.0"; + }; + + testFormatMax = { + expr = ip.format 4294967295; + expected = "255.255.255.255"; + }; + + testFormatPassthroughString = { + expr = ip.format "192.168.1.1"; + expected = "192.168.1.1"; + }; + + testToOctetsBasic = { + expr = ip.toOctets "192.168.1.1"; + expected = [ + 192 + 168 + 1 + 1 + ]; + }; + + testToOctetsFromInt = { + expr = ip.toOctets 3232235777; + expected = [ + 192 + 168 + 1 + 1 + ]; + }; + + testToOctetsZeros = { + expr = ip.toOctets "0.0.0.0"; + expected = [ + 0 + 0 + 0 + 0 + ]; + }; + + testFromOctetsBasic = { + expr = ip.fromOctets [ + 192 + 168 + 1 + 1 + ]; + expected = 3232235777; + }; + + testFromOctetsZeros = { + expr = ip.fromOctets [ + 0 + 0 + 0 + 0 + ]; + expected = 0; + }; + + testFromOctetsTen = { + expr = ip.fromOctets [ + 10 + 0 + 0 + 1 + ]; + expected = 167772161; + }; + + testAddBasic = { + expr = ip.add "192.168.1.1" 10; + expected = 3232235787; + }; + + testAddStrBasic = { + expr = ip.addStr "192.168.1.1" 10; + expected = "192.168.1.11"; + }; + + testAddCrossOctet = { + expr = ip.addStr "192.168.1.250" 10; + expected = "192.168.2.4"; + }; + + testSubtractBasic = { + expr = ip.subtract "192.168.1.10" 5; + expected = 3232235781; + }; + + testSubtractStrBasic = { + expr = ip.subtractStr "192.168.1.10" 5; + expected = "192.168.1.5"; + }; + + testDiffBasic = { + expr = ip.diff "192.168.1.10" "192.168.1.1"; + expected = 9; + }; + + testDiffNegative = { + expr = ip.diff "192.168.1.1" "192.168.1.10"; + expected = -9; + }; + + testCompareLess = { + expr = ip.compare "10.0.0.1" "10.0.0.2"; + expected = -1; + }; + + testCompareGreater = { + expr = ip.compare "10.0.0.2" "10.0.0.1"; + expected = 1; + }; + + testCompareEqual = { + expr = ip.compare "10.0.0.1" "10.0.0.1"; + expected = 0; + }; + + testLtTrue = { + expr = ip.lt "10.0.0.1" "10.0.0.2"; + expected = true; + }; + + testLtFalse = { + expr = ip.lt "10.0.0.2" "10.0.0.1"; + expected = false; + }; + + testGtTrue = { + expr = ip.gt "10.0.0.2" "10.0.0.1"; + expected = true; + }; + + testEqTrue = { + expr = ip.eq "10.0.0.1" "10.0.0.1"; + expected = true; + }; + + testEqFalse = { + expr = ip.eq "10.0.0.1" "10.0.0.2"; + expected = false; + }; + + testMinStr = { + expr = ip.minStr "10.0.0.5" "10.0.0.2"; + expected = "10.0.0.2"; + }; + + testMaxStr = { + expr = ip.maxStr "10.0.0.5" "10.0.0.2"; + expected = "10.0.0.5"; + }; + + testRoundtrip = { + expr = ip.format (ip.parse "172.16.32.64"); + expected = "172.16.32.64"; + }; +} diff --git a/tests/iterate-tests.nix b/tests/iterate-tests.nix new file mode 100644 index 0000000..2a1e424 --- /dev/null +++ b/tests/iterate-tests.nix @@ -0,0 +1,195 @@ +{ + lib, + iterate, + ip, +}: + +let + inherit (lib) runTests; + +in +runTests { + testRangeBasic = { + expr = iterate.rangeStr "10.0.0.1" "10.0.0.3"; + expected = [ + "10.0.0.1" + "10.0.0.2" + "10.0.0.3" + ]; + }; + + testRangeSingle = { + expr = iterate.rangeStr "10.0.0.1" "10.0.0.1"; + expected = [ "10.0.0.1" ]; + }; + + testRangeEmpty = { + expr = iterate.rangeStr "10.0.0.5" "10.0.0.1"; + expected = [ ]; + }; + + testRangeCrossOctet = { + expr = iterate.rangeStr "10.0.0.254" "10.0.1.2"; + expected = [ + "10.0.0.254" + "10.0.0.255" + "10.0.1.0" + "10.0.1.1" + "10.0.1.2" + ]; + }; + + testCidrAddresses30 = { + expr = iterate.cidrAddressesStr "10.0.0.0/30"; + expected = [ + "10.0.0.0" + "10.0.0.1" + "10.0.0.2" + "10.0.0.3" + ]; + }; + + testCidrAddresses32 = { + expr = iterate.cidrAddressesStr "10.0.0.1/32"; + expected = [ "10.0.0.1" ]; + }; + + testCidrAddressesCount = { + expr = builtins.length (iterate.cidrAddresses "10.0.0.0/28"); + expected = 16; + }; + + testCidrHosts30 = { + expr = iterate.cidrHostsStr "10.0.0.0/30"; + expected = [ + "10.0.0.1" + "10.0.0.2" + ]; + }; + + testCidrHosts31 = { + expr = iterate.cidrHostsStr "10.0.0.0/31"; + expected = [ + "10.0.0.0" + "10.0.0.1" + ]; + }; + + testCidrHosts32 = { + expr = iterate.cidrHostsStr "10.0.0.1/32"; + expected = [ "10.0.0.1" ]; + }; + + testCidrHostsCount = { + expr = builtins.length (iterate.cidrHosts "10.0.0.0/28"); + expected = 14; + }; + + testInRangeTrue = { + expr = iterate.inRange "10.0.0.1" "10.0.0.5" "10.0.0.3"; + expected = true; + }; + + testInRangeStart = { + expr = iterate.inRange "10.0.0.1" "10.0.0.5" "10.0.0.1"; + expected = true; + }; + + testInRangeEnd = { + expr = iterate.inRange "10.0.0.1" "10.0.0.5" "10.0.0.5"; + expected = true; + }; + + testInRangeFalseLow = { + expr = iterate.inRange "10.0.0.2" "10.0.0.5" "10.0.0.1"; + expected = false; + }; + + testInRangeFalseHigh = { + expr = iterate.inRange "10.0.0.1" "10.0.0.5" "10.0.0.6"; + expected = false; + }; + + testCountRangeBasic = { + expr = iterate.countRange "10.0.0.1" "10.0.0.10"; + expected = 10; + }; + + testCountRangeSingle = { + expr = iterate.countRange "10.0.0.1" "10.0.0.1"; + expected = 1; + }; + + testCountRangeEmpty = { + expr = iterate.countRange "10.0.0.5" "10.0.0.1"; + expected = 0; + }; + + testMapRangeFormat = { + expr = iterate.mapRange ip.format "10.0.0.1" "10.0.0.3"; + expected = [ + "10.0.0.1" + "10.0.0.2" + "10.0.0.3" + ]; + }; + + testMapRangeOctets = { + expr = iterate.mapRange ip.toOctets "10.0.0.1" "10.0.0.2"; + expected = [ + [ + 10 + 0 + 0 + 1 + ] + [ + 10 + 0 + 0 + 2 + ] + ]; + }; + + testFilterRangeEven = { + expr = builtins.map ip.format (iterate.filterRange (i: lib.mod i 2 == 0) "10.0.0.1" "10.0.0.5"); + expected = [ + "10.0.0.2" + "10.0.0.4" + ]; + }; + + testTakeRangeBasic = { + expr = iterate.takeRangeStr 3 "10.0.0.1" "10.0.0.10"; + expected = [ + "10.0.0.1" + "10.0.0.2" + "10.0.0.3" + ]; + }; + + testTakeRangeMoreThanAvailable = { + expr = iterate.takeRangeStr 10 "10.0.0.1" "10.0.0.3"; + expected = [ + "10.0.0.1" + "10.0.0.2" + "10.0.0.3" + ]; + }; + + testTakeRangeZero = { + expr = iterate.takeRangeStr 0 "10.0.0.1" "10.0.0.10"; + expected = [ ]; + }; + + testFindInRangeBasic = { + expr = iterate.findInRangeStr (i: lib.mod i 5 == 0) "10.0.0.1" "10.0.0.10"; + expected = "10.0.0.5"; + }; + + testFindInRangeNotFound = { + expr = iterate.findInRange (i: i > 1000000000) "10.0.0.1" "10.0.0.10"; + expected = null; + }; +} diff --git a/tests/validate-tests.nix b/tests/validate-tests.nix new file mode 100644 index 0000000..e97b17d --- /dev/null +++ b/tests/validate-tests.nix @@ -0,0 +1,266 @@ +{ lib, validate }: + +let + inherit (lib) runTests; + +in runTests { + testIsValidIpBasic = { + expr = validate.isValidIp "192.168.1.1"; + expected = true; + }; + + testIsValidIpZeros = { + expr = validate.isValidIp "0.0.0.0"; + expected = true; + }; + + testIsValidIpMax = { + expr = validate.isValidIp "255.255.255.255"; + expected = true; + }; + + testIsValidIpInvalidOctet = { + expr = validate.isValidIp "256.1.1.1"; + expected = false; + }; + + testIsValidIpTooFewOctets = { + expr = validate.isValidIp "192.168.1"; + expected = false; + }; + + testIsValidIpTooManyOctets = { + expr = validate.isValidIp "192.168.1.1.1"; + expected = false; + }; + + testIsValidIpEmpty = { + expr = validate.isValidIp ""; + expected = false; + }; + + testIsValidIpLetters = { + expr = validate.isValidIp "abc.def.ghi.jkl"; + expected = false; + }; + + testIsValidIpNegative = { + expr = validate.isValidIp "-1.0.0.0"; + expected = false; + }; + + testIsValidIpNotString = { + expr = validate.isValidIp 12345; + expected = false; + }; + + testIsValidCidrBasic = { + expr = validate.isValidCidr "192.168.1.0/24"; + expected = true; + }; + + testIsValidCidrSlash0 = { + expr = validate.isValidCidr "0.0.0.0/0"; + expected = true; + }; + + testIsValidCidrSlash32 = { + expr = validate.isValidCidr "192.168.1.1/32"; + expected = true; + }; + + testIsValidCidrInvalidPrefix = { + expr = validate.isValidCidr "192.168.1.0/33"; + expected = false; + }; + + testIsValidCidrNoPrefix = { + expr = validate.isValidCidr "192.168.1.0"; + expected = false; + }; + + testIsValidCidrInvalidIp = { + expr = validate.isValidCidr "256.168.1.0/24"; + expected = false; + }; + + testIsValidCidrEmpty = { + expr = validate.isValidCidr ""; + expected = false; + }; + + testIsValidCidrNotString = { + expr = validate.isValidCidr 12345; + expected = false; + }; + + testTryParseIpValid = { + expr = validate.tryParseIp "192.168.1.1"; + expected = 3232235777; + }; + + testTryParseIpInvalid = { + expr = validate.tryParseIp "invalid"; + expected = null; + }; + + testTryParseCidrValid = { + expr = validate.tryParseCidr "192.168.1.0/24"; + expected = { address = 3232235776; prefixLen = 24; }; + }; + + testTryParseCidrInvalid = { + expr = validate.tryParseCidr "invalid"; + expected = null; + }; + + testIsPrivate10 = { + expr = validate.isPrivate "10.1.2.3"; + expected = true; + }; + + testIsPrivate172 = { + expr = validate.isPrivate "172.16.5.10"; + expected = true; + }; + + testIsPrivate172Edge = { + expr = validate.isPrivate "172.31.255.255"; + expected = true; + }; + + testIsPrivate172Outside = { + expr = validate.isPrivate "172.32.0.0"; + expected = false; + }; + + testIsPrivate192 = { + expr = validate.isPrivate "192.168.100.50"; + expected = true; + }; + + testIsPrivatePublic = { + expr = validate.isPrivate "8.8.8.8"; + expected = false; + }; + + testIsLoopback127 = { + expr = validate.isLoopback "127.0.0.1"; + expected = true; + }; + + testIsLoopback127Any = { + expr = validate.isLoopback "127.255.255.254"; + expected = true; + }; + + testIsLoopbackFalse = { + expr = validate.isLoopback "128.0.0.1"; + expected = false; + }; + + testIsLinkLocalTrue = { + expr = validate.isLinkLocal "169.254.1.1"; + expected = true; + }; + + testIsLinkLocalFalse = { + expr = validate.isLinkLocal "169.255.1.1"; + expected = false; + }; + + testIsMulticastTrue = { + expr = validate.isMulticast "224.0.0.1"; + expected = true; + }; + + testIsMulticastMax = { + expr = validate.isMulticast "239.255.255.255"; + expected = true; + }; + + testIsMulticastFalse = { + expr = validate.isMulticast "223.255.255.255"; + expected = false; + }; + + testIsBroadcastTrue = { + expr = validate.isBroadcast "255.255.255.255"; + expected = true; + }; + + testIsBroadcastFalse = { + expr = validate.isBroadcast "255.255.255.254"; + expected = false; + }; + + testIsUnspecifiedTrue = { + expr = validate.isUnspecified "0.0.0.0"; + expected = true; + }; + + testIsUnspecifiedFalse = { + expr = validate.isUnspecified "0.0.0.1"; + expected = false; + }; + + testIsDocumentation192 = { + expr = validate.isDocumentation "192.0.2.1"; + expected = true; + }; + + testIsDocumentation198 = { + expr = validate.isDocumentation "198.51.100.50"; + expected = true; + }; + + testIsDocumentation203 = { + expr = validate.isDocumentation "203.0.113.100"; + expected = true; + }; + + testIsDocumentationFalse = { + expr = validate.isDocumentation "192.0.3.1"; + expected = false; + }; + + testIsGlobalUnicastPublic = { + expr = validate.isGlobalUnicast "8.8.8.8"; + expected = true; + }; + + testIsGlobalUnicastPrivate = { + expr = validate.isGlobalUnicast "192.168.1.1"; + expected = false; + }; + + testIsGlobalUnicastLoopback = { + expr = validate.isGlobalUnicast "127.0.0.1"; + expected = false; + }; + + testClassifyLoopback = { + expr = validate.classify "127.0.0.1"; + expected = "loopback"; + }; + + testClassifyPrivate = { + expr = validate.classify "192.168.1.1"; + expected = "private"; + }; + + testClassifyLinkLocal = { + expr = validate.classify "169.254.1.1"; + expected = "link-local"; + }; + + testClassifyMulticast = { + expr = validate.classify "224.0.0.1"; + expected = "multicast"; + }; + + testClassifyGlobal = { + expr = validate.classify "8.8.8.8"; + expected = "global-unicast"; + }; +}