summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorRobert Hensing <roberth@users.noreply.github.com>2022-10-24 13:22:42 +0200
committerGitHub <noreply@github.com>2022-10-24 13:22:42 +0200
commitbc4ce318bf6aa52031ad891675c573ce31308c8d (patch)
treefde3afb99befe8b7cfa46ae90cab9536bf19cdf6 /lib
parentdf9da891637929d52a8a189ea3399c1ee555ef51 (diff)
parented71173841618bd4c69f40d07fb467ccabc5db0b (diff)
Merge pull request #173949 from jacoblambda/fix-toInt-zero-padding
lib: add strings.toIntBase10 to parse zero-padded strings
Diffstat (limited to 'lib')
-rw-r--r--lib/default.nix2
-rw-r--r--lib/strings.nix93
-rw-r--r--lib/tests/misc.nix71
-rwxr-xr-xlib/tests/modules.sh2
4 files changed, 160 insertions, 8 deletions
diff --git a/lib/default.nix b/lib/default.nix
index 0c0e2d5e1021..8bb06954518b 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -103,7 +103,7 @@ let
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
diff --git a/lib/strings.nix b/lib/strings.nix
index af26532aa430..b5f5a4d9060b 100644
--- a/lib/strings.nix
+++ b/lib/strings.nix
@@ -783,24 +783,105 @@ rec {
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.
diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix
index 8e0cf1f45bb6..31c938a8ffda 100644
--- a/lib/tests/misc.nix
+++ b/lib/tests/misc.nix
@@ -327,6 +327,77 @@ runTests {
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 = {
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index 2be9b5835090..c9ea674ee104 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -162,7 +162,7 @@ checkConfigError 'A definition for option .* is not.*string or signed integer co
# 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