Skip to content

Commit 966b137

Browse files
authored
feat: add symbolic link functionality (#965)
Implement the `CreateSymbolicLink` and `ResolveLinkTarget` methods on `Directory`, `DirectoryInfo`, `File` and `FileInfo`.
1 parent f6c2971 commit 966b137

File tree

12 files changed

+389
-23
lines changed

12 files changed

+389
-23
lines changed

src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDirectory.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,43 @@ public override void Move(string sourceDirName, string destDirName)
530530
/// <inheritdoc />
531531
public override IFileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget)
532532
{
533-
throw CommonExceptions.NotImplemented();
533+
var initialContainer = mockFileDataAccessor.GetFile(linkPath);
534+
if (initialContainer.LinkTarget != null)
535+
{
536+
var nextLocation = initialContainer.LinkTarget;
537+
var nextContainer = mockFileDataAccessor.GetFile(nextLocation);
538+
539+
if (returnFinalTarget)
540+
{
541+
// The maximum number of symbolic links that are followed:
542+
// https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks
543+
int maxResolveLinks = XFS.IsWindowsPlatform() ? 63 : 40;
544+
for (int i = 1; i < maxResolveLinks; i++)
545+
{
546+
if (nextContainer.LinkTarget == null)
547+
{
548+
break;
549+
}
550+
nextLocation = nextContainer.LinkTarget;
551+
nextContainer = mockFileDataAccessor.GetFile(nextLocation);
552+
}
553+
554+
if (nextContainer.LinkTarget != null)
555+
{
556+
throw new IOException($"The name of the file cannot be resolved by the system. : '{linkPath}'");
557+
}
558+
}
559+
560+
if (nextContainer.IsDirectory)
561+
{
562+
return new MockDirectoryInfo(mockFileDataAccessor, nextLocation);
563+
}
564+
else
565+
{
566+
return new MockFileInfo(mockFileDataAccessor, nextLocation);
567+
}
568+
}
569+
throw new IOException($"The name of the file cannot be resolved by the system. : '{linkPath}'");
534570
}
535571

536572
#endif

src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDirectoryInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public MockDirectoryInfo(IMockFileDataAccessor mockFileDataAccessor, string dire
5252
/// <inheritdoc />
5353
public override void CreateAsSymbolicLink(string pathToTarget)
5454
{
55-
throw CommonExceptions.NotImplemented();
55+
FileSystem.Directory.CreateSymbolicLink(FullName, pathToTarget);
5656
}
5757
#endif
5858

@@ -74,7 +74,7 @@ public override void Refresh()
7474
/// <inheritdoc />
7575
public override IFileSystemInfo ResolveLinkTarget(bool returnFinalTarget)
7676
{
77-
throw CommonExceptions.NotImplemented();
77+
return FileSystem.Directory.ResolveLinkTarget(FullName, returnFinalTarget);
7878
}
7979
#endif
8080

src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFile.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,43 @@ public override void Replace(string sourceFileName, string destinationFileName,
777777
/// <inheritdoc />
778778
public override IFileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget)
779779
{
780-
throw CommonExceptions.NotImplemented();
780+
var initialContainer = mockFileDataAccessor.GetFile(linkPath);
781+
if (initialContainer.LinkTarget != null)
782+
{
783+
var nextLocation = initialContainer.LinkTarget;
784+
var nextContainer = mockFileDataAccessor.GetFile(nextLocation);
785+
786+
if (returnFinalTarget)
787+
{
788+
// The maximum number of symbolic links that are followed:
789+
// https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks
790+
int maxResolveLinks = XFS.IsWindowsPlatform() ? 63 : 40;
791+
for (int i = 1; i < maxResolveLinks; i++)
792+
{
793+
if (nextContainer.LinkTarget == null)
794+
{
795+
break;
796+
}
797+
nextLocation = nextContainer.LinkTarget;
798+
nextContainer = mockFileDataAccessor.GetFile(nextLocation);
799+
}
800+
801+
if (nextContainer.LinkTarget != null)
802+
{
803+
throw new IOException($"The name of the file cannot be resolved by the system. : '{linkPath}'");
804+
}
805+
}
806+
807+
if (nextContainer.IsDirectory)
808+
{
809+
return new MockDirectoryInfo(mockFileDataAccessor, nextLocation);
810+
}
811+
else
812+
{
813+
return new MockFileInfo(mockFileDataAccessor, nextLocation);
814+
}
815+
}
816+
throw new IOException($"The name of the file cannot be resolved by the system. : '{linkPath}'");
781817
}
782818
#endif
783819

src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public MockFileInfo(IMockFileDataAccessor mockFileSystem, string path) : base(mo
2929
/// <inheritdoc />
3030
public override void CreateAsSymbolicLink(string pathToTarget)
3131
{
32-
throw CommonExceptions.NotImplemented();
32+
FileSystem.File.CreateSymbolicLink(FullName, pathToTarget);
3333
}
3434
#endif
3535

@@ -51,7 +51,7 @@ public override void Refresh()
5151
/// <inheritdoc />
5252
public override IFileSystemInfo ResolveLinkTarget(bool returnFinalTarget)
5353
{
54-
throw CommonExceptions.NotImplemented();
54+
return FileSystem.File.ResolveLinkTarget(FullName, returnFinalTarget);
5555
}
5656
#endif
5757

src/TestableIO.System.IO.Abstractions.Wrappers/DirectoryInfoWrapper.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public DirectoryInfoWrapper(IFileSystem fileSystem, DirectoryInfo instance) : ba
2222
/// <inheritdoc />
2323
public override void CreateAsSymbolicLink(string pathToTarget)
2424
{
25-
throw new NotImplementedException();
25+
instance.CreateAsSymbolicLink(pathToTarget);
2626
}
2727
#endif
2828

@@ -42,7 +42,8 @@ public override void Refresh()
4242
/// <inheritdoc />
4343
public override IFileSystemInfo ResolveLinkTarget(bool returnFinalTarget)
4444
{
45-
throw new NotImplementedException();
45+
return instance.ResolveLinkTarget(returnFinalTarget)
46+
.WrapFileSystemInfo(FileSystem);
4647
}
4748
#endif
4849

src/TestableIO.System.IO.Abstractions.Wrappers/DirectoryWrapper.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public override IDirectoryInfo CreateDirectory(string path, UnixFileMode unixCre
3434
/// <inheritdoc />
3535
public override IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
3636
{
37-
return Directory.CreateSymbolicLink(path, pathToTarget).WrapFileSystemInfo(FileSystem);
37+
return Directory.CreateSymbolicLink(path, pathToTarget)
38+
.WrapFileSystemInfo(FileSystem);
3839
}
3940
#endif
4041

@@ -219,7 +220,8 @@ public override void Move(string sourceDirName, string destDirName)
219220
/// <inheritdoc />
220221
public override IFileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget)
221222
{
222-
throw new NotSupportedException("TODO: Missing object implementing `IFileSystemInfo`");
223+
return Directory.ResolveLinkTarget(linkPath, returnFinalTarget)
224+
.WrapFileSystemInfo(FileSystem);
223225
}
224226
#endif
225227

src/TestableIO.System.IO.Abstractions.Wrappers/FileInfoWrapper.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ public override void Refresh()
3939
/// <inheritdoc />
4040
public override IFileSystemInfo ResolveLinkTarget(bool returnFinalTarget)
4141
{
42-
throw new NotImplementedException();
42+
return instance.ResolveLinkTarget(returnFinalTarget)
43+
.WrapFileSystemInfo(FileSystem);
4344
}
4445
#endif
4546

src/TestableIO.System.IO.Abstractions.Wrappers/FileWrapper.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ public override FileSystemStream Create(string path, int bufferSize, FileOptions
7979
/// <inheritdoc />
8080
public override IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
8181
{
82-
return File.CreateSymbolicLink(path, pathToTarget).WrapFileSystemInfo(FileSystem);
82+
return File.CreateSymbolicLink(path, pathToTarget)
83+
.WrapFileSystemInfo(FileSystem);
8384
}
8485
#endif
8586
/// <inheritdoc />
@@ -347,7 +348,8 @@ public override void Replace(string sourceFileName, string destinationFileName,
347348
/// <inheritdoc />
348349
public override IFileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget)
349350
{
350-
throw new NotImplementedException();
351+
return File.ResolveLinkTarget(linkPath, returnFinalTarget)
352+
.WrapFileSystemInfo(FileSystem);
351353
}
352354
#endif
353355

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Runtime.Versioning;
4+
using System.Security.AccessControl;
5+
using NUnit.Framework;
6+
7+
namespace System.IO.Abstractions.TestingHelpers.Tests
8+
{
9+
using XFS = MockUnixSupport;
10+
11+
[TestFixture]
12+
public class MockDirectoryInfoSymlinkTests
13+
{
14+
15+
#if FEATURE_CREATE_SYMBOLIC_LINK
16+
17+
[Test]
18+
public void MockDirectoryInfo_ResolveLinkTarget_ShouldReturnPathOfTargetLink()
19+
{
20+
var fileSystem = new MockFileSystem();
21+
fileSystem.Directory.CreateDirectory("bar");
22+
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
23+
24+
var result = fileSystem.DirectoryInfo.New("foo").ResolveLinkTarget(false);
25+
26+
Assert.AreEqual("bar", result.Name);
27+
}
28+
29+
[Test]
30+
public void MockDirectoryInfo_ResolveLinkTarget_WithFinalTarget_ShouldReturnPathOfTargetLink()
31+
{
32+
var fileSystem = new MockFileSystem();
33+
fileSystem.Directory.CreateDirectory("bar");
34+
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
35+
fileSystem.Directory.CreateSymbolicLink("foo1", "foo");
36+
37+
var result = fileSystem.DirectoryInfo.New("foo1").ResolveLinkTarget(true);
38+
39+
Assert.AreEqual("bar", result.Name);
40+
}
41+
42+
[Test]
43+
public void MockDirectoryInfo_ResolveLinkTarget_WithoutFinalTarget_ShouldReturnFirstLink()
44+
{
45+
var fileSystem = new MockFileSystem();
46+
fileSystem.Directory.CreateDirectory("bar");
47+
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
48+
fileSystem.Directory.CreateSymbolicLink("foo1", "foo");
49+
50+
var result = fileSystem.DirectoryInfo.New("foo1").ResolveLinkTarget(false);
51+
52+
Assert.AreEqual("foo", result.Name);
53+
}
54+
55+
[Test]
56+
public void MockDirectoryInfo_ResolveLinkTarget_WithoutTargetLink_ShouldThrowIOException()
57+
{
58+
var fileSystem = new MockFileSystem();
59+
fileSystem.Directory.CreateDirectory("bar");
60+
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
61+
62+
Assert.Throws<IOException>(() =>
63+
{
64+
fileSystem.DirectoryInfo.New("bar").ResolveLinkTarget(false);
65+
});
66+
}
67+
#endif
68+
}
69+
}

tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectorySymlinkTests.cs

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
using System.Collections.Generic;
2-
using System.Linq;
3-
using System.Runtime.Versioning;
4-
using System.Security.AccessControl;
5-
using NUnit.Framework;
1+
using NUnit.Framework;
62

73
namespace System.IO.Abstractions.TestingHelpers.Tests
84
{
@@ -201,6 +197,85 @@ public void MockDirectory_CreateSymbolicLink_ShouldFailIfTargetDoesNotExist()
201197
// Assert
202198
Assert.That(ex.Message.Contains(pathToTarget));
203199
}
200+
201+
[Test]
202+
public void MockDirectory_ResolveLinkTarget_ShouldReturnPathOfTargetLink()
203+
{
204+
var fileSystem = new MockFileSystem();
205+
fileSystem.Directory.CreateDirectory("bar");
206+
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
207+
208+
var result = fileSystem.Directory.ResolveLinkTarget("foo", false);
209+
210+
Assert.AreEqual("bar", result.Name);
211+
}
212+
213+
[Test]
214+
public void MockDirectory_ResolveLinkTarget_WithFinalTarget_ShouldReturnPathOfTargetLink()
215+
{
216+
// The maximum number of symbolic links that are followed:
217+
// https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks
218+
var maxResolveLinks = XFS.IsWindowsPlatform() ? 63 : 40;
219+
var fileSystem = new MockFileSystem();
220+
fileSystem.Directory.CreateDirectory("bar");
221+
var previousPath = "bar";
222+
for (int i = 0; i < maxResolveLinks; i++)
223+
{
224+
string newPath = $"foo-{i}";
225+
fileSystem.Directory.CreateSymbolicLink(newPath, previousPath);
226+
previousPath = newPath;
227+
}
228+
229+
var result = fileSystem.Directory.ResolveLinkTarget(previousPath, true);
230+
231+
Assert.AreEqual("bar", result.Name);
232+
}
233+
234+
[Test]
235+
public void MockDirectory_ResolveLinkTarget_WithFinalTargetWithTooManyLinks_ShouldThrowIOException()
236+
{
237+
// The maximum number of symbolic links that are followed:
238+
// https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks
239+
var maxResolveLinks = XFS.IsWindowsPlatform() ? 63 : 40;
240+
maxResolveLinks++;
241+
var fileSystem = new MockFileSystem();
242+
fileSystem.Directory.CreateDirectory("bar");
243+
var previousPath = "bar";
244+
for (int i = 0; i < maxResolveLinks; i++)
245+
{
246+
string newPath = $"foo-{i}";
247+
fileSystem.Directory.CreateSymbolicLink(newPath, previousPath);
248+
previousPath = newPath;
249+
}
250+
251+
Assert.Throws<IOException>(() => fileSystem.Directory.ResolveLinkTarget(previousPath, true));
252+
}
253+
254+
[Test]
255+
public void MockDirectory_ResolveLinkTarget_WithoutFinalTarget_ShouldReturnFirstLink()
256+
{
257+
var fileSystem = new MockFileSystem();
258+
fileSystem.Directory.CreateDirectory("bar");
259+
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
260+
fileSystem.Directory.CreateSymbolicLink("foo1", "foo");
261+
262+
var result = fileSystem.Directory.ResolveLinkTarget("foo1", false);
263+
264+
Assert.AreEqual("foo", result.Name);
265+
}
266+
267+
[Test]
268+
public void MockDirectory_ResolveLinkTarget_WithoutTargetLink_ShouldThrowIOException()
269+
{
270+
var fileSystem = new MockFileSystem();
271+
fileSystem.Directory.CreateDirectory("bar");
272+
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
273+
274+
Assert.Throws<IOException>(() =>
275+
{
276+
fileSystem.Directory.ResolveLinkTarget("bar", false);
277+
});
278+
}
204279
#endif
205280
}
206281
}

0 commit comments

Comments
 (0)