/// WARNING: This method does not implicitly handle long paths. Use GetFullPathName or PathHelper.
/// </summary>
[DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false, ExactSpelling = true)]
- unsafe internal static extern uint GetFullPathNameW(char* path, uint numBufferChars, char[] buffer, IntPtr mustBeZero);
+ internal static extern uint GetFullPathNameW(string path, uint numBufferChars, ref char buffer, IntPtr mustBeZero);
}
}
/// WARNING: This method does not implicitly handle long paths. Use GetFullPath/PathHelper.
/// </summary>
[DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false, ExactSpelling = true)]
- internal static extern uint GetLongPathNameW(char[] lpszShortPath, char[] lpszLongPath, uint cchBuffer);
+ internal static extern uint GetLongPathNameW(ref char lpszShortPath, ref char lpszLongPath, uint cchBuffer);
}
}
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\OutAttribute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\PreserveSigAttribute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\SafeBuffer.cs" />
- <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\StringBuffer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\StructLayoutAttribute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\UnmanagedFunctionPointerAttribute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\UnmanagedType.cs" />
using System.Diagnostics;
using System.Runtime.InteropServices;
+using System.Text;
namespace System.IO
{
/// <returns>Normalized path</returns>
internal static string Normalize(string path)
{
+ Span<char> initialBuffer = stackalloc char[PathInternal.MaxShortPath];
+ ValueStringBuilder builder = new ValueStringBuilder(initialBuffer);
+
// Get the full path
- StringBuffer fullPath = new StringBuffer(path.Length);
+ GetFullPathName(path, ref builder);
- try
- {
- GetFullPathName(path, ref fullPath);
+ // If we have the exact same string we were passed in, don't allocate another string.
+ // TryExpandShortName does this input identity check.
+ string result = builder.AsSpan().Contains('~')
+ ? TryExpandShortFileName(ref builder, originalPath: path)
+ : builder.AsSpan().Equals(path.AsReadOnlySpan()) ? path : builder.ToString();
- if (fullPath.Contains('~'))
- {
- return TryExpandShortFileName(ref fullPath, originalPath: path);
- }
- else
- {
- if (fullPath.Length == path.Length && fullPath.StartsWith(path))
- {
- // If we have the exact same string we were passed in, don't bother to allocate another string from the StringBuffer.
- return path;
- }
- return fullPath.ToString();
- }
- }
- finally
- {
- // Clear the buffer
- fullPath.Free();
- }
+ // Clear the buffer
+ builder.Dispose();
+ return result;
}
- private static unsafe void GetFullPathName(string path, ref StringBuffer fullPath)
+ private static void GetFullPathName(string path, ref ValueStringBuilder builder)
{
// If the string starts with an extended prefix we would need to remove it from the path before we call GetFullPathName as
// it doesn't root extended paths correctly. We don't currently resolve extended paths, so we'll just assert here.
Debug.Assert(PathInternal.IsPartiallyQualified(path) || !PathInternal.IsExtended(path));
- fixed (char* pathStart = path)
+ uint result = 0;
+ while ((result = Interop.Kernel32.GetFullPathNameW(path, (uint)builder.Capacity, ref builder.GetPinnableReference(), IntPtr.Zero)) > builder.Capacity)
{
- uint result = 0;
- while ((result = Interop.Kernel32.GetFullPathNameW(pathStart, (uint)fullPath.Capacity, fullPath.UnderlyingArray, IntPtr.Zero)) > fullPath.Capacity)
- {
- // Reported size is greater than the buffer size. Increase the capacity.
- fullPath.EnsureCapacity(checked((int)result));
- }
-
- if (result == 0)
- {
- // Failure, get the error and throw
- int errorCode = Marshal.GetLastWin32Error();
- if (errorCode == 0)
- errorCode = Interop.Errors.ERROR_BAD_PATHNAME;
- throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
- }
+ // Reported size is greater than the buffer size. Increase the capacity.
+ builder.EnsureCapacity(checked((int)result));
+ }
- fullPath.Length = checked((int)result);
+ if (result == 0)
+ {
+ // Failure, get the error and throw
+ int errorCode = Marshal.GetLastWin32Error();
+ if (errorCode == 0)
+ errorCode = Interop.Errors.ERROR_BAD_PATHNAME;
+ throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
}
+
+ builder.Length = (int)result;
}
- private static int GetInputBuffer(ref StringBuffer content, bool isDosUnc, ref StringBuffer buffer)
+ private static int PrependDevicePathChars(ref ValueStringBuilder content, bool isDosUnc, ref ValueStringBuilder buffer)
{
int length = content.Length;
: PathInternal.DevicePrefixLength;
buffer.EnsureCapacity(length + 1);
+ buffer.Length = 0;
if (isDosUnc)
{
- // Put the extended UNC prefix (\\?\UNC\) in front of the path
- buffer.CopyFrom(bufferIndex: 0, source: PathInternal.UncExtendedPathPrefix);
+ // Is a \\Server\Share, put \\?\UNC\ in the front
+ buffer.Append(PathInternal.UncExtendedPathPrefix);
- // Copy the source buffer over after the existing UNC prefix
- content.CopyTo(
- bufferIndex: PathInternal.UncPrefixLength,
- destination: ref buffer,
- destinationIndex: PathInternal.UncExtendedPrefixLength,
- count: content.Length - PathInternal.UncPrefixLength);
+ // Copy Server\Share\... over to the buffer
+ buffer.Append(content.AsSpan().Slice(PathInternal.UncPrefixLength));
// Return the prefix difference
return PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength;
}
else
{
- int prefixSize = PathInternal.ExtendedPathPrefix.Length;
- buffer.CopyFrom(bufferIndex: 0, source: PathInternal.ExtendedPathPrefix);
- content.CopyTo(bufferIndex: 0, destination: ref buffer, destinationIndex: prefixSize, count: content.Length);
- return prefixSize;
+ // Not an UNC, put the \\?\ prefix in front, then the original string
+ buffer.Append(PathInternal.ExtendedPathPrefix);
+ buffer.Append(content.AsSpan());
+ return PathInternal.DevicePrefixLength;
}
}
- private static string TryExpandShortFileName(ref StringBuffer outputBuffer, string originalPath)
+ private static string TryExpandShortFileName(ref ValueStringBuilder outputBuilder, string originalPath)
{
// We guarantee we'll expand short names for paths that only partially exist. As such, we need to find the part of the path that actually does exist. To
// avoid allocating like crazy we'll create only one input array and modify the contents with embedded nulls.
- Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuffer.AsSpan()), "should have resolved by now");
+ Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuilder.AsSpan()), "should have resolved by now");
// We'll have one of a few cases by now (the normalized path will have already:
//
// 2. Dos UNC (\\Server\Share)
// 3. Dos device path (\\.\C:\, \\?\C:\)
//
- // We want to put the extended syntax on the front if it doesn't already have it, which may mean switching from \\.\.
+ // We want to put the extended syntax on the front if it doesn't already have it (for long path support and speed), which may mean switching from \\.\.
//
// Note that we will never get \??\ here as GetFullPathName() does not recognize \??\ and will return it as C:\??\ (or whatever the current drive is).
- int rootLength = PathInternal.GetRootLength(outputBuffer.AsSpan());
- bool isDevice = PathInternal.IsDevice(outputBuffer.AsSpan());
+ int rootLength = PathInternal.GetRootLength(outputBuilder.AsSpan());
+ bool isDevice = PathInternal.IsDevice(outputBuilder.AsSpan());
- StringBuffer inputBuffer = new StringBuffer(0);
- try
- {
- bool isDosUnc = false;
- int rootDifference = 0;
- bool wasDotDevice = false;
+ // As this is a corner case we're not going to add a stackalloc here to keep the stack pressure down.
+ ValueStringBuilder inputBuilder = new ValueStringBuilder();
- // Add the extended prefix before expanding to allow growth over MAX_PATH
- if (isDevice)
- {
- // We have one of the following (\\?\ or \\.\)
- inputBuffer.Append(ref outputBuffer);
+ bool isDosUnc = false;
+ int rootDifference = 0;
+ bool wasDotDevice = false;
- if (outputBuffer[2] == '.')
- {
- wasDotDevice = true;
- inputBuffer[2] = '?';
- }
- }
- else
+ // Add the extended prefix before expanding to allow growth over MAX_PATH
+ if (isDevice)
+ {
+ // We have one of the following (\\?\ or \\.\)
+ inputBuilder.Append(outputBuilder.AsSpan());
+
+ if (outputBuilder[2] == '.')
{
- isDosUnc = !PathInternal.IsDevice(outputBuffer.AsSpan()) && outputBuffer.Length > 1 && outputBuffer[0] == '\\' && outputBuffer[1] == '\\';
- rootDifference = GetInputBuffer(ref outputBuffer, isDosUnc, ref inputBuffer);
+ wasDotDevice = true;
+ inputBuilder[2] = '?';
}
+ }
+ else
+ {
+ isDosUnc = !PathInternal.IsDevice(outputBuilder.AsSpan()) && outputBuilder.Length > 1 && outputBuilder[0] == '\\' && outputBuilder[1] == '\\';
+ rootDifference = PrependDevicePathChars(ref outputBuilder, isDosUnc, ref inputBuilder);
+ }
- rootLength += rootDifference;
- int inputLength = inputBuffer.Length;
+ rootLength += rootDifference;
+ int inputLength = inputBuilder.Length;
- bool success = false;
- int foundIndex = inputBuffer.Length - 1;
+ bool success = false;
+ int foundIndex = inputBuilder.Length - 1;
- while (!success)
- {
- uint result = Interop.Kernel32.GetLongPathNameW(inputBuffer.UnderlyingArray, outputBuffer.UnderlyingArray, (uint)outputBuffer.Capacity);
+ // Need to null terminate the input builder
+ inputBuilder.Append('\0');
- // Replace any temporary null we added
- if (inputBuffer[foundIndex] == '\0') inputBuffer[foundIndex] = '\\';
+ while (!success)
+ {
+ uint result = Interop.Kernel32.GetLongPathNameW(ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);
+
+ // Replace any temporary null we added
+ if (inputBuilder[foundIndex] == '\0') inputBuilder[foundIndex] = '\\';
- if (result == 0)
+ if (result == 0)
+ {
+ // Look to see if we couldn't find the file
+ int error = Marshal.GetLastWin32Error();
+ if (error != Interop.Errors.ERROR_FILE_NOT_FOUND && error != Interop.Errors.ERROR_PATH_NOT_FOUND)
{
- // Look to see if we couldn't find the file
- int error = Marshal.GetLastWin32Error();
- if (error != Interop.Errors.ERROR_FILE_NOT_FOUND && error != Interop.Errors.ERROR_PATH_NOT_FOUND)
- {
- // Some other failure, give up
- break;
- }
-
- // We couldn't find the path at the given index, start looking further back in the string.
- foundIndex--;
-
- for (; foundIndex > rootLength && inputBuffer[foundIndex] != '\\'; foundIndex--) ;
- if (foundIndex == rootLength)
- {
- // Can't trim the path back any further
- break;
- }
- else
- {
- // Temporarily set a null in the string to get Windows to look further up the path
- inputBuffer[foundIndex] = '\0';
- }
+ // Some other failure, give up
+ break;
}
- else if (result > outputBuffer.Capacity)
+
+ // We couldn't find the path at the given index, start looking further back in the string.
+ foundIndex--;
+
+ for (; foundIndex > rootLength && inputBuilder[foundIndex] != '\\'; foundIndex--) ;
+ if (foundIndex == rootLength)
{
- // Not enough space. The result count for this API does not include the null terminator.
- outputBuffer.EnsureCapacity(checked((int)result));
- result = Interop.Kernel32.GetLongPathNameW(inputBuffer.UnderlyingArray, outputBuffer.UnderlyingArray, (uint)outputBuffer.Capacity);
+ // Can't trim the path back any further
+ break;
}
else
{
- // Found the path
- success = true;
- outputBuffer.Length = checked((int)result);
- if (foundIndex < inputLength - 1)
- {
- // It was a partial find, put the non-existent part of the path back
- outputBuffer.Append(ref inputBuffer, foundIndex, inputBuffer.Length - foundIndex);
- }
+ // Temporarily set a null in the string to get Windows to look further up the path
+ inputBuilder[foundIndex] = '\0';
}
}
-
- // Strip out the prefix and return the string
- ref StringBuffer bufferToUse = ref Choose(success, ref outputBuffer, ref inputBuffer);
-
- // Switch back from \\?\ to \\.\ if necessary
- if (wasDotDevice)
- bufferToUse[2] = '.';
-
- string returnValue = null;
-
- int newLength = bufferToUse.Length - rootDifference;
- if (isDosUnc)
- {
- // Need to go from \\?\UNC\ to \\?\UN\\
- bufferToUse[PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength] = '\\';
- }
-
- // We now need to strip out any added characters at the front of the string
- if (bufferToUse.SubstringEquals(originalPath, rootDifference, newLength))
+ else if (result > outputBuilder.Capacity)
{
- // Use the original path to avoid allocating
- returnValue = originalPath;
+ // Not enough space. The result count for this API does not include the null terminator.
+ outputBuilder.EnsureCapacity(checked((int)result));
+ result = Interop.Kernel32.GetLongPathNameW(ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);
}
else
{
- returnValue = bufferToUse.Substring(rootDifference, newLength);
+ // Found the path
+ success = true;
+ outputBuilder.Length = checked((int)result);
+ if (foundIndex < inputLength - 1)
+ {
+ // It was a partial find, put the non-existent part of the path back
+ outputBuilder.Append(inputBuilder.AsSpan().Slice(foundIndex, inputBuilder.Length - foundIndex));
+ }
}
-
- return returnValue;
- }
- finally
- {
- inputBuffer.Free();
}
- }
- // Helper method to workaround lack of operator ? support for ref values
- private static ref StringBuffer Choose(bool condition, ref StringBuffer s1, ref StringBuffer s2)
- {
- if (condition) return ref s1;
- else return ref s2;
+ // If we were able to expand the path, use it, otherwise use the original full path result
+ ref ValueStringBuilder builderToUse = ref (success ? ref outputBuilder : ref inputBuilder);
+
+ // Switch back from \\?\ to \\.\ if necessary
+ if (wasDotDevice)
+ builderToUse[2] = '.';
+
+ // Change from \\?\UNC\ to \\?\UN\\ if needed
+ if (isDosUnc)
+ builderToUse[PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength] = '\\';
+
+ // Strip out any added characters at the front of the string
+ ReadOnlySpan<char> output = builderToUse.AsSpan().Slice(rootDifference);
+
+ string returnValue = output.Equals(originalPath.AsReadOnlySpan())
+ ? originalPath : new string(output);
+
+ inputBuilder.Dispose();
+ return returnValue;
}
}
}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Buffers;
-using System.Runtime.CompilerServices;
-
-namespace System.Runtime.InteropServices
-{
- /// <summary>
- /// Buffer that deals in char size increments. Dispose to free memory. Always makes ordinal
- /// comparisons. Not thread safe.
- ///
- /// A more performant replacement for StringBuilder when performing native interop.
- ///
- /// "No copy" valuetype. Has to be passed as "ref".
- ///
- /// </summary>
- /// <remarks>
- /// Suggested use through P/Invoke: define DllImport arguments that take a character buffer as SafeHandle and pass StringBuffer.GetHandle().
- /// </remarks>
- internal struct StringBuffer
- {
- private char[] _buffer;
- private int _length;
-
- /// <summary>
- /// Instantiate the buffer with capacity for at least the specified number of characters. Capacity
- /// includes the trailing null character.
- /// </summary>
- public StringBuffer(int initialCapacity)
- {
- _buffer = ArrayPool<char>.Shared.Rent(initialCapacity);
- _length = 0;
- }
-
- /// <summary>
- /// Get/set the character at the given index.
- /// </summary>
- /// <exception cref="ArgumentOutOfRangeException">Thrown if attempting to index outside of the buffer length.</exception>
- public char this[int index]
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get
- {
- if (index >= _length) throw new ArgumentOutOfRangeException(nameof(index));
- return _buffer[index];
- }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- set
- {
- if (index >= _length) throw new ArgumentOutOfRangeException(nameof(index));
- _buffer[index] = value;
- }
- }
-
- /// <summary>
- /// Underlying storage of the buffer. Used for interop.
- /// </summary>
- public char[] UnderlyingArray => _buffer;
-
- /// <summary>
- /// Character capacity of the buffer. Includes the count for the trailing null character.
- /// </summary>
- public int Capacity => _buffer.Length;
-
- /// <summary>
- /// Ensure capacity in characters is at least the given minimum.
- /// </summary>
- /// <exception cref="OutOfMemoryException">Thrown if unable to allocate memory when setting.</exception>
- public void EnsureCapacity(int minCapacity)
- {
- if (minCapacity > Capacity)
- {
- char[] oldBuffer = _buffer;
- _buffer = ArrayPool<char>.Shared.Rent(minCapacity);
- Array.Copy(oldBuffer, 0, _buffer, 0, oldBuffer.Length);
- ArrayPool<char>.Shared.Return(oldBuffer);
- }
- }
-
- /// <summary>
- /// The logical length of the buffer in characters. (Does not include the final null.) Will automatically attempt to increase capacity.
- /// This is where the usable data ends.
- /// </summary>
- /// <exception cref="OutOfMemoryException">Thrown if unable to allocate memory when setting.</exception>
- /// <exception cref="ArgumentOutOfRangeException">Thrown if the set size in bytes is int.MaxValue (as space is implicitly reserved for the trailing null).</exception>
- public int Length
- {
- get { return _length; }
- set
- {
- // Null terminate
- EnsureCapacity(checked(value + 1));
- _buffer[value] = '\0';
-
- _length = value;
- }
- }
-
- /// <summary>
- /// True if the buffer contains the given character.
- /// </summary>
- public unsafe bool Contains(char value)
- {
- fixed (char* start = _buffer)
- {
- int length = _length;
- for (int i = 0; i < length; i++)
- {
- if (start[i] == value) return true;
- }
- }
-
- return false;
- }
-
- /// <summary>
- /// Returns true if the buffer starts with the given string.
- /// </summary>
- public bool StartsWith(string value)
- {
- if (value == null) throw new ArgumentNullException(nameof(value));
- if (_length < value.Length) return false;
- return SubstringEquals(value, startIndex: 0, count: value.Length);
- }
-
- /// <summary>
- /// Returns true if the specified StringBuffer substring equals the given value.
- /// </summary>
- /// <param name="value">The value to compare against the specified substring.</param>
- /// <param name="startIndex">Start index of the sub string.</param>
- /// <param name="count">Length of the substring, or -1 to check all remaining.</param>
- /// <exception cref="ArgumentOutOfRangeException">
- /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range
- /// of the buffer's length.
- /// </exception>
- public unsafe bool SubstringEquals(string value, int startIndex = 0, int count = -1)
- {
- if (value == null) return false;
- if (count < -1) throw new ArgumentOutOfRangeException(nameof(count));
- if (startIndex > _length) throw new ArgumentOutOfRangeException(nameof(startIndex));
-
- int realCount = count == -1 ? _length - startIndex : (int)count;
- if (checked(startIndex + realCount) > _length) throw new ArgumentOutOfRangeException(nameof(count));
-
- int length = value.Length;
-
- // Check the substring length against the input length
- if (realCount != length) return false;
-
- fixed (char* valueStart = value)
- fixed (char* bufferStart = _buffer)
- {
- char* subStringStart = bufferStart + startIndex;
-
- for (int i = 0; i < length; i++)
- {
- if (subStringStart[i] != valueStart[i]) return false;
- }
- }
-
- return true;
- }
-
- /// <summary>
- /// Append the given buffer.
- /// </summary>
- /// <param name="value">The buffer to append.</param>
- /// <param name="startIndex">The index in the input buffer to start appending from.</param>
- /// <param name="count">The count of characters to copy from the buffer string.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is null.</exception>
- /// <exception cref="ArgumentOutOfRangeException">
- /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range
- /// of <paramref name="value"/> characters.
- /// </exception>
- public void Append(ref StringBuffer value, int startIndex = 0)
- {
- if (value.Length == 0) return;
-
- value.CopyTo(
- bufferIndex: startIndex,
- destination: ref this,
- destinationIndex: _length,
- count: value.Length);
- }
-
- /// <summary>
- /// Append the given buffer.
- /// </summary>
- /// <param name="value">The buffer to append.</param>
- /// <param name="startIndex">The index in the input buffer to start appending from.</param>
- /// <param name="count">The count of characters to copy from the buffer string.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is null.</exception>
- /// <exception cref="ArgumentOutOfRangeException">
- /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range
- /// of <paramref name="value"/> characters.
- /// </exception>
- public void Append(ref StringBuffer value, int startIndex, int count)
- {
- if (count == 0) return;
-
- value.CopyTo(
- bufferIndex: startIndex,
- destination: ref this,
- destinationIndex: _length,
- count: count);
- }
-
- /// <summary>
- /// Copy contents to the specified buffer. Destination index must be within current destination length.
- /// Will grow the destination buffer if needed.
- /// </summary>
- /// <exception cref="ArgumentOutOfRangeException">
- /// Thrown if <paramref name="bufferIndex"/> or <paramref name="destinationIndex"/> or <paramref name="count"/> are outside the range
- /// of <paramref name="value"/> characters.
- /// </exception>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="destination"/> is null.</exception>
- public void CopyTo(int bufferIndex, ref StringBuffer destination, int destinationIndex, int count)
- {
- if (destinationIndex > destination._length) throw new ArgumentOutOfRangeException(nameof(destinationIndex));
- if (bufferIndex >= _length) throw new ArgumentOutOfRangeException(nameof(bufferIndex));
- if (_length < checked(bufferIndex + count)) throw new ArgumentOutOfRangeException(nameof(count));
-
- if (count == 0) return;
- int lastIndex = checked(destinationIndex + count);
- if (destination.Length < lastIndex) destination.Length = lastIndex;
-
- Array.Copy(UnderlyingArray, bufferIndex, destination.UnderlyingArray, destinationIndex, count);
- }
-
- /// <summary>
- /// Copy contents from the specified string into the buffer at the given index. Start index must be within the current length of
- /// the buffer, will grow as necessary.
- /// </summary>
- public void CopyFrom(int bufferIndex, string source, int sourceIndex = 0, int count = -1)
- {
- if (source == null) throw new ArgumentNullException(nameof(source));
- if (bufferIndex > _length) throw new ArgumentOutOfRangeException(nameof(bufferIndex));
- if (sourceIndex < 0 || sourceIndex > source.Length) throw new ArgumentOutOfRangeException(nameof(sourceIndex));
- if (count == -1) count = source.Length - sourceIndex;
- if (count < 0 || source.Length - count < sourceIndex) throw new ArgumentOutOfRangeException(nameof(count));
-
- if (count == 0) return;
- int lastIndex = bufferIndex + (int)count;
- if (_length < lastIndex) Length = lastIndex;
-
- source.CopyTo(sourceIndex, UnderlyingArray, bufferIndex, count);
- }
-
- /// <summary>
- /// Trim the specified values from the end of the buffer. If nothing is specified, nothing is trimmed.
- /// </summary>
- public void TrimEnd(char[] values)
- {
- if (values == null || values.Length == 0 || _length == 0) return;
-
- while (_length > 0 && Array.IndexOf(values, _buffer[_length - 1]) >= 0)
- {
- Length = _length - 1;
- }
- }
-
- /// <summary>
- /// String representation of the entire buffer. If the buffer is larger than the maximum size string (int.MaxValue) this will throw.
- /// </summary>
- /// <exception cref="InvalidOperationException">Thrown if the buffer is too big to fit into a string.</exception>
- public override string ToString()
- {
- return new string(_buffer, startIndex: 0, length: _length);
- }
-
- /// <summary>
- /// Get the given substring in the buffer.
- /// </summary>
- /// <param name="count">Count of characters to take, or remaining characters from <paramref name="startIndex"/> if -1.</param>
- /// <exception cref="ArgumentOutOfRangeException">
- /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range of the buffer's length
- /// or count is greater than the maximum string size (int.MaxValue).
- /// </exception>
- public string Substring(int startIndex, int count = -1)
- {
- if (startIndex > (_length == 0 ? 0 : _length - 1)) throw new ArgumentOutOfRangeException(nameof(startIndex));
- if (count < -1) throw new ArgumentOutOfRangeException(nameof(count));
-
- int realCount = count == -1 ? _length - startIndex : (int)count;
- if (realCount > int.MaxValue || checked(startIndex + realCount) > _length) throw new ArgumentOutOfRangeException(nameof(count));
-
- // The buffer could be bigger than will fit into a string, but the substring might fit. As the starting
- // index might be bigger than int we need to index ourselves.
- return new string(_buffer, startIndex: startIndex, length: realCount);
- }
-
- public void Free()
- {
- ArrayPool<char>.Shared.Return(_buffer);
- _buffer = null;
- _length = 0;
- }
-
- public ReadOnlySpan<char> AsSpan()
- => new ReadOnlySpan<char>(UnderlyingArray, 0, Length);
- }
-}
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace System.Text
{
_pos = 0;
}
- public int Length => _pos;
+ public int Length
+ {
+ get => _pos;
+ set
+ {
+ Debug.Assert(value <= _chars.Length);
+ _pos = value;
+ }
+ }
+
+ public int Capacity => _chars.Length;
+
+ public void EnsureCapacity(int capacity)
+ {
+ if (capacity > _chars.Length)
+ Grow(capacity - _chars.Length);
+ }
+
+ public ref char GetPinnableReference() => ref MemoryMarshal.GetReference(_chars);
public ref char this[int index]
{
return s;
}
+ public ReadOnlySpan<char> AsSpan() => _chars.Slice(0, _pos);
+
public bool TryCopyTo(Span<char> destination, out int charsWritten)
{
if (_chars.Slice(0, _pos).TryCopyTo(destination))
_pos += length;
}
+ public unsafe void Append(ReadOnlySpan<char> value)
+ {
+ int pos = _pos;
+ if (pos > _chars.Length - value.Length)
+ {
+ Grow(value.Length);
+ }
+
+ value.CopyTo(_chars.Slice(_pos));
+ _pos += value.Length;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<char> AppendSpan(int length)
{
[MethodImpl(MethodImplOptions.NoInlining)]
private void Grow(int requiredAdditionalCapacity)
{
- Debug.Assert(requiredAdditionalCapacity > _chars.Length - _pos);
+ Debug.Assert(requiredAdditionalCapacity > 0);
char[] poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2));