From 970cdd559cee5f72ecf7496ae34d3c3a71cdb721 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 15:32:03 +0000 Subject: [PATCH] Ignore a run of trailing NULs in Parse, as decimal and double do [patch] Text from a fixed-size or NUL-padded buffer ends with '\0' characters, which the BCL parsers drop. Parse and TryParse now drop one trailing run of them before anything else, under every style, unless the text is nothing but NULs. NULs anywhere else, or followed by white space, are still errors. Fixes ktsu-dev/PreciseNumber#193 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW4sJAUyKbe8WkeBPVB --- .../PreciseNumberParseTrailingNulTests.cs | 54 +++++++++++++++++++ PreciseNumber/PreciseNumber.cs | 16 ++++++ 2 files changed, 70 insertions(+) create mode 100644 PreciseNumber.Test/PreciseNumberParseTrailingNulTests.cs 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) {