From 97d80108e9983e2f6a8f2e5f7e9f622a5448d6e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 08:25:58 +0000 Subject: [PATCH] Reject hex and binary NumberStyles in Parse and TryParse [patch] Parse and TryParse never looked at AllowHexSpecifier or AllowBinarySpecifier, so Parse("10", NumberStyles.HexNumber) read the digits as decimal and returned 10, with no error. Both styles now throw ArgumentException, as decimal and double do, from Parse and TryParse alike. Fixes #177 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5ShapPU8BXcXvikJm --- .../PreciseNumberParseCultureTests.cs | 22 +++++++++++++++++++ PreciseNumber/PreciseNumber.cs | 15 +++++++++++++ 2 files changed, 37 insertions(+) diff --git a/PreciseNumber.Test/PreciseNumberParseCultureTests.cs b/PreciseNumber.Test/PreciseNumberParseCultureTests.cs index 59108e9..1ad4117 100644 --- a/PreciseNumber.Test/PreciseNumberParseCultureTests.cs +++ b/PreciseNumber.Test/PreciseNumberParseCultureTests.cs @@ -3,6 +3,7 @@ namespace ktsu.PreciseNumber.Test; using System.Globalization; +using System.Numerics; [TestClass] public class PreciseNumberParseCultureTests @@ -135,6 +136,27 @@ public void Parse_ThrowsForWhatTheStyleDoesNotAllow() Assert.ThrowsExactly(() => PreciseNumber.Parse("1e 5", NumberStyles.Float, CultureInfo.InvariantCulture)); } + [TestMethod] + [DataRow("10", NumberStyles.HexNumber)] + [DataRow("A", NumberStyles.HexNumber)] + [DataRow("10", NumberStyles.AllowHexSpecifier)] + [DataRow("1010", NumberStyles.BinaryNumber)] + [DataRow("1010", NumberStyles.AllowBinarySpecifier)] + public void ParseAndTryParse_RejectHexAndBinaryStylesAsDecimalDoes(string text, NumberStyles style) + { + Assert.ThrowsExactly(() => decimal.Parse(text, style, CultureInfo.InvariantCulture)); + + Assert.ThrowsExactly(() => PreciseNumber.Parse(text, style, CultureInfo.InvariantCulture)); + Assert.ThrowsExactly(() => PreciseNumber.Parse(text.AsSpan(), style, CultureInfo.InvariantCulture)); + Assert.ThrowsExactly(() => PreciseNumber.TryParse(text, style, CultureInfo.InvariantCulture, out _)); + Assert.ThrowsExactly(() => PreciseNumber.TryParse(text.AsSpan(), style, CultureInfo.InvariantCulture, out _)); + Assert.ThrowsExactly(() => ParseGeneric(text, style)); + } + + private static T ParseGeneric(string text, NumberStyles style) + where T : INumberBase => + T.Parse(text, style, CultureInfo.InvariantCulture); + [TestMethod] public void Parse_WithoutAStyleStillReadsFractionsExponentsAndSigns() { diff --git a/PreciseNumber/PreciseNumber.cs b/PreciseNumber/PreciseNumber.cs index 40355c7..9e9167e 100644 --- a/PreciseNumber/PreciseNumber.cs +++ b/PreciseNumber/PreciseNumber.cs @@ -48,6 +48,14 @@ public readonly partial record struct PreciseNumber /// The message carried by the that parsing throws. /// private const string InvalidFormatMessage = "Input string was not in a correct format."; + private const string UnsupportedParseStylesMessage = "The number styles AllowHexSpecifier and AllowBinarySpecifier are not supported by PreciseNumber."; + +#if NET8_0_OR_GREATER + private const NumberStyles UnsupportedParseStyles = NumberStyles.AllowHexSpecifier | NumberStyles.AllowBinarySpecifier; +#else + // NumberStyles.AllowBinarySpecifier arrived in .NET 8, but a caller on net7.0 can still pass its value. + private const NumberStyles UnsupportedParseStyles = NumberStyles.AllowHexSpecifier | (NumberStyles)0x400; +#endif /// /// The fewest significant digits produces @@ -1203,6 +1211,13 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor // Parse(x.ToString(p), p) round-trips for every provider. NumberFormatInfo numberFormat = NumberFormatInfo.GetInstance(provider ?? InvariantCulture); + // Hex and binary input would be read as decimal digits and come back as the wrong number, + // so refuse those styles outright, as decimal and double do. + if ((style & UnsupportedParseStyles) != 0) + { + throw new ArgumentException(UnsupportedParseStylesMessage, nameof(style)); + } + s = TrimAllowedWhite(s, style); if (s.IsEmpty)