| // Protocol Buffers - Google's data interchange format |
| // Copyright 2008 Google Inc. All rights reserved. |
| // https://developers.google.com/protocol-buffers/ |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| // Author: kenton@google.com (Kenton Varda) |
| // Based on original Protocol Buffers design by |
| // Sanjay Ghemawat, Jeff Dean, and others. |
| |
| #include <google/protobuf/compiler/importer.h> |
| |
| #include <memory> |
| #include <unordered_map> |
| |
| #include <google/protobuf/stubs/logging.h> |
| #include <google/protobuf/stubs/common.h> |
| #include <google/protobuf/testing/file.h> |
| #include <google/protobuf/testing/file.h> |
| #include <google/protobuf/testing/file.h> |
| #include <google/protobuf/io/zero_copy_stream_impl.h> |
| #include <google/protobuf/descriptor.h> |
| #include <google/protobuf/testing/googletest.h> |
| #include <gtest/gtest.h> |
| #include <google/protobuf/stubs/substitute.h> |
| #include <google/protobuf/stubs/map_util.h> |
| #include <google/protobuf/stubs/strutil.h> |
| |
| namespace google { |
| namespace protobuf { |
| namespace compiler { |
| |
| namespace { |
| |
| bool FileExists(const std::string& path) { |
| return File::Exists(path); |
| } |
| |
| #define EXPECT_SUBSTRING(needle, haystack) \ |
| EXPECT_PRED_FORMAT2(testing::IsSubstring, (needle), (haystack)) |
| |
| class MockErrorCollector : public MultiFileErrorCollector { |
| public: |
| MockErrorCollector() {} |
| ~MockErrorCollector() {} |
| |
| std::string text_; |
| std::string warning_text_; |
| |
| // implements ErrorCollector --------------------------------------- |
| void AddError(const std::string& filename, int line, int column, |
| const std::string& message) { |
| strings::SubstituteAndAppend(&text_, "$0:$1:$2: $3\n", filename, line, column, |
| message); |
| } |
| |
| void AddWarning(const std::string& filename, int line, int column, |
| const std::string& message) { |
| strings::SubstituteAndAppend(&warning_text_, "$0:$1:$2: $3\n", filename, line, |
| column, message); |
| } |
| }; |
| |
| // ------------------------------------------------------------------- |
| |
| // A dummy implementation of SourceTree backed by a simple map. |
| class MockSourceTree : public SourceTree { |
| public: |
| MockSourceTree() {} |
| ~MockSourceTree() {} |
| |
| void AddFile(const std::string& name, const char* contents) { |
| files_[name] = contents; |
| } |
| |
| // implements SourceTree ------------------------------------------- |
| io::ZeroCopyInputStream* Open(const std::string& filename) { |
| const char* contents = FindPtrOrNull(files_, filename); |
| if (contents == NULL) { |
| return NULL; |
| } else { |
| return new io::ArrayInputStream(contents, strlen(contents)); |
| } |
| } |
| |
| std::string GetLastErrorMessage() { return "File not found."; } |
| |
| private: |
| std::unordered_map<std::string, const char*> files_; |
| }; |
| |
| // =================================================================== |
| |
| class ImporterTest : public testing::Test { |
| protected: |
| ImporterTest() : importer_(&source_tree_, &error_collector_) {} |
| |
| void AddFile(const std::string& filename, const char* text) { |
| source_tree_.AddFile(filename, text); |
| } |
| |
| // Return the collected error text |
| std::string warning() const { return error_collector_.warning_text_; } |
| |
| MockErrorCollector error_collector_; |
| MockSourceTree source_tree_; |
| Importer importer_; |
| }; |
| |
| TEST_F(ImporterTest, Import) { |
| // Test normal importing. |
| AddFile("foo.proto", |
| "syntax = \"proto2\";\n" |
| "message Foo {}\n"); |
| |
| const FileDescriptor* file = importer_.Import("foo.proto"); |
| EXPECT_EQ("", error_collector_.text_); |
| ASSERT_TRUE(file != NULL); |
| |
| ASSERT_EQ(1, file->message_type_count()); |
| EXPECT_EQ("Foo", file->message_type(0)->name()); |
| |
| // Importing again should return same object. |
| EXPECT_EQ(file, importer_.Import("foo.proto")); |
| } |
| |
| TEST_F(ImporterTest, ImportNested) { |
| // Test that importing a file which imports another file works. |
| AddFile("foo.proto", |
| "syntax = \"proto2\";\n" |
| "import \"bar.proto\";\n" |
| "message Foo {\n" |
| " optional Bar bar = 1;\n" |
| "}\n"); |
| AddFile("bar.proto", |
| "syntax = \"proto2\";\n" |
| "message Bar {}\n"); |
| |
| // Note that both files are actually parsed by the first call to Import() |
| // here, since foo.proto imports bar.proto. The second call just returns |
| // the same ProtoFile for bar.proto which was constructed while importing |
| // foo.proto. We test that this is the case below by checking that bar |
| // is among foo's dependencies (by pointer). |
| const FileDescriptor* foo = importer_.Import("foo.proto"); |
| const FileDescriptor* bar = importer_.Import("bar.proto"); |
| EXPECT_EQ("", error_collector_.text_); |
| ASSERT_TRUE(foo != NULL); |
| ASSERT_TRUE(bar != NULL); |
| |
| // Check that foo's dependency is the same object as bar. |
| ASSERT_EQ(1, foo->dependency_count()); |
| EXPECT_EQ(bar, foo->dependency(0)); |
| |
| // Check that foo properly cross-links bar. |
| ASSERT_EQ(1, foo->message_type_count()); |
| ASSERT_EQ(1, bar->message_type_count()); |
| ASSERT_EQ(1, foo->message_type(0)->field_count()); |
| ASSERT_EQ(FieldDescriptor::TYPE_MESSAGE, |
| foo->message_type(0)->field(0)->type()); |
| EXPECT_EQ(bar->message_type(0), |
| foo->message_type(0)->field(0)->message_type()); |
| } |
| |
| TEST_F(ImporterTest, FileNotFound) { |
| // Error: Parsing a file that doesn't exist. |
| EXPECT_TRUE(importer_.Import("foo.proto") == NULL); |
| EXPECT_EQ("foo.proto:-1:0: File not found.\n", error_collector_.text_); |
| } |
| |
| TEST_F(ImporterTest, ImportNotFound) { |
| // Error: Importing a file that doesn't exist. |
| AddFile("foo.proto", |
| "syntax = \"proto2\";\n" |
| "import \"bar.proto\";\n"); |
| |
| EXPECT_TRUE(importer_.Import("foo.proto") == NULL); |
| EXPECT_EQ( |
| "bar.proto:-1:0: File not found.\n" |
| "foo.proto:1:0: Import \"bar.proto\" was not found or had errors.\n", |
| error_collector_.text_); |
| } |
| |
| TEST_F(ImporterTest, RecursiveImport) { |
| // Error: Recursive import. |
| AddFile("recursive1.proto", |
| "syntax = \"proto2\";\n" |
| "\n" |
| "import \"recursive2.proto\";\n"); |
| AddFile("recursive2.proto", |
| "syntax = \"proto2\";\n" |
| "import \"recursive1.proto\";\n"); |
| |
| EXPECT_TRUE(importer_.Import("recursive1.proto") == NULL); |
| EXPECT_EQ( |
| "recursive1.proto:2:0: File recursively imports itself: " |
| "recursive1.proto " |
| "-> recursive2.proto -> recursive1.proto\n" |
| "recursive2.proto:1:0: Import \"recursive1.proto\" was not found " |
| "or had errors.\n" |
| "recursive1.proto:2:0: Import \"recursive2.proto\" was not found " |
| "or had errors.\n", |
| error_collector_.text_); |
| } |
| |
| TEST_F(ImporterTest, RecursiveImportSelf) { |
| // Error: Recursive import. |
| AddFile("recursive.proto", |
| "syntax = \"proto2\";\n" |
| "\n" |
| "import \"recursive.proto\";\n"); |
| |
| EXPECT_TRUE(importer_.Import("recursive.proto") == nullptr); |
| EXPECT_EQ( |
| "recursive.proto:2:0: File recursively imports itself: " |
| "recursive.proto -> recursive.proto\n", |
| error_collector_.text_); |
| } |
| |
| TEST_F(ImporterTest, LiteRuntimeImport) { |
| // Error: Recursive import. |
| AddFile("bar.proto", |
| "syntax = \"proto2\";\n" |
| "option optimize_for = LITE_RUNTIME;\n"); |
| AddFile("foo.proto", |
| "syntax = \"proto2\";\n" |
| "import \"bar.proto\";\n"); |
| |
| EXPECT_TRUE(importer_.Import("foo.proto") == nullptr); |
| EXPECT_EQ( |
| "foo.proto:1:0: Files that do not use optimize_for = LITE_RUNTIME " |
| "cannot import files which do use this option. This file is not " |
| "lite, but it imports \"bar.proto\" which is.\n", |
| error_collector_.text_); |
| } |
| |
| |
| // =================================================================== |
| |
| class DiskSourceTreeTest : public testing::Test { |
| protected: |
| virtual void SetUp() { |
| dirnames_.push_back(TestTempDir() + "/test_proto2_import_path_1"); |
| dirnames_.push_back(TestTempDir() + "/test_proto2_import_path_2"); |
| |
| for (int i = 0; i < dirnames_.size(); i++) { |
| if (FileExists(dirnames_[i])) { |
| File::DeleteRecursively(dirnames_[i], NULL, NULL); |
| } |
| GOOGLE_CHECK_OK(File::CreateDir(dirnames_[i], 0777)); |
| } |
| } |
| |
| virtual void TearDown() { |
| for (int i = 0; i < dirnames_.size(); i++) { |
| if (FileExists(dirnames_[i])) { |
| File::DeleteRecursively(dirnames_[i], NULL, NULL); |
| } |
| } |
| } |
| |
| void AddFile(const std::string& filename, const char* contents) { |
| GOOGLE_CHECK_OK(File::SetContents(filename, contents, true)); |
| } |
| |
| void AddSubdir(const std::string& dirname) { |
| GOOGLE_CHECK_OK(File::CreateDir(dirname, 0777)); |
| } |
| |
| void ExpectFileContents(const std::string& filename, |
| const char* expected_contents) { |
| std::unique_ptr<io::ZeroCopyInputStream> input(source_tree_.Open(filename)); |
| |
| ASSERT_FALSE(input == NULL); |
| |
| // Read all the data from the file. |
| std::string file_contents; |
| const void* data; |
| int size; |
| while (input->Next(&data, &size)) { |
| file_contents.append(reinterpret_cast<const char*>(data), size); |
| } |
| |
| EXPECT_EQ(expected_contents, file_contents); |
| } |
| |
| void ExpectCannotOpenFile(const std::string& filename, |
| const std::string& error_message) { |
| std::unique_ptr<io::ZeroCopyInputStream> input(source_tree_.Open(filename)); |
| EXPECT_TRUE(input == NULL); |
| EXPECT_EQ(error_message, source_tree_.GetLastErrorMessage()); |
| } |
| |
| DiskSourceTree source_tree_; |
| |
| // Paths of two on-disk directories to use during the test. |
| std::vector<std::string> dirnames_; |
| }; |
| |
| TEST_F(DiskSourceTreeTest, MapRoot) { |
| // Test opening a file in a directory that is mapped to the root of the |
| // source tree. |
| AddFile(dirnames_[0] + "/foo", "Hello World!"); |
| source_tree_.MapPath("", dirnames_[0]); |
| |
| ExpectFileContents("foo", "Hello World!"); |
| ExpectCannotOpenFile("bar", "File not found."); |
| } |
| |
| TEST_F(DiskSourceTreeTest, MapDirectory) { |
| // Test opening a file in a directory that is mapped to somewhere other |
| // than the root of the source tree. |
| |
| AddFile(dirnames_[0] + "/foo", "Hello World!"); |
| source_tree_.MapPath("baz", dirnames_[0]); |
| |
| ExpectFileContents("baz/foo", "Hello World!"); |
| ExpectCannotOpenFile("baz/bar", "File not found."); |
| ExpectCannotOpenFile("foo", "File not found."); |
| ExpectCannotOpenFile("bar", "File not found."); |
| |
| // Non-canonical file names should not work. |
| ExpectCannotOpenFile("baz//foo", |
| "Backslashes, consecutive slashes, \".\", or \"..\" are " |
| "not allowed in the virtual path"); |
| ExpectCannotOpenFile("baz/../baz/foo", |
| "Backslashes, consecutive slashes, \".\", or \"..\" are " |
| "not allowed in the virtual path"); |
| ExpectCannotOpenFile("baz/./foo", |
| "Backslashes, consecutive slashes, \".\", or \"..\" are " |
| "not allowed in the virtual path"); |
| ExpectCannotOpenFile("baz/foo/", "File not found."); |
| } |
| |
| TEST_F(DiskSourceTreeTest, NoParent) { |
| // Test that we cannot open files in a parent of a mapped directory. |
| |
| AddFile(dirnames_[0] + "/foo", "Hello World!"); |
| AddSubdir(dirnames_[0] + "/bar"); |
| AddFile(dirnames_[0] + "/bar/baz", "Blah."); |
| source_tree_.MapPath("", dirnames_[0] + "/bar"); |
| |
| ExpectFileContents("baz", "Blah."); |
| ExpectCannotOpenFile("../foo", |
| "Backslashes, consecutive slashes, \".\", or \"..\" are " |
| "not allowed in the virtual path"); |
| ExpectCannotOpenFile("../bar/baz", |
| "Backslashes, consecutive slashes, \".\", or \"..\" are " |
| "not allowed in the virtual path"); |
| } |
| |
| TEST_F(DiskSourceTreeTest, MapFile) { |
| // Test opening a file that is mapped directly into the source tree. |
| |
| AddFile(dirnames_[0] + "/foo", "Hello World!"); |
| source_tree_.MapPath("foo", dirnames_[0] + "/foo"); |
| |
| ExpectFileContents("foo", "Hello World!"); |
| ExpectCannotOpenFile("bar", "File not found."); |
| } |
| |
| TEST_F(DiskSourceTreeTest, SearchMultipleDirectories) { |
| // Test mapping and searching multiple directories. |
| |
| AddFile(dirnames_[0] + "/foo", "Hello World!"); |
| AddFile(dirnames_[1] + "/foo", "This file should be hidden."); |
| AddFile(dirnames_[1] + "/bar", "Goodbye World!"); |
| source_tree_.MapPath("", dirnames_[0]); |
| source_tree_.MapPath("", dirnames_[1]); |
| |
| ExpectFileContents("foo", "Hello World!"); |
| ExpectFileContents("bar", "Goodbye World!"); |
| ExpectCannotOpenFile("baz", "File not found."); |
| } |
| |
| TEST_F(DiskSourceTreeTest, OrderingTrumpsSpecificity) { |
| // Test that directories are always searched in order, even when a latter |
| // directory is more-specific than a former one. |
| |
| // Create the "bar" directory so we can put a file in it. |
| GOOGLE_CHECK_OK(File::CreateDir(dirnames_[0] + "/bar", 0777)); |
| |
| // Add files and map paths. |
| AddFile(dirnames_[0] + "/bar/foo", "Hello World!"); |
| AddFile(dirnames_[1] + "/foo", "This file should be hidden."); |
| source_tree_.MapPath("", dirnames_[0]); |
| source_tree_.MapPath("bar", dirnames_[1]); |
| |
| // Check. |
| ExpectFileContents("bar/foo", "Hello World!"); |
| } |
| |
| TEST_F(DiskSourceTreeTest, DiskFileToVirtualFile) { |
| // Test DiskFileToVirtualFile. |
| |
| AddFile(dirnames_[0] + "/foo", "Hello World!"); |
| AddFile(dirnames_[1] + "/foo", "This file should be hidden."); |
| source_tree_.MapPath("bar", dirnames_[0]); |
| source_tree_.MapPath("bar", dirnames_[1]); |
| |
| std::string virtual_file; |
| std::string shadowing_disk_file; |
| |
| EXPECT_EQ(DiskSourceTree::NO_MAPPING, |
| source_tree_.DiskFileToVirtualFile("/foo", &virtual_file, |
| &shadowing_disk_file)); |
| |
| EXPECT_EQ(DiskSourceTree::SHADOWED, |
| source_tree_.DiskFileToVirtualFile( |
| dirnames_[1] + "/foo", &virtual_file, &shadowing_disk_file)); |
| EXPECT_EQ("bar/foo", virtual_file); |
| EXPECT_EQ(dirnames_[0] + "/foo", shadowing_disk_file); |
| |
| EXPECT_EQ(DiskSourceTree::CANNOT_OPEN, |
| source_tree_.DiskFileToVirtualFile( |
| dirnames_[1] + "/baz", &virtual_file, &shadowing_disk_file)); |
| EXPECT_EQ("bar/baz", virtual_file); |
| |
| EXPECT_EQ(DiskSourceTree::SUCCESS, |
| source_tree_.DiskFileToVirtualFile( |
| dirnames_[0] + "/foo", &virtual_file, &shadowing_disk_file)); |
| EXPECT_EQ("bar/foo", virtual_file); |
| } |
| |
| TEST_F(DiskSourceTreeTest, DiskFileToVirtualFileCanonicalization) { |
| // Test handling of "..", ".", etc. in DiskFileToVirtualFile(). |
| |
| source_tree_.MapPath("dir1", ".."); |
| source_tree_.MapPath("dir2", "../../foo"); |
| source_tree_.MapPath("dir3", "./foo/bar/."); |
| source_tree_.MapPath("dir4", "."); |
| source_tree_.MapPath("", "/qux"); |
| source_tree_.MapPath("dir5", "/quux/"); |
| |
| std::string virtual_file; |
| std::string shadowing_disk_file; |
| |
| // "../.." should not be considered to be under "..". |
| EXPECT_EQ(DiskSourceTree::NO_MAPPING, |
| source_tree_.DiskFileToVirtualFile("../../baz", &virtual_file, |
| &shadowing_disk_file)); |
| |
| // "/foo" is not mapped (it should not be misinterpreted as being under "."). |
| EXPECT_EQ(DiskSourceTree::NO_MAPPING, |
| source_tree_.DiskFileToVirtualFile("/foo", &virtual_file, |
| &shadowing_disk_file)); |
| |
| #ifdef WIN32 |
| // "C:\foo" is not mapped (it should not be misinterpreted as being under |
| // "."). |
| EXPECT_EQ(DiskSourceTree::NO_MAPPING, |
| source_tree_.DiskFileToVirtualFile("C:\\foo", &virtual_file, |
| &shadowing_disk_file)); |
| #endif // WIN32 |
| |
| // But "../baz" should be. |
| EXPECT_EQ(DiskSourceTree::CANNOT_OPEN, |
| source_tree_.DiskFileToVirtualFile("../baz", &virtual_file, |
| &shadowing_disk_file)); |
| EXPECT_EQ("dir1/baz", virtual_file); |
| |
| // "../../foo/baz" is under "../../foo". |
| EXPECT_EQ(DiskSourceTree::CANNOT_OPEN, |
| source_tree_.DiskFileToVirtualFile("../../foo/baz", &virtual_file, |
| &shadowing_disk_file)); |
| EXPECT_EQ("dir2/baz", virtual_file); |
| |
| // "foo/./bar/baz" is under "./foo/bar/.". |
| EXPECT_EQ(DiskSourceTree::CANNOT_OPEN, |
| source_tree_.DiskFileToVirtualFile("foo/bar/baz", &virtual_file, |
| &shadowing_disk_file)); |
| EXPECT_EQ("dir3/baz", virtual_file); |
| |
| // "bar" is under ".". |
| EXPECT_EQ(DiskSourceTree::CANNOT_OPEN, |
| source_tree_.DiskFileToVirtualFile("bar", &virtual_file, |
| &shadowing_disk_file)); |
| EXPECT_EQ("dir4/bar", virtual_file); |
| |
| // "/qux/baz" is under "/qux". |
| EXPECT_EQ(DiskSourceTree::CANNOT_OPEN, |
| source_tree_.DiskFileToVirtualFile("/qux/baz", &virtual_file, |
| &shadowing_disk_file)); |
| EXPECT_EQ("baz", virtual_file); |
| |
| // "/quux/bar" is under "/quux". |
| EXPECT_EQ(DiskSourceTree::CANNOT_OPEN, |
| source_tree_.DiskFileToVirtualFile("/quux/bar", &virtual_file, |
| &shadowing_disk_file)); |
| EXPECT_EQ("dir5/bar", virtual_file); |
| } |
| |
| TEST_F(DiskSourceTreeTest, VirtualFileToDiskFile) { |
| // Test VirtualFileToDiskFile. |
| |
| AddFile(dirnames_[0] + "/foo", "Hello World!"); |
| AddFile(dirnames_[1] + "/foo", "This file should be hidden."); |
| AddFile(dirnames_[1] + "/quux", "This file should not be hidden."); |
| source_tree_.MapPath("bar", dirnames_[0]); |
| source_tree_.MapPath("bar", dirnames_[1]); |
| |
| // Existent files, shadowed and non-shadowed case. |
| std::string disk_file; |
| EXPECT_TRUE(source_tree_.VirtualFileToDiskFile("bar/foo", &disk_file)); |
| EXPECT_EQ(dirnames_[0] + "/foo", disk_file); |
| EXPECT_TRUE(source_tree_.VirtualFileToDiskFile("bar/quux", &disk_file)); |
| EXPECT_EQ(dirnames_[1] + "/quux", disk_file); |
| |
| // Nonexistent file in existent directory and vice versa. |
| std::string not_touched = "not touched"; |
| EXPECT_FALSE(source_tree_.VirtualFileToDiskFile("bar/baz", ¬_touched)); |
| EXPECT_EQ("not touched", not_touched); |
| EXPECT_FALSE(source_tree_.VirtualFileToDiskFile("baz/foo", ¬_touched)); |
| EXPECT_EQ("not touched", not_touched); |
| |
| // Accept NULL as output parameter. |
| EXPECT_TRUE(source_tree_.VirtualFileToDiskFile("bar/foo", NULL)); |
| EXPECT_FALSE(source_tree_.VirtualFileToDiskFile("baz/foo", NULL)); |
| } |
| |
| } // namespace |
| |
| } // namespace compiler |
| } // namespace protobuf |
| } // namespace google |