Skip to content

Commit

Permalink
feat: add symbolic link functionality (#965)
Browse files Browse the repository at this point in the history
Implement the `CreateSymbolicLink` and `ResolveLinkTarget` methods on `Directory`, `DirectoryInfo`, `File` and `FileInfo`.
  • Loading branch information
vbreuss authored Apr 11, 2023
1 parent f6c2971 commit 966b137
Show file tree
Hide file tree
Showing 12 changed files with 389 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,43 @@ public override void Move(string sourceDirName, string destDirName)
/// <inheritdoc />
public override IFileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget)
{
throw CommonExceptions.NotImplemented();
var initialContainer = mockFileDataAccessor.GetFile(linkPath);
if (initialContainer.LinkTarget != null)
{
var nextLocation = initialContainer.LinkTarget;
var nextContainer = mockFileDataAccessor.GetFile(nextLocation);

if (returnFinalTarget)
{
// The maximum number of symbolic links that are followed:
// https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks
int maxResolveLinks = XFS.IsWindowsPlatform() ? 63 : 40;
for (int i = 1; i < maxResolveLinks; i++)
{
if (nextContainer.LinkTarget == null)
{
break;
}
nextLocation = nextContainer.LinkTarget;
nextContainer = mockFileDataAccessor.GetFile(nextLocation);
}

if (nextContainer.LinkTarget != null)
{
throw new IOException($"The name of the file cannot be resolved by the system. : '{linkPath}'");
}
}

if (nextContainer.IsDirectory)
{
return new MockDirectoryInfo(mockFileDataAccessor, nextLocation);
}
else
{
return new MockFileInfo(mockFileDataAccessor, nextLocation);
}
}
throw new IOException($"The name of the file cannot be resolved by the system. : '{linkPath}'");
}

#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public MockDirectoryInfo(IMockFileDataAccessor mockFileDataAccessor, string dire
/// <inheritdoc />
public override void CreateAsSymbolicLink(string pathToTarget)
{
throw CommonExceptions.NotImplemented();
FileSystem.Directory.CreateSymbolicLink(FullName, pathToTarget);
}
#endif

Expand All @@ -74,7 +74,7 @@ public override void Refresh()
/// <inheritdoc />
public override IFileSystemInfo ResolveLinkTarget(bool returnFinalTarget)
{
throw CommonExceptions.NotImplemented();
return FileSystem.Directory.ResolveLinkTarget(FullName, returnFinalTarget);
}
#endif

Expand Down
38 changes: 37 additions & 1 deletion src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,43 @@ public override void Replace(string sourceFileName, string destinationFileName,
/// <inheritdoc />
public override IFileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget)
{
throw CommonExceptions.NotImplemented();
var initialContainer = mockFileDataAccessor.GetFile(linkPath);
if (initialContainer.LinkTarget != null)
{
var nextLocation = initialContainer.LinkTarget;
var nextContainer = mockFileDataAccessor.GetFile(nextLocation);

if (returnFinalTarget)
{
// The maximum number of symbolic links that are followed:
// https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks
int maxResolveLinks = XFS.IsWindowsPlatform() ? 63 : 40;
for (int i = 1; i < maxResolveLinks; i++)
{
if (nextContainer.LinkTarget == null)
{
break;
}
nextLocation = nextContainer.LinkTarget;
nextContainer = mockFileDataAccessor.GetFile(nextLocation);
}

if (nextContainer.LinkTarget != null)
{
throw new IOException($"The name of the file cannot be resolved by the system. : '{linkPath}'");
}
}

if (nextContainer.IsDirectory)
{
return new MockDirectoryInfo(mockFileDataAccessor, nextLocation);
}
else
{
return new MockFileInfo(mockFileDataAccessor, nextLocation);
}
}
throw new IOException($"The name of the file cannot be resolved by the system. : '{linkPath}'");
}
#endif

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public MockFileInfo(IMockFileDataAccessor mockFileSystem, string path) : base(mo
/// <inheritdoc />
public override void CreateAsSymbolicLink(string pathToTarget)
{
throw CommonExceptions.NotImplemented();
FileSystem.File.CreateSymbolicLink(FullName, pathToTarget);
}
#endif

Expand All @@ -51,7 +51,7 @@ public override void Refresh()
/// <inheritdoc />
public override IFileSystemInfo ResolveLinkTarget(bool returnFinalTarget)
{
throw CommonExceptions.NotImplemented();
return FileSystem.File.ResolveLinkTarget(FullName, returnFinalTarget);
}
#endif

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public DirectoryInfoWrapper(IFileSystem fileSystem, DirectoryInfo instance) : ba
/// <inheritdoc />
public override void CreateAsSymbolicLink(string pathToTarget)
{
throw new NotImplementedException();
instance.CreateAsSymbolicLink(pathToTarget);
}
#endif

Expand All @@ -42,7 +42,8 @@ public override void Refresh()
/// <inheritdoc />
public override IFileSystemInfo ResolveLinkTarget(bool returnFinalTarget)
{
throw new NotImplementedException();
return instance.ResolveLinkTarget(returnFinalTarget)
.WrapFileSystemInfo(FileSystem);
}
#endif

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public override IDirectoryInfo CreateDirectory(string path, UnixFileMode unixCre
/// <inheritdoc />
public override IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
return Directory.CreateSymbolicLink(path, pathToTarget).WrapFileSystemInfo(FileSystem);
return Directory.CreateSymbolicLink(path, pathToTarget)
.WrapFileSystemInfo(FileSystem);
}
#endif

Expand Down Expand Up @@ -219,7 +220,8 @@ public override void Move(string sourceDirName, string destDirName)
/// <inheritdoc />
public override IFileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget)
{
throw new NotSupportedException("TODO: Missing object implementing `IFileSystemInfo`");
return Directory.ResolveLinkTarget(linkPath, returnFinalTarget)
.WrapFileSystemInfo(FileSystem);
}
#endif

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public override void Refresh()
/// <inheritdoc />
public override IFileSystemInfo ResolveLinkTarget(bool returnFinalTarget)
{
throw new NotImplementedException();
return instance.ResolveLinkTarget(returnFinalTarget)
.WrapFileSystemInfo(FileSystem);
}
#endif

Expand Down
6 changes: 4 additions & 2 deletions src/TestableIO.System.IO.Abstractions.Wrappers/FileWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ public override FileSystemStream Create(string path, int bufferSize, FileOptions
/// <inheritdoc />
public override IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
return File.CreateSymbolicLink(path, pathToTarget).WrapFileSystemInfo(FileSystem);
return File.CreateSymbolicLink(path, pathToTarget)
.WrapFileSystemInfo(FileSystem);
}
#endif
/// <inheritdoc />
Expand Down Expand Up @@ -347,7 +348,8 @@ public override void Replace(string sourceFileName, string destinationFileName,
/// <inheritdoc />
public override IFileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget)
{
throw new NotImplementedException();
return File.ResolveLinkTarget(linkPath, returnFinalTarget)
.WrapFileSystemInfo(FileSystem);
}
#endif

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using NUnit.Framework;

namespace System.IO.Abstractions.TestingHelpers.Tests
{
using XFS = MockUnixSupport;

[TestFixture]
public class MockDirectoryInfoSymlinkTests
{

#if FEATURE_CREATE_SYMBOLIC_LINK

[Test]
public void MockDirectoryInfo_ResolveLinkTarget_ShouldReturnPathOfTargetLink()
{
var fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("bar");
fileSystem.Directory.CreateSymbolicLink("foo", "bar");

var result = fileSystem.DirectoryInfo.New("foo").ResolveLinkTarget(false);

Assert.AreEqual("bar", result.Name);
}

[Test]
public void MockDirectoryInfo_ResolveLinkTarget_WithFinalTarget_ShouldReturnPathOfTargetLink()
{
var fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("bar");
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
fileSystem.Directory.CreateSymbolicLink("foo1", "foo");

var result = fileSystem.DirectoryInfo.New("foo1").ResolveLinkTarget(true);

Assert.AreEqual("bar", result.Name);
}

[Test]
public void MockDirectoryInfo_ResolveLinkTarget_WithoutFinalTarget_ShouldReturnFirstLink()
{
var fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("bar");
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
fileSystem.Directory.CreateSymbolicLink("foo1", "foo");

var result = fileSystem.DirectoryInfo.New("foo1").ResolveLinkTarget(false);

Assert.AreEqual("foo", result.Name);
}

[Test]
public void MockDirectoryInfo_ResolveLinkTarget_WithoutTargetLink_ShouldThrowIOException()
{
var fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("bar");
fileSystem.Directory.CreateSymbolicLink("foo", "bar");

Assert.Throws<IOException>(() =>
{
fileSystem.DirectoryInfo.New("bar").ResolveLinkTarget(false);
});
}
#endif
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using NUnit.Framework;
using NUnit.Framework;

namespace System.IO.Abstractions.TestingHelpers.Tests
{
Expand Down Expand Up @@ -201,6 +197,85 @@ public void MockDirectory_CreateSymbolicLink_ShouldFailIfTargetDoesNotExist()
// Assert
Assert.That(ex.Message.Contains(pathToTarget));
}

[Test]
public void MockDirectory_ResolveLinkTarget_ShouldReturnPathOfTargetLink()
{
var fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("bar");
fileSystem.Directory.CreateSymbolicLink("foo", "bar");

var result = fileSystem.Directory.ResolveLinkTarget("foo", false);

Assert.AreEqual("bar", result.Name);
}

[Test]
public void MockDirectory_ResolveLinkTarget_WithFinalTarget_ShouldReturnPathOfTargetLink()
{
// The maximum number of symbolic links that are followed:
// https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks
var maxResolveLinks = XFS.IsWindowsPlatform() ? 63 : 40;
var fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("bar");
var previousPath = "bar";
for (int i = 0; i < maxResolveLinks; i++)
{
string newPath = $"foo-{i}";
fileSystem.Directory.CreateSymbolicLink(newPath, previousPath);
previousPath = newPath;
}

var result = fileSystem.Directory.ResolveLinkTarget(previousPath, true);

Assert.AreEqual("bar", result.Name);
}

[Test]
public void MockDirectory_ResolveLinkTarget_WithFinalTargetWithTooManyLinks_ShouldThrowIOException()
{
// The maximum number of symbolic links that are followed:
// https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.resolvelinktarget?view=net-6.0#remarks
var maxResolveLinks = XFS.IsWindowsPlatform() ? 63 : 40;
maxResolveLinks++;
var fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("bar");
var previousPath = "bar";
for (int i = 0; i < maxResolveLinks; i++)
{
string newPath = $"foo-{i}";
fileSystem.Directory.CreateSymbolicLink(newPath, previousPath);
previousPath = newPath;
}

Assert.Throws<IOException>(() => fileSystem.Directory.ResolveLinkTarget(previousPath, true));
}

[Test]
public void MockDirectory_ResolveLinkTarget_WithoutFinalTarget_ShouldReturnFirstLink()
{
var fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("bar");
fileSystem.Directory.CreateSymbolicLink("foo", "bar");
fileSystem.Directory.CreateSymbolicLink("foo1", "foo");

var result = fileSystem.Directory.ResolveLinkTarget("foo1", false);

Assert.AreEqual("foo", result.Name);
}

[Test]
public void MockDirectory_ResolveLinkTarget_WithoutTargetLink_ShouldThrowIOException()
{
var fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("bar");
fileSystem.Directory.CreateSymbolicLink("foo", "bar");

Assert.Throws<IOException>(() =>
{
fileSystem.Directory.ResolveLinkTarget("bar", false);
});
}
#endif
}
}
Loading

0 comments on commit 966b137

Please sign in to comment.