summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJanik <80165193+Janik-Haag@users.noreply.github.com>2024-07-11 20:57:53 +0200
committerGitHub <noreply@github.com>2024-07-11 20:57:53 +0200
commitc20399ea2eb952e0fe67a22e2c0988f82ab1b734 (patch)
treecafb34e6af7ee39c3e7f3c5ff2b205110f55f8bf /lib
parentMerge pull request #326277 from dotlambda/getmail6 (diff)
parentlib.network: ipv6 parser from string (diff)
downloadnixpkgs-c20399ea2eb952e0fe67a22e2c0988f82ab1b734.tar.gz
Merge pull request #318712 from woojiq/lib-network-ipv6-parser
lib.network: add ipv6 parser
Diffstat (limited to 'lib')
-rw-r--r--lib/default.nix5
-rw-r--r--lib/network/default.nix49
-rw-r--r--lib/network/internal.nix209
-rw-r--r--lib/tests/misc.nix16
-rwxr-xr-xlib/tests/network.sh117
-rw-r--r--lib/tests/test-with-nix.nix3
-rw-r--r--lib/trivial.nix26
7 files changed, 424 insertions, 1 deletions
diff --git a/lib/default.nix b/lib/default.nix
index ef0bb60ab8c5..2605da47679e 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -64,6 +64,9 @@ let
# linux kernel configuration
kernel = callLibs ./kernel.nix;
+ # network
+ network = callLibs ./network;
+
# TODO: For consistency, all builtins should also be available from a sub-library;
# these are the only ones that are currently not
inherit (builtins) addErrorContext isPath trace typeOf unsafeGetAttrPos;
@@ -73,7 +76,7 @@ let
info showWarnings nixpkgsVersion version isInOldestRelease
mod compare splitByAndCompare seq deepSeq lessThan add sub
functionArgs setFunctionArgs isFunction toFunction mirrorFunctionArgs
- toHexString toBaseDigits inPureEvalMode isBool isInt pathExists
+ fromHexString toHexString toBaseDigits inPureEvalMode isBool isInt pathExists
genericClosure readFile;
inherit (self.fixedPoints) fix fix' converge extends composeExtensions
composeManyExtensions makeExtensible makeExtensibleWithCustomName;
diff --git a/lib/network/default.nix b/lib/network/default.nix
new file mode 100644
index 000000000000..e0c583ee7506
--- /dev/null
+++ b/lib/network/default.nix
@@ -0,0 +1,49 @@
+{ lib }:
+let
+ inherit (import ./internal.nix { inherit lib; }) _ipv6;
+in
+{
+ ipv6 = {
+ /**
+ Creates an `IPv6Address` object from an IPv6 address as a string. If
+ the prefix length is omitted, it defaults to 64. The parser is limited
+ to the first two versions of IPv6 addresses addressed in RFC 4291.
+ The form "x:x:x:x:x:x:d.d.d.d" is not yet implemented. Addresses are
+ NOT compressed, so they are not always the same as the canonical text
+ representation of IPv6 addresses defined in RFC 5952.
+
+ # Type
+
+ ```
+ fromString :: String -> IPv6Address
+ ```
+
+ # Examples
+
+ ```nix
+ fromString "2001:DB8::ffff/32"
+ => {
+ address = "2001:db8:0:0:0:0:0:ffff";
+ prefixLength = 32;
+ }
+ ```
+
+ # Arguments
+
+ - [addr] An IPv6 address with optional prefix length.
+ */
+ fromString =
+ addr:
+ let
+ splittedAddr = _ipv6.split addr;
+
+ addrInternal = splittedAddr.address;
+ prefixLength = splittedAddr.prefixLength;
+
+ address = _ipv6.toStringFromExpandedIp addrInternal;
+ in
+ {
+ inherit address prefixLength;
+ };
+ };
+}
diff --git a/lib/network/internal.nix b/lib/network/internal.nix
new file mode 100644
index 000000000000..3e05be90c547
--- /dev/null
+++ b/lib/network/internal.nix
@@ -0,0 +1,209 @@
+{
+ lib ? import ../.,
+}:
+let
+ inherit (builtins)
+ map
+ match
+ genList
+ length
+ concatMap
+ head
+ toString
+ ;
+
+ inherit (lib) lists strings trivial;
+
+ inherit (lib.lists) last;
+
+ /*
+ IPv6 addresses are 128-bit identifiers. The preferred form is 'x:x:x:x:x:x:x:x',
+ where the 'x's are one to four hexadecimal digits of the eight 16-bit pieces of
+ the address. See RFC 4291.
+ */
+ ipv6Bits = 128;
+ ipv6Pieces = 8; # 'x:x:x:x:x:x:x:x'
+ ipv6PieceBits = 16; # One piece in range from 0 to 0xffff.
+ ipv6PieceMaxValue = 65535; # 2^16 - 1
+in
+let
+ /**
+ Expand an IPv6 address by removing the "::" compression and padding them
+ with the necessary number of zeros. Converts an address from the string to
+ the list of strings which then can be parsed using `_parseExpanded`.
+ Throws an error when the address is malformed.
+
+ # Type: String -> [ String ]
+
+ # Example:
+
+ ```nix
+ expandIpv6 "2001:DB8::ffff"
+ => ["2001" "DB8" "0" "0" "0" "0" "0" "ffff"]
+ ```
+ */
+ expandIpv6 =
+ addr:
+ if match "^[0-9A-Fa-f:]+$" addr == null then
+ throw "${addr} contains malformed characters for IPv6 address"
+ else
+ let
+ pieces = strings.splitString ":" addr;
+ piecesNoEmpty = lists.remove "" pieces;
+ piecesNoEmptyLen = length piecesNoEmpty;
+ zeros = genList (_: "0") (ipv6Pieces - piecesNoEmptyLen);
+ hasPrefix = strings.hasPrefix "::" addr;
+ hasSuffix = strings.hasSuffix "::" addr;
+ hasInfix = strings.hasInfix "::" addr;
+ in
+ if addr == "::" then
+ zeros
+ else if
+ let
+ emptyCount = length pieces - piecesNoEmptyLen;
+ emptyExpected =
+ # splitString produces two empty pieces when "::" in the beginning
+ # or in the end, and only one when in the middle of an address.
+ if hasPrefix || hasSuffix then
+ 2
+ else if hasInfix then
+ 1
+ else
+ 0;
+ in
+ emptyCount != emptyExpected
+ || (hasInfix && piecesNoEmptyLen >= ipv6Pieces) # "::" compresses at least one group of zeros.
+ || (!hasInfix && piecesNoEmptyLen != ipv6Pieces)
+ then
+ throw "${addr} is not a valid IPv6 address"
+ # Create a list of 8 elements, filling some of them with zeros depending
+ # on where the "::" was found.
+ else if hasPrefix then
+ zeros ++ piecesNoEmpty
+ else if hasSuffix then
+ piecesNoEmpty ++ zeros
+ else if hasInfix then
+ concatMap (piece: if piece == "" then zeros else [ piece ]) pieces
+ else
+ pieces;
+
+ /**
+ Parses an expanded IPv6 address (see `expandIpv6`), converting each part
+ from a string to an u16 integer. Returns an internal representation of IPv6
+ address (list of integers) that can be easily processed by other helper
+ functions.
+ Throws an error some element is not an u16 integer.
+
+ # Type: [ String ] -> IPv6
+
+ # Example:
+
+ ```nix
+ parseExpandedIpv6 ["2001" "DB8" "0" "0" "0" "0" "0" "ffff"]
+ => [8193 3512 0 0 0 0 0 65535]
+ ```
+ */
+ parseExpandedIpv6 =
+ addr:
+ assert lib.assertMsg (
+ length addr == ipv6Pieces
+ ) "parseExpandedIpv6: expected list of integers with ${ipv6Pieces} elements";
+ let
+ u16FromHexStr =
+ hex:
+ let
+ parsed = trivial.fromHexString hex;
+ in
+ if 0 <= parsed && parsed <= ipv6PieceMaxValue then
+ parsed
+ else
+ throw "0x${hex} is not a valid u16 integer";
+ in
+ map (piece: u16FromHexStr piece) addr;
+in
+let
+ /**
+ Parses an IPv6 address from a string to the internal representation (list
+ of integers).
+
+ # Type: String -> IPv6
+
+ # Example:
+
+ ```nix
+ parseIpv6FromString "2001:DB8::ffff"
+ => [8193 3512 0 0 0 0 0 65535]
+ ```
+ */
+ parseIpv6FromString = addr: parseExpandedIpv6 (expandIpv6 addr);
+in
+{
+ /*
+ Internally, an IPv6 address is stored as a list of 16-bit integers with 8
+ elements. Wherever you see `IPv6` in internal functions docs, it means that
+ it is a list of integers produced by one of the internal parsers, such as
+ `parseIpv6FromString`
+ */
+ _ipv6 = {
+ /**
+ Converts an internal representation of an IPv6 address (i.e, a list
+ of integers) to a string. The returned string is not a canonical
+ representation as defined in RFC 5952, i.e zeros are not compressed.
+
+ # Type: IPv6 -> String
+
+ # Example:
+
+ ```nix
+ parseIpv6FromString [8193 3512 0 0 0 0 0 65535]
+ => "2001:db8:0:0:0:0:0:ffff"
+ ```
+ */
+ toStringFromExpandedIp =
+ pieces: strings.concatMapStringsSep ":" (piece: strings.toLower (trivial.toHexString piece)) pieces;
+
+ /**
+ Extract an address and subnet prefix length from a string. The subnet
+ prefix length is optional and defaults to 128. The resulting address and
+ prefix length are validated and converted to an internal representation
+ that can be used by other functions.
+
+ # Type: String -> [ {address :: IPv6, prefixLength :: Int} ]
+
+ # Example:
+
+ ```nix
+ split "2001:DB8::ffff/32"
+ => {
+ address = [8193 3512 0 0 0 0 0 65535];
+ prefixLength = 32;
+ }
+ ```
+ */
+ split =
+ addr:
+ let
+ splitted = strings.splitString "/" addr;
+ splittedLength = length splitted;
+ in
+ if splittedLength == 1 then # [ ip ]
+ {
+ address = parseIpv6FromString addr;
+ prefixLength = ipv6Bits;
+ }
+ else if splittedLength == 2 then # [ ip subnet ]
+ {
+ address = parseIpv6FromString (head splitted);
+ prefixLength =
+ let
+ n = strings.toInt (last splitted);
+ in
+ if 1 <= n && n <= ipv6Bits then
+ n
+ else
+ throw "${addr} IPv6 subnet should be in range [1;${toString ipv6Bits}], got ${toString n}";
+ }
+ else
+ throw "${addr} is not a valid IPv6 address in CIDR notation";
+ };
+}
diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix
index 408ea5416293..dd12923e636e 100644
--- a/lib/tests/misc.nix
+++ b/lib/tests/misc.nix
@@ -102,6 +102,7 @@ let
testAllTrue
toBaseDigits
toHexString
+ fromHexString
toInt
toIntBase10
toShellVars
@@ -286,6 +287,21 @@ runTests {
expected = "FA";
};
+ testFromHexStringFirstExample = {
+ expr = fromHexString "FF";
+ expected = 255;
+ };
+
+ testFromHexStringSecondExample = {
+ expr = fromHexString (builtins.hashString "sha256" "test");
+ expected = 9223372036854775807;
+ };
+
+ testFromHexStringWithPrefix = {
+ expr = fromHexString "0Xf";
+ expected = 15;
+ };
+
testToBaseDigits = {
expr = toBaseDigits 2 6;
expected = [ 1 1 0 ];
diff --git a/lib/tests/network.sh b/lib/tests/network.sh
new file mode 100755
index 000000000000..54ca476d2deb
--- /dev/null
+++ b/lib/tests/network.sh
@@ -0,0 +1,117 @@
+#!/usr/bin/env bash
+
+# Tests lib/network.nix
+# Run:
+# [nixpkgs]$ lib/tests/network.sh
+# or:
+# [nixpkgs]$ nix-build lib/tests/release.nix
+
+set -euo pipefail
+shopt -s inherit_errexit
+
+if [[ -n "${TEST_LIB:-}" ]]; then
+ NIX_PATH=nixpkgs="$(dirname "$TEST_LIB")"
+else
+ NIX_PATH=nixpkgs="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.."; pwd)"
+fi
+export NIX_PATH
+
+die() {
+ echo >&2 "test case failed: " "$@"
+ exit 1
+}
+
+tmp="$(mktemp -d)"
+clean_up() {
+ rm -rf "$tmp"
+}
+trap clean_up EXIT SIGINT SIGTERM
+work="$tmp/work"
+mkdir "$work"
+cd "$work"
+
+prefixExpression='
+ let
+ lib = import <nixpkgs/lib>;
+ internal = import <nixpkgs/lib/network/internal.nix> {
+ inherit lib;
+ };
+ in
+ with lib;
+ with lib.network;
+'
+
+expectSuccess() {
+ local expr=$1
+ local expectedResult=$2
+ if ! result=$(nix-instantiate --eval --strict --json --show-trace \
+ --expr "$prefixExpression ($expr)"); then
+ die "$expr failed to evaluate, but it was expected to succeed"
+ fi
+ if [[ ! "$result" == "$expectedResult" ]]; then
+ die "$expr == $result, but $expectedResult was expected"
+ fi
+}
+
+expectSuccessRegex() {
+ local expr=$1
+ local expectedResultRegex=$2
+ if ! result=$(nix-instantiate --eval --strict --json --show-trace \
+ --expr "$prefixExpression ($expr)"); then
+ die "$expr failed to evaluate, but it was expected to succeed"
+ fi
+ if [[ ! "$result" =~ $expectedResultRegex ]]; then
+ die "$expr == $result, but $expectedResultRegex was expected"
+ fi
+}
+
+expectFailure() {
+ local expr=$1
+ local expectedErrorRegex=$2
+ if result=$(nix-instantiate --eval --strict --json --show-trace 2>"$work/stderr" \
+ --expr "$prefixExpression ($expr)"); then
+ die "$expr evaluated successfully to $result, but it was expected to fail"
+ fi
+ if [[ ! "$(<"$work/stderr")" =~ $expectedErrorRegex ]]; then
+ die "Error was $(<"$work/stderr"), but $expectedErrorRegex was expected"
+ fi
+}
+
+# Internal functions
+expectSuccess '(internal._ipv6.split "0:0:0:0:0:0:0:0").address' '[0,0,0,0,0,0,0,0]'
+expectSuccess '(internal._ipv6.split "000a:000b:000c:000d:000e:000f:ffff:aaaa").address' '[10,11,12,13,14,15,65535,43690]'
+expectSuccess '(internal._ipv6.split "::").address' '[0,0,0,0,0,0,0,0]'
+expectSuccess '(internal._ipv6.split "::0000").address' '[0,0,0,0,0,0,0,0]'
+expectSuccess '(internal._ipv6.split "::1").address' '[0,0,0,0,0,0,0,1]'
+expectSuccess '(internal._ipv6.split "::ffff").address' '[0,0,0,0,0,0,0,65535]'
+expectSuccess '(internal._ipv6.split "::000f").address' '[0,0,0,0,0,0,0,15]'
+expectSuccess '(internal._ipv6.split "::1:1:1:1:1:1:1").address' '[0,1,1,1,1,1,1,1]'
+expectSuccess '(internal._ipv6.split "1::").address' '[1,0,0,0,0,0,0,0]'
+expectSuccess '(internal._ipv6.split "1:1:1:1:1:1:1::").address' '[1,1,1,1,1,1,1,0]'
+expectSuccess '(internal._ipv6.split "1:1:1:1::1:1:1").address' '[1,1,1,1,0,1,1,1]'
+expectSuccess '(internal._ipv6.split "1::1").address' '[1,0,0,0,0,0,0,1]'
+
+expectFailure 'internal._ipv6.split "0:0:0:0:0:0:0:-1"' "contains malformed characters for IPv6 address"
+expectFailure 'internal._ipv6.split "::0:"' "is not a valid IPv6 address"
+expectFailure 'internal._ipv6.split ":0::"' "is not a valid IPv6 address"
+expectFailure 'internal._ipv6.split "0::0:"' "is not a valid IPv6 address"
+expectFailure 'internal._ipv6.split "0:0:"' "is not a valid IPv6 address"
+expectFailure 'internal._ipv6.split "0:0:0:0:0:0:0:0:0"' "is not a valid IPv6 address"
+expectFailure 'internal._ipv6.split "0:0:0:0:0:0:0:0:"' "is not a valid IPv6 address"
+expectFailure 'internal._ipv6.split "::0:0:0:0:0:0:0:0"' "is not a valid IPv6 address"
+expectFailure 'internal._ipv6.split "0::0:0:0:0:0:0:0"' "is not a valid IPv6 address"
+expectFailure 'internal._ipv6.split "::10000"' "0x10000 is not a valid u16 integer"
+
+expectSuccess '(internal._ipv6.split "::").prefixLength' '128'
+expectSuccess '(internal._ipv6.split "::/1").prefixLength' '1'
+expectSuccess '(internal._ipv6.split "::/128").prefixLength' '128'
+
+expectFailure '(internal._ipv6.split "::/0").prefixLength' "IPv6 subnet should be in range \[1;128\], got 0"
+expectFailure '(internal._ipv6.split "::/129").prefixLength' "IPv6 subnet should be in range \[1;128\], got 129"
+expectFailure '(internal._ipv6.split "/::/").prefixLength' "is not a valid IPv6 address in CIDR notation"
+
+# Library API
+expectSuccess 'lib.network.ipv6.fromString "2001:DB8::ffff/64"' '{"address":"2001:db8:0:0:0:0:0:ffff","prefixLength":64}'
+expectSuccess 'lib.network.ipv6.fromString "1234:5678:90ab:cdef:fedc:ba09:8765:4321/44"' '{"address":"1234:5678:90ab:cdef:fedc:ba09:8765:4321","prefixLength":44}'
+
+echo >&2 tests ok
diff --git a/lib/tests/test-with-nix.nix b/lib/tests/test-with-nix.nix
index 9d66b91cab42..63b4b10bae8c 100644
--- a/lib/tests/test-with-nix.nix
+++ b/lib/tests/test-with-nix.nix
@@ -65,6 +65,9 @@ pkgs.runCommand "nixpkgs-lib-tests-nix-${nix.version}" {
echo "Running lib/tests/sources.sh"
TEST_LIB=$PWD/lib bash lib/tests/sources.sh
+ echo "Running lib/tests/network.sh"
+ TEST_LIB=$PWD/lib bash lib/tests/network.sh
+
echo "Running lib/fileset/tests.sh"
TEST_LIB=$PWD/lib bash lib/fileset/tests.sh
diff --git a/lib/trivial.nix b/lib/trivial.nix
index 546aed746d1c..771a28bc9dad 100644
--- a/lib/trivial.nix
+++ b/lib/trivial.nix
@@ -1075,6 +1075,32 @@ in {
else k: v;
/**
+ Convert a hexadecimal string to it's integer representation.
+
+ # Type
+
+ ```
+ fromHexString :: String -> [ String ]
+ ```
+
+ # Examples
+
+ ```nix
+ fromHexString "FF"
+ => 255
+
+ fromHexString (builtins.hashString "sha256" "test")
+ => 9223372036854775807
+ ```
+ */
+ fromHexString = value:
+ let
+ noPrefix = lib.strings.removePrefix "0x" (lib.strings.toLower value);
+ in let
+ parsed = builtins.fromTOML "v=0x${noPrefix}";
+ in parsed.v;
+
+ /**
Convert the given positive integer to a string of its hexadecimal
representation. For example: