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;
}
}
}