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 @@ -22,6 +22,7 @@ #include "llvm/ADT/STLFunctionalExtras.h" #include "llvm/Support/Chrono.h" #include "llvm/Support/ErrorOr.h" +#include "llvm/Support/Errc.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/Path.h" #include "llvm/Support/SourceMgr.h" @@ -478,6 +479,24 @@ Status makeStatus() const; }; +class NamedNodeOrError { + ErrorOr, const detail::InMemoryNode *>> + Value; + +public: + NamedNodeOrError(llvm::SmallString<128> Name, + const detail::InMemoryNode *Node) + : Value(std::make_pair(Name, Node)) {} + NamedNodeOrError(std::error_code EC) : Value(EC) {} + NamedNodeOrError(llvm::errc EC) : Value(EC) {} + + StringRef getName() const { return (*Value).first; } + operator bool() const { return static_cast(Value); } + operator std::error_code() const { return Value.getError(); } + std::error_code getError() const { return Value.getError(); } + const detail::InMemoryNode *operator*() const { return (*Value).second; } +}; + } // namespace detail /// An in-memory file system. @@ -496,6 +515,12 @@ Optional Type, Optional Perms, MakeNodeFn MakeNode); + /// Looks up the in-memory node for the path \param P. + /// If \param FollowFinalSymlink is true, the returned node is guaranteed to + /// not be a symlink and its path may differ from \param P. + detail::NamedNodeOrError lookupNode(const Twine &P, bool FollowFinalSymlink, + size_t SymlinkDepth = 0) const; + public: explicit InMemoryFileSystem(bool UseNormalizedPaths = true); ~InMemoryFileSystem() override; @@ -519,12 +544,25 @@ /// to distinguish between the link and the file after the link has been /// added. /// - /// The To path must be an existing file or a hardlink. The From file must not - /// have been added before. The To Path must not be a directory. The From Node - /// is added as a hard link which points to the resolved file of To Node. + /// The \param Target path must be an existing file or a hardlink. The + /// \param NewLink file must not have been added before. The \param Target + /// path must not be a directory. The \param NewLink node is added as a hard + /// link which points to the resolved file of \param Target node. /// \return true if the above condition is satisfied and hardlink was /// successfully created, false otherwise. - bool addHardLink(const Twine &From, const Twine &To); + bool addHardLink(const Twine &NewLink, const Twine &Target); + + /// 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 \param Target doesn't need + /// to refer to a file (or refer to anything, as it happens). Also, an + /// in-memory directory for \param Target isn't automatically created. + bool addSymbolicLink(const Twine &NewLink, const Twine &Target, + 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 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 @@ -590,10 +590,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; @@ -662,6 +667,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. @@ -710,7 +739,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(); @@ -897,22 +926,26 @@ }); } -static ErrorOr -lookupInMemoryNode(const InMemoryFileSystem &FS, detail::InMemoryDirectory *Dir, - const Twine &P) { +/// Looks up the in-memory node for the path \param P. +/// If \param FollowFinalSymlink is true, the returned node is guaranteed to not +/// be a symlink. +detail::NamedNodeOrError +InMemoryFileSystem::lookupNode(const Twine &P, bool FollowFinalSymlink, + size_t SymlinkDepth) const { SmallString<128> Path; P.toVector(Path); // Fix up relative paths. This just prepends the current working directory. - std::error_code EC = FS.makeAbsolute(Path); + std::error_code EC = makeAbsolute(Path); assert(!EC); (void)EC; - if (FS.useNormalizedPaths()) + if (useNormalizedPaths()) llvm::sys::path::remove_dots(Path, /*remove_dot_dot=*/true); + const detail::InMemoryDirectory *Dir = Root.get(); if (Path.empty()) - return Dir; + return detail::NamedNodeOrError(Path, Dir); auto I = llvm::sys::path::begin(Path), E = llvm::sys::path::end(Path); while (true) { @@ -921,43 +954,99 @@ 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 && !FollowFinalSymlink) + return detail::NamedNodeOrError(Path, Symlink); + + if (SymlinkDepth > InMemoryFileSystem::MaxSymlinkDepth) + return errc::no_such_file_or_directory; + + SmallString<128> TargetPath = Symlink->getTargetPath(); + if (std::error_code EC = 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. + auto Target = + lookupNode(TargetPath, /*FollowFinalSymlink=*/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) - return File; + return detail::NamedNodeOrError(Path, File); return errc::no_such_file_or_directory; } // If Node is HardLink then return the resolved file. if (auto File = dyn_cast(Node)) { if (I == E) - return &File->getResolvedFile(); + return detail::NamedNodeOrError(Path, &File->getResolvedFile()); return errc::no_such_file_or_directory; } // Traverse directories. Dir = cast(Node); if (I == E) - return Dir; + return detail::NamedNodeOrError(Path, Dir); } } -bool InMemoryFileSystem::addHardLink(const Twine &FromPath, - const Twine &ToPath) { - auto FromNode = lookupInMemoryNode(*this, Root.get(), FromPath); - auto ToNode = lookupInMemoryNode(*this, Root.get(), ToPath); +bool InMemoryFileSystem::addHardLink(const Twine &NewLink, + const Twine &Target) { + auto NewLinkNode = lookupNode(NewLink, /*FollowFinalSymlink=*/false); + // Whether symlinks in the hardlink target are followed is + // implementation-defined in POSIX. + // We're following symlinks here to be consistent with macOS. + auto TargetNode = lookupNode(Target, /*FollowFinalSymlink=*/true); // FromPath must not have been added before. ToPath must have been added // before. Resolved ToPath must be a File. - if (!ToNode || FromNode || !isa(*ToNode)) + if (!TargetNode || NewLinkNode || !isa(*TargetNode)) return false; - return addFile(FromPath, 0, nullptr, None, None, None, None, + return addFile(NewLink, 0, nullptr, None, None, None, None, [&](detail::NewInMemoryNodeInfo NNI) { return std::make_unique( - NNI.Path.str(), *cast(*ToNode)); + NNI.Path.str(), + *cast(*TargetNode)); + }); +} + +bool InMemoryFileSystem::addSymbolicLink(const Twine &NewLink, + const Twine &Target, + time_t ModificationTime, + Optional User, + Optional Group, + Optional Perms) { + auto NewLinkNode = lookupNode(NewLink, /*FollowFinalSymlink=*/false); + if (NewLinkNode) + return false; + + SmallString<128> NewLinkStr, TargetStr; + NewLink.toVector(NewLinkStr); + Target.toVector(TargetStr); + + return addFile(NewLinkStr, ModificationTime, nullptr, User, Group, + sys::fs::file_type::symlink_file, Perms, + [&](detail::NewInMemoryNodeInfo NNI) { + return std::make_unique( + NewLinkStr, TargetStr, NNI.makeStatus()); }); } llvm::ErrorOr InMemoryFileSystem::status(const Twine &Path) { - auto Node = lookupInMemoryNode(*this, Root.get(), Path); + auto Node = lookupNode(Path, /*FollowFinalSymlink=*/true); if (Node) return (*Node)->getStatus(Path); return Node.getError(); @@ -965,7 +1054,7 @@ llvm::ErrorOr> InMemoryFileSystem::openFileForRead(const Twine &Path) { - auto Node = lookupInMemoryNode(*this, Root.get(), Path); + auto Node = lookupNode(Path,/*FollowFinalSymlink=*/true); if (!Node) return Node.getError(); @@ -981,8 +1070,11 @@ namespace { +using LookupNodeFn = std::function; + /// Adaptor from InMemoryDir::iterator to directory_iterator. class InMemoryDirIterator : public llvm::vfs::detail::DirIterImpl { + LookupNodeFn LookupNode; detail::InMemoryDirectory::const_iterator I; detail::InMemoryDirectory::const_iterator E; std::string RequestedDirName; @@ -1000,6 +1092,12 @@ case detail::IME_Directory: Type = sys::fs::file_type::directory_file; break; + case detail::IME_SymbolicLink: + if (auto SymlinkTarget = LookupNode(Path)) { + Path = SymlinkTarget.getName(); + Type = (*SymlinkTarget)->getStatus(Path).getType(); + } + break; } CurrentEntry = directory_entry(std::string(Path.str()), Type); } else { @@ -1012,9 +1110,10 @@ public: InMemoryDirIterator() = default; - explicit InMemoryDirIterator(const detail::InMemoryDirectory &Dir, + explicit InMemoryDirIterator(LookupNodeFn &&LookupNode, + const detail::InMemoryDirectory &Dir, std::string RequestedDirName) - : I(Dir.begin()), E(Dir.end()), + : LookupNode(std::move(LookupNode)), I(Dir.begin()), E(Dir.end()), RequestedDirName(std::move(RequestedDirName)) { setCurrentEntry(); } @@ -1030,15 +1129,18 @@ directory_iterator InMemoryFileSystem::dir_begin(const Twine &Dir, std::error_code &EC) { - auto Node = lookupInMemoryNode(*this, Root.get(), Dir); + auto Node = lookupNode(Dir, /*FollowFinalSymlink=*/true); if (!Node) { EC = Node.getError(); return directory_iterator(std::make_shared()); } if (auto *DirNode = dyn_cast(*Node)) - return directory_iterator( - std::make_shared(*DirNode, Dir.str())); + return directory_iterator(std::make_shared( + [&](const Twine &P) { + return lookupNode(P, /*FollowFinalSymlink=*/true); + }, + *DirNode, Dir.str())); EC = make_error_code(llvm::errc::not_a_directory); 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 @@ -1301,6 +1301,13 @@ EXPECT_FALSE(FS.addHardLink(Link, Dir)); } +TEST_F(InMemoryFileSystemTest, AddHardLinkToASymlink) { + EXPECT_TRUE(FS.addFile("/file", 0, MemoryBuffer::getMemBuffer("content"))); + EXPECT_TRUE(FS.addSymbolicLink("/symlink", "/file", 0)); + EXPECT_TRUE(FS.addHardLink("/hardlink", "/symlink")); + EXPECT_EQ((*FS.getBufferForFile("/hardlink"))->getBuffer(), "content"); +} + TEST_F(InMemoryFileSystemTest, AddHardLinkFromADirectory) { StringRef Dir = "path/to/dummy/dir"; StringRef Target = "path/to/dummy/dir/target"; @@ -1351,6 +1358,85 @@ EXPECT_EQ(FS.status("/a")->getUniqueID(), FS2.status("/a")->getUniqueID()); } +TEST_F(InMemoryFileSystemTest, AddSymlinkToAFile) { + 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, AddSymlinkToADirectory) { + 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, AddSymlinkToASymlink) { + 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, AddRecursiveSymlink) { + 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); +} + +TEST_F(InMemoryFileSystemTest, DirectoryIteratorWithSymlinkToAFile) { + std::error_code EC; + + EXPECT_TRUE(FS.addFile("/file", 0, MemoryBuffer::getMemBuffer(""))); + EXPECT_TRUE(FS.addSymbolicLink("/symlink", "/file", 0)); + + vfs::directory_iterator I = FS.dir_begin("/", EC), E; + ASSERT_FALSE(EC); + + std::vector Nodes; + for (; !EC && I != E; I.increment(EC)) + Nodes.push_back(getPosixPath(std::string(I->path()))); + + EXPECT_THAT(Nodes, testing::UnorderedElementsAre("/file", "/file")); +} + +TEST_F(InMemoryFileSystemTest, RecursiveDirectoryIteratorWithSymlinkToADir) { + std::error_code EC; + + EXPECT_TRUE(FS.addFile("/dir/file", 0, MemoryBuffer::getMemBuffer(""))); + EXPECT_TRUE(FS.addSymbolicLink("/dir_symlink", "/dir", 0)); + + vfs::recursive_directory_iterator I(FS, "/", EC), E; + ASSERT_FALSE(EC); + + std::vector Nodes; + for (; !EC && I != E; I.increment(EC)) + Nodes.push_back(getPosixPath(std::string(I->path()))); + + EXPECT_THAT(Nodes, testing::UnorderedElementsAre("/dir", "/dir/file", "/dir", + "/dir/file")); +} + // 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 {