Index: include/clang/Basic/VirtualFileSystem.h =================================================================== --- include/clang/Basic/VirtualFileSystem.h +++ include/clang/Basic/VirtualFileSystem.h @@ -323,6 +323,7 @@ namespace detail { class InMemoryDirectory; +class InMemoryNode; } // namespace detail @@ -332,6 +333,15 @@ std::string WorkingDirectory; bool UseNormalizedPaths = true; + /// If HardLinkTarget is non-null, a hardlink is created to the To path which + /// must be a file. If it is null then it adds the file as the public addFile. + bool addFile(const Twine &Path, time_t ModificationTime, + std::unique_ptr Buffer, + Optional User, Optional Group, + Optional Type, + Optional Perms, + const detail::InMemoryNode *HardLinkTarget); + public: explicit InMemoryFileSystem(bool UseNormalizedPaths = true); ~InMemoryFileSystem() override; @@ -348,6 +358,20 @@ Optional Type = None, Optional Perms = None); + public: + /// Add a hard link to a file. + /// The To path must be an existing file or a hardlink. The From file must not + /// have been added before. The From Node is added as an InMemoryHardLink + /// which points to the resolved file of To Node. The From path or the To Path + /// must not be a directory. + /// \return true if the above condition is satisfied and hardlink was + /// successfully created, false otherwise. + bool addHardLink(const Twine &From, const Twine &To, + Optional User = None, + Optional Group = None, + Optional Type = 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. Index: lib/Basic/VirtualFileSystem.cpp =================================================================== --- lib/Basic/VirtualFileSystem.cpp +++ lib/Basic/VirtualFileSystem.cpp @@ -16,6 +16,7 @@ #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/IntrusiveRefCntPtr.h" +#include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallString.h" @@ -466,36 +467,21 @@ namespace detail { -enum InMemoryNodeKind { IME_File, IME_Directory }; +enum InMemoryNodeKind { IME_File, IME_Directory, IME_HardLink }; /// The in memory file system is a tree of Nodes. Every node can either be a -/// file or a directory. +/// file , hardlink or a directory. class InMemoryNode { - Status Stat; InMemoryNodeKind Kind; - -protected: - /// Return Stat. This should only be used for internal/debugging use. When - /// clients wants the Status of this node, they should use - /// \p getStatus(StringRef). - const Status &getStatus() const { return Stat; } + string FileName; public: - InMemoryNode(Status Stat, InMemoryNodeKind Kind) - : Stat(std::move(Stat)), Kind(Kind) {} + InMemoryNode(string FileName, InMemoryNodeKind Kind) + : Kind(Kind), FileName(llvm::sys::path::filename(FileName.data())) {} virtual ~InMemoryNode() = default; - /// Return the \p Status for this node. \p RequestedName should be the name - /// through which the caller referred to this node. It will override - /// \p Status::Name in the return value, to mimic the behavior of \p RealFile. - Status getStatus(StringRef RequestedName) const { - return Status::copyWithNewName(Stat, RequestedName); - } - /// Get the filename of this node (the name without the directory part). - StringRef getFileName() const { - return llvm::sys::path::filename(Stat.getName()); - } + StringRef getFileName() const { return FileName.data(); } InMemoryNodeKind getKind() const { return Kind; } virtual std::string toString(unsigned Indent) const = 0; }; @@ -503,16 +489,24 @@ namespace { class InMemoryFile : public InMemoryNode { + Status Stat; std::unique_ptr Buffer; public: InMemoryFile(Status Stat, std::unique_ptr Buffer) - : InMemoryNode(std::move(Stat), IME_File), Buffer(std::move(Buffer)) {} + : InMemoryNode(Stat.getName().str(), IME_File), Stat(std::move(Stat)), + Buffer(std::move(Buffer)) {} - llvm::MemoryBuffer *getBuffer() { return Buffer.get(); } + /// Return the \p Status for this node. \p RequestedName should be the name + /// through which the caller referred to this node. It will override + /// \p Status::Name in the return value, to mimic the behavior of \p RealFile. + Status getStatus(StringRef RequestedName) const { + return Status::copyWithNewName(Stat, RequestedName); + } + llvm::MemoryBuffer *getBuffer() const { return Buffer.get(); } std::string toString(unsigned Indent) const override { - return (std::string(Indent, ' ') + getStatus().getName() + "\n").str(); + return (std::string(Indent, ' ') + Stat.getName() + "\n").str(); } static bool classof(const InMemoryNode *N) { @@ -520,16 +514,35 @@ } }; +class InMemoryHardLink : public InMemoryNode { + const InMemoryFile &ResolvedFile; + +public: + InMemoryHardLink(StringRef Path, const InMemoryFile &ResolvedFile) + : InMemoryNode(Path.str(), IME_HardLink), ResolvedFile(ResolvedFile) {} + const InMemoryFile *getResolvedFile() const { return &ResolvedFile; } + + std::string toString(unsigned Indent) const override { + return std::string(Indent, ' ') + "HardLink to -> " + + ResolvedFile.toString(0); + } + + static bool classof(const InMemoryNode *N) { + return N->getKind() == IME_HardLink; + } +}; + /// Adapt a InMemoryFile for VFS' File interface. The goal is to make /// \p InMemoryFileAdaptor mimic as much as possible the behavior of /// \p RealFile. class InMemoryFileAdaptor : public File { - InMemoryFile &Node; + const InMemoryFile &Node; /// The name to use when returning a Status for this file. std::string RequestedName; public: - explicit InMemoryFileAdaptor(InMemoryFile &Node, std::string RequestedName) + explicit InMemoryFileAdaptor(const InMemoryFile &Node, + std::string RequestedName) : Node(Node), RequestedName(std::move(RequestedName)) {} llvm::ErrorOr status() override { @@ -550,12 +563,20 @@ } // namespace class InMemoryDirectory : public InMemoryNode { + Status Stat; std::map> Entries; public: InMemoryDirectory(Status Stat) - : InMemoryNode(std::move(Stat), IME_Directory) {} + : InMemoryNode(Stat.getName().str(), IME_Directory), + Stat(std::move(Stat)) {} + /// Return the \p Status for this node. \p RequestedName should be the name + /// through which the caller referred to this node. It will override + /// \p Status::Name in the return value, to mimic the behavior of \p RealFile. + Status getStatus(StringRef RequestedName) const { + return Status::copyWithNewName(Stat, RequestedName); + } InMemoryNode *getChild(StringRef Name) { auto I = Entries.find(Name); if (I != Entries.end()) @@ -575,7 +596,7 @@ std::string toString(unsigned Indent) const override { std::string Result = - (std::string(Indent, ' ') + getStatus().getName() + "\n").str(); + (std::string(Indent, ' ') + Stat.getName() + "\n").str(); for (const auto &Entry : Entries) Result += Entry.second->toString(Indent + 2); return Result; @@ -606,7 +627,8 @@ Optional User, Optional Group, Optional Type, - Optional Perms) { + Optional Perms, + const detail::InMemoryNode *HardLinkTarget) { SmallString<128> Path; P.toVector(Path); @@ -627,6 +649,9 @@ const auto ResolvedGroup = Group.getValueOr(0); const auto ResolvedType = Type.getValueOr(sys::fs::file_type::regular_file); const auto ResolvedPerms = Perms.getValueOr(sys::fs::all_all); + // Cannot create HardLink from a directory. + if (HardLinkTarget && (!isa(HardLinkTarget) || Buffer)) + return false; // Any intermediate directories we create should be accessible by // the owner, even if Perms says otherwise for the final path. const auto NewDirectoryPerms = ResolvedPerms | sys::fs::owner_all; @@ -639,14 +664,20 @@ // End of the path, create a new file or directory. Status Stat(P.str(), getNextVirtualUniqueID(), llvm::sys::toTimePoint(ModificationTime), ResolvedUser, - ResolvedGroup, Buffer->getBufferSize(), ResolvedType, - ResolvedPerms); + ResolvedGroup, Buffer ? Buffer->getBufferSize() : 0, + ResolvedType, ResolvedPerms); std::unique_ptr Child; if (ResolvedType == sys::fs::file_type::directory_file) { Child.reset(new detail::InMemoryDirectory(std::move(Stat))); } else { - Child.reset(new detail::InMemoryFile(std::move(Stat), - std::move(Buffer))); + if (HardLinkTarget) + Child.reset(new detail::InMemoryHardLink( + P.str(), + *static_cast(HardLinkTarget))); + + else + Child.reset( + new detail::InMemoryFile(std::move(Stat), std::move(Buffer))); } Dir->addChild(Name, std::move(Child)); return true; @@ -656,8 +687,8 @@ Status Stat( StringRef(Path.str().begin(), Name.end() - Path.str().begin()), getNextVirtualUniqueID(), llvm::sys::toTimePoint(ModificationTime), - ResolvedUser, ResolvedGroup, Buffer->getBufferSize(), - sys::fs::file_type::directory_file, NewDirectoryPerms); + ResolvedUser, ResolvedGroup, 0, sys::fs::file_type::directory_file, + NewDirectoryPerms); Dir = cast(Dir->addChild( Name, llvm::make_unique(std::move(Stat)))); continue; @@ -666,20 +697,35 @@ if (auto *NewDir = dyn_cast(Node)) { Dir = NewDir; } else { - assert(isa(Node) && - "Must be either file or directory!"); + assert((isa(Node) || + isa(Node)) && + "Must be either file, hardlink or directory!"); // Trying to insert a directory in place of a file. if (I != E) return false; // Return false only if the new file is different from the existing one. + if (auto Link = dyn_cast(Node)) { + return Link->getResolvedFile()->getBuffer()->getBuffer() == + Buffer->getBuffer(); + } return cast(Node)->getBuffer()->getBuffer() == Buffer->getBuffer(); } } } +bool InMemoryFileSystem::addFile(const Twine &P, time_t ModificationTime, + std::unique_ptr Buffer, + Optional User, + Optional Group, + Optional Type, + Optional Perms) { + return addFile(P, ModificationTime, std::move(Buffer), User, Group, Type, + Perms, /*HardLink=*/nullptr); +} + bool InMemoryFileSystem::addFileNoOwn(const Twine &P, time_t ModificationTime, llvm::MemoryBuffer *Buffer, Optional User, @@ -693,7 +739,7 @@ std::move(Perms)); } -static ErrorOr +static ErrorOr lookupInMemoryNode(const InMemoryFileSystem &FS, detail::InMemoryDirectory *Dir, const Twine &P) { SmallString<128> Path; @@ -724,6 +770,12 @@ 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 errc::no_such_file_or_directory; + } // Traverse directories. Dir = cast(Node); if (I == E) @@ -731,10 +783,30 @@ } } +bool InMemoryFileSystem::addHardLink(const Twine &FromPath, const Twine &ToPath, + Optional User, + Optional Group, + Optional Type, + Optional Perms) { + auto FromNode = lookupInMemoryNode(*this, Root.get(), FromPath); + auto ToNode = lookupInMemoryNode(*this, Root.get(), ToPath); + // FromPath must not have been added before. ToPath must have been added + // before. Resolved ToPath must be a File. + if (!ToNode || FromNode || !isa(*ToNode)) + return false; + return this->addFile(FromPath, 0, nullptr, User, Group, Type, Perms, + static_cast(*ToNode)); +} + llvm::ErrorOr InMemoryFileSystem::status(const Twine &Path) { auto Node = lookupInMemoryNode(*this, Root.get(), Path); - if (Node) - return (*Node)->getStatus(Path.str()); + if (Node) { + assert(!isa(*Node)); + if (auto Dir = dyn_cast(*Node)) + return Dir->getStatus(Path.str()); + if (auto File = dyn_cast(*Node)) + return File->getStatus(Path.str()); + } return Node.getError(); } @@ -766,7 +838,12 @@ if (I != E) { SmallString<256> Path(RequestedDirName); llvm::sys::path::append(Path, I->second->getFileName()); - CurrentEntry = I->second->getStatus(Path); + if (auto Dir = dyn_cast(I->second.get())) + CurrentEntry = Dir->getStatus(Path); + else if (auto File = dyn_cast(I->second.get())) + CurrentEntry = File->getStatus(Path); + else if (auto Link = dyn_cast(I->second.get())) + CurrentEntry = Link->getResolvedFile()->getStatus(Path); } else { // When we're at the end, make CurrentEntry invalid and DirIterImpl will // do the rest. @@ -777,7 +854,7 @@ public: InMemoryDirIterator() = default; - explicit InMemoryDirIterator(detail::InMemoryDirectory &Dir, + explicit InMemoryDirIterator(const detail::InMemoryDirectory &Dir, std::string RequestedDirName) : I(Dir.begin()), E(Dir.end()), RequestedDirName(std::move(RequestedDirName)) { Index: unittests/Basic/VirtualFileSystemTest.cpp =================================================================== --- unittests/Basic/VirtualFileSystemTest.cpp +++ unittests/Basic/VirtualFileSystemTest.cpp @@ -16,7 +16,9 @@ #include "llvm/Support/Path.h" #include "llvm/Support/SourceMgr.h" #include "gtest/gtest.h" +#include "gmock/gmock.h" #include +#include using namespace clang; using namespace llvm; @@ -697,6 +699,29 @@ NormalizedFS(/*UseNormalizedPaths=*/true) {} }; +MATCHER_P3(IsHardLinktTo, FS, Target, ExpectedBuffer, "") { + Twine From = arg; + Twine To = Target; + auto OpenedFrom = FS->openFileForRead(From); + if (OpenedFrom.getError()) + return false; + auto OpenedTo = FS->openFileForRead(To); + if (OpenedTo.getError()) + return false; + if ((*OpenedFrom)->status()->getSize() != (*OpenedTo)->status()->getSize()) + return false; + if ((*OpenedFrom)->status()->getUniqueID() != + (*OpenedTo)->status()->getUniqueID()) + return false; + if ((*OpenedFrom)->getBuffer(From)->get()->getBuffer().data() != + ExpectedBuffer) + return false; + if ((*OpenedFrom)->getBuffer(From)->get()->getBuffer().data() != + ExpectedBuffer) + return false; + return true; +} + TEST_F(InMemoryFileSystemTest, IsEmpty) { auto Stat = FS.status("/a"); ASSERT_EQ(Stat.getError(),errc::no_such_file_or_directory) << FS.toString(); @@ -958,6 +983,126 @@ ASSERT_EQ("../b/c", getPosixPath(It->getName())); } +TEST_F(InMemoryFileSystemTest, AddHardLinkToFile) { + Twine FromLink = "/path/to/FROM/link"; + Twine Target = "/path/to/TO/file"; + StringRef content = "content of target"; + FS.addFile(Target, 0, MemoryBuffer::getMemBuffer(content)); + EXPECT_TRUE(FS.addHardLink(FromLink, Target)); + EXPECT_THAT(FromLink, IsHardLinktTo(&FS, Target, content.data())); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkInChainPattern) { + StringRef content = "content of target file"; + Twine link0 = "/path/to/0/link"; + Twine link1 = "/path/to/1/link"; + Twine link2 = "/path/to/2/link"; + Twine target = "/path/to/target"; + FS.addFile(target, 0, MemoryBuffer::getMemBuffer(content)); + EXPECT_TRUE(FS.addHardLink(link2, target)); + EXPECT_TRUE(FS.addHardLink(link1, link2)); + EXPECT_TRUE(FS.addHardLink(link0, link1)); + EXPECT_THAT(link0, IsHardLinktTo(&FS, target, content.data())); + EXPECT_THAT(link1, IsHardLinktTo(&FS, target, content.data())); + EXPECT_THAT(link2, IsHardLinktTo(&FS, target, content.data())); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkToAFileThatWasNotAddedBefore) { + Twine link = "/path/to/link"; + Twine target = "/path/to/target"; + EXPECT_FALSE(FS.addHardLink(link, target)); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkFromAFileThatWasAddedBefore) { + Twine link = "/path/to/link"; + StringRef content_link = "content of link"; + Twine target = "/path/to/target"; + StringRef content = "content of target"; + FS.addFile(target, 0, MemoryBuffer::getMemBuffer(content)); + FS.addFile(link, 0, MemoryBuffer::getMemBuffer(content_link)); + EXPECT_FALSE(FS.addHardLink(link, target)); +} + +TEST_F(InMemoryFileSystemTest, AddSameHardLinkMoreThanOnce) { + Twine link = "/path/to/link"; + Twine target = "/path/to/target"; + StringRef content = "content of target"; + FS.addFile(target, 0, MemoryBuffer::getMemBuffer(content)); + EXPECT_TRUE(FS.addHardLink(link, target)); + EXPECT_FALSE(FS.addHardLink(link, target)); +} + +TEST_F(InMemoryFileSystemTest, AddFileInPlaceOfAHardLinkWithSameContent) { + Twine link = "/path/to/link"; + Twine target = "/path/to/target"; + StringRef content = "content of target"; + EXPECT_TRUE(FS.addFile(target, 0, MemoryBuffer::getMemBuffer(content))); + EXPECT_TRUE(FS.addHardLink(link, target)); + EXPECT_TRUE(FS.addFile(link, 0, MemoryBuffer::getMemBuffer(content))); +} + +TEST_F(InMemoryFileSystemTest, AddFileInPlaceOfAHardLinkWithDifferentContent) { + Twine link = "/path/to/link"; + Twine target = "/path/to/target"; + StringRef content = "content of target"; + StringRef link_content = "different content of link"; + EXPECT_TRUE(FS.addFile(target, 0, MemoryBuffer::getMemBuffer(content))); + EXPECT_TRUE(FS.addHardLink(link, target)); + EXPECT_FALSE(FS.addFile(link, 0, MemoryBuffer::getMemBuffer(link_content))); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkToADirectory) { + Twine dir = "path/to/dummy/dir"; + Twine link = "/path/to/link"; + Twine dummy_file = dir + "/target"; + StringRef content = "content of target"; + EXPECT_TRUE(FS.addFile(dummy_file, 0, MemoryBuffer::getMemBuffer(content))); + EXPECT_FALSE(FS.addHardLink(link, dir)); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkFromADirectory) { + Twine dir = "path/to/dummy/dir"; + Twine target = dir + "/target"; + StringRef content = "content of target"; + EXPECT_TRUE(FS.addFile(target, 0, MemoryBuffer::getMemBuffer(content))); + EXPECT_FALSE(FS.addHardLink(dir, target)); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkUnderAFile) { + StringRef common_content = "content string"; + FS.addFile("/a/b", 0, MemoryBuffer::getMemBuffer(common_content)); + FS.addFile("/c/d", 0, MemoryBuffer::getMemBuffer(common_content)); + EXPECT_FALSE(FS.addHardLink("/c/d/e", "/a/b")); +} + +TEST_F(InMemoryFileSystemTest, RecursiveIterationWithHardLink) { + std::error_code EC; + StringRef common_content = "content string"; + FS.addFile("/a/b", 0, MemoryBuffer::getMemBuffer(common_content)); + EXPECT_TRUE(FS.addHardLink("/c/d", "/a/b")); + auto I = vfs::recursive_directory_iterator(FS, "/", EC); + ASSERT_FALSE(EC); + std::vector Nodes; + for (auto E = vfs::recursive_directory_iterator(); !EC && I != E; + I.increment(EC)) { + Nodes.push_back(I->getName()); + } + + // Check contents, which may be in any order + EXPECT_EQ(4U, Nodes.size()); + int Counts[4] = {0, 0, 0, 0}; + for (const std::string &Name : Nodes) { + ASSERT_FALSE(Name.empty()); + int Index = Name[Name.size() - 1] - 'a'; + ASSERT_TRUE(Index >= 0 && Index < 4); + Counts[Index]++; + } + EXPECT_EQ(1, Counts[0]); // a + EXPECT_EQ(1, Counts[1]); // b + EXPECT_EQ(1, Counts[2]); // c + EXPECT_EQ(1, Counts[3]); // d +} + // 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 {