From a0113d1c28a34a675e1e893dd51570cc8bf9b83c Mon Sep 17 00:00:00 2001 From: ElectLive <343459429+ElectLive@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:31:46 +0800 Subject: [PATCH 1/5] Kernel32: drop the unused Win32 declarations Remove the constants and enum members of TruePath.Kernel32 that nothing in the codebase uses, as recommended by the maintainer in #241. Among the dropped members are the two whose values did not match the Windows headers: FileShare.Delete (0x03 instead of 0x04) and FileAttributes.PosixSemantics (0x0100000 instead of 0x01000000). The remaining declarations are now covered by Kernel32ConstantsTests, pinning their values to the Windows headers so hand-copied constants cannot silently go stale again. Closes #241 --- CHANGELOG.md | 1 + TruePath.Tests/Kernel32ConstantsTests.cs | 60 +++++++++ TruePath/Kernel32.cs | 159 +---------------------- 3 files changed, 64 insertions(+), 156 deletions(-) create mode 100644 TruePath.Tests/Kernel32ConstantsTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f67ce..49a817d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - **Breaking:** `LocalPath.StartsWith` and `AbsolutePath.StartsWith` now compare whole path segments instead of raw strings, which makes them exact inverses of `IsPrefixOf` as originally intended in [#43](https://github.com/ForNeVeR/TruePath/issues/43). For example, `new LocalPath("/foo1").StartsWith(new LocalPath("/foo"))` is now `false`, where it used to be `true`. - `LocalPath.IsPrefixOf` now treats an empty path — the normalized form of `""`, `"."` and `"a/.."`, and the parent of any single-segment relative path — as the current directory, so it is a prefix of every path of kind `PathKind.Relative` that does not begin with a `..` reference. - The platform-default path comparers (`LocalPath.PlatformDefaultComparer`, `AbsolutePath.PlatformDefaultComparer`) are now case-insensitive on iOS and tvOS as well, matching the .NET runtime. +- [#241](https://github.com/ForNeVeR/TruePath/issues/241): Drop the unused Win32 declarations from the internal `TruePath.Kernel32` type, including the incorrectly valued `FileShare.Delete` and `FileAttributes.PosixSemantics` members. This is an internal cleanup with no effect on the library behavior. ### Fixed - [#226](https://github.com/ForNeVeR/TruePath/issues/226): `LocalPath.IsPrefixOf` and `StartsWith` now take leading `..` references of relative paths into account instead of comparing them as strings. For example, `new LocalPath("..").IsPrefixOf(new LocalPath("foo"))` is now `true`, and `new LocalPath("..").IsPrefixOf(new LocalPath("../.."))` is now `false`. diff --git a/TruePath.Tests/Kernel32ConstantsTests.cs b/TruePath.Tests/Kernel32ConstantsTests.cs new file mode 100644 index 0000000..0b66f38 --- /dev/null +++ b/TruePath.Tests/Kernel32ConstantsTests.cs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 TruePath contributors +// +// SPDX-License-Identifier: MIT + +namespace TruePath.Tests; + +/// +/// Pins the Win32 constants kept in to the values from the Windows headers +/// (see and +/// ). +/// These values used to be hand-copied and were wrong in some cases (see issue #241). +/// +public class Kernel32ConstantsTests +{ + [Fact] + public void FileShareReadMatchesWindowsHeader() + { + // winnt.h: FILE_SHARE_READ 0x00000001 + Assert.Equal(0x00000001u, (uint)Kernel32.FileShare.Read); + } + + [Fact] + public void FileAccessValuesMatchWindowsHeader() + { + // winnt.h: FILE_READ_EA 0x0008, FILE_READ_ATTRIBUTES 0x0080 + Assert.Equal(0x0008u, (uint)Kernel32.FileAccess.ReadEa); + Assert.Equal(0x0080u, (uint)Kernel32.FileAccess.ReadAttributes); + } + + [Fact] + public void FileAttributesValuesMatchWindowsHeader() + { + // winnt.h: FILE_FLAG_BACKUP_SEMANTICS 0x02000000, FILE_FLAG_OPEN_REPARSE_POINT 0x00200000 + Assert.Equal(0x02000000u, (uint)Kernel32.FileAttributes.BackupSemantics); + Assert.Equal(0x00200000u, (uint)Kernel32.FileAttributes.OpenReparsePoint); + } + + [Fact] + public void ReparseDataConstantsMatchWindowsHeaders() + { + // winioctl.h: FSCTL_GET_REPARSE_POINT = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 42, METHOD_BUFFERED, FILE_ANY_ACCESS) = 0x000900A8 + Assert.Equal(0x000900A8, Kernel32.FSCTL_GET_REPARSE_POINT); + + // winioctl.h: MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024 + Assert.Equal(16384, Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); + + // winnlt.h: IO_REPARSE_TAG_MOUNT_POINT = (0xA0000003L) + Assert.Equal(0xA0000003u, Kernel32.IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT); + } + + [Fact] + public void SymbolicLinkReparseBufferMatchesWindowsLayout() + { + // winioctl.h: REPARSE_DATA_BUFFER's symlink sub-structure: + // ULONG ReparseTag + USHORT ReparseDataLength + USHORT Reserved + // + USHORT SubstituteNameOffset + USHORT SubstituteNameLength + // + USHORT PrintNameOffset + USHORT PrintNameLength + ULONG Flags = 20 bytes. + Assert.Equal(20, Marshal.SizeOf()); + } +} diff --git a/TruePath/Kernel32.cs b/TruePath/Kernel32.cs index dcab9c6..efb702b 100644 --- a/TruePath/Kernel32.cs +++ b/TruePath/Kernel32.cs @@ -32,163 +32,28 @@ internal static extern unsafe uint GetFinalPathNameByHandle( [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 + OpenReparsePoint = 0x00200000 } [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 + ReadAttributes = 0x0080 } [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 + Read = 0x01 } /// @@ -223,14 +88,6 @@ internal static extern SafeFileHandle CreateFile( /// 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. /// @@ -273,20 +130,10 @@ internal static extern unsafe bool DeviceIoControl( /// 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; } /// From eedf2668b14a8fb833b745a90b44ee2d01ca479f Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 24 Oct 2026 21:20:58 +0200 Subject: [PATCH 2/5] (#241) Kernel32: bump the copyright year --- TruePath/Kernel32.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TruePath/Kernel32.cs b/TruePath/Kernel32.cs index efb702b..33325cc 100644 --- a/TruePath/Kernel32.cs +++ b/TruePath/Kernel32.cs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 TruePath contributors +// SPDX-FileCopyrightText: 2024-2026 TruePath contributors // // SPDX-License-Identifier: MIT From edbaf841a87e026da4902c4c273341f2c2c3de72 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 24 Oct 2026 21:21:43 +0200 Subject: [PATCH 3/5] (#241) Kernel32: only leave one test about the data structure size --- TruePath.Tests/Kernel32ConstantsTests.cs | 60 ------------------------ TruePath.Tests/Kernel32Tests.cs | 18 +++++++ 2 files changed, 18 insertions(+), 60 deletions(-) delete mode 100644 TruePath.Tests/Kernel32ConstantsTests.cs create mode 100644 TruePath.Tests/Kernel32Tests.cs diff --git a/TruePath.Tests/Kernel32ConstantsTests.cs b/TruePath.Tests/Kernel32ConstantsTests.cs deleted file mode 100644 index 0b66f38..0000000 --- a/TruePath.Tests/Kernel32ConstantsTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2026 TruePath contributors -// -// SPDX-License-Identifier: MIT - -namespace TruePath.Tests; - -/// -/// Pins the Win32 constants kept in to the values from the Windows headers -/// (see and -/// ). -/// These values used to be hand-copied and were wrong in some cases (see issue #241). -/// -public class Kernel32ConstantsTests -{ - [Fact] - public void FileShareReadMatchesWindowsHeader() - { - // winnt.h: FILE_SHARE_READ 0x00000001 - Assert.Equal(0x00000001u, (uint)Kernel32.FileShare.Read); - } - - [Fact] - public void FileAccessValuesMatchWindowsHeader() - { - // winnt.h: FILE_READ_EA 0x0008, FILE_READ_ATTRIBUTES 0x0080 - Assert.Equal(0x0008u, (uint)Kernel32.FileAccess.ReadEa); - Assert.Equal(0x0080u, (uint)Kernel32.FileAccess.ReadAttributes); - } - - [Fact] - public void FileAttributesValuesMatchWindowsHeader() - { - // winnt.h: FILE_FLAG_BACKUP_SEMANTICS 0x02000000, FILE_FLAG_OPEN_REPARSE_POINT 0x00200000 - Assert.Equal(0x02000000u, (uint)Kernel32.FileAttributes.BackupSemantics); - Assert.Equal(0x00200000u, (uint)Kernel32.FileAttributes.OpenReparsePoint); - } - - [Fact] - public void ReparseDataConstantsMatchWindowsHeaders() - { - // winioctl.h: FSCTL_GET_REPARSE_POINT = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 42, METHOD_BUFFERED, FILE_ANY_ACCESS) = 0x000900A8 - Assert.Equal(0x000900A8, Kernel32.FSCTL_GET_REPARSE_POINT); - - // winioctl.h: MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024 - Assert.Equal(16384, Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); - - // winnlt.h: IO_REPARSE_TAG_MOUNT_POINT = (0xA0000003L) - Assert.Equal(0xA0000003u, Kernel32.IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT); - } - - [Fact] - public void SymbolicLinkReparseBufferMatchesWindowsLayout() - { - // winioctl.h: REPARSE_DATA_BUFFER's symlink sub-structure: - // ULONG ReparseTag + USHORT ReparseDataLength + USHORT Reserved - // + USHORT SubstituteNameOffset + USHORT SubstituteNameLength - // + USHORT PrintNameOffset + USHORT PrintNameLength + ULONG Flags = 20 bytes. - Assert.Equal(20, Marshal.SizeOf()); - } -} diff --git a/TruePath.Tests/Kernel32Tests.cs b/TruePath.Tests/Kernel32Tests.cs new file mode 100644 index 0000000..bf69724 --- /dev/null +++ b/TruePath.Tests/Kernel32Tests.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2026 TruePath contributors +// +// SPDX-License-Identifier: MIT + +namespace TruePath.Tests; + +public class Kernel32Tests +{ + [Fact] + public void SymbolicLinkReparseBufferMatchesWindowsLayout() + { + // winioctl.h: REPARSE_DATA_BUFFER's symlink sub-structure: + // ULONG ReparseTag + USHORT ReparseDataLength + USHORT Reserved + // + USHORT SubstituteNameOffset + USHORT SubstituteNameLength + // + USHORT PrintNameOffset + USHORT PrintNameLength + ULONG Flags = 20 bytes. + Assert.Equal(20, Marshal.SizeOf()); + } +} From c9ffd64e1fb9bf92f46f64d483903a61f8bb55ee Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 24 Oct 2026 21:21:57 +0200 Subject: [PATCH 4/5] (#241) Docs: drop the changelog entry about internal change --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49a817d..73f67ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - **Breaking:** `LocalPath.StartsWith` and `AbsolutePath.StartsWith` now compare whole path segments instead of raw strings, which makes them exact inverses of `IsPrefixOf` as originally intended in [#43](https://github.com/ForNeVeR/TruePath/issues/43). For example, `new LocalPath("/foo1").StartsWith(new LocalPath("/foo"))` is now `false`, where it used to be `true`. - `LocalPath.IsPrefixOf` now treats an empty path — the normalized form of `""`, `"."` and `"a/.."`, and the parent of any single-segment relative path — as the current directory, so it is a prefix of every path of kind `PathKind.Relative` that does not begin with a `..` reference. - The platform-default path comparers (`LocalPath.PlatformDefaultComparer`, `AbsolutePath.PlatformDefaultComparer`) are now case-insensitive on iOS and tvOS as well, matching the .NET runtime. -- [#241](https://github.com/ForNeVeR/TruePath/issues/241): Drop the unused Win32 declarations from the internal `TruePath.Kernel32` type, including the incorrectly valued `FileShare.Delete` and `FileAttributes.PosixSemantics` members. This is an internal cleanup with no effect on the library behavior. ### Fixed - [#226](https://github.com/ForNeVeR/TruePath/issues/226): `LocalPath.IsPrefixOf` and `StartsWith` now take leading `..` references of relative paths into account instead of comparing them as strings. For example, `new LocalPath("..").IsPrefixOf(new LocalPath("foo"))` is now `true`, and `new LocalPath("..").IsPrefixOf(new LocalPath("../.."))` is now `false`. From 73a3685b0776b9bf72bc5ed9a813cd1626d2022b Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 24 Oct 2026 21:48:04 +0200 Subject: [PATCH 5/5] (#241) DiskUtils: migrate to use CsWin32 instead of our own interop --- TruePath.Tests/Kernel32Tests.cs | 18 --- TruePath/DiskUtils.cs | 139 +++++++++++------------ TruePath/Kernel32.cs | 189 -------------------------------- TruePath/NativeMethods.txt | 12 ++ TruePath/TruePath.csproj | 1 + 5 files changed, 76 insertions(+), 283 deletions(-) delete mode 100644 TruePath.Tests/Kernel32Tests.cs delete mode 100644 TruePath/Kernel32.cs create mode 100644 TruePath/NativeMethods.txt diff --git a/TruePath.Tests/Kernel32Tests.cs b/TruePath.Tests/Kernel32Tests.cs deleted file mode 100644 index bf69724..0000000 --- a/TruePath.Tests/Kernel32Tests.cs +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2026 TruePath contributors -// -// SPDX-License-Identifier: MIT - -namespace TruePath.Tests; - -public class Kernel32Tests -{ - [Fact] - public void SymbolicLinkReparseBufferMatchesWindowsLayout() - { - // winioctl.h: REPARSE_DATA_BUFFER's symlink sub-structure: - // ULONG ReparseTag + USHORT ReparseDataLength + USHORT Reserved - // + USHORT SubstituteNameOffset + USHORT SubstituteNameLength - // + USHORT PrintNameOffset + USHORT PrintNameLength + ULONG Flags = 20 bytes. - Assert.Equal(20, Marshal.SizeOf()); - } -} 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 33325cc..0000000 --- a/TruePath/Kernel32.cs +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-FileCopyrightText: 2024-2026 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_FLAG_BACKUP_SEMANTICS - BackupSemantics = 0x02000000, - - /// FILE_FLAG_OPEN_REPARSE_POINT - OpenReparsePoint = 0x00200000 - } - - [Flags] - internal enum FileAccess : uint - { - /// FILE_READ_EA - ReadEa = 0x0008, - - /// FILE_READ_ATTRIBUTES - ReadAttributes = 0x0080 - } - - [Flags] - internal enum FileShare : uint - { - /// FILE_SHARE_READ - Read = 0x01 - } - - /// - /// 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; - - /// - /// 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 mount point. - /// - internal const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; - } - - /// - /// 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 +