diff --git a/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs b/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs index 0b9b075..9f4f4e5 100644 --- a/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs +++ b/src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs @@ -120,6 +120,16 @@ 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 bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) + { + return _fs.TryResolveLinkTarget(linkPath, out resolvedPath); + } + 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/TestMountFileSystem.cs b/src/Zio.Tests/FileSystems/TestMountFileSystem.cs index 5d22c16..78676f0 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 = Path.Combine(SystemPath, "Source"); + var pathDest = "/physical" / pathInfo / "Dest"; + var filePathDest = pathDest / "test.txt"; + var systemPathDest = Path.Combine(SystemPath, "Dest"); + + 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/TestPhysicalFileSystem.cs b/src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs index dba95bb..5f12f9d 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; @@ -427,4 +428,128 @@ public void TestFileWindowsExceptions() 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); + + // 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); + } + } + + [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); + + // ResolveSymbolicLink + Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath)); + Assert.Equal(pathSource, resolvedPath); + + // 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)); + + // DeleteFile + fs.DeleteFile(pathDest); + Assert.False(File.Exists(systemPathDest)); + Assert.True(File.Exists(systemPathSource)); + } + finally + { + SafeDeleteFile(systemPathSource); + SafeDeleteFile(systemPathDest); + } + } } \ No newline at end of file diff --git a/src/Zio.Tests/FileSystems/TestSubFileSystem.cs b/src/Zio.Tests/FileSystems/TestSubFileSystem.cs index ed79648..620789f 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; @@ -72,4 +73,66 @@ 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 fs = new SubFileSystem(physicalFs, physicalFs.ConvertPathFromInternal(SystemPath)); + + UPath pathSource = "/Source"; + var filePathSource = pathSource / "test.txt"; + var systemPathSource = fs.ConvertPathToInternal(pathSource); + UPath pathDest = "/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); + + // 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/FileEntry.cs b/src/Zio/FileEntry.cs index 1833930..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) diff --git a/src/Zio/FileSystems/ComposeFileSystem.cs b/src/Zio/FileSystems/ComposeFileSystem.cs index 5eb84b2..f201821 100644 --- a/src/Zio/FileSystems/ComposeFileSystem.cs +++ b/src/Zio/FileSystems/ComposeFileSystem.cs @@ -186,6 +186,25 @@ 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)); + } + + /// + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) + { + if (!FallbackSafe.TryResolveLinkTarget(ConvertPathToDelegate(linkPath), out var resolvedPathDelegate)) + { + resolvedPath = default; + return false; + } + + resolvedPath = ConvertPathFromDelegate(resolvedPathDelegate); + return true; + } + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/FileSystem.cs b/src/Zio/FileSystems/FileSystem.cs index f85dea8..f0de432 100644 --- a/src/Zio/FileSystems/FileSystem.cs +++ b/src/Zio/FileSystems/FileSystem.cs @@ -431,6 +431,35 @@ 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); + + + /// + public bool TryResolveLinkTarget(UPath linkPath, out UPath resolvedPath) + { + AssertNotDisposed(); + return TryResolveLinkTargetImpl(ValidatePath(linkPath), out resolvedPath); + } + + /// + /// Resolves the target of a symbolic link. + /// + /// The path of the symbolic link to resolve. + /// + protected abstract bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath); + // ---------------------------------------------- // Search API // ---------------------------------------------- @@ -546,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/MemoryFileSystem.cs b/src/Zio/FileSystems/MemoryFileSystem.cs index 362c258..8180c6c 100644 --- a/src/Zio/FileSystems/MemoryFileSystem.cs +++ b/src/Zio/FileSystems/MemoryFileSystem.cs @@ -753,6 +753,19 @@ 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"); + } + + /// + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) + { + resolvedPath = default; + return false; + } + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/MountFileSystem.cs b/src/Zio/FileSystems/MountFileSystem.cs index 2a666aa..4b486af 100644 --- a/src/Zio/FileSystems/MountFileSystem.cs +++ b/src/Zio/FileSystems/MountFileSystem.cs @@ -550,6 +550,49 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time) } } + /// + 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); + } + else + { + throw NewFileNotFoundException(originalSrcPath); + } + } + + /// + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) + { + var mountfs = TryGetMountOrNext(ref linkPath, out var mountPath); + + 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; + } + /// 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 6d4ffd7..1b1164b 100644 --- a/src/Zio/FileSystems/PhysicalFileSystem.cs +++ b/src/Zio/FileSystems/PhysicalFileSystem.cs @@ -439,6 +439,123 @@ 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 + } + + /// + protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) + { + 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 + { + resolvedPath = default; + return false; + } + +#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 + + if (systemResult == null) + { + resolvedPath = default; + return false; + } + + resolvedPath = ConvertPathFromInternal(systemResult); + return true; + } + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/FileSystems/ReadOnlyFileSystem.cs b/src/Zio/FileSystems/ReadOnlyFileSystem.cs index 50432d0..deb71e0 100644 --- a/src/Zio/FileSystems/ReadOnlyFileSystem.cs +++ b/src/Zio/FileSystems/ReadOnlyFileSystem.cs @@ -133,6 +133,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 c178888..356ceaf 100644 --- a/src/Zio/FileSystems/ZipArchiveFileSystem.cs +++ b/src/Zio/FileSystems/ZipArchiveFileSystem.cs @@ -820,6 +820,19 @@ 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 bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath) + { + resolvedPath = UPath.Empty; + return false; + } + /// protected override IFileSystemWatcher WatchImpl(UPath path) { diff --git a/src/Zio/IFileSystem.cs b/src/Zio/IFileSystem.cs index 7de7037..7210000 100644 --- a/src/Zio/IFileSystem.cs +++ b/src/Zio/IFileSystem.cs @@ -163,6 +163,20 @@ 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); + + /// + /// Resolves the target of a symbolic link. + /// + /// The path of the symbolic link to resolve. + /// The path of the symbolic link resolved if true is returned. + bool TryResolveLinkTarget(UPath linkPath, out UPath resolvedPath); + // ---------------------------------------------- // Search API // ---------------------------------------------- diff --git a/src/Zio/Interop.cs b/src/Zio/Interop.cs new file mode 100644 index 0000000..8325462 --- /dev/null +++ b/src/Zio/Interop.cs @@ -0,0 +1,113 @@ +// 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.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Zio; + +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, + Directory = 1 + } + } + + 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