// 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
{
// 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<object?> 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<char> 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<char> 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.
}
[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];
}
}
}
}