From 720732501f1d844d1947a20ea207e5d7f760159a Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Mon, 9 May 2016 19:25:32 -0700 Subject: [PATCH] Allow long paths Allows long paths in mscorlib. Fixes wraps for FEATURE_PATHCOMPAT and enables for desktop builds. Add feature FEATURE_IMPLICIT_LONGPATH for implicit long path support (adding \\?\). Implicit support added to Path.GetFullPath, which allows execution over MAX_PATH. Without this change corerun would fail here: HOSTLOG: AppDomainCompatSwitch=UseLatestBehaviorWhenTFMNotSpecified HOSTLOG: APP_LOCAL_WINMETADATA= HOSTLOG: Failed call to CreateAppDomainWithManager. ERRORCODE: 0x800700ce HOSTLOG: Execution failed --- clr.coreclr.props | 4 + clr.defines.targets | 4 +- clr.desktop.props | 1 + src/mscorlib/mscorlib.shared.sources.props | 2 +- src/mscorlib/src/Microsoft/Win32/Win32Native.cs | 3 + .../src/System/AppContext/AppContextSwitches.cs | 30 ++ src/mscorlib/src/System/AppDomain.cs | 16 +- src/mscorlib/src/System/AppDomainSetup.cs | 10 +- src/mscorlib/src/System/IO/Directory.cs | 99 +++-- src/mscorlib/src/System/IO/FileSecurityState.cs | 13 +- src/mscorlib/src/System/IO/LongPathHelper.cs | 170 ++++++++- src/mscorlib/src/System/IO/Path.cs | 419 ++++++++++++++------- src/mscorlib/src/System/IO/PathInternal.cs | 150 +++++++- .../Security/Permissions/FileIOPermission.cs | 181 ++++++++- .../System/Security/Util/StringExpressionSet.cs | 50 +-- src/mscorlib/src/System/Security/Util/URLString.cs | 42 ++- 16 files changed, 956 insertions(+), 238 deletions(-) diff --git a/clr.coreclr.props b/clr.coreclr.props index ce27318..8ed490a 100644 --- a/clr.coreclr.props +++ b/clr.coreclr.props @@ -101,4 +101,8 @@ true + + + true + diff --git a/clr.defines.targets b/clr.defines.targets index 2d8d0ce..4c668e5 100644 --- a/clr.defines.targets +++ b/clr.defines.targets @@ -8,7 +8,7 @@ $(CDefines);FEATURE_APPDOMAINMANAGER_INITOPTIONS $(CDefines);FEATURE_APPX $(CDefines);FEATURE_APPX_BINDER - $(CDefines);FEATURE_APTCA + $(CDefines);FEATURE_APTCA $(CDefines);FEATURE_ARRAYSTUB_AS_IL $(CDefines);FEATURE_STUBS_AS_IL $(CDefines);FEATURE_BCL_FORMATTING @@ -162,6 +162,7 @@ $(DefineConstants);FEATURE_IDENTITY_REFERENCE $(DefineConstants);FEATURE_IMPERSONATION $(DefineConstants);FEATURE_INCLUDE_ALL_INTERFACES + $(DefineConstants);FEATURE_IMPLICIT_LONGPATH $(DefineConstants);FEATURE_ISOLATED_STORAGE_QUOTA_ENFORCEMENT $(DefineConstants);FEATURE_ISOSTORE $(DefineConstants);FEATURE_ISOSTORE_LIGHT @@ -178,6 +179,7 @@ $(DefineConstants);FEATURE_NONGENERIC_COLLECTIONS $(DefineConstants);FEATURE_NORM_IDNA_ONLY $(DefineConstants);FEATURE_PAL + $(DefineConstants);FEATURE_PATHCOMPAT $(DefineConstants);FEATURE_EVENTSOURCE_XPLAT $(DefineConstants);FEATURE_PERFMON $(DefineConstants);FEATURE_PLS diff --git a/clr.desktop.props b/clr.desktop.props index 050249a..7c94bcd 100644 --- a/clr.desktop.props +++ b/clr.desktop.props @@ -67,6 +67,7 @@ true true true + true true true true diff --git a/src/mscorlib/mscorlib.shared.sources.props b/src/mscorlib/mscorlib.shared.sources.props index 2a073c1..d2b27fc 100644 --- a/src/mscorlib/mscorlib.shared.sources.props +++ b/src/mscorlib/mscorlib.shared.sources.props @@ -784,7 +784,7 @@ - + diff --git a/src/mscorlib/src/Microsoft/Win32/Win32Native.cs b/src/mscorlib/src/Microsoft/Win32/Win32Native.cs index 34fbf0d..ba6030e 100644 --- a/src/mscorlib/src/Microsoft/Win32/Win32Native.cs +++ b/src/mscorlib/src/Microsoft/Win32/Win32Native.cs @@ -1301,6 +1301,9 @@ namespace Microsoft.Win32 { int nBufferLength, [Out]StringBuilder lpBuffer); + [DllImport(KERNEL32, SetLastError = true, ExactSpelling = true)] + internal static extern uint GetCurrentDirectoryW(uint nBufferLength, SafeHandle lpBuffer); + [DllImport(KERNEL32, SetLastError=true, CharSet=CharSet.Auto, BestFitMapping=false)] internal static extern bool GetFileAttributesEx(String name, int fileInfoLevel, ref WIN32_FILE_ATTRIBUTE_DATA lpFileInformation); diff --git a/src/mscorlib/src/System/AppContext/AppContextSwitches.cs b/src/mscorlib/src/System/AppContext/AppContextSwitches.cs index 5fdd2bc..3a96ec2 100644 --- a/src/mscorlib/src/System/AppContext/AppContextSwitches.cs +++ b/src/mscorlib/src/System/AppContext/AppContextSwitches.cs @@ -39,6 +39,36 @@ namespace System } } +#if FEATURE_PATHCOMPAT + private static int _useLegacyPathHandling; + + /// + /// Use legacy path normalization logic and blocking of extended syntax. + /// + public static bool UseLegacyPathHandling + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return GetCachedSwitchValue(AppContextDefaultValues.SwitchUseLegacyPathHandling, ref _useLegacyPathHandling); + } + } + + private static int _blockLongPaths; + + /// + /// Throw PathTooLongException for paths greater than MAX_PATH or directories greater than 248 (as per CreateDirectory Win32 limitations) + /// + public static bool BlockLongPaths + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return GetCachedSwitchValue(AppContextDefaultValues.SwitchBlockLongPaths, ref _blockLongPaths); + } + } +#endif // FEATURE_PATHCOMPAT + // // Implementation details // diff --git a/src/mscorlib/src/System/AppDomain.cs b/src/mscorlib/src/System/AppDomain.cs index d78418d..c84a7aa 100644 --- a/src/mscorlib/src/System/AppDomain.cs +++ b/src/mscorlib/src/System/AppDomain.cs @@ -3816,11 +3816,19 @@ namespace System { [SecuritySafeCritical] internal static string NormalizePath(string path, bool fullCheck) { - return Path.NormalizePath( +#if FEATURE_PATHCOMPAT + // Appcontext switches can't currently be safely hit during AppDomain bringup + return Path.LegacyNormalizePath( path: path, fullCheck: fullCheck, maxPathLength: PathInternal.MaxShortPath, expandShortPaths: true); +#else + return Path.NormalizePath( + path: path, + fullCheck: fullCheck, + expandShortPaths: true); +#endif } #if FEATURE_APTCA @@ -3933,9 +3941,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 ad99e63..f1057da 100644 --- a/src/mscorlib/src/System/AppDomainSetup.cs +++ b/src/mscorlib/src/System/AppDomainSetup.cs @@ -340,8 +340,16 @@ namespace System { // If we add very long file name support ("\\?\") to the Path class then this is unnecesary, // but we do not plan on doing this for now. + + // Long path checks can be quirked, and as loading default quirks too early in the setup of an AppDomain is risky + // we'll avoid checking path lengths- we'll still fail at MAX_PATH later if we're !useAppBase when we call Path's + // NormalizePath. if (!useAppBase) - path = System.Security.Util.URLString.PreProcessForExtendedPathRemoval(path, false); + path = Security.Util.URLString.PreProcessForExtendedPathRemoval( + checkPathLength: false, + url: path, + isFileUrl: false); + int len = path.Length; if (len == 0) diff --git a/src/mscorlib/src/System/IO/Directory.cs b/src/mscorlib/src/System/IO/Directory.cs index 95f2f37..be74538 100644 --- a/src/mscorlib/src/System/IO/Directory.cs +++ b/src/mscorlib/src/System/IO/Directory.cs @@ -215,9 +215,17 @@ namespace System.IO { int count = stackDir.Count; - if (stackDir.Count != 0) + if (stackDir.Count != 0 +#if FEATURE_CAS_POLICY + // All demands in full trust domains are no-ops, so skip + // + // The full path went through validity checks by being passed through FileIOPermissions already. + // As a sub string of the full path can't fail the checks if the full path passes. + && !CodeAccessSecurityEngine.QuickCheckForAllDemands() +#endif + ) { - String [] securityList = new String[stackDir.Count]; + String[] securityList = new String[stackDir.Count]; stackDir.CopyTo(securityList, 0); for (int j = 0 ; j < securityList.Length; j++) securityList[j] += "\\."; // leaf will never have a slash at the end @@ -225,7 +233,7 @@ namespace System.IO { // Security check for all directories not present only. #if FEATURE_MACL AccessControlActions control = (dirSecurity == null) ? AccessControlActions.None : AccessControlActions.Change; - new FileIOPermission(FileIOPermissionAccess.Write, control, securityList, false, false ).Demand(); + FileIOPermission.QuickDemand(FileIOPermissionAccess.Write, control, securityList, false, false); #else #if FEATURE_CORECLR if (checkHost) @@ -237,7 +245,7 @@ namespace System.IO { } } #else - new FileIOPermission(FileIOPermissionAccess.Write, securityList, false, false ).Demand(); + FileIOPermission.QuickDemand(FileIOPermissionAccess.Write, securityList, false, false); #endif #endif //FEATURE_MACL } @@ -265,7 +273,7 @@ namespace System.IO { while (stackDir.Count > 0) { String name = stackDir[stackDir.Count - 1]; stackDir.RemoveAt(stackDir.Count - 1); - if (name.Length >= Path.MAX_DIRECTORY_PATH) + if (PathInternal.IsDirectoryTooLong(name)) throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); r = Win32Native.CreateDirectory(name, secAttrs); if (!r && (firstError == 0)) { @@ -293,7 +301,7 @@ namespace System.IO { state.EnsureState(); } #else - new FileIOPermission(FileIOPermissionAccess.PathDiscovery, GetDemandDir(name, true)).Demand(); + FileIOPermission.QuickDemand(FileIOPermissionAccess.PathDiscovery, GetDemandDir(name, true)); #endif // FEATURE_CORECLR errorString = name; } @@ -950,8 +958,32 @@ namespace System.IO { return InternalGetCurrentDirectory(false); } + [System.Security.SecuritySafeCritical] + private static string InternalGetCurrentDirectory(bool checkHost) + { + string currentDirectory = ( +#if FEATURE_PATHCOMPAT + AppContextSwitches.UseLegacyPathHandling ? LegacyGetCurrentDirectory() : +#endif + NewGetCurrentDirectory()); + + string demandPath = GetDemandDir(currentDirectory, true); + +#if FEATURE_CORECLR + if (checkHost) + { + FileSecurityState state = new FileSecurityState(FileSecurityStateAccess.PathDiscovery, String.Empty, demandPath); + state.EnsureState(); + } +#else + FileIOPermission.QuickDemand(FileIOPermissionAccess.PathDiscovery, demandPath, false, false); +#endif + return currentDirectory; + } + +#if FEATURE_PATHCOMPAT [System.Security.SecurityCritical] - private static String InternalGetCurrentDirectory(bool checkHost) + private static String LegacyGetCurrentDirectory() { StringBuilder sb = StringBuilderCache.Acquire(Path.MaxPath + 1); if (Win32Native.GetCurrentDirectory(sb.Capacity, sb) == 0) @@ -962,7 +994,7 @@ namespace System.IO { // this will return a short file name. if (currentDirectory.IndexOf('~') >= 0) { int r = Win32Native.GetLongPathName(currentDirectory, sb, sb.Capacity); - if (r == 0 || r >= Path.MaxPath) { + if (r == 0 || r >= Path.MaxPath) { int errorCode = Marshal.GetLastWin32Error(); if (r >= Path.MaxPath) errorCode = Win32Native.ERROR_FILENAME_EXCED_RANGE; @@ -977,19 +1009,36 @@ namespace System.IO { StringBuilderCache.Release(sb); String demandPath = GetDemandDir(currentDirectory, true); - -#if FEATURE_CORECLR - if (checkHost) - { - FileSecurityState state = new FileSecurityState(FileSecurityStateAccess.PathDiscovery, String.Empty, demandPath); - state.EnsureState(); - } -#else - new FileIOPermission( FileIOPermissionAccess.PathDiscovery, new String[] { demandPath }, false, false ).Demand(); -#endif return currentDirectory; } +#endif // FEATURE_PATHCOMPAT + + [System.Security.SecurityCritical] + private static string NewGetCurrentDirectory() + { + using (StringBuffer buffer = new StringBuffer(PathInternal.MaxShortPath)) + { + uint result = 0; + while ((result = Win32Native.GetCurrentDirectoryW(buffer.CharCapacity, buffer.GetHandle())) > buffer.CharCapacity) + { + // Reported size is greater than the buffer size. Increase the capacity. + // The size returned includes the null only if more space is needed (this case). + buffer.EnsureCharCapacity(result); + } + + if (result == 0) + __Error.WinIOError(); + buffer.Length = result; + +#if !PLATFORM_UNIX + if (buffer.Contains('~')) + return LongPathHelper.GetLongPathName(buffer); +#endif + + return buffer.ToString(); + } + } #if FEATURE_CORECLR [System.Security.SecurityCritical] // auto-generated @@ -997,7 +1046,7 @@ namespace System.IO { [System.Security.SecuritySafeCritical] #endif public static void SetCurrentDirectory(String path) - { + { if (path==null) throw new ArgumentNullException("value"); if (path.Length==0) @@ -1050,16 +1099,16 @@ namespace System.IO { String fullsourceDirName = Path.GetFullPathInternal(sourceDirName); String sourcePath = GetDemandDir(fullsourceDirName, false); - - if (sourcePath.Length >= Path.MAX_DIRECTORY_PATH) + + if (PathInternal.IsDirectoryTooLong(sourcePath)) throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); String fulldestDirName = Path.GetFullPathInternal(destDirName); String destPath = GetDemandDir(fulldestDirName, false); - if (destPath.Length >= Path.MAX_DIRECTORY_PATH) + if (PathInternal.IsDirectoryTooLong(destPath)) throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); - + #if FEATURE_CORECLR if (checkHost) { FileSecurityState sourceState = new FileSecurityState(FileSecurityStateAccess.Write | FileSecurityStateAccess.Read, sourceDirName, sourcePath); @@ -1071,14 +1120,14 @@ namespace System.IO { FileIOPermission.QuickDemand(FileIOPermissionAccess.Write | FileIOPermissionAccess.Read, sourcePath, false, false); FileIOPermission.QuickDemand(FileIOPermissionAccess.Write, destPath, false, false); #endif - + if (String.Compare(sourcePath, destPath, StringComparison.OrdinalIgnoreCase) == 0) throw new IOException(Environment.GetResourceString("IO.IO_SourceDestMustBeDifferent")); String sourceRoot = Path.GetPathRoot(sourcePath); String destinationRoot = Path.GetPathRoot(destPath); if (String.Compare(sourceRoot, destinationRoot, StringComparison.OrdinalIgnoreCase) != 0) - throw new IOException(Environment.GetResourceString("IO.IO_SourceDestMustHaveSameRoot")); + throw new IOException(Environment.GetResourceString("IO.IO_SourceDestMustHaveSameRoot")); if (!Win32Native.MoveFile(sourceDirName, destDirName)) { diff --git a/src/mscorlib/src/System/IO/FileSecurityState.cs b/src/mscorlib/src/System/IO/FileSecurityState.cs index 530acfa..249848a 100644 --- a/src/mscorlib/src/System/IO/FileSecurityState.cs +++ b/src/mscorlib/src/System/IO/FileSecurityState.cs @@ -122,16 +122,11 @@ namespace System.IO path = path.Trim(); #if !PLATFORM_UNIX - if (path.Length > 2 && path.IndexOf( ':', 2 ) != -1) - throw new NotSupportedException( Environment.GetResourceString( "Argument_PathFormatNotSupported" ) ); -#endif // !PLATFORM_UNIX - - System.IO.Path.CheckInvalidPathChars(path); + if (!PathInternal.IsDevice(path) && PathInternal.HasInvalidVolumeSeparator(path)) + throw new ArgumentException(Environment.GetResourceString("Argument_PathFormatNotSupported")); +#endif -#if !PLATFORM_UNIX - if (path.IndexOfAny( m_illegalCharacters ) != -1) - throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidPathChars" ) ); -#endif // !PLATFORM_UNIX + System.IO.Path.CheckInvalidPathChars(path, checkAdditional: true); } } } diff --git a/src/mscorlib/src/System/IO/LongPathHelper.cs b/src/mscorlib/src/System/IO/LongPathHelper.cs index 5adfb07..41cbb15 100644 --- a/src/mscorlib/src/System/IO/LongPathHelper.cs +++ b/src/mscorlib/src/System/IO/LongPathHelper.cs @@ -20,20 +20,6 @@ namespace System.IO 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; @@ -66,7 +52,7 @@ namespace System.IO GetFullPathName(path, fullPath); // Trim whitespace off the end of the string. Win32 normalization trims only U+0020. - fullPath.TrimEnd(s_trimEndChars); + fullPath.TrimEnd(Path.TrimEndChars); if (fullPath.Length >= maxPathLength) { @@ -287,6 +273,9 @@ namespace System.IO __Error.WinIOError(errorCode, path); } + // It is significantly more complicated to get the long path with minimal allocations if we're injecting the extended dos path prefix. The implicit version + // should match up with what is in CoreFx System.Runtime.Extensions. +#if !FEATURE_IMPLICIT_LONGPATH [System.Security.SecuritySafeCritical] private unsafe static string TryExpandShortFileName(StringBuffer outputBuffer, string originalPath) { @@ -363,5 +352,156 @@ namespace System.IO return bufferToUse.ToString(); } } +#else // !FEATURE_IMPLICIT_LONGPATH + + private static uint GetInputBuffer(StringBuffer content, bool isDosUnc, out StringBuffer buffer) + { + uint length = content.Length; + + length += isDosUnc ? (uint)PathInternal.UncExtendedPrefixToInsert.Length : (uint)PathInternal.ExtendedPathPrefix.Length; + buffer = new StringBuffer(length); + + if (isDosUnc) + { + buffer.CopyFrom(bufferIndex: 0, source: PathInternal.UncExtendedPathPrefix); + uint prefixDifference = (uint)(PathInternal.UncExtendedPathPrefix.Length - PathInternal.UncPathPrefix.Length); + content.CopyTo(bufferIndex: prefixDifference, destination: buffer, destinationIndex: (uint)PathInternal.ExtendedPathPrefix.Length, count: content.Length - prefixDifference); + return prefixDifference; + } + else + { + uint prefixSize = (uint)PathInternal.ExtendedPathPrefix.Length; + buffer.CopyFrom(bufferIndex: 0, source: PathInternal.ExtendedPathPrefix); + content.CopyTo(bufferIndex: 0, destination: buffer, destinationIndex: prefixSize, count: content.Length); + return prefixSize; + } + } + + private static string TryExpandShortFileName(StringBuffer outputBuffer, string originalPath) + { + // We'll have one of a few cases by now (the normalized path will have already: + // + // 1. Dos path (C:\) + // 2. Dos UNC (\\Server\Share) + // 3. Dos device path (\\.\C:\, \\?\C:\) + // + // We want to put the extended syntax on the front if it doesn't already have it, which may mean switching from \\.\. + + uint rootLength = PathInternal.GetRootLength(outputBuffer); + bool isDevice = PathInternal.IsDevice(outputBuffer); + + StringBuffer inputBuffer = null; + bool isDosUnc = false; + uint rootDifference = 0; + bool wasDotDevice = false; + + // Add the extended prefix before expanding to allow growth over MAX_PATH + if (isDevice) + { + // We have one of the following (\\?\ or \\.\) + // We will never get \??\ here as GetFullPathName() does not recognize \??\ and will return it as C:\??\ (or whatever the current drive is). + inputBuffer = new StringBuffer(); + inputBuffer.Append(outputBuffer); + + if (outputBuffer[2] == '.') + { + wasDotDevice = true; + inputBuffer[2] = '?'; + } + } + else + { + // \\Server\Share, but not \\.\ or \\?\. + // We need to know this to be able to push \\?\UNC\ on if required + isDosUnc = outputBuffer.Length > 1 && outputBuffer[0] == '\\' && outputBuffer[1] == '\\' && !PathInternal.IsDevice(outputBuffer); + rootDifference = GetInputBuffer(outputBuffer, isDosUnc, out inputBuffer); + } + + rootLength += rootDifference; + uint inputLength = inputBuffer.Length; + + bool success = false; + uint foundIndex = inputBuffer.Length - 1; + + 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); + result = Win32Native.GetLongPathNameW(inputBuffer.GetHandle(), outputBuffer.GetHandle(), outputBuffer.CharCapacity); + } + else + { + // Found the path + success = true; + outputBuffer.Length = result; + if (foundIndex < inputLength - 1) + { + // It was a partial find, put the non-existent part of the path back + outputBuffer.Append(inputBuffer, foundIndex, inputBuffer.Length - foundIndex); + } + } + } + + // Strip out the prefix and return the string + StringBuffer bufferToUse = success ? outputBuffer : inputBuffer; + if (wasDotDevice) + bufferToUse[2] = '.'; + + string returnValue = null; + + int newLength = (int)(bufferToUse.Length - rootDifference); + if (isDosUnc) + { + // Need to go from \\?\UNC\ to \\?\UN\\ + bufferToUse[(uint)PathInternal.UncExtendedPathPrefix.Length - 1] = '\\'; + } + + // We now need to strip out any added characters at the front of the string + if (bufferToUse.SubstringEquals(originalPath, rootDifference, newLength)) + { + // Use the original path to avoid allocating + returnValue = originalPath; + } + else + { + returnValue = bufferToUse.Substring(rootDifference, newLength); + } + + inputBuffer.Dispose(); + return returnValue; + } +#endif // FEATURE_IMPLICIT_LONGPATH } } \ No newline at end of file diff --git a/src/mscorlib/src/System/IO/Path.cs b/src/mscorlib/src/System/IO/Path.cs index 6f247c2..10da27c 100644 --- a/src/mscorlib/src/System/IO/Path.cs +++ b/src/mscorlib/src/System/IO/Path.cs @@ -38,19 +38,19 @@ namespace System.IO { // Platform specific directory separator character. This is backslash // ('\') on Windows and slash ('/') on Unix. // -#if !PLATFORM_UNIX +#if !PLATFORM_UNIX public static readonly char DirectorySeparatorChar = '\\'; internal const string DirectorySeparatorCharAsString = "\\"; #else public static readonly char DirectorySeparatorChar = '/'; internal const string DirectorySeparatorCharAsString = "/"; #endif // !PLATFORM_UNIX - - // Platform specific alternate directory separator character. + + // Platform specific alternate directory separator character. // There is only one directory separator char on Unix, // so the same definition is used for both Unix and Windows. public static readonly char AltDirectorySeparatorChar = '/'; - + // Platform specific volume separator character. This is colon (':') // on Windows and MacOS, and slash ('/') on Unix. This is mostly // useful for parsing paths like "c:\windows" or "MacVolume:System Folder". @@ -59,8 +59,8 @@ namespace System.IO { public static readonly char VolumeSeparatorChar = ':'; #else public static readonly char VolumeSeparatorChar = '/'; -#endif // !PLATFORM_UNIX - +#endif // !PLATFORM_UNIX + // Platform specific invalid list of characters in a path. // See the "Naming a File" MSDN conceptual docs for more details on // what is valid in a file name (which is slightly different from what @@ -74,22 +74,26 @@ namespace System.IO { #endif // !PLATFORM_UNIX // Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space. - // String.WhitespaceChars will trim aggressively than what the underlying FS does (for ex, NTFS, FAT). - internal static readonly char[] TrimEndChars = { (char) 0x9, (char) 0xA, (char) 0xB, (char) 0xC, (char) 0xD, (char) 0x20, (char) 0x85, (char) 0xA0}; - -#if !PLATFORM_UNIX - private static readonly char[] RealInvalidPathChars = { '\"', '<', '>', '|', '\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 }; + // String.WhitespaceChars will trim aggressively than what the underlying FS does (for ex, NTFS, FAT). + internal static readonly char[] TrimEndChars = + { + (char)0x09, // Horizontal tab + (char)0x0A, // Line feed + (char)0x0B, // Vertical tab + (char)0x0C, // Form feed + (char)0x0D, // Carriage return + (char)0x20, // Space + (char)0x85, // Next line + (char)0xA0 // Non breaking space + }; - // This is used by HasIllegalCharacters - private static readonly char[] InvalidPathCharsWithAdditionalChecks = { '\"', '<', '>', '|', '\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, '*', '?' }; +#if !PLATFORM_UNIX + private static readonly char[] RealInvalidPathChars = PathInternal.InvalidPathChars; private static readonly char[] InvalidFileNameChars = { '\"', '<', '>', '|', '\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, ':', '*', '?', '\\', '/' }; #else private static readonly char[] RealInvalidPathChars = { '\0' }; - // This is used by HasIllegalCharacters - private static readonly char[] InvalidPathCharsWithAdditionalChecks = { '\0', '*', '?' }; - private static readonly char[] InvalidFileNameChars = { '\0', '/' }; #endif // !PLATFORM_UNIX @@ -103,18 +107,14 @@ namespace System.IO { // Make this public sometime. // The max total path is 260, and the max individual component length is 255. // For example, D:\<256 char file name> isn't legal, even though it's under 260 chars. -#if !PLATFORM_UNIX - internal static readonly int MaxPath = 260; -#else - internal static readonly int MaxPath = 1024; -#endif + internal static readonly int MaxPath = PathInternal.MaxShortPath; private static readonly int MaxDirectoryLength = 255; // Windows API definitions internal const int MAX_PATH = 260; // From WinDef.h internal const int MAX_DIRECTORY_PATH = 248; // cannot create directories greater than 248 characters - + // Changes the extension of a file path. The path parameter // specifies a file path, and the extension parameter // specifies a file extension (with a leading period, such as @@ -151,28 +151,47 @@ namespace System.IO { return null; } - // Returns the directory path of a file path. This method effectively // removes the last element of the given file path, i.e. it returns a // string consisting of all characters up to but not including the last // backslash ("\") in the file path. The returned value is null if the file // path is null or if the file path denotes a root (such as "\", "C:", or // "\\server\share"). - // - public static String GetDirectoryName(String path) { - if (path != null) { + public static String GetDirectoryName(String path) + { + if (path != null) + { CheckInvalidPathChars(path); - string normalizedPath = NormalizePath(path, false); + // Expanding short paths is dangerous in this case as the results will change with the current directory. + // + // Suppose you have a path called "PICTUR~1\Foo". Now suppose you have two folders on disk "C:\Mine\Pictures Of Me" + // and "C:\Yours\Pictures of You". If the current directory is neither you'll get back "PICTUR~1". If it is "C:\Mine" + // get back "Pictures Of Me". "C:\Yours" would give back "Pictures of You". + // + // Because of this and as it isn't documented that short paths are expanded we will not expand short names unless + // we're in legacy mode. + string normalizedPath = NormalizePath(path, fullCheck: false, expandShortPaths: +#if FEATURE_PATHCOMPAT + AppContextSwitches.UseLegacyPathHandling +#else + false +#endif + ); // If there are no permissions for PathDiscovery to this path, we should NOT expand the short paths // as this would leak information about paths to which the user would not have access to. - if (path.Length > 0) + if (path.Length > 0 +#if FEATURE_CAS_POLICY + // Only do the extra logic if we're not in full trust + && !CodeAccessSecurityEngine.QuickCheckForAllDemands() +#endif + ) { try { // If we were passed in a path with \\?\ we need to remove it as FileIOPermission does not like it. - string tempPath = Path.RemoveLongPathPrefix(path); + string tempPath = RemoveLongPathPrefix(path); // FileIOPermission cannot handle paths that contain ? or * // So we only pass to FileIOPermission the text up to them. @@ -185,14 +204,15 @@ namespace System.IO { // While we don't use the result of this call we are using it as a consistent way of // doing the security checks. if (pos > 0) - Path.GetFullPath(tempPath.Substring(0, pos)); + GetFullPath(tempPath.Substring(0, pos)); } - catch (SecurityException) { + catch (SecurityException) + { // If the user did not have permissions to the path, make sure that we don't leak expanded short paths // Only re-normalize if the original path had a ~ in it. if (path.IndexOf("~", StringComparison.Ordinal) != -1) { - normalizedPath = NormalizePath(path, /*fullCheck*/ false, /*expandShortPaths*/ false); + normalizedPath = NormalizePath(path, fullCheck: false, expandShortPaths: false); } } catch (PathTooLongException) { } @@ -205,10 +225,11 @@ namespace System.IO { int root = GetRootLength(path); int i = path.Length; - if (i > root) { + if (i > root) + { i = path.Length; if (i == root) return null; - while (i > root && path[--i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar); + while (i > root && path[--i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar); return path.Substring(0, i); } } @@ -218,41 +239,46 @@ namespace System.IO { // Gets the length of the root DirectoryInfo or whatever DirectoryInfo markers // are specified for the first part of the DirectoryInfo name. // - internal static int GetRootLength(String path) { + internal static int GetRootLength(string path) + { CheckInvalidPathChars(path); - - int i = 0; - int length = path.Length; -#if !PLATFORM_UNIX - if (length >= 1 && (IsDirectorySeparator(path[0]))) { - // handles UNC names and directories off current drive's root. - i = 1; - if (length >= 2 && (IsDirectorySeparator(path[1]))) { +#if !PLATFORM_UNIX && FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + int i = 0; + int length = path.Length; + + if (length >= 1 && (IsDirectorySeparator(path[0]))) + { + // handles UNC names and directories off current drive's root. + i = 1; + if (length >= 2 && (IsDirectorySeparator(path[1]))) + { + i = 2; + int n = 2; + while (i < length && ((path[i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar) || --n > 0)) i++; + } + } + else if (length >= 2 && path[1] == VolumeSeparatorChar) + { + // handles A:\foo. i = 2; - int n = 2; - while (i < length && ((path[i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar) || --n > 0)) i++; + if (length >= 3 && (IsDirectorySeparator(path[2]))) i++; } + return i; } - else if (length >= 2 && path[1] == VolumeSeparatorChar) { - // handles A:\foo. - i = 2; - if (length >= 3 && (IsDirectorySeparator(path[2]))) i++; - } - return i; -#else - if (length >= 1 && (IsDirectorySeparator(path[0]))) { - i = 1; + else +#endif // !PLATFORM_UNIX && FEATURE_PATHCOMPAT + { + return PathInternal.GetRootLength(path); } - return i; -#endif // !PLATFORM_UNIX } internal static bool IsDirectorySeparator(char c) { return (c==DirectorySeparatorChar || c == AltDirectorySeparatorChar); } - public static char[] GetInvalidPathChars() { return (char[]) RealInvalidPathChars.Clone(); @@ -324,34 +350,160 @@ namespace System.IO { // method does that, finding the current drive &; directory. But // as long as we don't return this info to the user, we're good. However, // the public GetFullPath does need to do a security check. - internal static String GetFullPathInternal(String path) { + internal static string GetFullPathInternal(string path) + { if (path == null) throw new ArgumentNullException("path"); Contract.EndContractBlock(); - String newPath = NormalizePath(path, true); - + string newPath = NormalizePath(path, fullCheck: true); return newPath; } [System.Security.SecuritySafeCritical] // auto-generated - internal unsafe static String NormalizePath(String path, bool fullCheck) { - return NormalizePath(path, fullCheck, MaxPath); + internal unsafe static string NormalizePath(string path, bool fullCheck) + { + return NormalizePath(path, fullCheck, +#if FEATURE_PATHCOMPAT + AppContextSwitches.BlockLongPaths ? PathInternal.MaxShortPath : +#endif + PathInternal.MaxLongPath); } [System.Security.SecuritySafeCritical] // auto-generated - internal unsafe static String NormalizePath(String path, bool fullCheck, bool expandShortPaths) + internal unsafe static string NormalizePath(string path, bool fullCheck, bool expandShortPaths) { - return NormalizePath(path, fullCheck, MaxPath, expandShortPaths); + return NormalizePath(path, fullCheck, +#if FEATURE_PATHCOMPAT + AppContextSwitches.BlockLongPaths ? PathInternal.MaxShortPath : +#endif + PathInternal.MaxLongPath, + expandShortPaths); } [System.Security.SecuritySafeCritical] // auto-generated - internal unsafe static String NormalizePath(String path, bool fullCheck, int maxPathLength) { - return NormalizePath(path, fullCheck, maxPathLength, true); + internal static string NormalizePath(string path, bool fullCheck, int maxPathLength) + { + return NormalizePath(path, fullCheck, maxPathLength, expandShortPaths: true); + } + + [System.Security.SecuritySafeCritical] + internal static string NormalizePath(string path, bool fullCheck, int maxPathLength, bool expandShortPaths) + { +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + return LegacyNormalizePath(path, fullCheck, maxPathLength, expandShortPaths); + } + else +#endif // FEATURE_APPCOMPAT + { + if (PathInternal.IsExtended(path)) + { + // We can't really know what is valid for all cases of extended paths. + // + // - object names can include other characters as well (':', '/', etc.) + // - even file objects have different rules (pipe names can contain most characters) + // + // As such we will do no further analysis of extended paths to avoid blocking known and unknown + // scenarios as well as minimizing compat breaks should we block now and need to unblock later. + return path; + } + + string normalizedPath = null; + + if (fullCheck == false) + { + // Disabled fullCheck is only called by GetDirectoryName and GetPathRoot. + // Avoid adding addtional callers and try going direct to lighter weight NormalizeDirectorySeparators. + normalizedPath = NewNormalizePathLimitedChecks(path, maxPathLength, expandShortPaths); + } + else + { + normalizedPath = NewNormalizePath(path, maxPathLength, expandShortPaths: true); + } + + if (string.IsNullOrWhiteSpace(normalizedPath)) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + return normalizedPath; + } } + [System.Security.SecuritySafeCritical] + private static string NewNormalizePathLimitedChecks(string path, int maxPathLength, bool expandShortPaths) + { + string normalized = PathInternal.NormalizeDirectorySeparators(path); + + if (PathInternal.IsPathTooLong(normalized) || PathInternal.AreSegmentsTooLong(normalized)) + throw new PathTooLongException(); + +#if !PLATFORM_UNIX + if (!PathInternal.IsDevice(normalized) && PathInternal.HasInvalidVolumeSeparator(path)) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + if (expandShortPaths && normalized.IndexOf('~') != -1) + { + try + { + return LongPathHelper.GetLongPathName(normalized); + } + catch + { + // Don't care if we can't get the long path- might not exist, etc. + } + } +#endif + + return normalized; + } + + /// + /// Normalize the path and check for bad characters or other invalid syntax. + /// + [System.Security.SecuritySafeCritical] + [ResourceExposure(ResourceScope.Machine)] + [ResourceConsumption(ResourceScope.Machine)] + private static string NewNormalizePath(string path, int maxPathLength, bool expandShortPaths) + { + Contract.Requires(path != null, "path can't be null"); + + // Embedded null characters are the only invalid character case we want to check up front. + // This is because the nulls will signal the end of the string to Win32 and therefore have + // unpredictable results. Other invalid characters we give a chance to be normalized out. + if (path.IndexOf('\0') != -1) + throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars")); + +#if !PLATFORM_UNIX + // Note that colon and wildcard checks happen in FileIOPermissions + + // Technically this doesn't matter but we used to throw for this case + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + // We don't want to check invalid characters for device format- see comments for extended above + return LongPathHelper.Normalize(path, (uint)maxPathLength, checkInvalidCharacters: !PathInternal.IsDevice(path), expandShortPaths: expandShortPaths); +#else + if (path.Length == 0) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + // Expand with current directory if necessary + if (!IsPathRooted(path)) + path = Combine(Directory.GetCurrentDirectory(), path); + + // We would ideally use realpath to do this, but it resolves symlinks, requires that the file actually exist, + // and turns it into a full path, which we only want if fullCheck is true. + string collapsedString = PathInternal.RemoveRelativeSegments(path); + + if (collapsedString.Length > maxPathLength) + throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); + + return collapsedString.Length == 0 ? "/" : collapsedString; +#endif // PLATFORM_UNIX + } + +#if FEATURE_PATHCOMPAT [System.Security.SecurityCritical] // auto-generated - internal unsafe static String NormalizePath(String path, bool fullCheck, int maxPathLength, bool expandShortPaths) { + internal unsafe static String LegacyNormalizePath(String path, bool fullCheck, int maxPathLength, bool expandShortPaths) { Contract.Requires(path != null, "path can't be null"); // If we're doing a full path check, trim whitespace and look for @@ -362,7 +514,8 @@ namespace System.IO { path = path.TrimEnd(TrimEndChars); // Look for illegal path characters. - CheckInvalidPathChars(path); + if (PathInternal.AnyPathHasIllegalCharacters(path)) + throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars")); } int index = 0; @@ -771,52 +924,87 @@ namespace System.IO { return newBuffer.ToStringOrExisting(path); } - internal const int MaxLongPath = 32000; +#endif // FEATURE_PATHCOMPAT + + internal const int MaxLongPath = PathInternal.MaxLongPath; - private const string LongPathPrefix = @"\\?\"; - private const string UNCPathPrefix = @"\\"; - private const string UNCLongPathPrefixToInsert = @"?\UNC\"; - private const string UNCLongPathPrefix = @"\\?\UNC\"; + private const string LongPathPrefix = PathInternal.ExtendedPathPrefix; + private const string UNCPathPrefix = PathInternal.UncPathPrefix; + private const string UNCLongPathPrefixToInsert = PathInternal.UncExtendedPrefixToInsert; + private const string UNCLongPathPrefix = PathInternal.UncExtendedPathPrefix; - internal unsafe static bool HasLongPathPrefix(String path) + internal static bool HasLongPathPrefix(string path) { - return path.StartsWith(LongPathPrefix, StringComparison.Ordinal); +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + return path.StartsWith(LongPathPrefix, StringComparison.Ordinal); + else +#endif + return PathInternal.IsExtended(path); } - internal unsafe static String AddLongPathPrefix(String path) + internal static string AddLongPathPrefix(string path) { - if (path.StartsWith(LongPathPrefix, StringComparison.Ordinal)) - return path; +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + if (path.StartsWith(LongPathPrefix, StringComparison.Ordinal)) + return path; - if (path.StartsWith(UNCPathPrefix, StringComparison.Ordinal)) - return path.Insert(2, UNCLongPathPrefixToInsert); // Given \\server\share in longpath becomes \\?\UNC\server\share => UNCLongPathPrefix + path.SubString(2); => The actual command simply reduces the operation cost. + if (path.StartsWith(UNCPathPrefix, StringComparison.Ordinal)) + return path.Insert(2, UNCLongPathPrefixToInsert); // Given \\server\share in longpath becomes \\?\UNC\server\share => UNCLongPathPrefix + path.SubString(2); => The actual command simply reduces the operation cost. - return LongPathPrefix + path; + return LongPathPrefix + path; + } + else +#endif + { + return PathInternal.EnsureExtendedPrefix(path); + } } - internal unsafe static String RemoveLongPathPrefix(String path) + internal static string RemoveLongPathPrefix(string path) { - if (!path.StartsWith(LongPathPrefix, StringComparison.Ordinal)) - return path; +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + if (!path.StartsWith(LongPathPrefix, StringComparison.Ordinal)) + return path; - if (path.StartsWith(UNCLongPathPrefix, StringComparison.OrdinalIgnoreCase)) - return path.Remove(2, 6); // Given \\?\UNC\server\share we return \\server\share => @'\\' + path.SubString(UNCLongPathPrefix.Length) => The actual command simply reduces the operation cost. + if (path.StartsWith(UNCLongPathPrefix, StringComparison.OrdinalIgnoreCase)) + return path.Remove(2, 6); // Given \\?\UNC\server\share we return \\server\share => @'\\' + path.SubString(UNCLongPathPrefix.Length) => The actual command simply reduces the operation cost. - return path.Substring(4); + return path.Substring(4); + } + else +#endif + { + return PathInternal.RemoveExtendedPrefix(path); + } } - internal unsafe static StringBuilder RemoveLongPathPrefix(StringBuilder pathSB) + internal static StringBuilder RemoveLongPathPrefix(StringBuilder pathSB) { - string path = pathSB.ToString(); - if (!path.StartsWith(LongPathPrefix, StringComparison.Ordinal)) - return pathSB; +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + if (!PathInternal.StartsWithOrdinal(pathSB, LongPathPrefix)) + return pathSB; - if (path.StartsWith(UNCLongPathPrefix, StringComparison.OrdinalIgnoreCase)) - return pathSB.Remove(2, 6); // Given \\?\UNC\server\share we return \\server\share => @'\\' + path.SubString(UNCLongPathPrefix.Length) => The actual command simply reduces the operation cost. + // Given \\?\UNC\server\share we return \\server\share => @'\\' + path.SubString(UNCLongPathPrefix.Length) => The actual command simply reduces the operation cost. + if (PathInternal.StartsWithOrdinal(pathSB, UNCLongPathPrefix, ignoreCase: true)) + return pathSB.Remove(2, 6); - return pathSB.Remove(0, 4); + return pathSB.Remove(0, 4); + } + else +#endif + { + return PathInternal.RemoveExtendedPrefix(pathSB); + } } + // Returns the name and extension parts of the given path. The resulting // string contains the characters of path that follow the last // backslash ("\"), slash ("/"), or colon (":") character in @@ -868,7 +1056,10 @@ namespace System.IO { [Pure] public static String GetPathRoot(String path) { if (path == null) return null; - path = NormalizePath(path, false); + + // Expanding short paths has no impact on the path root- there is no such thing as an + // 8.3 volume or server/share name. + path = NormalizePath(path, fullCheck: false, expandShortPaths: false); return path.Substring(0, GetRootLength(path)); } @@ -878,8 +1069,8 @@ namespace System.IO { #if !FEATURE_CORECLR new EnvironmentPermission(PermissionState.Unrestricted).Demand(); #endif - StringBuilder sb = new StringBuilder(MaxPath); - uint r = Win32Native.GetTempPath(MaxPath, sb); + StringBuilder sb = new StringBuilder(PathInternal.MaxShortPath); + uint r = Win32Native.GetTempPath(PathInternal.MaxShortPath, sb); String path = sb.ToString(); if (r==0) __Error.WinIOError(); path = GetFullPathInternal(path); @@ -893,17 +1084,7 @@ namespace System.IO { internal static bool IsRelative(string path) { Contract.Assert(path != null, "path can't be null"); -#if !PLATFORM_UNIX - if ((path.Length >= 3 && path[1] == VolumeSeparatorChar && path[2] == DirectorySeparatorChar && - ((path[0] >= 'a' && path[0] <= 'z') || (path[0] >= 'A' && path[0] <= 'Z'))) || - (path.Length >= 2 && path[0] == '\\' && path[1] == '\\')) -#else - if(path.Length >= 1 && path[0] == VolumeSeparatorChar) -#endif // !PLATFORM_UNIX - return false; - else - return true; - + return PathInternal.IsPartiallyQualified(path); } // Returns a cryptographically strong random 8.3 string that can be @@ -958,7 +1139,7 @@ namespace System.IO { return InternalGetTempFileName(false); } - [System.Security.SecurityCritical] + [System.Security.SecurityCritical] private static String InternalGetTempFileName(bool checkHost) { String path = GetTempPath(); @@ -974,7 +1155,7 @@ namespace System.IO { state.EnsureState(); } #else - new FileIOPermission(FileIOPermissionAccess.Write, path).Demand(); + FileIOPermission.QuickDemand(FileIOPermissionAccess.Write, path); #endif StringBuilder sb = new StringBuilder(MaxPath); uint r = Win32Native.GetTempFileName(path, "tmp", 0, sb); @@ -1005,8 +1186,7 @@ namespace System.IO { } return false; } - - + // Tests if the given path contains a root. A path is considered rooted // if it starts with a backslash ("\") or a drive letter and a colon (":"). // @@ -1218,28 +1398,15 @@ namespace System.IO { } - internal static bool HasIllegalCharacters(String path, bool checkAdditional) - { - Contract.Requires(path != null); - - if (checkAdditional) - { - return path.IndexOfAny(InvalidPathCharsWithAdditionalChecks) >= 0; - } - - return path.IndexOfAny(RealInvalidPathChars) >= 0; - } - internal static void CheckInvalidPathChars(String path, bool checkAdditional = false) { if (path == null) throw new ArgumentNullException("path"); - if (Path.HasIllegalCharacters(path, checkAdditional)) + if (PathInternal.HasIllegalCharacters(path, checkAdditional)) throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars")); } - internal static String InternalCombine(String path1, String path2) { if (path1==null || path2==null) throw new ArgumentNullException((path1==null) ? "path1" : "path2"); diff --git a/src/mscorlib/src/System/IO/PathInternal.cs b/src/mscorlib/src/System/IO/PathInternal.cs index ac1afb8..a7441b5 100644 --- a/src/mscorlib/src/System/IO/PathInternal.cs +++ b/src/mscorlib/src/System/IO/PathInternal.cs @@ -20,8 +20,13 @@ namespace System.IO internal const string UncExtendedPathPrefix = @"\\?\UNC\"; internal const string DevicePathPrefix = @"\\.\"; internal const int DevicePrefixLength = 4; +#if !PLATFORM_UNIX internal const int MaxShortPath = 260; internal const int MaxShortDirectoryPath = 248; +#else + internal const int MaxShortPath = 1024; + internal const int MaxShortDirectoryPath = MaxShortPath; +#endif // 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. @@ -29,6 +34,7 @@ namespace System.IO internal const int MaxLongPath = short.MaxValue; internal static readonly int MaxComponentLength = 255; +#if !PLATFORM_UNIX internal static readonly char[] InvalidPathChars = { '\"', '<', '>', '|', '\0', @@ -37,6 +43,10 @@ namespace System.IO (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31 }; +#else + internal static readonly char[] InvalidPathChars = { '\0' }; +#endif + /// /// Validates volume separator only occurs as C: or \\?\C:. This logic is meant to filter out Alternate Data Streams. @@ -353,7 +363,11 @@ namespace System.IO /// internal static bool AnyPathHasIllegalCharacters(string path, bool checkAdditional = false) { - return path.IndexOfAny(InvalidPathChars) >= 0 || (checkAdditional && AnyPathHasWildCardCharacters(path)); + return path.IndexOfAny(InvalidPathChars) >= 0 +#if !PLATFORM_UNIX + || (checkAdditional && AnyPathHasWildCardCharacters(path)) +#endif + ; } /// @@ -411,6 +425,11 @@ namespace System.IO private unsafe static uint GetRootLength(char* path, ulong pathLength) { uint i = 0; + +#if PLATFORM_UNIX + if (pathLength >= 1 && (IsDirectorySeparator(path[0]))) + i = 1; +#else uint volumeSeparatorLength = 2; // Length to the colon "C:" uint uncRootLength = 2; // Length to the start of the server name "\\" @@ -452,6 +471,7 @@ namespace System.IO i = volumeSeparatorLength; if (pathLength >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++; } +#endif // !PLATFORM_UNIX return i; } @@ -480,6 +500,9 @@ namespace System.IO /// internal static bool IsPartiallyQualified(string path) { +#if PLATFORM_UNIX + return !(path.Length >= 1 && path[0] == Path.DirectorySeparatorChar); +#else if (path.Length < 2) { // It isn't fixed, it must be relative. There is no way to specify a fixed @@ -502,6 +525,7 @@ namespace System.IO // 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])); +#endif // !PLATFORM_UNIX } /// @@ -518,6 +542,9 @@ namespace System.IO /// internal static bool IsPartiallyQualified(StringBuffer path) { +#if PLATFORM_UNIX + return !(path.Length >= 1 && path[0] == Path.DirectorySeparatorChar); +#else if (path.Length < 2) { // It isn't fixed, it must be relative. There is no way to specify a fixed @@ -540,10 +567,11 @@ namespace System.IO // 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])); +#endif // !PLATFORM_UNIX } /// - /// Returns the characters to skip at the start of the path if it starts with space(s) and a drive or directory separator. + /// On Windows, 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(). /// @@ -552,6 +580,7 @@ namespace System.IO /// internal static int PathStartSkip(string path) { +#if !PLATFORM_UNIX int startIndex = 0; while (startIndex < path.Length && path[startIndex] == ' ') startIndex++; @@ -561,6 +590,7 @@ namespace System.IO // Go ahead and skip spaces as we're either " C:" or " \" return startIndex; } +#endif return 0; } @@ -571,7 +601,11 @@ namespace System.IO [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsDirectorySeparator(char c) { - return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + return c == Path.DirectorySeparatorChar +#if !PLATFORM_UNIX + || c == Path.AltDirectorySeparatorChar +#endif + ; } /// @@ -616,10 +650,13 @@ namespace System.IO current = path[i]; if (IsDirectorySeparator(current) && (current != Path.DirectorySeparatorChar +#if !PLATFORM_UNIX // 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])))) + || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1])) +#endif + )) { - normalized = false; + normalized = false; break; } } @@ -629,11 +666,14 @@ namespace System.IO StringBuilder builder = StringBuilderCache.Acquire(path.Length); +#if !PLATFORM_UNIX + // On Windows we always keep the first separator, even if the next is a separator (we need to keep initial two for UNC/extended) if (IsDirectorySeparator(path[start])) { start++; builder.Append(Path.DirectorySeparatorChar); } +#endif for (int i = start; i < path.Length; i++) { @@ -657,5 +697,105 @@ namespace System.IO return StringBuilderCache.GetStringAndRelease(builder); } + +#if PLATFORM_UNIX + // We rely on Windows to remove relative segments on Windows. This would need to be updated to + // handle the proper rooting on Windows if we for some reason need it. + + /// + /// Try to remove relative segments from the given path (without combining with a root). + /// + /// Skip the specified number of characters before evaluating. + internal static string RemoveRelativeSegments(string path, int skip = 0) + { + bool flippedSeparator = false; + + // Remove "//", "/./", and "/../" from the path by copying each character to the output, + // except the ones we're removing, such that the builder contains the normalized path + // at the end. + var sb = StringBuilderCache.Acquire(path.Length); + if (skip > 0) + { + sb.Append(path, 0, skip); + } + + int componentCharCount = 0; + for (int i = skip; i < path.Length; i++) + { + char c = path[i]; + + if (PathInternal.IsDirectorySeparator(c) && i + 1 < path.Length) + { + componentCharCount = 0; + + // Skip this character if it's a directory separator and if the next character is, too, + // e.g. "parent//child" => "parent/child" + if (PathInternal.IsDirectorySeparator(path[i + 1])) + { + continue; + } + + // Skip this character and the next if it's referring to the current directory, + // e.g. "parent/./child" =? "parent/child" + if ((i + 2 == path.Length || PathInternal.IsDirectorySeparator(path[i + 2])) && + path[i + 1] == '.') + { + i++; + continue; + } + + // Skip this character and the next two if it's referring to the parent directory, + // e.g. "parent/child/../grandchild" => "parent/grandchild" + if (i + 2 < path.Length && + (i + 3 == path.Length || PathInternal.IsDirectorySeparator(path[i + 3])) && + path[i + 1] == '.' && path[i + 2] == '.') + { + // Unwind back to the last slash (and if there isn't one, clear out everything). + int s; + for (s = sb.Length - 1; s >= 0; s--) + { + if (PathInternal.IsDirectorySeparator(sb[s])) + { + sb.Length = s; + break; + } + } + if (s < 0) + { + sb.Length = 0; + } + + i += 2; + continue; + } + } + + if (++componentCharCount > PathInternal.MaxComponentLength) + { + throw new PathTooLongException(); + } + + // Normalize the directory separator if needed + if (c != Path.DirectorySeparatorChar && c == Path.AltDirectorySeparatorChar) + { + c = Path.DirectorySeparatorChar; + flippedSeparator = true; + } + + sb.Append(c); + } + + if (flippedSeparator || sb.Length != path.Length) + { + return StringBuilderCache.GetStringAndRelease(sb); + } + else + { + // We haven't changed the source path, return the original + StringBuilderCache.Release(sb); + return path; + } + } +#endif // PLATFORM_UNIX } } \ No newline at end of file diff --git a/src/mscorlib/src/System/Security/Permissions/FileIOPermission.cs b/src/mscorlib/src/System/Security/Permissions/FileIOPermission.cs index e66eb34..e43c49c 100644 --- a/src/mscorlib/src/System/Security/Permissions/FileIOPermission.cs +++ b/src/mscorlib/src/System/Security/Permissions/FileIOPermission.cs @@ -220,9 +220,18 @@ namespace System.Security.Permissions { Array.Copy(pathListOrig, pathList, pathListOrig.Length); } - CheckIllegalCharacters( pathList ); ArrayList pathArrayList = StringExpressionSet.CreateListFromExpressions(pathList, needFullPath); - + + // If we need the full path the standard illegal characters will be checked in StringExpressionSet. + CheckIllegalCharacters(pathList, onlyCheckExtras: needFullPath); + + // StringExpressionSet will do minor normalization, trimming spaces and replacing alternate + // directory separators. It will make an attemt to expand short file names and will check + // for standard colon placement. + // + // If needFullPath is true it will call NormalizePath- which performs short name expansion + // and does the normal validity checks. + if ((access & FileIOPermissionAccess.Read) != 0) { if (m_read == null) @@ -533,13 +542,53 @@ namespace System.Security.Permissions { } } - private static void CheckIllegalCharacters( String[] str ) + private static void CheckIllegalCharacters(String[] str, bool onlyCheckExtras) { +#if !PLATFORM_UNIX + for (int i = 0; i < str.Length; ++i) + { + // FileIOPermission doesn't allow for normalizing across various volume names. This means "C:\" and + // "\\?\C:\" won't be considered correctly. In addition there are many other aliases for the volume + // besides "C:" such as (in one concrete example) "\\?\Harddisk0Partition2\", "\\?\HarddiskVolume6\", + // "\\?\Volume{d1655348-0000-0000-0000-f01500000000}\", etc. + // + // We'll continue to explicitly block extended syntax here by disallowing wildcards no matter where + // they occur in the string (e.g. \\?\ isn't ok) + if (CheckExtraPathCharacters(str[i])) + throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars")); + + if (!onlyCheckExtras) + Path.CheckInvalidPathChars(str[i]); + } +#else + // There are no "extras" on Unix + if (onlyCheckExtras) + return; + for (int i = 0; i < str.Length; ++i) { - Path.CheckInvalidPathChars(str[i], true); } +#endif + } + +#if !PLATFORM_UNIX + /// + /// Check for ?,* and null, ignoring extended syntax. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe static bool CheckExtraPathCharacters(string path) + { + char currentChar; + for (int i = 0; i < path.Length; i++) + { + currentChar = path[i]; + + // We also check for null here as StringExpressionSet will trim it out. (Ensuring we still throw as we always have.) + if (currentChar == '*' || currentChar == '?' || currentChar == '\0') return true; + } + return false; } +#endif private static bool AccessIsSet( FileIOPermissionAccess access, FileIOPermissionAccess question ) { @@ -942,12 +991,7 @@ namespace System.Security.Permissions { /// FileIOPermission object would have performed. /// /// IMPORTANT: This method should only be used after calling GetFullPath on the path to verify - /// /// - /// - /// - /// - /// [System.Security.SecuritySafeCritical] internal static void QuickDemand(FileIOPermissionAccess access, string fullPath, bool checkForDuplicates, bool needFullPath) { @@ -957,25 +1001,126 @@ namespace System.Security.Permissions { new FileIOPermission(access, new string[] { fullPath }, checkForDuplicates, needFullPath).Demand(); } else -#endif +#endif { - //Emulate FileIOPermission checks - Path.CheckInvalidPathChars(fullPath, true); + EmulateFileIOPermissionChecks(fullPath); + } + } - if (fullPath.Length > 2 && fullPath.IndexOf(':', 2) != -1) + /// + /// Call this method if you don't need a the FileIOPermission for anything other than calling Demand() once. + /// + /// This method tries to verify full access before allocating a FileIOPermission object. + /// If full access is there, then we still have to emulate the checks that creating the + /// FileIOPermission object would have performed. + /// + /// IMPORTANT: This method should only be used after calling GetFullPath on the path to verify + /// + /// + [System.Security.SecuritySafeCritical] + internal static void QuickDemand(FileIOPermissionAccess access, string[] fullPathList, bool checkForDuplicates = false, bool needFullPath = true) + { +#if FEATURE_CAS_POLICY + if (!CodeAccessSecurityEngine.QuickCheckForAllDemands()) + { + new FileIOPermission(access, fullPathList, checkForDuplicates, needFullPath).Demand(); + } + else +#endif + { + foreach (string fullPath in fullPathList) + { + EmulateFileIOPermissionChecks(fullPath); + } + } + } + + [System.Security.SecuritySafeCritical] + internal static void QuickDemand(PermissionState state) + { + // Should be a no-op without CAS +#if FEATURE_CAS_POLICY + if (!CodeAccessSecurityEngine.QuickCheckForAllDemands()) + { + new FileIOPermission(state).Demand(); + } +#endif + } + +#if FEATURE_MACL + [System.Security.SecuritySafeCritical] + internal static void QuickDemand(FileIOPermissionAccess access, AccessControlActions control, string fullPath, bool checkForDuplicates = false, bool needFullPath = true) + { + if (!CodeAccessSecurityEngine.QuickCheckForAllDemands()) + { + new FileIOPermission(access, control, new string[] { fullPath }, checkForDuplicates, needFullPath).Demand(); + } + else + { + EmulateFileIOPermissionChecks(fullPath); + } + } + + [System.Security.SecuritySafeCritical] + internal static void QuickDemand(FileIOPermissionAccess access, AccessControlActions control, string[] fullPathList, bool checkForDuplicates = true, bool needFullPath = true) + { + if (!CodeAccessSecurityEngine.QuickCheckForAllDemands()) + { + new FileIOPermission(access, control, fullPathList, checkForDuplicates, needFullPath).Demand(); + } + else + { + foreach (string fullPath in fullPathList) + { + EmulateFileIOPermissionChecks(fullPath); + } + } + } +#endif + + /// + /// Perform the additional path checks that would normally happen when creating a FileIOPermission object. + /// + /// A path that has already gone through GetFullPath or Normalize + internal static void EmulateFileIOPermissionChecks(string fullPath) + { + // Callers should have already made checks for invalid path format via normalization. This method will only make the + // additional checks needed to throw the same exceptions that would normally throw when using FileIOPermission. + // These checks are done via CheckIllegalCharacters() and StringExpressionSet in AddPathList() above. + // + // We have to check the beginning as some paths may be passed in as path + @"\.", which will be normalized away. + BCLDebug.Assert( + fullPath.StartsWith(Path.NormalizePath(fullPath, fullCheck: false), StringComparison.OrdinalIgnoreCase), + string.Format("path isn't normalized: {0}", fullPath)); + + // Checking for colon / invalid characters on device paths blocks legitimate access to objects such as named pipes. + if ( +#if FEATURE_PATHCOMPAT + AppContextSwitches.UseLegacyPathHandling || +#endif + !PathInternal.IsDevice(fullPath)) + { + // GetFullPath already checks normal invalid path characters. We need to just check additional (wildcard) characters here. + // (By calling the standard helper we can allow extended paths \\?\ through when the support is enabled.) + if (PathInternal.HasWildCardCharacters(fullPath)) + { + throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars")); + } + + if (PathInternal.HasInvalidVolumeSeparator(fullPath)) { throw new NotSupportedException(Environment.GetResourceString("Argument_PathFormatNotSupported")); } } } } - + [Serializable] internal sealed class FileIOAccess { #if !FEATURE_CASE_SENSITIVE_FILESYSTEM private bool m_ignoreCase = true; -#else +#else private bool m_ignoreCase = false; #endif // !FEATURE_CASE_SENSITIVE_FILESYSTEM @@ -1235,16 +1380,16 @@ namespace System.Security.Permissions { return true; } - + private static String GetRoot( String path ) { -#if !PLATFORM_UNIX +#if !PLATFORM_UNIX String str = path.Substring( 0, 3 ); if (str.EndsWith( ":\\", StringComparison.Ordinal)) #else String str = path.Substring( 0, 1 ); if(str == "/") -#endif // !PLATFORM_UNIX +#endif // !PLATFORM_UNIX { return str; } diff --git a/src/mscorlib/src/System/Security/Util/StringExpressionSet.cs b/src/mscorlib/src/System/Security/Util/StringExpressionSet.cs index ae45546..19937f5 100644 --- a/src/mscorlib/src/System/Security/Util/StringExpressionSet.cs +++ b/src/mscorlib/src/System/Security/Util/StringExpressionSet.cs @@ -219,10 +219,12 @@ namespace System.Security.Util { if (str[index] == null) throw new ArgumentNullException( "str" ); + // Replace alternate directory separators String oneString = StaticProcessWholeString( str[index] ); if (oneString != null && oneString.Length != 0) { + // Trim leading and trailing spaces String temp = StaticProcessSingleString( oneString); int indexOfNull = temp.IndexOf( '\0' ); @@ -232,14 +234,13 @@ namespace System.Security.Util { if (temp != null && temp.Length != 0) { - if (Path.IsRelative(temp)) + if (PathInternal.IsPartiallyQualified(temp)) { - throw new ArgumentException( Environment.GetResourceString( "Argument_AbsolutePathRequired" ) ); + throw new ArgumentException(Environment.GetResourceString( "Argument_AbsolutePathRequired" ) ); } temp = CanonicalizePath( temp, needFullPath ); - retArrayList.Add( temp ); } } @@ -737,27 +738,14 @@ namespace System.Security.Util { } [System.Security.SecurityCritical] // auto-generated - internal static String CanonicalizePath( String path, bool needFullPath ) + internal static string CanonicalizePath(string path, bool needFullPath) { - -#if !PLATFORM_UNIX - if (path.IndexOf( '~' ) != -1) - { - string longPath = null; - GetLongPathName(path, JitHelpers.GetStringHandleOnStack(ref longPath)); - path = (longPath != null) ? longPath : path; - } - - if (path.IndexOf( ':', 2 ) != -1) - throw new NotSupportedException( Environment.GetResourceString( "Argument_PathFormatNotSupported" ) ); -#endif // !PLATFORM_UNIX - if (needFullPath) { - String newPath = System.IO.Path.GetFullPathInternal( path ); - if (path.EndsWith( m_directorySeparator + ".", StringComparison.Ordinal )) + string newPath = Path.GetFullPathInternal(path); + if (path.EndsWith(m_directorySeparator + ".", StringComparison.Ordinal)) { - if (newPath.EndsWith( m_directorySeparator )) + if (newPath.EndsWith(m_directorySeparator)) { newPath += "."; } @@ -765,11 +753,25 @@ namespace System.Security.Util { { newPath += m_directorySeparator + "."; } - } - return newPath; + } + path = newPath; } - else - return path; +#if !PLATFORM_UNIX + else if (path.IndexOf('~') != -1) + { + // GetFullPathInternal() will expand 8.3 file names + string longPath = null; + GetLongPathName(path, JitHelpers.GetStringHandleOnStack(ref longPath)); + path = (longPath != null) ? longPath : path; + } + + // This blocks usage of alternate data streams and some extended syntax paths (\\?\C:\). Checking after + // normalization allows valid paths such as " C:\" to be considered ok (as it will become "C:\"). + if (path.IndexOf(':', 2) != -1) + throw new NotSupportedException(Environment.GetResourceString("Argument_PathFormatNotSupported")); +#endif // !PLATFORM_UNIX + + return path; } } } diff --git a/src/mscorlib/src/System/Security/Util/URLString.cs b/src/mscorlib/src/System/Security/Util/URLString.cs index b09e1c4..8ac00ca 100644 --- a/src/mscorlib/src/System/Security/Util/URLString.cs +++ b/src/mscorlib/src/System/Security/Util/URLString.cs @@ -281,7 +281,7 @@ namespace System.Security.Util { temp = url.Substring( index ); #endif // !PLATFORM_UNIX - } + } else { throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) ); @@ -355,7 +355,7 @@ namespace System.Security.Util { temp = temp.Substring( Rindex, portIndex - Rindex ) + temp.Substring( tempIndex ); } else - throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) ); + throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) ); } else throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) ); @@ -366,7 +366,7 @@ namespace System.Security.Util { } return temp; - } + } // This does three things: // 1. It makes the following modifications to the start of the string: @@ -377,13 +377,24 @@ namespace System.Security.Util { // 3. Throws a PathTooLongException if the length of the resulting URL is >= MAX_PATH. // This is done to prevent security issues due to canonicalization truncations. // Remove this method when the Path class supports "\\?\" - internal static String PreProcessForExtendedPathRemoval(String url, bool isFileUrl) + internal static string PreProcessForExtendedPathRemoval(string url, bool isFileUrl) + { + return PreProcessForExtendedPathRemoval(checkPathLength: true, url: url, isFileUrl: isFileUrl); + } + + internal static string PreProcessForExtendedPathRemoval(bool checkPathLength, string url, bool isFileUrl) + { + bool isUncShare = false; + return PreProcessForExtendedPathRemoval(checkPathLength: checkPathLength, url: url, isFileUrl: isFileUrl, isUncShare: ref isUncShare); + } + + // Keeping this signature to avoid reflection breaks + private static string PreProcessForExtendedPathRemoval(string url, bool isFileUrl, ref bool isUncShare) { - bool uncShare = false; - return PreProcessForExtendedPathRemoval(url, isFileUrl, ref uncShare); + return PreProcessForExtendedPathRemoval(checkPathLength: true, url: url, isFileUrl: isFileUrl, isUncShare: ref isUncShare); } - private static String PreProcessForExtendedPathRemoval(String url, bool isFileUrl, ref bool isUncShare) + private static string PreProcessForExtendedPathRemoval(bool checkPathLength, string url, bool isFileUrl, ref bool isUncShare) { // This is the modified URL that we will return StringBuilder modifiedUrl = new StringBuilder(url); @@ -457,15 +468,28 @@ namespace System.Security.Util { } // ITEM 3 - If the path is greater than or equal (due to terminating NULL in windows) MAX_PATH, we throw. - if (modifiedUrl.Length >= Path.MaxPath) + if (checkPathLength) { - throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); + // This needs to be a separate method to avoid hitting the static constructor on AppContextSwitches + CheckPathTooLong(modifiedUrl); } // Create the result string from the StringBuilder return modifiedUrl.ToString(); } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void CheckPathTooLong(StringBuilder path) + { + if (path.Length >= ( +#if FEATURE_PATHCOMPAT + AppContextSwitches.BlockLongPaths ? PathInternal.MaxShortPath : +#endif + PathInternal.MaxLongPath)) + { + throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); + } + } // Do any misc massaging of data in the URL private String PreProcessURL(String url, bool isFileURL) -- 2.7.4