Skip to content

Commit e7f364e

Browse files
authored
fix(FileSystem): When creating chained sym links of different types MockFileSystem in windows doesn't throw (#833)
Closes #818
1 parent 7c6b256 commit e7f364e

File tree

7 files changed

+203
-55
lines changed

7 files changed

+203
-55
lines changed

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
<ItemGroup>
3636
<PackageVersion Include="AutoFixture.AutoNSubstitute" Version="5.0.0-preview0012"/>
3737
<PackageVersion Include="AutoFixture.Xunit3" Version="5.0.0-preview0012"/>
38-
<PackageVersion Include="aweXpect" Version="2.21.0"/>
38+
<PackageVersion Include="aweXpect" Version="2.21.1"/>
3939
<PackageVersion Include="aweXpect.Testably" Version="0.11.0"/>
4040
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
4141
<PackageVersion Include="xunit.v3" Version="2.0.3"/>

Source/Testably.Abstractions.Testing/FileSystem/FileSystemInfoMock.cs

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -336,27 +336,14 @@ public void Refresh()
336336
/// <inheritdoc cref="IFileSystemInfo.ResolveLinkTarget(bool)" />
337337
public IFileSystemInfo? ResolveLinkTarget(bool returnFinalTarget)
338338
{
339-
using IDisposable registration = RegisterPathMethod(nameof(ResolveLinkTarget),
340-
returnFinalTarget);
339+
using IDisposable registration = RegisterPathMethod(
340+
nameof(ResolveLinkTarget), returnFinalTarget
341+
);
341342

342-
try
343-
{
344-
IStorageLocation? targetLocation =
345-
_fileSystem.Storage.ResolveLinkTarget(
346-
Location,
347-
returnFinalTarget);
348-
if (targetLocation != null)
349-
{
350-
return New(targetLocation, _fileSystem);
351-
}
343+
IStorageLocation? targetLocation
344+
= _fileSystem.Storage.ResolveLinkTarget(Location, returnFinalTarget);
352345

353-
return null;
354-
}
355-
catch (IOException ex) when (ex.HResult != -2147024773)
356-
{
357-
throw ExceptionFactory.FileNameCannotBeResolved(Location.FullPath,
358-
_fileSystem.Execute.IsWindows ? -2147022975 : -2146232800);
359-
}
346+
return targetLocation != null ? New(targetLocation, _fileSystem) : null;
360347
}
361348
#endif
362349

Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,13 @@ internal static ArgumentOutOfRangeException TimerArgumentOutOfRange(string prope
289289
internal static TimeoutException TimerWaitTimeoutException(int executionCount, int timeout)
290290
=> new($"The execution count {executionCount} was not reached in {timeout}ms.");
291291

292-
#if FEATURE_FILESYSTEM_UNIXFILEMODE
293-
internal static UnauthorizedAccessException UnixFileModeAccessDenied(string path)
292+
internal static UnauthorizedAccessException AccessDenied(string path)
294293
=> new($"Access to the path '{path}' is denied.")
295294
{
296295
#if FEATURE_EXCEPTION_HRESULT
297296
HResult = -2147024891,
298297
#endif
299298
};
300-
#endif
301299

302300
internal static PlatformNotSupportedException UnixFileModeNotSupportedOnThisPlatform()
303301
=> new("Unix file modes are not supported on this platform.")

Source/Testably.Abstractions.Testing/Storage/InMemoryContainer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ public IStorageAccessHandle RequestAccess(FileAccess access, FileShare share,
196196
if (!deleteAccess && !_fileSystem.UnixFileModeStrategy
197197
.IsAccessGranted(_location.FullPath, _extensibility, UnixFileMode, access))
198198
{
199-
throw ExceptionFactory.UnixFileModeAccessDenied(_location.FullPath);
199+
throw ExceptionFactory.AccessDenied(_location.FullPath);
200200
}
201201
#endif
202202

Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -558,19 +558,28 @@ public IStorageContainer GetOrCreateContainer(
558558

559559
#if FEATURE_FILESYSTEM_LINK
560560
/// <inheritdoc cref="IStorage.ResolveLinkTarget(IStorageLocation, bool)" />
561-
public IStorageLocation? ResolveLinkTarget(IStorageLocation location,
562-
bool returnFinalTarget = false)
561+
public IStorageLocation? ResolveLinkTarget(
562+
IStorageLocation location,
563+
bool returnFinalTarget = false
564+
)
563565
{
564566
if (!_containers.TryGetValue(location, out IStorageContainer? initialContainer)
565567
|| initialContainer.LinkTarget == null)
566568
{
567569
return null;
568570
}
569571

570-
IStorageLocation? nextLocation =
571-
_fileSystem.Storage.GetLocation(initialContainer.LinkTarget);
572+
IStorageLocation? nextLocation
573+
= _fileSystem.Storage.GetLocation(initialContainer.LinkTarget);
574+
575+
if (!_containers.TryGetValue(nextLocation, out IStorageContainer? container))
576+
{
577+
return nextLocation;
578+
}
579+
580+
ThrowOnLinkTypeChange(initialContainer, location, container);
572581

573-
if (returnFinalTarget && _containers.TryGetValue(nextLocation, out IStorageContainer? container) && container.LinkTarget != null)
582+
if (returnFinalTarget && container.LinkTarget != null)
574583
{
575584
nextLocation = ResolveFinalLinkTarget(container, location);
576585
}
@@ -755,7 +764,7 @@ private void CheckAndAdjustParentDirectoryTimes(IStorageLocation location)
755764
catch (UnauthorizedAccessException)
756765
{
757766
// On Unix, if the parent directory is not writable, we include the child path in the exception.
758-
throw ExceptionFactory.UnixFileModeAccessDenied(location.FullPath);
767+
throw ExceptionFactory.AccessDenied(location.FullPath);
759768
}
760769
#else
761770
using (parentContainer.RequestAccess(FileAccess.Write, FileShare.ReadWrite, onBehalfOfLocation: location))
@@ -1022,11 +1031,14 @@ private bool IncludeItemInEnumeration(
10221031
}
10231032

10241033
#if FEATURE_FILESYSTEM_LINK
1025-
private IStorageLocation? ResolveFinalLinkTarget(IStorageContainer container,
1026-
IStorageLocation originalLocation)
1034+
private IStorageLocation? ResolveFinalLinkTarget(
1035+
IStorageContainer container,
1036+
IStorageLocation originalLocation
1037+
)
10271038
{
10281039
int maxResolveLinks = _fileSystem.Execute.IsWindows ? 63 : 40;
10291040
IStorageLocation? nextLocation = null;
1041+
10301042
for (int i = 1; i < maxResolveLinks; i++)
10311043
{
10321044
if (container.LinkTarget == null)
@@ -1035,23 +1047,51 @@ private bool IncludeItemInEnumeration(
10351047
}
10361048

10371049
nextLocation = _fileSystem.Storage.GetLocation(container.LinkTarget);
1038-
if (!_containers.TryGetValue(nextLocation,
1039-
out IStorageContainer? nextContainer))
1050+
1051+
if (!_containers.TryGetValue(nextLocation, out IStorageContainer? nextContainer))
10401052
{
10411053
return nextLocation;
10421054
}
10431055

1056+
ThrowOnLinkTypeChange(container, originalLocation, nextContainer);
1057+
10441058
container = nextContainer;
10451059
}
10461060

10471061
if (container.LinkTarget != null)
10481062
{
10491063
throw ExceptionFactory.FileNameCannotBeResolved(
1050-
originalLocation.FullPath);
1064+
originalLocation.FullPath, _fileSystem.Execute.IsWindows ? -2147022975 : -2146232800
1065+
);
10511066
}
10521067

10531068
return nextLocation;
10541069
}
1070+
1071+
private void ThrowOnLinkTypeChange(
1072+
IStorageContainer previous,
1073+
IStorageLocation previousLocation,
1074+
IStorageContainer next
1075+
)
1076+
{
1077+
if (!_fileSystem.Execute.IsWindows)
1078+
{
1079+
return;
1080+
}
1081+
1082+
if (previous.Type == next.Type)
1083+
{
1084+
return;
1085+
}
1086+
1087+
switch (previous.Type)
1088+
{
1089+
case FileSystemTypes.File:
1090+
throw ExceptionFactory.AccessDenied(previousLocation.FullPath);
1091+
case FileSystemTypes.Directory:
1092+
throw ExceptionFactory.InvalidDirectoryName(previousLocation.FullPath);
1093+
}
1094+
}
10551095
#endif
10561096

10571097
/// <summary>

Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/ResolveLinkTargetTests.cs

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,51 @@
11
#if FEATURE_FILESYSTEM_LINK
22
using System.IO;
3+
using System.Text.RegularExpressions;
34

45
namespace Testably.Abstractions.Tests.FileSystem.DirectoryInfo;
56

67
[FileSystemTests]
78
public partial class ResolveLinkTargetTests
89
{
10+
#region Test Setup
11+
12+
/// <summary>
13+
/// The maximum number of symbolic links that are followed.<br />
14+
/// <see href="https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks" />
15+
/// </summary>
16+
private int MaxResolveLinks => Test.RunsOnWindows ? 63 : 40;
17+
18+
#endregion
19+
920
[Theory]
1021
[AutoData]
11-
public async Task ResolveLinkTarget_ShouldThrow(string path)
22+
public async Task ResolveLinkTarget_FinalTargetWithTooManyLevels_ShouldThrowIOException(
23+
string path,
24+
string pathToFinalTarget
25+
)
1226
{
13-
IFileSystemInfo link = FileSystem.Directory.CreateSymbolicLink(path, path + "-start");
27+
int maxLinks = MaxResolveLinks + 1;
28+
FileSystem.Directory.CreateDirectory(pathToFinalTarget);
29+
string previousPath = pathToFinalTarget;
30+
31+
for (int i = 0; i < maxLinks; i++)
32+
{
33+
string newPath = $"{path}-{i}";
34+
IDirectoryInfo linkDirectoryInfo = FileSystem.DirectoryInfo.New(newPath);
35+
linkDirectoryInfo.CreateAsSymbolicLink(previousPath);
36+
previousPath = newPath;
37+
}
38+
39+
IDirectoryInfo directoryInfo = FileSystem.DirectoryInfo.New(previousPath);
1440

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

21-
await That(() => link.ResolveLinkTarget(true)).Throws<IOException>();
46+
await That(Act).Throws<IOException>()
47+
.WithHResult(Test.RunsOnWindows ? -2147022975 : -2146232800).And
48+
.WithMessageContaining($"'{directoryInfo.FullName}'");
2249
}
2350

2451
[Theory]
@@ -53,10 +80,7 @@ IFileSystemInfo innerLink
5380

5481
[Theory]
5582
[AutoData]
56-
public async Task ResolveLinkTarget_ShouldReturnImmediateFile(
57-
string path,
58-
string pathToTarget
59-
)
83+
public async Task ResolveLinkTarget_ShouldReturnImmediateFile(string path, string pathToTarget)
6084
{
6185
IDirectoryInfo targetDir = FileSystem.DirectoryInfo.New(pathToTarget);
6286
targetDir.Create();
@@ -125,5 +149,41 @@ IFileSystemInfo outerLink
125149

126150
await That(resolvedTarget?.FullName).IsEqualTo(targetDir.FullName);
127151
}
152+
153+
[Theory]
154+
[AutoData]
155+
public async Task ResolveLinkTarget_OfDifferentTypes_ShouldThrow(
156+
string directoryName,
157+
string fileLinkName,
158+
string directoryLinkName
159+
)
160+
{
161+
IDirectoryInfo targetDirectory = FileSystem.Directory.CreateDirectory(directoryName);
162+
163+
IFileSystemInfo fileSymLink = FileSystem.File.CreateSymbolicLink(
164+
fileLinkName, targetDirectory.FullName
165+
);
166+
167+
IFileSystemInfo dirSymLink = FileSystem.Directory.CreateSymbolicLink(
168+
directoryLinkName, fileSymLink.FullName
169+
);
170+
171+
string? Act()
172+
{
173+
return dirSymLink.ResolveLinkTarget(true)?.FullName;
174+
}
175+
176+
if (Test.RunsOnWindows)
177+
{
178+
await That(Act).Throws<IOException>()
179+
.WithMessage(
180+
$@"^.*directory.*invalid.*\'{Regex.Escape(dirSymLink.FullName)}\'"
181+
).AsRegex().And.WithHResult(-2147024629);
182+
}
183+
else
184+
{
185+
await That(Act()).IsEqualTo(targetDirectory.FullName);
186+
}
187+
}
128188
}
129189
#endif

0 commit comments

Comments
 (0)