Skip to content
Merged
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
139 changes: 63 additions & 76 deletions TruePath/DiskUtils.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// SPDX-FileCopyrightText: 2024 TruePath contributors <https://github.com/ForNeVeR/TruePath>
// SPDX-FileCopyrightText: 2024-2026 TruePath contributors <https://github.com/ForNeVeR/TruePath>
//
// 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;

Expand All @@ -20,78 +23,61 @@ internal static class DiskUtils
/// </summary>
private const short WindowsMaxPath = short.MaxValue;

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

/// <summary>
/// Determines whether the specified path is a junction (mount point).
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns><see langword="true"/> if the specified path is a junction; otherwise, <see langword="false"/>.</returns>
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;
}

/// <summary>
/// Tries to get the reparse data buffer for the specified path.
/// Tries to get the reparse tag for the specified path.
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns>
/// A <see cref="SymbolicLinkReparseBuffer"/> containing the reparse data if successful; otherwise, <see langword="null"/>.
/// The reparse tag if the path is a reparse point; otherwise, <see langword="null"/>.
/// </returns>
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<byte>.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<byte>.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<byte> bufferSpan = new(buffer);
success = MemoryMarshal.TryRead(bufferSpan, out SymbolicLinkReparseBuffer reparseDataBuffer);
success = MemoryMarshal.TryRead(buffer, out REPARSE_DATA_BUFFER reparseDataBuffer);
ArrayPool<byte>.Shared.Return(rentedBuffer);

ArrayPool<byte>.Shared.Return(buffer);

if (success)
{
return reparseDataBuffer;
}

return null;
return success ? reparseDataBuffer.ReparseTag : null;
}

/// <summary>
Expand Down Expand Up @@ -133,30 +119,31 @@ private static string GetPosixRealPath(string path)
/// </summary>
/// <param name="path">The file path to resolve.</param>
/// <returns>The resolved absolute path, or the original path if resolution fails.</returns>
[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;
}

/// <summary>
/// Gets the real (absolute) path by file handle for Windows systems.
/// </summary>
/// <param name="handle">The file handle.</param>
/// <returns>The resolved absolute path, or <c>null</c> if resolution fails.</returns>
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
Expand Down Expand Up @@ -190,28 +177,28 @@ bool GetPath(uint bufferSize, out string? outputPath)
{
var charArray = ArrayPool<char>.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<char>.Shared.Return(charArray);
return false;
}

// return value is the size of the path
if (result > bufferSize)
{
outputPath = null;
ArrayPool<char>.Shared.Return(charArray);
return false;
}

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

// return value is the size of the path
if (result > bufferSize)
{
outputPath = null;
ArrayPool<char>.Shared.Return(charArray);
return false;
}

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