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);
                }
            }
        }
示例#2
0
        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));
        }
示例#3
0
        // 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);
        }
示例#4
0
    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));
    }
示例#5
0
        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));
        }
示例#6
0
        // 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);
        }
示例#8
0
        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));
            }
        }
示例#11
0
            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);
                        }
                    }
                }
            }
示例#12
0
        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);
            }
        }
示例#13
0
        /// <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);
            }
        }
示例#14
0
 public void IsExtendedTest(string path, bool expected)
 {
     Assert.Equal(expected, PathInternal.IsExtended(path));
 }
示例#15
0
 public static FullPath FromPath(string path)
 {
     if (PathInternal.IsExtended(path))
     {
         path = path[4..];