diff options
author | Jeremy Kuhne <jeremy.kuhne@microsoft.com> | 2018-02-22 12:13:59 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-22 12:13:59 -0800 |
commit | a0cfda07fbdc319d8e5137a1b28d190ec242597e (patch) | |
tree | 0a051e80cdd0c728b7b02440fcfd653cb1f207e2 /src | |
parent | 593782e030e803d93562ff802be984681959f2dd (diff) | |
download | coreclr-a0cfda07fbdc319d8e5137a1b28d190ec242597e.tar.gz coreclr-a0cfda07fbdc319d8e5137a1b28d190ec242597e.tar.bz2 coreclr-a0cfda07fbdc319d8e5137a1b28d190ec242597e.zip |
Remove most preemptive checks from GetFullPath (#16478)
* Removing the colon block that tosses out paths that are not device path.
Fixes: #26359
* Striping 2nd and 3rd args of PathHelper.Normalize
* Build on Maryam's changes
- Remove all path validity checks outside of
- Null
- Embedded null
- Empty or all spaces (effectively empty)
- Remove PathStartSkip helper
- Use span overloads for StringBuffer usage
- Clean up some comments
* Address feedback
* Tweak to match other AsSpan methods
Diffstat (limited to 'src')
8 files changed, 49 insertions, 309 deletions
diff --git a/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems b/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems index d0dc4394b4..0ce28f5cc0 100644 --- a/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems +++ b/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems @@ -717,7 +717,6 @@ <Compile Include="$(MSBuildThisFileDirectory)System\IO\Path.Windows.cs" /> <Compile Include="$(MSBuildThisFileDirectory)System\IO\PathHelper.Windows.cs" /> <Compile Include="$(MSBuildThisFileDirectory)System\IO\PathInternal.Windows.cs" /> - <Compile Include="$(MSBuildThisFileDirectory)System\IO\PathInternal.Windows.StringBuffer.cs" /> <Compile Include="$(MSBuildThisFileDirectory)System\IO\DisableMediaInsertionPrompt.cs" /> <Compile Include="$(MSBuildThisFileDirectory)System\Security\SafeBSTRHandle.cs" /> <Compile Include="$(MSBuildThisFileDirectory)System\Security\SecureString.Windows.cs" /> diff --git a/src/mscorlib/shared/System/IO/Path.Windows.cs b/src/mscorlib/shared/System/IO/Path.Windows.cs index b0443c5fd8..846dd53492 100644 --- a/src/mscorlib/shared/System/IO/Path.Windows.cs +++ b/src/mscorlib/shared/System/IO/Path.Windows.cs @@ -27,64 +27,32 @@ namespace System.IO (char)31 }; - // Expands the given path to a fully qualified path. + // Expands the given path to a fully qualified path. public static string GetFullPath(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); - // Embedded null characters are the only invalid character case we want to check up front. + // If the path would normalize to string empty, we'll consider it empty + if (PathInternal.IsEffectivelyEmpty(path)) + throw new ArgumentException(SR.Arg_PathEmpty, nameof(path)); + + // Embedded null characters are the only invalid character case we trully care about. // 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. + // unpredictable results. if (path.IndexOf('\0') != -1) throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path)); 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. + // \\?\ paths are considered normalized by definition. Windows doesn't normalize \\?\ + // paths and neither should we. Even if we wanted to GetFullPathName does not work + // properly with device paths. If one wants to pass a \\?\ path through normalization + // one can chop off the prefix, pass it to GetFullPath and add it again. return path; } - bool isDevice = PathInternal.IsDevice(path); - if (!isDevice) - { - // 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:". - // (Note that we used to explicitly check "http:" and "file:"- these are caught by this check now.) - int startIndex = PathInternal.PathStartSkip(path); - - // Move past the colon - startIndex += 2; - - if ((path.Length > 0 && path[0] == PathInternal.VolumeSeparatorChar) - || (path.Length >= startIndex && path[startIndex - 1] == PathInternal.VolumeSeparatorChar && !PathInternal.IsValidDriveChar(path[startIndex - 2])) - || (path.Length > startIndex && path.IndexOf(PathInternal.VolumeSeparatorChar, startIndex) != -1)) - { - throw new NotSupportedException(SR.Format(SR.Argument_PathFormatNotSupported_Path, path)); - } - } - - // Technically this doesn't matter but we used to throw for this case - if (PathInternal.IsEffectivelyEmpty(path)) - throw new ArgumentException(SR.Arg_PathEmpty, nameof(path)); - - // We don't want to check invalid characters for device format- see comments for extended above - string fullPath = PathHelper.Normalize(path, checkInvalidCharacters: !isDevice, expandShortPaths: true); - - if (!isDevice) - { - // Emulate FileIOPermissions checks, retained for compatibility (normal invalid characters have already been checked) - if (PathInternal.HasWildCardCharacters(fullPath)) - throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path)); - } - - return fullPath; + return PathHelper.Normalize(path); } public static string GetFullPath(string path, string basePath) diff --git a/src/mscorlib/shared/System/IO/Path.cs b/src/mscorlib/shared/System/IO/Path.cs index 079eab27f8..3784b877d3 100644 --- a/src/mscorlib/shared/System/IO/Path.cs +++ b/src/mscorlib/shared/System/IO/Path.cs @@ -119,10 +119,12 @@ namespace System.IO return end; } - // Returns the extension of the given path. The returned value includes the - // period (".") character of the extension except when you have a terminal period when you get string.Empty, such as ".exe" or - // ".cpp". The returned value is null if the given path is - // null or if the given path does not include an extension. + /// <summary> + /// Returns the extension of the given path. The returned value includes the period (".") character of the + /// extension except when you have a terminal period when you get string.Empty, such as ".exe" or ".cpp". + /// The returned value is null if the given path is null or empty if the given path does not include an + /// extension. + /// </summary> public static string GetExtension(string path) { if (path == null) @@ -157,9 +159,11 @@ namespace System.IO return ReadOnlySpan<char>.Empty; } - // Returns the name and extension parts of the given path. The resulting - // string contains the characters of path that follow the last - // separator in path. The resulting string is null if path is null. + /// <summary> + /// Returns the name and extension parts of the given path. The resulting string contains + /// the characters of path that follow the last separator in path. The resulting string is + /// null if path is null. + /// </summary> public static string GetFileName(string path) { if (path == null) @@ -215,8 +219,10 @@ namespace System.IO fileName.Slice(0, lastPeriod); } - // Returns a cryptographically strong random 8.3 string that can be - // used as either a folder name or a file name. + /// <summary> + /// Returns a cryptographically strong random 8.3 string that can be + /// used as either a folder name or a file name. + /// </summary> public static unsafe string GetRandomFileName() { byte* pKey = stackalloc byte[KeyLength]; @@ -256,10 +262,11 @@ namespace System.IO return !PathInternal.IsPartiallyQualified(path); } - // Tests if a path includes a file extension. The result is - // true if the characters that follow the last directory - // separator ('\\' or '/') or volume separator (':') in the path include - // a period (".") other than a terminal period. The result is false otherwise. + + /// <summary> + /// Tests if a path's file name includes a file extension. A trailing period + /// is not considered an extension. + /// </summary> public static bool HasExtension(string path) { if (path != null) @@ -801,9 +808,6 @@ namespace System.IO return StringBuilderCache.GetStringAndRelease(sb); } - // StringComparison and IsCaseSensitive are also available in PathInternal.CaseSensitivity but we are - // too low in System.Runtime.Extensions to use it (no FileStream, etc.) - /// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary> internal static StringComparison StringComparison { diff --git a/src/mscorlib/shared/System/IO/PathHelper.Windows.cs b/src/mscorlib/shared/System/IO/PathHelper.Windows.cs index 6aa01d0495..bde11fd423 100644 --- a/src/mscorlib/shared/System/IO/PathHelper.Windows.cs +++ b/src/mscorlib/shared/System/IO/PathHelper.Windows.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace System.IO @@ -13,136 +12,26 @@ namespace System.IO /// </summary> internal class PathHelper { - // 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; - /// <summary> /// Normalize the given path. /// </summary> /// <remarks> - /// Normalizes via Win32 GetFullPathName(). 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) + /// Normalizes via Win32 GetFullPathName(). /// </remarks> /// <param name="path">Path to normalize</param> - /// <param name="checkInvalidCharacters">True to check for invalid characters</param> - /// <param name="expandShortPaths">Attempt to expand short paths if true</param> - /// <exception cref="ArgumentException">Thrown if the path is an illegal UNC (does not contain a full server/share) or contains illegal characters.</exception> - /// <exception cref="PathTooLongException">Thrown if the path or a path segment exceeds the filesystem limits.</exception> - /// <exception cref="FileNotFoundException">Thrown if Windows returns ERROR_FILE_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error)</exception> - /// <exception cref="DirectoryNotFoundException">Thrown if Windows returns ERROR_PATH_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error)</exception> - /// <exception cref="UnauthorizedAccessException">Thrown if Windows returns ERROR_ACCESS_DENIED. (See Win32Marshal.GetExceptionForWin32Error)</exception> - /// <exception cref="IOException">Thrown if Windows returns an error that doesn't map to the above. (See Win32Marshal.GetExceptionForWin32Error)</exception> + /// <exception cref="PathTooLongException">Thrown if we have a string that is too large to fit into a UNICODE_STRING.</exception> + /// <exception cref="IOException">Thrown if the path is empty.</exception> /// <returns>Normalized path</returns> - internal static string Normalize(string path, bool checkInvalidCharacters, bool expandShortPaths) + internal static string Normalize(string path) { // Get the full path - StringBuffer fullPath = new StringBuffer(PathInternal.MaxShortPath); + StringBuffer fullPath = new StringBuffer(path.Length); try { GetFullPathName(path, ref fullPath); - // 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\. - - // As the path could be > 30K, 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(ref fullPath); - bool possibleBadUnc = specialPath && !isDevice; - int index = specialPath ? 2 : 0; - int lastSeparator = specialPath ? 1 : 0; - int segmentLength; - char current; - - while (index < fullPath.Length) - { - current = fullPath[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(SR.Argument_InvalidPathChars); - foundTilde = false; - break; - case '~': - foundTilde = true; - break; - case '\\': - segmentLength = index - lastSeparator - 1; - 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(SR.Format(SR.Arg_PathIllegalUNC_Path, fullPath.ToString())); - else - possibleBadUnc = false; - } - - break; - - default: - if (checkInvalidCharacters && current < ' ') throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path)); - break; - } - } - - index++; - } - - if (possibleBadUnc) - throw new ArgumentException(SR.Format(SR.Arg_PathIllegalUNC_Path, fullPath.ToString())); - - segmentLength = fullPath.Length - lastSeparator - 1; - - 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) + if (fullPath.Contains('~')) { return TryExpandShortFileName(ref fullPath, originalPath: path); } @@ -150,13 +39,10 @@ namespace System.IO { if (fullPath.Length == path.Length && fullPath.StartsWith(path)) { - // If we have the exact same string we were passed in, don't bother to allocate another string from the StringBuilder. + // If we have the exact same string we were passed in, don't bother to allocate another string from the StringBuffer. return path; } - else - { - return fullPath.ToString(); - } + return fullPath.ToString(); } } finally @@ -166,12 +52,6 @@ namespace System.IO } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsDosUnc(ref StringBuffer buffer) - { - return !PathInternal.IsDevice(ref buffer) && buffer.Length > 1 && buffer[0] == '\\' && buffer[1] == '\\'; - } - private static unsafe void GetFullPathName(string path, ref StringBuffer fullPath) { // If the string starts with an extended prefix we would need to remove it from the path before we call GetFullPathName as @@ -239,7 +119,7 @@ namespace System.IO // We guarantee we'll expand short names for paths that only partially exist. As such, we need to find the part of the path that actually does exist. To // avoid allocating like crazy we'll create only one input array and modify the contents with embedded nulls. - Debug.Assert(!PathInternal.IsPartiallyQualified(ref outputBuffer), "should have resolved by now"); + Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuffer.AsSpan()), "should have resolved by now"); // We'll have one of a few cases by now (the normalized path will have already: // @@ -251,8 +131,8 @@ namespace System.IO // // Note that we will never get \??\ here as GetFullPathName() does not recognize \??\ and will return it as C:\??\ (or whatever the current drive is). - int rootLength = PathInternal.GetRootLength(ref outputBuffer); - bool isDevice = PathInternal.IsDevice(ref outputBuffer); + int rootLength = PathInternal.GetRootLength(outputBuffer.AsSpan()); + bool isDevice = PathInternal.IsDevice(outputBuffer.AsSpan()); StringBuffer inputBuffer = new StringBuffer(0); try @@ -275,7 +155,7 @@ namespace System.IO } else { - isDosUnc = IsDosUnc(ref outputBuffer); + isDosUnc = !PathInternal.IsDevice(outputBuffer.AsSpan()) && outputBuffer.Length > 1 && outputBuffer[0] == '\\' && outputBuffer[1] == '\\'; rootDifference = GetInputBuffer(ref outputBuffer, isDosUnc, ref inputBuffer); } @@ -345,7 +225,7 @@ namespace System.IO string returnValue = null; - int newLength = (int)(bufferToUse.Length - rootDifference); + int newLength = bufferToUse.Length - rootDifference; if (isDosUnc) { // Need to go from \\?\UNC\ to \\?\UN\\ diff --git a/src/mscorlib/shared/System/IO/PathInternal.Windows.StringBuffer.cs b/src/mscorlib/shared/System/IO/PathInternal.Windows.StringBuffer.cs deleted file mode 100644 index ea132423b2..0000000000 --- a/src/mscorlib/shared/System/IO/PathInternal.Windows.StringBuffer.cs +++ /dev/null @@ -1,91 +0,0 @@ -// 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; -using System.Runtime.InteropServices; - -namespace System.IO -{ - /// <summary>Contains internal path helpers that are shared between many projects.</summary> - internal static partial class PathInternal - { - /// <summary> - /// Returns true if the path uses the extended syntax (\\?\) - /// </summary> - internal static bool IsExtended(ref StringBuffer path) - { - // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths. - // Skipping of normalization will *only* occur if back slashes ('\') are used. - return path.Length >= DevicePrefixLength - && path[0] == '\\' - && (path[1] == '\\' || path[1] == '?') - && path[2] == '?' - && path[3] == '\\'; - } - - /// <summary> - /// Gets the length of the root of the path (drive, share, etc.). - /// </summary> - internal static int GetRootLength(ref StringBuffer path) - { - if (path.Length == 0) - return 0; - - return GetRootLength(new ReadOnlySpan<char>(path.UnderlyingArray, 0, path.Length)); - } - - /// <summary> - /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") - /// </summary> - internal static bool IsDevice(ref StringBuffer path) - { - // If the path begins with any two separators is will be recognized and normalized and prepped with - // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not. - return IsExtended(ref path) - || - ( - path.Length >= DevicePrefixLength - && IsDirectorySeparator(path[0]) - && IsDirectorySeparator(path[1]) - && (path[2] == '.' || path[2] == '?') - && IsDirectorySeparator(path[3]) - ); - } - - /// <summary> - /// Returns true if the path specified is relative to the current drive or working directory. - /// Returns false if the path is fixed to a specific drive or UNC path. This method does no - /// validation of the path (URIs will be returned as relative as a result). - /// </summary> - /// <remarks> - /// Handles paths that use the alternate directory separator. It is a frequent mistake to - /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case. - /// "C:a" is drive relative- meaning that it will be resolved against the current directory - /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory - /// will not be used to modify the path). - /// </remarks> - internal static bool IsPartiallyQualified(ref 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] == VolumeSeparatorChar) - && IsDirectorySeparator(path[2])); - } - } -} diff --git a/src/mscorlib/shared/System/IO/PathInternal.Windows.cs b/src/mscorlib/shared/System/IO/PathInternal.Windows.cs index d4d5917bba..95bd0625d8 100644 --- a/src/mscorlib/shared/System/IO/PathInternal.Windows.cs +++ b/src/mscorlib/shared/System/IO/PathInternal.Windows.cs @@ -275,29 +275,6 @@ namespace System.IO } /// <summary> - /// Returns the characters to skip at the start of the path if it starts with space(s) and a drive or directory separator. - /// (examples are " C:", " \") - /// This is a legacy behavior of Path.GetFullPath(). - /// </summary> - /// <remarks> - /// Note that this conflicts with IsPathRooted() which doesn't (and never did) such a skip. - /// </remarks> - internal static int PathStartSkip(ReadOnlySpan<char> 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] == ':' && IsValidDriveChar(path[startIndex]))) - { - // Go ahead and skip spaces as we're either " C:" or " \" - return startIndex; - } - - return 0; - } - - /// <summary> /// True if the given character is a directory separator. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/mscorlib/shared/System/IO/Win32Marshal.cs b/src/mscorlib/shared/System/IO/Win32Marshal.cs index 14a064a700..0a8f2dca0c 100644 --- a/src/mscorlib/shared/System/IO/Win32Marshal.cs +++ b/src/mscorlib/shared/System/IO/Win32Marshal.cs @@ -69,7 +69,7 @@ namespace System.IO return new PathTooLongException(SR.Format(SR.IO_PathTooLong_Path, path)); case Interop.Errors.ERROR_INVALID_DRIVE: - throw new DriveNotFoundException(SR.Format(SR.IO_DriveNotFound_Drive, path)); + throw new DriveNotFoundException(SR.Format(SR.IO_DriveNotFound_Drive, path)); case Interop.Errors.ERROR_INVALID_PARAMETER: return new IOException(Interop.Kernel32.GetMessage(errorCode), MakeHRFromErrorCode(errorCode)); diff --git a/src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs b/src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs index fdd0b95590..42bf9cc7b7 100644 --- a/src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs +++ b/src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs @@ -297,5 +297,8 @@ namespace System.Runtime.InteropServices _buffer = null; _length = 0; } + + public ReadOnlySpan<char> AsSpan() + => new ReadOnlySpan<char>(UnderlyingArray, 0, Length); } } |