/
githubmirror
/
PowerShell
Обзор
Документация
Войти
/
githubmirror
/
PowerShell
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/System.Management.Automation/utils/PathUtils.cs
907 строк
37 KB
Armaan Mcleod
Update `IndexOfAny` calls with invalid path/filename to `SearchValues<char>` for more efficient char searching (#24896)
30 янв 2025, 15:42
Не верифицирован
30 янв 2025, 15:42
2a1d17e
Код
Авторство
О чём код?
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Buffers; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Management.Automation.Internal; using System.Management.Automation.Runspaces; using System.Runtime.CompilerServices; using System.Text; using Microsoft.PowerShell.Commands; using Dbg = System.Management.Automation.Diagnostics; namespace System.Management.Automation { /// <summary> /// Defines generic utilities and helper methods for PowerShell. /// </summary> internal static class PathUtils { /// <summary> /// THE method for opening a file for writing. /// Should be used by all cmdlets that write to a file. /// </summary> /// <param name="cmdlet">Cmdlet that is opening the file (used mainly for error reporting).</param> /// <param name="filePath">Path to the file (as specified on the command line - this method will resolve the path).</param> /// <param name="encoding">Encoding (this method will convert the command line string to an Encoding instance).</param> /// <param name="defaultEncoding">If <see langword="true"/>, then we will use default .NET encoding instead of the encoding specified in <paramref name="encoding"/> parameter.</param> /// <param name="Append"></param> /// <param name="Force"></param> /// <param name="NoClobber"></param> /// <param name="fileStream">Result1: <see cref="FileStream"/> opened for writing.</param> /// <param name="streamWriter">Result2: <see cref="StreamWriter"/> (inherits from <see cref="TextWriter"/>) opened for writing.</param> /// <param name="readOnlyFileInfo">Result3: file info that should be used to restore file attributes after done with the file (<see langword="null"/> is this is not needed).</param> /// <param name="isLiteralPath">True if wildcard expansion should be bypassed.</param> internal static void MasterStreamOpen( PSCmdlet cmdlet, string filePath, string encoding, bool defaultEncoding, bool Append, bool Force, bool NoClobber, out FileStream fileStream, out StreamWriter streamWriter, out FileInfo readOnlyFileInfo, bool isLiteralPath ) { Encoding resolvedEncoding = EncodingConversion.Convert(cmdlet, encoding); MasterStreamOpen(cmdlet, filePath, resolvedEncoding, defaultEncoding, Append, Force, NoClobber, out fileStream, out streamWriter, out readOnlyFileInfo, isLiteralPath); } /// <summary> /// THE method for opening a file for writing. /// Should be used by all cmdlets that write to a file. /// </summary> /// <param name="cmdlet">Cmdlet that is opening the file (used mainly for error reporting).</param> /// <param name="filePath">Path to the file (as specified on the command line - this method will resolve the path).</param> /// <param name="resolvedEncoding">Encoding (this method will convert the command line string to an Encoding instance).</param> /// <param name="defaultEncoding">If <see langword="true"/>, then we will use default .NET encoding instead of the encoding specified in <paramref name="encoding"/> parameter.</param> /// <param name="Append"></param> /// <param name="Force"></param> /// <param name="NoClobber"></param> /// <param name="fileStream">Result1: <see cref="FileStream"/> opened for writing.</param> /// <param name="streamWriter">Result2: <see cref="StreamWriter"/> (inherits from <see cref="TextWriter"/>) opened for writing.</param> /// <param name="readOnlyFileInfo">Result3: file info that should be used to restore file attributes after done with the file (<see langword="null"/> is this is not needed).</param> /// <param name="isLiteralPath">True if wildcard expansion should be bypassed.</param> internal static void MasterStreamOpen( PSCmdlet cmdlet, string filePath, Encoding resolvedEncoding, bool defaultEncoding, bool Append, bool Force, bool NoClobber, out FileStream fileStream, out StreamWriter streamWriter, out FileInfo readOnlyFileInfo, bool isLiteralPath) { fileStream = null; streamWriter = null; readOnlyFileInfo = null; string resolvedPath = ResolveFilePath(filePath, cmdlet, isLiteralPath); try { MasterStreamOpenImpl( resolvedPath, resolvedEncoding, defaultEncoding, Append, Force, NoClobber, out fileStream, out streamWriter, out readOnlyFileInfo); } // These are the known exceptions for File.Load and StreamWriter.ctor catch (ArgumentException e) { // NOTE: this call will throw ReportFileOpenFailure(cmdlet, resolvedPath, e); } catch (IOException e) { if (NoClobber && File.Exists(resolvedPath)) { // This probably happened because the file already exists ErrorRecord errorRecord = new( e, "NoClobber", ErrorCategory.ResourceExists, resolvedPath); errorRecord.ErrorDetails = new ErrorDetails( cmdlet, "PathUtilsStrings", "UtilityFileExistsNoClobber", filePath, "NoClobber"); // prevents localization // NOTE: this call will throw cmdlet.ThrowTerminatingError(errorRecord); } // NOTE: this call will throw ReportFileOpenFailure(cmdlet, resolvedPath, e); } catch (UnauthorizedAccessException e) { // NOTE: this call will throw ReportFileOpenFailure(cmdlet, resolvedPath, e); } catch (NotSupportedException e) { // NOTE: this call will throw ReportFileOpenFailure(cmdlet, resolvedPath, e); } catch (System.Security.SecurityException e) { // NOTE: this call will throw ReportFileOpenFailure(cmdlet, resolvedPath, e); } } /// <summary> /// THE method for opening a file for writing. /// Should be used by all cmdlets that write to a file. /// </summary> /// <param name="filePath">Path to the file (as specified on the command line - this method will resolve the path).</param> /// <param name="resolvedEncoding">Encoding (this method will convert the command line string to an Encoding instance).</param> /// <param name="defaultEncoding">If <see langword="true"/>, then we will use default .NET encoding instead of the encoding specified in <paramref name="encoding"/> parameter.</param> /// <param name="Append"></param> /// <param name="Force"></param> /// <param name="NoClobber"></param> /// <param name="fileStream">Result1: <see cref="FileStream"/> opened for writing.</param> /// <param name="streamWriter">Result2: <see cref="StreamWriter"/> (inherits from <see cref="TextWriter"/>) opened for writing.</param> /// <param name="readOnlyFileInfo">Result3: file info that should be used to restore file attributes after done with the file (<see langword="null"/> is this is not needed).</param> /// <param name="isLiteralPath">True if wildcard expansion should be bypassed.</param> internal static void MasterStreamOpen( string filePath, Encoding resolvedEncoding, bool defaultEncoding, bool Append, bool Force, bool NoClobber, out FileStream fileStream, out StreamWriter streamWriter, out FileInfo readOnlyFileInfo, bool isLiteralPath) { fileStream = null; streamWriter = null; readOnlyFileInfo = null; string resolvedPath = ResolveFilePath(filePath, isLiteralPath); try { MasterStreamOpenImpl( resolvedPath, resolvedEncoding, defaultEncoding, Append, Force, NoClobber, out fileStream, out streamWriter, out readOnlyFileInfo); } // These are the known exceptions for File.Load and StreamWriter.ctor catch (ArgumentException e) { AddFileOpenErrorRecord(e); throw; } catch (IOException e) { if (NoClobber && File.Exists(resolvedPath)) { string msg = StringUtil.Format( PathUtilsStrings.UtilityFileExistsNoClobber, filePath, "NoClobber"); // This probably happened because the file already exists ErrorRecord errorRecord = new ErrorRecord( e, "NoClobber", ErrorCategory.ResourceExists, resolvedPath); errorRecord.ErrorDetails = new ErrorDetails(msg); e.Data[typeof(ErrorRecord)] = errorRecord; throw; } AddFileOpenErrorRecord(e); throw; } catch (UnauthorizedAccessException e) { AddFileOpenErrorRecord(e); throw; } catch (NotSupportedException e) { AddFileOpenErrorRecord(e); throw; } catch (System.Security.SecurityException e) { AddFileOpenErrorRecord(e); throw; } static void AddFileOpenErrorRecord(Exception e) { ErrorRecord errorRecord = new ErrorRecord( e, "FileOpenFailure", ErrorCategory.OpenError, null); e.Data[typeof(ErrorRecord)] = errorRecord; } } /// <summary> /// THE method for opening a file for writing. /// Should be used by all cmdlets that write to a file. /// </summary> /// <param name="resolvedPath">Path to the file (as specified on the command line - this method will resolve the path).</param> /// <param name="resolvedEncoding">Encoding (this method will convert the command line string to an Encoding instance).</param> /// <param name="defaultEncoding">If <see langword="true"/>, then we will use default .NET encoding instead of the encoding specified in <paramref name="encoding"/> parameter.</param> /// <param name="Append"></param> /// <param name="Force"></param> /// <param name="NoClobber"></param> /// <param name="fileStream">Result1: <see cref="FileStream"/> opened for writing.</param> /// <param name="streamWriter">Result2: <see cref="StreamWriter"/> (inherits from <see cref="TextWriter"/>) opened for writing.</param> /// <param name="readOnlyFileInfo">Result3: file info that should be used to restore file attributes after done with the file (<see langword="null"/> is this is not needed).</param> internal static void MasterStreamOpenImpl( string resolvedPath, Encoding resolvedEncoding, bool defaultEncoding, bool Append, bool Force, bool NoClobber, out FileStream fileStream, out StreamWriter streamWriter, out FileInfo readOnlyFileInfo) { fileStream = null; streamWriter = null; readOnlyFileInfo = null; // variable to track file open mode // this is controlled by append/force parameters FileMode mode = FileMode.Create; if (Append) { mode = FileMode.Append; } else if (NoClobber) { // throw IOException if file exists mode = FileMode.CreateNew; } if (Force && (Append || !NoClobber)) { if (File.Exists(resolvedPath)) { FileInfo fInfo = new FileInfo(resolvedPath); if ((fInfo.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) { // remember to reset the read-only attribute later readOnlyFileInfo = fInfo; // Clear the read-only attribute fInfo.Attributes &= ~(FileAttributes.ReadOnly); } } } // if the user knows what he/she is doing and uses "-Force" switch, // then we let more than 1 process write to the same file at the same time FileShare fileShare = Force ? FileShare.ReadWrite : FileShare.Read; // mode is controlled by force and ShouldContinue() fileStream = new FileStream(resolvedPath, mode, FileAccess.Write, fileShare); // create stream writer // NTRAID#Windows Out Of Band Releases-931008-2006/03/27 // For some reason, calling this without specifying // the encoding is different from passing Encoding.Default. if (defaultEncoding) streamWriter = new StreamWriter(fileStream); else streamWriter = new StreamWriter(fileStream, resolvedEncoding); } internal static void ReportFileOpenFailure(Cmdlet cmdlet, string filePath, Exception e) { ErrorRecord errorRecord = new ErrorRecord( e, "FileOpenFailure", ErrorCategory.OpenError, null); cmdlet.ThrowTerminatingError(errorRecord); } internal static void ReportFileOpenFailure(string filePath, Exception e) { ErrorRecord errorRecord = new ErrorRecord( e, "FileOpenFailure", ErrorCategory.OpenError, null); throw new RuntimeException( e.Message, errorRecord.Exception, errorRecord); } internal static StreamReader OpenStreamReader(PSCmdlet command, string filePath, Encoding encoding, bool isLiteralPath) { FileStream fileStream = OpenFileStream(filePath, command, isLiteralPath); return new StreamReader(fileStream, encoding); } internal static FileStream OpenFileStream(string filePath, PSCmdlet command, bool isLiteralPath) { string resolvedPath = PathUtils.ResolveFilePath(filePath, command, isLiteralPath); try { return new FileStream(resolvedPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); } // These are the known exceptions for FileStream.ctor catch (ArgumentException e) { PathUtils.ReportFileOpenFailure(command, filePath, e); return null; // the line above will throw - silencing the compiler } catch (IOException e) { PathUtils.ReportFileOpenFailure(command, filePath, e); return null; // the line above will throw - silencing the compiler } catch (UnauthorizedAccessException e) { PathUtils.ReportFileOpenFailure(command, filePath, e); return null; // the line above will throw - silencing the compiler } catch (NotSupportedException e) { PathUtils.ReportFileOpenFailure(command, filePath, e); return null; // the line above will throw - silencing the compiler } catch (System.Management.Automation.DriveNotFoundException e) { PathUtils.ReportFileOpenFailure(command, filePath, e); return null; // the line above will throw - silencing the compiler } } /// <summary> /// Resolve a user provided file name or path (including globbing characters) /// to a fully qualified file path, using the file system provider. /// </summary> /// <param name="filePath"></param> /// <param name="command"></param> /// <returns></returns> internal static string ResolveFilePath(string filePath, PSCmdlet command) { return ResolveFilePath(filePath, command, false); } /// <summary> /// Resolve a user provided file name or path (including globbing characters) /// to a fully qualified file path, using the file system provider. /// </summary> /// <param name="filePath"></param> /// <param name="command"></param> /// <param name="isLiteralPath"></param> /// <returns></returns> internal static string ResolveFilePath(string filePath, PSCmdlet command, bool isLiteralPath) { string path = null; try { ProviderInfo provider = null; PSDriveInfo drive = null; List<string> filePaths = new List<string>(); if (isLiteralPath) { filePaths.Add(command.SessionState.Path.GetUnresolvedProviderPathFromPSPath(filePath, out provider, out drive)); } else { filePaths.AddRange(command.SessionState.Path.GetResolvedProviderPathFromPSPath(filePath, out provider)); } if (!provider.NameEquals(command.Context.ProviderNames.FileSystem)) { ReportWrongProviderType(command, provider.FullName); } if (filePaths.Count > 1) { ReportMultipleFilesNotSupported(command); } if (filePaths.Count == 0) { ReportWildcardingFailure(command, filePath); } path = filePaths[0]; } catch (ItemNotFoundException) { path = null; } if (string.IsNullOrEmpty(path)) { CmdletProviderContext cmdletProviderContext = new CmdletProviderContext(command); ProviderInfo provider = null; PSDriveInfo drive = null; path = command.SessionState.Path.GetUnresolvedProviderPathFromPSPath( filePath, cmdletProviderContext, out provider, out drive); cmdletProviderContext.ThrowFirstErrorOrDoNothing(); if (!provider.NameEquals(command.Context.ProviderNames.FileSystem)) { ReportWrongProviderType(command, provider.FullName); } } return path; } /// <summary> /// Resolve a user provided file name or path (including globbing characters) /// to a fully qualified file path, using the file system provider. /// </summary> /// <param name="filePath"></param> /// <param name="isLiteralPath"></param> /// <returns></returns> internal static string ResolveFilePath(string filePath, bool isLiteralPath) { string path = null; SessionState sessionState = LocalPipeline.GetExecutionContextFromTLS()?.EngineSessionState?.PublicSessionState; if (sessionState is null) { return null; } try { ProviderInfo provider = null; PSDriveInfo drive = null; List<string> filePaths = new(); if (isLiteralPath) { filePaths.Add(sessionState.Path.GetUnresolvedProviderPathFromPSPath(filePath, out provider, out drive)); } else { filePaths.AddRange(sessionState.Path.GetResolvedProviderPathFromPSPath(filePath, out provider)); } if (!provider.NameEquals(FileSystemProvider.ProviderName)) { ReportWrongProviderType(provider.FullName); } if (filePaths.Count > 1) { ReportMultipleFilesNotSupported(); } if (filePaths.Count == 0) { ReportWildcardingFailure(filePath); } path = filePaths[0]; } catch (ItemNotFoundException) { path = null; } return path; } internal static void ReportWrongProviderType(Cmdlet cmdlet, string providerId) { string msg = StringUtil.Format(PathUtilsStrings.OutFile_ReadWriteFileNotFileSystemProvider, providerId); ErrorRecord errorRecord = new ErrorRecord( PSTraceSource.NewInvalidOperationException(), "ReadWriteFileNotFileSystemProvider", ErrorCategory.InvalidArgument, null); errorRecord.ErrorDetails = new ErrorDetails(msg); cmdlet.ThrowTerminatingError(errorRecord); } internal static void ReportWrongProviderType(string providerId) { string msg = StringUtil.Format(PathUtilsStrings.OutFile_ReadWriteFileNotFileSystemProvider, providerId); PSInvalidOperationException exception = PSTraceSource.NewInvalidOperationException(); ErrorRecord errorRecord = new( exception, "ReadWriteFileNotFileSystemProvider", ErrorCategory.InvalidArgument, null); errorRecord.ErrorDetails = new ErrorDetails(msg); exception.Data[typeof(ErrorRecord)] = errorRecord; throw exception; } internal static void ReportMultipleFilesNotSupported(Cmdlet cmdlet) { string msg = StringUtil.Format(PathUtilsStrings.OutFile_MultipleFilesNotSupported); ErrorRecord errorRecord = new ErrorRecord( PSTraceSource.NewInvalidOperationException(), "ReadWriteMultipleFilesNotSupported", ErrorCategory.InvalidArgument, null); errorRecord.ErrorDetails = new ErrorDetails(msg); cmdlet.ThrowTerminatingError(errorRecord); } internal static void ReportMultipleFilesNotSupported() { string msg = StringUtil.Format(PathUtilsStrings.OutFile_MultipleFilesNotSupported); PSInvalidOperationException exception = PSTraceSource.NewInvalidOperationException(); ErrorRecord errorRecord = new( exception, "ReadWriteMultipleFilesNotSupported", ErrorCategory.InvalidArgument, null); errorRecord.ErrorDetails = new ErrorDetails(msg); exception.Data[typeof(ErrorRecord)] = errorRecord; throw exception; } internal static void ReportWildcardingFailure(Cmdlet cmdlet, string filePath) { string msg = StringUtil.Format(PathUtilsStrings.OutFile_DidNotResolveFile, filePath); ErrorRecord errorRecord = new ErrorRecord( new FileNotFoundException(), "FileOpenFailure", ErrorCategory.OpenError, filePath); errorRecord.ErrorDetails = new ErrorDetails(msg); cmdlet.ThrowTerminatingError(errorRecord); } internal static void ReportWildcardingFailure(string filePath) { string msg = StringUtil.Format(PathUtilsStrings.OutFile_DidNotResolveFile, filePath); FileNotFoundException exception = new(); ErrorRecord errorRecord = new( exception, "FileOpenFailure", ErrorCategory.OpenError, filePath); errorRecord.ErrorDetails = new ErrorDetails(msg); exception.Data[typeof(ErrorRecord)] = errorRecord; throw exception; } internal static DirectoryInfo CreateModuleDirectory(PSCmdlet cmdlet, string moduleNameOrPath, bool force) { Dbg.Assert(cmdlet != null, "Caller should verify cmdlet != null"); Dbg.Assert(!string.IsNullOrEmpty(moduleNameOrPath), "Caller should verify !string.IsNullOrEmpty(moduleNameOrPath)"); DirectoryInfo directoryInfo = null; try { // Even if 'moduleNameOrPath' is a rooted path, 'ResolveRootedFilePath' may return null when the path doesn't exist yet, // or when it contains wildcards but cannot be resolved to a single path. string rootedPath = ModuleCmdletBase.ResolveRootedFilePath(moduleNameOrPath, cmdlet.Context); if (string.IsNullOrEmpty(rootedPath) && moduleNameOrPath.StartsWith('.')) { PathInfo currentPath = cmdlet.CurrentProviderLocation(cmdlet.Context.ProviderNames.FileSystem); rootedPath = Path.Combine(currentPath.ProviderPath, moduleNameOrPath); } if (string.IsNullOrEmpty(rootedPath)) { if (Path.IsPathRooted(moduleNameOrPath)) { rootedPath = moduleNameOrPath; } else { string personalModuleRoot = ModuleIntrinsics.GetPersonalModulePath(); if (string.IsNullOrEmpty(personalModuleRoot)) { cmdlet.ThrowTerminatingError( new ErrorRecord( new ArgumentException(StringUtil.Format(PathUtilsStrings.ExportPSSession_ErrorModuleNameOrPath, moduleNameOrPath)), "ExportPSSession_ErrorModuleNameOrPath", ErrorCategory.InvalidArgument, cmdlet)); } rootedPath = Path.Combine(personalModuleRoot, moduleNameOrPath); } } directoryInfo = new DirectoryInfo(rootedPath); if (directoryInfo.Exists) { if (!force) { string errorMessage = string.Format( CultureInfo.InvariantCulture, // directory name should be treated as culture-invariant PathUtilsStrings.ExportPSSession_ErrorDirectoryExists, directoryInfo.FullName); ErrorDetails details = new ErrorDetails(errorMessage); ErrorRecord errorRecord = new ErrorRecord( new ArgumentException(details.Message), "ExportProxyCommand_OutputDirectoryExists", ErrorCategory.ResourceExists, directoryInfo); cmdlet.ThrowTerminatingError(errorRecord); } } else { directoryInfo.Create(); } } catch (Exception e) { string errorMessage = string.Format( CultureInfo.InvariantCulture, // directory name should be treated as culture-invariant PathUtilsStrings.ExportPSSession_CannotCreateOutputDirectory, moduleNameOrPath, e.Message); ErrorDetails details = new ErrorDetails(errorMessage); ErrorRecord errorRecord = new ErrorRecord( new ArgumentException(details.Message, e), "ExportProxyCommand_CannotCreateOutputDirectory", ErrorCategory.ResourceExists, moduleNameOrPath); cmdlet.ThrowTerminatingError(errorRecord); } return directoryInfo; } internal static DirectoryInfo CreateTemporaryDirectory() { DirectoryInfo temporaryDirectory = new DirectoryInfo(Path.GetTempPath()); DirectoryInfo moduleDirectory; do { moduleDirectory = new DirectoryInfo( Path.Combine( temporaryDirectory.FullName, string.Format( null, "tmp_{0}", Path.GetRandomFileName()))); } while (moduleDirectory.Exists); Directory.CreateDirectory(moduleDirectory.FullName); return new DirectoryInfo(moduleDirectory.FullName); } internal static bool TryDeleteFile(string filepath) { if (IO.File.Exists(filepath)) { try { IO.File.Delete(filepath); return true; } catch (IOException) { // file is in use on Windows } catch (UnauthorizedAccessException) { // user does not have permissions } } return false; } #region Helpers for long paths from .Net Runtime // Code here is copied from .NET's internal path helper implementation: // https://github.com/dotnet/runtime/blob/dcce0f56e10f5ac9539354b049341a2d7c0cdebf/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs // It has been left as a verbatim copy. #nullable enable /// <summary> /// Adds the extended path prefix (\\?\) if not already a device path, IF the path is not relative, /// AND the path is more than 259 characters. (> MAX_PATH + null). This will also insert the extended /// prefix if the path ends with a period or a space. Trailing periods and spaces are normally eaten /// away from paths during normalization, but if we see such a path at this point it should be /// normalized and has retained the final characters. (Typically from one of the *Info classes). /// </summary> /// <param name="path">File path.</param> /// <returns>File path (with extended prefix if the path is long path).</returns> [return: NotNullIfNotNull(nameof(path))] internal static string? EnsureExtendedPrefixIfNeeded(string? path) { if (path != null && (path.Length >= MaxShortPath || EndsWithPeriodOrSpace(path))) { return EnsureExtendedPrefix(path); } else { return path; } } internal static string EnsureExtendedPrefix(string path) { if (IsPartiallyQualified(path) || IsDevice(path)) return path; // Given \\server\share in longpath becomes \\?\UNC\server\share if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase)) return path.Insert(2, UncDevicePrefixToInsert); return ExtendedDevicePathPrefix + path; } private const string ExtendedDevicePathPrefix = @"\\?\"; private const string UncPathPrefix = @"\\"; private const string UncDevicePrefixToInsert = @"?\UNC\"; private const string UncExtendedPathPrefix = @"\\?\UNC\"; private const string DevicePathPrefix = @"\\.\"; private const int MaxShortPath = 260; // \\?\, \\.\, \??\ private const int DevicePrefixLength = 4; private static bool EndsWithPeriodOrSpace(string? path) { if (string.IsNullOrEmpty(path)) { return false; } char c = path[path.Length - 1]; return c == ' ' || c == '.'; } /// <summary> /// Returns true if the given character is a valid drive letter /// </summary> private static bool IsValidDriveChar(char value) { return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z')); } private static bool IsDevice(string path) { return IsExtended(path) || ( path.Length >= DevicePrefixLength && IsDirectorySeparator(path[0]) && IsDirectorySeparator(path[1]) && (path[2] == '.' || path[2] == '?') && IsDirectorySeparator(path[3]) ); } private static bool IsExtended(string path) { return path.Length >= DevicePrefixLength && path[0] == '\\' && (path[1] == '\\' || path[1] == '?') && path[2] == '?' && 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> private static bool IsPartiallyQualified(string path) { if (path.Length < 2) { // It isn't fixed, it must be relative. There is no way to specify a fixed // path with one character (or less). return true; } if (IsDirectorySeparator(path[0])) { // There is no valid way to specify a relative path with two initial slashes or // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ return !(path[1] == '?' || IsDirectorySeparator(path[1])); } // The only way to specify a fixed path that doesn't begin with two slashes // is the drive, colon, slash format- i.e. C:\ return !((path.Length >= 3) && (path[1] == Path.VolumeSeparatorChar) && IsDirectorySeparator(path[2]) // To match old behavior we'll check the drive character for validity as the path is technically // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. && IsValidDriveChar(path[0])); } /// <summary> /// True if the given character is a directory separator. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsDirectorySeparator(char c) { return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; } #endregion #region Helpers for checking invalid paths using SearchValues /// <summary> /// Contains characters that are invalid in file names. /// </summary> private static readonly SearchValues<char> s_invalidFileNameChars = SearchValues.Create(Path.GetInvalidFileNameChars()); /// <summary> /// Contains characters that are invalid in path names. /// </summary> private static readonly SearchValues<char> s_invalidPathChars = SearchValues.Create(Path.GetInvalidPathChars()); /// <summary> /// Checks if the specified filename contains any characters that are invalid in file names. /// </summary> /// <param name="filename">The path to check.</param> /// <returns>True if the filename contains invalid file name characters, otherwise false.</returns> internal static bool ContainsInvalidFileNameChars(ReadOnlySpan<char> filename) => filename.ContainsAny(s_invalidFileNameChars); /// <summary> /// Checks if the specified path contains any characters that are invalid in path names. /// </summary> /// <param name="path">The path to check.</param> /// <returns>True if the path contains invalid path characters, otherwise false.</returns> internal static bool ContainsInvalidPathChars(ReadOnlySpan<char> path) => path.ContainsAny(s_invalidPathChars); #endregion } }