diff --git a/CHANGELOG.md b/CHANGELOG.md index ed93a1f..f0fe10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), with one exception: the behavior of the members and types marked as `[System.Diagnostics.CodeAnalysis.ExperimentalAttribute]` may change without a major version bump (see [Versioning](README.md#versioning)). +## [Unreleased] +### Changed +- [#252](https://github.com/ForNeVeR/TruePath/issues/252): on Windows, canonical path resolution now stops after a hard `GetFinalPathNameByHandle` failure and uses the exact required buffer size when retrying long paths. + ## [2.0.0] - 2026-10-25 This is a major release with certain breaking changes. No public APIs have been broken, but the amount of minor behavior changes required us to bump the major version. diff --git a/TruePath.Tests/DiskUtilsTests.cs b/TruePath.Tests/DiskUtilsTests.cs index b7058c5..d58296a 100644 --- a/TruePath.Tests/DiskUtilsTests.cs +++ b/TruePath.Tests/DiskUtilsTests.cs @@ -44,6 +44,31 @@ public void DiskUtils_OnCaseInsensitiveFs_PassNonCanonicalPath_ReturnCanonicalPa Assert.Equal(expected.Value, actual); } + [Fact] + public void DiskUtils_OnWindows_PassLongPath_ReturnCanonicalPath() + { + if (!OperatingSystem.IsWindows()) return; + + var tempFolder = Temporary.CreateTempFolder(); + try + { + var longPath = tempFolder; + while (longPath.Value.Length <= 256) + { + longPath /= new string('a', 100); + longPath.CreateDirectory(); + } + + var actual = DiskUtils.GetRealPath(longPath.Value); + + Assert.Equal(longPath.Value, actual); + } + finally + { + tempFolder.DeleteDirectoryRecursively(); + } + } + [Fact] public void DiskUtils_PassNonExistentPath_KeepFileDescriptorZeroOpen() { diff --git a/TruePath/DiskUtils.cs b/TruePath/DiskUtils.cs index 9f9a181..720bac5 100644 --- a/TruePath/DiskUtils.cs +++ b/TruePath/DiskUtils.cs @@ -16,12 +16,6 @@ namespace TruePath; /// internal static class DiskUtils { - // from https://github.com/dotnet/corefx/blob/9c06da6a34fcefa6fb37776ac57b80730e37387c/src/Common/src/System/IO/PathInternal.Windows.cs#L52 - /// - /// The maximum path length for Windows. - /// - private const short WindowsMaxPath = short.MaxValue; - private const string WindowsOnlyJustification = "Only called on Windows, which is checked by the callers."; /// @@ -146,11 +140,18 @@ private static string GetWindowsRealPath(string path) [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 - if (GetPath(256, out var realPath) == false) + const uint initialBufferSize = 256; + var result = GetPath(initialBufferSize, out var realPath); + if (result == 0) { - if (GetPath((uint)WindowsMaxPath, out realPath) == false) + return null; + } + + if (result > initialBufferSize) + { + var requiredBufferSize = result; + result = GetPath(requiredBufferSize, out realPath); + if (result == 0 || result > requiredBufferSize) return null; } @@ -174,32 +175,25 @@ private static string GetWindowsRealPath(string path) return realPath; - bool GetPath(uint bufferSize, out string? outputPath) + uint GetPath(uint bufferSize, out string? outputPath) { var charArray = ArrayPool.Shared.Rent((int)bufferSize); - - var result = PInvoke.GetFinalPathNameByHandle( - handle, - charArray.AsSpan(0, (int)bufferSize), - GETFINALPATHNAMEBYHANDLE_FLAGS.FILE_NAME_NORMALIZED); - if (result == 0) + try { - outputPath = null; - ArrayPool.Shared.Return(charArray); - return false; + var pathLength = PInvoke.GetFinalPathNameByHandle( + handle, + charArray.AsSpan(0, (int)bufferSize), + GETFINALPATHNAMEBYHANDLE_FLAGS.FILE_NAME_NORMALIZED); + + outputPath = pathLength == 0 || pathLength > bufferSize + ? null + : new string(charArray, 0, (int)pathLength); + return pathLength; } - - // return value is the size of the path - if (result > bufferSize) + finally { - outputPath = null; ArrayPool.Shared.Return(charArray); - return false; } - - outputPath = new string(charArray, 0, (int)result); - ArrayPool.Shared.Return(charArray); - return true; } } }