Unverified Commit bc4ce318 authored by Robert Hensing's avatar Robert Hensing Committed by GitHub
Browse files

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

lib: add strings.toIntBase10 to parse zero-padded strings
parents df9da891 ed711738
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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
+87 −6
Original line number Diff line number Diff line
@@ -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.
+71 −0
Original line number Diff line number Diff line
@@ -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 = {
+1 −1
Original line number Diff line number Diff line
@@ -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