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 InMemoryFile; } // 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::InMemoryFile *HardLinkTarget); + public: explicit InMemoryFileSystem(bool UseNormalizedPaths = true); ~InMemoryFileSystem() override; @@ -348,6 +358,21 @@ Optional Type = None, Optional Perms = None); + /// Add a hard link to a file. + /// Here hard links are not intended to be fully equivalent to the classical + /// filesystem. Both the hard link and the file share the same buffer and + /// status (and thus have the same UniqueID). Because of this there is no way + /// 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 From path or 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. + /// \return true if the above condition is satisfied and hardlink was + /// successfully created, false otherwise. + bool addHardLink(const Twine &From, const Twine &To); + /// 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 @@ -12,10 +12,12 @@ //===----------------------------------------------------------------------===// #include "clang/Basic/VirtualFileSystem.h" +#include "llvm/ADT/StringRef.h" #include "clang/Basic/LLVM.h" #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,57 +468,68 @@ 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; } + std::string FileName; public: - InMemoryNode(Status Stat, InMemoryNodeKind Kind) - : Stat(std::move(Stat)), Kind(Kind) {} + InMemoryNode(llvm::StringRef FileName, InMemoryNodeKind Kind) + : Kind(Kind), FileName(llvm::sys::path::filename(FileName)) {} virtual ~InMemoryNode() = default; + /// Get the filename of this node (the name without the directory part). + StringRef getFileName() const { return FileName.data(); } + InMemoryNodeKind getKind() const { return Kind; } + virtual std::string toString(unsigned Indent) const = 0; +}; + +class InMemoryFile : public InMemoryNode { + Status Stat; + std::unique_ptr Buffer; + +public: + InMemoryFile(Status Stat, std::unique_ptr Buffer) + : InMemoryNode(Stat.getName(), 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() const { return Buffer.get(); } - /// Get the filename of this node (the name without the directory part). - StringRef getFileName() const { - return llvm::sys::path::filename(Stat.getName()); + std::string toString(unsigned Indent) const override { + return (std::string(Indent, ' ') + Stat.getName() + "\n").str(); + } + + static bool classof(const InMemoryNode *N) { + return N->getKind() == IME_File; } - InMemoryNodeKind getKind() const { return Kind; } - virtual std::string toString(unsigned Indent) const = 0; }; namespace { -class InMemoryFile : public InMemoryNode { - std::unique_ptr Buffer; +class InMemoryHardLink : public InMemoryNode { + const InMemoryFile &ResolvedFile; public: - InMemoryFile(Status Stat, std::unique_ptr Buffer) - : InMemoryNode(std::move(Stat), IME_File), Buffer(std::move(Buffer)) {} - - llvm::MemoryBuffer *getBuffer() { return Buffer.get(); } + InMemoryHardLink(StringRef Path, const InMemoryFile &ResolvedFile) + : InMemoryNode(Path, IME_HardLink), ResolvedFile(ResolvedFile) {} + const InMemoryFile &getResolvedFile() const { return ResolvedFile; } std::string toString(unsigned Indent) const override { - return (std::string(Indent, ' ') + getStatus().getName() + "\n").str(); + return std::string(Indent, ' ') + "HardLink to -> " + + ResolvedFile.toString(0); } static bool classof(const InMemoryNode *N) { - return N->getKind() == IME_File; + return N->getKind() == IME_HardLink; } }; @@ -524,12 +537,13 @@ /// \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 { @@ -546,16 +560,22 @@ std::error_code close() override { return {}; } }; - } // namespace class InMemoryDirectory : public InMemoryNode { + Status Stat; std::map> Entries; public: InMemoryDirectory(Status Stat) - : InMemoryNode(std::move(Stat), IME_Directory) {} + : InMemoryNode(Stat.getName(), 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 +595,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; @@ -586,6 +606,17 @@ } }; +namespace { +Status getNodeStatus(const InMemoryNode *Node, StringRef RequestedName) { + if (auto Dir = dyn_cast(Node)) + return Dir->getStatus(RequestedName); + else if (auto File = dyn_cast(Node)) + return File->getStatus(RequestedName); + else if (auto Link = dyn_cast(Node)) + return Link->getResolvedFile().getStatus(RequestedName); + llvm_unreachable("Unknown node type"); +} +} // namespace } // namespace detail InMemoryFileSystem::InMemoryFileSystem(bool UseNormalizedPaths) @@ -606,7 +637,8 @@ Optional User, Optional Group, Optional Type, - Optional Perms) { + Optional Perms, + const detail::InMemoryFile *HardLinkTarget) { SmallString<128> Path; P.toVector(Path); @@ -627,6 +659,8 @@ 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); + // HardLink cannot have a buffer of its own. + assert(!(HardLinkTarget && Buffer)); // 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; @@ -636,17 +670,22 @@ ++I; if (!Node) { if (I == E) { - // 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); + // End of the path 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(), *HardLinkTarget)); + else { + // Create a new file or directory. + Status Stat(P.str(), getNextVirtualUniqueID(), + llvm::sys::toTimePoint(ModificationTime), ResolvedUser, + ResolvedGroup, Buffer->getBufferSize(), ResolvedType, + ResolvedPerms); + 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))); + } } Dir->addChild(Name, std::move(Child)); return true; @@ -656,8 +695,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 +705,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, /*HardLinkTarget=*/nullptr); +} + bool InMemoryFileSystem::addFileNoOwn(const Twine &P, time_t ModificationTime, llvm::MemoryBuffer *Buffer, Optional User, @@ -693,7 +747,7 @@ std::move(Perms)); } -static ErrorOr +static ErrorOr lookupInMemoryNode(const InMemoryFileSystem &FS, detail::InMemoryDirectory *Dir, const Twine &P) { SmallString<128> Path; @@ -724,6 +778,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 +791,22 @@ } } +bool InMemoryFileSystem::addHardLink(const Twine &FromPath, + const Twine &ToPath) { + 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, None, None, None, None, + cast(*ToNode)); +} + llvm::ErrorOr InMemoryFileSystem::status(const Twine &Path) { auto Node = lookupInMemoryNode(*this, Root.get(), Path); if (Node) - return (*Node)->getStatus(Path.str()); + return detail::getNodeStatus(*Node, Path.str()); return Node.getError(); } @@ -766,7 +838,7 @@ if (I != E) { SmallString<256> Path(RequestedDirName); llvm::sys::path::append(Path, I->second->getFileName()); - CurrentEntry = I->second->getStatus(Path); + CurrentEntry = detail::getNodeStatus(I->second.get(), Path); } else { // When we're at the end, make CurrentEntry invalid and DirIterImpl will // do the rest. @@ -777,7 +849,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,16 @@ NormalizedFS(/*UseNormalizedPaths=*/true) {} }; +MATCHER_P2(IsHardLinkTo, FS, Target, "") { + StringRef From = arg; + StringRef To = Target; + auto OpenedFrom = FS->openFileForRead(From); + auto OpenedTo = FS->openFileForRead(To); + return !(OpenedFrom.getError() || OpenedTo.getError() || + (*OpenedFrom)->status()->getUniqueID() != + (*OpenedTo)->status()->getUniqueID()); +} + TEST_F(InMemoryFileSystemTest, IsEmpty) { auto Stat = FS.status("/a"); ASSERT_EQ(Stat.getError(),errc::no_such_file_or_directory) << FS.toString(); @@ -958,6 +970,108 @@ ASSERT_EQ("../b/c", getPosixPath(It->getName())); } +TEST_F(InMemoryFileSystemTest, AddHardLinkToFile) { + StringRef FromLink = "/path/to/FROM/link"; + StringRef Target = "/path/to/TO/file"; + FS.addFile(Target, 0, MemoryBuffer::getMemBuffer("content of target")); + EXPECT_TRUE(FS.addHardLink(FromLink, Target)); + EXPECT_THAT(FromLink, IsHardLinkTo(&FS, Target)); + EXPECT_TRUE(FS.status(FromLink)->getSize() == FS.status(Target)->getSize()); + EXPECT_TRUE(FS.getBufferForFile(FromLink)->get()->getBuffer() == + FS.getBufferForFile(Target)->get()->getBuffer()); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkInChainPattern) { + StringRef Link0 = "/path/to/0/link"; + StringRef Link1 = "/path/to/1/link"; + StringRef Link2 = "/path/to/2/link"; + StringRef Target = "/path/to/target"; + FS.addFile(Target, 0, MemoryBuffer::getMemBuffer("content of target file")); + EXPECT_TRUE(FS.addHardLink(Link2, Target)); + EXPECT_TRUE(FS.addHardLink(Link1, Link2)); + EXPECT_TRUE(FS.addHardLink(Link0, Link1)); + EXPECT_THAT(Link0, IsHardLinkTo(&FS, Target)); + EXPECT_THAT(Link1, IsHardLinkTo(&FS, Target)); + EXPECT_THAT(Link2, IsHardLinkTo(&FS, Target)); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkToAFileThatWasNotAddedBefore) { + EXPECT_FALSE(FS.addHardLink("/path/to/link", "/path/to/target")); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkFromAFileThatWasAddedBefore) { + StringRef Link = "/path/to/link"; + StringRef Target = "/path/to/target"; + FS.addFile(Target, 0, MemoryBuffer::getMemBuffer("content of target")); + FS.addFile(Link, 0, MemoryBuffer::getMemBuffer("content of link")); + EXPECT_FALSE(FS.addHardLink(Link, Target)); +} + +TEST_F(InMemoryFileSystemTest, AddSameHardLinkMoreThanOnce) { + StringRef Link = "/path/to/link"; + StringRef Target = "/path/to/target"; + FS.addFile(Target, 0, MemoryBuffer::getMemBuffer("content of target")); + EXPECT_TRUE(FS.addHardLink(Link, Target)); + EXPECT_FALSE(FS.addHardLink(Link, Target)); +} + +TEST_F(InMemoryFileSystemTest, AddFileInPlaceOfAHardLinkWithSameContent) { + StringRef Link = "/path/to/link"; + StringRef 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) { + StringRef Link = "/path/to/link"; + StringRef Target = "/path/to/target"; + StringRef Content = "content of target"; + StringRef LinkContent = "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(LinkContent))); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkToADirectory) { + StringRef Dir = "path/to/dummy/dir"; + StringRef Link = "/path/to/link"; + StringRef File = "path/to/dummy/dir/target"; + StringRef Content = "content of target"; + EXPECT_TRUE(FS.addFile(File, 0, MemoryBuffer::getMemBuffer(Content))); + EXPECT_FALSE(FS.addHardLink(Link, Dir)); +} + +TEST_F(InMemoryFileSystemTest, AddHardLinkFromADirectory) { + StringRef Dir = "path/to/dummy/dir"; + StringRef Target = "path/to/dummy/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 CommonContent = "content string"; + FS.addFile("/a/b", 0, MemoryBuffer::getMemBuffer(CommonContent)); + FS.addFile("/c/d", 0, MemoryBuffer::getMemBuffer(CommonContent)); + EXPECT_FALSE(FS.addHardLink("/c/d/e", "/a/b")); +} + +TEST_F(InMemoryFileSystemTest, RecursiveIterationWithHardLink) { + std::error_code EC; + FS.addFile("/a/b", 0, MemoryBuffer::getMemBuffer("content string")); + 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(getPosixPath(I->getName())); + } + EXPECT_THAT(Nodes, testing::UnorderedElementsAre("/a", "/a/b", "/c", "/c/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 {