From: Jeremy Kuhne Date: Fri, 6 May 2016 00:20:27 +0000 (-0700) Subject: This change ports the support files from NetFxDev1. It doesn't change behavior yet... X-Git-Tag: accepted/tizen/base/20180629.140029~4784^2 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=2ae90a015933dc85192b73c2bf8361cf59cef035;p=platform%2Fupstream%2Fcoreclr.git This change ports the support files from NetFxDev1. It doesn't change behavior yet. Adding a feature for the app context switch for path compat as we don't intend to use it in core. [tfs-changeset: 1602284] --- diff --git a/src/mscorlib/mscorlib.shared.sources.props b/src/mscorlib/mscorlib.shared.sources.props index 7fef3f5e..2a073c1 100644 --- a/src/mscorlib/mscorlib.shared.sources.props +++ b/src/mscorlib/mscorlib.shared.sources.props @@ -123,6 +123,7 @@ + @@ -134,6 +135,8 @@ + + @@ -781,6 +784,8 @@ + + diff --git a/src/mscorlib/src/Microsoft/Win32/Win32Native.cs b/src/mscorlib/src/Microsoft/Win32/Win32Native.cs index 84fdf8a..34fbf0d 100644 --- a/src/mscorlib/src/Microsoft/Win32/Win32Native.cs +++ b/src/mscorlib/src/Microsoft/Win32/Win32Native.cs @@ -897,9 +897,18 @@ namespace Microsoft.Win32 { [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 diff --git a/src/mscorlib/src/System/AppContext/AppContextDefaultValues.Defaults.cs b/src/mscorlib/src/System/AppContext/AppContextDefaultValues.Defaults.cs index 50b275e..c80913e 100644 --- a/src/mscorlib/src/System/AppContext/AppContextDefaultValues.Defaults.cs +++ b/src/mscorlib/src/System/AppContext/AppContextDefaultValues.Defaults.cs @@ -12,8 +12,11 @@ namespace System 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 @@ -40,6 +43,13 @@ namespace System 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": diff --git a/src/mscorlib/src/System/AppDomain.cs b/src/mscorlib/src/System/AppDomain.cs index 855564b..d78418d 100644 --- a/src/mscorlib/src/System/AppDomain.cs +++ b/src/mscorlib/src/System/AppDomain.cs @@ -3628,7 +3628,7 @@ namespace System { 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 @@ -3636,10 +3636,10 @@ namespace System { { 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") { @@ -3655,8 +3655,8 @@ namespace System { 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") { @@ -3710,7 +3710,7 @@ namespace System { 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); } @@ -3719,12 +3719,12 @@ namespace System { { 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 @@ -3813,6 +3813,15 @@ namespace System { #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 @@ -3924,9 +3933,9 @@ namespace System { #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 diff --git a/src/mscorlib/src/System/AppDomainSetup.cs b/src/mscorlib/src/System/AppDomainSetup.cs index 9deae2a..ad99e63 100644 --- a/src/mscorlib/src/System/AppDomainSetup.cs +++ b/src/mscorlib/src/System/AppDomainSetup.cs @@ -471,7 +471,9 @@ namespace System { 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) @@ -516,7 +518,7 @@ namespace System { path = StringBuilderCache.GetStringAndRelease(result); } else - path = Path.GetFullPathInternal(path); + path = AppDomain.NormalizePath(path, fullCheck: true); } return path; diff --git a/src/mscorlib/src/System/IO/LongPathHelper.cs b/src/mscorlib/src/System/IO/LongPathHelper.cs new file mode 100644 index 0000000..5adfb07 --- /dev/null +++ b/src/mscorlib/src/System/IO/LongPathHelper.cs @@ -0,0 +1,367 @@ +// 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 +{ + /// + /// Wrapper to help with path normalization. + /// + 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; + + /// + /// Normalize the given path. + /// + /// + /// 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) + /// + /// Path to normalize + /// True to check for invalid characters + /// Attempt to expand short paths if true + /// Thrown if the path is an illegal UNC (does not contain a full server/share) or contains illegal characters. + /// Thrown if the path or a path segment exceeds the filesystem limits. + /// Thrown if Windows returns ERROR_FILE_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error) + /// Thrown if Windows returns ERROR_PATH_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error) + /// Thrown if Windows returns ERROR_ACCESS_DENIED. (See Win32Marshal.GetExceptionForWin32Error) + /// Thrown if Windows returns an error that doesn't map to the above. (See Win32Marshal.GetExceptionForWin32Error) + /// Normalized path + [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 diff --git a/src/mscorlib/src/System/IO/PathInternal.cs b/src/mscorlib/src/System/IO/PathInternal.cs new file mode 100644 index 0000000..ac1afb8 --- /dev/null +++ b/src/mscorlib/src/System/IO/PathInternal.cs @@ -0,0 +1,661 @@ +// 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 +{ + /// Contains internal path helpers that are shared between many projects. + 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 + }; + + /// + /// Validates volume separator only occurs as C: or \\?\C:. This logic is meant to filter out Alternate Data Streams. + /// + /// True if the path has an invalid volume separator. + 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; + } + + /// + /// Returns true if the given StringBuilder starts with the given value. + /// + /// The string to compare against the start of the StringBuilder. + 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; + } + + /// + /// Returns true if the given character is a valid drive letter + /// + internal static bool IsValidDriveChar(char value) + { + return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z')); + } + + /// + /// Returns true if the path is too long + /// + 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; + } + + /// + /// Return true if any path segments are too long + /// + 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; + } + + /// + /// Returns true if the directory is too long + /// + 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); + } + + /// + /// Adds the extended path prefix (\\?\) if not relative or already a device path. + /// + 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; + } + + /// + /// Removes the extended path prefix (\\?\) if present. + /// + 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); + } + + /// + /// Removes the extended path prefix (\\?\) if present. + /// + 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); + } + + /// + /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") + /// + 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]) + ); + } + + /// + /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") + /// + 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]) + ); + } + + /// + /// 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. + /// + 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] == '\\'; + } + + /// + /// 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. + /// + 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] == '\\'; + } + + /// + /// 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. + /// + 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] == '\\'; + } + + /// + /// Returns true if the path uses the extended UNC syntax (\\?\UNC\ or \??\UNC\) + /// + 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] == '\\'; + } + + /// + /// Returns true if the path uses the extended UNC syntax (\\?\UNC\ or \??\UNC\) + /// + 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] == '\\'; + } + + /// + /// 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). + /// + 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); + } + + /// + /// 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. + /// + internal static bool AnyPathHasIllegalCharacters(string path, bool checkAdditional = false) + { + return path.IndexOfAny(InvalidPathChars) >= 0 || (checkAdditional && AnyPathHasWildCardCharacters(path)); + } + + /// + /// Check for ? and *. + /// + 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); + } + + /// + /// 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. + /// + 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; + } + + /// + /// Gets the length of the root of the path (drive, share, etc.). + /// + [System.Security.SecuritySafeCritical] + internal unsafe static int GetRootLength(string path) + { + fixed (char* value = path) + { + return (int)GetRootLength(value, (ulong)path.Length); + } + } + + /// + /// Gets the length of the root of the path (drive, share, etc.). + /// + [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; + } + + /// + /// 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). + /// + /// + /// 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). + /// + 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])); + } + + /// + /// 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). + /// + /// + /// 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). + /// + 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])); + } + + /// + /// 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(). + /// + /// + /// Note that this conflicts with IsPathRooted() which doesn't (and never did) such a skip. + /// + 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; + } + + /// + /// True if the given character is a directory separator. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirectorySeparator(char c) + { + return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + } + + /// + /// 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. + /// + /// + /// 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. + /// + 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 diff --git a/src/mscorlib/src/System/Runtime/InteropServices/NativeBuffer.cs b/src/mscorlib/src/System/Runtime/InteropServices/NativeBuffer.cs new file mode 100644 index 0000000..ebe5b7b --- /dev/null +++ b/src/mscorlib/src/System/Runtime/InteropServices/NativeBuffer.cs @@ -0,0 +1,194 @@ +// 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 +{ + /// + /// 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 + /// + /// + /// 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. + /// + 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 + } + + /// + /// Create a buffer with at least the specified initial capacity in bytes. + /// + 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; + } + } + + /// + /// Get the handle for the buffer. + /// + [System.Security.SecuritySafeCritical] + public SafeHandle GetHandle() + { + // Marshalling code will throw on null for SafeHandle + return _handle ?? s_emptyHandle; + } + + /// + /// The capacity of the buffer in bytes. + /// + public ulong ByteCapacity + { + get { return _capacity; } + } + + /// + /// Ensure capacity in bytes is at least the given minimum. + /// + /// Thrown if unable to allocate memory when setting. + /// Thrown if attempting to set to a value that is larger than the maximum addressable memory. + [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; + } + } + + /// + /// Release the backing buffer + /// + [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 diff --git a/src/mscorlib/src/System/Runtime/InteropServices/SafeHeapHandle.cs b/src/mscorlib/src/System/Runtime/InteropServices/SafeHeapHandle.cs new file mode 100644 index 0000000..b0c422d --- /dev/null +++ b/src/mscorlib/src/System/Runtime/InteropServices/SafeHeapHandle.cs @@ -0,0 +1,115 @@ +// 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 +{ + /// + /// Handle for heap memory that allows tracking of capacity and reallocating. + /// + [System.Security.SecurityCritical] + internal sealed class SafeHeapHandle : SafeBuffer + { + /// + /// Allocate a buffer of the given size if requested. + /// + /// Required size in bytes. Must be less than UInt32.MaxValue for 32 bit or UInt64.MaxValue for 64 bit. + /// Thrown if the requested memory size cannot be allocated. + /// Thrown if size is greater than the maximum memory size. + public SafeHeapHandle(ulong byteLength) : base(ownsHandle: true) + { + Resize(byteLength); + } + + public override bool IsInvalid + { + [System.Security.SecurityCritical] + get + { return handle == IntPtr.Zero; } + } + + /// + /// Resize the buffer to the given size if requested. + /// + /// Required size in bytes. Must be less than UInt32.MaxValue for 32 bit or UInt64.MaxValue for 64 bit. + /// Thrown if the requested memory size cannot be allocated. + /// Thrown if size is greater than the maximum memory size. + 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 diff --git a/src/mscorlib/src/System/Runtime/InteropServices/StringBuffer.cs b/src/mscorlib/src/System/Runtime/InteropServices/StringBuffer.cs new file mode 100644 index 0000000..15b1b6a --- /dev/null +++ b/src/mscorlib/src/System/Runtime/InteropServices/StringBuffer.cs @@ -0,0 +1,402 @@ +// 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 +{ + /// + /// 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. + /// + /// + /// Suggested use through P/Invoke: define DllImport arguments that take a character buffer as IntPtr. + /// NativeStringBuffer has an implicit conversion to IntPtr. + /// + internal class StringBuffer : NativeBuffer + { + private uint _length; + + /// + /// Instantiate the buffer with capacity for at least the specified number of characters. Capacity + /// includes the trailing null character. + /// + public StringBuffer(uint initialCapacity = 0) + : base(initialCapacity * (ulong)sizeof(char)) + { + } + + /// + /// Instantiate the buffer with a copy of the specified string. + /// + 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); + } + } + + /// + /// Instantiate the buffer with a copy of the specified StringBuffer. + /// + 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); + } + } + + /// + /// Get/set the character at the given index. + /// + /// Thrown if attempting to index outside of the buffer length. + 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; + } + } + + /// + /// Character capacity of the buffer. Includes the count for the trailing null character. + /// + 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; + } + } + + /// + /// Ensure capacity in characters is at least the given minimum. Capacity includes space for the trailing + /// null, which is not part of the Length. + /// + /// Thrown if unable to allocate memory when setting. + [System.Security.SecuritySafeCritical] + public void EnsureCharCapacity(uint minCapacity) + { + EnsureByteCapacity(minCapacity * (ulong)sizeof(char)); + } + + /// + /// 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. + /// + /// Thrown if unable to allocate memory when setting. + /// Thrown if the set size in bytes is uint.MaxValue (as space is implicitly reservced for the trailing null). + 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; + } + } + + /// + /// 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. + /// + [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; + } + } + + /// + /// True if the buffer contains the given character. + /// + [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; + } + + /// + /// Returns true if the buffer starts with the given string. + /// + [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); + } + + /// + /// Returns true if the specified StringBuffer substring equals the given value. + /// + /// The value to compare against the specified substring. + /// Start index of the sub string. + /// Length of the substring, or -1 to check all remaining. + /// + /// Thrown if or are outside the range + /// of the buffer's length. + /// + [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; + } + + /// + /// Append the given string. + /// + /// The string to append. + /// The index in the input string to start appending from. + /// The count of characters to copy from the input string, or -1 for all remaining. + /// Thrown if is null. + /// + /// Thrown if or are outside the range + /// of characters. + /// + [System.Security.SecuritySafeCritical] + public void Append(string value, int startIndex = 0, int count = -1) + { + CopyFrom( + bufferIndex: _length, + source: value, + sourceIndex: startIndex, + count: count); + } + + /// + /// Append the given buffer. + /// + /// The buffer to append. + /// The index in the input buffer to start appending from. + /// Thrown if is null. + /// + /// Thrown if is outside the range of characters. + /// + 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); + } + + /// + /// Append the given buffer. + /// + /// The buffer to append. + /// The index in the input buffer to start appending from. + /// The count of characters to copy from the buffer string. + /// Thrown if is null. + /// + /// Thrown if or are outside the range + /// of characters. + /// + 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); + } + + /// + /// Copy contents to the specified buffer. Destination index must be within current destination length. + /// Will grow the destination buffer if needed. + /// + /// + /// Thrown if or or are outside the range + /// of characters. + /// + /// Thrown if is null. + [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))); + } + + /// + /// 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. + /// + /// + /// Thrown if or or are outside the range + /// of characters. + /// + /// Thrown if is null. + [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)); + } + } + + /// + /// Trim the specified values from the end of the buffer. If nothing is specified, nothing is trimmed. + /// + [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--; + } + } + + /// + /// String representation of the entire buffer. If the buffer is larger than the maximum size string (int.MaxValue) this will throw. + /// + /// Thrown if the buffer is too big to fit into a string. + [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); + } + + /// + /// Get the given substring in the buffer. + /// + /// Count of characters to take, or remaining characters from if -1. + /// + /// Thrown if or are outside the range of the buffer's length + /// or count is greater than the maximum string size (int.MaxValue). + /// + [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