diff --git a/llvm/include/llvm/Support/VirtualFileSystem.h b/llvm/include/llvm/Support/VirtualFileSystem.h --- a/llvm/include/llvm/Support/VirtualFileSystem.h +++ b/llvm/include/llvm/Support/VirtualFileSystem.h @@ -483,6 +483,18 @@ /// successfully created, false otherwise. bool addHardLink(const Twine &From, const Twine &To); + /// Arbitrary max depth to search through symlinks. We can get into problems + /// if a link links to a link that links back to the link, for example. + static constexpr size_t MaxSymlinkDepth = 16; + + /// Add a symbolic link. Unlike a HardLink, because To doesn't need to refer + /// to a file (or refer to anything, as it happens). Also, an in-memory + /// directory for \param To isn't automatically created. + bool addSymbolicLink(const Twine &From, const Twine &To, + time_t ModificationTime, Optional User = None, + Optional Group = None, + Optional Perms = None); + /// Add a buffer to the VFS with a path. The VFS does not own the buffer. /// If present, User, Group, Type and Perms apply to the newly-created file /// or directory. diff --git a/llvm/lib/Support/VirtualFileSystem.cpp b/llvm/lib/Support/VirtualFileSystem.cpp --- a/llvm/lib/Support/VirtualFileSystem.cpp +++ b/llvm/lib/Support/VirtualFileSystem.cpp @@ -560,10 +560,15 @@ namespace detail { -enum InMemoryNodeKind { IME_File, IME_Directory, IME_HardLink }; +enum InMemoryNodeKind { + IME_File, + IME_Directory, + IME_HardLink, + IME_SymbolicLink +}; /// The in memory file system is a tree of Nodes. Every node can either be a -/// file , hardlink or a directory. +/// file, symlink, hardlink or a directory. class InMemoryNode { InMemoryNodeKind Kind; std::string FileName; @@ -632,6 +637,30 @@ } }; +class InMemorySymbolicLink : public InMemoryNode { + std::string TargetPath; + Status Stat; + +public: + InMemorySymbolicLink(StringRef Path, StringRef TargetPath, Status Stat) + : InMemoryNode(Path, IME_SymbolicLink), + TargetPath(std::move(TargetPath)), Stat(Stat) {} + + std::string toString(unsigned Indent) const override { + return std::string(Indent, ' ') + "SymbolicLink to -> " + TargetPath; + } + + Status getStatus(const Twine &RequestedName) const override { + return Status::copyWithNewName(Stat, RequestedName); + } + + StringRef getTargetPath() const { return TargetPath; } + + static bool classof(const InMemoryNode *N) { + return N->getKind() == IME_SymbolicLink; + } +}; + /// Adapt a InMemoryFile for VFS' File interface. The goal is to make /// \p InMemoryFileAdaptor mimic as much as possible the behavior of /// \p RealFile. @@ -680,7 +709,7 @@ UniqueID getUniqueID() const { return Stat.getUniqueID(); } - InMemoryNode *getChild(StringRef Name) { + InMemoryNode *getChild(StringRef Name) const { auto I = Entries.find(Name); if (I != Entries.end()) return I->second.get(); @@ -868,8 +897,9 @@ } static ErrorOr -lookupInMemoryNode(const InMemoryFileSystem &FS, detail::InMemoryDirectory *Dir, - const Twine &P) { +lookupInMemoryNode(const InMemoryFileSystem &FS, + detail::InMemoryDirectory *RootDir, const Twine &P, + bool FollowSymlinks, size_t SymlinkDepth = 0) { SmallString<128> Path; P.toVector(Path); @@ -881,6 +911,7 @@ if (FS.useNormalizedPaths()) llvm::sys::path::remove_dots(Path, /*remove_dot_dot=*/true); + const detail::InMemoryDirectory *Dir = RootDir; if (Path.empty()) return Dir; @@ -891,6 +922,36 @@ if (!Node) return errc::no_such_file_or_directory; + if (auto Symlink = dyn_cast(Node)) { + // If we're at the end of the path, and we're not following through + // terminal symlinks, then we're done. + if (I == E && !FollowSymlinks) + return Symlink; + + if (SymlinkDepth > InMemoryFileSystem::MaxSymlinkDepth) + return errc::no_such_file_or_directory; + + SmallString<128> TargetPath = Symlink->getTargetPath(); + if (std::error_code EC = FS.makeAbsolute(TargetPath)) + return EC; + + // Keep going with the target. We always want to follow symlinks here + // because we're either at the end of a path that we want to follow, or + // not at the end of a path, in which case we need to follow the symlink + // regardless. + ErrorOr Target = lookupInMemoryNode( + FS, RootDir, TargetPath, /*FollowSymlinks=*/true, SymlinkDepth + 1); + if (!Target || I == E) + return Target; + + if (!isa(*Target)) + return errc::no_such_file_or_directory; + + // Otherwise, continue on the search in the symlinked directory. + Dir = cast(*Target); + continue; + } + // Return the file if it's at the end of the path. if (auto File = dyn_cast(Node)) { if (I == E) @@ -913,8 +974,10 @@ bool InMemoryFileSystem::addHardLink(const Twine &FromPath, const Twine &ToPath) { - auto FromNode = lookupInMemoryNode(*this, Root.get(), FromPath); - auto ToNode = lookupInMemoryNode(*this, Root.get(), ToPath); + auto FromNode = + lookupInMemoryNode(*this, Root.get(), FromPath, /*FollowSymlinks=*/false); + auto ToNode = + lookupInMemoryNode(*this, Root.get(), ToPath, /*FollowSymlinks=*/false); // FromPath must not have been added before. ToPath must have been added // before. Resolved ToPath must be a File. if (!ToNode || FromNode || !isa(*ToNode)) @@ -926,8 +989,32 @@ }); } +bool InMemoryFileSystem::addSymbolicLink(const Twine &FromPath, + const Twine &ToPath, + time_t ModificationTime, + Optional User, + Optional Group, + Optional Perms) { + auto FromNode = + lookupInMemoryNode(*this, Root.get(), FromPath, /*FollowSymlinks=*/false); + if (FromNode) + return false; + + SmallString<128> FromStr, ToStr; + FromPath.toVector(FromStr); + ToPath.toVector(ToStr); + + return addFile(FromStr, ModificationTime, nullptr, User, Group, + sys::fs::file_type::symlink_file, Perms, + [&](detail::NewInMemoryNodeInfo NNI) { + return std::make_unique( + FromStr, ToStr, NNI.makeStatus()); + }); +} + llvm::ErrorOr InMemoryFileSystem::status(const Twine &Path) { - auto Node = lookupInMemoryNode(*this, Root.get(), Path); + auto Node = + lookupInMemoryNode(*this, Root.get(), Path, /*FollowSymlinks=*/true); if (Node) return (*Node)->getStatus(Path); return Node.getError(); @@ -935,7 +1022,8 @@ llvm::ErrorOr> InMemoryFileSystem::openFileForRead(const Twine &Path) { - auto Node = lookupInMemoryNode(*this, Root.get(), Path); + auto Node = lookupInMemoryNode(*this, Root.get(), Path, + /*FollowSymlinks=*/true); if (!Node) return Node.getError(); @@ -965,6 +1053,7 @@ switch (I->second->getKind()) { case detail::IME_File: case detail::IME_HardLink: + case detail::IME_SymbolicLink: Type = sys::fs::file_type::regular_file; break; case detail::IME_Directory: @@ -1000,7 +1089,8 @@ directory_iterator InMemoryFileSystem::dir_begin(const Twine &Dir, std::error_code &EC) { - auto Node = lookupInMemoryNode(*this, Root.get(), Dir); + auto Node = + lookupInMemoryNode(*this, Root.get(), Dir, /*FollowSymlinks=*/true); if (!Node) { EC = Node.getError(); return directory_iterator(std::make_shared()); diff --git a/llvm/unittests/Support/VirtualFileSystemTest.cpp b/llvm/unittests/Support/VirtualFileSystemTest.cpp --- a/llvm/unittests/Support/VirtualFileSystemTest.cpp +++ b/llvm/unittests/Support/VirtualFileSystemTest.cpp @@ -1299,6 +1299,52 @@ EXPECT_EQ(FS.status("/a")->getUniqueID(), FS2.status("/a")->getUniqueID()); } +TEST_F(InMemoryFileSystemTest, SymlinkToFile) { + EXPECT_TRUE( + FS.addFile("/some/file", 0, MemoryBuffer::getMemBuffer("contents"))); + EXPECT_TRUE(FS.addSymbolicLink("/other/file/link", "/some/file", 0)); + ErrorOr Stat = FS.status("/some/file"); + EXPECT_TRUE(Stat->isRegularFile()); +} + +TEST_F(InMemoryFileSystemTest, SymlinkToDirectory) { + EXPECT_TRUE(FS.addSymbolicLink("/link", "/target", 0)); + EXPECT_TRUE( + FS.addFile("/target/foo.h", 0, MemoryBuffer::getMemBuffer("foo"))); + ErrorOr Stat = FS.status("/link/foo.h"); + EXPECT_TRUE(Stat); + EXPECT_EQ((*Stat).getName(), "/link/foo.h"); + EXPECT_TRUE(Stat->isRegularFile()); +} + +TEST_F(InMemoryFileSystemTest, SymlinkToSymlink) { + EXPECT_TRUE(FS.addSymbolicLink("/first", "/second", 0)); + EXPECT_TRUE(FS.addSymbolicLink("/second", "/third", 0)); + EXPECT_TRUE(FS.addFile("/third", 0, MemoryBuffer::getMemBuffer(""))); + ErrorOr Stat = FS.status("/first"); + EXPECT_TRUE(Stat); + EXPECT_EQ((*Stat).getName(), "/first"); + // Follow-through symlinks by default. This matches RealFileSystem's + // semantics. + EXPECT_TRUE(Stat->isRegularFile()); + Stat = FS.status("/second"); + EXPECT_TRUE(Stat); + EXPECT_EQ((*Stat).getName(), "/second"); + EXPECT_TRUE(Stat->isRegularFile()); + Stat = FS.status("/third"); + EXPECT_TRUE(Stat); + EXPECT_EQ((*Stat).getName(), "/third"); + EXPECT_TRUE(Stat->isRegularFile()); +} + +TEST_F(InMemoryFileSystemTest, RecursiveSymlink) { + EXPECT_TRUE(FS.addSymbolicLink("/link-a", "/link-b", 0)); + EXPECT_TRUE(FS.addSymbolicLink("/link-b", "/link-a", 0)); + ErrorOr Stat = FS.status("/link-a/foo"); + EXPECT_FALSE(Stat); + EXPECT_EQ(Stat.getError(), errc::no_such_file_or_directory); +} + // NOTE: in the tests below, we use '//root/' as our root directory, since it is // a legal *absolute* path on Windows as well as *nix. class VFSFromYAMLTest : public ::testing::Test {