diff --git a/lib/default.nix b/lib/default.nix index fe737a125e680..d6a4345ed4562 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -47,6 +47,7 @@ let # misc asserts = callLibs ./asserts.nix; debug = callLibs ./debug.nix; + int-representations = callLibs ./int-representations.nix; misc = callLibs ./deprecated.nix; # domain-specific @@ -75,7 +76,7 @@ let info showWarnings nixpkgsVersion version isInOldestRelease mod compare splitByAndCompare functionArgs setFunctionArgs isFunction toFunction - toHexString toBaseDigits inPureEvalMode; + mkLookupTable fromHexString toHexString toBaseDigits inPureEvalMode; inherit (self.fixedPoints) fix fix' converge extends composeExtensions composeManyExtensions makeExtensible makeExtensibleWithCustomName; inherit (self.attrsets) attrByPath hasAttrByPath setAttrByPath diff --git a/lib/int-representations.nix b/lib/int-representations.nix new file mode 100644 index 0000000000000..818cfe1c2dd94 --- /dev/null +++ b/lib/int-representations.nix @@ -0,0 +1,78 @@ +/* Collection of functions for bit representation conversions such as binary, + octal, decimal and hexadecimal. + + These functions works by using a bit mapping, a attrset that maps each + character to a numerical value. The base of the numeric system is the length + of the keys of this attrset. +*/ + +{ lib }: + +{ + /* Default bit mappings for the most common bases */ + mappings = { # maps characters to values + bin = [ "0" "1"]; + oct = [ "0" "1" "2" "3" "4" "5" "6" "7" ]; + dec = [ "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" ]; + hex = [ "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F" ]; + }; + + /* Convert an integer to the string representation following the mapping. + + Type: toStringRepresentation :: attrs -> int -> string + + Example: + toStringRepresentation bitMappings.hex 255 + => "FF" + */ + toStringRepresentation = + # List of symbols of the base alphabet. Base number is inferred from the size. + mapping: + # Integer to be converted + number: + let + mappingBase = lib.length mapping; + digits = lib.toBaseDigits mappingBase number; + + chars = map (digit: lib.elemAt mapping digit) digits; + + in lib.concatStringsSep "" chars; + + /* Convert a string representation to integer using the mapping. + + Note that this function doesn't support prefixes such as 0x for + hexadecimal. + + Type: fromStringRepresentation :: attrs -> string -> int + + Example: + fromStringRepresentation bitMappings.hex "ff" + => 255 + */ + fromStringRepresentation = + # List of symbols of the base alphabet. Base number is inferred from the size. + mapping: + # String representation to be converted + repr: + let + throwInvalidChar = throw "invalid symbol in '${repr}', all valid symbols in this mapping: ${lib.concatStringsSep ", " mapping}"; + + lookupTable = lib.mkLookupTable mapping; + + # which base? + mappingMultiplier = lib.length mapping; + + chars = lib.splitString "" repr; + nonEmptyChars = (lib.filter (c: c != "")) chars; + + getCharValue = c: lookupTable.${c} or throwInvalidChar; + + charValues = map getCharValue nonEmptyChars; + reversedChars = lib.reverseList charValues; + + convertChars = charList: + if lib.length charList == 0 + then 0 + else (mappingMultiplier * (convertChars (lib.tail charList))) + (lib.head charList); + in convertChars reversedChars; +} diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index 0d30e93aafb9d..cc6fee52e1f86 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -159,6 +159,31 @@ runTests { expected = "FA"; }; + testFromHexString = { + expr = fromHexString "FA"; + expected = 250; + }; + + testFromHexStringHashedSHA512 = { + expr = fromHexString (toUpper (builtins.hashString "sha512" "test")); + expected = -8183225288127633153; # the overflow is expected + }; + + testFromHexStringHashedSHA256 = { + expr = fromHexString (toUpper (builtins.hashString "sha256" "test")); + expected = -3360410906529887736; # the overflow is expected + }; + + testFromHexStringHashedSHA1 = { + expr = fromHexString (toUpper (builtins.hashString "sha1" "test")); + expected = -3201521091500590125; # the overflow is expected + }; + + testFromHexStringHashedMD5 = { + expr = fromHexString (toUpper (builtins.hashString "md5" "test")); + expected = -3828536308030524170; # the overflow is expected + }; + testToBaseDigits = { expr = toBaseDigits 2 6; expected = [ 1 1 0 ]; @@ -1939,4 +1964,52 @@ runTests { testGetExe'FailureSecondArg = testingThrow ( getExe' { type = "derivation"; } "dir/executable" ); + + # BIT REPRESENTATION + testIntRepresentationsParseZero = { + expr = mapAttrs (k: v: int-representations.fromStringRepresentation v "0") int-representations.mappings; + expected = mapAttrs (k: v: 0) int-representations.mappings; + }; + + testIntRepresentationsStringifyMaxPlusOne = { + expr = int-representations.toStringRepresentation int-representations.mappings.hex 16; + expected = "10"; + }; + + testIntRepresentationsStringifyExample = { + expr = int-representations.toStringRepresentation int-representations.mappings.hex 255; + expected = "FF"; + }; + + testIntRepresentationsParseExample = { + expr = int-representations.fromStringRepresentation int-representations.mappings.hex "FF"; + expected = 255; + }; + + # python code used to generate these numbers: + # from random import randint; x = randint(0, 999999); hx = hex(x); print(x, hx) + testIntRepresentationsStringifyFromPython-1 = { + expr = int-representations.toStringRepresentation int-representations.mappings.hex 158530; + expected = "26B42"; + }; + + testIntRepresentationsStringifyFromPython-2 = { + expr = int-representations.toStringRepresentation int-representations.mappings.hex 10374; + expected = "2886"; + }; + + testIntRepresentationsStringifyFromPython-3 = { + expr = int-representations.toStringRepresentation int-representations.mappings.hex 741819; + expected = "B51BB"; + }; + + testIntRepresentationStringifyCustomRepresentation = { + expr = int-representations.toStringRepresentation [ "A" "U" "C" "G" ] 420; + expected = "UCCUA"; + }; + testIntRepresentationParseCustomRepresentation = { + expr = int-representations.fromStringRepresentation [ "A" "U" "C" "G" ] "UCCUA"; + expected = 420; + }; + } diff --git a/lib/trivial.nix b/lib/trivial.nix index c23fc6070be46..4ecce91233929 100644 --- a/lib/trivial.nix +++ b/lib/trivial.nix @@ -467,6 +467,46 @@ rec { then v else k: v; + /* Builds a lookup table from a list of values so + getting the index of a value becomes $O(1)$ instead + of $O(n)$ + + Type: mkLookupTable :: list -> attrs + + Example: + mkLookupTable [ "a" "b" "c" ] + => {a = 0; b = 1; c = 2; } + */ + mkLookupTable = + # List of values that are or can be converted to string. + mapping: + let + foldFn = a: b: a // { + "${b}" = a._index; + _index = a._index + 1; + }; + folded = lib.foldl foldFn {_index = 0;} mapping; + in lib.attrsets.removeAttrs folded [ "_index" ]; + + + /* Convert a hexadecimal representation to a positive integer + + Type: fromHexString :: string -> int + + Example: + fromHexString "0" + => 0 + + fromHexString "10" + => 16 + + fromHexString "FA" + => 250 + */ + fromHexString = representation: + lib.int-representations.fromStringRepresentation + lib.int-representations.mappings.hex representation; + /* Convert the given positive integer to a string of its hexadecimal representation. For example: @@ -477,21 +517,8 @@ rec { toHexString 250 => "FA" */ toHexString = i: - let - toHexDigit = d: - if d < 10 - then toString d - else - { - "10" = "A"; - "11" = "B"; - "12" = "C"; - "13" = "D"; - "14" = "E"; - "15" = "F"; - }.${toString d}; - in - lib.concatMapStrings toHexDigit (toBaseDigits 16 i); + lib.int-representations.toStringRepresentation + lib.int-representations.mappings.hex i; /* `toBaseDigits base i` converts the positive integer i to a list of its digits in the given base. For example: