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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ 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`.
- **Breaking:** path normalization now represents the current directory as `.` instead of an empty string: `PathStrings.Normalize` returns `"."` for `""`, `"."` and `"a/.."`, and so do `LocalPath.Value` and `ToString()` (and `LocalPath.Parent` of a single-segment relative path is `.`). As a consequence, `LocalPath.FileName` of such a path is `.`, the same way it is `..` for `..`, and `GetExtensionWithDot` returns `.` for it.
- `LocalPath.IsPrefixOf` now treats the current directory 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.
- [#240](https://github.com/ForNeVeR/TruePath/issues/240): more performant `AbsolutePath.ReadKind`: it now queries the entry attributes only once, and on Windows it no longer allocates or performs additional IO in cases not concerning reparse points. Thanks to @FalconTechDev!
- `AbsolutePath.ReadKind` now only returns `null` if the entry doesn't exist, and throws an exception if its attributes cannot be read for another reason: e.g. an `UnauthorizedAccessException` if the access is denied, or an `IOException` if the path contains a name rejected by the file system (on .NET Framework, also `ArgumentException` or `PathTooLongException`). Previously, it returned `null` in these cases.
- The platform-default path comparers (`LocalPath.PlatformDefaultComparer`, `AbsolutePath.PlatformDefaultComparer`) are now case-insensitive on iOS and tvOS as well, matching the .NET runtime.

### Fixed
- `AbsolutePath.ReadKind` no longer throws an exception if the entry gets deleted during the call.
- [#239](https://github.com/ForNeVeR/TruePath/issues/239): on Unix, `AbsolutePath.Canonicalize()` no longer closes the file descriptor 0 when the path doesn't exist. This used to close the standard input of the process, or any file or socket that later took this descriptor number. Also, it no longer leaks the memory allocated for the resolved path.
- [#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`.
- [#225](https://github.com/ForNeVeR/TruePath/issues/225): Make path prefix checks use the same platform-default case sensitivity as path equality.
Expand Down
41 changes: 34 additions & 7 deletions TruePath.Tests/AbsolutePathTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT

using System.Diagnostics;
using TruePath.SystemIo;

namespace TruePath.Tests;

Expand Down Expand Up @@ -36,17 +37,43 @@ public void PathRootOfRootReturnsItself()
[Fact]
public void ReadKind_NonExistent()
{
// Arrange
var currentDirectory = Path.Combine(Environment.CurrentDirectory, Guid.NewGuid().ToString());
var localPath = new AbsolutePath(currentDirectory);

// Act
var kind = localPath.ReadKind();
var path = AbsolutePath.CurrentWorkingDirectory / Guid.NewGuid().ToString();
var kind = path.ReadKind();
Assert.Null(kind);
}

// Assert
[Fact]
public void ReadKind_NonExistentParent()
{
var path = AbsolutePath.CurrentWorkingDirectory / Guid.NewGuid().ToString() / "child";
var kind = path.ReadKind();
Assert.Null(kind);
}

[Fact]
public void ReadKind_ChildOfFile()
{
var tempFile = Temporary.CreateTempFile();
try
{
var kind = (tempFile / "child").ReadKind();
Assert.Null(kind);
}
finally
{
tempFile.Delete();
}
}

[Fact]
public void OnWindows_ReadKind_InvalidName_Throws()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;

var localPath = AbsolutePath.CurrentWorkingDirectory / "a|b";
Assert.ThrowsAny<IOException>(() => localPath.ReadKind());
}

[Fact]
public void ReadKind_IsDirectory()
{
Expand Down
22 changes: 11 additions & 11 deletions TruePath/AbsolutePath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,33 +241,33 @@ public override bool Equals(object? obj)
/// <remarks>
/// This method checks if the specified path represents a file, directory, symbolic link, or junction. On Windows, it uses <see cref="DiskUtils.IsJunction"/> to identify junctions and checks for the <see cref="FileAttributes.ReparsePoint"/> flag to identify symbolic links.
/// </remarks>
/// <exception cref="UnauthorizedAccessException">The caller doesn't have the permission to read the entry attributes.</exception>
/// <exception cref="IOException">
/// The entry attributes cannot be read for a reason other than the entry not existing, e.g. the path contains a name rejected by the file system.
/// </exception>
public FileEntryKind? ReadKind()
{
if (!File.Exists(Value) && !Directory.Exists(Value))
FileAttributes attributes;
try
{
attributes = File.GetAttributes(Value);
}
catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException)
{
return null;
}

var attributes = File.GetAttributes(Value);

if (!attributes.HasFlag(FileAttributes.Directory))
{
return FileEntryKind.File;
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (DiskUtils.IsJunction(Value))
if (attributes.HasFlag(FileAttributes.ReparsePoint) && DiskUtils.IsJunction(Value))
{
return FileEntryKind.Junction;
}

if (attributes.HasFlag(FileAttributes.ReparsePoint))
{
return FileEntryKind.Symlink;
}

return FileEntryKind.Directory;
}

if (attributes.HasFlag(FileAttributes.ReparsePoint))
Expand Down
22 changes: 9 additions & 13 deletions TruePath/DiskUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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;
Expand Down Expand Up @@ -58,26 +57,23 @@ public static bool IsJunction(string path)
return null;
}

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(
FILE_ATTRIBUTE_TAG_INFO info;
bool success = PInvoke.GetFileInformationByHandleEx(
output,
PInvoke.FSCTL_GET_REPARSE_POINT,
lpInBuffer: default,
lpOutBuffer: buffer,
out _,
lpOverlapped: null);
FILE_INFO_BY_HANDLE_CLASS.FileAttributeTagInfo,
new Span<byte>(&info, sizeof(FILE_ATTRIBUTE_TAG_INFO)));

if (!success)
{
return null;
}

success = MemoryMarshal.TryRead(buffer, out REPARSE_DATA_BUFFER reparseDataBuffer);
ArrayPool<byte>.Shared.Return(rentedBuffer);
if ((info.FileAttributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_REPARSE_POINT) == 0)
{
return null;
}

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

/// <summary>
Expand Down
6 changes: 2 additions & 4 deletions TruePath/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
// SPDX-License-Identifier: MIT

CreateFile
DeviceIoControl
FILE_ACCESS_RIGHTS
FSCTL_GET_REPARSE_POINT
FILE_ATTRIBUTE_TAG_INFO
GetFileInformationByHandleEx
GetFinalPathNameByHandle
IO_REPARSE_TAG_MOUNT_POINT
MAXIMUM_REPARSE_DATA_BUFFER_SIZE
REPARSE_DATA_BUFFER
Loading