diff --git a/GVFS/GVFS.Common/FileSystem/PhysicalFileSystem.cs b/GVFS/GVFS.Common/FileSystem/PhysicalFileSystem.cs index 9d0b23998d..cbe88f8516 100644 --- a/GVFS/GVFS.Common/FileSystem/PhysicalFileSystem.cs +++ b/GVFS/GVFS.Common/FileSystem/PhysicalFileSystem.cs @@ -103,9 +103,9 @@ public virtual void DeleteDirectory(string path, bool recursive = false) RecursiveDelete(path); } - public virtual bool IsSymlink(string path) + public virtual bool IsSymLink(string path) { - return (this.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint && NativeMethods.IsSymlink(path); + return (this.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint && NativeMethods.IsSymLink(path); } public virtual IEnumerable ItemsInDirectory(string path) diff --git a/GVFS/GVFS.Common/Git/GVFSGitObjects.cs b/GVFS/GVFS.Common/Git/GVFSGitObjects.cs index 6c77a211d3..6f3335f61a 100644 --- a/GVFS/GVFS.Common/Git/GVFSGitObjects.cs +++ b/GVFS/GVFS.Common/Git/GVFSGitObjects.cs @@ -28,6 +28,7 @@ public enum RequestSource FileStreamCallback, GVFSVerb, NamedPipeMessage, + SymLinkCreation, } protected GVFSContext Context { get; private set; } diff --git a/GVFS/GVFS.Common/NativeMethods.cs b/GVFS/GVFS.Common/NativeMethods.cs index 4f19f1e252..65074fdb05 100644 --- a/GVFS/GVFS.Common/NativeMethods.cs +++ b/GVFS/GVFS.Common/NativeMethods.cs @@ -127,7 +127,7 @@ public static uint GetWindowsBuildNumber() return versionInfo.BuildNumber; } - public static bool IsSymlink(string path) + public static bool IsSymLink(string path) { using (SafeFileHandle output = CreateFile( path, diff --git a/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs b/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs index 1f39f0285e..5dcca69e78 100644 --- a/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs +++ b/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs @@ -365,14 +365,14 @@ private void PerformIOBeforePlaceholderDatabaseUpgradeTest() this.fileSystem.DeleteDirectory(Path.Combine(this.Enlistment.RepoRoot, "GVFS\\GVFS.Tests\\Properties")); string junctionTarget = Path.Combine(this.Enlistment.EnlistmentRoot, "DirJunction"); - string symlinkTarget = Path.Combine(this.Enlistment.EnlistmentRoot, "DirSymlink"); + string symLinkTarget = Path.Combine(this.Enlistment.EnlistmentRoot, "DirSymLink"); Directory.CreateDirectory(junctionTarget); - Directory.CreateDirectory(symlinkTarget); + Directory.CreateDirectory(symLinkTarget); string junctionLink = Path.Combine(this.Enlistment.RepoRoot, "DirJunction"); - string symlink = Path.Combine(this.Enlistment.RepoRoot, "DirLink"); + string symLink = Path.Combine(this.Enlistment.RepoRoot, "DirLink"); ProcessHelper.Run("CMD.exe", "/C mklink /J " + junctionLink + " " + junctionTarget); - ProcessHelper.Run("CMD.exe", "/C mklink /D " + symlink + " " + symlinkTarget); + ProcessHelper.Run("CMD.exe", "/C mklink /D " + symLink + " " + symLinkTarget); string target = Path.Combine(this.Enlistment.EnlistmentRoot, "GVFS", "GVFS", "GVFS.UnitTests"); string link = Path.Combine(this.Enlistment.RepoRoot, "UnitTests"); diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs index cd1a79337a..747345cd56 100644 --- a/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs @@ -53,6 +53,14 @@ public BashRunner() } } + private enum FileType + { + Invalid, + File, + Directory, + SymLink, + } + protected override string FileName { get @@ -86,15 +94,22 @@ public static void DeleteDirectoryWithUnlimitedRetries(string path) } } - public override bool FileExists(string path) + public bool IsSymbolicLink(string path) { - string bashPath = this.ConvertWinPathToBashPath(path); + return this.FileExistsOnDisk(path, FileType.SymLink); + } - string command = string.Format("-c \"[ -f {0} ] && echo {1} || echo {2}\"", bashPath, ShellRunner.SuccessOutput, ShellRunner.FailureOutput); + public void CreateSymbolicLink(string newLinkFilePath, string existingFilePath) + { + string existingFileBashPath = this.ConvertWinPathToBashPath(existingFilePath); + string newLinkBashPath = this.ConvertWinPathToBashPath(newLinkFilePath); - string output = this.RunProcess(command).Trim(); + this.RunProcess(string.Format("-c \"ln -s -F {0} {1}\"", existingFileBashPath, newLinkBashPath)); + } - return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); + public override bool FileExists(string path) + { + return this.FileExistsOnDisk(path, FileType.File); } public override string MoveFile(string sourcePath, string targetPath) @@ -187,11 +202,7 @@ public override void WriteAllTextShouldFail(string path, string c public override bool DirectoryExists(string path) { - string bashPath = this.ConvertWinPathToBashPath(path); - - string output = this.RunProcess(string.Format("-c \"[ -d {0} ] && echo {1} || echo {2}\"", bashPath, ShellRunner.SuccessOutput, ShellRunner.FailureOutput)).Trim(); - - return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); + return this.FileExistsOnDisk(path, FileType.Directory); } public override void MoveDirectory(string sourcePath, string targetPath) @@ -267,6 +278,31 @@ public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) Assert.Fail("Unlike the other runners, bash.exe does not check folder handle before recusively deleting"); } + private bool FileExistsOnDisk(string path, FileType type) + { + string checkArgument = string.Empty; + switch (type) + { + case FileType.File: + checkArgument = "-f"; + break; + case FileType.Directory: + checkArgument = "-d"; + break; + case FileType.SymLink: + checkArgument = "-h"; + break; + default: + Assert.Fail($"{nameof(FileExistsOnDisk)} does not support {nameof(FileType)} {type}"); + break; + } + + string bashPath = this.ConvertWinPathToBashPath(path); + string command = $"-c \"[ {checkArgument} {bashPath} ] && echo {ShellRunner.SuccessOutput} || echo {ShellRunner.FailureOutput}\""; + string output = this.RunProcess(command).Trim(); + return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); + } + private string ConvertWinPathToBashPath(string winPath) { string bashPath = string.Concat("/", winPath); diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/SymbolicLinkTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/SymbolicLinkTests.cs new file mode 100644 index 0000000000..3b32e037b0 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/SymbolicLinkTests.cs @@ -0,0 +1,206 @@ +using System.IO; +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + // MacOnly until issue #297 (add SymLink support for Windows) is complete + [Category(Categories.MacOnly)] + [TestFixture] + public class SymbolicLinkTests : TestsWithEnlistmentPerFixture + { + private const string TestFolderName = "Test_EPF_SymbolicLinks"; + + // FunctionalTests/20180925_SymLinksPart1 files + private const string TestFileName = "TestFile.txt"; + private const string TestFileContents = "This is a real file"; + private const string TestFile2Name = "TestFile2.txt"; + private const string TestFile2Contents = "This is the second real file"; + private const string ChildFolderName = "ChildDir"; + private const string ChildLinkName = "LinkToFileInFolder"; + private const string GrandChildLinkName = "LinkToFileInParentFolder"; + + // FunctionalTests/20180925_SymLinksPart2 files + // Note: In this branch ChildLinkName has been changed to point to TestFile2Name + private const string GrandChildFileName = "TestFile3.txt"; + private const string GrandChildFileContents = "This is the third file"; + private const string GrandChildLinkNowAFileContents = "This was a link but is now a file"; + + // FunctionalTests/20180925_SymLinksPart3 files + private const string ChildFolder2Name = "ChildDir2"; + + // FunctionalTests/20180925_SymLinksPart4 files + // Note: In this branch ChildLinkName has been changed to a directory and ChildFolder2Name has been changed to a link to ChildFolderName + + private BashRunner bashRunner; + public SymbolicLinkTests() + { + this.bashRunner = new BashRunner(); + } + + [TestCase, Order(1)] + public void CheckoutBranchWithSymLinks() + { + GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart1"); + GitHelpers.CheckGitCommandAgainstGVFSRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart1", + "nothing to commit, working tree clean"); + + string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeFalse($"{testFilePath} should not be a symlink"); + GVFSHelpers.ModifiedPathsShouldNotContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + TestFileName); + + string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); + testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); + GVFSHelpers.ModifiedPathsShouldNotContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + TestFile2Name); + + string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); + this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink"); + childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents); + GVFSHelpers.ModifiedPathsShouldContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + ChildLinkName); + + string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName)); + this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeTrue($"{grandChildLinkPath} should be a symlink"); + grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + GVFSHelpers.ModifiedPathsShouldContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + ChildFolderName + "/" + GrandChildLinkName); + } + + [TestCase, Order(2)] + public void CheckoutBranchWhereSymLinksChangeContentsAndTransitionToFile() + { + GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart2"); + GitHelpers.CheckGitCommandAgainstGVFSRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart2", + "nothing to commit, working tree clean"); + + // testFilePath and testFile2Path are unchanged from FunctionalTests/20180925_SymLinksPart2 + string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeFalse($"{testFilePath} should not be a symlink"); + GVFSHelpers.ModifiedPathsShouldNotContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + TestFileName); + + string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); + testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); + GVFSHelpers.ModifiedPathsShouldNotContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + TestFile2Name); + + // In this branch childLinkPath has been changed to point to testFile2Path + string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); + this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink"); + childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + GVFSHelpers.ModifiedPathsShouldContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + ChildLinkName); + + // grandChildLinkPath should now be a file + string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName)); + this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeFalse($"{grandChildLinkPath} should not be a symlink"); + grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildLinkNowAFileContents); + + // There should also be a new file in the child folder + string newGrandChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildFileName)); + newGrandChildFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); + this.bashRunner.IsSymbolicLink(newGrandChildFilePath).ShouldBeFalse($"{newGrandChildFilePath} should not be a symlink"); + GVFSHelpers.ModifiedPathsShouldNotContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + ChildFolderName + "/" + GrandChildFileName); + } + + [TestCase, Order(3)] + public void CheckoutBranchWhereFilesTransitionToSymLinks() + { + GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart3"); + GitHelpers.CheckGitCommandAgainstGVFSRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart3", + "nothing to commit, working tree clean"); + + // In this branch testFilePath has been changed to point to newGrandChildFilePath + string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink"); + GVFSHelpers.ModifiedPathsShouldContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + TestFileName); + + // There should be a new ChildFolder2Name directory + string childFolder2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolder2Name)); + this.bashRunner.IsSymbolicLink(childFolder2Path).ShouldBeFalse($"{childFolder2Path} should not be a symlink"); + childFolder2Path.ShouldBeADirectory(this.bashRunner); + GVFSHelpers.ModifiedPathsShouldNotContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + ChildFolder2Name); + + // The rest of the files are unchanged from FunctionalTests/20180925_SymLinksPart2 + string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); + testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); + + string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); + this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink"); + childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + GVFSHelpers.ModifiedPathsShouldContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + ChildLinkName); + + string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName)); + this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeFalse($"{grandChildLinkPath} should not be a symlink"); + grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildLinkNowAFileContents); + + string newGrandChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildFileName)); + newGrandChildFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); + this.bashRunner.IsSymbolicLink(newGrandChildFilePath).ShouldBeFalse($"{newGrandChildFilePath} should not be a symlink"); + } + + [TestCase, Order(4)] + public void CheckoutBranchWhereSymLinkTransistionsToFolderAndFolderTransitionsToSymlink() + { + GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart4"); + GitHelpers.CheckGitCommandAgainstGVFSRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart4", + "nothing to commit, working tree clean"); + + // In this branch ChildLinkName has been changed to a directory and ChildFolder2Name has been changed to a link to ChildFolderName + string linkNowADirectoryPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); + this.bashRunner.IsSymbolicLink(linkNowADirectoryPath).ShouldBeFalse($"{linkNowADirectoryPath} should not be a symlink"); + linkNowADirectoryPath.ShouldBeADirectory(this.bashRunner); + GVFSHelpers.ModifiedPathsShouldContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + ChildLinkName); + + string directoryNowALinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolder2Name)); + this.bashRunner.IsSymbolicLink(directoryNowALinkPath).ShouldBeTrue($"{directoryNowALinkPath} should be a symlink"); + GVFSHelpers.ModifiedPathsShouldContain(this.bashRunner, this.Enlistment.DotGVFSRoot, TestFolderName + "/" + ChildFolder2Name); + } + + [TestCase, Order(5)] + public void GitStatusReportsSymLinkChanges() + { + GitHelpers.CheckGitCommandAgainstGVFSRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart4", + "nothing to commit, working tree clean"); + + string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink"); + + string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); + testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); + + // Update testFilePath's symlink to point to testFile2Path + this.bashRunner.CreateSymbolicLink(testFilePath, testFile2Path); + + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink"); + + GitHelpers.CheckGitCommandAgainstGVFSRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart4", + $"modified: {TestFolderName}/{TestFileName}"); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs b/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs index 58abd79851..0de0d127ca 100644 --- a/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs +++ b/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; namespace GVFS.FunctionalTests.Tools { diff --git a/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs b/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs index 127173d0e0..4b20c7e4ae 100644 --- a/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs +++ b/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Threading; namespace GVFS.Platform.Mac @@ -16,6 +17,8 @@ public class MacFileSystemVirtualizer : FileSystemVirtualizer { public static readonly byte[] PlaceholderVersionId = ToVersionIdByteArray(new byte[] { PlaceholderVersion }); + private const int SymLinkTargetBufferSize = 4096; + private const string ClassName = nameof(MacFileSystemVirtualizer); private VirtualizationInstance virtualizationInstance; @@ -80,15 +83,46 @@ public override FileSystemResult WritePlaceholderFile( string sha) { // TODO(Mac): Add functional tests that validate file mode is set correctly - ushort fileMode = this.FileSystemCallbacks.GitIndexProjection.GetFilePathMode(relativePath); - Result result = this.virtualizationInstance.WritePlaceholderFile( - relativePath, - PlaceholderVersionId, - ToVersionIdByteArray(FileSystemVirtualizer.ConvertShaToContentId(sha)), - (ulong)endOfFile, - fileMode); + GitIndexProjection.FileType fileType; + ushort fileMode; + this.FileSystemCallbacks.GitIndexProjection.GetFileTypeAndMode(relativePath, out fileType, out fileMode); - return new FileSystemResult(ResultToFSResult(result), unchecked((int)result)); + if (fileType == GitIndexProjection.FileType.Regular) + { + Result result = this.virtualizationInstance.WritePlaceholderFile( + relativePath, + PlaceholderVersionId, + ToVersionIdByteArray(FileSystemVirtualizer.ConvertShaToContentId(sha)), + (ulong)endOfFile, + fileMode); + + return new FileSystemResult(ResultToFSResult(result), unchecked((int)result)); + } + else if (fileType == GitIndexProjection.FileType.SymLink) + { + string symLinkTarget; + if (this.TryGetSymLinkTarget(sha, out symLinkTarget)) + { + Result result = this.virtualizationInstance.WriteSymLink(relativePath, symLinkTarget); + + this.FileSystemCallbacks.OnFileSymLinkCreated(relativePath); + + return new FileSystemResult(ResultToFSResult(result), unchecked((int)result)); + } + + EventMetadata metadata = this.CreateEventMetadata(relativePath); + metadata.Add(nameof(sha), sha); + this.Context.Tracer.RelatedError(metadata, $"{nameof(this.WritePlaceholderFile)}: Failed to read contents of symlink object"); + return new FileSystemResult(FSResult.IOError, 0); + } + else + { + EventMetadata metadata = this.CreateEventMetadata(relativePath); + metadata.Add(nameof(fileType), fileType); + metadata.Add(nameof(fileMode), fileMode); + this.Context.Tracer.RelatedError(metadata, $"{nameof(this.WritePlaceholderFile)}: Unsupported fileType"); + return new FileSystemResult(FSResult.IOError, 0); + } } public override FileSystemResult WritePlaceholderDirectory(string relativePath) @@ -114,18 +148,56 @@ public override FileSystemResult UpdatePlaceholderIfNeeded( // TODO(Mac): Add functional tests that include: // - Mode + content changes between commits // - Mode only changes (without any change to content, see issue #223) - ushort fileMode = this.FileSystemCallbacks.GitIndexProjection.GetFilePathMode(relativePath); - - Result result = this.virtualizationInstance.UpdatePlaceholderIfNeeded( - relativePath, - PlaceholderVersionId, - ToVersionIdByteArray(ConvertShaToContentId(shaContentId)), - (ulong)endOfFile, - fileMode, - (UpdateType)updateFlags, - out failureCause); - failureReason = (UpdateFailureReason)failureCause; - return new FileSystemResult(ResultToFSResult(result), unchecked((int)result)); + GitIndexProjection.FileType fileType; + ushort fileMode; + this.FileSystemCallbacks.GitIndexProjection.GetFileTypeAndMode(relativePath, out fileType, out fileMode); + + if (fileType == GitIndexProjection.FileType.Regular) + { + Result result = this.virtualizationInstance.UpdatePlaceholderIfNeeded( + relativePath, + PlaceholderVersionId, + ToVersionIdByteArray(ConvertShaToContentId(shaContentId)), + (ulong)endOfFile, + fileMode, + (UpdateType)updateFlags, + out failureCause); + + failureReason = (UpdateFailureReason)failureCause; + return new FileSystemResult(ResultToFSResult(result), unchecked((int)result)); + } + else if (fileType == GitIndexProjection.FileType.SymLink) + { + string symLinkTarget; + if (this.TryGetSymLinkTarget(shaContentId, out symLinkTarget)) + { + Result result = this.virtualizationInstance.ReplacePlaceholderFileWithSymLink( + relativePath, + symLinkTarget, + (UpdateType)updateFlags, + out failureCause); + + this.FileSystemCallbacks.OnFileSymLinkCreated(relativePath); + + failureReason = (UpdateFailureReason)failureCause; + return new FileSystemResult(ResultToFSResult(result), unchecked((int)result)); + } + + EventMetadata metadata = this.CreateEventMetadata(relativePath); + metadata.Add(nameof(shaContentId), shaContentId); + this.Context.Tracer.RelatedError(metadata, $"{nameof(this.UpdatePlaceholderIfNeeded)}: Failed to read contents of symlink object"); + failureReason = UpdateFailureReason.NoFailure; + return new FileSystemResult(FSResult.IOError, 0); + } + else + { + EventMetadata metadata = this.CreateEventMetadata(relativePath); + metadata.Add(nameof(fileType), fileType); + metadata.Add(nameof(fileMode), fileMode); + this.Context.Tracer.RelatedError(metadata, $"{nameof(this.UpdatePlaceholderIfNeeded)}: Unsupported fileType"); + failureReason = UpdateFailureReason.NoFailure; + return new FileSystemResult(FSResult.IOError, 0); + } } protected override bool TryStart(out string error) @@ -165,6 +237,86 @@ private static byte[] ToVersionIdByteArray(byte[] version) return bytes; } + /// + /// Gets the target of the symbolic link. + /// + /// SHA of the loose object containing the target path of the symbolic link + /// Target path of the symbolic link + private bool TryGetSymLinkTarget(string sha, out string symLinkTarget) + { + symLinkTarget = null; + + string symLinkBlobContents = null; + try + { + if (!this.GitObjects.TryCopyBlobContentStream( + sha, + CancellationToken.None, + GVFSGitObjects.RequestSource.SymLinkCreation, + (stream, blobLength) => + { + byte[] buffer = new byte[SymLinkTargetBufferSize]; + uint bufferIndex = 0; + + // TODO(Mac): Find a better solution than reading from the stream one byte at at time + int nextByte = stream.ReadByte(); + while (nextByte != -1) + { + while (bufferIndex < buffer.Length && nextByte != -1) + { + buffer[bufferIndex] = (byte)nextByte; + nextByte = stream.ReadByte(); + ++bufferIndex; + } + + if (bufferIndex < buffer.Length) + { + buffer[bufferIndex] = 0; + symLinkBlobContents = Encoding.UTF8.GetString(buffer); + } + else + { + buffer[bufferIndex - 1] = 0; + + EventMetadata metadata = this.CreateEventMetadata(); + metadata.Add(nameof(sha), sha); + metadata.Add("bufferContents", Encoding.UTF8.GetString(buffer)); + this.Context.Tracer.RelatedError(metadata, $"{nameof(this.TryGetSymLinkTarget)}: SymLink target exceeds buffer size"); + + throw new GetSymLinkTargetException("SymLink target exceeds buffer size");; + } + } + })) + { + EventMetadata metadata = this.CreateEventMetadata(); + metadata.Add(nameof(sha), sha); + this.Context.Tracer.RelatedError(metadata, $"{nameof(this.TryGetSymLinkTarget)}: TryCopyBlobContentStream failed"); + + return false; + } + } + catch (GetSymLinkTargetException e) + { + EventMetadata metadata = this.CreateEventMetadata(relativePath: null, exception: e); + metadata.Add(nameof(sha), sha); + this.Context.Tracer.RelatedError(metadata, $"{nameof(this.TryGetSymLinkTarget)}: TryCopyBlobContentStream caught GetSymLinkTargetException"); + + return false; + } + catch (DecoderFallbackException e) + { + EventMetadata metadata = this.CreateEventMetadata(relativePath: null, exception: e); + metadata.Add(nameof(sha), sha); + this.Context.Tracer.RelatedError(metadata, $"{nameof(this.TryGetSymLinkTarget)}: TryCopyBlobContentStream caught DecoderFallbackException"); + + return false; + } + + symLinkTarget = symLinkBlobContents; + + return true; + } + private Result OnGetFileStream( ulong commandId, string relativePath, @@ -258,7 +410,7 @@ private Result OnGetFileStream( { activity.RelatedError(metadata, $"{nameof(this.OnGetFileStream)}: TryCopyBlobContentStream failed"); - // TODO: Is this the correct Result to return? + // TODO(Mac): Is this the correct Result to return? return Result.EFileNotFound; } } @@ -536,5 +688,13 @@ public GetFileStreamException(string message, Result result) public Result Result { get; } } + + private class GetSymLinkTargetException : Exception + { + public GetSymLinkTargetException(string message) + : base(message) + { + } + } } } diff --git a/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout12to13Upgrade_FolderPlaceholder.cs b/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout12to13Upgrade_FolderPlaceholder.cs index 28653438ca..e2d88bf09c 100644 --- a/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout12to13Upgrade_FolderPlaceholder.cs +++ b/GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout12to13Upgrade_FolderPlaceholder.cs @@ -75,7 +75,7 @@ public override bool TryUpgrade(ITracer tracer, string enlistmentRoot) private static IEnumerable GetFolderPlaceholdersFromDisk(ITracer tracer, PhysicalFileSystem fileSystem, string path) { - if (!fileSystem.IsSymlink(path)) + if (!fileSystem.IsSymLink(path)) { foreach (string directory in fileSystem.EnumerateDirectories(path)) { diff --git a/GVFS/GVFS.UnitTests/Common/NamedPipeStreamReaderWriterTests.cs b/GVFS/GVFS.UnitTests/Common/NamedPipeStreamReaderWriterTests.cs index 003ca048fe..7e630fea6a 100644 --- a/GVFS/GVFS.UnitTests/Common/NamedPipeStreamReaderWriterTests.cs +++ b/GVFS/GVFS.UnitTests/Common/NamedPipeStreamReaderWriterTests.cs @@ -1,5 +1,6 @@ using GVFS.Common.NamedPipes; using GVFS.Tests.Should; +using GVFS.UnitTests.Category; using NUnit.Framework; using System.IO; @@ -58,6 +59,7 @@ public void CanSendBufferSizedMessage() [Test] [Description("Verify that the expected exception is thrown if message is not terminated with expected byte.")] + [Category(CategoryConstants.ExceptionExpected)] public void ReadingPartialMessgeThrows() { byte[] bytes = System.Text.Encoding.ASCII.GetBytes("This is a partial message"); diff --git a/GVFS/GVFS.UnitTests/Mock/Mac/MockVirtualizationInstance.cs b/GVFS/GVFS.UnitTests/Mock/Mac/MockVirtualizationInstance.cs index 57ae1b7204..532bbcbbf6 100644 --- a/GVFS/GVFS.UnitTests/Mock/Mac/MockVirtualizationInstance.cs +++ b/GVFS/GVFS.UnitTests/Mock/Mac/MockVirtualizationInstance.cs @@ -15,6 +15,8 @@ public MockVirtualizationInstance() { this.commandCompleted = new AutoResetEvent(false); this.CreatedPlaceholders = new ConcurrentDictionary(); + this.UpdatedPlaceholders = new ConcurrentDictionary(); + this.CreatedSymLinks = new ConcurrentHashSet(); this.WriteFileReturnResult = Result.Success; } @@ -27,6 +29,9 @@ public MockVirtualizationInstance() public UpdateFailureCause DeleteFileUpdateFailureCause { get; set; } public ConcurrentDictionary CreatedPlaceholders { get; private set; } + public ConcurrentDictionary UpdatedPlaceholders { get; private set; } + + public ConcurrentHashSet CreatedSymLinks { get; } public override EnumerateDirectoryCallback OnEnumerateDirectory { get; set; } public override GetFileStreamCallback OnGetFileStream { get; set; } @@ -79,6 +84,14 @@ public override Result WritePlaceholderFile( return Result.Success; } + public override Result WriteSymLink( + string relativePath, + string symLinkTarget) + { + this.CreatedSymLinks.Add(relativePath); + return Result.Success; + } + public override Result UpdatePlaceholderIfNeeded( string relativePath, byte[] providerId, @@ -88,6 +101,22 @@ public override Result UpdatePlaceholderIfNeeded( UpdateType updateFlags, out UpdateFailureCause failureCause) { + failureCause = this.UpdatePlaceholderIfNeededFailureCause; + if (failureCause == UpdateFailureCause.NoFailure) + { + this.UpdatedPlaceholders[relativePath] = fileMode; + } + + return this.UpdatePlaceholderIfNeededResult; + } + + public override Result ReplacePlaceholderFileWithSymLink( + string relativePath, + string symLinkTarget, + UpdateType updateFlags, + out UpdateFailureCause failureCause) + { + this.CreatedSymLinks.Add(relativePath); failureCause = this.UpdatePlaceholderIfNeededFailureCause; return this.UpdatePlaceholderIfNeededResult; } diff --git a/GVFS/GVFS.UnitTests/Mock/Virtualization/Projection/MockGitIndexProjection.cs b/GVFS/GVFS.UnitTests/Mock/Virtualization/Projection/MockGitIndexProjection.cs index 904946610a..c83730a735 100644 --- a/GVFS/GVFS.UnitTests/Mock/Virtualization/Projection/MockGitIndexProjection.cs +++ b/GVFS/GVFS.UnitTests/Mock/Virtualization/Projection/MockGitIndexProjection.cs @@ -36,7 +36,7 @@ public MockGitIndexProjection(IEnumerable projectedFiles) this.PlaceholdersCreated = new ConcurrentHashSet(); this.ExpandedFolders = new ConcurrentHashSet(); - this.MockFileModes = new ConcurrentDictionary(); + this.MockFileTypesAndModes = new ConcurrentDictionary(); this.unblockGetProjectedItems = new ManualResetEvent(true); this.waitForGetProjectedItems = new ManualResetEvent(true); @@ -56,7 +56,7 @@ public MockGitIndexProjection(IEnumerable projectedFiles) public ConcurrentHashSet ExpandedFolders { get; } - public ConcurrentDictionary MockFileModes { get; } + public ConcurrentDictionary MockFileTypesAndModes { get; } public bool ThrowOperationCanceledExceptionOnProjectionRequest { get; set; } @@ -161,15 +161,18 @@ public override bool TryGetProjectedItemsFromMemory(string folderPath, out List< return false; } - public override ushort GetFilePathMode(string path) + public override void GetFileTypeAndMode(string path, out FileType fileType, out ushort fileMode) { - ushort result; - if (this.MockFileModes.TryGetValue(path, out result)) + fileType = FileType.Invalid; + fileMode = 0; + + ushort mockFileTypeAndMode; + if (this.MockFileTypesAndModes.TryGetValue(path, out mockFileTypeAndMode)) { - return result; + FileTypeAndMode typeAndMode = new FileTypeAndMode(mockFileTypeAndMode); + fileType = typeAndMode.Type; + fileMode = typeAndMode.Mode; } - - return 0; } public override List GetProjectedItems( diff --git a/GVFS/GVFS.UnitTests/Platform.Mac/MacFileSystemVirtualizerTests.cs b/GVFS/GVFS.UnitTests/Platform.Mac/MacFileSystemVirtualizerTests.cs index 675ae8a938..96c2d6e288 100644 --- a/GVFS/GVFS.UnitTests/Platform.Mac/MacFileSystemVirtualizerTests.cs +++ b/GVFS/GVFS.UnitTests/Platform.Mac/MacFileSystemVirtualizerTests.cs @@ -10,6 +10,7 @@ using GVFS.UnitTests.Virtual; using GVFS.Virtualization; using GVFS.Virtualization.FileSystem; +using GVFS.Virtualization.Projection; using NUnit.Framework; using PrjFSLib.Mac; using System; @@ -21,10 +22,6 @@ namespace GVFS.UnitTests.Platform.Mac [TestFixture] public class MacFileSystemVirtualizerTests : TestsWithCommonRepo { - private static readonly ushort FileMode644 = Convert.ToUInt16("644", 8); - private static readonly ushort FileMode664 = Convert.ToUInt16("664", 8); - private static readonly ushort FileMode755 = Convert.ToUInt16("755", 8); - private static readonly Dictionary MappedResults = new Dictionary() { { Result.Success, FSResult.Ok }, @@ -54,19 +51,20 @@ public void DeleteFile() using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance()) using (MacFileSystemVirtualizer virtualizer = new MacFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization)) { + const string DeleteTestFileName = "deleteMe.txt"; UpdateFailureReason failureReason = UpdateFailureReason.NoFailure; mockVirtualization.DeleteFileResult = Result.Success; mockVirtualization.DeleteFileUpdateFailureCause = UpdateFailureCause.NoFailure; virtualizer - .DeleteFile("test.txt", UpdatePlaceholderType.AllowReadOnly, out failureReason) + .DeleteFile(DeleteTestFileName, UpdatePlaceholderType.AllowReadOnly, out failureReason) .ShouldEqual(new FileSystemResult(FSResult.Ok, (int)mockVirtualization.DeleteFileResult)); failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.DeleteFileUpdateFailureCause); mockVirtualization.DeleteFileResult = Result.EFileNotFound; mockVirtualization.DeleteFileUpdateFailureCause = UpdateFailureCause.NoFailure; virtualizer - .DeleteFile("test.txt", UpdatePlaceholderType.AllowReadOnly, out failureReason) + .DeleteFile(DeleteTestFileName, UpdatePlaceholderType.AllowReadOnly, out failureReason) .ShouldEqual(new FileSystemResult(FSResult.FileOrPathNotFound, (int)mockVirtualization.DeleteFileResult)); failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.DeleteFileUpdateFailureCause); @@ -76,7 +74,7 @@ public void DeleteFile() // TODO: The result should probably be VirtualizationInvalidOperation but for now it's IOError mockVirtualization.DeleteFileUpdateFailureCause = UpdateFailureCause.DirtyData; virtualizer - .DeleteFile("test.txt", UpdatePlaceholderType.AllowReadOnly, out failureReason) + .DeleteFile(DeleteTestFileName, UpdatePlaceholderType.AllowReadOnly, out failureReason) .ShouldEqual(new FileSystemResult(FSResult.IOError, (int)mockVirtualization.DeleteFileResult)); failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.DeleteFileUpdateFailureCause); } @@ -85,9 +83,10 @@ public void DeleteFile() [TestCase] public void UpdatePlaceholderIfNeeded() { + const string UpdatePlaceholderFileName = "testUpdatePlaceholder.txt"; using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner()) using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance()) - using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { "test.txt" })) + using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { UpdatePlaceholderFileName })) using (MacFileSystemVirtualizer virtualizer = new MacFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization)) using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks( this.Repo.Context, @@ -98,7 +97,10 @@ public void UpdatePlaceholderIfNeeded() backgroundTaskRunner, virtualizer)) { - gitIndexProjection.MockFileModes.TryAdd("test" + Path.DirectorySeparatorChar + "test.txt", FileMode644); + gitIndexProjection.MockFileTypesAndModes.TryAdd( + UpdatePlaceholderFileName, + ConvertFileTypeAndModeToIndexFormat(GitIndexProjection.FileType.Regular, GitIndexProjection.FileMode644)); + string error; fileSystemCallbacks.TryStart(out error).ShouldEqual(true); @@ -108,7 +110,7 @@ public void UpdatePlaceholderIfNeeded() mockVirtualization.UpdatePlaceholderIfNeededFailureCause = UpdateFailureCause.NoFailure; virtualizer .UpdatePlaceholderIfNeeded( - "test.txt", + UpdatePlaceholderFileName, DateTime.Now, DateTime.Now, DateTime.Now, @@ -120,12 +122,14 @@ public void UpdatePlaceholderIfNeeded() out failureReason) .ShouldEqual(new FileSystemResult(FSResult.Ok, (int)mockVirtualization.UpdatePlaceholderIfNeededResult)); failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.UpdatePlaceholderIfNeededFailureCause); + mockVirtualization.UpdatedPlaceholders.ShouldContain(path => path.Key.Equals(UpdatePlaceholderFileName) && path.Value == GitIndexProjection.FileMode644); + mockVirtualization.UpdatedPlaceholders.Clear(); mockVirtualization.UpdatePlaceholderIfNeededResult = Result.EFileNotFound; mockVirtualization.UpdatePlaceholderIfNeededFailureCause = UpdateFailureCause.NoFailure; virtualizer .UpdatePlaceholderIfNeeded( - "test.txt", + UpdatePlaceholderFileName, DateTime.Now, DateTime.Now, DateTime.Now, @@ -145,7 +149,7 @@ public void UpdatePlaceholderIfNeeded() // TODO: The result should probably be VirtualizationInvalidOperation but for now it's IOError virtualizer .UpdatePlaceholderIfNeeded( - "test.txt", + UpdatePlaceholderFileName, DateTime.Now, DateTime.Now, DateTime.Now, @@ -161,6 +165,103 @@ public void UpdatePlaceholderIfNeeded() } } + [TestCase] + public void WritePlaceholderForSymLink() + { + const string WriteSymLinkFileName = "testWriteSymLink.txt"; + using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner()) + using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance()) + using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { WriteSymLinkFileName })) + using (MacFileSystemVirtualizer virtualizer = new MacFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization)) + using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks( + this.Repo.Context, + this.Repo.GitObjects, + RepoMetadata.Instance, + new MockBlobSizes(), + gitIndexProjection, + backgroundTaskRunner, + virtualizer)) + { + gitIndexProjection.MockFileTypesAndModes.TryAdd( + WriteSymLinkFileName, + ConvertFileTypeAndModeToIndexFormat(GitIndexProjection.FileType.SymLink, fileMode: 0)); + + string error; + fileSystemCallbacks.TryStart(out error).ShouldEqual(true); + + virtualizer.WritePlaceholderFile( + WriteSymLinkFileName, + endOfFile: 0, + sha: string.Empty).ShouldEqual(new FileSystemResult(FSResult.Ok, (int)Result.Success)); + + mockVirtualization.CreatedPlaceholders.ShouldBeEmpty(); + mockVirtualization.CreatedSymLinks.Count.ShouldEqual(1); + mockVirtualization.CreatedSymLinks.ShouldContain(entry => entry.Equals(WriteSymLinkFileName)); + + // Creating a symlink should schedule a background task + backgroundTaskRunner.Count.ShouldEqual(1); + backgroundTaskRunner.BackgroundTasks[0].Operation.ShouldEqual(GVFS.Virtualization.Background.FileSystemTask.OperationType.OnFileSymLinkCreated); + backgroundTaskRunner.BackgroundTasks[0].VirtualPath.ShouldEqual(WriteSymLinkFileName); + + fileSystemCallbacks.Stop(); + } + } + + [TestCase] + public void UpdatePlaceholderToSymLink() + { + const string PlaceholderToLinkFileName = "testUpdatePlaceholderToLink.txt"; + using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner()) + using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance()) + using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { PlaceholderToLinkFileName })) + using (MacFileSystemVirtualizer virtualizer = new MacFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization)) + using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks( + this.Repo.Context, + this.Repo.GitObjects, + RepoMetadata.Instance, + new MockBlobSizes(), + gitIndexProjection, + backgroundTaskRunner, + virtualizer)) + { + gitIndexProjection.MockFileTypesAndModes.TryAdd( + PlaceholderToLinkFileName, + ConvertFileTypeAndModeToIndexFormat(GitIndexProjection.FileType.SymLink, fileMode: 0)); + + string error; + fileSystemCallbacks.TryStart(out error).ShouldEqual(true); + + UpdateFailureReason failureReason = UpdateFailureReason.NoFailure; + + mockVirtualization.UpdatePlaceholderIfNeededResult = Result.Success; + mockVirtualization.UpdatePlaceholderIfNeededFailureCause = UpdateFailureCause.NoFailure; + virtualizer + .UpdatePlaceholderIfNeeded( + PlaceholderToLinkFileName, + DateTime.Now, + DateTime.Now, + DateTime.Now, + DateTime.Now, + 0, + 15, + string.Empty, + UpdatePlaceholderType.AllowReadOnly, + out failureReason) + .ShouldEqual(new FileSystemResult(FSResult.Ok, (int)mockVirtualization.UpdatePlaceholderIfNeededResult)); + failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.UpdatePlaceholderIfNeededFailureCause); + mockVirtualization.UpdatedPlaceholders.Count.ShouldEqual(0, "UpdatePlaceholderIfNeeded should not be called when converting a placeholder to a link"); + mockVirtualization.CreatedSymLinks.Count.ShouldEqual(1); + mockVirtualization.CreatedSymLinks.ShouldContain(entry => entry.Equals(PlaceholderToLinkFileName)); + + // Creating a symlink should schedule a background task + backgroundTaskRunner.Count.ShouldEqual(1); + backgroundTaskRunner.BackgroundTasks[0].Operation.ShouldEqual(GVFS.Virtualization.Background.FileSystemTask.OperationType.OnFileSymLinkCreated); + backgroundTaskRunner.BackgroundTasks[0].VirtualPath.ShouldEqual(PlaceholderToLinkFileName); + + fileSystemCallbacks.Stop(); + } + } + [TestCase] public void ClearNegativePathCacheIsNoOp() { @@ -176,9 +277,15 @@ public void ClearNegativePathCacheIsNoOp() [TestCase] public void OnEnumerateDirectoryReturnsSuccessWhenResultsNotInMemory() { + const string TestFileName = "test.txt"; + const string TestFolderName = "testFolder"; + string testFilePath = Path.Combine(TestFolderName, TestFileName); using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner()) using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance()) - using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { "test.txt" })) + + // Don't include TestFolderName as MockGitIndexProjection returns the same list of files regardless of what folder name + // it is passed + using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { TestFileName })) using (MacFileSystemVirtualizer virtualizer = new MacFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization)) using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks( this.Repo.Context, @@ -189,16 +296,18 @@ public void OnEnumerateDirectoryReturnsSuccessWhenResultsNotInMemory() backgroundFileSystemTaskRunner: backgroundTaskRunner, fileSystemVirtualizer: virtualizer)) { - gitIndexProjection.MockFileModes.TryAdd("test" + Path.DirectorySeparatorChar + "test.txt", FileMode644); + gitIndexProjection.MockFileTypesAndModes.TryAdd( + testFilePath, + ConvertFileTypeAndModeToIndexFormat(GitIndexProjection.FileType.Regular, GitIndexProjection.FileMode644)); string error; fileSystemCallbacks.TryStart(out error).ShouldEqual(true); Guid enumerationGuid = Guid.NewGuid(); gitIndexProjection.EnumerationInMemory = false; - mockVirtualization.OnEnumerateDirectory(1, "test", triggeringProcessId: 1, triggeringProcessName: "UnitTests").ShouldEqual(Result.Success); + mockVirtualization.OnEnumerateDirectory(1, TestFolderName, triggeringProcessId: 1, triggeringProcessName: "UnitTests").ShouldEqual(Result.Success); mockVirtualization.CreatedPlaceholders.ShouldContain( - kvp => kvp.Key.Equals(Path.Combine("test", "test.txt"), StringComparison.OrdinalIgnoreCase) && kvp.Value == FileMode644); + kvp => kvp.Key.Equals(testFilePath, StringComparison.OrdinalIgnoreCase) && kvp.Value == GitIndexProjection.FileMode644); fileSystemCallbacks.Stop(); } } @@ -206,9 +315,15 @@ public void OnEnumerateDirectoryReturnsSuccessWhenResultsNotInMemory() [TestCase] public void OnEnumerateDirectoryReturnsSuccessWhenResultsInMemory() { + const string TestFileName = "test.txt"; + const string TestFolderName = "testFolder"; + string testFilePath = Path.Combine(TestFolderName, TestFileName); using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner()) using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance()) - using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { "test.txt" })) + + // Don't include TestFolderName as MockGitIndexProjection returns the same list of files regardless of what folder name + // it is passed + using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { TestFileName })) using (MacFileSystemVirtualizer virtualizer = new MacFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization)) using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks( this.Repo.Context, @@ -219,17 +334,19 @@ public void OnEnumerateDirectoryReturnsSuccessWhenResultsInMemory() backgroundFileSystemTaskRunner: backgroundTaskRunner, fileSystemVirtualizer: virtualizer)) { - gitIndexProjection.MockFileModes.TryAdd("test" + Path.DirectorySeparatorChar + "test.txt", FileMode644); + gitIndexProjection.MockFileTypesAndModes.TryAdd( + testFilePath, + ConvertFileTypeAndModeToIndexFormat(GitIndexProjection.FileType.Regular, GitIndexProjection.FileMode644)); string error; fileSystemCallbacks.TryStart(out error).ShouldEqual(true); Guid enumerationGuid = Guid.NewGuid(); gitIndexProjection.EnumerationInMemory = true; - mockVirtualization.OnEnumerateDirectory(1, "test", triggeringProcessId: 1, triggeringProcessName: "UnitTests").ShouldEqual(Result.Success); + mockVirtualization.OnEnumerateDirectory(1, TestFolderName, triggeringProcessId: 1, triggeringProcessName: "UnitTests").ShouldEqual(Result.Success); mockVirtualization.CreatedPlaceholders.ShouldContain( - kvp => kvp.Key.Equals(Path.Combine("test", "test.txt"), StringComparison.OrdinalIgnoreCase) && kvp.Value == FileMode644); - gitIndexProjection.ExpandedFolders.ShouldMatchInOrder("test"); + kvp => kvp.Key.Equals(testFilePath, StringComparison.OrdinalIgnoreCase) && kvp.Value == GitIndexProjection.FileMode644); + gitIndexProjection.ExpandedFolders.ShouldMatchInOrder(TestFolderName); fileSystemCallbacks.Stop(); } } @@ -237,9 +354,19 @@ public void OnEnumerateDirectoryReturnsSuccessWhenResultsInMemory() [TestCase] public void OnEnumerateDirectorySetsFileModes() { + const string TestFile644Name = "test644.txt"; + const string TestFile664Name = "test664.txt"; + const string TestFile755Name = "test755.txt"; + const string TestFolderName = "testFolder"; + string testFile644Path = Path.Combine(TestFolderName, TestFile644Name); + string testFile664Path = Path.Combine(TestFolderName, TestFile664Name); + string testFile755Path = Path.Combine(TestFolderName, TestFile755Name); using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner()) using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance()) - using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { "test644.txt", "test664.txt", "test755.txt" })) + + // Don't include TestFolderName as MockGitIndexProjection returns the same list of files regardless of what folder name + // it is passed + using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { TestFile644Name, TestFile664Name, TestFile755Name })) using (MacFileSystemVirtualizer virtualizer = new MacFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization)) using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks( this.Repo.Context, @@ -250,22 +377,28 @@ public void OnEnumerateDirectorySetsFileModes() backgroundFileSystemTaskRunner: backgroundTaskRunner, fileSystemVirtualizer: virtualizer)) { - gitIndexProjection.MockFileModes.TryAdd("test" + Path.DirectorySeparatorChar + "test644.txt", FileMode644); - gitIndexProjection.MockFileModes.TryAdd("test" + Path.DirectorySeparatorChar + "test664.txt", FileMode664); - gitIndexProjection.MockFileModes.TryAdd("test" + Path.DirectorySeparatorChar + "test755.txt", FileMode755); + gitIndexProjection.MockFileTypesAndModes.TryAdd( + testFile644Path, + ConvertFileTypeAndModeToIndexFormat(GitIndexProjection.FileType.Regular, GitIndexProjection.FileMode644)); + gitIndexProjection.MockFileTypesAndModes.TryAdd( + testFile664Path, + ConvertFileTypeAndModeToIndexFormat(GitIndexProjection.FileType.Regular, GitIndexProjection.FileMode664)); + gitIndexProjection.MockFileTypesAndModes.TryAdd( + testFile755Path, + ConvertFileTypeAndModeToIndexFormat(GitIndexProjection.FileType.Regular, GitIndexProjection.FileMode755)); string error; fileSystemCallbacks.TryStart(out error).ShouldEqual(true); Guid enumerationGuid = Guid.NewGuid(); gitIndexProjection.EnumerationInMemory = true; - mockVirtualization.OnEnumerateDirectory(1, "test", triggeringProcessId: 1, triggeringProcessName: "UnitTests").ShouldEqual(Result.Success); + mockVirtualization.OnEnumerateDirectory(1, TestFolderName, triggeringProcessId: 1, triggeringProcessName: "UnitTests").ShouldEqual(Result.Success); mockVirtualization.CreatedPlaceholders.ShouldContain( - kvp => kvp.Key.Equals(Path.Combine("test", "test644.txt"), StringComparison.OrdinalIgnoreCase) && kvp.Value == FileMode644); + kvp => kvp.Key.Equals(testFile644Path, StringComparison.OrdinalIgnoreCase) && kvp.Value == GitIndexProjection.FileMode644); mockVirtualization.CreatedPlaceholders.ShouldContain( - kvp => kvp.Key.Equals(Path.Combine("test", "test664.txt"), StringComparison.OrdinalIgnoreCase) && kvp.Value == FileMode664); + kvp => kvp.Key.Equals(testFile664Path, StringComparison.OrdinalIgnoreCase) && kvp.Value == GitIndexProjection.FileMode664); mockVirtualization.CreatedPlaceholders.ShouldContain( - kvp => kvp.Key.Equals(Path.Combine("test", "test755.txt"), StringComparison.OrdinalIgnoreCase) && kvp.Value == FileMode755); + kvp => kvp.Key.Equals(testFile755Path, StringComparison.OrdinalIgnoreCase) && kvp.Value == GitIndexProjection.FileMode755); fileSystemCallbacks.Stop(); } } @@ -273,9 +406,10 @@ public void OnEnumerateDirectorySetsFileModes() [TestCase] public void OnGetFileStreamReturnsSuccessWhenFileStreamAvailable() { + const string TestFileName = "test.txt"; using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner()) using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance()) - using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { "test.txt" })) + using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { TestFileName })) using (MacFileSystemVirtualizer virtualizer = new MacFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization)) using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks( this.Repo.Context, @@ -299,7 +433,7 @@ public void OnGetFileStreamReturnsSuccessWhenFileStreamAvailable() mockVirtualization.OnGetFileStream( commandId: 1, - relativePath: "test.txt", + relativePath: TestFileName, providerId: placeholderVersion, contentId: contentId, triggeringProcessId: 2, @@ -316,9 +450,10 @@ public void OnGetFileStreamReturnsSuccessWhenFileStreamAvailable() [Category(CategoryConstants.ExceptionExpected)] public void OnGetFileStreamReturnsErrorWhenWriteFileContentsFails() { + const string TestFileName = "test.txt"; using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner()) using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance()) - using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { "test.txt" })) + using (MockGitIndexProjection gitIndexProjection = new MockGitIndexProjection(new[] { TestFileName })) using (MacFileSystemVirtualizer virtualizer = new MacFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization)) using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks( this.Repo.Context, @@ -342,7 +477,7 @@ public void OnGetFileStreamReturnsErrorWhenWriteFileContentsFails() mockVirtualization.OnGetFileStream( commandId: 1, - relativePath: "test.txt", + relativePath: TestFileName, providerId: placeholderVersion, contentId: contentId, triggeringProcessId: 2, @@ -352,5 +487,29 @@ public void OnGetFileStreamReturnsErrorWhenWriteFileContentsFails() fileSystemCallbacks.Stop(); } } + + private static ushort ConvertFileTypeAndModeToIndexFormat(GitIndexProjection.FileType fileType, ushort fileMode) + { + // Values used in the index file to indicate the type of the file + const ushort RegularFileIndexEntry = 0x8000; + const ushort SymLinkFileIndexEntry = 0xA000; + const ushort GitLinkFileIndexEntry = 0xE000; + + switch (fileType) + { + case GitIndexProjection.FileType.Regular: + return (ushort)(RegularFileIndexEntry | fileMode); + + case GitIndexProjection.FileType.SymLink: + return (ushort)(SymLinkFileIndexEntry | fileMode); + + case GitIndexProjection.FileType.GitLink: + return (ushort)(GitLinkFileIndexEntry | fileMode); + + default: + Assert.Fail($"Invalid fileType {fileType}"); + return 0; + } + } } } diff --git a/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs b/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs index eaab198a4e..ff7cae855f 100644 --- a/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs +++ b/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs @@ -32,6 +32,7 @@ public enum OperationType OnFileHardLinkCreated, OnFilePreDelete, OnFolderPreDelete, + OnFileSymLinkCreated, } public OperationType Operation { get; } @@ -54,6 +55,11 @@ public static FileSystemTask OnFileHardLinkCreated(string newLinkRelativePath) return new FileSystemTask(OperationType.OnFileHardLinkCreated, newLinkRelativePath, oldVirtualPath: null); } + public static FileSystemTask OnFileSymLinkCreated(string newLinkRelativePath) + { + return new FileSystemTask(OperationType.OnFileSymLinkCreated, newLinkRelativePath, oldVirtualPath: null); + } + public static FileSystemTask OnFileDeleted(string virtualPath) { return new FileSystemTask(OperationType.OnFileDeleted, virtualPath, oldVirtualPath: null); diff --git a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs index d7c0d57b4c..1031d625fd 100644 --- a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs +++ b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs @@ -433,6 +433,11 @@ public virtual void OnFileHardLinkCreated(string newLinkRelativePath) this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileHardLinkCreated(newLinkRelativePath)); } + public virtual void OnFileSymLinkCreated(string newLinkRelativePath) + { + this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileSymLinkCreated(newLinkRelativePath)); + } + public void OnFileDeleted(string relativePath) { this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileDeleted(relativePath)); @@ -631,6 +636,7 @@ private FileSystemTaskResult ExecuteBackgroundOperation(FileSystemTask gitUpdate case FileSystemTask.OperationType.OnFileCreated: case FileSystemTask.OperationType.OnFailedPlaceholderDelete: case FileSystemTask.OperationType.OnFileHardLinkCreated: + case FileSystemTask.OperationType.OnFileSymLinkCreated: metadata.Add("virtualPath", gitUpdate.VirtualPath); result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath); break; diff --git a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FileTypeAndMode.cs b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FileTypeAndMode.cs new file mode 100644 index 0000000000..7c20ca2c58 --- /dev/null +++ b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FileTypeAndMode.cs @@ -0,0 +1,47 @@ +using System; +namespace GVFS.Virtualization.Projection +{ + public partial class GitIndexProjection + { + internal struct FileTypeAndMode + { + // Bitmasks for extracting file type and mode from the ushort stored in the index + private const ushort FileTypeMask = 0xF000; + private const ushort FileModeMask = 0x1FF; + + // Values used in the index file to indicate the type of the file + private const ushort RegularFileIndexEntry = 0x8000; + private const ushort SymLinkFileIndexEntry = 0xA000; + private const ushort GitLinkFileIndexEntry = 0xE000; + + public FileTypeAndMode(ushort typeAndModeInIndexFormat) + { + switch (typeAndModeInIndexFormat & FileTypeMask) + { + case RegularFileIndexEntry: + this.Type = FileType.Regular; + break; + case SymLinkFileIndexEntry: + this.Type = FileType.SymLink; + break; + case GitLinkFileIndexEntry: + this.Type = FileType.GitLink; + break; + default: + this.Type = FileType.Invalid; + break; + } + + this.Mode = (ushort)(typeAndModeInIndexFormat & FileModeMask); + } + + public FileType Type { get; } + public ushort Mode { get; } + + public string GetModeAsOctalString() + { + return Convert.ToString(this.Mode, 8); + } + } + } +} diff --git a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexEntry.cs b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexEntry.cs index 1da6d66426..8cd38e283d 100644 --- a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexEntry.cs +++ b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexEntry.cs @@ -27,7 +27,7 @@ public GitIndexEntry() public byte[] Sha { get; } = new byte[20]; public bool SkipWorktree { get; set; } - public ushort FileMode { get; set; } + public FileTypeAndMode TypeAndMode { get; set; } public GitIndexParser.MergeStage MergeState { get; set; } public int ReplaceIndex { get; set; } diff --git a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexParser.cs b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexParser.cs index 682f9ec3ba..4893ede463 100644 --- a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexParser.cs +++ b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexParser.cs @@ -181,8 +181,36 @@ private FileSystemTaskResult ParseIndex(ITracer tracer, Stream indexStream, Func // 3-bit unused // 9-bit unix permission. Only 0755 and 0644 are valid for regular files. (Legacy repos can also contain 664) // Symbolic links and gitlinks have value 0 in this field. - ushort mode = this.ReadUInt16(); - this.resuableParsedIndexEntry.FileMode = (ushort)(mode & 0x1FF); + ushort indexFormatTypeAndMode = this.ReadUInt16(); + + FileTypeAndMode typeAndMode = new FileTypeAndMode(indexFormatTypeAndMode); + + switch (typeAndMode.Type) + { + case FileType.Regular: + if (typeAndMode.Mode != FileMode755 && + typeAndMode.Mode != FileMode644 && + typeAndMode.Mode != FileMode664) + { + throw new InvalidDataException($"Invalid file mode {typeAndMode.GetModeAsOctalString()} found for regular file in index"); + } + + break; + + case FileType.SymLink: + case FileType.GitLink: + if (typeAndMode.Mode != 0) + { + throw new InvalidDataException($"Invalid file mode {typeAndMode.GetModeAsOctalString()} found for link file({typeAndMode.Type:X}) in index"); + } + + break; + + default: + throw new InvalidDataException($"Invalid file type {typeAndMode.Type:X} found in index"); + } + + this.resuableParsedIndexEntry.TypeAndMode = typeAndMode; this.Skip(12); } diff --git a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.cs b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.cs index 66b1396156..c99808b744 100644 --- a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.cs +++ b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.cs @@ -22,7 +22,9 @@ public partial class GitIndexProjection : IDisposable, IProfilerOnlyIndexProject { public const string ProjectionIndexBackupName = "GVFS_projection"; - protected static readonly ushort FileMode644 = Convert.ToUInt16("644", 8); + public static readonly ushort FileMode755 = Convert.ToUInt16("755", 8); + public static readonly ushort FileMode664 = Convert.ToUInt16("664", 8); + public static readonly ushort FileMode644 = Convert.ToUInt16("644", 8); private const int IndexFileStreamBufferSize = 512 * 1024; @@ -52,9 +54,9 @@ public partial class GitIndexProjection : IDisposable, IProfilerOnlyIndexProject // Cache of folder paths (in Windows format) to folder data private ConcurrentDictionary projectionFolderCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - // nonDefaultFileModes is only populated when the platform supports file mode - // On platforms that support file modes, file paths that are not in nonDefaultFileModes have mode 644 - private Dictionary nonDefaultFileModes = new Dictionary(StringComparer.OrdinalIgnoreCase); + // nonDefaultFileTypesAndModes is only populated when the platform supports file mode + // On platforms that support file modes, file paths that are not in nonDefaultFileTypesAndModes are regular files with mode 644 + private Dictionary nonDefaultFileTypesAndModes = new Dictionary(StringComparer.OrdinalIgnoreCase); private BlobSizes blobSizes; private PlaceholderListDatabase placeholderList; @@ -118,6 +120,15 @@ protected GitIndexProjection() { } + public enum FileType : short + { + Invalid, + + Regular, + SymLink, + GitLink, + } + public int EstimatedPlaceholderCount { get @@ -356,24 +367,26 @@ public virtual bool TryGetProjectedItemsFromMemory(string folderPath, out List

")); } diff --git a/ProjFS.Mac/PrjFSLib.Mac.Managed/Interop/PrjFSLib.cs b/ProjFS.Mac/PrjFSLib.Mac.Managed/Interop/PrjFSLib.cs index 6edba146fe..13b0e58630 100644 --- a/ProjFS.Mac/PrjFSLib.Mac.Managed/Interop/PrjFSLib.cs +++ b/ProjFS.Mac/PrjFSLib.Mac.Managed/Interop/PrjFSLib.cs @@ -32,8 +32,13 @@ public static extern Result WritePlaceholderFile( ulong fileSize, ushort fileMode); + [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_WriteSymLink")] + public static extern Result WriteSymLink( + string relativePath, + string symLinkTarget); + [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_UpdatePlaceholderFileIfNeeded")] - public static extern Result UpdatePlaceholderFileIfNeeded( + public static extern Result UpdatePlaceholderFileIfNeeded( string relativePath, [MarshalAs(UnmanagedType.LPArray, SizeConst = PlaceholderIdLength)] byte[] providerId, @@ -44,6 +49,13 @@ public static extern Result UpdatePlaceholderFileIfNeeded( UpdateType updateType, ref UpdateFailureCause failureCause); + [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_ReplacePlaceholderFileWithSymLink")] + public static extern Result ReplacePlaceholderFileWithSymLink( + string relativePath, + string symLinkTarget, + UpdateType updateType, + ref UpdateFailureCause failureCause); + [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_DeleteFile")] public static extern Result DeleteFile( string relativePath, diff --git a/ProjFS.Mac/PrjFSLib.Mac.Managed/VirtualizationInstance.cs b/ProjFS.Mac/PrjFSLib.Mac.Managed/VirtualizationInstance.cs index 3b7cf74b47..97b22f8200 100644 --- a/ProjFS.Mac/PrjFSLib.Mac.Managed/VirtualizationInstance.cs +++ b/ProjFS.Mac/PrjFSLib.Mac.Managed/VirtualizationInstance.cs @@ -108,6 +108,13 @@ public virtual Result WritePlaceholderFile( fileMode); } + public virtual Result WriteSymLink( + string relativePath, + string symLinkTarget) + { + return Interop.PrjFSLib.WriteSymLink(relativePath, symLinkTarget); + } + public virtual Result UpdatePlaceholderIfNeeded( string relativePath, byte[] providerId, @@ -137,6 +144,23 @@ public virtual Result UpdatePlaceholderIfNeeded( return result; } + public virtual Result ReplacePlaceholderFileWithSymLink( + string relativePath, + string symLinkTarget, + UpdateType updateFlags, + out UpdateFailureCause failureCause) + { + UpdateFailureCause updateFailureCause = UpdateFailureCause.NoFailure; + Result result = Interop.PrjFSLib.ReplacePlaceholderFileWithSymLink( + relativePath, + symLinkTarget, + updateFlags, + ref updateFailureCause); + + failureCause = updateFailureCause; + return result; + } + public virtual Result CompleteCommand( ulong commandId, Result result) diff --git a/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp b/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp index be777c7a3d..82e273fb46 100644 --- a/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp +++ b/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp @@ -373,6 +373,40 @@ PrjFS_Result PrjFS_WritePlaceholderFile( return PrjFS_Result_EIOError; } +PrjFS_Result PrjFS_WriteSymLink( + _In_ const char* relativePath, + _In_ const char* symLinkTarget) +{ +#ifdef DEBUG + cout + << "PrjFS_WriteSymLink(" + << relativePath << ", " + << symLinkTarget << ")" << endl; +#endif + + if (nullptr == relativePath || nullptr == symLinkTarget) + { + return PrjFS_Result_EInvalidArgs; + } + + char fullPath[PrjFSMaxPath]; + CombinePaths(s_virtualizationRootFullPath.c_str(), relativePath, fullPath); + + if(symlink(symLinkTarget, fullPath)) + { + goto CleanupAndFail; + } + + SetBitInFileFlags(fullPath, FileFlags_IsInVirtualizationRoot, true); + + return PrjFS_Result_Success; + +CleanupAndFail: + + return PrjFS_Result_EIOError; + +} + PrjFS_Result PrjFS_UpdatePlaceholderFileIfNeeded( _In_ const char* relativePath, _In_ unsigned char providerId[PrjFS_PlaceholderIdLength], @@ -405,6 +439,29 @@ PrjFS_Result PrjFS_UpdatePlaceholderFileIfNeeded( return PrjFS_WritePlaceholderFile(relativePath, providerId, contentId, fileSize, fileMode); } +PrjFS_Result PrjFS_ReplacePlaceholderFileWithSymLink( + _In_ const char* relativePath, + _In_ const char* symLinkTarget, + _In_ PrjFS_UpdateType updateFlags, + _Out_ PrjFS_UpdateFailureCause* failureCause) +{ +#ifdef DEBUG + cout + << "PrjFS_ReplacePlaceholderFileWithSymLink(" + << relativePath << ", " + << symLinkTarget << ", " + << hex << updateFlags << dec << ")" << endl; +#endif + + PrjFS_Result result = PrjFS_DeleteFile(relativePath, updateFlags, failureCause); + if (result != PrjFS_Result_Success) + { + return result; + } + + return PrjFS_WriteSymLink(relativePath, symLinkTarget); +} + PrjFS_Result PrjFS_DeleteFile( _In_ const char* relativePath, _In_ PrjFS_UpdateType updateFlags, @@ -817,7 +874,7 @@ static void CombinePaths(const char* root, const char* relative, char (&combined static bool SetBitInFileFlags(const char* path, uint32_t bit, bool value) { struct stat fileAttributes; - if (stat(path, &fileAttributes)) + if (lstat(path, &fileAttributes)) { return false; } @@ -832,7 +889,7 @@ static bool SetBitInFileFlags(const char* path, uint32_t bit, bool value) newValue = fileAttributes.st_flags & ~bit; } - if (chflags(path, newValue)) + if (lchflags(path, newValue)) { return false; } @@ -843,7 +900,7 @@ static bool SetBitInFileFlags(const char* path, uint32_t bit, bool value) static bool IsBitSetInFileFlags(const char* path, uint32_t bit) { struct stat fileAttributes; - if (stat(path, &fileAttributes)) + if (lstat(path, &fileAttributes)) { return false; } diff --git a/ProjFS.Mac/PrjFSLib/PrjFSLib.h b/ProjFS.Mac/PrjFSLib/PrjFSLib.h index 6e7a49a0b7..ca2cfb0579 100644 --- a/ProjFS.Mac/PrjFSLib/PrjFSLib.h +++ b/ProjFS.Mac/PrjFSLib/PrjFSLib.h @@ -85,6 +85,10 @@ extern "C" PrjFS_Result PrjFS_WritePlaceholderFile( _In_ unsigned long fileSize, _In_ uint16_t fileMode); +extern "C" PrjFS_Result PrjFS_WriteSymLink( + _In_ const char* relativePath, + _In_ const char* symLinkTarget); + typedef enum { PrjFS_UpdateType_Invalid = 0x00000000, @@ -111,6 +115,12 @@ extern "C" PrjFS_Result PrjFS_UpdatePlaceholderFileIfNeeded( _In_ PrjFS_UpdateType updateFlags, _Out_ PrjFS_UpdateFailureCause* failureCause); +extern "C" PrjFS_Result PrjFS_ReplacePlaceholderFileWithSymLink( + _In_ const char* relativePath, + _In_ const char* symLinkTarget, + _In_ PrjFS_UpdateType updateFlags, + _Out_ PrjFS_UpdateFailureCause* failureCause); + extern "C" PrjFS_Result PrjFS_DeleteFile( _In_ const char* relativePath, _In_ PrjFS_UpdateType updateFlags,