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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<ItemGroup>
<PackageVersion Include="AutoFixture.AutoNSubstitute" Version="5.0.0-preview0012"/>
<PackageVersion Include="AutoFixture.Xunit3" Version="5.0.0-preview0012"/>
<PackageVersion Include="aweXpect" Version="2.21.0"/>
<PackageVersion Include="aweXpect" Version="2.21.1"/>
<PackageVersion Include="aweXpect.Testably" Version="0.11.0"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageVersion Include="xunit.v3" Version="2.0.3"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,27 +336,14 @@ public void Refresh()
/// <inheritdoc cref="IFileSystemInfo.ResolveLinkTarget(bool)" />
public IFileSystemInfo? ResolveLinkTarget(bool returnFinalTarget)
{
using IDisposable registration = RegisterPathMethod(nameof(ResolveLinkTarget),
returnFinalTarget);
using IDisposable registration = RegisterPathMethod(
nameof(ResolveLinkTarget), returnFinalTarget
);

try
{
IStorageLocation? targetLocation =
_fileSystem.Storage.ResolveLinkTarget(
Location,
returnFinalTarget);
if (targetLocation != null)
{
return New(targetLocation, _fileSystem);
}
IStorageLocation? targetLocation
= _fileSystem.Storage.ResolveLinkTarget(Location, returnFinalTarget);

return null;
}
catch (IOException ex) when (ex.HResult != -2147024773)
{
throw ExceptionFactory.FileNameCannotBeResolved(Location.FullPath,
_fileSystem.Execute.IsWindows ? -2147022975 : -2146232800);
}
return targetLocation != null ? New(targetLocation, _fileSystem) : null;
}
#endif

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,15 +292,13 @@ internal static ArgumentOutOfRangeException TimerArgumentOutOfRange(string prope
internal static TimeoutException TimerWaitTimeoutException(int executionCount, int timeout)
=> new($"The execution count {executionCount} was not reached in {timeout}ms.");

#if FEATURE_FILESYSTEM_UNIXFILEMODE
internal static UnauthorizedAccessException UnixFileModeAccessDenied(string path)
internal static UnauthorizedAccessException AccessDenied(string path)
=> new($"Access to the path '{path}' is denied.")
{
#if FEATURE_EXCEPTION_HRESULT
HResult = -2147024891,
#endif
};
#endif

internal static PlatformNotSupportedException UnixFileModeNotSupportedOnThisPlatform()
=> new("Unix file modes are not supported on this platform.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ public IStorageAccessHandle RequestAccess(FileAccess access, FileShare share,
if (!deleteAccess && !_fileSystem.UnixFileModeStrategy
.IsAccessGranted(_location.FullPath, _extensibility, UnixFileMode, access))
{
throw ExceptionFactory.UnixFileModeAccessDenied(_location.FullPath);
throw ExceptionFactory.AccessDenied(_location.FullPath);
}
#endif

Expand Down
62 changes: 51 additions & 11 deletions Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -558,19 +558,28 @@ public IStorageContainer GetOrCreateContainer(

#if FEATURE_FILESYSTEM_LINK
/// <inheritdoc cref="IStorage.ResolveLinkTarget(IStorageLocation, bool)" />
public IStorageLocation? ResolveLinkTarget(IStorageLocation location,
bool returnFinalTarget = false)
public IStorageLocation? ResolveLinkTarget(
IStorageLocation location,
bool returnFinalTarget = false
)
{
if (!_containers.TryGetValue(location, out IStorageContainer? initialContainer)
|| initialContainer.LinkTarget == null)
{
return null;
}

IStorageLocation? nextLocation =
_fileSystem.Storage.GetLocation(initialContainer.LinkTarget);
IStorageLocation? nextLocation
= _fileSystem.Storage.GetLocation(initialContainer.LinkTarget);

if (!_containers.TryGetValue(nextLocation, out IStorageContainer? container))
{
return nextLocation;
}

ThrowOnLinkTypeChange(initialContainer, location, container);

if (returnFinalTarget && _containers.TryGetValue(nextLocation, out IStorageContainer? container) && container.LinkTarget != null)
if (returnFinalTarget && container.LinkTarget != null)
{
nextLocation = ResolveFinalLinkTarget(container, location);
}
Expand Down Expand Up @@ -755,7 +764,7 @@ private void CheckAndAdjustParentDirectoryTimes(IStorageLocation location)
catch (UnauthorizedAccessException)
{
// On Unix, if the parent directory is not writable, we include the child path in the exception.
throw ExceptionFactory.UnixFileModeAccessDenied(location.FullPath);
throw ExceptionFactory.AccessDenied(location.FullPath);
}
#else
using (parentContainer.RequestAccess(FileAccess.Write, FileShare.ReadWrite))
Expand Down Expand Up @@ -1022,11 +1031,14 @@ private bool IncludeItemInEnumeration(
}

#if FEATURE_FILESYSTEM_LINK
private IStorageLocation? ResolveFinalLinkTarget(IStorageContainer container,
IStorageLocation originalLocation)
private IStorageLocation? ResolveFinalLinkTarget(
IStorageContainer container,
IStorageLocation originalLocation
)
{
int maxResolveLinks = _fileSystem.Execute.IsWindows ? 63 : 40;
IStorageLocation? nextLocation = null;

for (int i = 1; i < maxResolveLinks; i++)
{
if (container.LinkTarget == null)
Expand All @@ -1035,23 +1047,51 @@ private bool IncludeItemInEnumeration(
}

nextLocation = _fileSystem.Storage.GetLocation(container.LinkTarget);
if (!_containers.TryGetValue(nextLocation,
out IStorageContainer? nextContainer))

if (!_containers.TryGetValue(nextLocation, out IStorageContainer? nextContainer))
{
return nextLocation;
}

ThrowOnLinkTypeChange(container, originalLocation, nextContainer);

container = nextContainer;
}

if (container.LinkTarget != null)
{
throw ExceptionFactory.FileNameCannotBeResolved(
originalLocation.FullPath);
originalLocation.FullPath, _fileSystem.Execute.IsWindows ? -2147022975 : -2146232800
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded HResult values (-2147022975 and -2146232800) should be defined as named constants to improve readability and maintainability. Consider creating constants like WINDOWS_TOO_MANY_LINKS_HRESULT and UNIX_TOO_MANY_LINKS_HRESULT.

Copilot uses AI. Check for mistakes.
);
}

return nextLocation;
}

private void ThrowOnLinkTypeChange(
IStorageContainer previous,
IStorageLocation previousLocation,
IStorageContainer next
)
{
if (!_fileSystem.Execute.IsWindows)
{
return;
}

if (previous.Type == next.Type)
{
return;
}

switch (previous.Type)
{
case FileSystemTypes.File:
throw ExceptionFactory.AccessDenied(previousLocation.FullPath);
case FileSystemTypes.Directory:
throw ExceptionFactory.InvalidDirectoryName(previousLocation.FullPath);
}
}
#endif

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
#if FEATURE_FILESYSTEM_LINK
using System.IO;
using System.Text.RegularExpressions;

namespace Testably.Abstractions.Tests.FileSystem.DirectoryInfo;

[FileSystemTests]
public partial class ResolveLinkTargetTests
{
#region Test Setup

/// <summary>
/// The maximum number of symbolic links that are followed.<br />
/// <see href="https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks" />
/// </summary>
private int MaxResolveLinks => Test.RunsOnWindows ? 63 : 40;

#endregion

[Theory]
[AutoData]
public async Task ResolveLinkTarget_ShouldThrow(string path)
public async Task ResolveLinkTarget_FinalTargetWithTooManyLevels_ShouldThrowIOException(
string path,
string pathToFinalTarget
)
{
IFileSystemInfo link = FileSystem.Directory.CreateSymbolicLink(path, path + "-start");
int maxLinks = MaxResolveLinks + 1;
FileSystem.Directory.CreateDirectory(pathToFinalTarget);
string previousPath = pathToFinalTarget;

for (int i = 0; i < maxLinks; i++)
{
string newPath = $"{path}-{i}";
IDirectoryInfo linkDirectoryInfo = FileSystem.DirectoryInfo.New(newPath);
linkDirectoryInfo.CreateAsSymbolicLink(previousPath);
previousPath = newPath;
}

IDirectoryInfo directoryInfo = FileSystem.DirectoryInfo.New(previousPath);

// UNIX allows 43 and Windows 63 nesting, so 70 is plenty to force the exception
for (int i = 0; i < 70; i++)
void Act()
{
link = FileSystem.Directory.CreateSymbolicLink($"{path}{i}", link.Name);
_ = directoryInfo.ResolveLinkTarget(true);
}

await That(() => link.ResolveLinkTarget(true)).Throws<IOException>();
await That(Act).Throws<IOException>()
.WithHResult(Test.RunsOnWindows ? -2147022975 : -2146232800).And
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded HResult values (-2147022975 and -2146232800) are duplicated across test files. Consider defining these as shared constants in a test utilities class to ensure consistency and easier maintenance.

Suggested change
.WithHResult(Test.RunsOnWindows ? -2147022975 : -2146232800).And
.WithHResult(Test.RunsOnWindows ? TestConstants.HResultTooManySymbolicLinksWindows : TestConstants.HResultTooManySymbolicLinksNonWindows).And

Copilot uses AI. Check for mistakes.
.WithMessageContaining($"'{directoryInfo.FullName}'");
}

[Theory]
Expand Down Expand Up @@ -53,10 +80,7 @@ IFileSystemInfo innerLink

[Theory]
[AutoData]
public async Task ResolveLinkTarget_ShouldReturnImmediateFile(
string path,
string pathToTarget
)
public async Task ResolveLinkTarget_ShouldReturnImmediateFile(string path, string pathToTarget)
{
IDirectoryInfo targetDir = FileSystem.DirectoryInfo.New(pathToTarget);
targetDir.Create();
Expand Down Expand Up @@ -125,5 +149,41 @@ IFileSystemInfo outerLink

await That(resolvedTarget?.FullName).IsEqualTo(targetDir.FullName);
}

[Theory]
[AutoData]
public async Task ResolveLinkTarget_OfDifferentTypes_ShouldThrow(
string directoryName,
string fileLinkName,
string directoryLinkName
)
{
IDirectoryInfo targetDirectory = FileSystem.Directory.CreateDirectory(directoryName);

IFileSystemInfo fileSymLink = FileSystem.File.CreateSymbolicLink(
fileLinkName, targetDirectory.FullName
);

IFileSystemInfo dirSymLink = FileSystem.Directory.CreateSymbolicLink(
directoryLinkName, fileSymLink.FullName
);

string? Act()
{
return dirSymLink.ResolveLinkTarget(true)?.FullName;
}

if (Test.RunsOnWindows)
{
await That(Act).Throws<IOException>()
.WithMessage(
$@"^.*directory.*invalid.*\'{Regex.Escape(dirSymLink.FullName)}\'"
).AsRegex().And.WithHResult(-2147024629);
}
else
{
await That(Act()).IsEqualTo(targetDirectory.FullName);
}
}
}
#endif
Loading
Loading