From 706abfd5a2cbee2a08f0ba91a59da8a897b4b827 Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Thu, 23 May 2024 17:49:24 +0200 Subject: [PATCH 1/8] Add symlink support --- .../FileSystems/FileSystemEntryRedirect.cs | 5 + .../FileSystems/TestPhysicalFileSystem.cs | 107 ++++++++++++++++++ src/Zio.Tests/Zio.Tests.csproj | 1 + src/Zio.sln.DotSettings | 1 + src/Zio/FileEntry.cs | 4 +- src/Zio/FileSystems/ComposeFileSystem.cs | 6 + src/Zio/FileSystems/FileSystem.cs | 14 +++ src/Zio/FileSystems/MemoryFileSystem.cs | 6 + src/Zio/FileSystems/MountFileSystem.cs | 15 +++ src/Zio/FileSystems/PhysicalFileSystem.cs | 76 +++++++++++++ src/Zio/FileSystems/ReadOnlyFileSystem.cs | 6 + src/Zio/FileSystems/ZipArchiveFileSystem.cs | 6 + src/Zio/IFileSystem.cs | 7 ++ src/Zio/Interop.cs | 30 +++++ 14 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 src/Zio/Interop.cs diff --git a/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs b/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs index 0b9b075..83ebb22 100644 --- a/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs +++ b/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs @@ -120,6 +120,11 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time) _fs.GetFileSystemEntry(path).LastWriteTime = time; } + protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) + { + _fs.CreateSymbolicLink(path, pathToTarget); + } + protected override IEnumerable EnumeratePathsImpl(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) { return _fs.GetDirectoryEntry(path).EnumerateEntries(searchPattern, searchOption, searchTarget).Select(e => e.Path); diff --git a/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs b/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs index 20c3620..9344e7d 100644 --- a/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs +++ b/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using System.IO; +using System.Security.Principal; using System.Text; using Zio.FileSystems; @@ -400,4 +401,110 @@ public void TestFileExceptions() SafeDeleteFile(systemFilePath); } } + + [SkippableFact] + public void TestDirectorySymlink() + { +#if NETCOREAPP + if (OperatingSystem.IsWindows()) +#else + if (IsWindows) +#endif + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + + Skip.IfNot(principal.IsInRole(WindowsBuiltInRole.Administrator), "This test requires to be run as an administrator on Windows"); + } + + var fs = new PhysicalFileSystem(); + var pathInfo = fs.ConvertPathFromInternal(SystemPath); + var pathSource = pathInfo / "Source"; + var filePathSource = pathSource / "test.txt"; + var systemPathSource = fs.ConvertPathToInternal(pathSource); + var pathDest = pathInfo / "Dest"; + var filePathDest = pathDest / "test.txt"; + var systemPathDest = fs.ConvertPathToInternal(pathDest); + try + { + // CreateDirectory + Assert.False(Directory.Exists(systemPathSource)); + fs.CreateDirectory(pathSource); + Assert.True(Directory.Exists(systemPathSource)); + + // CreateFile / OpenFile + var fileStream = fs.CreateFile(filePathSource); + var buffer = Encoding.UTF8.GetBytes("This is a test"); + fileStream.Write(buffer, 0, buffer.Length); + fileStream.Dispose(); + Assert.Equal(buffer.Length, fs.GetFileLength(filePathSource)); + + // CreateSymbolicLink + fs.CreateSymbolicLink(pathDest, pathSource); + + // FileExists + Assert.True(fs.FileExists(filePathDest)); + Assert.Equal(buffer.Length, fs.GetFileLength(filePathDest)); + } + finally + { + SafeDeleteDirectory(systemPathSource); + SafeDeleteDirectory(systemPathDest); + } + } + + [SkippableFact] + public void TestFileSymlink() + { +#if NETCOREAPP + if (OperatingSystem.IsWindows()) +#else + if (IsWindows) +#endif + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + + Skip.IfNot(principal.IsInRole(WindowsBuiltInRole.Administrator), "This test requires to be run as an administrator on Windows"); + } + + var fs = new PhysicalFileSystem(); + var pathInfo = fs.ConvertPathFromInternal(SystemPath); + var pathSource = pathInfo / "source.txt"; + var systemPathSource = fs.ConvertPathToInternal(pathSource); + var pathDest = pathInfo / "dest.txt"; + var systemPathDest = fs.ConvertPathToInternal(pathDest); + try + { + // CreateEmptyFile + fs.CreateFile(pathSource).Dispose(); + + // CreateSymbolicLink + fs.CreateSymbolicLink(pathDest, pathSource); + + // FileExists + Assert.True(fs.FileExists(pathDest)); + + // CreateFile / OpenFile + var fileStream = fs.OpenFile(pathSource, FileMode.Open, FileAccess.ReadWrite); + var buffer = Encoding.UTF8.GetBytes("This is a test"); + fileStream.Write(buffer, 0, buffer.Length); + fileStream.Dispose(); + Assert.Equal(buffer.Length, fs.GetFileLength(pathSource)); + + // ReadAllBytes + // Note: we can't check the length, since on Windows the symlink length is 0 + var symlinkBuffer = fs.ReadAllBytes(pathDest); + Assert.Equal(buffer, symlinkBuffer); + + // FileEntry + var entry = fs.GetFileSystemEntry(pathDest); + Assert.True(entry.Attributes.HasFlag(FileAttributes.ReparsePoint)); + } + finally + { + SafeDeleteFile(systemPathSource); + SafeDeleteFile(systemPathDest); + } + } } \ No newline at end of file diff --git a/src/Zio.Tests/Zio.Tests.csproj b/src/Zio.Tests/Zio.Tests.csproj index 89d8391..2508a12 100644 --- a/src/Zio.Tests/Zio.Tests.csproj +++ b/src/Zio.Tests/Zio.Tests.csproj @@ -14,6 +14,7 @@ runtime; build; native; contentfiles; analyzers + diff --git a/src/Zio.sln.DotSettings b/src/Zio.sln.DotSettings index ec726d9..0e0c8ec 100644 --- a/src/Zio.sln.DotSettings +++ b/src/Zio.sln.DotSettings @@ -4,6 +4,7 @@ Copyright (c) Alexandre Mutel. All rights reserved. This file is licensed under the BSD-Clause 2 license. See the license.txt file in the project root for more information. + True True True True diff --git a/src/Zio/FileEntry.cs b/src/Zio/FileEntry.cs index 1833930..c49c561 100644 --- a/src/Zio/FileEntry.cs +++ b/src/Zio/FileEntry.cs @@ -284,7 +284,7 @@ public void WriteAllText(string content, Encoding encoding) /// /// Given a string and a file path, this method opens the specified file, appends the string to the end of the file, /// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised. - /// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the + /// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the /// path parameter must contain existing directories. /// public void AppendAllText(string content) @@ -301,7 +301,7 @@ public void AppendAllText(string content) /// /// Given a string and a file path, this method opens the specified file, appends the string to the end of the file, /// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised. - /// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the + /// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the /// path parameter must contain existing directories. /// public void AppendAllText(string content, Encoding encoding) diff --git a/src/Zio/FileSystems/ComposeFileSystem.cs b/src/Zio/FileSystems/ComposeFileSystem.cs index 5eb84b2..6d1a2df 100644 --- a/src/Zio/FileSystems/ComposeFileSystem.cs +++ b/src/Zio/FileSystems/ComposeFileSystem.cs @@ -186,6 +186,12 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time) FallbackSafe.SetLastWriteTime(ConvertPathToDelegate(path), time); } + /// + protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) + { + FallbackSafe.CreateSymbolicLink(ConvertPathToDelegate(path), ConvertPathToDelegate(pathToTarget)); + } + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/FileSystem.cs b/src/Zio/FileSystems/FileSystem.cs index 0f7feb0..c6c2821 100644 --- a/src/Zio/FileSystems/FileSystem.cs +++ b/src/Zio/FileSystems/FileSystem.cs @@ -431,6 +431,20 @@ public void SetLastWriteTime(UPath path, DateTime time) /// A containing the value to set for the last write date and time of path. This value is expressed in local time. protected abstract void SetLastWriteTimeImpl(UPath path, DateTime time); + /// + public void CreateSymbolicLink(UPath path, UPath pathToTarget) + { + AssertNotDisposed(); + CreateSymbolicLinkImpl(ValidatePath(path), ValidatePath(pathToTarget)); + } + + /// + /// Creates a symbolic link. + /// + /// The path of the symbolic link to create. + /// The path of the target for the symbolic link. + protected abstract void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget); + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/MemoryFileSystem.cs b/src/Zio/FileSystems/MemoryFileSystem.cs index c96c2e8..e2b1fb1 100644 --- a/src/Zio/FileSystems/MemoryFileSystem.cs +++ b/src/Zio/FileSystems/MemoryFileSystem.cs @@ -753,6 +753,12 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time) TryGetDispatcher()?.RaiseChange(path); } + /// + protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) + { + throw new NotSupportedException("Symbolic links are not supported by MemoryFileSystem"); + } + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/MountFileSystem.cs b/src/Zio/FileSystems/MountFileSystem.cs index 2a666aa..6da1b57 100644 --- a/src/Zio/FileSystems/MountFileSystem.cs +++ b/src/Zio/FileSystems/MountFileSystem.cs @@ -550,6 +550,21 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time) } } + /// + protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) + { + var originalSrcPath = path; + var mountfs = TryGetMountOrNext(ref path); + if (mountfs != null) + { + mountfs.CreateSymbolicLink(path, pathToTarget); + } + else + { + throw NewFileNotFoundException(originalSrcPath); + } + } + /// protected override IEnumerable EnumeratePathsImpl(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) { diff --git a/src/Zio/FileSystems/PhysicalFileSystem.cs b/src/Zio/FileSystems/PhysicalFileSystem.cs index cccfa1e..be12a75 100644 --- a/src/Zio/FileSystems/PhysicalFileSystem.cs +++ b/src/Zio/FileSystems/PhysicalFileSystem.cs @@ -439,6 +439,82 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time) } } + protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) + { + if (IsWithinSpecialDirectory(path)) + { + throw new UnauthorizedAccessException($"The access to `{path}` is denied"); + } + + if (IsWithinSpecialDirectory(pathToTarget)) + { + throw new UnauthorizedAccessException($"The access to `{pathToTarget}` is denied"); + } + + var systemPath = ConvertPathToInternal(path); + + if (File.Exists(systemPath)) + { + throw NewDestinationFileExistException(path); + } + + if (Directory.Exists(systemPath)) + { + throw NewDestinationDirectoryExistException(path); + } + + var systemPathToTarget = ConvertPathToInternal(pathToTarget); + + bool isDirectory; + + if (File.Exists(systemPathToTarget)) + { + isDirectory = false; + } + else if (Directory.Exists(systemPathToTarget)) + { + isDirectory = true; + } + else + { + throw NewDirectoryNotFoundException(path); + } + +#if NET7_0_OR_GREATER + if (isDirectory) + { + Directory.CreateSymbolicLink(systemPath, systemPathToTarget); + } + else + { + File.CreateSymbolicLink(systemPath, systemPathToTarget); + } +#else + bool success; + + if (IsOnWindows) + { + var type = isDirectory ? Interop.Windows.SymbolicLink.Directory : Interop.Windows.SymbolicLink.File; + + success = Interop.Windows.CreateSymbolicLink(systemPath, systemPathToTarget, type); + + if (!success && Marshal.GetLastWin32Error() == 1314) + { + throw new UnauthorizedAccessException($"Could not create symbolic link `{path}` to `{pathToTarget}` due to insufficient privileges"); + } + } + else + { + success = Interop.Unix.symlink(systemPathToTarget, systemPath) == 0; + } + + if (!success) + { + throw new IOException($"Could not create symbolic link `{path}` to `{pathToTarget}`"); + } +#endif + } + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/ReadOnlyFileSystem.cs b/src/Zio/FileSystems/ReadOnlyFileSystem.cs index 9986f82..5836460 100644 --- a/src/Zio/FileSystems/ReadOnlyFileSystem.cs +++ b/src/Zio/FileSystems/ReadOnlyFileSystem.cs @@ -129,6 +129,12 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time) throw new IOException(FileSystemIsReadOnly); } + /// + protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) + { + throw new IOException(FileSystemIsReadOnly); + } + // ---------------------------------------------- // Path // ---------------------------------------------- diff --git a/src/Zio/FileSystems/ZipArchiveFileSystem.cs b/src/Zio/FileSystems/ZipArchiveFileSystem.cs index 7c7ce23..196f442 100644 --- a/src/Zio/FileSystems/ZipArchiveFileSystem.cs +++ b/src/Zio/FileSystems/ZipArchiveFileSystem.cs @@ -817,6 +817,12 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time) entry.LastWriteTime = time; } + /// + protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) + { + throw new NotSupportedException("Symbolic links are not supported by ZipArchiveFileSystem"); + } + /// protected override IFileSystemWatcher WatchImpl(UPath path) { diff --git a/src/Zio/IFileSystem.cs b/src/Zio/IFileSystem.cs index 7de7037..6588269 100644 --- a/src/Zio/IFileSystem.cs +++ b/src/Zio/IFileSystem.cs @@ -163,6 +163,13 @@ public interface IFileSystem : IDisposable /// A containing the value to set for the last write date and time of path. This value is expressed in local time. void SetLastWriteTime(UPath path, DateTime time); + /// + /// Creates a symbolic link. + /// + /// The path of the symbolic link to create. + /// The path of the target for the symbolic link. + void CreateSymbolicLink(UPath path, UPath pathToTarget); + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/Interop.cs b/src/Zio/Interop.cs new file mode 100644 index 0000000..8d4a4d9 --- /dev/null +++ b/src/Zio/Interop.cs @@ -0,0 +1,30 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +#if !NET7_0_OR_GREATER +using System.Runtime.InteropServices; + +namespace Zio; + +internal static class Interop +{ + public static class Windows + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymbolicLink dwFlags); + + public enum SymbolicLink + { + File = 0, + Directory = 1 + } + } + + public static class Unix + { + [DllImport("libc", SetLastError = true)] + public static extern int symlink(string target, string linkpath); + } +} +#endif \ No newline at end of file From b216f9e93af0a69adf6ca603ca53a669cc02554e Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Thu, 23 May 2024 17:58:58 +0200 Subject: [PATCH 2/8] Add symlink delete test --- src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs b/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs index 9344e7d..0560c7f 100644 --- a/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs +++ b/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs @@ -445,6 +445,11 @@ public void TestDirectorySymlink() // FileExists Assert.True(fs.FileExists(filePathDest)); Assert.Equal(buffer.Length, fs.GetFileLength(filePathDest)); + + // RemoveDirectory + fs.DeleteDirectory(pathDest, false); + Assert.False(Directory.Exists(systemPathDest)); + Assert.True(Directory.Exists(systemPathSource)); } finally { @@ -500,6 +505,11 @@ public void TestFileSymlink() // FileEntry var entry = fs.GetFileSystemEntry(pathDest); Assert.True(entry.Attributes.HasFlag(FileAttributes.ReparsePoint)); + + // DeleteFile + fs.DeleteFile(pathDest); + Assert.False(File.Exists(systemPathDest)); + Assert.True(File.Exists(systemPathSource)); } finally { From d9f34424c750aae3610c3bcb6132ab2f9b803de9 Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Fri, 24 May 2024 13:20:01 +0200 Subject: [PATCH 3/8] Add ResolveLinkTarget --- .../FileSystems/FileSystemEntryRedirect.cs | 5 ++ .../FileSystems/TestPhysicalFileSystem.cs | 6 ++ src/Zio/FileSystems/ComposeFileSystem.cs | 8 ++ src/Zio/FileSystems/FileSystem.cs | 16 +++- src/Zio/FileSystems/MemoryFileSystem.cs | 6 ++ src/Zio/FileSystems/MountFileSystem.cs | 9 ++ src/Zio/FileSystems/PhysicalFileSystem.cs | 33 ++++++++ src/Zio/FileSystems/ZipArchiveFileSystem.cs | 6 ++ src/Zio/IFileSystem.cs | 6 ++ src/Zio/Interop.cs | 83 +++++++++++++++++++ 10 files changed, 177 insertions(+), 1 deletion(-) diff --git a/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs b/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs index 83ebb22..e1237cf 100644 --- a/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs +++ b/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs @@ -125,6 +125,11 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) _fs.CreateSymbolicLink(path, pathToTarget); } + protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + { + return _fs.ResolveLinkTarget(linkPath); + } + protected override IEnumerable EnumeratePathsImpl(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) { return _fs.GetDirectoryEntry(path).EnumerateEntries(searchPattern, searchOption, searchTarget).Select(e => e.Path); diff --git a/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs b/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs index 0560c7f..a14669c 100644 --- a/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs +++ b/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs @@ -442,6 +442,9 @@ public void TestDirectorySymlink() // CreateSymbolicLink fs.CreateSymbolicLink(pathDest, pathSource); + // ResolveSymbolicLink + Assert.Equal(pathSource, fs.ResolveLinkTarget(pathDest)); + // FileExists Assert.True(fs.FileExists(filePathDest)); Assert.Equal(buffer.Length, fs.GetFileLength(filePathDest)); @@ -487,6 +490,9 @@ public void TestFileSymlink() // CreateSymbolicLink fs.CreateSymbolicLink(pathDest, pathSource); + // ResolveSymbolicLink + Assert.Equal(pathSource, fs.ResolveLinkTarget(pathDest)); + // FileExists Assert.True(fs.FileExists(pathDest)); diff --git a/src/Zio/FileSystems/ComposeFileSystem.cs b/src/Zio/FileSystems/ComposeFileSystem.cs index 6d1a2df..546ddb7 100644 --- a/src/Zio/FileSystems/ComposeFileSystem.cs +++ b/src/Zio/FileSystems/ComposeFileSystem.cs @@ -192,6 +192,14 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) FallbackSafe.CreateSymbolicLink(ConvertPathToDelegate(path), ConvertPathToDelegate(pathToTarget)); } + /// + protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + { + var path = FallbackSafe.ResolveLinkTarget(ConvertPathToDelegate(linkPath)); + + return path.HasValue ? ConvertPathFromDelegate(path.Value) : default(UPath?); + } + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/FileSystem.cs b/src/Zio/FileSystems/FileSystem.cs index c6c2821..ce4ea0f 100644 --- a/src/Zio/FileSystems/FileSystem.cs +++ b/src/Zio/FileSystems/FileSystem.cs @@ -444,7 +444,21 @@ public void CreateSymbolicLink(UPath path, UPath pathToTarget) /// The path of the symbolic link to create. /// The path of the target for the symbolic link. protected abstract void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget); - + + + /// + public UPath? ResolveLinkTarget(UPath linkPath) + { + AssertNotDisposed(); + return ResolveLinkTargetImpl(ValidatePath(linkPath)); + } + + /// + /// Resolves the target of a symbolic link. + /// + /// The path of the symbolic link to resolve. + protected abstract UPath? ResolveLinkTargetImpl(UPath linkPath); + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/MemoryFileSystem.cs b/src/Zio/FileSystems/MemoryFileSystem.cs index e2b1fb1..88669c6 100644 --- a/src/Zio/FileSystems/MemoryFileSystem.cs +++ b/src/Zio/FileSystems/MemoryFileSystem.cs @@ -759,6 +759,12 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) throw new NotSupportedException("Symbolic links are not supported by MemoryFileSystem"); } + /// + protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + { + return null; + } + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/MountFileSystem.cs b/src/Zio/FileSystems/MountFileSystem.cs index 6da1b57..f08b7a5 100644 --- a/src/Zio/FileSystems/MountFileSystem.cs +++ b/src/Zio/FileSystems/MountFileSystem.cs @@ -565,6 +565,15 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) } } + /// + protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + { + var mountfs = TryGetMountOrNext(ref linkPath, out var mountPath); + var path = mountfs?.ResolveLinkTarget(linkPath); + + return path != null ? CombinePrefix(mountPath, path.Value) : default(UPath?); + } + /// protected override IEnumerable EnumeratePathsImpl(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) { diff --git a/src/Zio/FileSystems/PhysicalFileSystem.cs b/src/Zio/FileSystems/PhysicalFileSystem.cs index be12a75..0b9ec43 100644 --- a/src/Zio/FileSystems/PhysicalFileSystem.cs +++ b/src/Zio/FileSystems/PhysicalFileSystem.cs @@ -515,6 +515,39 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) #endif } + /// + protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + { + if (IsWithinSpecialDirectory(linkPath)) + { + throw new UnauthorizedAccessException($"The access to `{linkPath}` is denied"); + } + + var systemPath = ConvertPathToInternal(linkPath); + bool isDirectory; + + if (File.Exists(systemPath)) + { + isDirectory = false; + } + else if (Directory.Exists(systemPath)) + { + isDirectory = true; + } + else + { + return null; + } + +#if NET7_0_OR_GREATER + var systemResult = isDirectory ? Directory.ResolveLinkTarget(systemPath, true)?.FullName : File.ResolveLinkTarget(systemPath, true)?.FullName; +#else + var systemResult = IsOnWindows ? Interop.Windows.GetFinalPathName(systemPath) : Interop.Unix.readlink(systemPath); +#endif + + return systemResult != null ? ConvertPathFromInternal(systemResult) : default(UPath?); + } + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/ZipArchiveFileSystem.cs b/src/Zio/FileSystems/ZipArchiveFileSystem.cs index 196f442..575c7ad 100644 --- a/src/Zio/FileSystems/ZipArchiveFileSystem.cs +++ b/src/Zio/FileSystems/ZipArchiveFileSystem.cs @@ -823,6 +823,12 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) throw new NotSupportedException("Symbolic links are not supported by ZipArchiveFileSystem"); } + /// + protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + { + return null; + } + /// protected override IFileSystemWatcher WatchImpl(UPath path) { diff --git a/src/Zio/IFileSystem.cs b/src/Zio/IFileSystem.cs index 6588269..7d665a3 100644 --- a/src/Zio/IFileSystem.cs +++ b/src/Zio/IFileSystem.cs @@ -170,6 +170,12 @@ public interface IFileSystem : IDisposable /// The path of the target for the symbolic link. void CreateSymbolicLink(UPath path, UPath pathToTarget); + /// + /// Resolves the target of a symbolic link. + /// + /// The path of the symbolic link to resolve. + UPath? ResolveLinkTarget(UPath linkPath); + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/Interop.cs b/src/Zio/Interop.cs index 8d4a4d9..8325462 100644 --- a/src/Zio/Interop.cs +++ b/src/Zio/Interop.cs @@ -3,7 +3,10 @@ // See the license.txt file in the project root for more information. #if !NET7_0_OR_GREATER +using System.ComponentModel; +using System.IO; using System.Runtime.InteropServices; +using System.Text; namespace Zio; @@ -11,9 +14,78 @@ internal static class Interop { public static class Windows { + private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); + + private const uint FILE_READ_EA = 0x0008; + private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x2000000; + [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymbolicLink dwFlags); + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern uint GetFinalPathNameByHandle(IntPtr hFile, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr CreateFile( + [MarshalAs(UnmanagedType.LPTStr)] string filename, + [MarshalAs(UnmanagedType.U4)] uint access, + [MarshalAs(UnmanagedType.U4)] FileShare share, + IntPtr securityAttributes, + [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, + [MarshalAs(UnmanagedType.U4)] uint flagsAndAttributes, + IntPtr templateFile); + + public static string GetFinalPathName(string path) + { + var h = CreateFile(path, + FILE_READ_EA, + FileShare.ReadWrite | FileShare.Delete, + IntPtr.Zero, + FileMode.Open, + FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero); + + if (h == INVALID_HANDLE_VALUE) + { + throw new Win32Exception(); + } + + try + { + var sb = new StringBuilder(1024); + var res = GetFinalPathNameByHandle(h, sb, 1024, 0); + if (res == 0) + { + throw new Win32Exception(); + } + + // Trim '\\?\' + if (sb.Length >= 4 && sb[0] == '\\' && sb[1] == '\\' && sb[2] == '?' && sb[3] == '\\') + { + sb.Remove(0, 4); + + // Trim 'UNC\' + if (sb.Length >= 4 && sb[0] == 'U' && sb[1] == 'N' && sb[2] == 'C' && sb[3] == '\\') + { + sb.Remove(0, 4); + + // Add the default UNC prefix + sb.Insert(0, @"\\"); + } + } + + return sb.ToString(); + } + finally + { + CloseHandle(h); + } + } + public enum SymbolicLink { File = 0, @@ -25,6 +97,17 @@ public static class Unix { [DllImport("libc", SetLastError = true)] public static extern int symlink(string target, string linkpath); + + [DllImport ("libc")] + private static extern int readlink (string path, byte[] buffer, int buflen); + + public static string? readlink(string path) + { + var buf = new byte[1024]; + var ret = readlink(path, buf, buf.Length); + + return ret == -1 ? null : Encoding.Default.GetString(buf, 0, ret); + } } } #endif \ No newline at end of file From 186c4e9f3e37d09b1cf798ae4d145f3141efe507 Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Fri, 24 May 2024 14:50:51 +0200 Subject: [PATCH 4/8] Refactor ResolveLinkTarget to TryResolveLinkTarget --- .../FileSystems/FileSystemEntryRedirect.cs | 4 ++-- .../FileSystems/TestPhysicalFileSystem.cs | 6 ++++-- src/Zio/FileSystems/ComposeFileSystem.cs | 11 ++++++++--- src/Zio/FileSystems/FileSystem.cs | 7 ++++--- src/Zio/FileSystems/MemoryFileSystem.cs | 5 +++-- src/Zio/FileSystems/MountFileSystem.cs | 18 +++++++++++++++--- src/Zio/FileSystems/PhysicalFileSystem.cs | 14 +++++++++++--- src/Zio/FileSystems/ZipArchiveFileSystem.cs | 5 +++-- src/Zio/IFileSystem.cs | 3 ++- 9 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs b/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs index e1237cf..9f4f4e5 100644 --- a/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs +++ b/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs @@ -125,9 +125,9 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) _fs.CreateSymbolicLink(path, pathToTarget); } - protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) { - return _fs.ResolveLinkTarget(linkPath); + return _fs.TryResolveLinkTarget(linkPath, out resolvedPath); } protected override IEnumerable EnumeratePathsImpl(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) diff --git a/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs b/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs index a14669c..e09a873 100644 --- a/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs +++ b/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs @@ -443,7 +443,8 @@ public void TestDirectorySymlink() fs.CreateSymbolicLink(pathDest, pathSource); // ResolveSymbolicLink - Assert.Equal(pathSource, fs.ResolveLinkTarget(pathDest)); + Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath)); + Assert.Equal(pathSource, resolvedPath); // FileExists Assert.True(fs.FileExists(filePathDest)); @@ -491,7 +492,8 @@ public void TestFileSymlink() fs.CreateSymbolicLink(pathDest, pathSource); // ResolveSymbolicLink - Assert.Equal(pathSource, fs.ResolveLinkTarget(pathDest)); + Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath)); + Assert.Equal(pathSource, resolvedPath); // FileExists Assert.True(fs.FileExists(pathDest)); diff --git a/src/Zio/FileSystems/ComposeFileSystem.cs b/src/Zio/FileSystems/ComposeFileSystem.cs index 546ddb7..f201821 100644 --- a/src/Zio/FileSystems/ComposeFileSystem.cs +++ b/src/Zio/FileSystems/ComposeFileSystem.cs @@ -193,11 +193,16 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) } /// - protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) { - var path = FallbackSafe.ResolveLinkTarget(ConvertPathToDelegate(linkPath)); + if (!FallbackSafe.TryResolveLinkTarget(ConvertPathToDelegate(linkPath), out var resolvedPathDelegate)) + { + resolvedPath = default; + return false; + } - return path.HasValue ? ConvertPathFromDelegate(path.Value) : default(UPath?); + resolvedPath = ConvertPathFromDelegate(resolvedPathDelegate); + return true; } // ---------------------------------------------- diff --git a/src/Zio/FileSystems/FileSystem.cs b/src/Zio/FileSystems/FileSystem.cs index ce4ea0f..da7f714 100644 --- a/src/Zio/FileSystems/FileSystem.cs +++ b/src/Zio/FileSystems/FileSystem.cs @@ -447,17 +447,18 @@ public void CreateSymbolicLink(UPath path, UPath pathToTarget) /// - public UPath? ResolveLinkTarget(UPath linkPath) + public bool TryResolveLinkTarget(UPath linkPath, out UPath resolvedPath) { AssertNotDisposed(); - return ResolveLinkTargetImpl(ValidatePath(linkPath)); + return TryResolveLinkTargetImpl(ValidatePath(linkPath), out resolvedPath); } /// /// Resolves the target of a symbolic link. /// /// The path of the symbolic link to resolve. - protected abstract UPath? ResolveLinkTargetImpl(UPath linkPath); + /// + protected abstract bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath); // ---------------------------------------------- // Search API diff --git a/src/Zio/FileSystems/MemoryFileSystem.cs b/src/Zio/FileSystems/MemoryFileSystem.cs index 88669c6..3a14757 100644 --- a/src/Zio/FileSystems/MemoryFileSystem.cs +++ b/src/Zio/FileSystems/MemoryFileSystem.cs @@ -760,9 +760,10 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) } /// - protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) { - return null; + resolvedPath = default; + return false; } // ---------------------------------------------- diff --git a/src/Zio/FileSystems/MountFileSystem.cs b/src/Zio/FileSystems/MountFileSystem.cs index f08b7a5..2539e38 100644 --- a/src/Zio/FileSystems/MountFileSystem.cs +++ b/src/Zio/FileSystems/MountFileSystem.cs @@ -566,12 +566,24 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) } /// - protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) { var mountfs = TryGetMountOrNext(ref linkPath, out var mountPath); - var path = mountfs?.ResolveLinkTarget(linkPath); - return path != null ? CombinePrefix(mountPath, path.Value) : default(UPath?); + if (mountfs is null) + { + resolvedPath = default; + return false; + } + + if (!mountfs.TryResolveLinkTarget(linkPath, out var resolved)) + { + resolvedPath = default; + return false; + } + + resolvedPath = CombinePrefix(mountPath, resolved); + return true; } /// diff --git a/src/Zio/FileSystems/PhysicalFileSystem.cs b/src/Zio/FileSystems/PhysicalFileSystem.cs index 0b9ec43..67a7657 100644 --- a/src/Zio/FileSystems/PhysicalFileSystem.cs +++ b/src/Zio/FileSystems/PhysicalFileSystem.cs @@ -516,7 +516,7 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) } /// - protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) { if (IsWithinSpecialDirectory(linkPath)) { @@ -536,7 +536,8 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) } else { - return null; + resolvedPath = default; + return false; } #if NET7_0_OR_GREATER @@ -545,7 +546,14 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) var systemResult = IsOnWindows ? Interop.Windows.GetFinalPathName(systemPath) : Interop.Unix.readlink(systemPath); #endif - return systemResult != null ? ConvertPathFromInternal(systemResult) : default(UPath?); + if (systemResult == null) + { + resolvedPath = default; + return false; + } + + resolvedPath = ConvertPathFromInternal(systemResult); + return true; } // ---------------------------------------------- diff --git a/src/Zio/FileSystems/ZipArchiveFileSystem.cs b/src/Zio/FileSystems/ZipArchiveFileSystem.cs index 575c7ad..fdc81de 100644 --- a/src/Zio/FileSystems/ZipArchiveFileSystem.cs +++ b/src/Zio/FileSystems/ZipArchiveFileSystem.cs @@ -824,9 +824,10 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) } /// - protected override UPath? ResolveLinkTargetImpl(UPath linkPath) + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) { - return null; + resolvedPath = UPath.Empty; + return false; } /// diff --git a/src/Zio/IFileSystem.cs b/src/Zio/IFileSystem.cs index 7d665a3..7210000 100644 --- a/src/Zio/IFileSystem.cs +++ b/src/Zio/IFileSystem.cs @@ -174,7 +174,8 @@ public interface IFileSystem : IDisposable /// Resolves the target of a symbolic link. /// /// The path of the symbolic link to resolve. - UPath? ResolveLinkTarget(UPath linkPath); + /// The path of the symbolic link resolved if true is returned. + bool TryResolveLinkTarget(UPath linkPath, out UPath resolvedPath); // ---------------------------------------------- // Search API From 880b7c166c1d8dfdcfaffdf1daf4226cafcfd57f Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Fri, 24 May 2024 16:26:18 +0200 Subject: [PATCH 5/8] Fix XMLDoc --- src/Zio/FileEntry.cs | 4 ++-- src/Zio/FileSystemExtensions.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Zio/FileEntry.cs b/src/Zio/FileEntry.cs index c49c561..58eba90 100644 --- a/src/Zio/FileEntry.cs +++ b/src/Zio/FileEntry.cs @@ -284,7 +284,7 @@ public void WriteAllText(string content, Encoding encoding) /// /// Given a string and a file path, this method opens the specified file, appends the string to the end of the file, /// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised. - /// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the + /// The method creates the file if it doesn't exist, but it doesn't create new directories. Therefore, the value of the /// path parameter must contain existing directories. /// public void AppendAllText(string content) @@ -301,7 +301,7 @@ public void AppendAllText(string content) /// /// Given a string and a file path, this method opens the specified file, appends the string to the end of the file, /// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised. - /// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the + /// The method creates the file if it doesn't exist, but it doesn't create new directories. Therefore, the value of the /// path parameter must contain existing directories. /// public void AppendAllText(string content, Encoding encoding) diff --git a/src/Zio/FileSystemExtensions.cs b/src/Zio/FileSystemExtensions.cs index c7882ca..3113f48 100644 --- a/src/Zio/FileSystemExtensions.cs +++ b/src/Zio/FileSystemExtensions.cs @@ -504,7 +504,7 @@ public static void WriteAllText(this IFileSystem fs, UPath path, string content, /// /// Given a string and a file path, this method opens the specified file, appends the string to the end of the file, /// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised. - /// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the + /// The method creates the file if it doesn't exist, but it doesn't create new directories. Therefore, the value of the /// path parameter must contain existing directories. /// public static void AppendAllText(this IFileSystem fs, UPath path, string content) @@ -531,7 +531,7 @@ public static void AppendAllText(this IFileSystem fs, UPath path, string content /// /// Given a string and a file path, this method opens the specified file, appends the string to the end of the file, /// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised. - /// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the + /// The method creates the file if it doesn't exist, but it doesn't create new directories. Therefore, the value of the /// path parameter must contain existing directories. /// public static void AppendAllText(this IFileSystem fs, UPath path, string content, Encoding encoding) From 10a829d5f20241660796daeb065c719197dcba25 Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Fri, 24 May 2024 16:44:45 +0200 Subject: [PATCH 6/8] Fix MountFileSystem implementation --- .../FileSystems/TestSubFileSystem.cs | 71 ++++++++++++++++++- src/Zio/FileSystems/MountFileSystem.cs | 20 ++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/Zio.Tests/FileSystems/TestSubFileSystem.cs b/src/Zio.Tests/FileSystems/TestSubFileSystem.cs index 48d4353..a7faa13 100644 --- a/src/Zio.Tests/FileSystems/TestSubFileSystem.cs +++ b/src/Zio.Tests/FileSystems/TestSubFileSystem.cs @@ -3,7 +3,8 @@ // See the license.txt file in the project root for more information. using System.IO; - +using System.Security.Principal; +using System.Text; using Zio.FileSystems; namespace Zio.Tests.FileSystems; @@ -68,4 +69,72 @@ public void TestWatcher() System.Threading.Thread.Sleep(100); Assert.True(gotChange); } + + + [SkippableFact] + public void TestDirectorySymlink() + { +#if NETCOREAPP + if (OperatingSystem.IsWindows()) +#else + if (IsWindows) +#endif + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + + Skip.IfNot(principal.IsInRole(WindowsBuiltInRole.Administrator), "This test requires to be run as an administrator on Windows"); + } + + var physicalFs = new PhysicalFileSystem(); + var memoryFs = new MemoryFileSystem(); + var fs = new MountFileSystem(); + fs.Mount("/physical", physicalFs); + fs.Mount("/memory", memoryFs); + + var pathInfo = physicalFs.ConvertPathFromInternal(SystemPath).ToRelative(); + var pathSource = "/physical" / pathInfo / "Source"; + var filePathSource = pathSource / "test.txt"; + var systemPathSource = fs.ConvertPathToInternal(pathSource); + var pathDest = "/physical" / pathInfo / "Dest"; + var filePathDest = pathDest / "test.txt"; + var systemPathDest = fs.ConvertPathToInternal(pathDest); + + try + { + // CreateDirectory + Assert.False(Directory.Exists(systemPathSource)); + fs.CreateDirectory(pathSource); + Assert.True(Directory.Exists(systemPathSource)); + + // CreateFile / OpenFile + var fileStream = fs.CreateFile(filePathSource); + var buffer = Encoding.UTF8.GetBytes("This is a test"); + fileStream.Write(buffer, 0, buffer.Length); + fileStream.Dispose(); + Assert.Equal(buffer.Length, fs.GetFileLength(filePathSource)); + + // CreateSymbolicLink + fs.CreateSymbolicLink(pathDest, pathSource); + Assert.Throws(() => fs.CreateSymbolicLink("/memory/invalid", pathSource)); + + // ResolveSymbolicLink + Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath)); + Assert.Equal(pathSource, resolvedPath); + + // FileExists + Assert.True(fs.FileExists(filePathDest)); + Assert.Equal(buffer.Length, fs.GetFileLength(filePathDest)); + + // RemoveDirectory + fs.DeleteDirectory(pathDest, false); + Assert.False(Directory.Exists(systemPathDest)); + Assert.True(Directory.Exists(systemPathSource)); + } + finally + { + SafeDeleteDirectory(systemPathSource); + SafeDeleteDirectory(systemPathDest); + } + } } \ No newline at end of file diff --git a/src/Zio/FileSystems/MountFileSystem.cs b/src/Zio/FileSystems/MountFileSystem.cs index 2539e38..72c2a64 100644 --- a/src/Zio/FileSystems/MountFileSystem.cs +++ b/src/Zio/FileSystems/MountFileSystem.cs @@ -555,6 +555,13 @@ protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget) { var originalSrcPath = path; var mountfs = TryGetMountOrNext(ref path); + var mountTargetfs = TryGetMountOrNext(ref pathToTarget); + + if (mountfs != mountTargetfs) + { + throw new InvalidOperationException("Cannot create a symbolic link between two different filesystems"); + } + if (mountfs != null) { mountfs.CreateSymbolicLink(path, pathToTarget); @@ -969,6 +976,19 @@ protected override UPath ConvertPathFromDelegate(UPath path) return path; } + protected override string ConvertPathToInternalImpl(UPath path) + { + var mountPath = path; + var mountfs = TryGetMountOrNext(ref mountPath); + + if (mountfs != null) + { + return mountfs.ConvertPathToInternal(mountPath); + } + + return base.ConvertPathToInternalImpl(path); + } + private IFileSystem? TryGetMountOrNext(ref UPath path) { return TryGetMountOrNext(ref path, out var _); From fd0e5de6fb36b66d8b5767e13888faf468cb947c Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Fri, 24 May 2024 16:47:22 +0200 Subject: [PATCH 7/8] Add SubFileSystem test --- .../FileSystems/TestMountFileSystem.cs | 70 ++++++++++++++++++- .../FileSystems/TestSubFileSystem.cs | 12 +--- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/Zio.Tests/FileSystems/TestMountFileSystem.cs b/src/Zio.Tests/FileSystems/TestMountFileSystem.cs index 5d22c16..d702a0e 100644 --- a/src/Zio.Tests/FileSystems/TestMountFileSystem.cs +++ b/src/Zio.Tests/FileSystems/TestMountFileSystem.cs @@ -5,7 +5,8 @@ using System.Collections; using System.IO; using System.Reflection; - +using System.Security.Principal; +using System.Text; using Zio.FileSystems; namespace Zio.Tests.FileSystems; @@ -427,4 +428,71 @@ public void CopyAndMoveFileCross() Assert.True(memfs2.FileExists("/file1.txt")); Assert.Equal("content1", memfs2.ReadAllText("/file1.txt")); } + + [SkippableFact] + public void TestDirectorySymlink() + { +#if NETCOREAPP + if (OperatingSystem.IsWindows()) +#else + if (IsWindows) +#endif + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + + Skip.IfNot(principal.IsInRole(WindowsBuiltInRole.Administrator), "This test requires to be run as an administrator on Windows"); + } + + var physicalFs = new PhysicalFileSystem(); + var memoryFs = new MemoryFileSystem(); + var fs = new MountFileSystem(); + fs.Mount("/physical", physicalFs); + fs.Mount("/memory", memoryFs); + + var pathInfo = physicalFs.ConvertPathFromInternal(SystemPath).ToRelative(); + var pathSource = "/physical" / pathInfo / "Source"; + var filePathSource = pathSource / "test.txt"; + var systemPathSource = fs.ConvertPathToInternal(pathSource); + var pathDest = "/physical" / pathInfo / "Dest"; + var filePathDest = pathDest / "test.txt"; + var systemPathDest = fs.ConvertPathToInternal(pathDest); + + try + { + // CreateDirectory + Assert.False(Directory.Exists(systemPathSource)); + fs.CreateDirectory(pathSource); + Assert.True(Directory.Exists(systemPathSource)); + + // CreateFile / OpenFile + var fileStream = fs.CreateFile(filePathSource); + var buffer = Encoding.UTF8.GetBytes("This is a test"); + fileStream.Write(buffer, 0, buffer.Length); + fileStream.Dispose(); + Assert.Equal(buffer.Length, fs.GetFileLength(filePathSource)); + + // CreateSymbolicLink + fs.CreateSymbolicLink(pathDest, pathSource); + Assert.Throws(() => fs.CreateSymbolicLink("/memory/invalid", pathSource)); + + // ResolveSymbolicLink + Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath)); + Assert.Equal(pathSource, resolvedPath); + + // FileExists + Assert.True(fs.FileExists(filePathDest)); + Assert.Equal(buffer.Length, fs.GetFileLength(filePathDest)); + + // RemoveDirectory + fs.DeleteDirectory(pathDest, false); + Assert.False(Directory.Exists(systemPathDest)); + Assert.True(Directory.Exists(systemPathSource)); + } + finally + { + SafeDeleteDirectory(systemPathSource); + SafeDeleteDirectory(systemPathDest); + } + } } \ No newline at end of file diff --git a/src/Zio.Tests/FileSystems/TestSubFileSystem.cs b/src/Zio.Tests/FileSystems/TestSubFileSystem.cs index a7faa13..b83f11c 100644 --- a/src/Zio.Tests/FileSystems/TestSubFileSystem.cs +++ b/src/Zio.Tests/FileSystems/TestSubFileSystem.cs @@ -70,7 +70,6 @@ public void TestWatcher() Assert.True(gotChange); } - [SkippableFact] public void TestDirectorySymlink() { @@ -87,16 +86,12 @@ public void TestDirectorySymlink() } var physicalFs = new PhysicalFileSystem(); - var memoryFs = new MemoryFileSystem(); - var fs = new MountFileSystem(); - fs.Mount("/physical", physicalFs); - fs.Mount("/memory", memoryFs); + var fs = new SubFileSystem(physicalFs, physicalFs.ConvertPathFromInternal(SystemPath)); - var pathInfo = physicalFs.ConvertPathFromInternal(SystemPath).ToRelative(); - var pathSource = "/physical" / pathInfo / "Source"; + UPath pathSource = "/Source"; var filePathSource = pathSource / "test.txt"; var systemPathSource = fs.ConvertPathToInternal(pathSource); - var pathDest = "/physical" / pathInfo / "Dest"; + UPath pathDest = "/Dest"; var filePathDest = pathDest / "test.txt"; var systemPathDest = fs.ConvertPathToInternal(pathDest); @@ -116,7 +111,6 @@ public void TestDirectorySymlink() // CreateSymbolicLink fs.CreateSymbolicLink(pathDest, pathSource); - Assert.Throws(() => fs.CreateSymbolicLink("/memory/invalid", pathSource)); // ResolveSymbolicLink Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath)); From 52fadd524c1850726b077e8883e65beb19f97d26 Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Fri, 24 May 2024 17:49:22 +0200 Subject: [PATCH 8/8] Fix tests --- src/Zio.Tests/FileSystems/TestMountFileSystem.cs | 4 ++-- src/Zio/FileSystems/FileSystem.cs | 1 + src/Zio/FileSystems/MountFileSystem.cs | 13 ------------- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/Zio.Tests/FileSystems/TestMountFileSystem.cs b/src/Zio.Tests/FileSystems/TestMountFileSystem.cs index d702a0e..78676f0 100644 --- a/src/Zio.Tests/FileSystems/TestMountFileSystem.cs +++ b/src/Zio.Tests/FileSystems/TestMountFileSystem.cs @@ -453,10 +453,10 @@ public void TestDirectorySymlink() var pathInfo = physicalFs.ConvertPathFromInternal(SystemPath).ToRelative(); var pathSource = "/physical" / pathInfo / "Source"; var filePathSource = pathSource / "test.txt"; - var systemPathSource = fs.ConvertPathToInternal(pathSource); + var systemPathSource = Path.Combine(SystemPath, "Source"); var pathDest = "/physical" / pathInfo / "Dest"; var filePathDest = pathDest / "test.txt"; - var systemPathDest = fs.ConvertPathToInternal(pathDest); + var systemPathDest = Path.Combine(SystemPath, "Dest"); try { diff --git a/src/Zio/FileSystems/FileSystem.cs b/src/Zio/FileSystems/FileSystem.cs index da7f714..146124b 100644 --- a/src/Zio/FileSystems/FileSystem.cs +++ b/src/Zio/FileSystems/FileSystem.cs @@ -575,6 +575,7 @@ public UPath ConvertPathFromInternal(string systemPath) if (systemPath is null) throw new ArgumentNullException(nameof(systemPath)); return ValidatePath(ConvertPathFromInternalImpl(systemPath)); } + /// /// Implementation for , is guaranteed to be not null and return path to be validated through . /// Converts the specified system path to a path. diff --git a/src/Zio/FileSystems/MountFileSystem.cs b/src/Zio/FileSystems/MountFileSystem.cs index 72c2a64..4b486af 100644 --- a/src/Zio/FileSystems/MountFileSystem.cs +++ b/src/Zio/FileSystems/MountFileSystem.cs @@ -976,19 +976,6 @@ protected override UPath ConvertPathFromDelegate(UPath path) return path; } - protected override string ConvertPathToInternalImpl(UPath path) - { - var mountPath = path; - var mountfs = TryGetMountOrNext(ref mountPath); - - if (mountfs != null) - { - return mountfs.ConvertPathToInternal(mountPath); - } - - return base.ConvertPathToInternalImpl(path); - } - private IFileSystem? TryGetMountOrNext(ref UPath path) { return TryGetMountOrNext(ref path, out var _);