diff --git a/src/fable-library-py/src/ints.rs b/src/fable-library-py/src/ints.rs index eeb7b5a04..2b9993959 100644 --- a/src/fable-library-py/src/ints.rs +++ b/src/fable-library-py/src/ints.rs @@ -1067,9 +1067,9 @@ fn determine_radix(string: &str, style: i32, default_radix: i32) -> i32 { // `get` returns None when the string is shorter than two bytes or the // boundary falls inside a multi-byte character, so this never panics match string.get(..2) { - Some("0x") | Some("0X") => 16, - Some("0b") | Some("0B") => 2, - Some("0o") | Some("0O") => 8, + Some("0x" | "0X") => 16, + Some("0b" | "0B") => 2, + Some("0o" | "0O") => 8, _ => default_radix, } } @@ -1091,15 +1091,15 @@ fn trim_whitespace(string: &str, style: i32) -> &str { /// Removes numeric prefixes (0x, 0b, 0o) when radix is not decimal #[inline] fn remove_prefix(string: &str, radix: i32) -> &str { - if radix != 10 { - match string.get(..2) { - Some("0x") | Some("0X") | Some("0b") | Some("0B") | Some("0o") | Some("0O") => { - &string[2..] - } - _ => string, - } - } else { - string + if radix == 10 { + return string; + } + + // `get` never panics on a multi-byte boundary; `&string[2..]` is only + // reached after matching a two-byte ASCII prefix, so byte 2 is a boundary + match string.get(..2) { + Some("0x" | "0X" | "0b" | "0B" | "0o" | "0O") => &string[2..], + _ => string, } } diff --git a/src/fable-library-py/tests/test_ints.py b/src/fable-library-py/tests/test_ints.py index 1159cff3d..e51fc05fe 100644 --- a/src/fable-library-py/tests/test_ints.py +++ b/src/fable-library-py/tests/test_ints.py @@ -56,6 +56,10 @@ def test_uint64_create() -> None: assert uint64(uint32(-1)) == 4294967295 +# NumberStyles.Any (511) has no AllowHexSpecifier, so it reaches the prefix +# check in `determine_radix`. NumberStyles.HexNumber (515) short-circuits to +# radix 16 and reaches the prefix check in `remove_prefix` instead. Both used +# to panic on a multi-byte first character. @pytest.mark.parametrize( "style", [ diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index a1b368c55..8047782f0 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -160,6 +160,8 @@ let tests = (fun () -> Int32.Parse("1FFFFFFFF", System.Globalization.NumberStyles.HexNumber)) |> throwsError "" (fun () -> Int32.Parse("5foo", System.Globalization.NumberStyles.HexNumber)) |> throwsError "" (fun () -> Int32.Parse("foo5", System.Globalization.NumberStyles.HexNumber)) |> throwsError "" + // Multi-byte UTF-8 input must fail to parse, not crash (see #4823) + (fun () -> Int32.Parse("–", System.Globalization.NumberStyles.HexNumber)) |> throwsError "" testCase "System.Int64.Parse works" <| fun () -> Int64.Parse("5") |> equal 5L @@ -207,6 +209,10 @@ let tests = tryParse Int32.TryParse 0 "X9TRE34" |> equal (false, 0) tryParse Int32.TryParse 0 "9SayWhat12Huh" |> equal (false, 0) tryParse Int32.TryParse 0 "-1" |> equal (true, -1) + // Multi-byte UTF-8 input must fail to parse, not crash (see #4823) + tryParse Int32.TryParse 0 "–" |> equal (false, 0) + tryParse Int32.TryParse 0 "é" |> equal (false, 0) + tryParse Int32.TryParse 0 "𝟙" |> equal (false, 0) testCase "BigInt.TryParse works" <| fun () -> tryParse bigint.TryParse 0I "4234523548923954" |> equal (true, 4234523548923954I) diff --git a/tests/Python/TestConvert.fs b/tests/Python/TestConvert.fs index 32b228343..f8c2e4b54 100644 --- a/tests/Python/TestConvert.fs +++ b/tests/Python/TestConvert.fs @@ -160,6 +160,8 @@ let ``test System.Int32.Parse with hex works`` () = (fun () -> Int32.Parse("1FFFFFFFF", System.Globalization.NumberStyles.HexNumber)) |> throwsError "" (fun () -> Int32.Parse("5foo", System.Globalization.NumberStyles.HexNumber)) |> throwsError "" (fun () -> Int32.Parse("foo5", System.Globalization.NumberStyles.HexNumber)) |> throwsError "" + // Multi-byte UTF-8 input must fail to parse, not crash (see #4823) + (fun () -> Int32.Parse("–", System.Globalization.NumberStyles.HexNumber)) |> throwsError "" [] let ``test System.Int64.Parse works`` () = @@ -214,6 +216,10 @@ let ``test System.Int32.TryParse works`` () = tryParse Int32.TryParse 0 "X9TRE34" |> equal (false, 0) tryParse Int32.TryParse 0 "9SayWhat12Huh" |> equal (false, 0) tryParse Int32.TryParse 0 "-1" |> equal (true, -1) + // Multi-byte UTF-8 input must fail to parse, not crash (see #4823) + tryParse Int32.TryParse 0 "–" |> equal (false, 0) + tryParse Int32.TryParse 0 "é" |> equal (false, 0) + tryParse Int32.TryParse 0 "𝟙" |> equal (false, 0) [] let ``test BigInt.TryParse works`` () =