From 1094879c1a89525180137d9a16f9b28a68b0bd97 Mon Sep 17 00:00:00 2001 From: kzrnm Date: Fri, 12 Sep 2025 05:02:38 +0900 Subject: [PATCH 1/5] ValueStringBuilder.EnsureTerminated --- .../ValueStringBuilder.EnsureTerminated.cs | 20 +++++++++++++ .../src/System/Text/ValueStringBuilder.cs | 28 ------------------- .../src/System.Diagnostics.Process.csproj | 4 ++- .../src/System/Diagnostics/Process.Windows.cs | 6 ++-- .../System.IO.FileSystem.AccessControl.csproj | 2 ++ .../System.Private.CoreLib.Shared.projitems | 3 ++ .../src/System/IO/PathHelper.Windows.cs | 6 ++-- 7 files changed, 36 insertions(+), 33 deletions(-) create mode 100644 src/libraries/Common/src/System/Text/ValueStringBuilder.EnsureTerminated.cs diff --git a/src/libraries/Common/src/System/Text/ValueStringBuilder.EnsureTerminated.cs b/src/libraries/Common/src/System/Text/ValueStringBuilder.EnsureTerminated.cs new file mode 100644 index 00000000000000..c78c050a13132e --- /dev/null +++ b/src/libraries/Common/src/System/Text/ValueStringBuilder.EnsureTerminated.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace System.Text +{ + internal ref partial struct ValueStringBuilder + { + /// + /// Ensures that the builder is terminated with a NUL character. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void EnsureTerminated() + { + EnsureCapacity(_pos + 1); + _chars[_pos] = '\0'; + } + } +} diff --git a/src/libraries/Common/src/System/Text/ValueStringBuilder.cs b/src/libraries/Common/src/System/Text/ValueStringBuilder.cs index e5c8610cd60dc5..f729484b9b39c5 100644 --- a/src/libraries/Common/src/System/Text/ValueStringBuilder.cs +++ b/src/libraries/Common/src/System/Text/ValueStringBuilder.cs @@ -64,20 +64,6 @@ public ref char GetPinnableReference() return ref MemoryMarshal.GetReference(_chars); } - /// - /// Get a pinnable reference to the builder. - /// - /// Ensures that the builder has a null char after - public ref char GetPinnableReference(bool terminate) - { - if (terminate) - { - EnsureCapacity(Length + 1); - _chars[Length] = '\0'; - } - return ref MemoryMarshal.GetReference(_chars); - } - public ref char this[int index] { get @@ -97,20 +83,6 @@ public override string ToString() /// Returns the underlying storage of the builder. public Span RawChars => _chars; - /// - /// Returns a span around the contents of the builder. - /// - /// Ensures that the builder has a null char after - public ReadOnlySpan AsSpan(bool terminate) - { - if (terminate) - { - EnsureCapacity(Length + 1); - _chars[Length] = '\0'; - } - return _chars.Slice(0, _pos); - } - public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index ad692fafc90f2c..a407df98e7efa7 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent) @@ -225,6 +225,8 @@ + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index dc05e138a5aec6..20b4d079a42354 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -545,9 +545,10 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) logonFlags = Interop.Advapi32.LogonFlags.LOGON_NETCREDENTIALS_ONLY; } + commandLine.EnsureTerminated(); fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty) fixed (char* environmentBlockPtr = environmentBlock) - fixed (char* commandLinePtr = &commandLine.GetPinnableReference(terminate: true)) + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) { IntPtr passwordPtr = (startInfo.Password != null) ? Marshal.SecureStringToGlobalAllocUnicode(startInfo.Password) : IntPtr.Zero; @@ -579,8 +580,9 @@ ref processInfo // pointer to PROCESS_INFORMATION } else { + commandLine.EnsureTerminated(); fixed (char* environmentBlockPtr = environmentBlock) - fixed (char* commandLinePtr = &commandLine.GetPinnableReference(terminate: true)) + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) { retVal = Interop.Kernel32.CreateProcess( null, // we don't need this since all the info is in commandLine diff --git a/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj b/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj index 3df2d07dc15e4b..d7c9d227c3c08a 100644 --- a/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj +++ b/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj @@ -78,6 +78,8 @@ Link="Common\System\IO\Win32Marshal.cs" /> + Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs + + Common\System\Text\ValueStringBuilder.EnsureTerminated.cs + Common\System\Threading\ITimer.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/PathHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/PathHelper.Windows.cs index ff479ce957f2c7..537d5cead7f216 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/PathHelper.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/PathHelper.Windows.cs @@ -51,7 +51,8 @@ internal static string Normalize(ref ValueStringBuilder path) var builder = new ValueStringBuilder(stackalloc char[PathInternal.MaxShortPath]); // Get the full path - GetFullPathName(path.AsSpan(terminate: true), ref builder); + path.EnsureTerminated(); + GetFullPathName(path.AsSpan(), ref builder); string result = builder.AsSpan().IndexOf('~') >= 0 ? TryExpandShortFileName(ref builder, originalPath: null) @@ -176,8 +177,9 @@ internal static string TryExpandShortFileName(ref ValueStringBuilder outputBuild while (!success) { + inputBuilder.EnsureTerminated(); uint result = Interop.Kernel32.GetLongPathNameW( - ref inputBuilder.GetPinnableReference(terminate: true), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity); + ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity); // Replace any temporary null we added if (inputBuilder[foundIndex] == '\0') inputBuilder[foundIndex] = '\\'; From 41a1695bcfc20aade95ed79f617ae2a2050eaee1 Mon Sep 17 00:00:00 2001 From: kzrnm Date: Fri, 12 Sep 2025 05:04:11 +0900 Subject: [PATCH 2/5] Standardize AppendFormat between StringBuilder and ValueStringBuilder --- .../System.Private.CoreLib.Shared.projitems | 1 + .../src/System/IO/StreamWriter.cs | 2 +- .../src/System/String.Manipulation.cs | 2 +- .../src/System/Text/StringBuilder.cs | 279 +---------------- .../src/System/Text/StringBuilderInternal.cs | 295 ++++++++++++++++++ .../Text/ValueStringBuilder.AppendFormat.cs | 274 +--------------- 6 files changed, 313 insertions(+), 540 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderInternal.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 9ac221df03c431..00b3bbaae5b579 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1216,6 +1216,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs index 229509254e1d99..72a683c2cb2c22 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs @@ -505,7 +505,7 @@ private void WriteFormatHelper(string format, ReadOnlySpan args, bool a new ValueStringBuilder(stackalloc char[256]) : new ValueStringBuilder(estimatedLength); - vsb.AppendFormatHelper(null, format!, args); // AppendFormatHelper will appropriately throw ArgumentNullException for a null format + vsb.AppendFormat(null, format!, args); // AppendFormatHelper will appropriately throw ArgumentNullException for a null format WriteSpan(vsb.AsSpan(), appendNewLine); diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs index 46738b2c59bafe..9a21709e9277fa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs @@ -540,7 +540,7 @@ private static string FormatHelper(IFormatProvider? provider, string format, Rea var sb = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); sb.EnsureCapacity(format.Length + args.Length * 8); - sb.AppendFormatHelper(provider, format, args); + sb.AppendFormat(provider, format, args); return sb.ToString(); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs index a159895c0ed721..175b2e7e97ec23 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs @@ -21,7 +21,7 @@ namespace System.Text // class to carry out modifications upon strings. [Serializable] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public sealed partial class StringBuilder : ISerializable + public sealed partial class StringBuilder : ISerializable, IStringBuilderInternal { // A StringBuilder is internally represented as a linked list of blocks each of which holds // a chunk of the string. It turns out string as a whole can also be represented as just a chunk, @@ -1518,274 +1518,9 @@ public StringBuilder AppendFormat(IFormatProvider? provider, [StringSyntax(Strin /// public StringBuilder AppendFormat(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan args) { - ArgumentNullException.ThrowIfNull(format); - - // Undocumented exclusive limits on the range for Argument Hole Index and Argument Hole Alignment. - const int IndexLimit = 1_000_000; // Note: 0 <= ArgIndex < IndexLimit - const int WidthLimit = 1_000_000; // Note: -WidthLimit < ArgAlign < WidthLimit - - // Query the provider (if one was supplied) for an ICustomFormatter. If there is one, - // it needs to be used to transform all arguments. - ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter)); - - // Repeatedly find the next hole and process it. - int pos = 0; - char ch; - while (true) - { - // Skip until either the end of the input or the first unescaped opening brace, whichever comes first. - // Along the way we need to also unescape escaped closing braces. - while (true) - { - // Find the next brace. If there isn't one, the remainder of the input is text to be appended, and we're done. - if ((uint)pos >= (uint)format.Length) - { - return this; - } - - ReadOnlySpan remainder = format.AsSpan(pos); - int countUntilNextBrace = remainder.IndexOfAny('{', '}'); - if (countUntilNextBrace < 0) - { - Append(remainder); - return this; - } - - // Append the text until the brace. - Append(remainder.Slice(0, countUntilNextBrace)); - pos += countUntilNextBrace; - - // Get the brace. It must be followed by another character, either a copy of itself in the case of being - // escaped, or an arbitrary character that's part of the hole in the case of an opening brace. - char brace = format[pos]; - ch = MoveNext(format, ref pos); - if (brace == ch) - { - Append(ch); - pos++; - continue; - } - - // This wasn't an escape, so it must be an opening brace. - if (brace != '{') - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnexpectedClosingBrace); - } - - // Proceed to parse the hole. - break; - } - - // We're now positioned just after the opening brace of an argument hole, which consists of - // an opening brace, an index, an optional width preceded by a comma, and an optional format - // preceded by a colon, with arbitrary amounts of spaces throughout. - int width = 0; - bool leftJustify = false; - ReadOnlySpan itemFormatSpan = default; // used if itemFormat is null - - // First up is the index parameter, which is of the form: - // at least on digit - // optional any number of spaces - // We've already read the first digit into ch. - Debug.Assert(format[pos - 1] == '{'); - Debug.Assert(ch != '{'); - int index = ch - '0'; - if ((uint)index >= 10u) - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); - } - - // Common case is a single digit index followed by a closing brace. If it's not a closing brace, - // proceed to finish parsing the full hole format. - ch = MoveNext(format, ref pos); - if (ch != '}') - { - // Continue consuming optional additional digits. - while (char.IsAsciiDigit(ch) && index < IndexLimit) - { - index = index * 10 + ch - '0'; - ch = MoveNext(format, ref pos); - } - - // Consume optional whitespace. - while (ch == ' ') - { - ch = MoveNext(format, ref pos); - } - - // Parse the optional alignment, which is of the form: - // comma - // optional any number of spaces - // optional - - // at least one digit - // optional any number of spaces - if (ch == ',') - { - // Consume optional whitespace. - do - { - ch = MoveNext(format, ref pos); - } - while (ch == ' '); - - // Consume an optional minus sign indicating left alignment. - if (ch == '-') - { - leftJustify = true; - ch = MoveNext(format, ref pos); - } - - // Parse alignment digits. The read character must be a digit. - width = ch - '0'; - if ((uint)width >= 10u) - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); - } - ch = MoveNext(format, ref pos); - while (char.IsAsciiDigit(ch) && width < WidthLimit) - { - width = width * 10 + ch - '0'; - ch = MoveNext(format, ref pos); - } - - // Consume optional whitespace - while (ch == ' ') - { - ch = MoveNext(format, ref pos); - } - } - - // The next character needs to either be a closing brace for the end of the hole, - // or a colon indicating the start of the format. - if (ch != '}') - { - if (ch != ':') - { - // Unexpected character - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); - } - - // Search for the closing brace; everything in between is the format, - // but opening braces aren't allowed. - int startingPos = pos; - while (true) - { - ch = MoveNext(format, ref pos); - - if (ch == '}') - { - // Argument hole closed - break; - } - - if (ch == '{') - { - // Braces inside the argument hole are not supported - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); - } - } - - startingPos++; - itemFormatSpan = format.AsSpan(startingPos, pos - startingPos); - } - } - - // Construct the output for this arg hole. - Debug.Assert(format[pos] == '}'); - pos++; - string? s = null; - string? itemFormat = null; - - if ((uint)index >= (uint)args.Length) - { - ThrowHelper.ThrowFormatIndexOutOfRange(); - } - object? arg = args[index]; - - if (cf != null) - { - if (!itemFormatSpan.IsEmpty) - { - itemFormat = new string(itemFormatSpan); - } - - s = cf.Format(itemFormat, arg, provider); - } - - if (s == null) - { - // If arg is ISpanFormattable and the beginning doesn't need padding, - // try formatting it into the remaining current chunk. - if ((leftJustify || width == 0) && - arg is ISpanFormattable spanFormattableArg && - spanFormattableArg.TryFormat(RemainingCurrentChunk, out int charsWritten, itemFormatSpan, provider)) - { - if ((uint)charsWritten > (uint)RemainingCurrentChunk.Length) - { - // Untrusted ISpanFormattable implementations might return an erroneous charsWritten value, - // and m_ChunkLength might end up being used in Unsafe code, so fail if we get back an - // out-of-range charsWritten value. - ThrowHelper.ThrowFormatInvalidString(); - } - - m_ChunkLength += charsWritten; - - // Pad the end, if needed. - if (leftJustify && width > charsWritten) - { - Append(' ', width - charsWritten); - } - - // Continue to parse other characters. - continue; - } - - // Otherwise, fallback to trying IFormattable or calling ToString. - if (arg is IFormattable formattableArg) - { - if (itemFormatSpan.Length != 0) - { - itemFormat ??= new string(itemFormatSpan); - } - s = formattableArg.ToString(itemFormat, provider); - } - else - { - s = arg?.ToString(); - } - - s ??= string.Empty; - } - - // Append it to the final output of the Format String. - if (width <= s.Length) - { - Append(s); - } - else if (leftJustify) - { - Append(s); - Append(' ', width - s.Length); - } - else - { - Append(' ', width - s.Length); - Append(s); - } - - // Continue parsing the rest of the format string. - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static char MoveNext(string format, ref int pos) - { - pos++; - if ((uint)pos >= (uint)format.Length) - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); - } - return format[pos]; - } + StringBuilder sb = this; + StringBuilderInternal.AppendFormatHelper(ref sb, provider, format, args); + return this; } /// @@ -2798,6 +2533,12 @@ private void Remove(int startIndex, int count, out StringBuilder chunk, out int AssertInvariants(); } + Span IStringBuilderInternal.RemainingCurrentChunk => RemainingCurrentChunk; + void IStringBuilderInternal.Append(char item) => Append(item); + void IStringBuilderInternal.Append(char item, int count) => Append(item, count); + void IStringBuilderInternal.Append(ReadOnlySpan value) => Append(value); + void IStringBuilderInternal.UnsafeGrow(int size) => m_ChunkLength += size; + /// Provides a handler used by the language compiler to append interpolated strings into instances. [EditorBrowsable(EditorBrowsableState.Never)] [InterpolatedStringHandler] diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderInternal.cs b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderInternal.cs new file mode 100644 index 00000000000000..9e5a9459305c2f --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderInternal.cs @@ -0,0 +1,295 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System.Text +{ + internal interface IStringBuilderInternal + { + void Append(char item); + void Append(char item, int count); + void Append(ReadOnlySpan value); + void UnsafeGrow(int size); + Span RemainingCurrentChunk { get; } + } + + internal static class StringBuilderInternal + { + /// + internal static void AppendFormatHelper(ref TBuilder builder, IFormatProvider? provider, string format, ReadOnlySpan args) + where TBuilder : IStringBuilderInternal, allows ref struct + { + ArgumentNullException.ThrowIfNull(format); + + // Undocumented exclusive limits on the range for Argument Hole Index and Argument Hole Alignment. + const int IndexLimit = 1_000_000; // Note: 0 <= ArgIndex < IndexLimit + const int WidthLimit = 1_000_000; // Note: -WidthLimit < ArgAlign < WidthLimit + + // Query the provider (if one was supplied) for an ICustomFormatter. If there is one, + // it needs to be used to transform all arguments. + ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter)); + + // Repeatedly find the next hole and process it. + int pos = 0; + char ch; + while (true) + { + // Skip until either the end of the input or the first unescaped opening brace, whichever comes first. + // Along the way we need to also unescape escaped closing braces. + while (true) + { + // Find the next brace. If there isn't one, the remainder of the input is text to be appended, and we're done. + if ((uint)pos >= (uint)format.Length) + { + return; + } + + ReadOnlySpan remainder = format.AsSpan(pos); + int countUntilNextBrace = remainder.IndexOfAny('{', '}'); + if (countUntilNextBrace < 0) + { + builder.Append(remainder); + return; + } + + // Append the text until the brace. + builder.Append(remainder.Slice(0, countUntilNextBrace)); + pos += countUntilNextBrace; + + // Get the brace. It must be followed by another character, either a copy of itself in the case of being + // escaped, or an arbitrary character that's part of the hole in the case of an opening brace. + char brace = format[pos]; + ch = MoveNext(format, ref pos); + if (brace == ch) + { + builder.Append(ch); + pos++; + continue; + } + + // This wasn't an escape, so it must be an opening brace. + if (brace != '{') + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnexpectedClosingBrace); + } + + // Proceed to parse the hole. + break; + } + + // We're now positioned just after the opening brace of an argument hole, which consists of + // an opening brace, an index, an optional width preceded by a comma, and an optional format + // preceded by a colon, with arbitrary amounts of spaces throughout. + int width = 0; + bool leftJustify = false; + ReadOnlySpan itemFormatSpan = default; // used if itemFormat is null + + // First up is the index parameter, which is of the form: + // at least on digit + // optional any number of spaces + // We've already read the first digit into ch. + Debug.Assert(format[pos - 1] == '{'); + Debug.Assert(ch != '{'); + int index = ch - '0'; + if ((uint)index >= 10u) + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); + } + + // Common case is a single digit index followed by a closing brace. If it's not a closing brace, + // proceed to finish parsing the full hole format. + ch = MoveNext(format, ref pos); + if (ch != '}') + { + // Continue consuming optional additional digits. + while (char.IsAsciiDigit(ch) && index < IndexLimit) + { + index = index * 10 + ch - '0'; + ch = MoveNext(format, ref pos); + } + + // Consume optional whitespace. + while (ch == ' ') + { + ch = MoveNext(format, ref pos); + } + + // Parse the optional alignment, which is of the form: + // comma + // optional any number of spaces + // optional - + // at least one digit + // optional any number of spaces + if (ch == ',') + { + // Consume optional whitespace. + do + { + ch = MoveNext(format, ref pos); + } + while (ch == ' '); + + // Consume an optional minus sign indicating left alignment. + if (ch == '-') + { + leftJustify = true; + ch = MoveNext(format, ref pos); + } + + // Parse alignment digits. The read character must be a digit. + width = ch - '0'; + if ((uint)width >= 10u) + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); + } + ch = MoveNext(format, ref pos); + while (char.IsAsciiDigit(ch) && width < WidthLimit) + { + width = width * 10 + ch - '0'; + ch = MoveNext(format, ref pos); + } + + // Consume optional whitespace + while (ch == ' ') + { + ch = MoveNext(format, ref pos); + } + } + + // The next character needs to either be a closing brace for the end of the hole, + // or a colon indicating the start of the format. + if (ch != '}') + { + if (ch != ':') + { + // Unexpected character + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); + } + + // Search for the closing brace; everything in between is the format, + // but opening braces aren't allowed. + int startingPos = pos; + while (true) + { + ch = MoveNext(format, ref pos); + + if (ch == '}') + { + // Argument hole closed + break; + } + + if (ch == '{') + { + // Braces inside the argument hole are not supported + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); + } + } + + startingPos++; + itemFormatSpan = format.AsSpan(startingPos, pos - startingPos); + } + } + + // Construct the output for this arg hole. + Debug.Assert(format[pos] == '}'); + pos++; + string? s = null; + string? itemFormat = null; + + if ((uint)index >= (uint)args.Length) + { + ThrowHelper.ThrowFormatIndexOutOfRange(); + } + object? arg = args[index]; + + if (cf != null) + { + if (!itemFormatSpan.IsEmpty) + { + itemFormat = new string(itemFormatSpan); + } + + s = cf.Format(itemFormat, arg, provider); + } + + if (s == null) + { + // If arg is ISpanFormattable and the beginning doesn't need padding, + // try formatting it into the remaining current chunk. + if ((leftJustify || width == 0) && + arg is ISpanFormattable spanFormattableArg && + spanFormattableArg.TryFormat(builder.RemainingCurrentChunk, out int charsWritten, itemFormatSpan, provider)) + { + if ((uint)charsWritten > (uint)builder.RemainingCurrentChunk.Length) + { + // Untrusted ISpanFormattable implementations might return an erroneous charsWritten value, + // and m_ChunkLength might end up being used in Unsafe code, so fail if we get back an + // out-of-range charsWritten value. + ThrowHelper.ThrowFormatInvalidString(); + } + + builder.UnsafeGrow(charsWritten); + + // Pad the end, if needed. + if (leftJustify && width > charsWritten) + { + builder.Append(' ', width - charsWritten); + } + + // Continue to parse other characters. + continue; + } + + // Otherwise, fallback to trying IFormattable or calling ToString. + if (arg is IFormattable formattableArg) + { + if (itemFormatSpan.Length != 0) + { + itemFormat ??= new string(itemFormatSpan); + } + s = formattableArg.ToString(itemFormat, provider); + } + else + { + s = arg?.ToString(); + } + + s ??= string.Empty; + } + + // Append it to the final output of the Format String. + if (width <= s.Length) + { + builder.Append(s); + } + else if (leftJustify) + { + builder.Append(s); + builder.Append(' ', width - s.Length); + } + else + { + builder.Append(' ', width - s.Length); + builder.Append(s); + } + + // Continue parsing the rest of the format string. + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static char MoveNext(string format, ref int pos) + { + pos++; + if ((uint)pos >= (uint)format.Length) + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); + } + return format[pos]; + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs index 4c618953c237a7..c815e6324af7ff 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs @@ -1,278 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - namespace System.Text { - internal ref partial struct ValueStringBuilder + internal ref partial struct ValueStringBuilder : IStringBuilderInternal { - // Copied from StringBuilder, can't be done via generic extension - // as ValueStringBuilder is a ref struct and cannot be used in a generic. - internal void AppendFormatHelper(IFormatProvider? provider, string format, ReadOnlySpan args) - { - ArgumentNullException.ThrowIfNull(format); - - // Undocumented exclusive limits on the range for Argument Hole Index and Argument Hole Alignment. - const int IndexLimit = 1_000_000; // Note: 0 <= ArgIndex < IndexLimit - const int WidthLimit = 1_000_000; // Note: -WidthLimit < ArgAlign < WidthLimit - - // Query the provider (if one was supplied) for an ICustomFormatter. If there is one, - // it needs to be used to transform all arguments. - ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter)); - - // Repeatedly find the next hole and process it. - int pos = 0; - char ch; - while (true) - { - // Skip until either the end of the input or the first unescaped opening brace, whichever comes first. - // Along the way we need to also unescape escaped closing braces. - while (true) - { - // Find the next brace. If there isn't one, the remainder of the input is text to be appended, and we're done. - if ((uint)pos >= (uint)format.Length) - { - return; - } - - ReadOnlySpan remainder = format.AsSpan(pos); - int countUntilNextBrace = remainder.IndexOfAny('{', '}'); - if (countUntilNextBrace < 0) - { - Append(remainder); - return; - } - - // Append the text until the brace. - Append(remainder.Slice(0, countUntilNextBrace)); - pos += countUntilNextBrace; - - // Get the brace. It must be followed by another character, either a copy of itself in the case of being - // escaped, or an arbitrary character that's part of the hole in the case of an opening brace. - char brace = format[pos]; - ch = MoveNext(format, ref pos); - if (brace == ch) - { - Append(ch); - pos++; - continue; - } - - // This wasn't an escape, so it must be an opening brace. - if (brace != '{') - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnexpectedClosingBrace); - } - - // Proceed to parse the hole. - break; - } - - // We're now positioned just after the opening brace of an argument hole, which consists of - // an opening brace, an index, an optional width preceded by a comma, and an optional format - // preceded by a colon, with arbitrary amounts of spaces throughout. - int width = 0; - bool leftJustify = false; - ReadOnlySpan itemFormatSpan = default; // used if itemFormat is null - - // First up is the index parameter, which is of the form: - // at least on digit - // optional any number of spaces - // We've already read the first digit into ch. - Debug.Assert(format[pos - 1] == '{'); - Debug.Assert(ch != '{'); - int index = ch - '0'; - if ((uint)index >= 10u) - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); - } - - // Common case is a single digit index followed by a closing brace. If it's not a closing brace, - // proceed to finish parsing the full hole format. - ch = MoveNext(format, ref pos); - if (ch != '}') - { - // Continue consuming optional additional digits. - while (char.IsAsciiDigit(ch) && index < IndexLimit) - { - index = index * 10 + ch - '0'; - ch = MoveNext(format, ref pos); - } - - // Consume optional whitespace. - while (ch == ' ') - { - ch = MoveNext(format, ref pos); - } - - // Parse the optional alignment, which is of the form: - // comma - // optional any number of spaces - // optional - - // at least one digit - // optional any number of spaces - if (ch == ',') - { - // Consume optional whitespace. - do - { - ch = MoveNext(format, ref pos); - } - while (ch == ' '); - - // Consume an optional minus sign indicating left alignment. - if (ch == '-') - { - leftJustify = true; - ch = MoveNext(format, ref pos); - } - - // Parse alignment digits. The read character must be a digit. - width = ch - '0'; - if ((uint)width >= 10u) - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); - } - ch = MoveNext(format, ref pos); - while (char.IsAsciiDigit(ch) && width < WidthLimit) - { - width = width * 10 + ch - '0'; - ch = MoveNext(format, ref pos); - } - - // Consume optional whitespace - while (ch == ' ') - { - ch = MoveNext(format, ref pos); - } - } - - // The next character needs to either be a closing brace for the end of the hole, - // or a colon indicating the start of the format. - if (ch != '}') - { - if (ch != ':') - { - // Unexpected character - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); - } - - // Search for the closing brace; everything in between is the format, - // but opening braces aren't allowed. - int startingPos = pos; - while (true) - { - ch = MoveNext(format, ref pos); - - if (ch == '}') - { - // Argument hole closed - break; - } - - if (ch == '{') - { - // Braces inside the argument hole are not supported - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); - } - } - - startingPos++; - itemFormatSpan = format.AsSpan(startingPos, pos - startingPos); - } - } - - // Construct the output for this arg hole. - Debug.Assert(format[pos] == '}'); - pos++; - string? s = null; - string? itemFormat = null; - - if ((uint)index >= (uint)args.Length) - { - ThrowHelper.ThrowFormatIndexOutOfRange(); - } - object? arg = args[index]; - - if (cf != null) - { - if (!itemFormatSpan.IsEmpty) - { - itemFormat = new string(itemFormatSpan); - } - - s = cf.Format(itemFormat, arg, provider); - } - - if (s == null) - { - // If arg is ISpanFormattable and the beginning doesn't need padding, - // try formatting it into the remaining current chunk. - if ((leftJustify || width == 0) && - arg is ISpanFormattable spanFormattableArg && - spanFormattableArg.TryFormat(_chars.Slice(_pos), out int charsWritten, itemFormatSpan, provider)) - { - _pos += charsWritten; - - // Pad the end, if needed. - if (leftJustify && width > charsWritten) - { - Append(' ', width - charsWritten); - } - - // Continue to parse other characters. - continue; - } - - // Otherwise, fallback to trying IFormattable or calling ToString. - if (arg is IFormattable formattableArg) - { - if (itemFormatSpan.Length != 0) - { - itemFormat ??= new string(itemFormatSpan); - } - s = formattableArg.ToString(itemFormat, provider); - } - else - { - s = arg?.ToString(); - } - - s ??= string.Empty; - } - - // Append it to the final output of the Format String. - if (width <= s.Length) - { - Append(s); - } - else if (leftJustify) - { - Append(s); - Append(' ', width - s.Length); - } - else - { - Append(' ', width - s.Length); - Append(s); - } - - // Continue parsing the rest of the format string. - } + Span IStringBuilderInternal.RemainingCurrentChunk => _chars.Slice(_pos); + void IStringBuilderInternal.UnsafeGrow(int size) => _pos += size; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static char MoveNext(string format, ref int pos) - { - pos++; - if ((uint)pos >= (uint)format.Length) - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); - } - return format[pos]; - } - } + internal void AppendFormat(IFormatProvider? provider, string format, ReadOnlySpan args) + => StringBuilderInternal.AppendFormatHelper(ref this, provider, format, args); } } From e67feeb1173ab4268326b807dd6ffddeb13324e3 Mon Sep 17 00:00:00 2001 From: kzrnm Date: Fri, 12 Sep 2025 13:37:40 +0900 Subject: [PATCH 3/5] Remove ValueStringBuilder.TryCopyTo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Despite the unintuitive behavior of TryCopyTo also calling Dispose, it’s only being used in a single place. --- .../src/System/Text/ValueStringBuilder.cs | 16 -------- .../Common/tests/Common.Tests.csproj | 7 +--- .../System/Text/ValueStringBuilderTests.cs | 41 ------------------- .../src/System/Number.BigInteger.cs | 3 +- 4 files changed, 3 insertions(+), 64 deletions(-) diff --git a/src/libraries/Common/src/System/Text/ValueStringBuilder.cs b/src/libraries/Common/src/System/Text/ValueStringBuilder.cs index f729484b9b39c5..d135342ce6e7e2 100644 --- a/src/libraries/Common/src/System/Text/ValueStringBuilder.cs +++ b/src/libraries/Common/src/System/Text/ValueStringBuilder.cs @@ -87,22 +87,6 @@ public override string ToString() public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); - public bool TryCopyTo(Span destination, out int charsWritten) - { - if (_chars.Slice(0, _pos).TryCopyTo(destination)) - { - charsWritten = _pos; - Dispose(); - return true; - } - else - { - charsWritten = 0; - Dispose(); - return false; - } - } - public void Insert(int index, char value, int count) { if (_pos > _chars.Length - count) diff --git a/src/libraries/Common/tests/Common.Tests.csproj b/src/libraries/Common/tests/Common.Tests.csproj index dab6d98a4ebd7d..df0afcee4cd29a 100644 --- a/src/libraries/Common/tests/Common.Tests.csproj +++ b/src/libraries/Common/tests/Common.Tests.csproj @@ -159,12 +159,7 @@ - - - - - - + diff --git a/src/libraries/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs b/src/libraries/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs index 3aadbc26fed28e..7959d4298e6d00 100644 --- a/src/libraries/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs +++ b/src/libraries/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs @@ -178,46 +178,6 @@ public void ToString_ClearsBuilder_ThenReusable() Assert.Equal(0, vsb.Length); Assert.Equal(string.Empty, vsb.ToString()); - Assert.True(vsb.TryCopyTo(Span.Empty, out _)); - - const string Text2 = "another test"; - vsb.Append(Text2); - Assert.Equal(Text2.Length, vsb.Length); - Assert.Equal(Text2, vsb.ToString()); - } - - [Fact] - public void TryCopyTo_FailsWhenDestinationIsTooSmall_SucceedsWhenItsLargeEnough() - { - var vsb = new ValueStringBuilder(); - - const string Text = "expected text"; - vsb.Append(Text); - Assert.Equal(Text.Length, vsb.Length); - - Span dst = new char[Text.Length - 1]; - Assert.False(vsb.TryCopyTo(dst, out int charsWritten)); - Assert.Equal(0, charsWritten); - Assert.Equal(0, vsb.Length); - } - - [Fact] - public void TryCopyTo_ClearsBuilder_ThenReusable() - { - const string Text1 = "test"; - var vsb = new ValueStringBuilder(); - - vsb.Append(Text1); - Assert.Equal(Text1.Length, vsb.Length); - - Span dst = new char[Text1.Length]; - Assert.True(vsb.TryCopyTo(dst, out int charsWritten)); - Assert.Equal(Text1.Length, charsWritten); - Assert.Equal(Text1, new string(dst)); - - Assert.Equal(0, vsb.Length); - Assert.Equal(string.Empty, vsb.ToString()); - Assert.True(vsb.TryCopyTo(Span.Empty, out _)); const string Text2 = "another test"; vsb.Append(Text2); @@ -238,7 +198,6 @@ public void Dispose_ClearsBuilder_ThenReusable() Assert.Equal(0, vsb.Length); Assert.Equal(string.Empty, vsb.ToString()); - Assert.True(vsb.TryCopyTo(Span.Empty, out _)); const string Text2 = "another test"; vsb.Append(Text2); diff --git a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs index 2ec19b8d824907..1bb961bce55f53 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs @@ -620,7 +620,8 @@ static uint MultiplyAdd(Span bits, uint multiplier, uint addValue) if (targetSpan) { - spanSuccess = sb.TryCopyTo(destination, out charsWritten); + charsWritten = (spanSuccess = sb.AsSpan().TryCopyTo(destination)) ? sb.Length : 0; + sb.Dispose(); return null; } else From b56305a1d96387ab223922246956208c3a924b4a Mon Sep 17 00:00:00 2001 From: kzrnm Date: Tue, 16 Sep 2025 00:49:10 +0900 Subject: [PATCH 4/5] Revert "Standardize AppendFormat between StringBuilder and ValueStringBuilder" This reverts commit 41a1695bcfc20aade95ed79f617ae2a2050eaee1. --- .../src/System.Diagnostics.Process.csproj | 2 +- .../System.Private.CoreLib.Shared.projitems | 1 - .../src/System/IO/StreamWriter.cs | 2 +- .../src/System/String.Manipulation.cs | 2 +- .../src/System/Text/StringBuilder.cs | 279 ++++++++++++++++- .../src/System/Text/StringBuilderInternal.cs | 295 ------------------ .../Text/ValueStringBuilder.AppendFormat.cs | 274 +++++++++++++++- 7 files changed, 541 insertions(+), 314 deletions(-) delete mode 100644 src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderInternal.cs diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index a407df98e7efa7..2782d79326c7ec 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent) diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 00b3bbaae5b579..9ac221df03c431 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1216,7 +1216,6 @@ - diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs index 72a683c2cb2c22..229509254e1d99 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs @@ -505,7 +505,7 @@ private void WriteFormatHelper(string format, ReadOnlySpan args, bool a new ValueStringBuilder(stackalloc char[256]) : new ValueStringBuilder(estimatedLength); - vsb.AppendFormat(null, format!, args); // AppendFormatHelper will appropriately throw ArgumentNullException for a null format + vsb.AppendFormatHelper(null, format!, args); // AppendFormatHelper will appropriately throw ArgumentNullException for a null format WriteSpan(vsb.AsSpan(), appendNewLine); diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs index 9a21709e9277fa..46738b2c59bafe 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs @@ -540,7 +540,7 @@ private static string FormatHelper(IFormatProvider? provider, string format, Rea var sb = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); sb.EnsureCapacity(format.Length + args.Length * 8); - sb.AppendFormat(provider, format, args); + sb.AppendFormatHelper(provider, format, args); return sb.ToString(); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs index 175b2e7e97ec23..a159895c0ed721 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs @@ -21,7 +21,7 @@ namespace System.Text // class to carry out modifications upon strings. [Serializable] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public sealed partial class StringBuilder : ISerializable, IStringBuilderInternal + public sealed partial class StringBuilder : ISerializable { // A StringBuilder is internally represented as a linked list of blocks each of which holds // a chunk of the string. It turns out string as a whole can also be represented as just a chunk, @@ -1518,9 +1518,274 @@ public StringBuilder AppendFormat(IFormatProvider? provider, [StringSyntax(Strin /// public StringBuilder AppendFormat(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan args) { - StringBuilder sb = this; - StringBuilderInternal.AppendFormatHelper(ref sb, provider, format, args); - return this; + ArgumentNullException.ThrowIfNull(format); + + // Undocumented exclusive limits on the range for Argument Hole Index and Argument Hole Alignment. + const int IndexLimit = 1_000_000; // Note: 0 <= ArgIndex < IndexLimit + const int WidthLimit = 1_000_000; // Note: -WidthLimit < ArgAlign < WidthLimit + + // Query the provider (if one was supplied) for an ICustomFormatter. If there is one, + // it needs to be used to transform all arguments. + ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter)); + + // Repeatedly find the next hole and process it. + int pos = 0; + char ch; + while (true) + { + // Skip until either the end of the input or the first unescaped opening brace, whichever comes first. + // Along the way we need to also unescape escaped closing braces. + while (true) + { + // Find the next brace. If there isn't one, the remainder of the input is text to be appended, and we're done. + if ((uint)pos >= (uint)format.Length) + { + return this; + } + + ReadOnlySpan remainder = format.AsSpan(pos); + int countUntilNextBrace = remainder.IndexOfAny('{', '}'); + if (countUntilNextBrace < 0) + { + Append(remainder); + return this; + } + + // Append the text until the brace. + Append(remainder.Slice(0, countUntilNextBrace)); + pos += countUntilNextBrace; + + // Get the brace. It must be followed by another character, either a copy of itself in the case of being + // escaped, or an arbitrary character that's part of the hole in the case of an opening brace. + char brace = format[pos]; + ch = MoveNext(format, ref pos); + if (brace == ch) + { + Append(ch); + pos++; + continue; + } + + // This wasn't an escape, so it must be an opening brace. + if (brace != '{') + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnexpectedClosingBrace); + } + + // Proceed to parse the hole. + break; + } + + // We're now positioned just after the opening brace of an argument hole, which consists of + // an opening brace, an index, an optional width preceded by a comma, and an optional format + // preceded by a colon, with arbitrary amounts of spaces throughout. + int width = 0; + bool leftJustify = false; + ReadOnlySpan itemFormatSpan = default; // used if itemFormat is null + + // First up is the index parameter, which is of the form: + // at least on digit + // optional any number of spaces + // We've already read the first digit into ch. + Debug.Assert(format[pos - 1] == '{'); + Debug.Assert(ch != '{'); + int index = ch - '0'; + if ((uint)index >= 10u) + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); + } + + // Common case is a single digit index followed by a closing brace. If it's not a closing brace, + // proceed to finish parsing the full hole format. + ch = MoveNext(format, ref pos); + if (ch != '}') + { + // Continue consuming optional additional digits. + while (char.IsAsciiDigit(ch) && index < IndexLimit) + { + index = index * 10 + ch - '0'; + ch = MoveNext(format, ref pos); + } + + // Consume optional whitespace. + while (ch == ' ') + { + ch = MoveNext(format, ref pos); + } + + // Parse the optional alignment, which is of the form: + // comma + // optional any number of spaces + // optional - + // at least one digit + // optional any number of spaces + if (ch == ',') + { + // Consume optional whitespace. + do + { + ch = MoveNext(format, ref pos); + } + while (ch == ' '); + + // Consume an optional minus sign indicating left alignment. + if (ch == '-') + { + leftJustify = true; + ch = MoveNext(format, ref pos); + } + + // Parse alignment digits. The read character must be a digit. + width = ch - '0'; + if ((uint)width >= 10u) + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); + } + ch = MoveNext(format, ref pos); + while (char.IsAsciiDigit(ch) && width < WidthLimit) + { + width = width * 10 + ch - '0'; + ch = MoveNext(format, ref pos); + } + + // Consume optional whitespace + while (ch == ' ') + { + ch = MoveNext(format, ref pos); + } + } + + // The next character needs to either be a closing brace for the end of the hole, + // or a colon indicating the start of the format. + if (ch != '}') + { + if (ch != ':') + { + // Unexpected character + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); + } + + // Search for the closing brace; everything in between is the format, + // but opening braces aren't allowed. + int startingPos = pos; + while (true) + { + ch = MoveNext(format, ref pos); + + if (ch == '}') + { + // Argument hole closed + break; + } + + if (ch == '{') + { + // Braces inside the argument hole are not supported + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); + } + } + + startingPos++; + itemFormatSpan = format.AsSpan(startingPos, pos - startingPos); + } + } + + // Construct the output for this arg hole. + Debug.Assert(format[pos] == '}'); + pos++; + string? s = null; + string? itemFormat = null; + + if ((uint)index >= (uint)args.Length) + { + ThrowHelper.ThrowFormatIndexOutOfRange(); + } + object? arg = args[index]; + + if (cf != null) + { + if (!itemFormatSpan.IsEmpty) + { + itemFormat = new string(itemFormatSpan); + } + + s = cf.Format(itemFormat, arg, provider); + } + + if (s == null) + { + // If arg is ISpanFormattable and the beginning doesn't need padding, + // try formatting it into the remaining current chunk. + if ((leftJustify || width == 0) && + arg is ISpanFormattable spanFormattableArg && + spanFormattableArg.TryFormat(RemainingCurrentChunk, out int charsWritten, itemFormatSpan, provider)) + { + if ((uint)charsWritten > (uint)RemainingCurrentChunk.Length) + { + // Untrusted ISpanFormattable implementations might return an erroneous charsWritten value, + // and m_ChunkLength might end up being used in Unsafe code, so fail if we get back an + // out-of-range charsWritten value. + ThrowHelper.ThrowFormatInvalidString(); + } + + m_ChunkLength += charsWritten; + + // Pad the end, if needed. + if (leftJustify && width > charsWritten) + { + Append(' ', width - charsWritten); + } + + // Continue to parse other characters. + continue; + } + + // Otherwise, fallback to trying IFormattable or calling ToString. + if (arg is IFormattable formattableArg) + { + if (itemFormatSpan.Length != 0) + { + itemFormat ??= new string(itemFormatSpan); + } + s = formattableArg.ToString(itemFormat, provider); + } + else + { + s = arg?.ToString(); + } + + s ??= string.Empty; + } + + // Append it to the final output of the Format String. + if (width <= s.Length) + { + Append(s); + } + else if (leftJustify) + { + Append(s); + Append(' ', width - s.Length); + } + else + { + Append(' ', width - s.Length); + Append(s); + } + + // Continue parsing the rest of the format string. + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static char MoveNext(string format, ref int pos) + { + pos++; + if ((uint)pos >= (uint)format.Length) + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); + } + return format[pos]; + } } /// @@ -2533,12 +2798,6 @@ private void Remove(int startIndex, int count, out StringBuilder chunk, out int AssertInvariants(); } - Span IStringBuilderInternal.RemainingCurrentChunk => RemainingCurrentChunk; - void IStringBuilderInternal.Append(char item) => Append(item); - void IStringBuilderInternal.Append(char item, int count) => Append(item, count); - void IStringBuilderInternal.Append(ReadOnlySpan value) => Append(value); - void IStringBuilderInternal.UnsafeGrow(int size) => m_ChunkLength += size; - /// Provides a handler used by the language compiler to append interpolated strings into instances. [EditorBrowsable(EditorBrowsableState.Never)] [InterpolatedStringHandler] diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderInternal.cs b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderInternal.cs deleted file mode 100644 index 9e5a9459305c2f..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderInternal.cs +++ /dev/null @@ -1,295 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace System.Text -{ - internal interface IStringBuilderInternal - { - void Append(char item); - void Append(char item, int count); - void Append(ReadOnlySpan value); - void UnsafeGrow(int size); - Span RemainingCurrentChunk { get; } - } - - internal static class StringBuilderInternal - { - /// - internal static void AppendFormatHelper(ref TBuilder builder, IFormatProvider? provider, string format, ReadOnlySpan args) - where TBuilder : IStringBuilderInternal, allows ref struct - { - ArgumentNullException.ThrowIfNull(format); - - // Undocumented exclusive limits on the range for Argument Hole Index and Argument Hole Alignment. - const int IndexLimit = 1_000_000; // Note: 0 <= ArgIndex < IndexLimit - const int WidthLimit = 1_000_000; // Note: -WidthLimit < ArgAlign < WidthLimit - - // Query the provider (if one was supplied) for an ICustomFormatter. If there is one, - // it needs to be used to transform all arguments. - ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter)); - - // Repeatedly find the next hole and process it. - int pos = 0; - char ch; - while (true) - { - // Skip until either the end of the input or the first unescaped opening brace, whichever comes first. - // Along the way we need to also unescape escaped closing braces. - while (true) - { - // Find the next brace. If there isn't one, the remainder of the input is text to be appended, and we're done. - if ((uint)pos >= (uint)format.Length) - { - return; - } - - ReadOnlySpan remainder = format.AsSpan(pos); - int countUntilNextBrace = remainder.IndexOfAny('{', '}'); - if (countUntilNextBrace < 0) - { - builder.Append(remainder); - return; - } - - // Append the text until the brace. - builder.Append(remainder.Slice(0, countUntilNextBrace)); - pos += countUntilNextBrace; - - // Get the brace. It must be followed by another character, either a copy of itself in the case of being - // escaped, or an arbitrary character that's part of the hole in the case of an opening brace. - char brace = format[pos]; - ch = MoveNext(format, ref pos); - if (brace == ch) - { - builder.Append(ch); - pos++; - continue; - } - - // This wasn't an escape, so it must be an opening brace. - if (brace != '{') - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnexpectedClosingBrace); - } - - // Proceed to parse the hole. - break; - } - - // We're now positioned just after the opening brace of an argument hole, which consists of - // an opening brace, an index, an optional width preceded by a comma, and an optional format - // preceded by a colon, with arbitrary amounts of spaces throughout. - int width = 0; - bool leftJustify = false; - ReadOnlySpan itemFormatSpan = default; // used if itemFormat is null - - // First up is the index parameter, which is of the form: - // at least on digit - // optional any number of spaces - // We've already read the first digit into ch. - Debug.Assert(format[pos - 1] == '{'); - Debug.Assert(ch != '{'); - int index = ch - '0'; - if ((uint)index >= 10u) - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); - } - - // Common case is a single digit index followed by a closing brace. If it's not a closing brace, - // proceed to finish parsing the full hole format. - ch = MoveNext(format, ref pos); - if (ch != '}') - { - // Continue consuming optional additional digits. - while (char.IsAsciiDigit(ch) && index < IndexLimit) - { - index = index * 10 + ch - '0'; - ch = MoveNext(format, ref pos); - } - - // Consume optional whitespace. - while (ch == ' ') - { - ch = MoveNext(format, ref pos); - } - - // Parse the optional alignment, which is of the form: - // comma - // optional any number of spaces - // optional - - // at least one digit - // optional any number of spaces - if (ch == ',') - { - // Consume optional whitespace. - do - { - ch = MoveNext(format, ref pos); - } - while (ch == ' '); - - // Consume an optional minus sign indicating left alignment. - if (ch == '-') - { - leftJustify = true; - ch = MoveNext(format, ref pos); - } - - // Parse alignment digits. The read character must be a digit. - width = ch - '0'; - if ((uint)width >= 10u) - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); - } - ch = MoveNext(format, ref pos); - while (char.IsAsciiDigit(ch) && width < WidthLimit) - { - width = width * 10 + ch - '0'; - ch = MoveNext(format, ref pos); - } - - // Consume optional whitespace - while (ch == ' ') - { - ch = MoveNext(format, ref pos); - } - } - - // The next character needs to either be a closing brace for the end of the hole, - // or a colon indicating the start of the format. - if (ch != '}') - { - if (ch != ':') - { - // Unexpected character - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); - } - - // Search for the closing brace; everything in between is the format, - // but opening braces aren't allowed. - int startingPos = pos; - while (true) - { - ch = MoveNext(format, ref pos); - - if (ch == '}') - { - // Argument hole closed - break; - } - - if (ch == '{') - { - // Braces inside the argument hole are not supported - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); - } - } - - startingPos++; - itemFormatSpan = format.AsSpan(startingPos, pos - startingPos); - } - } - - // Construct the output for this arg hole. - Debug.Assert(format[pos] == '}'); - pos++; - string? s = null; - string? itemFormat = null; - - if ((uint)index >= (uint)args.Length) - { - ThrowHelper.ThrowFormatIndexOutOfRange(); - } - object? arg = args[index]; - - if (cf != null) - { - if (!itemFormatSpan.IsEmpty) - { - itemFormat = new string(itemFormatSpan); - } - - s = cf.Format(itemFormat, arg, provider); - } - - if (s == null) - { - // If arg is ISpanFormattable and the beginning doesn't need padding, - // try formatting it into the remaining current chunk. - if ((leftJustify || width == 0) && - arg is ISpanFormattable spanFormattableArg && - spanFormattableArg.TryFormat(builder.RemainingCurrentChunk, out int charsWritten, itemFormatSpan, provider)) - { - if ((uint)charsWritten > (uint)builder.RemainingCurrentChunk.Length) - { - // Untrusted ISpanFormattable implementations might return an erroneous charsWritten value, - // and m_ChunkLength might end up being used in Unsafe code, so fail if we get back an - // out-of-range charsWritten value. - ThrowHelper.ThrowFormatInvalidString(); - } - - builder.UnsafeGrow(charsWritten); - - // Pad the end, if needed. - if (leftJustify && width > charsWritten) - { - builder.Append(' ', width - charsWritten); - } - - // Continue to parse other characters. - continue; - } - - // Otherwise, fallback to trying IFormattable or calling ToString. - if (arg is IFormattable formattableArg) - { - if (itemFormatSpan.Length != 0) - { - itemFormat ??= new string(itemFormatSpan); - } - s = formattableArg.ToString(itemFormat, provider); - } - else - { - s = arg?.ToString(); - } - - s ??= string.Empty; - } - - // Append it to the final output of the Format String. - if (width <= s.Length) - { - builder.Append(s); - } - else if (leftJustify) - { - builder.Append(s); - builder.Append(' ', width - s.Length); - } - else - { - builder.Append(' ', width - s.Length); - builder.Append(s); - } - - // Continue parsing the rest of the format string. - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static char MoveNext(string format, ref int pos) - { - pos++; - if ((uint)pos >= (uint)format.Length) - { - ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); - } - return format[pos]; - } - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs index c815e6324af7ff..4c618953c237a7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs @@ -1,14 +1,278 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + namespace System.Text { - internal ref partial struct ValueStringBuilder : IStringBuilderInternal + internal ref partial struct ValueStringBuilder { - Span IStringBuilderInternal.RemainingCurrentChunk => _chars.Slice(_pos); - void IStringBuilderInternal.UnsafeGrow(int size) => _pos += size; + // Copied from StringBuilder, can't be done via generic extension + // as ValueStringBuilder is a ref struct and cannot be used in a generic. + internal void AppendFormatHelper(IFormatProvider? provider, string format, ReadOnlySpan args) + { + ArgumentNullException.ThrowIfNull(format); + + // Undocumented exclusive limits on the range for Argument Hole Index and Argument Hole Alignment. + const int IndexLimit = 1_000_000; // Note: 0 <= ArgIndex < IndexLimit + const int WidthLimit = 1_000_000; // Note: -WidthLimit < ArgAlign < WidthLimit + + // Query the provider (if one was supplied) for an ICustomFormatter. If there is one, + // it needs to be used to transform all arguments. + ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter)); + + // Repeatedly find the next hole and process it. + int pos = 0; + char ch; + while (true) + { + // Skip until either the end of the input or the first unescaped opening brace, whichever comes first. + // Along the way we need to also unescape escaped closing braces. + while (true) + { + // Find the next brace. If there isn't one, the remainder of the input is text to be appended, and we're done. + if ((uint)pos >= (uint)format.Length) + { + return; + } + + ReadOnlySpan remainder = format.AsSpan(pos); + int countUntilNextBrace = remainder.IndexOfAny('{', '}'); + if (countUntilNextBrace < 0) + { + Append(remainder); + return; + } + + // Append the text until the brace. + Append(remainder.Slice(0, countUntilNextBrace)); + pos += countUntilNextBrace; + + // Get the brace. It must be followed by another character, either a copy of itself in the case of being + // escaped, or an arbitrary character that's part of the hole in the case of an opening brace. + char brace = format[pos]; + ch = MoveNext(format, ref pos); + if (brace == ch) + { + Append(ch); + pos++; + continue; + } + + // This wasn't an escape, so it must be an opening brace. + if (brace != '{') + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnexpectedClosingBrace); + } + + // Proceed to parse the hole. + break; + } + + // We're now positioned just after the opening brace of an argument hole, which consists of + // an opening brace, an index, an optional width preceded by a comma, and an optional format + // preceded by a colon, with arbitrary amounts of spaces throughout. + int width = 0; + bool leftJustify = false; + ReadOnlySpan itemFormatSpan = default; // used if itemFormat is null + + // First up is the index parameter, which is of the form: + // at least on digit + // optional any number of spaces + // We've already read the first digit into ch. + Debug.Assert(format[pos - 1] == '{'); + Debug.Assert(ch != '{'); + int index = ch - '0'; + if ((uint)index >= 10u) + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); + } + + // Common case is a single digit index followed by a closing brace. If it's not a closing brace, + // proceed to finish parsing the full hole format. + ch = MoveNext(format, ref pos); + if (ch != '}') + { + // Continue consuming optional additional digits. + while (char.IsAsciiDigit(ch) && index < IndexLimit) + { + index = index * 10 + ch - '0'; + ch = MoveNext(format, ref pos); + } + + // Consume optional whitespace. + while (ch == ' ') + { + ch = MoveNext(format, ref pos); + } + + // Parse the optional alignment, which is of the form: + // comma + // optional any number of spaces + // optional - + // at least one digit + // optional any number of spaces + if (ch == ',') + { + // Consume optional whitespace. + do + { + ch = MoveNext(format, ref pos); + } + while (ch == ' '); + + // Consume an optional minus sign indicating left alignment. + if (ch == '-') + { + leftJustify = true; + ch = MoveNext(format, ref pos); + } + + // Parse alignment digits. The read character must be a digit. + width = ch - '0'; + if ((uint)width >= 10u) + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit); + } + ch = MoveNext(format, ref pos); + while (char.IsAsciiDigit(ch) && width < WidthLimit) + { + width = width * 10 + ch - '0'; + ch = MoveNext(format, ref pos); + } + + // Consume optional whitespace + while (ch == ' ') + { + ch = MoveNext(format, ref pos); + } + } + + // The next character needs to either be a closing brace for the end of the hole, + // or a colon indicating the start of the format. + if (ch != '}') + { + if (ch != ':') + { + // Unexpected character + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); + } + + // Search for the closing brace; everything in between is the format, + // but opening braces aren't allowed. + int startingPos = pos; + while (true) + { + ch = MoveNext(format, ref pos); + + if (ch == '}') + { + // Argument hole closed + break; + } + + if (ch == '{') + { + // Braces inside the argument hole are not supported + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); + } + } + + startingPos++; + itemFormatSpan = format.AsSpan(startingPos, pos - startingPos); + } + } + + // Construct the output for this arg hole. + Debug.Assert(format[pos] == '}'); + pos++; + string? s = null; + string? itemFormat = null; + + if ((uint)index >= (uint)args.Length) + { + ThrowHelper.ThrowFormatIndexOutOfRange(); + } + object? arg = args[index]; + + if (cf != null) + { + if (!itemFormatSpan.IsEmpty) + { + itemFormat = new string(itemFormatSpan); + } + + s = cf.Format(itemFormat, arg, provider); + } + + if (s == null) + { + // If arg is ISpanFormattable and the beginning doesn't need padding, + // try formatting it into the remaining current chunk. + if ((leftJustify || width == 0) && + arg is ISpanFormattable spanFormattableArg && + spanFormattableArg.TryFormat(_chars.Slice(_pos), out int charsWritten, itemFormatSpan, provider)) + { + _pos += charsWritten; + + // Pad the end, if needed. + if (leftJustify && width > charsWritten) + { + Append(' ', width - charsWritten); + } + + // Continue to parse other characters. + continue; + } + + // Otherwise, fallback to trying IFormattable or calling ToString. + if (arg is IFormattable formattableArg) + { + if (itemFormatSpan.Length != 0) + { + itemFormat ??= new string(itemFormatSpan); + } + s = formattableArg.ToString(itemFormat, provider); + } + else + { + s = arg?.ToString(); + } + + s ??= string.Empty; + } + + // Append it to the final output of the Format String. + if (width <= s.Length) + { + Append(s); + } + else if (leftJustify) + { + Append(s); + Append(' ', width - s.Length); + } + else + { + Append(' ', width - s.Length); + Append(s); + } + + // Continue parsing the rest of the format string. + } - internal void AppendFormat(IFormatProvider? provider, string format, ReadOnlySpan args) - => StringBuilderInternal.AppendFormatHelper(ref this, provider, format, args); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static char MoveNext(string format, ref int pos) + { + pos++; + if ((uint)pos >= (uint)format.Length) + { + ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem); + } + return format[pos]; + } + } } } From 401bb830d70f76094d0a8e911635cde3c50f76bd Mon Sep 17 00:00:00 2001 From: kzrnm Date: Tue, 16 Sep 2025 09:54:04 +0900 Subject: [PATCH 5/5] =?UTF-8?q?EnsureTerminated=20=E2=86=92=20NullTerminat?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ValueStringBuilder.EnsureTerminated.cs | 20 ------------------- .../src/System/Text/ValueStringBuilder.cs | 10 ++++++++++ .../src/System.Diagnostics.Process.csproj | 2 -- .../src/System/Diagnostics/Process.Windows.cs | 4 ++-- .../System.IO.FileSystem.AccessControl.csproj | 2 -- .../System.Private.CoreLib.Shared.projitems | 3 --- .../src/System/IO/PathHelper.Windows.cs | 4 ++-- 7 files changed, 14 insertions(+), 31 deletions(-) delete mode 100644 src/libraries/Common/src/System/Text/ValueStringBuilder.EnsureTerminated.cs diff --git a/src/libraries/Common/src/System/Text/ValueStringBuilder.EnsureTerminated.cs b/src/libraries/Common/src/System/Text/ValueStringBuilder.EnsureTerminated.cs deleted file mode 100644 index c78c050a13132e..00000000000000 --- a/src/libraries/Common/src/System/Text/ValueStringBuilder.EnsureTerminated.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.CompilerServices; - -namespace System.Text -{ - internal ref partial struct ValueStringBuilder - { - /// - /// Ensures that the builder is terminated with a NUL character. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void EnsureTerminated() - { - EnsureCapacity(_pos + 1); - _chars[_pos] = '\0'; - } - } -} diff --git a/src/libraries/Common/src/System/Text/ValueStringBuilder.cs b/src/libraries/Common/src/System/Text/ValueStringBuilder.cs index d135342ce6e7e2..8fb266df99ae57 100644 --- a/src/libraries/Common/src/System/Text/ValueStringBuilder.cs +++ b/src/libraries/Common/src/System/Text/ValueStringBuilder.cs @@ -53,6 +53,16 @@ public void EnsureCapacity(int capacity) Grow(capacity - _pos); } + /// + /// Ensures that the builder is terminated with a NUL character. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void NullTerminate() + { + EnsureCapacity(_pos + 1); + _chars[_pos] = '\0'; + } + /// /// Get a pinnable reference to the builder. /// Does not ensure there is a null char after diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index 2782d79326c7ec..ad692fafc90f2c 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -225,8 +225,6 @@ - diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index 20b4d079a42354..cebd7469d43667 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -545,7 +545,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) logonFlags = Interop.Advapi32.LogonFlags.LOGON_NETCREDENTIALS_ONLY; } - commandLine.EnsureTerminated(); + commandLine.NullTerminate(); fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty) fixed (char* environmentBlockPtr = environmentBlock) fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) @@ -580,7 +580,7 @@ ref processInfo // pointer to PROCESS_INFORMATION } else { - commandLine.EnsureTerminated(); + commandLine.NullTerminate(); fixed (char* environmentBlockPtr = environmentBlock) fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) { diff --git a/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj b/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj index d7c9d227c3c08a..3df2d07dc15e4b 100644 --- a/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj +++ b/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj @@ -78,8 +78,6 @@ Link="Common\System\IO\Win32Marshal.cs" /> - Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs - - Common\System\Text\ValueStringBuilder.EnsureTerminated.cs - Common\System\Threading\ITimer.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/PathHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/PathHelper.Windows.cs index 537d5cead7f216..687478435ff06c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/PathHelper.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/PathHelper.Windows.cs @@ -51,7 +51,7 @@ internal static string Normalize(ref ValueStringBuilder path) var builder = new ValueStringBuilder(stackalloc char[PathInternal.MaxShortPath]); // Get the full path - path.EnsureTerminated(); + path.NullTerminate(); GetFullPathName(path.AsSpan(), ref builder); string result = builder.AsSpan().IndexOf('~') >= 0 @@ -177,7 +177,7 @@ internal static string TryExpandShortFileName(ref ValueStringBuilder outputBuild while (!success) { - inputBuilder.EnsureTerminated(); + inputBuilder.NullTerminate(); uint result = Interop.Kernel32.GetLongPathNameW( ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);