From 47ad31a761115e5aee7b3e5263f57f0f5150783b Mon Sep 17 00:00:00 2001 From: William Baker Date: Thu, 27 Sep 2018 15:18:37 -0700 Subject: [PATCH] Mac: Update MirrorProvider to support symbolic links --- .../MacFileSystemVirtualizer.cs | 63 +++++++++- .../MirrorProvider/FileSystemVirtualizer.cs | 117 ++++++++++++------ .../MirrorProvider/ProjectedFileInfo.cs | 17 ++- 3 files changed, 157 insertions(+), 40 deletions(-) diff --git a/MirrorProvider/MirrorProvider.Mac/MacFileSystemVirtualizer.cs b/MirrorProvider/MirrorProvider.Mac/MacFileSystemVirtualizer.cs index 3d9396c82e..b8df167896 100644 --- a/MirrorProvider/MirrorProvider.Mac/MacFileSystemVirtualizer.cs +++ b/MirrorProvider/MirrorProvider.Mac/MacFileSystemVirtualizer.cs @@ -1,7 +1,9 @@ using PrjFSLib.Mac; using System; using System.IO; - +using System.Runtime.InteropServices; +using System.Text; + namespace MirrorProvider.Mac { public class MacFileSystemVirtualizer : FileSystemVirtualizer @@ -58,7 +60,7 @@ private Result OnEnumerateDirectory( foreach (ProjectedFileInfo child in this.GetChildItems(relativePath)) { - if (child.IsDirectory) + if (child.Type == ProjectedFileInfo.FileType.Directory) { Result result = this.virtualizationInstance.WritePlaceholderDirectory( Path.Combine(relativePath, child.Name)); @@ -69,6 +71,28 @@ private Result OnEnumerateDirectory( return result; } } + else if (child.Type == ProjectedFileInfo.FileType.SymLink) + { + string childRelativePath = Path.Combine(relativePath, child.Name); + + string symLinkTarget; + if (this.TryGetSymLinkTarget(childRelativePath, out symLinkTarget)) + { + Result result = this.virtualizationInstance.WriteSymLink( + childRelativePath, + symLinkTarget); + + if (result != Result.Success) + { + Console.WriteLine($"WriteSymLink failed: {result}"); + return result; + } + } + else + { + return Result.EIOError; + } + } else { // The MirrorProvider marks every file as executable (mode 755), but this is just a shortcut to avoid the pain of @@ -176,6 +200,35 @@ private void OnHardLinkCreated(string relativeNewLinkPath) { Console.WriteLine($"OnHardLinkCreated: {relativeNewLinkPath}"); } + + private bool TryGetSymLinkTarget(string relativePath, out string symLinkTarget) + { + symLinkTarget = null; + string fullPathInMirror = this.GetFullPathInMirror(relativePath); + + const ulong BufSize = 4096; + byte[] targetBuffer = new byte[BufSize]; + long bytesRead = ReadLink(fullPathInMirror, targetBuffer, BufSize); + if (bytesRead < 0) + { + Console.WriteLine($"GetSymLinkTarget failed: {Marshal.GetLastWin32Error()}"); + return false; + } + + targetBuffer[bytesRead] = 0; + symLinkTarget = Encoding.UTF8.GetString(targetBuffer); + + if (symLinkTarget.StartsWith(this.Enlistment.MirrorRoot, StringComparison.OrdinalIgnoreCase)) + { + // Link target is an absolute path inside the MirrorRoot. + // The target needs to be adjusted to point inside the src root + symLinkTarget = Path.Combine( + this.Enlistment.SrcRoot.TrimEnd(Path.DirectorySeparatorChar), + symLinkTarget.Substring(this.Enlistment.MirrorRoot.Length).TrimStart(Path.DirectorySeparatorChar)); + } + + return true; + } private static byte[] ToVersionIdByteArray(byte version) { @@ -184,5 +237,11 @@ private static byte[] ToVersionIdByteArray(byte version) return bytes; } + + [DllImport("libc", EntryPoint = "readlink", SetLastError = true)] + private static extern long ReadLink( + string path, + byte[] buf, + ulong bufsize); } } diff --git a/MirrorProvider/MirrorProvider/FileSystemVirtualizer.cs b/MirrorProvider/MirrorProvider/FileSystemVirtualizer.cs index 40efddbe9e..cb73aa0b01 100644 --- a/MirrorProvider/MirrorProvider/FileSystemVirtualizer.cs +++ b/MirrorProvider/MirrorProvider/FileSystemVirtualizer.cs @@ -1,25 +1,30 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; - +using System.Linq; + namespace MirrorProvider { public abstract class FileSystemVirtualizer { - private Enlistment enlistment; + protected Enlistment Enlistment { get; private set; } public abstract bool TryConvertVirtualizationRoot(string directory, out string error); public virtual bool TryStartVirtualizationInstance(Enlistment enlistment, out string error) { - this.enlistment = enlistment; + this.Enlistment = enlistment; error = null; return true; } + protected string GetFullPathInMirror(string relativePath) + { + return Path.Combine(this.Enlistment.MirrorRoot, relativePath); + } + protected bool DirectoryExists(string relativePath) { - string fullPathInMirror = Path.Combine(this.enlistment.MirrorRoot, relativePath); + string fullPathInMirror = this.GetFullPathInMirror(relativePath); DirectoryInfo dirInfo = new DirectoryInfo(fullPathInMirror); return dirInfo.Exists; @@ -27,7 +32,7 @@ protected bool DirectoryExists(string relativePath) protected bool FileExists(string relativePath) { - string fullPathInMirror = Path.Combine(this.enlistment.MirrorRoot, relativePath); + string fullPathInMirror = this.GetFullPathInMirror(relativePath); FileInfo fileInfo = new FileInfo(fullPathInMirror); return fileInfo.Exists; @@ -35,18 +40,18 @@ protected bool FileExists(string relativePath) protected ProjectedFileInfo GetFileInfo(string relativePath) { - string fullPathInMirror = Path.Combine(this.enlistment.MirrorRoot, relativePath); + string fullPathInMirror = this.GetFullPathInMirror(relativePath); string fullParentPath = Path.GetDirectoryName(fullPathInMirror); string fileName = Path.GetFileName(relativePath); string actualCaseName; - if (this.DirectoryExists(fullParentPath, fileName, out actualCaseName)) + ProjectedFileInfo.FileType type; + if (this.FileOrDirectoryExists(fullParentPath, fileName, out actualCaseName, out type)) { - return new ProjectedFileInfo(actualCaseName, size: 0, isDirectory: true); - } - else if (this.FileExists(fullParentPath, fileName, out actualCaseName)) - { - return new ProjectedFileInfo(actualCaseName, size: new FileInfo(fullPathInMirror).Length, isDirectory: false); + return new ProjectedFileInfo( + actualCaseName, + size: (type == ProjectedFileInfo.FileType.File) ? new FileInfo(fullPathInMirror).Length : 0, + type: type); } return null; @@ -54,7 +59,7 @@ protected ProjectedFileInfo GetFileInfo(string relativePath) protected IEnumerable GetChildItems(string relativePath) { - string fullPathInMirror = Path.Combine(this.enlistment.MirrorRoot, relativePath); + string fullPathInMirror = this.GetFullPathInMirror(relativePath); DirectoryInfo dirInfo = new DirectoryInfo(fullPathInMirror); if (!dirInfo.Exists) @@ -62,20 +67,38 @@ protected IEnumerable GetChildItems(string relativePath) yield break; } - foreach (FileInfo file in dirInfo.GetFiles()) + foreach (FileSystemInfo fileSystemInfo in dirInfo.GetFileSystemInfos()) { - yield return new ProjectedFileInfo(file.Name, file.Length, isDirectory: false); - } + if ((fileSystemInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) + { + // While not 100% accurate on all platforms, for simplicity assume that if the the file has reparse data it's a symlink + yield return new ProjectedFileInfo( + fileSystemInfo.Name, + size: 0, + type: ProjectedFileInfo.FileType.SymLink); + } + else if ((fileSystemInfo.Attributes & FileAttributes.Directory) == FileAttributes.Directory) + { + yield return new ProjectedFileInfo( + fileSystemInfo.Name, + size: 0, + type: ProjectedFileInfo.FileType.Directory); + } + else + { + FileInfo fileInfo = fileSystemInfo as FileInfo; + yield return new ProjectedFileInfo( + fileInfo.Name, + fileInfo.Length, + ProjectedFileInfo.FileType.File); + } - foreach (DirectoryInfo subDirectory in dirInfo.GetDirectories()) - { - yield return new ProjectedFileInfo(subDirectory.Name, size: 0, isDirectory: true); } } protected FileSystemResult HydrateFile(string relativePath, int bufferSize, Func tryWriteBytes) { - string fullPathInMirror = Path.Combine(this.enlistment.MirrorRoot, relativePath); + string fullPathInMirror = this.GetFullPathInMirror(relativePath); if (!File.Exists(fullPathInMirror)) { return FileSystemResult.EFileNotFound; @@ -106,23 +129,47 @@ protected FileSystemResult HydrateFile(string relativePath, int bufferSize, Func return FileSystemResult.Success; } - private bool DirectoryExists(string fullParentPath, string directoryName, out string actualCaseName) + private bool FileOrDirectoryExists( + string fullParentPath, + string fileName, + out string actualCaseName, + out ProjectedFileInfo.FileType type) { - return this.NameExists(Directory.GetDirectories(fullParentPath), directoryName, out actualCaseName); - } + actualCaseName = null; + type = ProjectedFileInfo.FileType.Invalid; - private bool FileExists(string fullParentPath, string fileName, out string actualCaseName) - { - return this.NameExists(Directory.GetFiles(fullParentPath), fileName, out actualCaseName); - } + DirectoryInfo dirInfo = new DirectoryInfo(fullParentPath); + if (!dirInfo.Exists) + { + return false; + } - private bool NameExists(IEnumerable paths, string name, out string actualCaseName) - { - actualCaseName = - paths - .Select(path => Path.GetFileName(path)) - .FirstOrDefault(actualName => actualName.Equals(name, StringComparison.OrdinalIgnoreCase)); - return actualCaseName != null; + FileSystemInfo fileSystemInfo = + dirInfo + .GetFileSystemInfos() + .FirstOrDefault(fsInfo => fsInfo.Name.Equals(fileName, StringComparison.OrdinalIgnoreCase)); + + if (fileSystemInfo == null) + { + return false; + } + + actualCaseName = fileSystemInfo.Name; + + if ((fileSystemInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) + { + type = ProjectedFileInfo.FileType.SymLink; + } + else if ((fileSystemInfo.Attributes & FileAttributes.Directory) == FileAttributes.Directory) + { + type = ProjectedFileInfo.FileType.Directory; + } + else + { + type = ProjectedFileInfo.FileType.File; + } + + return true; } } } diff --git a/MirrorProvider/MirrorProvider/ProjectedFileInfo.cs b/MirrorProvider/MirrorProvider/ProjectedFileInfo.cs index 310447b69c..71269ad2eb 100644 --- a/MirrorProvider/MirrorProvider/ProjectedFileInfo.cs +++ b/MirrorProvider/MirrorProvider/ProjectedFileInfo.cs @@ -2,15 +2,26 @@ { public class ProjectedFileInfo { - public ProjectedFileInfo(string name, long size, bool isDirectory) + public ProjectedFileInfo(string name, long size, FileType type) { this.Name = name; this.Size = size; - this.IsDirectory = isDirectory; + this.Type = type; + } + + public enum FileType + { + Invalid, + + File, + Directory, + SymLink + } public string Name { get; } public long Size { get; } - public bool IsDirectory { get; } + public FileType Type { get; } + public bool IsDirectory => this.Type == FileType.Directory; } }