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 @@ -359,6 +359,10 @@ /// their addition. FileSystemList FSList; + /// The last CWD successfully set on at least one of the filesystems in + /// \c FSList. + std::string CWD; + public: OverlayFileSystem(IntrusiveRefCntPtr Base); @@ -369,8 +373,14 @@ llvm::ErrorOr> openFileForRead(const Twine &Path) override; directory_iterator dir_begin(const Twine &Dir, std::error_code &EC) override; + + /// Gets the last successfully set CWD, which starts as the CWD from \c Base. llvm::ErrorOr getCurrentWorkingDirectory() const override; + + /// Attempts to set the CWD on all contained FS. Only errors if they all + /// fail. std::error_code setCurrentWorkingDirectory(const Twine &Path) override; + std::error_code isLocal(const Twine &Path, bool &Result) override; std::error_code getRealPath(const Twine &Path, SmallVectorImpl &Output) const override; 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 @@ -73,15 +73,19 @@ Size(Size), Type(Type), Perms(Perms) {} Status Status::copyWithNewSize(const Status &In, uint64_t NewSize) { - return Status(In.getName(), In.getUniqueID(), In.getLastModificationTime(), - In.getUser(), In.getGroup(), NewSize, In.getType(), - In.getPermissions()); + Status S = Status(In.getName(), In.getUniqueID(), + In.getLastModificationTime(), In.getUser(), In.getGroup(), + NewSize, In.getType(), In.getPermissions()); + S.IsVFSMapped = In.IsVFSMapped; + return S; } Status Status::copyWithNewName(const Status &In, const Twine &NewName) { - return Status(NewName, In.getUniqueID(), In.getLastModificationTime(), - In.getUser(), In.getGroup(), In.getSize(), In.getType(), - In.getPermissions()); + Status S = Status(NewName, In.getUniqueID(), In.getLastModificationTime(), + In.getUser(), In.getGroup(), In.getSize(), In.getType(), + In.getPermissions()); + S.IsVFSMapped = In.IsVFSMapped; + return S; } Status Status::copyWithNewName(const file_status &In, const Twine &NewName) { @@ -238,7 +242,6 @@ } void RealFile::setPath(const Twine &Path) { - RealName = Path.str(); if (auto Status = status()) S = Status.get().copyWithNewName(Status.get(), Path); } @@ -417,64 +420,129 @@ //===-----------------------------------------------------------------------===/ OverlayFileSystem::OverlayFileSystem(IntrusiveRefCntPtr BaseFS) { + if (auto NewCWD = BaseFS->getCurrentWorkingDirectory()) + CWD = *NewCWD; FSList.push_back(std::move(BaseFS)); } void OverlayFileSystem::pushOverlay(IntrusiveRefCntPtr FS) { FSList.push_back(FS); - // Synchronize added file systems by duplicating the working directory from - // the first one in the list. - FS->setCurrentWorkingDirectory(getCurrentWorkingDirectory().get()); } ErrorOr OverlayFileSystem::status(const Twine &Path) { // FIXME: handle symlinks that cross file systems - for (iterator I = overlays_begin(), E = overlays_end(); I != E; ++I) { - ErrorOr Status = (*I)->status(Path); - if (Status || Status.getError() != llvm::errc::no_such_file_or_directory) + + SmallString<256> AbsPath; + Path.toVector(AbsPath); + if (std::error_code EC = makeAbsolute(AbsPath)) + return EC; + + for (auto &FS : overlays_range()) { + ErrorOr Status = FS->status(AbsPath); + if (!Status) { + if (Status.getError() != errc::no_such_file_or_directory) + return Status; + continue; + } + + // If the returned status had a different path to the one requested, just + // return it - the underlying FS mapped it to a different path, so we can't + // replace it. Otherwise, replace it with the originally requested path so + // that relative paths remain relative. + if (Status->getName() != AbsPath) return Status; + return Status::copyWithNewName(*Status, Path); } - return make_error_code(llvm::errc::no_such_file_or_directory); + return errc::no_such_file_or_directory; } ErrorOr> OverlayFileSystem::openFileForRead(const llvm::Twine &Path) { // FIXME: handle symlinks that cross file systems - for (iterator I = overlays_begin(), E = overlays_end(); I != E; ++I) { - auto Result = (*I)->openFileForRead(Path); - if (Result || Result.getError() != llvm::errc::no_such_file_or_directory) - return Result; + + SmallString<256> AbsPath; + Path.toVector(AbsPath); + if (std::error_code EC = makeAbsolute(AbsPath)) + return EC; + + for (auto &FS : overlays_range()) { + ErrorOr> Result = FS->openFileForRead(AbsPath); + if (!Result) { + if (Result.getError() != errc::no_such_file_or_directory) + return Result; + continue; + } + + // See \c OverlayFileSystem::status. Note that \c status() is being used + // rather than \c File::getName since the latter may be an already resolved + // real path (and hence would necessarily differ from the original). + if (ErrorOr Status = (*Result)->status()) { + if (Status->getName() != AbsPath) + return Result; + return File::getWithPath(std::move(Result), Path); + } } - return make_error_code(llvm::errc::no_such_file_or_directory); + return errc::no_such_file_or_directory; } -llvm::ErrorOr -OverlayFileSystem::getCurrentWorkingDirectory() const { - // All file systems are synchronized, just take the first working directory. - return FSList.front()->getCurrentWorkingDirectory(); +ErrorOr OverlayFileSystem::getCurrentWorkingDirectory() const { + if (CWD.empty()) + return errc::operation_not_permitted; + return CWD; } std::error_code OverlayFileSystem::setCurrentWorkingDirectory(const Twine &Path) { - for (auto &FS : FSList) - if (std::error_code EC = FS->setCurrentWorkingDirectory(Path)) - return EC; + SmallString<256> AbsPath; + Path.toVector(AbsPath); + if (std::error_code EC = makeAbsolute(AbsPath)) + return EC; + + bool Success = false; + for (auto &FS : overlays_range()) { + if (auto Status = FS->status(AbsPath)) { + if (Status->isDirectory()) { + Success = true; + CWD = AbsPath.str().str(); + break; + } + } + } + + // Possible that all the filesystems could have failed with an unrelated + // error, but this seems the most reasonable error to return. + if (!Success) + return errc::no_such_file_or_directory; return {}; } std::error_code OverlayFileSystem::isLocal(const Twine &Path, bool &Result) { - for (auto &FS : FSList) - if (FS->exists(Path)) - return FS->isLocal(Path, Result); + SmallString<256> AbsPath; + Path.toVector(AbsPath); + if (std::error_code EC = makeAbsolute(AbsPath)) + return EC; + + for (auto &FS : overlays_range()) { + std::error_code EC = FS->isLocal(AbsPath, Result); + if (!EC || EC != errc::no_such_file_or_directory) + return EC; + } return errc::no_such_file_or_directory; } std::error_code OverlayFileSystem::getRealPath(const Twine &Path, SmallVectorImpl &Output) const { - for (const auto &FS : FSList) - if (FS->exists(Path)) - return FS->getRealPath(Path, Output); + SmallString<256> AbsPath; + Path.toVector(AbsPath); + if (std::error_code EC = makeAbsolute(AbsPath)) + return EC; + + for (auto &FS : overlays_range()) { + std::error_code EC = FS->getRealPath(AbsPath, Output); + if (!EC || EC != errc::no_such_file_or_directory) + return EC; + } return errc::no_such_file_or_directory; } @@ -1056,8 +1124,10 @@ if (useNormalizedPaths()) llvm::sys::path::remove_dots(Path, /*remove_dot_dot=*/true); - if (!Path.empty()) - WorkingDirectory = std::string(Path.str()); + if (Path.empty()) + return errc::no_such_file_or_directory; + + WorkingDirectory = std::string(Path.str()); return {}; } 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 @@ -40,6 +40,7 @@ llvm_unreachable("unimplemented"); } std::error_code close() override { return std::error_code(); } + void setPath(const Twine &Path) override { S = S.copyWithNewName(S, Path); } }; class DummyFileSystem : public vfs::FileSystem { @@ -151,8 +152,11 @@ return FilesAndDirs.find(std::string(P.str())); } - void addRegularFile(StringRef Path, sys::fs::perms Perms = sys::fs::all_all) { - vfs::Status S(Path, UniqueID(FSID, FileID++), + void addRegularFile(StringRef Path, sys::fs::perms Perms = sys::fs::all_all, + StringRef StatusPath = StringRef()) { + if (StatusPath.empty()) + StatusPath = Path; + vfs::Status S(StatusPath, UniqueID(FSID, FileID++), std::chrono::system_clock::now(), 0, 0, 1024, sys::fs::file_type::regular_file, Perms); addEntry(Path, S); @@ -204,6 +208,39 @@ llvm::sys::path::native(S, Result, llvm::sys::path::Style::posix); return std::string(Result.str()); } + +#define EXPECT_PATHS(FS, Path, Expected, ExpectedRealPath) \ + checkAllFSPaths(FS, Path, Expected, ExpectedRealPath, __LINE__) + +/// Check that the path returned by \c openFileForRead(Path)->getName() and +/// \c status(Path)->getName() match \p Expected and \c getRealPath(Path) +/// matches \p ExpectedRealPath. +static void checkAllFSPaths(vfs::FileSystem &FS, StringRef Path, + StringRef Expected, StringRef ExpectedRealPath, + int Line) { + testing::ScopedTrace trace(__FILE__, Line, "EXPECT_PATHS"); + + auto F = FS.openFileForRead(Path); + ASSERT_FALSE(F.getError()); + + auto Name = (*F)->getName(); + ASSERT_FALSE(Name.getError()); + EXPECT_EQ(Expected, *Name); + auto Status = (*F)->status(); + ASSERT_FALSE(Status.getError()); + EXPECT_TRUE(Status->exists()); + EXPECT_EQ(Expected, Status->getName()); + + Status = FS.status(Path); + ASSERT_FALSE(Status.getError()); + EXPECT_EQ(Expected, Status->getName()); + + SmallString<256> RealPath; + auto EC = FS.getRealPath(Path, RealPath); + ASSERT_FALSE(EC); + EXPECT_EQ(ExpectedRealPath, RealPath); +} + } // end anonymous namespace TEST(VirtualFileSystemTest, StatusQueries) { @@ -897,6 +934,119 @@ Output); } +// Base has no CWD, Overlay shouldn't either +TEST(OverlayFileSystemTest, BaseNoWorkingDir) { + auto EmptyFS = makeIntrusiveRefCnt(); + auto OverlayFS = makeIntrusiveRefCnt(EmptyFS); + ASSERT_TRUE(OverlayFS->getCurrentWorkingDirectory().getError()); +} + +// Base has CWD, overlay should too +TEST(OverlayFileSystemTest, BaseWithWorkingDir) { + auto DummyFS1 = makeIntrusiveRefCnt(); + DummyFS1->setCurrentWorkingDirectory("/root"); + auto OverlayFS = makeIntrusiveRefCnt(DummyFS1); + auto CWD = OverlayFS->getCurrentWorkingDirectory(); + ASSERT_FALSE(CWD.getError()); + EXPECT_EQ("/root", *CWD); +} + +TEST(OverlayFileSystemTest, WorkingDir) { + auto EmptyFS = makeIntrusiveRefCnt(); + auto DummyFS = makeIntrusiveRefCnt(); + DummyFS->addDirectory("/root"); + DummyFS->addDirectory("/root2"); + DummyFS->addDirectory("/root2/b"); + DummyFS->setCurrentWorkingDirectory("/root"); + auto OverlayFS = makeIntrusiveRefCnt(EmptyFS); + + // OverlayFS should still have no CWD since it hasn't been set yet. The CWD + // of DummyFS should not be affected by the push. + OverlayFS->pushOverlay(DummyFS); + auto CWD = OverlayFS->getCurrentWorkingDirectory(); + EXPECT_TRUE(CWD.getError()); + CWD = DummyFS->getCurrentWorkingDirectory(); + ASSERT_FALSE(CWD.getError()); + EXPECT_EQ("/root", *CWD); + + // Should be able to setCWD on OverlayFS + auto EC = OverlayFS->setCurrentWorkingDirectory("/root2"); + EXPECT_FALSE(EC); + CWD = OverlayFS->getCurrentWorkingDirectory(); + ASSERT_FALSE(CWD.getError()); + EXPECT_EQ("/root2", *CWD); + + EC = OverlayFS->setCurrentWorkingDirectory("b"); + EXPECT_FALSE(EC); + CWD = OverlayFS->getCurrentWorkingDirectory(); + ASSERT_FALSE(CWD.getError()); + EXPECT_EQ("/root2/b", *CWD); + + // Can't set if the underlying path doesn't exist in any FS + EC = OverlayFS->setCurrentWorkingDirectory("/notfound"); + EXPECT_TRUE(EC); + CWD = OverlayFS->getCurrentWorkingDirectory(); + ASSERT_FALSE(CWD.getError()); + EXPECT_EQ("/root2/b", *CWD); + + // Shouldn't touch the underlying CWD + CWD = DummyFS->getCurrentWorkingDirectory(); + ASSERT_FALSE(CWD.getError()); + EXPECT_EQ("/root", *CWD); +} + +// Check CWD works across contained FS +TEST(OverlayFileSystemTest, BasicRelativePath) { + auto FS1 = makeIntrusiveRefCnt(); + FS1->addDirectory("/dir"); + auto FS2 = makeIntrusiveRefCnt(); + FS2->addRegularFile("/dir/../f"); + FS2->addRegularFile("/dir/../f2", sys::fs::all_all, "/another/path/f2"); + auto OverlayFS = makeIntrusiveRefCnt(FS2); + OverlayFS->pushOverlay(FS1); + + auto EC = OverlayFS->setCurrentWorkingDirectory("/dir"); + EXPECT_FALSE(EC); + auto CWD = OverlayFS->getCurrentWorkingDirectory(); + ASSERT_FALSE(CWD.getError()); + EXPECT_EQ("/dir", *CWD); + + EXPECT_PATHS(*OverlayFS, "../f", "../f", "/dir/../f"); + EXPECT_PATHS(*OverlayFS, "../f2", "/another/path/f2", "/dir/../f2"); +} + +// Check that we don't inadvertently get the wrong file when a higher FS doesn't +// contain the set CWD +TEST(OverlayFileSystemTest, DifferentRootRelativePath) { + auto FS1 = makeIntrusiveRefCnt(); + FS1->addFile("/a/b", 0, MemoryBuffer::getMemBuffer("/a/b")); + FS1->setCurrentWorkingDirectory("/"); + auto FS2 = makeIntrusiveRefCnt(); + FS2->addFile("/c/a/b", 0, MemoryBuffer::getMemBuffer("/c/a/b")); + auto OverlayFS = makeIntrusiveRefCnt(FS2); + OverlayFS->pushOverlay(FS1); + + auto EC = OverlayFS->setCurrentWorkingDirectory("/c"); + EXPECT_FALSE(EC); + auto CWD = OverlayFS->getCurrentWorkingDirectory(); + ASSERT_FALSE(CWD.getError()); + EXPECT_EQ("/c", *CWD); + + // FS1 is on top of FS2, but since our CWD is /c we should be looking for + // /c/a/b *not* /a/b and thus the file should be found from FS2 + auto F = OverlayFS->openFileForRead("a/b"); + ASSERT_FALSE(F.getError()); + auto Buffer = (*F)->getBuffer("ignored"); + ASSERT_FALSE(Buffer.getError()); + EXPECT_EQ("/c/a/b", (*Buffer)->getBuffer()); + auto Name = (*F)->getName(); + ASSERT_FALSE(Name.getError()); + EXPECT_EQ("a/b", *Name); + auto Status = (*F)->status(); + ASSERT_FALSE(Status.getError()); + EXPECT_EQ("a/b", Status->getName()); +} + TEST(ProxyFileSystemTest, Basic) { IntrusiveRefCntPtr Base( new vfs::InMemoryFileSystem()); @@ -1056,6 +1206,9 @@ } TEST_F(InMemoryFileSystemTest, WorkingDirectory) { + auto EC = FS.setCurrentWorkingDirectory(""); + ASSERT_EQ(EC, std::errc::no_such_file_or_directory); + FS.setCurrentWorkingDirectory("/b"); FS.addFile("c", 0, MemoryBuffer::getMemBuffer(""));