diff --git a/TruePath/DiskUtils.cs b/TruePath/DiskUtils.cs index aed0891..cac9300 100644 --- a/TruePath/DiskUtils.cs +++ b/TruePath/DiskUtils.cs @@ -1,11 +1,14 @@ -// SPDX-FileCopyrightText: 2024 TruePath contributors +// SPDX-FileCopyrightText: 2024-2026 TruePath contributors // // SPDX-License-Identifier: MIT using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +using Windows.Wdk.Storage.FileSystem; +using Windows.Win32; +using Windows.Win32.Storage.FileSystem; using Microsoft.Win32.SafeHandles; -using static TruePath.Kernel32; namespace TruePath; @@ -20,6 +23,8 @@ internal static class DiskUtils /// private const short WindowsMaxPath = short.MaxValue; + private const string WindowsOnlyJustification = "Only called on Windows, which is checked by the callers."; + /// /// Determines whether the specified path is a junction (mount point). /// @@ -27,71 +32,52 @@ internal static class DiskUtils /// if the specified path is a junction; otherwise, . public static bool IsJunction(string path) { - var reparseDataBuffer = TryGetReparseDataBuffer(path); - - if (reparseDataBuffer == null) - { - return false; - } - - return reparseDataBuffer.Value.ReparseTag == IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT; + return TryGetReparseTag(path) == PInvoke.IO_REPARSE_TAG_MOUNT_POINT; } /// - /// Tries to get the reparse data buffer for the specified path. + /// Tries to get the reparse tag for the specified path. /// /// The path to check. /// - /// A containing the reparse data if successful; otherwise, . + /// The reparse tag if the path is a reparse point; otherwise, . /// - internal static unsafe SymbolicLinkReparseBuffer? TryGetReparseDataBuffer(string path) + [SuppressMessage("Interoperability", "CA1416", Justification = WindowsOnlyJustification)] + private static unsafe uint? TryGetReparseTag(string path) { - using SafeFileHandle output = CreateFile( + using SafeFileHandle output = PInvoke.CreateFile( path, - Kernel32.FileAccess.ReadAttributes, - Kernel32.FileShare.Read, - IntPtr.Zero, - FileMode.Open, - Kernel32.FileAttributes.BackupSemantics | Kernel32.FileAttributes.OpenReparsePoint, - IntPtr.Zero); + (uint)FILE_ACCESS_RIGHTS.FILE_READ_ATTRIBUTES, + FILE_SHARE_MODE.FILE_SHARE_READ, + null, + FILE_CREATION_DISPOSITION.OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_OPEN_REPARSE_POINT); if (output.IsInvalid) { return null; } - byte[] buffer = ArrayPool.Shared.Rent(MAXIMUM_REPARSE_DATA_BUFFER_SIZE); - - bool success; - fixed (byte* pBuffer = buffer) + const int bufferSize = (int)PInvoke.MAXIMUM_REPARSE_DATA_BUFFER_SIZE; + var rentedBuffer = ArrayPool.Shared.Rent(bufferSize); + var buffer = rentedBuffer.AsSpan(0, bufferSize); + bool success = PInvoke.DeviceIoControl( + output, + PInvoke.FSCTL_GET_REPARSE_POINT, + lpInBuffer: default, + lpOutBuffer: buffer, + out _, + lpOverlapped: null); + + if (!success) { - success = DeviceIoControl( - output, - dwIoControlCode: FSCTL_GET_REPARSE_POINT, - lpInBuffer: null, - nInBufferSize: 0, - lpOutBuffer: pBuffer, - nOutBufferSize: MAXIMUM_REPARSE_DATA_BUFFER_SIZE, - out _, - IntPtr.Zero); - - if (!success) - { - return null; - } + return null; } - Span bufferSpan = new(buffer); - success = MemoryMarshal.TryRead(bufferSpan, out SymbolicLinkReparseBuffer reparseDataBuffer); + success = MemoryMarshal.TryRead(buffer, out REPARSE_DATA_BUFFER reparseDataBuffer); + ArrayPool.Shared.Return(rentedBuffer); - ArrayPool.Shared.Return(buffer); - - if (success) - { - return reparseDataBuffer; - } - - return null; + return success ? reparseDataBuffer.ReparseTag : null; } /// @@ -133,22 +119,22 @@ private static string GetPosixRealPath(string path) /// /// The file path to resolve. /// The resolved absolute path, or the original path if resolution fails. + [SuppressMessage("Interoperability", "CA1416", Justification = WindowsOnlyJustification)] private static string GetWindowsRealPath(string path) { - using var handle = CreateFile(path, - Kernel32.FileAccess.ReadEa, - Kernel32.FileShare.Read, - IntPtr.Zero, - FileMode.Open, - Kernel32.FileAttributes.BackupSemantics, - IntPtr.Zero); + using var handle = PInvoke.CreateFile(path, + (uint)FILE_ACCESS_RIGHTS.FILE_READ_EA, + FILE_SHARE_MODE.FILE_SHARE_READ, + null, + FILE_CREATION_DISPOSITION.OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS); if (handle.IsInvalid) { return path; } - return GetWindowsRealPathByHandle(handle.DangerousGetHandle()) ?? path; + return GetWindowsRealPathByHandle(handle) ?? path; } /// @@ -156,7 +142,8 @@ private static string GetWindowsRealPath(string path) /// /// The file handle. /// The resolved absolute path, or null if resolution fails. - private static unsafe string? GetWindowsRealPathByHandle(IntPtr handle) + [SuppressMessage("Interoperability", "CA1416", Justification = WindowsOnlyJustification)] + private static string? GetWindowsRealPathByHandle(SafeFileHandle handle) { // this is called for each storage environment for the Data, Journals and Temp paths // WindowsMaxPath is 32K and although this is called only once we can have a lot of storage environments @@ -190,28 +177,28 @@ bool GetPath(uint bufferSize, out string? outputPath) { var charArray = ArrayPool.Shared.Rent((int)bufferSize); - fixed (char* buffer = charArray) + var result = PInvoke.GetFinalPathNameByHandle( + handle, + charArray.AsSpan(0, (int)bufferSize), + GETFINALPATHNAMEBYHANDLE_FLAGS.FILE_NAME_NORMALIZED); + if (result == 0) { - var result = GetFinalPathNameByHandle(handle, buffer, bufferSize); - if (result == 0) - { - outputPath = null; - ArrayPool.Shared.Return(charArray); - return false; - } - - // return value is the size of the path - if (result > bufferSize) - { - outputPath = null; - ArrayPool.Shared.Return(charArray); - return false; - } - - outputPath = new string(charArray, 0, (int)result); + outputPath = null; ArrayPool.Shared.Return(charArray); - return true; + return false; } + + // return value is the size of the path + if (result > bufferSize) + { + outputPath = null; + ArrayPool.Shared.Return(charArray); + return false; + } + + outputPath = new string(charArray, 0, (int)result); + ArrayPool.Shared.Return(charArray); + return true; } } } diff --git a/TruePath/Kernel32.cs b/TruePath/Kernel32.cs deleted file mode 100644 index dcab9c6..0000000 --- a/TruePath/Kernel32.cs +++ /dev/null @@ -1,342 +0,0 @@ -// SPDX-FileCopyrightText: 2024 TruePath contributors -// -// SPDX-License-Identifier: MIT - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using Microsoft.Win32.SafeHandles; - -namespace TruePath; - -[SuppressMessage("ReSharper", "InconsistentNaming")] -internal static class Kernel32 -{ - /// - /// Retrieves the final path for the specified file handle. - /// - /// The file handle. - /// A buffer that receives the final path. - /// The size of the buffer, in characters. - /// The flags to specify the path format. - /// The length of the string copied to the buffer. - [DllImport("kernel32.dll", - EntryPoint = "GetFinalPathNameByHandleW", - SetLastError = true, - CharSet = CharSet.Unicode)] - internal static extern unsafe uint GetFinalPathNameByHandle( - IntPtr hFile, - char* buffer, - uint bufferLength, - uint dwFlags = 0); - - [Flags] - internal enum FileAttributes : uint - { - /// FILE_ATTRIBUTE_ARCHIVE - Archive = 0x20, - - /// FILE_ATTRIBUTE_COMPRESSED - Compressed = 0x800, - - /// FILE_ATTRIBUTE_DEVICE - Device = 0x40, - - /// FILE_ATTRIBUTE_DIRECTORY - Directory = 0x10, - - /// FILE_ATTRIBUTE_ENCRYPTED - Encrypted = 0x4000, - - /// FILE_ATTRIBUTE_HIDDEN - Hidden = 0x02, - - /// FILE_ATTRIBUTE_INTEGRITY_STREAM - IntegrityStream = 0x8000, - - /// FILE_ATTRIBUTE_NORMAL - Normal = 0x80, - - /// FILE_ATTRIBUTE_NOT_CONTENT_INDEXED - NotContentIndexed = 0x2000, - - /// FILE_ATTRIBUTE_NO_SCRUB_DATA - NoScrubData = 0x20000, - - /// FILE_ATTRIBUTE_OFFLINE - Offline = 0x1000, - - /// FILE_ATTRIBUTE_READONLY - Readonly = 0x01, - - /// FILE_ATTRIBUTE_REPARSE_POINT - ReparsePoint = 0x400, - - /// FILE_ATTRIBUTE_SPARSE_FILE - SparseFile = 0x200, - - /// FILE_ATTRIBUTE_SYSTEM - System = 0x04, - - /// FILE_ATTRIBUTE_TEMPORARY - Temporary = 0x100, - - /// FILE_ATTRIBUTE_VIRTUAL - Virtual = 0x10000, - - /// FILE_FLAG_BACKUP_SEMANTICS - BackupSemantics = 0x02000000, - - /// FILE_FLAG_DELETE_ON_CLOSE - DeleteOnClose = 0x04000000, - - /// FILE_FLAG_NO_BUFFERING - NoBuffering = 0x20000000, - - /// FILE_FLAG_OPEN_NO_RECALL - OpenNoRecall = 0x00100000, - - /// FILE_FLAG_OPEN_REPARSE_POINT - OpenReparsePoint = 0x00200000, - - /// FILE_FLAG_OVERLAPPED - Overlapped = 0x40000000, - - /// FILE_FLAG_POSIX_SEMANTICS - PosixSemantics = 0x0100000, - - /// FILE_FLAG_RANDOM_ACCESS - RandomAccess = 0x10000000, - - /// FILE_FLAG_SESSION_AWARE - SessionAware = 0x00800000, - - /// FILE_FLAG_SEQUENTIAL_SCAN - SequentialScan = 0x08000000, - - /// FILE_FLAG_WRITE_THROUGH - WriteThrough = 0x80000000 - } - - [Flags] - internal enum FileAccess : uint - { - /// FILE_READ_DATA - ReadData = 0x0001, - - /// FILE_LIST_DIRECTORY - ListDirectory = ReadData, - - /// FILE_WRITE_DATA - WriteData = 0x0002, - - /// FILE_ADD_FILE - AddFile = WriteData, - - /// FILE_APPEND_DATA - AppendData = 0x0004, - - /// FILE_ADD_SUBDIRECTORY - AddSubdirectory = AppendData, - - /// FILE_CREATE_PIPE_INSTANCE - CreatePipeInstance = AppendData, - - /// FILE_READ_EA - ReadEa = 0x0008, - - /// FILE_WRITE_EA - WriteEa = 0x0010, - - /// FILE_EXECUTE - Execute = 0x0020, - - /// FILE_TRAVERSE - Traverse = Execute, - - /// FILE_DELETE_CHILD - DeleteChild = 0x0040, - - /// FILE_READ_ATTRIBUTES - ReadAttributes = 0x0080, - - /// FILE_WRITE_ATTRIBUTES - WriteAttributes = 0x0100, - - /// GENERIC_READ - GenericRead = 0x80000000, - - /// GENERIC_WRITE - GenericWrite = 0x40000000, - - /// GENERIC_EXECUTE - GenericExecute = 0x20000000, - - /// GENERIC_ALL - GenericAll = 0x10000000 - } - - [Flags] - internal enum FileShare : uint - { - /// FILE_SHARE_NONE - None = 0x00, - - /// FILE_SHARE_READ - Read = 0x01, - - /// FILE_SHARE_WRITE - Write = 0x02, - - /// FILE_SHARE_DELETE - Delete = 0x03 - } - - /// - /// Creates or opens a file or I/O device. - /// - /// The name of the file or device to be created or opened. - /// The requested access to the file or device. - /// The requested sharing mode of the file or device. - /// A pointer to a SECURITY_ATTRIBUTES structure or IntPtr.Zero. - /// An action to take on a file or device that exists or does not exist. - /// The file or device attributes and flags. - /// A valid handle to a template file, or IntPtr.Zero. - /// A for the opened file or device. - [DllImport("kernel32.dll", - EntryPoint = "CreateFileW", - SetLastError = true, - CharSet = CharSet.Unicode)] - internal static extern SafeFileHandle CreateFile( - [MarshalAs(UnmanagedType.LPTStr)] string filename, - FileAccess access, - FileShare share, - nint securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero - FileMode creationDisposition, - FileAttributes flagsAndAttributes, - nint templateFile); - - /// - /// Control code for retrieving reparse point data. - /// - /// - /// For more information, see . - /// - internal const int FSCTL_GET_REPARSE_POINT = 0x000900a8; - - /// - /// Control code for reading the storage capacity of a device. - /// - /// - /// For more information, see . - /// - internal const int IOCTL_STORAGE_READ_CAPACITY = 0x002D5140; - - /// - /// Sends a control code directly to a specified device driver, causing the corresponding device to perform the corresponding operation. - /// - /// A handle to the device on which the operation is to be performed. - /// The control code for the operation. - /// A pointer to the input buffer that contains the data required to perform the operation. - /// The size of the input buffer, in bytes. - /// A pointer to the output buffer that receives the data returned by the operation. - /// The size of the output buffer, in bytes. - /// A variable that receives the size of the data stored in the output buffer, in bytes. - /// A pointer to an OVERLAPPED structure for asynchronous operations. For synchronous operations, this parameter is set to . - /// - /// if the operation succeeds; otherwise, . If the operation fails, call to get extended error information. - /// - /// - /// For more information, see . - /// - [DllImport("kernel32.dll", EntryPoint = "DeviceIoControl", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern unsafe bool DeviceIoControl( - SafeHandle hDevice, - uint dwIoControlCode, - void* lpInBuffer, - uint nInBufferSize, - void* lpOutBuffer, - uint nOutBufferSize, - out uint lpBytesReturned, - IntPtr lpOverlapped); - - /// - /// The maximum size of the reparse data buffer. - /// - /// - /// For more information, see . - /// - internal const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024; - - /// - /// Contains constants for reparse tags used in IO operations. - /// - internal static class IOReparseOptions - { - /// - /// Reparse tag for a file placeholder. - /// - internal const uint IO_REPARSE_TAG_FILE_PLACEHOLDER = 0x80000015; - - /// - /// Reparse tag for a mount point. - /// - internal const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; - - /// - /// Reparse tag for a symbolic link. - /// - internal const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C; - } - - /// - /// Represents a symbolic link reparse buffer. - /// - /// - /// For more information, see . - /// - [StructLayout(LayoutKind.Sequential)] - internal struct SymbolicLinkReparseBuffer - { - /// - /// The reparse tag. - /// - internal uint ReparseTag; - - /// - /// The length of the reparse data. - /// - internal ushort ReparseDataLength; - - /// - /// Reserved; do not use. - /// - internal ushort Reserved; - - /// - /// The offset, in bytes, to the substitute name string in the PathBuffer array. - /// - internal ushort SubstituteNameOffset; - - /// - /// The length, in bytes, of the substitute name string. If this string is null-terminated, SubstituteNameLength does not include space for the null character. - /// - internal ushort SubstituteNameLength; - - /// - /// The offset, in bytes, to the print name string in the PathBuffer array. - /// - internal ushort PrintNameOffset; - - /// - /// The length, in bytes, of the print name string. If this string is null-terminated, PrintNameLength does not include space for the null character. - /// - internal ushort PrintNameLength; - - /// - /// Flags that control the behavior of the symbolic link. - /// - internal uint Flags; - } - -} diff --git a/TruePath/NativeMethods.txt b/TruePath/NativeMethods.txt new file mode 100644 index 0000000..1bf1173 --- /dev/null +++ b/TruePath/NativeMethods.txt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 TruePath contributors +// +// SPDX-License-Identifier: MIT + +CreateFile +DeviceIoControl +FILE_ACCESS_RIGHTS +FSCTL_GET_REPARSE_POINT +GetFinalPathNameByHandle +IO_REPARSE_TAG_MOUNT_POINT +MAXIMUM_REPARSE_DATA_BUFFER_SIZE +REPARSE_DATA_BUFFER diff --git a/TruePath/TruePath.csproj b/TruePath/TruePath.csproj index 90fc6cd..4a72a10 100644 --- a/TruePath/TruePath.csproj +++ b/TruePath/TruePath.csproj @@ -21,5 +21,6 @@ SPDX-License-Identifier: MIT +