diff --git a/clang/include/clang/Basic/VirtualFileSystem.h b/clang/include/clang/Basic/VirtualFileSystem.h index 694c6ddec89d..a5f7f302f930 100644 --- a/clang/include/clang/Basic/VirtualFileSystem.h +++ b/clang/include/clang/Basic/VirtualFileSystem.h @@ -17,6 +17,7 @@ #include "llvm/ADT/IntrusiveRefCntPtr.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/ErrorOr.h" +#include "llvm/Support/SourceMgr.h" namespace llvm { class MemoryBuffer; @@ -157,6 +158,17 @@ public: OwningPtr &Result) LLVM_OVERRIDE; }; +/// \brief Get a globally unique ID for a virtual file or directory. +llvm::sys::fs::UniqueID getNextVirtualUniqueID(); + +/// \brief Gets a \p FileSystem for a virtual file system described in YAML +/// format. +/// +/// Takes ownership of \p Buffer. +IntrusiveRefCntPtr +getVFSFromYAML(llvm::MemoryBuffer *Buffer, llvm::SourceMgr::DiagHandlerTy, + IntrusiveRefCntPtr ExternalFS = getRealFileSystem()); + } // end namespace vfs } // end namespace clang #endif // LLVM_CLANG_BASIC_VIRTUAL_FILE_SYSTEM_H diff --git a/clang/lib/Basic/VirtualFileSystem.cpp b/clang/lib/Basic/VirtualFileSystem.cpp index 3fedf27f970f..665fcc7af47a 100644 --- a/clang/lib/Basic/VirtualFileSystem.cpp +++ b/clang/lib/Basic/VirtualFileSystem.cpp @@ -10,10 +10,14 @@ //===----------------------------------------------------------------------===// #include "clang/Basic/VirtualFileSystem.h" +#include "llvm/ADT/DenseMap.h" #include "llvm/ADT/OwningPtr.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/Support/Atomic.h" #include "llvm/Support/MemoryBuffer.h" -#include "llvm/Support/SourceMgr.h" #include "llvm/Support/Path.h" +#include "llvm/Support/YAMLParser.h" using namespace clang; using namespace clang::vfs; @@ -83,6 +87,7 @@ class RealFile : public File { RealFile(int FD) : FD(FD) { assert(FD >= 0 && "Invalid or inactive file descriptor"); } + public: ~RealFile(); ErrorOr status() LLVM_OVERRIDE; @@ -91,9 +96,7 @@ public: bool RequiresNullTerminator = true) LLVM_OVERRIDE; error_code close() LLVM_OVERRIDE; }; -RealFile::~RealFile() { - close(); -} +RealFile::~RealFile() { close(); } ErrorOr RealFile::status() { assert(FD != -1 && "cannot stat closed file"); @@ -191,3 +194,572 @@ error_code OverlayFileSystem::openFileForRead(const llvm::Twine &Path, } return error_code(errc::no_such_file_or_directory, system_category()); } + +//===-----------------------------------------------------------------------===/ +// VFSFromYAML implementation +//===-----------------------------------------------------------------------===/ + +// Allow DenseMap. This is useful below because we know all the +// strings are literals and will outlive the map, and there is no reason to +// store them. +namespace llvm { + template<> + struct DenseMapInfo { + // This assumes that "" will never be a valid key. + static inline StringRef getEmptyKey() { return StringRef(""); } + static inline StringRef getTombstoneKey() { return StringRef(); } + static unsigned getHashValue(StringRef Val) { return HashString(Val); } + static bool isEqual(StringRef LHS, StringRef RHS) { return LHS == RHS; } + }; +} + +namespace { + +enum EntryKind { + EK_Directory, + EK_File +}; + +/// \brief A single file or directory in the VFS. +class Entry { + EntryKind Kind; + std::string Name; + +public: + virtual ~Entry(); +#if LLVM_HAS_RVALUE_REFERENCES + Entry(EntryKind K, std::string Name) : Kind(K), Name(std::move(Name)) {} +#endif + Entry(EntryKind K, StringRef Name) : Kind(K), Name(Name) {} + StringRef getName() const { return Name; } + EntryKind getKind() const { return Kind; } +}; + +class DirectoryEntry : public Entry { + std::vector Contents; + Status S; + +public: + virtual ~DirectoryEntry(); +#if LLVM_HAS_RVALUE_REFERENCES + DirectoryEntry(std::string Name, std::vector Contents, Status S) + : Entry(EK_Directory, std::move(Name)), Contents(std::move(Contents)), + S(std::move(S)) {} +#endif + DirectoryEntry(StringRef Name, ArrayRef Contents, const Status &S) + : Entry(EK_Directory, Name), Contents(Contents), S(S) {} + Status getStatus() { return S; } + typedef std::vector::iterator iterator; + iterator contents_begin() { return Contents.begin(); } + iterator contents_end() { return Contents.end(); } + static bool classof(const Entry *E) { return E->getKind() == EK_Directory; } +}; + +class FileEntry : public Entry { + std::string ExternalContentsPath; + +public: +#if LLVM_HAS_RVALUE_REFERENCES + FileEntry(std::string Name, std::string ExternalContentsPath) + : Entry(EK_File, std::move(Name)), + ExternalContentsPath(std::move(ExternalContentsPath)) {} +#endif + FileEntry(StringRef Name, StringRef ExternalContentsPath) + : Entry(EK_File, Name), ExternalContentsPath(ExternalContentsPath) {} + StringRef getExternalContentsPath() const { return ExternalContentsPath; } + static bool classof(const Entry *E) { return E->getKind() == EK_File; } +}; + +/// \brief A virtual file system parsed from a YAML file. +/// +/// Currently, this class allows creating virtual directories and mapping +/// virtual file paths to existing external files, available in \c ExternalFS. +/// +/// The basic structure of the parsed file is: +/// \verbatim +/// { +/// 'version': , +/// +/// 'roots': [ +/// +/// ] +/// } +/// \endverbatim +/// +/// All configuration options are optional. +/// 'case-sensitive': +/// +/// Virtual directories are represented as +/// \verbatim +/// { +/// 'type': 'directory', +/// 'name': , +/// 'contents': [ ] +/// } +/// \endverbatim +/// +/// The default attributes for virtual directories are: +/// \verbatim +/// MTime = now() when created +/// Perms = 0777 +/// User = Group = 0 +/// Size = 0 +/// UniqueID = unspecified unique value +/// \endverbatim +/// +/// Re-mapped files are represented as +/// \verbatim +/// { +/// 'type': 'file', +/// 'name': , +/// 'external-contents': ) +/// } +/// \endverbatim +/// +/// and inherit their attributes from the external contents. +/// +/// In both cases, the 'name' field must be a single path component (containing +/// no separators). +class VFSFromYAML : public vfs::FileSystem { + std::vector Roots; ///< The root(s) of the virtual file system. + /// \brief The file system to use for external references. + IntrusiveRefCntPtr ExternalFS; + + /// @name Configuration + /// @{ + + /// \brief Whether to perform case-sensitive comparisons. + /// + /// Currently, case-insensitive matching only works correctly with ASCII. + bool CaseSensitive; ///< Whether to perform case-sensitive comparisons. + /// @} + + friend class VFSFromYAMLParser; + +private: + VFSFromYAML(IntrusiveRefCntPtr ExternalFS) + : ExternalFS(ExternalFS), CaseSensitive(true) {} + + /// \brief Looks up \p Path in \c Roots. + ErrorOr lookupPath(const Twine &Path); + + /// \brief Looks up the path [Start, End) in \p From, possibly + /// recursing into the contents of \p From if it is a directory. + ErrorOr lookupPath(sys::path::const_iterator Start, + sys::path::const_iterator End, Entry *From); + +public: + ~VFSFromYAML(); + + /// \brief Parses \p Buffer, which is expected to be in YAML format and + /// returns a virtual file system representing its contents. + /// + /// Takes ownership of \p Buffer. + static VFSFromYAML *create(MemoryBuffer *Buffer, + SourceMgr::DiagHandlerTy DiagHandler, + IntrusiveRefCntPtr ExternalFS); + + ErrorOr status(const Twine &Path) LLVM_OVERRIDE; + error_code openFileForRead(const Twine &Path, + OwningPtr &Result) LLVM_OVERRIDE; +}; + +/// \brief A helper class to hold the common YAML parsing state. +class VFSFromYAMLParser { + yaml::Stream &Stream; + + void error(yaml::Node *N, const Twine &Msg) { + Stream.printError(N, Msg); + } + + // false on error + bool parseScalarString(yaml::Node *N, StringRef &Result, + SmallVectorImpl &Storage) { + yaml::ScalarNode *S = dyn_cast(N); + if (!S) { + error(N, "expected string"); + return false; + } + Result = S->getValue(Storage); + return true; + } + + // false on error + bool parseScalarBool(yaml::Node *N, bool &Result) { + SmallString<5> Storage; + StringRef Value; + if (!parseScalarString(N, Value, Storage)) + return false; + + if (Value.equals_lower("true") || Value.equals_lower("on") || + Value.equals_lower("yes") || Value == "1") { + Result = true; + return true; + } else if (Value.equals_lower("false") || Value.equals_lower("off") || + Value.equals_lower("no") || Value == "0") { + Result = false; + return true; + } + + error(N, "expected boolean value"); + return false; + } + + struct KeyStatus { + KeyStatus(bool Required=false) : Required(Required), Seen(false) {} + bool Required; + bool Seen; + }; + typedef std::pair KeyStatusPair; + + // false on error + bool checkDuplicateOrUnknownKey(yaml::Node *KeyNode, StringRef Key, + DenseMap &Keys) { + if (!Keys.count(Key)) { + error(KeyNode, "unknown key"); + return false; + } + KeyStatus &S = Keys[Key]; + if (S.Seen) { + error(KeyNode, Twine("duplicate key '") + Key + "'"); + return false; + } + S.Seen = true; + return true; + } + + // false on error + bool checkMissingKeys(yaml::Node *Obj, DenseMap &Keys) { + for (DenseMap::iterator I = Keys.begin(), + E = Keys.end(); + I != E; ++I) { + if (I->second.Required && !I->second.Seen) { + error(Obj, Twine("missing key '") + I->first + "'"); + return false; + } + } + return true; + } + + Entry *parseEntry(yaml::Node *N) { + yaml::MappingNode *M = dyn_cast(N); + if (!M) { + error(N, "expected mapping node for file or directory entry"); + return NULL; + } + + KeyStatusPair Fields[] = { + KeyStatusPair("name", true), + KeyStatusPair("type", true), + KeyStatusPair("contents", false), + KeyStatusPair("external-contents", false) + }; + + DenseMap Keys( + &Fields[0], Fields + sizeof(Fields)/sizeof(Fields[0])); + + bool HasContents = false; // external or otherwise + std::vector EntryArrayContents; + std::string ExternalContentsPath; + std::string Name; + EntryKind Kind; + + for (yaml::MappingNode::iterator I = M->begin(), E = M->end(); I != E; + ++I) { + StringRef Key; + // Reuse the buffer for key and value, since we don't look at key after + // parsing value. + SmallString<256> Buffer; + if (!parseScalarString(I->getKey(), Key, Buffer)) + return NULL; + + if (!checkDuplicateOrUnknownKey(I->getKey(), Key, Keys)) + return NULL; + + StringRef Value; + if (Key == "name") { + if (!parseScalarString(I->getValue(), Value, Buffer)) + return NULL; + Name = Value; + if (sys::path::has_parent_path(Name)) { + error(I->getValue(), "unexpected path separator in name"); + return NULL; + } + } else if (Key == "type") { + if (!parseScalarString(I->getValue(), Value, Buffer)) + return NULL; + if (Value == "file") + Kind = EK_File; + else if (Value == "directory") + Kind = EK_Directory; + else { + error(I->getValue(), "unknown value for 'type'"); + return NULL; + } + } else if (Key == "contents") { + if (HasContents) { + error(I->getKey(), + "entry already has 'contents' or 'external-contents'"); + return NULL; + } + HasContents = true; + yaml::SequenceNode *Contents = + dyn_cast(I->getValue()); + if (!Contents) { + // FIXME: this is only for directories, what about files? + error(I->getValue(), "expected array"); + return NULL; + } + + for (yaml::SequenceNode::iterator I = Contents->begin(), + E = Contents->end(); + I != E; ++I) { + if (Entry *E = parseEntry(&*I)) + EntryArrayContents.push_back(E); + else + return NULL; + } + } else if (Key == "external-contents") { + if (HasContents) { + error(I->getKey(), + "entry already has 'contents' or 'external-contents'"); + return NULL; + } + HasContents = true; + if (!parseScalarString(I->getValue(), Value, Buffer)) + return NULL; + ExternalContentsPath = Value; + } else { + llvm_unreachable("key missing from Keys"); + } + } + + if (Stream.failed()) + return NULL; + + // check for missing keys + if (!HasContents) { + error(N, "missing key 'contents' or 'external-contents'"); + return NULL; + } + if (!checkMissingKeys(N, Keys)) + return NULL; + + switch (Kind) { + case EK_File: + return new FileEntry(llvm_move(Name), llvm_move(ExternalContentsPath)); + case EK_Directory: + return new DirectoryEntry( + llvm_move(Name), llvm_move(EntryArrayContents), + Status("", "", getNextVirtualUniqueID(), sys::TimeValue::now(), 0, 0, + 0, file_type::directory_file, sys::fs::all_all)); + } + } + +public: + VFSFromYAMLParser(yaml::Stream &S) : Stream(S) {} + + // false on error + bool parse(yaml::Node *Root, VFSFromYAML *FS) { + yaml::MappingNode *Top = dyn_cast(Root); + if (!Top) { + error(Root, "expected mapping node"); + return false; + } + + KeyStatusPair Fields[] = { + KeyStatusPair("version", true), + KeyStatusPair("case-sensitive", false), + KeyStatusPair("roots", true), + }; + + DenseMap Keys( + &Fields[0], Fields + sizeof(Fields)/sizeof(Fields[0])); + + // Parse configuration and 'roots' + for (yaml::MappingNode::iterator I = Top->begin(), E = Top->end(); I != E; + ++I) { + SmallString<10> KeyBuffer; + StringRef Key; + if (!parseScalarString(I->getKey(), Key, KeyBuffer)) + return false; + + if (!checkDuplicateOrUnknownKey(I->getKey(), Key, Keys)) + return false; + + if (Key == "roots") { + yaml::SequenceNode *Roots = dyn_cast(I->getValue()); + if (!Roots) { + error(I->getValue(), "expected array"); + return false; + } + + for (yaml::SequenceNode::iterator I = Roots->begin(), E = Roots->end(); + I != E; ++I) { + if (Entry *E = parseEntry(&*I)) + FS->Roots.push_back(E); + else + return false; + } + } else if (Key == "version") { + StringRef VersionString; + SmallString<4> Storage; + if (!parseScalarString(I->getValue(), VersionString, Storage)) + return false; + int Version; + if (VersionString.getAsInteger(10, Version)) { + error(I->getValue(), "expected integer"); + return false; + } + if (Version < 0) { + error(I->getValue(), "invalid version number"); + return false; + } + if (Version != 0) { + error(I->getValue(), "version mismatch, expected 0"); + return false; + } + } else if (Key == "case-sensitive") { + if (!parseScalarBool(I->getValue(), FS->CaseSensitive)) + return false; + } else { + llvm_unreachable("key missing from Keys"); + } + } + + if (Stream.failed()) + return false; + + if (!checkMissingKeys(Top, Keys)) + return false; + return true; + } +}; +} // end of anonymous namespace + +Entry::~Entry() {} +DirectoryEntry::~DirectoryEntry() { llvm::DeleteContainerPointers(Contents); } + +VFSFromYAML::~VFSFromYAML() { llvm::DeleteContainerPointers(Roots); } + +VFSFromYAML *VFSFromYAML::create(MemoryBuffer *Buffer, + SourceMgr::DiagHandlerTy DiagHandler, + IntrusiveRefCntPtr ExternalFS) { + + SourceMgr SM; + yaml::Stream Stream(Buffer, SM); + + SM.setDiagHandler(DiagHandler); + yaml::document_iterator DI = Stream.begin(); + yaml::Node *Root = DI->getRoot(); + if (DI == Stream.end() || !Root) { + SM.PrintMessage(SMLoc(), SourceMgr::DK_Error, "expected root node"); + return NULL; + } + + VFSFromYAMLParser P(Stream); + + OwningPtr FS(new VFSFromYAML(ExternalFS)); + if (!P.parse(Root, FS.get())) + return NULL; + + return FS.take(); +} + +ErrorOr VFSFromYAML::lookupPath(const Twine &Path_) { + SmallVector Storage; + StringRef Path = Path_.toNullTerminatedStringRef(Storage); + + if (Path.empty()) + return error_code(errc::invalid_argument, system_category()); + + sys::path::const_iterator Start = sys::path::begin(Path); + sys::path::const_iterator End = sys::path::end(Path); + for (std::vector::iterator I = Roots.begin(), E = Roots.end(); + I != E; ++I) { + ErrorOr Result = lookupPath(Start, End, *I); + if (Result || Result.getError() != errc::no_such_file_or_directory) + return Result; + } + return error_code(errc::no_such_file_or_directory, system_category()); +} + +ErrorOr VFSFromYAML::lookupPath(sys::path::const_iterator Start, + sys::path::const_iterator End, + Entry *From) { + // FIXME: handle . and .. + if (CaseSensitive ? !Start->equals(From->getName()) + : !Start->equals_lower(From->getName())) + // failure to match + return error_code(errc::no_such_file_or_directory, system_category()); + + ++Start; + + if (Start == End) { + // Match! + return From; + } + + DirectoryEntry *DE = dyn_cast(From); + if (!DE) + return error_code(errc::not_a_directory, system_category()); + + for (DirectoryEntry::iterator I = DE->contents_begin(), + E = DE->contents_end(); + I != E; ++I) { + ErrorOr Result = lookupPath(Start, End, *I); + if (Result || Result.getError() != errc::no_such_file_or_directory) + return Result; + } + return error_code(errc::no_such_file_or_directory, system_category()); +} + +ErrorOr VFSFromYAML::status(const Twine &Path) { + ErrorOr Result = lookupPath(Path); + if (!Result) + return Result.getError(); + + std::string PathStr(Path.str()); + if (FileEntry *F = dyn_cast(*Result)) { + ErrorOr S = ExternalFS->status(F->getExternalContentsPath()); + if (S) { + assert(S->getName() == S->getExternalName() && + S->getName() == F->getExternalContentsPath()); + S->setName(PathStr); + } + return S; + } else { // directory + DirectoryEntry *DE = cast(*Result); + Status S = DE->getStatus(); + S.setName(PathStr); + S.setExternalName(PathStr); + return S; + } +} + +error_code VFSFromYAML::openFileForRead(const Twine &Path, + OwningPtr &Result) { + ErrorOr E = lookupPath(Path); + if (!E) + return E.getError(); + + FileEntry *F = dyn_cast(*E); + if (!F) // FIXME: errc::not_a_file? + return error_code(errc::invalid_argument, system_category()); + + return ExternalFS->openFileForRead(Path, Result); +} + +IntrusiveRefCntPtr +vfs::getVFSFromYAML(MemoryBuffer *Buffer, SourceMgr::DiagHandlerTy DiagHandler, + IntrusiveRefCntPtr ExternalFS) { + return VFSFromYAML::create(Buffer, DiagHandler, ExternalFS); +} + +UniqueID vfs::getNextVirtualUniqueID() { + static volatile sys::cas_flag UID = 0; + sys::cas_flag ID = llvm::sys::AtomicIncrement(&UID); + // The following assumes that uint64_t max will never collide with a real + // dev_t value from the OS. + return UniqueID(std::numeric_limits::max(), ID); +} diff --git a/clang/unittests/Basic/VirtualFileSystemTest.cpp b/clang/unittests/Basic/VirtualFileSystemTest.cpp index e545e9c05972..50d11019f305 100644 --- a/clang/unittests/Basic/VirtualFileSystemTest.cpp +++ b/clang/unittests/Basic/VirtualFileSystemTest.cpp @@ -8,7 +8,9 @@ //===----------------------------------------------------------------------===// #include "clang/Basic/VirtualFileSystem.h" +#include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" +#include "llvm/Support/SourceMgr.h" #include "gtest/gtest.h" #include using namespace clang; @@ -31,7 +33,7 @@ public: ErrorOr status(const Twine &Path) { std::map::iterator I = - FilesAndDirs.find(Path.str()); + FilesAndDirs.find(Path.str()); if (I == FilesAndDirs.end()) return error_code(errc::no_such_file_or_directory, posix_category()); return I->second; @@ -50,13 +52,13 @@ public: FilesAndDirs[Path] = Status; } - void addRegularFile(StringRef Path, sys::fs::perms Perms=sys::fs::all_all) { + void addRegularFile(StringRef Path, sys::fs::perms Perms = sys::fs::all_all) { vfs::Status S(Path, Path, UniqueID(FSID, FileID++), sys::TimeValue::now(), 0, 0, 1024, sys::fs::file_type::regular_file, Perms); addEntry(Path, S); } - void addDirectory(StringRef Path, sys::fs::perms Perms=sys::fs::all_all) { + void addDirectory(StringRef Path, sys::fs::perms Perms = sys::fs::all_all) { vfs::Status S(Path, Path, UniqueID(FSID, FileID++), sys::TimeValue::now(), 0, 0, 0, sys::fs::file_type::directory_file, Perms); addEntry(Path, S); @@ -70,7 +72,7 @@ public: }; } // end anonymous namespace -TEST(VirtualFileSystemTest, statusQueries) { +TEST(VirtualFileSystemTest, StatusQueries) { IntrusiveRefCntPtr D(new DummyFileSystem()); ErrorOr Status((error_code())); @@ -110,7 +112,7 @@ TEST(VirtualFileSystemTest, statusQueries) { EXPECT_FALSE(Status->equivalent(*Status2)); } -TEST(VirtualFileSystemTest, baseOnlyOverlay) { +TEST(VirtualFileSystemTest, BaseOnlyOverlay) { IntrusiveRefCntPtr D(new DummyFileSystem()); ErrorOr Status((error_code())); EXPECT_FALSE(Status = D->status("/foo")); @@ -128,17 +130,18 @@ TEST(VirtualFileSystemTest, baseOnlyOverlay) { EXPECT_TRUE(Status->equivalent(*Status2)); } -TEST(VirtualFileSystemTest, overlayFiles) { +TEST(VirtualFileSystemTest, OverlayFiles) { IntrusiveRefCntPtr Base(new DummyFileSystem()); IntrusiveRefCntPtr Middle(new DummyFileSystem()); IntrusiveRefCntPtr Top(new DummyFileSystem()); - IntrusiveRefCntPtr O(new vfs::OverlayFileSystem(Base)); + IntrusiveRefCntPtr O( + new vfs::OverlayFileSystem(Base)); O->pushOverlay(Middle); O->pushOverlay(Top); ErrorOr Status1((error_code())), Status2((error_code())), - Status3((error_code())), StatusB((error_code())), - StatusM((error_code())), StatusT((error_code())); + Status3((error_code())), StatusB((error_code())), StatusM((error_code())), + StatusT((error_code())); Base->addRegularFile("/foo"); StatusB = Base->status("/foo"); @@ -165,11 +168,11 @@ TEST(VirtualFileSystemTest, overlayFiles) { EXPECT_FALSE(Status1->equivalent(*Status3)); } -TEST(VirtualFileSystemTest, overlayDirsNonMerged) { +TEST(VirtualFileSystemTest, OverlayDirsNonMerged) { IntrusiveRefCntPtr Lower(new DummyFileSystem()); IntrusiveRefCntPtr Upper(new DummyFileSystem()); - IntrusiveRefCntPtr - O(new vfs::OverlayFileSystem(Lower)); + IntrusiveRefCntPtr O( + new vfs::OverlayFileSystem(Lower)); O->pushOverlay(Upper); Lower->addDirectory("/lower-only"); @@ -189,12 +192,12 @@ TEST(VirtualFileSystemTest, overlayDirsNonMerged) { EXPECT_TRUE(Status1->equivalent(*Status2)); } -TEST(VirtualFileSystemTest, mergedDirPermissions) { +TEST(VirtualFileSystemTest, MergedDirPermissions) { // merged directories get the permissions of the upper dir IntrusiveRefCntPtr Lower(new DummyFileSystem()); IntrusiveRefCntPtr Upper(new DummyFileSystem()); - IntrusiveRefCntPtr - O(new vfs::OverlayFileSystem(Lower)); + IntrusiveRefCntPtr O( + new vfs::OverlayFileSystem(Lower)); O->pushOverlay(Upper); ErrorOr Status((error_code())); @@ -214,3 +217,248 @@ TEST(VirtualFileSystemTest, mergedDirPermissions) { ASSERT_EQ(errc::success, Status.getError()); EXPECT_EQ(0200, Status->getPermissions()); } + +static int NumDiagnostics = 0; +static void CountingDiagHandler(const SMDiagnostic &, void *) { + ++NumDiagnostics; +} + +static IntrusiveRefCntPtr +getFromYAMLRawString(StringRef Content, + IntrusiveRefCntPtr ExternalFS) { + MemoryBuffer *Buffer = MemoryBuffer::getMemBuffer(Content); + return getVFSFromYAML(Buffer, CountingDiagHandler, ExternalFS); +} + +static IntrusiveRefCntPtr getFromYAMLString( + StringRef Content, + IntrusiveRefCntPtr ExternalFS = new DummyFileSystem()) { + std::string VersionPlusContent("{\n 'version':0,\n"); + VersionPlusContent += Content.slice(Content.find('{') + 1, StringRef::npos); + return getFromYAMLRawString(VersionPlusContent, ExternalFS); +} + +TEST(VirtualFileSystemTest, BasicVFSFromYAML) { + NumDiagnostics = 0; + IntrusiveRefCntPtr FS; + FS = getFromYAMLString(""); + EXPECT_EQ(NULL, FS.getPtr()); + FS = getFromYAMLString("[]"); + EXPECT_EQ(NULL, FS.getPtr()); + FS = getFromYAMLString("'string'"); + EXPECT_EQ(NULL, FS.getPtr()); + EXPECT_EQ(3, NumDiagnostics); +} + +TEST(VirtualFileSystemTest, MappedFiles) { + NumDiagnostics = 0; + IntrusiveRefCntPtr Lower(new DummyFileSystem()); + Lower->addRegularFile("/foo/bar/a"); + IntrusiveRefCntPtr FS = + getFromYAMLString("{ 'roots': [\n" + "{\n" + " 'type': 'directory',\n" + " 'name': '/',\n" + " 'contents': [ {\n" + " 'type': 'file',\n" + " 'name': 'file1',\n" + " 'external-contents': '/foo/bar/a'\n" + " },\n" + " {\n" + " 'type': 'file',\n" + " 'name': 'file2',\n" + " 'external-contents': '/foo/b'\n" + " }\n" + " ]\n" + "}\n" + "]\n" + "}", + Lower); + ASSERT_TRUE(FS.getPtr()); + + IntrusiveRefCntPtr O( + new vfs::OverlayFileSystem(Lower)); + O->pushOverlay(FS); + + // file + ErrorOr S = O->status("/file1"); + ASSERT_EQ(errc::success, S.getError()); + EXPECT_EQ("/file1", S->getName()); + EXPECT_EQ("/foo/bar/a", S->getExternalName()); + + ErrorOr SLower = O->status("/foo/bar/a"); + EXPECT_EQ("/foo/bar/a", SLower->getName()); + EXPECT_TRUE(S->equivalent(*SLower)); + + // directory + S = O->status("/"); + ASSERT_EQ(errc::success, S.getError()); + EXPECT_TRUE(S->isDirectory()); + EXPECT_TRUE(S->equivalent(*O->status("/"))); // non-volatile UniqueID + + // broken mapping + EXPECT_EQ(errc::no_such_file_or_directory, O->status("/file2").getError()); + EXPECT_EQ(0, NumDiagnostics); +} + +TEST(VirtualFileSystemTest, CaseInsensitive) { + NumDiagnostics = 0; + IntrusiveRefCntPtr Lower(new DummyFileSystem()); + Lower->addRegularFile("/foo/bar/a"); + IntrusiveRefCntPtr FS = + getFromYAMLString("{ 'case-sensitive': 'false',\n" + " 'roots': [\n" + "{\n" + " 'type': 'directory',\n" + " 'name': '/',\n" + " 'contents': [ {\n" + " 'type': 'file',\n" + " 'name': 'XX',\n" + " 'external-contents': '/foo/bar/a'\n" + " }\n" + " ]\n" + "}]}", + Lower); + ASSERT_TRUE(FS.getPtr()); + + IntrusiveRefCntPtr O( + new vfs::OverlayFileSystem(Lower)); + O->pushOverlay(FS); + + ErrorOr S = O->status("/XX"); + ASSERT_EQ(errc::success, S.getError()); + + ErrorOr SS = O->status("/xx"); + ASSERT_EQ(errc::success, SS.getError()); + EXPECT_TRUE(S->equivalent(*SS)); + SS = O->status("/xX"); + EXPECT_TRUE(S->equivalent(*SS)); + SS = O->status("/Xx"); + EXPECT_TRUE(S->equivalent(*SS)); + EXPECT_EQ(0, NumDiagnostics); +} + +TEST(VirtualFileSystemTest, CaseSensitive) { + NumDiagnostics = 0; + IntrusiveRefCntPtr Lower(new DummyFileSystem()); + Lower->addRegularFile("/foo/bar/a"); + IntrusiveRefCntPtr FS = + getFromYAMLString("{ 'case-sensitive': 'true',\n" + " 'roots': [\n" + "{\n" + " 'type': 'directory',\n" + " 'name': '/',\n" + " 'contents': [ {\n" + " 'type': 'file',\n" + " 'name': 'XX',\n" + " 'external-contents': '/foo/bar/a'\n" + " }\n" + " ]\n" + "}]}", + Lower); + ASSERT_TRUE(FS.getPtr()); + + IntrusiveRefCntPtr O( + new vfs::OverlayFileSystem(Lower)); + O->pushOverlay(FS); + + ErrorOr SS = O->status("/xx"); + EXPECT_EQ(errc::no_such_file_or_directory, SS.getError()); + SS = O->status("/xX"); + EXPECT_EQ(errc::no_such_file_or_directory, SS.getError()); + SS = O->status("/Xx"); + EXPECT_EQ(errc::no_such_file_or_directory, SS.getError()); + EXPECT_EQ(0, NumDiagnostics); +} + +TEST(VirtualFileSystemTest, IllegalVFSFile) { + NumDiagnostics = 0; + IntrusiveRefCntPtr Lower(new DummyFileSystem()); + + // invalid YAML at top-level + IntrusiveRefCntPtr FS = getFromYAMLString("{]", Lower); + EXPECT_FALSE(FS.getPtr()); + // invalid YAML in roots + FS = getFromYAMLString("{ 'roots':[}", Lower); + // invalid YAML in directory + FS = getFromYAMLString( + "{ 'roots':[ { 'name': 'foo', 'type': 'directory', 'contents': [}", + Lower); + EXPECT_FALSE(FS.getPtr()); + + // invalid configuration + FS = getFromYAMLString("{ 'knobular': 'true', 'roots':[] }", Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString("{ 'case-sensitive': 'maybe', 'roots':[] }", Lower); + EXPECT_FALSE(FS.getPtr()); + + // invalid roots + FS = getFromYAMLString("{ 'roots':'' }", Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString("{ 'roots':{} }", Lower); + EXPECT_FALSE(FS.getPtr()); + + // invalid entries + FS = getFromYAMLString( + "{ 'roots':[ { 'type': 'other', 'name': 'me', 'contents': '' }", Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString("{ 'roots':[ { 'type': 'file', 'name': [], " + "'external-contents': 'other' }", + Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString( + "{ 'roots':[ { 'type': 'file', 'name': 'me', 'external-contents': [] }", + Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString( + "{ 'roots':[ { 'type': 'file', 'name': 'me', 'external-contents': {} }", + Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString( + "{ 'roots':[ { 'type': 'directory', 'name': 'me', 'contents': {} }", + Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString( + "{ 'roots':[ { 'type': 'directory', 'name': 'me', 'contents': '' }", + Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString( + "{ 'roots':[ { 'thingy': 'directory', 'name': 'me', 'contents': [] }", + Lower); + EXPECT_FALSE(FS.getPtr()); + + // missing mandatory fields + FS = getFromYAMLString("{ 'roots':[ { 'type': 'file', 'name': 'me' }", Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString( + "{ 'roots':[ { 'type': 'file', 'external-contents': 'other' }", Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString("{ 'roots':[ { 'name': 'me', 'contents': [] }", Lower); + EXPECT_FALSE(FS.getPtr()); + + // duplicate keys + FS = getFromYAMLString("{ 'roots':[], 'roots':[] }", Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLString( + "{ 'case-sensitive':'true', 'case-sensitive':'true', 'roots':[] }", + Lower); + EXPECT_FALSE(FS.getPtr()); + FS = + getFromYAMLString("{ 'roots':[{'name':'me', 'name':'you', 'type':'file', " + "'external-contents':'blah' } ] }", + Lower); + EXPECT_FALSE(FS.getPtr()); + + // missing version + FS = getFromYAMLRawString("{ 'roots':[] }", Lower); + EXPECT_FALSE(FS.getPtr()); + + // bad version number + FS = getFromYAMLRawString("{ 'version':'foo', 'roots':[] }", Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLRawString("{ 'version':-1, 'roots':[] }", Lower); + EXPECT_FALSE(FS.getPtr()); + FS = getFromYAMLRawString("{ 'version':100000, 'roots':[] }", Lower); + EXPECT_FALSE(FS.getPtr()); + EXPECT_EQ(24, NumDiagnostics); +}