Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
25 changes: 25 additions & 0 deletions TruePath.Tests/DiskUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
52 changes: 23 additions & 29 deletions TruePath/DiskUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ namespace TruePath;
/// </summary>
internal static class DiskUtils
{
// from https://github.com/dotnet/corefx/blob/9c06da6a34fcefa6fb37776ac57b80730e37387c/src/Common/src/System/IO/PathInternal.Windows.cs#L52
/// <summary>
/// The maximum path length for Windows.
/// </summary>
private const short WindowsMaxPath = short.MaxValue;

private const string WindowsOnlyJustification = "Only called on Windows, which is checked by the callers.";

/// <summary>
Expand Down Expand Up @@ -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;
}

Expand All @@ -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<char>.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<char>.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<char>.Shared.Return(charArray);
return false;
}

outputPath = new string(charArray, 0, (int)result);
ArrayPool<char>.Shared.Return(charArray);
return true;
}
}
}
Loading