<InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\SEHException.cs" />
<InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\SafeBuffer.cs" />
<InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\SafeHandle.cs" />
+ <InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\SafeHeapHandle.cs" />
<InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\BStrWrapper.cs" />
<InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\CurrencyWrapper.cs" />
<InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\ErrorWrapper.cs" />
<InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\InvalidComObjectException.cs" />
<InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\SafeArrayRankMismatchException.cs" />
<InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\SafeArrayTypeMismatchException.cs" />
+ <InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\NativeBuffer.cs" />
+ <InteropSources Include="$(BclSourcesRoot)\System\Runtime\InteropServices\StringBuffer.cs" />
<InteropSources Condition="'$(FeatureCoreClr)'=='true'" Include="$(BclSourcesRoot)\System\Runtime\InteropServices\NativeCallableAttribute.cs" />
<InteropSources Condition="'$(FeatureCominterop)' != 'true'" Include="$(BclSourcesRoot)\System\Runtime\InteropServices\NonPortable.cs" />
<InteropSources Condition="'$(FeatureCominterop)' == 'true'" Include="$(BclSourcesRoot)\System\Runtime\InteropServices\DispatchWrapper.cs" />
<IoSources Include="$(BclSourcesRoot)\System\IO\MemoryStream.cs" />
<IoSources Include="$(BclSourcesRoot)\System\IO\Path.cs" />
<IoSources Include="$(BclSourcesRoot)\System\IO\PathHelper.cs" />
+ <IoSources Include="$(BclSourcesRoot)\System\IO\LongPathHelper.cs" />
+ <IoSources Include="$(BclSourcesRoot)\System\IO\PathInternal.cs" />
<IoSources Include="$(BclSourcesRoot)\System\IO\PathTooLongException.cs" />
<IoSources Include="$(BclSourcesRoot)\System\IO\PinnedBufferMemoryStream.cs" />
<IoSources Include="$(BclSourcesRoot)\System\IO\ReadLinesIterator.cs" />
[DllImport(KERNEL32, SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
internal unsafe static extern int GetLongPathName(char* path, char* longPathBuffer, int bufferLength);
+ [DllImport(KERNEL32, SetLastError = true, ExactSpelling = true)]
+ internal unsafe static extern uint GetFullPathNameW(char* path, uint numBufferChars, SafeHandle buffer, IntPtr mustBeZero);
+
[DllImport(KERNEL32, SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
internal static extern int GetLongPathName(String path, [Out]StringBuilder longPathBuffer, int bufferLength);
+ [DllImport(KERNEL32, SetLastError = true, ExactSpelling = true)]
+ internal static extern uint GetLongPathNameW(SafeHandle lpszShortPath, SafeHandle lpszLongPath, uint cchBuffer);
+
+ [DllImport(KERNEL32, SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
+ internal static extern uint GetLongPathNameW(string lpszShortPath, SafeHandle lpszLongPath, uint cchBuffer);
+
// Disallow access to all non-file devices from methods that take
// a String. This disallows DOS devices like "con:", "com1:",
// "lpt1:", etc. Use this to avoid security problems, like allowing
internal static readonly string SwitchNoAsyncCurrentCulture = "Switch.System.Globalization.NoAsyncCurrentCulture";
internal static readonly string SwitchThrowExceptionIfDisposedCancellationTokenSource = "Switch.System.Threading.ThrowExceptionIfDisposedCancellationTokenSource";
internal static readonly string SwitchPreserveEventListnerObjectIdentity = "Switch.System.Diagnostics.EventSource.PreserveEventListnerObjectIdentity";
+#if FEATURE_PATHCOMPAT
+ internal static readonly string SwitchUseLegacyPathHandling = "Switch.System.IO.UseLegacyPathHandling";
+ internal static readonly string SwitchBlockLongPaths = "Switch.System.IO.BlockLongPaths";
+#endif
-
// This is a partial method. Platforms can provide an implementation of it that will set override values
// from whatever mechanism is available on that platform. If no implementation is provided, the compiler is going to remove the calls
// to it from the code
AppContext.DefineSwitchDefault(SwitchNoAsyncCurrentCulture, true);
AppContext.DefineSwitchDefault(SwitchThrowExceptionIfDisposedCancellationTokenSource, true);
}
+#if FEATURE_PATHCOMPAT
+ if (version <= 40601)
+ {
+ AppContext.DefineSwitchDefault(SwitchUseLegacyPathHandling, true);
+ AppContext.DefineSwitchDefault(SwitchBlockLongPaths, true);
+ }
+#endif
break;
}
case "WindowsPhone":
if (Path.IsRelative(propertyValues[i]))
throw new ArgumentException( Environment.GetResourceString( "Argument_AbsolutePathRequired" ) );
- newSetup.ApplicationBase=Path.NormalizePath(propertyValues[i],true);
+ newSetup.ApplicationBase = NormalizePath(propertyValues[i], fullCheck: true);
}
#if FEATURE_CAS_POLICY
{
providedSecurityInfo=new Evidence();
providedSecurityInfo.AddHostEvidence(new Url(propertyValues[i]));
- ad.SetDataHelper(propertyNames[i],propertyValues[i],null);
+ ad.SetDataHelper(propertyNames[i],propertyValues[i],null);
}
#endif // FEATURE_CAS_POLICY
-#if FEATURE_LOADER_OPTIMIZATION
+#if FEATURE_LOADER_OPTIMIZATION
else
if(propertyNames[i]=="LOADER_OPTIMIZATION")
{
default: throw new ArgumentException(Environment.GetResourceString("Argument_UnrecognizedLoaderOptimization"), "LOADER_OPTIMIZATION");
}
}
-#endif // FEATURE_LOADER_OPTIMIZATION
-#if FEATURE_CORECLR
+#endif // FEATURE_LOADER_OPTIMIZATION
+#if FEATURE_CORECLR
else
if(propertyNames[i]=="NATIVE_DLL_SEARCH_DIRECTORIES")
{
if (Path.IsRelative(path))
throw new ArgumentException( Environment.GetResourceString( "Argument_AbsolutePathRequired" ) );
- string appPath=Path.NormalizePath(path,true);
+ string appPath = NormalizePath(path, fullCheck: true);
normalisedAppPathList.Append(appPath);
normalisedAppPathList.Append(Path.PathSeparator);
}
{
normalisedAppPathList.Remove(normalisedAppPathList.Length - 1, 1);
}
- ad.SetDataHelper(propertyNames[i],normalisedAppPathList.ToString(),null); // not supported by fusion, so set explicitly
+ ad.SetDataHelper(propertyNames[i],normalisedAppPathList.ToString(),null); // not supported by fusion, so set explicitly
}
else
if(propertyNames[i]!= null)
{
- ad.SetDataHelper(propertyNames[i],propertyValues[i],null); // just propagate
+ ad.SetDataHelper(propertyNames[i],propertyValues[i],null); // just propagate
}
#endif
#endif // FEATURE_CLICKONCE
}
+ [SecuritySafeCritical]
+ internal static string NormalizePath(string path, bool fullCheck)
+ {
+ return Path.NormalizePath(
+ path: path,
+ fullCheck: fullCheck,
+ maxPathLength: PathInternal.MaxShortPath,
+ expandShortPaths: true);
+ }
#if FEATURE_APTCA
// Called from DomainAssembly in Conditional APTCA cases
#endif
- // This routine is called from unmanaged code to
- // set the default fusion context.
- [System.Security.SecurityCritical] // auto-generated
+ // This routine is called from unmanaged code to
+ // set the default fusion context.
+ [System.Security.SecurityCritical] // auto-generated
private void SetupDomain(bool allowRedirects, String path, String configFile, String[] propertyNames, String[] propertyValues)
{
// It is possible that we could have multiple threads initializing
bool slash = false;
if ((path[0] == '/') || (path[0] == '\\')) {
- String pathRoot = Path.GetPathRoot(appBase);
+ string pathRoot = AppDomain.NormalizePath(appBase, fullCheck: false);
+ pathRoot = pathRoot.Substring(0, IO.PathInternal.GetRootLength(pathRoot));
+
if (pathRoot.Length == 0) { // URL
int index = appBase.IndexOf(":/", StringComparison.Ordinal);
if (index == -1)
path = StringBuilderCache.GetStringAndRelease(result);
}
else
- path = Path.GetFullPathInternal(path);
+ path = AppDomain.NormalizePath(path, fullCheck: true);
}
return path;
--- /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.Diagnostics.Contracts;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Microsoft.Win32;
+
+namespace System.IO
+{
+ /// <summary>
+ /// Wrapper to help with path normalization.
+ /// </summary>
+ internal class LongPathHelper
+ {
+ // Can't be over 8.3 and be a short name
+ private const int MaxShortName = 12;
+
+ private const char LastAnsi = (char)255;
+ private const char Delete = (char)127;
+
+ // Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space.
+ // string.WhitespaceChars will trim more aggressively than what the underlying FS does (for ex, NTFS, FAT).
+ internal static readonly char[] s_trimEndChars =
+ {
+ (char)0x9, // Horizontal tab
+ (char)0xA, // Line feed
+ (char)0xB, // Vertical tab
+ (char)0xC, // Form feed
+ (char)0xD, // Carriage return
+ (char)0x20, // Space
+ (char)0x85, // Next line
+ (char)0xA0 // Non breaking space
+ };
+
+ [ThreadStatic]
+ private static StringBuffer t_fullPathBuffer;
+
+ /// <summary>
+ /// Normalize the given path.
+ /// </summary>
+ /// <remarks>
+ /// Normalizes via Win32 GetFullPathName(). It will also trim all "typical" whitespace at the end of the path (see s_trimEndChars). Will also trim initial
+ /// spaces if the path is determined to be rooted.
+ ///
+ /// Note that invalid characters will be checked after the path is normalized, which could remove bad characters. (C:\|\..\a.txt -- C:\a.txt)
+ /// </remarks>
+ /// <param name="path">Path to normalize</param>
+ /// <param name="checkInvalidCharacters">True to check for invalid characters</param>
+ /// <param name="expandShortPaths">Attempt to expand short paths if true</param>
+ /// <exception cref="ArgumentException">Thrown if the path is an illegal UNC (does not contain a full server/share) or contains illegal characters.</exception>
+ /// <exception cref="PathTooLongException">Thrown if the path or a path segment exceeds the filesystem limits.</exception>
+ /// <exception cref="FileNotFoundException">Thrown if Windows returns ERROR_FILE_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error)</exception>
+ /// <exception cref="DirectoryNotFoundException">Thrown if Windows returns ERROR_PATH_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error)</exception>
+ /// <exception cref="UnauthorizedAccessException">Thrown if Windows returns ERROR_ACCESS_DENIED. (See Win32Marshal.GetExceptionForWin32Error)</exception>
+ /// <exception cref="IOException">Thrown if Windows returns an error that doesn't map to the above. (See Win32Marshal.GetExceptionForWin32Error)</exception>
+ /// <returns>Normalized path</returns>
+ [System.Security.SecurityCritical]
+ unsafe internal static string Normalize(string path, uint maxPathLength, bool checkInvalidCharacters, bool expandShortPaths)
+ {
+ // Get the full path
+ StringBuffer fullPath = t_fullPathBuffer ?? (t_fullPathBuffer = new StringBuffer(PathInternal.MaxShortPath));
+ try
+ {
+ GetFullPathName(path, fullPath);
+
+ // Trim whitespace off the end of the string. Win32 normalization trims only U+0020.
+ fullPath.TrimEnd(s_trimEndChars);
+
+ if (fullPath.Length >= maxPathLength)
+ {
+ // Fullpath is genuinely too long
+ throw new PathTooLongException();
+ }
+
+ // Checking path validity used to happen before getting the full path name. To avoid additional input allocation
+ // (to trim trailing whitespace) we now do it after the Win32 call. This will allow legitimate paths through that
+ // used to get kicked back (notably segments with invalid characters might get removed via "..").
+ //
+ // There is no way that GetLongPath can invalidate the path so we'll do this (cheaper) check before we attempt to
+ // expand short file names.
+
+ // Scan the path for:
+ //
+ // - Illegal path characters.
+ // - Invalid UNC paths like \\, \\server, \\server\.
+ // - Segments that are too long (over MaxComponentLength)
+
+ // As the path could be > 60K, we'll combine the validity scan. None of these checks are performed by the Win32
+ // GetFullPathName() API.
+
+ bool possibleShortPath = false;
+ bool foundTilde = false;
+
+ // We can get UNCs as device paths through this code (e.g. \\.\UNC\), we won't validate them as there isn't
+ // an easy way to normalize without extensive cost (we'd have to hunt down the canonical name for any device
+ // path that contains UNC or to see if the path was doing something like \\.\GLOBALROOT\Device\Mup\,
+ // \\.\GLOBAL\UNC\, \\.\GLOBALROOT\GLOBAL??\UNC\, etc.
+ bool specialPath = fullPath.Length > 1 && fullPath[0] == '\\' && fullPath[1] == '\\';
+ bool isDevice = PathInternal.IsDevice(fullPath);
+ bool possibleBadUnc = specialPath && !isDevice;
+ uint index = specialPath ? 2u : 0;
+ uint lastSeparator = specialPath ? 1u : 0;
+ uint segmentLength;
+ char* start = fullPath.CharPointer;
+ char current;
+
+ while (index < fullPath.Length)
+ {
+ current = start[index];
+
+ // Try to skip deeper analysis. '?' and higher are valid/ignorable except for '\', '|', and '~'
+ if (current < '?' || current == '\\' || current == '|' || current == '~')
+ {
+ switch (current)
+ {
+ case '|':
+ case '>':
+ case '<':
+ case '\"':
+ if (checkInvalidCharacters) throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars"));
+ // No point in expanding a bad path
+ foundTilde = false;
+ break;
+ case '~':
+ foundTilde = true;
+ break;
+ case '\\':
+ segmentLength = index - lastSeparator - 1;
+ if (segmentLength > (uint)PathInternal.MaxComponentLength)
+ throw new PathTooLongException();
+ lastSeparator = index;
+
+ if (foundTilde)
+ {
+ if (segmentLength <= MaxShortName)
+ {
+ // Possibly a short path.
+ possibleShortPath = true;
+ }
+
+ foundTilde = false;
+ }
+
+ if (possibleBadUnc)
+ {
+ // If we're at the end of the path and this is the first separator, we're missing the share.
+ // Otherwise we're good, so ignore UNC tracking from here.
+ if (index == fullPath.Length - 1)
+ throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC"));
+ else
+ possibleBadUnc = false;
+ }
+
+ break;
+
+ default:
+ if (checkInvalidCharacters && current < ' ') throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars"));
+ break;
+ }
+ }
+
+ index++;
+ }
+
+ if (possibleBadUnc)
+ throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC"));
+
+ segmentLength = fullPath.Length - lastSeparator - 1;
+ if (segmentLength > (uint)PathInternal.MaxComponentLength)
+ throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
+
+ if (foundTilde && segmentLength <= MaxShortName)
+ possibleShortPath = true;
+
+ // Check for a short filename path and try and expand it. Technically you don't need to have a tilde for a short name, but
+ // this is how we've always done this. This expansion is costly so we'll continue to let other short paths slide.
+ if (expandShortPaths && possibleShortPath)
+ {
+ return TryExpandShortFileName(fullPath, originalPath: path);
+ }
+ else
+ {
+ if (fullPath.Length == (uint)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 StringBuilder.
+ return path;
+ }
+ else
+ {
+ return fullPath.ToString();
+ }
+ }
+ }
+ finally
+ {
+ // Clear the buffer
+ fullPath.Free();
+ }
+ }
+
+ [System.Security.SecurityCritical]
+ unsafe private static void GetFullPathName(string path, StringBuffer fullPath)
+ {
+ // 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.
+ Contract.Assert(PathInternal.IsPartiallyQualified(path) || !PathInternal.IsExtended(path));
+
+ // Historically we would skip leading spaces *only* if the path started with a drive " C:" or a UNC " \\"
+ int startIndex = PathInternal.PathStartSkip(path);
+
+ fixed (char* pathStart = path)
+ {
+ uint result = 0;
+ while ((result = Win32Native.GetFullPathNameW(pathStart + startIndex, fullPath.CharCapacity, fullPath.GetHandle(), IntPtr.Zero)) > fullPath.CharCapacity)
+ {
+ // Reported size (which does not include the null) is greater than the buffer size. Increase the capacity.
+ fullPath.EnsureCharCapacity(result);
+ }
+
+ if (result == 0)
+ {
+ // Failure, get the error and throw
+ int errorCode = Marshal.GetLastWin32Error();
+ if (errorCode == 0)
+ errorCode = Win32Native.ERROR_BAD_PATHNAME;
+ __Error.WinIOError(errorCode, path);
+ }
+
+ fullPath.Length = result;
+ }
+ }
+
+ [System.Security.SecurityCritical]
+ unsafe internal static string GetLongPathName(StringBuffer path)
+ {
+ using (StringBuffer outputBuffer = new StringBuffer(path.Length))
+ {
+ uint result = 0;
+ while ((result = Win32Native.GetLongPathNameW(path.GetHandle(), outputBuffer.GetHandle(), outputBuffer.CharCapacity)) > outputBuffer.CharCapacity)
+ {
+ // Reported size (which does not include the null) is greater than the buffer size. Increase the capacity.
+ outputBuffer.EnsureCharCapacity(result);
+ }
+
+ if (result == 0)
+ {
+ // Failure, get the error and throw
+ GetErrorAndThrow(path.ToString());
+ }
+
+ outputBuffer.Length = result;
+ return outputBuffer.ToString();
+ }
+ }
+
+ [System.Security.SecurityCritical]
+ unsafe internal static string GetLongPathName(string path)
+ {
+ using (StringBuffer outputBuffer = new StringBuffer((uint)path.Length))
+ {
+ uint result = 0;
+ while ((result = Win32Native.GetLongPathNameW(path, outputBuffer.GetHandle(), outputBuffer.CharCapacity)) > outputBuffer.CharCapacity)
+ {
+ // Reported size (which does not include the null) is greater than the buffer size. Increase the capacity.
+ outputBuffer.EnsureCharCapacity(result);
+ }
+
+ if (result == 0)
+ {
+ // Failure, get the error and throw
+ GetErrorAndThrow(path);
+ }
+
+ outputBuffer.Length = result;
+ return outputBuffer.ToString();
+ }
+ }
+
+ [System.Security.SecurityCritical]
+ private static void GetErrorAndThrow(string path)
+ {
+ int errorCode = Marshal.GetLastWin32Error();
+ if (errorCode == 0)
+ errorCode = Win32Native.ERROR_BAD_PATHNAME;
+ __Error.WinIOError(errorCode, path);
+ }
+
+ [System.Security.SecuritySafeCritical]
+ private unsafe static string TryExpandShortFileName(StringBuffer outputBuffer, 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.
+
+ Contract.Assert(!PathInternal.IsPartiallyQualified(outputBuffer), "should have resolved by now");
+
+ using (StringBuffer inputBuffer = new StringBuffer(outputBuffer))
+ {
+ bool success = false;
+ uint lastIndex = outputBuffer.Length - 1;
+ uint foundIndex = lastIndex;
+ uint rootLength = PathInternal.GetRootLength(outputBuffer);
+
+ while (!success)
+ {
+ uint result = Win32Native.GetLongPathNameW(inputBuffer.GetHandle(), outputBuffer.GetHandle(), outputBuffer.CharCapacity);
+
+ // Replace any temporary null we added
+ if (inputBuffer[foundIndex] == '\0') inputBuffer[foundIndex] = '\\';
+
+ if (result == 0)
+ {
+ // Look to see if we couldn't find the file
+ int error = Marshal.GetLastWin32Error();
+ if (error != Win32Native.ERROR_FILE_NOT_FOUND && error != Win32Native.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';
+ }
+ }
+ else if (result > outputBuffer.CharCapacity)
+ {
+ // Not enough space. The result count for this API does not include the null terminator.
+ outputBuffer.EnsureCharCapacity(result);
+ }
+ else
+ {
+ // Found the path
+ success = true;
+ outputBuffer.Length = result;
+ if (foundIndex < lastIndex)
+ {
+ // It was a partial find, put the non-existant part of the path back
+ outputBuffer.Append(inputBuffer, foundIndex, inputBuffer.Length - foundIndex);
+ }
+ }
+ }
+
+ StringBuffer bufferToUse = success ? outputBuffer : inputBuffer;
+
+ if (bufferToUse.SubstringEquals(originalPath))
+ {
+ // Use the original path to avoid allocating
+ return originalPath;
+ }
+
+ return bufferToUse.ToString();
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Win32;
+using System;
+using System.Diagnostics.Contracts;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security;
+using System.Text;
+
+namespace System.IO
+{
+ /// <summary>Contains internal path helpers that are shared between many projects.</summary>
+ internal static class PathInternal
+ {
+ internal const string ExtendedPathPrefix = @"\\?\";
+ internal const string UncPathPrefix = @"\\";
+ internal const string UncExtendedPrefixToInsert = @"?\UNC\";
+ internal const string UncExtendedPathPrefix = @"\\?\UNC\";
+ internal const string DevicePathPrefix = @"\\.\";
+ internal const int DevicePrefixLength = 4;
+ internal const int MaxShortPath = 260;
+ internal const int MaxShortDirectoryPath = 248;
+
+ // Windows is limited in long paths by the max size of its internal representation of a unicode string.
+ // UNICODE_STRING has a max length of USHORT in _bytes_ without a trailing null.
+ // https://msdn.microsoft.com/en-us/library/windows/hardware/ff564879.aspx
+ internal const int MaxLongPath = short.MaxValue;
+ internal static readonly int MaxComponentLength = 255;
+
+ internal static readonly char[] InvalidPathChars =
+ {
+ '\"', '<', '>', '|', '\0',
+ (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
+ (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
+ (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
+ (char)31
+ };
+
+ /// <summary>
+ /// Validates volume separator only occurs as C: or \\?\C:. This logic is meant to filter out Alternate Data Streams.
+ /// </summary>
+ /// <returns>True if the path has an invalid volume separator.</returns>
+ internal static bool HasInvalidVolumeSeparator(string path)
+ {
+ // Toss out paths with colons that aren't a valid drive specifier.
+ // Cannot start with a colon and can only be of the form "C:" or "\\?\C:".
+ // (Note that we used to explicitly check "http:" and "file:"- these are caught by this check now.)
+
+ // We don't care about skipping starting space for extended paths. Assume no knowledge of extended paths if we're forcing old path behavior.
+ bool isExtended =
+#if FEATURE_PATHCOMPAT
+ !AppContextSwitches.UseLegacyPathHandling &&
+#endif
+ IsExtended(path);
+ int startIndex = isExtended ? ExtendedPathPrefix.Length : PathStartSkip(path);
+
+ // If we start with a colon
+ if ((path.Length > startIndex && path[startIndex] == Path.VolumeSeparatorChar)
+ // Or have an invalid drive letter and colon
+ || (path.Length >= startIndex + 2 && path[startIndex + 1] == Path.VolumeSeparatorChar && !IsValidDriveChar(path[startIndex]))
+ // Or have any colons beyond the drive colon
+ || (path.Length > startIndex + 2 && path.IndexOf(Path.VolumeSeparatorChar, startIndex + 2) != -1))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Returns true if the given StringBuilder starts with the given value.
+ /// </summary>
+ /// <param name="value">The string to compare against the start of the StringBuilder.</param>
+ internal static bool StartsWithOrdinal(StringBuilder builder, string value, bool ignoreCase = false)
+ {
+ if (value == null || builder.Length < value.Length)
+ return false;
+
+ if (ignoreCase)
+ {
+ for (int i = 0; i < value.Length; i++)
+ if (char.ToUpperInvariant(builder[i]) != char.ToUpperInvariant(value[i])) return false;
+ }
+ else
+ {
+ for (int i = 0; i < value.Length; i++)
+ if (builder[i] != value[i]) return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Returns true if the given character is a valid drive letter
+ /// </summary>
+ internal static bool IsValidDriveChar(char value)
+ {
+ return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'));
+ }
+
+ /// <summary>
+ /// Returns true if the path is too long
+ /// </summary>
+ internal static bool IsPathTooLong(string fullPath)
+ {
+ // We'll never know precisely what will fail as paths get changed internally in Windows and
+ // may grow beyond / shrink below exceed MaxLongPath.
+#if FEATURE_PATHCOMPAT
+ if (AppContextSwitches.BlockLongPaths)
+ {
+ // We allow paths of any length if extended (and not in compat mode)
+ if (AppContextSwitches.UseLegacyPathHandling || !IsExtended(fullPath))
+ return fullPath.Length >= MaxShortPath;
+ }
+#endif
+
+ return fullPath.Length >= MaxLongPath;
+ }
+
+ /// <summary>
+ /// Return true if any path segments are too long
+ /// </summary>
+ internal static bool AreSegmentsTooLong(string fullPath)
+ {
+ int length = fullPath.Length;
+ int lastSeparator = 0;
+
+ for (int i = 0; i < length; i++)
+ {
+ if (IsDirectorySeparator(fullPath[i]))
+ {
+ if (i - lastSeparator > MaxComponentLength)
+ return true;
+ lastSeparator = i;
+ }
+ }
+
+ if (length - 1 - lastSeparator > MaxComponentLength)
+ return true;
+
+ return false;
+ }
+
+ /// <summary>
+ /// Returns true if the directory is too long
+ /// </summary>
+ internal static bool IsDirectoryTooLong(string fullPath)
+ {
+#if FEATURE_PATHCOMPAT
+ if (AppContextSwitches.BlockLongPaths)
+ {
+ // We allow paths of any length if extended (and not in compat mode)
+ if (AppContextSwitches.UseLegacyPathHandling || !IsExtended(fullPath))
+ return (fullPath.Length >= MaxShortDirectoryPath);
+ }
+#endif
+
+ return IsPathTooLong(fullPath);
+ }
+
+ /// <summary>
+ /// Adds the extended path prefix (\\?\) if not relative or already a device path.
+ /// </summary>
+ internal static string EnsureExtendedPrefix(string path)
+ {
+ // Putting the extended prefix on the path changes the processing of the path. It won't get normalized, which
+ // means adding to relative paths will prevent them from getting the appropriate current directory inserted.
+
+ // If it already has some variant of a device path (\??\, \\?\, \\.\, //./, etc.) we don't need to change it
+ // as it is either correct or we will be changing the behavior. When/if Windows supports long paths implicitly
+ // in the future we wouldn't want normalization to come back and break existing code.
+
+ // In any case, all internal usages should be hitting normalize path (Path.GetFullPath) before they hit this
+ // shimming method. (Or making a change that doesn't impact normalization, such as adding a filename to a
+ // normalized base path.)
+ if (IsPartiallyQualified(path) || IsDevice(path))
+ return path;
+
+ // Given \\server\share in longpath becomes \\?\UNC\server\share
+ if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase))
+ return path.Insert(2, UncExtendedPrefixToInsert);
+
+ return ExtendedPathPrefix + path;
+ }
+
+ /// <summary>
+ /// Removes the extended path prefix (\\?\) if present.
+ /// </summary>
+ internal static string RemoveExtendedPrefix(string path)
+ {
+ if (!IsExtended(path))
+ return path;
+
+ // Given \\?\UNC\server\share we return \\server\share
+ if (IsExtendedUnc(path))
+ return path.Remove(2, 6);
+
+ return path.Substring(DevicePrefixLength);
+ }
+
+ /// <summary>
+ /// Removes the extended path prefix (\\?\) if present.
+ /// </summary>
+ internal static StringBuilder RemoveExtendedPrefix(StringBuilder path)
+ {
+ if (!IsExtended(path))
+ return path;
+
+ // Given \\?\UNC\server\share we return \\server\share
+ if (IsExtendedUnc(path))
+ return path.Remove(2, 6);
+
+ return path.Remove(0, DevicePrefixLength);
+ }
+
+ /// <summary>
+ /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
+ /// </summary>
+ internal static bool IsDevice(string path)
+ {
+ // If the path begins with any two separators it will be recognized and normalized and prepped with
+ // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
+ return IsExtended(path)
+ ||
+ (
+ path.Length >= DevicePrefixLength
+ && IsDirectorySeparator(path[0])
+ && IsDirectorySeparator(path[1])
+ && (path[2] == '.' || path[2] == '?')
+ && IsDirectorySeparator(path[3])
+ );
+ }
+
+ /// <summary>
+ /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
+ /// </summary>
+ internal static bool IsDevice(StringBuffer path)
+ {
+ // If the path begins with any two separators it will be recognized and normalized and prepped with
+ // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
+ return IsExtended(path)
+ ||
+ (
+ path.Length >= DevicePrefixLength
+ && IsDirectorySeparator(path[0])
+ && IsDirectorySeparator(path[1])
+ && (path[2] == '.' || path[2] == '?')
+ && IsDirectorySeparator(path[3])
+ );
+ }
+
+ /// <summary>
+ /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
+ /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
+ /// and path length checks.
+ /// </summary>
+ internal static bool IsExtended(string path)
+ {
+ // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
+ // Skipping of normalization will *only* occur if back slashes ('\') are used.
+ return path.Length >= DevicePrefixLength
+ && path[0] == '\\'
+ && (path[1] == '\\' || path[1] == '?')
+ && path[2] == '?'
+ && path[3] == '\\';
+ }
+
+ /// <summary>
+ /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
+ /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
+ /// and path length checks.
+ /// </summary>
+ internal static bool IsExtended(StringBuilder path)
+ {
+ // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
+ // Skipping of normalization will *only* occur if back slashes ('\') are used.
+ return path.Length >= DevicePrefixLength
+ && path[0] == '\\'
+ && (path[1] == '\\' || path[1] == '?')
+ && path[2] == '?'
+ && path[3] == '\\';
+ }
+
+ /// <summary>
+ /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
+ /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
+ /// and path length checks.
+ /// </summary>
+ internal static bool IsExtended(StringBuffer path)
+ {
+ // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
+ // Skipping of normalization will *only* occur if back slashes ('\') are used.
+ return path.Length >= DevicePrefixLength
+ && path[0] == '\\'
+ && (path[1] == '\\' || path[1] == '?')
+ && path[2] == '?'
+ && path[3] == '\\';
+ }
+
+ /// <summary>
+ /// Returns true if the path uses the extended UNC syntax (\\?\UNC\ or \??\UNC\)
+ /// </summary>
+ internal static bool IsExtendedUnc(string path)
+ {
+ return path.Length >= UncExtendedPathPrefix.Length
+ && IsExtended(path)
+ && char.ToUpper(path[4]) == 'U'
+ && char.ToUpper(path[5]) == 'N'
+ && char.ToUpper(path[6]) == 'C'
+ && path[7] == '\\';
+ }
+
+ /// <summary>
+ /// Returns true if the path uses the extended UNC syntax (\\?\UNC\ or \??\UNC\)
+ /// </summary>
+ internal static bool IsExtendedUnc(StringBuilder path)
+ {
+ return path.Length >= UncExtendedPathPrefix.Length
+ && IsExtended(path)
+ && char.ToUpper(path[4]) == 'U'
+ && char.ToUpper(path[5]) == 'N'
+ && char.ToUpper(path[6]) == 'C'
+ && path[7] == '\\';
+ }
+
+ /// <summary>
+ /// Returns a value indicating if the given path contains invalid characters (", <, >, |
+ /// NUL, or any ASCII char whose integer representation is in the range of 1 through 31).
+ /// Does not check for wild card characters ? and *.
+ ///
+ /// Will not check if the path is a device path and not in Legacy mode as many of these
+ /// characters are valid for devices (pipes for example).
+ /// </summary>
+ internal static bool HasIllegalCharacters(string path, bool checkAdditional = false)
+ {
+ if (
+#if FEATURE_PATHCOMPAT
+ !AppContextSwitches.UseLegacyPathHandling &&
+#endif
+ IsDevice(path))
+ {
+ return false;
+ }
+
+ return AnyPathHasIllegalCharacters(path, checkAdditional: checkAdditional);
+ }
+
+ /// <summary>
+ /// Version of HasIllegalCharacters that checks no AppContextSwitches. Only use if you know you need to skip switches and don't care
+ /// about proper device path handling.
+ /// </summary>
+ internal static bool AnyPathHasIllegalCharacters(string path, bool checkAdditional = false)
+ {
+ return path.IndexOfAny(InvalidPathChars) >= 0 || (checkAdditional && AnyPathHasWildCardCharacters(path));
+ }
+
+ /// <summary>
+ /// Check for ? and *.
+ /// </summary>
+ internal static bool HasWildCardCharacters(string path)
+ {
+ // Question mark is part of some device paths
+ int startIndex =
+#if FEATURE_PATHCOMPAT
+ AppContextSwitches.UseLegacyPathHandling ? 0 :
+#endif
+ IsDevice(path) ? ExtendedPathPrefix.Length : 0;
+ return AnyPathHasWildCardCharacters(path, startIndex: startIndex);
+ }
+
+ /// <summary>
+ /// Version of HasWildCardCharacters that checks no AppContextSwitches. Only use if you know you need to skip switches and don't care
+ /// about proper device path handling.
+ /// </summary>
+ internal static bool AnyPathHasWildCardCharacters(string path, int startIndex = 0)
+ {
+ char currentChar;
+ for (int i = startIndex; i < path.Length; i++)
+ {
+ currentChar = path[i];
+ if (currentChar == '*' || currentChar == '?') return true;
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Gets the length of the root of the path (drive, share, etc.).
+ /// </summary>
+ [System.Security.SecuritySafeCritical]
+ internal unsafe static int GetRootLength(string path)
+ {
+ fixed (char* value = path)
+ {
+ return (int)GetRootLength(value, (ulong)path.Length);
+ }
+ }
+
+ /// <summary>
+ /// Gets the length of the root of the path (drive, share, etc.).
+ /// </summary>
+ [System.Security.SecuritySafeCritical]
+ internal unsafe static uint GetRootLength(StringBuffer path)
+ {
+ if (path.Length == 0) return 0;
+ return GetRootLength(path.CharPointer, path.Length);
+ }
+
+ [System.Security.SecurityCritical]
+ private unsafe static uint GetRootLength(char* path, ulong pathLength)
+ {
+ uint i = 0;
+ uint volumeSeparatorLength = 2; // Length to the colon "C:"
+ uint uncRootLength = 2; // Length to the start of the server name "\\"
+
+ bool extendedSyntax = StartsWithOrdinal(path, pathLength, ExtendedPathPrefix);
+ bool extendedUncSyntax = StartsWithOrdinal(path, pathLength, UncExtendedPathPrefix);
+ if (extendedSyntax)
+ {
+ // Shift the position we look for the root from to account for the extended prefix
+ if (extendedUncSyntax)
+ {
+ // "\\" -> "\\?\UNC\"
+ uncRootLength = (uint)UncExtendedPathPrefix.Length;
+ }
+ else
+ {
+ // "C:" -> "\\?\C:"
+ volumeSeparatorLength += (uint)ExtendedPathPrefix.Length;
+ }
+ }
+
+ if ((!extendedSyntax || extendedUncSyntax) && pathLength > 0 && IsDirectorySeparator(path[0]))
+ {
+ // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
+
+ i = 1; // Drive rooted (\foo) is one character
+ if (extendedUncSyntax || (pathLength > 1 && IsDirectorySeparator(path[1])))
+ {
+ // UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
+ // (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
+ i = uncRootLength;
+ int n = 2; // Maximum separators to skip
+ while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
+ }
+ }
+ else if (pathLength >= volumeSeparatorLength && path[volumeSeparatorLength - 1] == Path.VolumeSeparatorChar)
+ {
+ // Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
+ // If the colon is followed by a directory separator, move past it
+ i = volumeSeparatorLength;
+ if (pathLength >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++;
+ }
+ return i;
+ }
+
+ [System.Security.SecurityCritical]
+ private unsafe static bool StartsWithOrdinal(char* source, ulong sourceLength, string value)
+ {
+ if (sourceLength < (ulong)value.Length) return false;
+ for (int i = 0; i < value.Length; i++)
+ {
+ if (value[i] != source[i]) return false;
+ }
+ return true;
+ }
+
+ /// <summary>
+ /// Returns true if the path specified is relative to the current drive or working directory.
+ /// Returns false if the path is fixed to a specific drive or UNC path. This method does no
+ /// validation of the path (URIs will be returned as relative as a result).
+ /// </summary>
+ /// <remarks>
+ /// Handles paths that use the alternate directory separator. It is a frequent mistake to
+ /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
+ /// "C:a" is drive relative- meaning that it will be resolved against the current directory
+ /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
+ /// will not be used to modify the path).
+ /// </remarks>
+ internal static bool IsPartiallyQualified(string path)
+ {
+ if (path.Length < 2)
+ {
+ // It isn't fixed, it must be relative. There is no way to specify a fixed
+ // path with one character (or less).
+ return true;
+ }
+
+ if (IsDirectorySeparator(path[0]))
+ {
+ // There is no valid way to specify a relative path with two initial slashes or
+ // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
+ return !(path[1] == '?' || IsDirectorySeparator(path[1]));
+ }
+
+ // The only way to specify a fixed path that doesn't begin with two slashes
+ // is the drive, colon, slash format- i.e. C:\
+ return !((path.Length >= 3)
+ && (path[1] == Path.VolumeSeparatorChar)
+ && IsDirectorySeparator(path[2])
+ // To match old behavior we'll check the drive character for validity as the path is technically
+ // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
+ && IsValidDriveChar(path[0]));
+ }
+
+ /// <summary>
+ /// Returns true if the path specified is relative to the current drive or working directory.
+ /// Returns false if the path is fixed to a specific drive or UNC path. This method does no
+ /// validation of the path (URIs will be returned as relative as a result).
+ /// </summary>
+ /// <remarks>
+ /// Handles paths that use the alternate directory separator. It is a frequent mistake to
+ /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
+ /// "C:a" is drive relative- meaning that it will be resolved against the current directory
+ /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
+ /// will not be used to modify the path).
+ /// </remarks>
+ internal static bool IsPartiallyQualified(StringBuffer path)
+ {
+ if (path.Length < 2)
+ {
+ // It isn't fixed, it must be relative. There is no way to specify a fixed
+ // path with one character (or less).
+ return true;
+ }
+
+ if (IsDirectorySeparator(path[0]))
+ {
+ // There is no valid way to specify a relative path with two initial slashes or
+ // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
+ return !(path[1] == '?' || IsDirectorySeparator(path[1]));
+ }
+
+ // The only way to specify a fixed path that doesn't begin with two slashes
+ // is the drive, colon, slash format- i.e. C:\
+ return !((path.Length >= 3)
+ && (path[1] == Path.VolumeSeparatorChar)
+ && IsDirectorySeparator(path[2])
+ // To match old behavior we'll check the drive character for validity as the path is technically
+ // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
+ && IsValidDriveChar(path[0]));
+ }
+
+ /// <summary>
+ /// Returns the characters to skip at the start of the path if it starts with space(s) and a drive or directory separator.
+ /// (examples are " C:", " \")
+ /// This is a legacy behavior of Path.GetFullPath().
+ /// </summary>
+ /// <remarks>
+ /// Note that this conflicts with IsPathRooted() which doesn't (and never did) such a skip.
+ /// </remarks>
+ internal static int PathStartSkip(string path)
+ {
+ int startIndex = 0;
+ while (startIndex < path.Length && path[startIndex] == ' ') startIndex++;
+
+ if (startIndex > 0 && (startIndex < path.Length && IsDirectorySeparator(path[startIndex]))
+ || (startIndex + 1 < path.Length && path[startIndex + 1] == Path.VolumeSeparatorChar && IsValidDriveChar(path[startIndex])))
+ {
+ // Go ahead and skip spaces as we're either " C:" or " \"
+ return startIndex;
+ }
+
+ return 0;
+ }
+
+ /// <summary>
+ /// True if the given character is a directory separator.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsDirectorySeparator(char c)
+ {
+ return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
+ }
+
+ /// <summary>
+ /// Normalize separators in the given path. Converts forward slashes into back slashes and compresses slash runs, keeping initial 2 if present.
+ /// Also trims initial whitespace in front of "rooted" paths (see PathStartSkip).
+ ///
+ /// This effectively replicates the behavior of the legacy NormalizePath when it was called with fullCheck=false and expandShortpaths=false.
+ /// The current NormalizePath gets directory separator normalization from Win32's GetFullPathName(), which will resolve relative paths and as
+ /// such can't be used here (and is overkill for our uses).
+ ///
+ /// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments.
+ /// </summary>
+ /// <remarks>
+ /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do
+ /// not need trimming of trailing whitespace here.
+ ///
+ /// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization.
+ ///
+ /// For legacy desktop behavior with ExpandShortPaths:
+ /// - It has no impact on GetPathRoot() so doesn't need consideration.
+ /// - It could impact GetDirectoryName(), but only if the path isn't relative (C:\ or \\Server\Share).
+ ///
+ /// In the case of GetDirectoryName() the ExpandShortPaths behavior was undocumented and provided inconsistent results if the path was
+ /// fixed/relative. For example: "C:\PROGRA~1\A.TXT" would return "C:\Program Files" while ".\PROGRA~1\A.TXT" would return ".\PROGRA~1". If you
+ /// ultimately call GetFullPath() this doesn't matter, but if you don't or have any intermediate string handling could easily be tripped up by
+ /// this undocumented behavior.
+ /// </remarks>
+ internal static string NormalizeDirectorySeparators(string path)
+ {
+ if (string.IsNullOrEmpty(path)) return path;
+
+ char current;
+ int start = PathStartSkip(path);
+
+ if (start == 0)
+ {
+ // Make a pass to see if we need to normalize so we can potentially skip allocating
+ bool normalized = true;
+
+ for (int i = 0; i < path.Length; i++)
+ {
+ current = path[i];
+ if (IsDirectorySeparator(current)
+ && (current != Path.DirectorySeparatorChar
+ // Check for sequential separators past the first position (we need to keep initial two for UNC/extended)
+ || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))))
+ {
+ normalized = false;
+ break;
+ }
+ }
+
+ if (normalized) return path;
+ }
+
+ StringBuilder builder = StringBuilderCache.Acquire(path.Length);
+
+ if (IsDirectorySeparator(path[start]))
+ {
+ start++;
+ builder.Append(Path.DirectorySeparatorChar);
+ }
+
+ for (int i = start; i < path.Length; i++)
+ {
+ current = path[i];
+
+ // If we have a separator
+ if (IsDirectorySeparator(current))
+ {
+ // If the next is a separator, skip adding this
+ if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))
+ {
+ continue;
+ }
+
+ // Ensure it is the primary separator
+ current = Path.DirectorySeparatorChar;
+ }
+
+ builder.Append(current);
+ }
+
+ return StringBuilderCache.GetStringAndRelease(builder);
+ }
+ }
+}
\ No newline at end of file
--- /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.Runtime.CompilerServices;
+
+namespace System.Runtime.InteropServices
+{
+ /// <summary>
+ /// Wrapper for access to the native heap. Dispose to free the memory. Try to use with using statements.
+ /// Does not allocate zero size buffers, and will free the existing native buffer if capacity is dropped to zero.
+ ///
+#if !FEATURE_CORECLR
+ /// NativeBuffer utilizes a cache of heap buffers.
+#endif
+ /// </summary>
+ /// <remarks>
+ /// Suggested use through P/Invoke: define DllImport arguments that take a byte buffer as SafeHandle.
+ ///
+ /// Using SafeHandle will ensure that the buffer will not get collected during a P/Invoke.
+ /// (Notably AddRef and ReleaseRef will be called by the interop layer.)
+ ///
+ /// This class is not threadsafe, changing the capacity or disposing on multiple threads risks duplicate heap
+ /// handles or worse.
+ /// </remarks>
+ internal class NativeBuffer : IDisposable
+ {
+#if !FEATURE_CORECLR
+ // The need for caching the heap handles isn't as great in CoreCLR as most current usages of this class'
+ // consumers are wrapped by CoreFx. (As opposed to NetFX 4.6 where there is no wrapping)
+ private readonly static SafeHeapHandleCache s_handleCache;
+#endif
+ [System.Security.SecurityCritical]
+ private readonly static SafeHandle s_emptyHandle;
+ [System.Security.SecurityCritical]
+ private SafeHeapHandle _handle;
+ private ulong _capacity;
+
+ [System.Security.SecuritySafeCritical]
+ static NativeBuffer()
+ {
+ s_emptyHandle = new EmptySafeHandle();
+#if !FEATURE_CORECLR
+ s_handleCache = new SafeHeapHandleCache();
+#endif
+ }
+
+ /// <summary>
+ /// Create a buffer with at least the specified initial capacity in bytes.
+ /// </summary>
+ public NativeBuffer(ulong initialMinCapacity = 0)
+ {
+ EnsureByteCapacity(initialMinCapacity);
+ }
+
+ protected unsafe void* VoidPointer
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [System.Security.SecurityCritical]
+ get
+ {
+ return _handle == null ? null : _handle.DangerousGetHandle().ToPointer();
+ }
+ }
+
+ protected unsafe byte* BytePointer
+ {
+ [System.Security.SecurityCritical]
+ get
+ {
+ return (byte*)VoidPointer;
+ }
+ }
+
+ /// <summary>
+ /// Get the handle for the buffer.
+ /// </summary>
+ [System.Security.SecuritySafeCritical]
+ public SafeHandle GetHandle()
+ {
+ // Marshalling code will throw on null for SafeHandle
+ return _handle ?? s_emptyHandle;
+ }
+
+ /// <summary>
+ /// The capacity of the buffer in bytes.
+ /// </summary>
+ public ulong ByteCapacity
+ {
+ get { return _capacity; }
+ }
+
+ /// <summary>
+ /// Ensure capacity in bytes is at least the given minimum.
+ /// </summary>
+ /// <exception cref="OutOfMemoryException">Thrown if unable to allocate memory when setting.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">Thrown if attempting to set <paramref name="nameof(minCapacity)"/> to a value that is larger than the maximum addressable memory.</exception>
+ [System.Security.SecuritySafeCritical]
+ public void EnsureByteCapacity(ulong minCapacity)
+ {
+ if (_capacity < minCapacity)
+ {
+ Resize(minCapacity);
+ _capacity = minCapacity;
+ }
+ }
+
+ public unsafe byte this[ulong index]
+ {
+ [System.Security.SecuritySafeCritical]
+ get
+ {
+ if (index >= _capacity) throw new ArgumentOutOfRangeException();
+ return BytePointer[index];
+ }
+ [System.Security.SecuritySafeCritical]
+ set
+ {
+ if (index >= _capacity) throw new ArgumentOutOfRangeException();
+ BytePointer[index] = value;
+ }
+ }
+
+ [System.Security.SecuritySafeCritical]
+ private unsafe void Resize(ulong byteLength)
+ {
+ if (byteLength == 0)
+ {
+ ReleaseHandle();
+ return;
+ }
+
+ if (_handle == null)
+ {
+#if FEATURE_CORECLR
+ _handle = new SafeHeapHandle(byteLength);
+#else
+ _handle = s_handleCache.Acquire(byteLength);
+#endif
+ }
+ else
+ {
+ _handle.Resize(byteLength);
+ }
+ }
+
+ [System.Security.SecuritySafeCritical]
+ private void ReleaseHandle()
+ {
+ if (_handle != null)
+ {
+#if !FEATURE_CORECLR
+ s_handleCache.Release(_handle);
+#endif
+ _capacity = 0;
+ _handle = null;
+ }
+ }
+
+ /// <summary>
+ /// Release the backing buffer
+ /// </summary>
+ [System.Security.SecuritySafeCritical]
+ public virtual void Free()
+ {
+ ReleaseHandle();
+ }
+
+ [System.Security.SecuritySafeCritical]
+ public void Dispose()
+ {
+ Free();
+ }
+
+ [System.Security.SecurityCritical]
+ private sealed class EmptySafeHandle : SafeHandle
+ {
+ public EmptySafeHandle() : base(IntPtr.Zero, true) { }
+
+ public override bool IsInvalid
+ {
+ [System.Security.SecurityCritical]
+ get
+ { return true; }
+ }
+
+ [System.Security.SecurityCritical]
+ protected override bool ReleaseHandle()
+ {
+ return true;
+ }
+ }
+ }
+}
\ No newline at end of file
--- /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.
+
+namespace System.Runtime.InteropServices
+{
+ /// <summary>
+ /// Handle for heap memory that allows tracking of capacity and reallocating.
+ /// </summary>
+ [System.Security.SecurityCritical]
+ internal sealed class SafeHeapHandle : SafeBuffer
+ {
+ /// <summary>
+ /// Allocate a buffer of the given size if requested.
+ /// </summary>
+ /// <param name="byteLength">Required size in bytes. Must be less than UInt32.MaxValue for 32 bit or UInt64.MaxValue for 64 bit.</param>
+ /// <exception cref="OutOfMemoryException">Thrown if the requested memory size cannot be allocated.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">Thrown if size is greater than the maximum memory size.</exception>
+ public SafeHeapHandle(ulong byteLength) : base(ownsHandle: true)
+ {
+ Resize(byteLength);
+ }
+
+ public override bool IsInvalid
+ {
+ [System.Security.SecurityCritical]
+ get
+ { return handle == IntPtr.Zero; }
+ }
+
+ /// <summary>
+ /// Resize the buffer to the given size if requested.
+ /// </summary>
+ /// <param name="byteLength">Required size in bytes. Must be less than UInt32.MaxValue for 32 bit or UInt64.MaxValue for 64 bit.</param>
+ /// <exception cref="OutOfMemoryException">Thrown if the requested memory size cannot be allocated.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">Thrown if size is greater than the maximum memory size.</exception>
+ public void Resize(ulong byteLength)
+ {
+ if (IsClosed) throw new ObjectDisposedException("SafeHeapHandle");
+
+ ulong originalLength = 0;
+ if (handle == IntPtr.Zero)
+ {
+ handle = Marshal.AllocHGlobal((IntPtr)byteLength);
+ }
+ else
+ {
+ originalLength = ByteLength;
+
+ // This may or may not be the same handle, may realloc in place. If the
+ // handle changes Windows will deal with the old handle, trying to free it will
+ // cause an error.
+ handle = Marshal.ReAllocHGlobal(pv: handle, cb: (IntPtr)byteLength);
+ }
+
+ if (handle == IntPtr.Zero)
+ {
+ // Only real plausible answer
+ throw new OutOfMemoryException();
+ }
+
+ if (byteLength > originalLength)
+ {
+ // Add pressure
+ ulong addedBytes = byteLength - originalLength;
+ if (addedBytes > long.MaxValue)
+ {
+ GC.AddMemoryPressure(long.MaxValue);
+ GC.AddMemoryPressure((long)(addedBytes - long.MaxValue));
+ }
+ else
+ {
+ GC.AddMemoryPressure((long)addedBytes);
+ }
+ }
+ else
+ {
+ // Shrank or did nothing, release pressure if needed
+ RemoveMemoryPressure(originalLength - byteLength);
+ }
+
+ Initialize(byteLength);
+ }
+
+ private void RemoveMemoryPressure(ulong removedBytes)
+ {
+ if (removedBytes == 0) return;
+
+ if (removedBytes > long.MaxValue)
+ {
+ GC.RemoveMemoryPressure(long.MaxValue);
+ GC.RemoveMemoryPressure((long)(removedBytes - long.MaxValue));
+ }
+ else
+ {
+ GC.RemoveMemoryPressure((long)removedBytes);
+ }
+ }
+
+ [System.Security.SecurityCritical]
+ protected override bool ReleaseHandle()
+ {
+ IntPtr handle = this.handle;
+ this.handle = IntPtr.Zero;
+
+ if (handle != IntPtr.Zero)
+ {
+ RemoveMemoryPressure(ByteLength);
+ Marshal.FreeHGlobal(handle);
+ }
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
--- /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.
+
+namespace System.Runtime.InteropServices
+{
+ /// <summary>
+ /// Native buffer that deals in char size increments. Dispose to free memory. Allows buffers larger
+ /// than a maximum size string to enable working with very large string arrays. Always makes ordinal
+ /// comparisons.
+ ///
+ /// A more performant replacement for StringBuilder when performing native interop.
+ /// </summary>
+ /// <remarks>
+ /// Suggested use through P/Invoke: define DllImport arguments that take a character buffer as IntPtr.
+ /// NativeStringBuffer has an implicit conversion to IntPtr.
+ /// </remarks>
+ internal class StringBuffer : NativeBuffer
+ {
+ private uint _length;
+
+ /// <summary>
+ /// Instantiate the buffer with capacity for at least the specified number of characters. Capacity
+ /// includes the trailing null character.
+ /// </summary>
+ public StringBuffer(uint initialCapacity = 0)
+ : base(initialCapacity * (ulong)sizeof(char))
+ {
+ }
+
+ /// <summary>
+ /// Instantiate the buffer with a copy of the specified string.
+ /// </summary>
+ public StringBuffer(string initialContents)
+ : base(0)
+ {
+ // We don't pass the count of bytes to the base constructor, appending will
+ // initialize to the correct size for the specified initial contents.
+ if (initialContents != null)
+ {
+ Append(initialContents);
+ }
+ }
+
+ /// <summary>
+ /// Instantiate the buffer with a copy of the specified StringBuffer.
+ /// </summary>
+ public StringBuffer(StringBuffer initialContents)
+ : base(0)
+ {
+ // We don't pass the count of bytes to the base constructor, appending will
+ // initialize to the correct size for the specified initial contents.
+ if (initialContents != null)
+ {
+ Append(initialContents);
+ }
+ }
+
+ /// <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 unsafe char this[uint index]
+ {
+ [System.Security.SecuritySafeCritical]
+ get
+ {
+ if (index >= _length) throw new ArgumentOutOfRangeException("index");
+ return CharPointer[index];
+ }
+ [System.Security.SecuritySafeCritical]
+ set
+ {
+ if (index >= _length) throw new ArgumentOutOfRangeException("index");
+ CharPointer[index] = value;
+ }
+ }
+
+ /// <summary>
+ /// Character capacity of the buffer. Includes the count for the trailing null character.
+ /// </summary>
+ public uint CharCapacity
+ {
+ [System.Security.SecuritySafeCritical]
+ get
+ {
+ ulong byteCapacity = ByteCapacity;
+ ulong charCapacity = byteCapacity == 0 ? 0 : byteCapacity / sizeof(char);
+ return charCapacity > uint.MaxValue ? uint.MaxValue : (uint)charCapacity;
+ }
+ }
+
+ /// <summary>
+ /// Ensure capacity in characters is at least the given minimum. Capacity includes space for the trailing
+ /// null, which is not part of the Length.
+ /// </summary>
+ /// <exception cref="OutOfMemoryException">Thrown if unable to allocate memory when setting.</exception>
+ [System.Security.SecuritySafeCritical]
+ public void EnsureCharCapacity(uint minCapacity)
+ {
+ EnsureByteCapacity(minCapacity * (ulong)sizeof(char));
+ }
+
+ /// <summary>
+ /// The logical length of the buffer in characters. (Does not include the final null, which is auto appended.) 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 uint.MaxValue (as space is implicitly reservced for the trailing null).</exception>
+ public unsafe uint Length
+ {
+ get { return _length; }
+ [System.Security.SecuritySafeCritical]
+ set
+ {
+ if (value == uint.MaxValue) throw new ArgumentOutOfRangeException("Length");
+
+ // Null terminate
+ EnsureCharCapacity(value + 1);
+ CharPointer[value] = '\0';
+
+ _length = value;
+ }
+ }
+
+ /// <summary>
+ /// For use when the native api null terminates but doesn't return a length.
+ /// If no null is found, the length will not be changed.
+ /// </summary>
+ [System.Security.SecuritySafeCritical]
+ public unsafe void SetLengthToFirstNull()
+ {
+ char* buffer = CharPointer;
+ uint capacity = CharCapacity;
+ for (uint i = 0; i < capacity; i++)
+ {
+ if (buffer[i] == '\0')
+ {
+ _length = i;
+ break;
+ }
+ }
+ }
+
+ internal unsafe char* CharPointer
+ {
+ [System.Security.SecurityCritical]
+ get
+ {
+ return (char*)VoidPointer;
+ }
+ }
+
+ /// <summary>
+ /// True if the buffer contains the given character.
+ /// </summary>
+ [System.Security.SecurityCritical]
+ public unsafe bool Contains(char value)
+ {
+ char* start = CharPointer;
+ uint length = _length;
+
+ for (uint i = 0; i < length; i++)
+ {
+ if (*start++ == value) return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Returns true if the buffer starts with the given string.
+ /// </summary>
+ [System.Security.SecuritySafeCritical]
+ public bool StartsWith(string value)
+ {
+ if (value == null) throw new ArgumentNullException("value");
+ if (_length < (uint)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>
+ [System.Security.SecuritySafeCritical]
+ public unsafe bool SubstringEquals(string value, uint startIndex = 0, int count = -1)
+ {
+ if (value == null) return false;
+ if (count < -1) throw new ArgumentOutOfRangeException("count");
+ if (startIndex > _length) throw new ArgumentOutOfRangeException("startIndex");
+
+ uint realCount = count == -1 ? _length - startIndex : (uint)count;
+ if (checked(startIndex + realCount) > _length) throw new ArgumentOutOfRangeException("count");
+
+ int length = value.Length;
+
+ // Check the substring length against the input length
+ if (realCount != (uint)length) return false;
+
+ fixed (char* valueStart = value)
+ {
+ char* bufferStart = CharPointer + startIndex;
+ for (int i = 0; i < length; i++)
+ {
+ // Note that indexing in this case generates faster code than trying to copy the pointer and increment it
+ if (*bufferStart++ != valueStart[i]) return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Append the given string.
+ /// </summary>
+ /// <param name="value">The string to append.</param>
+ /// <param name="startIndex">The index in the input string to start appending from.</param>
+ /// <param name="count">The count of characters to copy from the input string, or -1 for all remaining.</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>
+ [System.Security.SecuritySafeCritical]
+ public void Append(string value, int startIndex = 0, int count = -1)
+ {
+ CopyFrom(
+ bufferIndex: _length,
+ source: value,
+ sourceIndex: startIndex,
+ count: count);
+ }
+
+ /// <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>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is null.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// Thrown if <paramref name="startIndex"/> is outside the range of <paramref name="value"/> characters.
+ /// </exception>
+ public void Append(StringBuffer value, uint startIndex = 0)
+ {
+ if (value == null) throw new ArgumentNullException("value");
+ if (value.Length == 0) return;
+ value.CopyTo(
+ bufferIndex: startIndex,
+ destination: 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(StringBuffer value, uint startIndex, uint count)
+ {
+ if (value == null) throw new ArgumentNullException("value");
+ if (count == 0) return;
+ value.CopyTo(
+ bufferIndex: startIndex,
+ destination: 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>
+ [System.Security.SecuritySafeCritical]
+ public unsafe void CopyTo(uint bufferIndex, StringBuffer destination, uint destinationIndex, uint count)
+ {
+ if (destination == null) throw new ArgumentNullException("destination");
+ if (destinationIndex > destination._length) throw new ArgumentOutOfRangeException("destinationIndex");
+ if (bufferIndex >= _length) throw new ArgumentOutOfRangeException("bufferIndex");
+ if (_length < checked(bufferIndex + count)) throw new ArgumentOutOfRangeException("count");
+
+ if (count == 0) return;
+ uint lastIndex = checked(destinationIndex + count);
+ if (destination._length < lastIndex) destination.Length = lastIndex;
+
+ Buffer.MemoryCopy(
+ source: CharPointer + bufferIndex,
+ destination: destination.CharPointer + destinationIndex,
+ destinationSizeInBytes: checked((long)(destination.ByteCapacity - (destinationIndex * sizeof(char)))),
+ sourceBytesToCopy: checked((long)count * sizeof(char)));
+ }
+
+ /// <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>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// Thrown if <paramref name="bufferIndex"/> or <paramref name="sourceIndex"/> or <paramref name="count"/> are outside the range
+ /// of <paramref name="value"/> characters.
+ /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
+ [System.Security.SecuritySafeCritical]
+ public unsafe void CopyFrom(uint bufferIndex, string source, int sourceIndex = 0, int count = -1)
+ {
+ if (source == null) throw new ArgumentNullException("source");
+ if (bufferIndex > _length) throw new ArgumentOutOfRangeException("bufferIndex");
+ if (sourceIndex < 0 || sourceIndex >= source.Length) throw new ArgumentOutOfRangeException("sourceIndex");
+ if (count == -1) count = source.Length - sourceIndex;
+ if (count < 0 || source.Length - count < sourceIndex) throw new ArgumentOutOfRangeException("count");
+
+ if (count == 0) return;
+ uint lastIndex = bufferIndex + (uint)count;
+ if (_length < lastIndex) Length = lastIndex;
+
+ fixed (char* content = source)
+ {
+ Buffer.MemoryCopy(
+ source: content + sourceIndex,
+ destination: CharPointer + bufferIndex,
+ destinationSizeInBytes: checked((long)(ByteCapacity - (bufferIndex * sizeof(char)))),
+ sourceBytesToCopy: (long)count * sizeof(char));
+ }
+ }
+
+ /// <summary>
+ /// Trim the specified values from the end of the buffer. If nothing is specified, nothing is trimmed.
+ /// </summary>
+ [System.Security.SecuritySafeCritical]
+ public unsafe void TrimEnd(char[] values)
+ {
+ if (values == null || values.Length == 0 || _length == 0) return;
+
+ char* end = CharPointer + _length - 1;
+
+ while (_length > 0 && Array.IndexOf(values, *end) >= 0)
+ {
+ Length = _length - 1;
+ end--;
+ }
+ }
+
+ /// <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>
+ [System.Security.SecuritySafeCritical]
+ public unsafe override string ToString()
+ {
+ if (_length == 0) return string.Empty;
+ if (_length > int.MaxValue) throw new InvalidOperationException();
+ return new string(CharPointer, startIndex: 0, length: (int)_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>
+ [System.Security.SecuritySafeCritical]
+ public unsafe string Substring(uint startIndex, int count = -1)
+ {
+ if (startIndex > (_length == 0 ? 0 : _length - 1)) throw new ArgumentOutOfRangeException("startIndex");
+ if (count < -1) throw new ArgumentOutOfRangeException("count");
+
+ uint realCount = count == -1 ? _length - startIndex : (uint)count;
+ if (realCount > int.MaxValue || checked(startIndex + realCount) > _length) throw new ArgumentOutOfRangeException("count");
+ if (realCount == 0) return string.Empty;
+
+ // 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(value: CharPointer + startIndex, startIndex: 0, length: (int)realCount);
+ }
+
+ [System.Security.SecuritySafeCritical]
+ public override void Free()
+ {
+ base.Free();
+ _length = 0;
+ }
+ }
+}
\ No newline at end of file