internal static void ValidateFileTypeForNonExtendedPaths(SafeFileHandle handle, string originalPath) { if (!PathInternal.IsExtended(originalPath)) { // To help avoid stumbling into opening COM/LPT ports by accident, we will block on non file handles unless // we were explicitly passed a path that has \\?\. GetFullPath() will turn paths like C:\foo\con.txt into // \\.\CON, so we'll only allow the \\?\ syntax. int fileType = handle.GetFileType(); if (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) { int errorCode = fileType == Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN ? Marshal.GetLastPInvokeError() : Interop.Errors.ERROR_SUCCESS; handle.Dispose(); if (errorCode != Interop.Errors.ERROR_SUCCESS) { throw Win32Marshal.GetExceptionForWin32Error(errorCode); } throw new NotSupportedException(SR.NotSupported_FileStreamOnNonFiles); } } }
public static string GetFullPath(string path) { if (path == null) { throw new ArgumentNullException(nameof(path)); } // If the path would normalize to string empty, we'll consider it empty if (PathInternal.IsEffectivelyEmpty(path.AsSpan())) { 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. if (path.Contains('\0')) { throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path)); } if (PathInternal.IsExtended(path.AsSpan())) { // \\?\ 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); } return(PathHelper.Normalize(path)); }
// Some Windows versions like Windows Nano Server have the %TEMP% environment variable set to "C:\TEMP" but the // actual folder name is "C:\Temp", which prevents asserting path values using Assert.Equal due to case sensitiveness. // So instead of using TestDirectory directly, we retrieve the real path with proper casing of the initial folder path. private unsafe string GetTestDirectoryActualCasing() { try { using SafeFileHandle handle = Interop.Kernel32.CreateFile( TestDirectory, dwDesiredAccess: 0, dwShareMode: FileShare.ReadWrite | FileShare.Delete, dwCreationDisposition: FileMode.Open, dwFlagsAndAttributes: OPEN_EXISTING | FILE_FLAG_BACKUP_SEMANTICS // Necessary to obtain a handle to a directory ); if (!handle.IsInvalid) { const int InitialBufferSize = 4096; char[]? buffer = ArrayPool <char> .Shared.Rent(InitialBufferSize); uint result = GetFinalPathNameByHandle(handle, buffer); // Remove extended prefix int skip = PathInternal.IsExtended(buffer) ? 4 : 0; return(new string( buffer, skip, (int)result - skip)); } } catch { } return(TestDirectory); }
public void IsExtendedTest(string path, bool expected) { StringBuffer sb = new StringBuffer(); sb.Append(path); Assert.Equal(expected, PathInternal.IsExtended(sb)); Assert.Equal(expected, PathInternal.IsExtended(path)); }
internal static string GetFullyQualifiedPath(string path) { if (PathInternal.IsExtended(path.AsSpan())) { // \\?\ 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); } return(PathHelper.Normalize(path)); }
// Gets the full path without argument validation private static string GetFullPathInternal(string path) { Debug.Assert(!string.IsNullOrEmpty(path)); Debug.Assert(!path.Contains('\0')); if (PathInternal.IsExtended(path.AsSpan())) { // \\?\ 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); } return(PathHelper.Normalize(path)); }
internal static int GetLongPathName(string path, [Out] StringBuilder longPathBuffer, int bufferLength) { bool wasExtended = PathInternal.IsExtended(path); if (!wasExtended) { path = PathInternal.EnsureExtendedPrefixOverMaxPath(path); } int result = GetLongPathNamePrivate(path, longPathBuffer, longPathBuffer.Capacity); if (!wasExtended) { // We don't want to give back \\?\ if we possibly added it ourselves PathInternal.RemoveExtendedPrefix(longPathBuffer); } return(result); }
internal static int GetFullPathName(string path, int numBufferChars, [Out] StringBuilder buffer, IntPtr mustBeZero) { bool wasExtended = PathInternal.IsExtended(path); if (!wasExtended) { path = PathInternal.EnsureExtendedPrefixOverMaxPath(path); } int result = GetFullPathNamePrivate(path, buffer.Capacity, buffer, mustBeZero); if (!wasExtended) { // We don't want to give back \\?\ if we possibly added it ourselves PathInternal.RemoveExtendedPrefix(buffer); } return(result); }
public static FullPath FromPath(string path) { if (PathInternal.IsExtended(path)) { path = path.Substring(4); } var fullPath = Path.GetFullPath(path); var fullPathWithoutTrailingDirectorySeparator = TrimEndingDirectorySeparator(fullPath); if (string.IsNullOrEmpty(fullPathWithoutTrailingDirectorySeparator)) { return(Empty); } return(new FullPath(fullPathWithoutTrailingDirectorySeparator)); }
internal static string GetFullPathInternal(string path) { if (path == null) { throw new ArgumentNullException("path"); } if (PathInternal.IsExtended(path)) { // Don't want to trim extended paths return(Path.GetFullPath(path)); } else { string pathTrimmed = path.TrimStart(TrimStartChars).TrimEnd(TrimEndChars); return(Path.GetFullPath(Path.IsPathRooted(pathTrimmed) ? pathTrimmed : path)); } }
internal static string GetSingleSymbolicLinkTarget(string path) { using (SafeFileHandle handle = Interop.Kernel32.CreateFile(path, 0, // No file access required, this avoids file in use FileShare.ReadWrite | FileShare.Delete, // Share all access FileMode.Open, Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT | // Open the reparse point, not its target Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS)) // Permit opening of directories { // https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/fsctl-get-reparse-point Interop.Kernel32.REPARSE_DATA_BUFFER_SYMLINK header; int sizeHeader = Marshal.SizeOf <Interop.Kernel32.REPARSE_DATA_BUFFER_SYMLINK>(); uint bytesRead = 0; ReadOnlySpan <byte> validBuffer; int bufferSize = sizeHeader + Interop.Kernel32.MAX_PATH; while (true) { byte[] buffer = ArrayPool <byte> .Shared.Rent(bufferSize); try { int result = Interop.Kernel32.DeviceIoControl(handle, Interop.Kernel32.FSCTL_GET_REPARSE_POINT, inBuffer: null, cbInBuffer: 0, buffer, (uint)buffer.Length, out bytesRead, overlapped: IntPtr.Zero) ? 0 : Marshal.GetLastWin32Error(); if (result != Interop.Errors.ERROR_SUCCESS && result != Interop.Errors.ERROR_INSUFFICIENT_BUFFER && result != Interop.Errors.ERROR_MORE_DATA) { throw new Win32Exception(result); } validBuffer = buffer.AsSpan().Slice(0, (int)bytesRead); if (!MemoryMarshal.TryRead(validBuffer, out header)) { if (result == Interop.Errors.ERROR_SUCCESS) { // didn't read enough for header throw new InvalidDataException("FSCTL_GET_REPARSE_POINT did not return sufficient data"); } // can't read header, guess at buffer length buffer = new byte[buffer.Length + Interop.Kernel32.MAX_PATH]; continue; } // we only care about SubstituteName. // Per https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/b41f1cbf-10df-4a47-98d4-1c52a833d913 print name is only valid for displaying to the user bufferSize = sizeHeader + header.SubstituteNameOffset + header.SubstituteNameLength; // bufferSize = sizeHeader + Math.Max(header.SubstituteNameOffset + header.SubstituteNameLength, header.PrintNameOffset + header.PrintNameLength); if (bytesRead >= bufferSize) { // got entire payload with valid header. #if NETSTANDARD2_0 string target = Encoding.Unicode.GetString(validBuffer.Slice(sizeHeader + header.SubstituteNameOffset, header.SubstituteNameLength).ToArray()); #elif NETCOREAPP3_1 || NET5_0 string target = Encoding.Unicode.GetString(validBuffer.Slice(sizeHeader + header.SubstituteNameOffset, header.SubstituteNameLength)); #else #error Platform not supported #endif if ((header.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0) { if (PathInternal.IsExtended(path)) { var rootPath = Path.GetDirectoryName(path.Substring(4)); if (rootPath != null) { target = path.Substring(0, 4) + Path.GetFullPath(Path.Combine(rootPath, target)); } else { target = path.Substring(0, 4) + Path.GetFullPath(target); } } else { var rootPath = Path.GetDirectoryName(path); if (rootPath != null) { target = Path.GetFullPath(Path.Combine(rootPath, target)); } else { target = Path.GetFullPath(target); } } } return(target); } if (bufferSize < buffer.Length) { throw new InvalidDataException($"FSCTL_GET_REPARSE_POINT did not return sufficient data ({bufferSize}) when provided buffer ({buffer.Length})."); } } finally { ArrayPool <byte> .Shared.Return(buffer); } } } }
private static unsafe string?GetFinalLinkTarget(string linkPath, bool isDirectory) { Interop.Kernel32.WIN32_FIND_DATA data = default; GetFindData(linkPath, isDirectory, ignoreAccessDenied: false, ref data); // The file or directory is not a reparse point. if ((data.dwFileAttributes & (uint)FileAttributes.ReparsePoint) == 0 || // Only symbolic links are supported at the moment. (data.dwReserved0 & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0) { return(null); } // We try to open the final file since they asked for the final target. using SafeFileHandle handle = OpenSafeFileHandle(linkPath, Interop.Kernel32.FileOperations.OPEN_EXISTING | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS); if (handle.IsInvalid) { // If the handle fails because it is unreachable, is because the link was broken. // We need to fallback to manually traverse the links and return the target of the last resolved link. int error = Marshal.GetLastWin32Error(); if (IsPathUnreachableError(error)) { return(GetFinalLinkTargetSlow(linkPath)); } throw Win32Marshal.GetExceptionForWin32Error(error, linkPath); } const int InitialBufferSize = 4096; char[] buffer = ArrayPool <char> .Shared.Rent(InitialBufferSize); try { uint result = GetFinalPathNameByHandle(handle, buffer); // If the function fails because lpszFilePath is too small to hold the string plus the terminating null character, // the return value is the required buffer size, in TCHARs. This value includes the size of the terminating null character. if (result > buffer.Length) { char[] toReturn = buffer; buffer = ArrayPool <char> .Shared.Rent((int)result); ArrayPool <char> .Shared.Return(toReturn); result = GetFinalPathNameByHandle(handle, buffer); } // If the function fails for any other reason, the return value is zero. if (result == 0) { throw Win32Marshal.GetExceptionForLastWin32Error(linkPath); } Debug.Assert(PathInternal.IsExtended(new string(buffer, 0, (int)result).AsSpan())); // GetFinalPathNameByHandle always returns with extended DOS prefix even if the link target was created without one. // While this does not interfere with correct behavior, it might be unexpected. // Hence we trim it if the passed-in path to the link wasn't extended. int start = PathInternal.IsExtended(linkPath.AsSpan()) ? 0 : 4; return(new string(buffer, start, (int)result - start)); } finally { ArrayPool <char> .Shared.Return(buffer); } uint GetFinalPathNameByHandle(SafeFileHandle handle, char[] buffer) { fixed(char *bufPtr = buffer) { return(Interop.Kernel32.GetFinalPathNameByHandle(handle, bufPtr, (uint)buffer.Length, Interop.Kernel32.FILE_NAME_NORMALIZED)); } } string?GetFinalLinkTargetSlow(string linkPath) { // Since all these paths will be passed to CreateFile, which takes a string anyway, it is pointless to use span. // I am not sure if it's possible to change CreateFile's param to ROS<char> and avoid all these allocations. // We don't throw on error since we already did all the proper validations before. string?current = GetImmediateLinkTarget(linkPath, isDirectory, throwOnError: false, returnFullPath: true); string?prev = null; while (current != null) { prev = current; current = GetImmediateLinkTarget(current, isDirectory, throwOnError: false, returnFullPath: true); } return(prev); } }
/// <summary> /// Gets reparse point information associated to <paramref name="linkPath"/>. /// </summary> /// <returns>The immediate link target, absolute or relative or null if the file is not a supported link.</returns> internal static unsafe string?GetImmediateLinkTarget(string linkPath, bool isDirectory, bool throwOnError, bool returnFullPath) { using SafeFileHandle handle = OpenSafeFileHandle(linkPath, Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS | Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT); if (handle.IsInvalid) { if (!throwOnError) { return(null); } int error = Marshal.GetLastWin32Error(); // File not found doesn't make much sense coming from a directory. if (isDirectory && error == Interop.Errors.ERROR_FILE_NOT_FOUND) { error = Interop.Errors.ERROR_PATH_NOT_FOUND; } throw Win32Marshal.GetExceptionForWin32Error(error, linkPath); } byte[] buffer = ArrayPool <byte> .Shared.Rent(Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); try { bool success = Interop.Kernel32.DeviceIoControl( handle, dwIoControlCode: Interop.Kernel32.FSCTL_GET_REPARSE_POINT, lpInBuffer: IntPtr.Zero, nInBufferSize: 0, lpOutBuffer: buffer, nOutBufferSize: Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE, out _, IntPtr.Zero); if (!success) { if (!throwOnError) { return(null); } int error = Marshal.GetLastWin32Error(); // The file or directory is not a reparse point. if (error == Interop.Errors.ERROR_NOT_A_REPARSE_POINT) { return(null); } throw Win32Marshal.GetExceptionForWin32Error(error, linkPath); } Span <byte> bufferSpan = new(buffer); success = MemoryMarshal.TryRead(bufferSpan, out Interop.Kernel32.REPARSE_DATA_BUFFER rdb); Debug.Assert(success); // Only symbolic links are supported at the moment. if ((rdb.ReparseTag & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0) { return(null); } // We use PrintName instead of SubstitutneName given that we don't want to return a NT path when the link wasn't created with such NT path. // Unlike SubstituteName and GetFinalPathNameByHandle(), PrintName doesn't start with a prefix. // Another nuance is that SubstituteName does not contain redundant path segments while PrintName does. // PrintName can ONLY return a NT path if the link was created explicitly targeting a file/folder in such way. e.g: mklink /D linkName \??\C:\path\to\target. int printNameNameOffset = sizeof(Interop.Kernel32.REPARSE_DATA_BUFFER) + rdb.ReparseBufferSymbolicLink.PrintNameOffset; int printNameNameLength = rdb.ReparseBufferSymbolicLink.PrintNameLength; Span <char> targetPath = MemoryMarshal.Cast <byte, char>(bufferSpan.Slice(printNameNameOffset, printNameNameLength)); Debug.Assert((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || !PathInternal.IsExtended(targetPath)); if (returnFullPath && (rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0) { // Target path is relative and is for ResolveLinkTarget(), we need to append the link directory. return(Path.Join(Path.GetDirectoryName(linkPath.AsSpan()), targetPath)); } return(targetPath.ToString()); } finally { ArrayPool <byte> .Shared.Return(buffer); } }
public void IsExtendedTest(string path, bool expected) { Assert.Equal(expected, PathInternal.IsExtended(path)); }
public static FullPath FromPath(string path) { if (PathInternal.IsExtended(path)) { path = path[4..];