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 @@ -331,6 +332,12 @@ std::unique_ptr Root; std::string WorkingDirectory; bool UseNormalizedPaths = true; + bool addFile(const Twine &Path, time_t ModificationTime, + std::unique_ptr Buffer, + Optional User, Optional Group, + Optional Type, + Optional Perms, + detail::InMemoryNode *HardLink); public: explicit InMemoryFileSystem(bool UseNormalizedPaths = true); @@ -348,6 +355,17 @@ Optional Type = None, Optional Perms = None); + public: + /// Add a HardLink 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. + 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,35 +467,22 @@ 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(std::move(FileName)) {} 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()); + return llvm::sys::path::filename(FileName.data()); } InMemoryNodeKind getKind() const { return Kind; } virtual std::string toString(unsigned Indent) const = 0; @@ -503,16 +491,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)) {} + /// 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() { 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,6 +516,24 @@ } }; +class InMemoryHardLink : public InMemoryNode { + InMemoryFile &ResolvedFile; + +public: + InMemoryHardLink(StringRef Path, InMemoryFile &ResolvedFile) + : InMemoryNode(Path.str(), IME_HardLink), ResolvedFile(ResolvedFile) {} + InMemoryFile *getResolvedFile() { 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. @@ -550,12 +564,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 +597,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 +628,8 @@ Optional User, Optional Group, Optional Type, - Optional Perms) { + Optional Perms, + detail::InMemoryNode *HardLink) { SmallString<128> Path; P.toVector(Path); @@ -627,6 +650,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 (HardLink && !isa(HardLink)) + 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; @@ -645,8 +671,12 @@ 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 (HardLink) + Child.reset(new detail::InMemoryHardLink( + P.str(), *static_cast(HardLink))); + else + Child.reset( + new detail::InMemoryFile(std::move(Stat), std::move(Buffer))); } Dir->addChild(Name, std::move(Child)); return true; @@ -666,20 +696,37 @@ 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, + llvm::MemoryBuffer::getMemBuffer( + Buffer->getBuffer(), Buffer->getBufferIdentifier()), + User, Group, Type, Perms, /*HardLink=*/nullptr); +} + bool InMemoryFileSystem::addFileNoOwn(const Twine &P, time_t ModificationTime, llvm::MemoryBuffer *Buffer, Optional User, @@ -724,6 +771,12 @@ return errc::no_such_file_or_directory; } + // If Node is HardLink then return the resolved link. + 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 +784,32 @@ } } +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) { + if (auto Dir = dyn_cast(*Node)) + return Dir->getStatus(Path.str()); + if (auto File = dyn_cast(*Node)) + return File->getStatus(Path.str()); + if (auto Link = dyn_cast(*Node)) + return Link->getResolvedFile()->getStatus(Path.str()); + } return Node.getError(); } @@ -766,7 +841,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); + if (auto File = dyn_cast(I->second.get())) + CurrentEntry = File->getStatus(Path); + 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. Index: unittests/Basic/VirtualFileSystemTest.cpp =================================================================== --- unittests/Basic/VirtualFileSystemTest.cpp +++ unittests/Basic/VirtualFileSystemTest.cpp @@ -17,6 +17,7 @@ #include "llvm/Support/SourceMgr.h" #include "gtest/gtest.h" #include +#include using namespace clang; using namespace llvm; @@ -695,6 +696,22 @@ InMemoryFileSystemTest() : FS(/*UseNormalizedPaths=*/false), NormalizedFS(/*UseNormalizedPaths=*/true) {} + +public: + void ExpectHardLink(Twine From, Twine To, const string &ExpectedBuffer) { + auto OpenedFrom = FS.openFileForRead(From); + ASSERT_FALSE(OpenedFrom.getError()); + auto OpenedTo = FS.openFileForRead(To); + ASSERT_FALSE(OpenedTo.getError()); + ASSERT_EQ((*OpenedFrom)->status()->getSize(), + (*OpenedTo)->status()->getSize()); + ASSERT_EQ((*OpenedFrom)->status()->getUniqueID(), + (*OpenedTo)->status()->getUniqueID()); + ASSERT_EQ((*OpenedFrom)->getBuffer(From)->get()->getBuffer().data(), + (*OpenedTo)->getBuffer(To)->get()->getBuffer().data()); + ASSERT_EQ((*OpenedFrom)->getBuffer(From)->get()->getBuffer().data(), + ExpectedBuffer); + } }; TEST_F(InMemoryFileSystemTest, IsEmpty) { @@ -958,6 +975,126 @@ ASSERT_EQ("../b/c", getPosixPath(It->getName())); } +TEST_F(InMemoryFileSystemTest, AddHardLinkToFile) { + std::pair From = {"/path/to/FROM/file", + "content of FROM file"}; + std::pair To = {"/path/to/TO/file", + "content of TO file"}; + FS.addFile(To.first, 0, MemoryBuffer::getMemBuffer(To.second)); + EXPECT_TRUE(FS.addHardLink(From.first, To.first)); + ExpectHardLink(From.first, To.first, To.second.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)); + ExpectHardLink(link0, target, content.data()); + ExpectHardLink(link1, target, content.data()); + ExpectHardLink(link2, 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 {