Merge pull request #173949 from jacoblambda/fix-toInt-zero-padding

lib: add strings.toIntBase10 to parse zero-padded strings

Changed files
+160 -8
lib
+1 -1
lib/default.nix
···
getName getVersion
nameFromURL enableFeature enableFeatureAs withFeature
withFeatureAs fixedWidthString fixedWidthNumber isStorePath
-
toInt readPathsFromFile fileContents;
+
toInt toIntBase10 readPathsFromFile fileContents;
inherit (self.stringsWithDeps) textClosureList textClosureMap
noDepEntry fullDepEntry packEntry stringAfter;
inherit (self.customisation) overrideDerivation makeOverridable
+87 -6
lib/strings.nix
···
else
false;
-
/* Parse a string as an int.
+
/* Parse a string as an int. Does not support parsing of integers with preceding zero due to
+
ambiguity between zero-padded and octal numbers. See toIntBase10.
Type: string -> int
Example:
+
toInt "1337"
=> 1337
+
toInt "-4"
=> -4
+
+
toInt " 123 "
+
=> 123
+
+
toInt "00024"
+
=> error: Ambiguity in interpretation of 00024 between octal and zero padded integer.
+
toInt "3.14"
=> error: floating point JSON numbers are not supported
*/
-
# Obviously, it is a bit hacky to use fromJSON this way.
toInt = str:
-
let may_be_int = fromJSON str; in
-
if isInt may_be_int
-
then may_be_int
-
else throw "Could not convert ${str} to int.";
+
let
+
# RegEx: Match any leading whitespace, then any digits, and finally match any trailing
+
# whitespace.
+
strippedInput = match "[[:space:]]*([[:digit:]]+)[[:space:]]*" str;
+
+
# RegEx: Match a leading '0' then one or more digits.
+
isLeadingZero = match "0[[:digit:]]+" (head strippedInput) == [];
+
+
# Attempt to parse input
+
parsedInput = fromJSON (head strippedInput);
+
+
generalError = "toInt: Could not convert ${escapeNixString str} to int.";
+
+
octalAmbigError = "toInt: Ambiguity in interpretation of ${escapeNixString str}"
+
+ " between octal and zero padded integer.";
+
+
in
+
# Error on presence of non digit characters.
+
if strippedInput == null
+
then throw generalError
+
# Error on presence of leading zero/octal ambiguity.
+
else if isLeadingZero
+
then throw octalAmbigError
+
# Error if parse function fails.
+
else if !isInt parsedInput
+
then throw generalError
+
# Return result.
+
else parsedInput;
+
+
+
/* Parse a string as a base 10 int. This supports parsing of zero-padded integers.
+
+
Type: string -> int
+
+
Example:
+
toIntBase10 "1337"
+
=> 1337
+
+
toIntBase10 "-4"
+
=> -4
+
+
toIntBase10 " 123 "
+
=> 123
+
+
toIntBase10 "00024"
+
=> 24
+
+
toIntBase10 "3.14"
+
=> error: floating point JSON numbers are not supported
+
*/
+
toIntBase10 = str:
+
let
+
# RegEx: Match any leading whitespace, then match any zero padding, capture any remaining
+
# digits after that, and finally match any trailing whitespace.
+
strippedInput = match "[[:space:]]*0*([[:digit:]]+)[[:space:]]*" str;
+
+
# RegEx: Match at least one '0'.
+
isZero = match "0+" (head strippedInput) == [];
+
+
# Attempt to parse input
+
parsedInput = fromJSON (head strippedInput);
+
+
generalError = "toIntBase10: Could not convert ${escapeNixString str} to int.";
+
+
in
+
# Error on presence of non digit characters.
+
if strippedInput == null
+
then throw generalError
+
# In the special case zero-padded zero (00000), return early.
+
else if isZero
+
then 0
+
# Error if parse function fails.
+
else if !isInt parsedInput
+
then throw generalError
+
# Return result.
+
else parsedInput;
/* Read a list of paths from `file`, relative to the `rootPath`.
Lines beginning with `#` are treated as comments and ignored.
+71
lib/tests/misc.nix
···
expected = "Hello\\x20World";
};
+
testToInt = testAllTrue [
+
# Naive
+
(123 == toInt "123")
+
(0 == toInt "0")
+
# Whitespace Padding
+
(123 == toInt " 123")
+
(123 == toInt "123 ")
+
(123 == toInt " 123 ")
+
(123 == toInt " 123 ")
+
(0 == toInt " 0")
+
(0 == toInt "0 ")
+
(0 == toInt " 0 ")
+
];
+
+
testToIntFails = testAllTrue [
+
( builtins.tryEval (toInt "") == { success = false; value = false; } )
+
( builtins.tryEval (toInt "123 123") == { success = false; value = false; } )
+
( builtins.tryEval (toInt "0 123") == { success = false; value = false; } )
+
( builtins.tryEval (toInt " 0d ") == { success = false; value = false; } )
+
( builtins.tryEval (toInt " 1d ") == { success = false; value = false; } )
+
( builtins.tryEval (toInt " d0 ") == { success = false; value = false; } )
+
( builtins.tryEval (toInt "00") == { success = false; value = false; } )
+
( builtins.tryEval (toInt "01") == { success = false; value = false; } )
+
( builtins.tryEval (toInt "002") == { success = false; value = false; } )
+
( builtins.tryEval (toInt " 002 ") == { success = false; value = false; } )
+
( builtins.tryEval (toInt " foo ") == { success = false; value = false; } )
+
( builtins.tryEval (toInt " foo 123 ") == { success = false; value = false; } )
+
( builtins.tryEval (toInt " foo123 ") == { success = false; value = false; } )
+
];
+
+
testToIntBase10 = testAllTrue [
+
# Naive
+
(123 == toIntBase10 "123")
+
(0 == toIntBase10 "0")
+
# Whitespace Padding
+
(123 == toIntBase10 " 123")
+
(123 == toIntBase10 "123 ")
+
(123 == toIntBase10 " 123 ")
+
(123 == toIntBase10 " 123 ")
+
(0 == toIntBase10 " 0")
+
(0 == toIntBase10 "0 ")
+
(0 == toIntBase10 " 0 ")
+
# Zero Padding
+
(123 == toIntBase10 "0123")
+
(123 == toIntBase10 "0000123")
+
(0 == toIntBase10 "000000")
+
# Whitespace and Zero Padding
+
(123 == toIntBase10 " 0123")
+
(123 == toIntBase10 "0123 ")
+
(123 == toIntBase10 " 0123 ")
+
(123 == toIntBase10 " 0000123")
+
(123 == toIntBase10 "0000123 ")
+
(123 == toIntBase10 " 0000123 ")
+
(0 == toIntBase10 " 000000")
+
(0 == toIntBase10 "000000 ")
+
(0 == toIntBase10 " 000000 ")
+
];
+
+
testToIntBase10Fails = testAllTrue [
+
( builtins.tryEval (toIntBase10 "") == { success = false; value = false; } )
+
( builtins.tryEval (toIntBase10 "123 123") == { success = false; value = false; } )
+
( builtins.tryEval (toIntBase10 "0 123") == { success = false; value = false; } )
+
( builtins.tryEval (toIntBase10 " 0d ") == { success = false; value = false; } )
+
( builtins.tryEval (toIntBase10 " 1d ") == { success = false; value = false; } )
+
( builtins.tryEval (toIntBase10 " d0 ") == { success = false; value = false; } )
+
( builtins.tryEval (toIntBase10 " foo ") == { success = false; value = false; } )
+
( builtins.tryEval (toIntBase10 " foo 123 ") == { success = false; value = false; } )
+
( builtins.tryEval (toIntBase10 " foo 00123 ") == { success = false; value = false; } )
+
( builtins.tryEval (toIntBase10 " foo00123 ") == { success = false; value = false; } )
+
];
+
# LISTS
testFilter = {
+1 -1
lib/tests/modules.sh
···
# Check coerced value with unsound coercion
checkConfigOutput '^12$' config.value ./declare-coerced-value-unsound.nix
checkConfigError 'A definition for option .* is not of type .*. Definition values:\n\s*- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix
-
checkConfigError 'json.exception.parse_error' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix
+
checkConfigError 'toInt: Could not convert .* to int' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix
# Check mkAliasOptionModule.
checkConfigOutput '^true$' config.enable ./alias-with-priority.nix