diff --git a/PreciseNumber.Test/PreciseNumberParseTrailingNulTests.cs b/PreciseNumber.Test/PreciseNumberParseTrailingNulTests.cs new file mode 100644 index 0000000..7fce253 --- /dev/null +++ b/PreciseNumber.Test/PreciseNumberParseTrailingNulTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.PreciseNumber.Test; + +using System.Globalization; + +/// +/// Covers the run of trailing '\0' characters the BCL parsers ignore, which text read out of a +/// fixed-size or NUL-padded buffer ends with. Each case is checked against +/// parsing the same text with the same style, so the expectation is the BCL's rather than ours. +/// +[TestClass] +public class PreciseNumberParseTrailingNulTests +{ + [TestMethod] + [DataRow("5\0", NumberStyles.None)] + [DataRow("5\0", NumberStyles.Float)] + [DataRow("1.5\0\0\0", NumberStyles.Float)] + [DataRow("1e3 \0", NumberStyles.Float)] + [DataRow("12345\0\0\0\0\0\0\0\0", NumberStyles.Integer)] + [DataRow("-0.25\0", NumberStyles.Number)] + public void ParseIgnoresTrailingNulsAsDecimalDoes(string text, NumberStyles style) + { + CultureInfo culture = CultureInfo.InvariantCulture; + Assert.IsTrue(decimal.TryParse(text, style, culture, out decimal expected), "The case must be one decimal accepts."); + PreciseNumber expectedNumber = expected.ToPreciseNumber(); + + Assert.AreEqual(expectedNumber, PreciseNumber.Parse(text, style, culture)); + Assert.AreEqual(expectedNumber, PreciseNumber.Parse(text.AsSpan(), style, culture)); + + Assert.IsTrue(PreciseNumber.TryParse(text, style, culture, out PreciseNumber fromString)); + Assert.AreEqual(expectedNumber, fromString); + + Assert.IsTrue(PreciseNumber.TryParse(text.AsSpan(), style, culture, out PreciseNumber fromSpan)); + Assert.AreEqual(expectedNumber, fromSpan); + } + + [TestMethod] + [DataRow("1.5\0 ", NumberStyles.Float)] + [DataRow("\0 5", NumberStyles.Float)] + [DataRow("\0", NumberStyles.Float)] + [DataRow("\0\0\0", NumberStyles.Float)] + [DataRow("5 \0", NumberStyles.None)] + [DataRow("5\0" + "1", NumberStyles.Float)] + public void ParseStillRejectsNulsThatAreNotTheLastCharacters(string text, NumberStyles style) + { + CultureInfo culture = CultureInfo.InvariantCulture; + Assert.IsFalse(decimal.TryParse(text, style, culture, out _), "The case must be one decimal rejects."); + + Assert.IsFalse(PreciseNumber.TryParse(text, style, culture, out _)); + Assert.IsFalse(PreciseNumber.TryParse(text.AsSpan(), style, culture, out _)); + Assert.ThrowsExactly(() => PreciseNumber.Parse(text, style, culture)); + } +} diff --git a/PreciseNumber/PreciseNumber.cs b/PreciseNumber/PreciseNumber.cs index 602eeaf..8a5c81c 100644 --- a/PreciseNumber/PreciseNumber.cs +++ b/PreciseNumber/PreciseNumber.cs @@ -1222,6 +1222,7 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor throw new ArgumentException(UnsupportedParseStylesMessage, nameof(style)); } + s = TrimTrailingNuls(s); s = TrimAllowedWhite(s, style); if (s.IsEmpty) @@ -1403,6 +1404,21 @@ private static PreciseNumber FromParsedParts(ReadOnlySpan digits, int deci /// private const string AllowedWhite = " \t\n\v\f\r"; + /// + /// Removes the run of '\0' characters that ends , as the BCL parsers + /// ignore it, so that text from a NUL-padded buffer parses the way reads it. + /// + /// + /// Under every style, white space included: the NULs are dropped only when they are the very last + /// characters, and only when something else comes before them, so text that is nothing but NULs + /// is still an error. + /// + private static ReadOnlySpan TrimTrailingNuls(ReadOnlySpan s) + { + ReadOnlySpan trimmed = s.TrimEnd('\0'); + return trimmed.IsEmpty ? s : trimmed; + } + /// Trims the leading and trailing white space that allows. private static ReadOnlySpan TrimAllowedWhite(ReadOnlySpan s, NumberStyles style) {