| 1 | // Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors | 
|---|
| 2 | // Licensed under the MIT License: | 
|---|
| 3 | // | 
|---|
| 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy | 
|---|
| 5 | // of this software and associated documentation files (the "Software"), to deal | 
|---|
| 6 | // in the Software without restriction, including without limitation the rights | 
|---|
| 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | 
|---|
| 8 | // copies of the Software, and to permit persons to whom the Software is | 
|---|
| 9 | // furnished to do so, subject to the following conditions: | 
|---|
| 10 | // | 
|---|
| 11 | // The above copyright notice and this permission notice shall be included in | 
|---|
| 12 | // all copies or substantial portions of the Software. | 
|---|
| 13 | // | 
|---|
| 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | 
|---|
| 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | 
|---|
| 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | 
|---|
| 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | 
|---|
| 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | 
|---|
| 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | 
|---|
| 20 | // THE SOFTWARE. | 
|---|
| 21 |  | 
|---|
| 22 | #pragma once | 
|---|
| 23 |  | 
|---|
| 24 | #if defined(__GNUC__) && !defined(CAPNP_HEADER_WARNINGS) | 
|---|
| 25 | #pragma GCC system_header | 
|---|
| 26 | #endif | 
|---|
| 27 |  | 
|---|
| 28 | #include "schema-loader.h" | 
|---|
| 29 | #include <kj/string.h> | 
|---|
| 30 | #include <kj/filesystem.h> | 
|---|
| 31 |  | 
|---|
| 32 | namespace capnp { | 
|---|
| 33 |  | 
|---|
| 34 | class ParsedSchema; | 
|---|
| 35 | class SchemaFile; | 
|---|
| 36 |  | 
|---|
| 37 | class SchemaParser { | 
|---|
| 38 | // Parses `.capnp` files to produce `Schema` objects. | 
|---|
| 39 | // | 
|---|
| 40 | // This class is thread-safe, hence all its methods are const. | 
|---|
| 41 |  | 
|---|
| 42 | public: | 
|---|
| 43 | SchemaParser(); | 
|---|
| 44 | ~SchemaParser() noexcept(false); | 
|---|
| 45 |  | 
|---|
| 46 | ParsedSchema parseFromDirectory( | 
|---|
| 47 | const kj::ReadableDirectory& baseDir, kj::Path path, | 
|---|
| 48 | kj::ArrayPtr<const kj::ReadableDirectory* const> importPath) const; | 
|---|
| 49 | // Parse a file from the KJ filesystem API.  Throws an exception if the file dosen't exist. | 
|---|
| 50 | // | 
|---|
| 51 | // `baseDir` and `path` are used together to resolve relative imports. `path` is the source | 
|---|
| 52 | // file's path within `baseDir`. Relative imports will be interpreted relative to `path` and | 
|---|
| 53 | // will be opened using `baseDir`. Note that the KJ filesystem API prohibits "breaking out" of | 
|---|
| 54 | // a directory using "..", so relative imports will be restricted to children of `baseDir`. | 
|---|
| 55 | // | 
|---|
| 56 | // `importPath` is used for absolute imports (imports that start with a '/'). Each directory in | 
|---|
| 57 | // the array will be searched in order until a file is found. | 
|---|
| 58 | // | 
|---|
| 59 | // All `ReadableDirectory` objects must remain valid until the `SchemaParser` is destroyed. Also, | 
|---|
| 60 | // the `importPath` array must remain valid. `path` will be copied; it need not remain valid. | 
|---|
| 61 | // | 
|---|
| 62 | // This method is a shortcut, equivalent to: | 
|---|
| 63 | //     parser.parseFromDirectory(SchemaFile::newDiskFile(baseDir, path, importPath))`; | 
|---|
| 64 | // | 
|---|
| 65 | // This method throws an exception if any errors are encountered in the file or in anything the | 
|---|
| 66 | // file depends on.  Note that merely importing another file does not count as a dependency on | 
|---|
| 67 | // anything in the imported file -- only the imported types which are actually used are | 
|---|
| 68 | // "dependencies". | 
|---|
| 69 | // | 
|---|
| 70 | // Hint: Use kj::newDiskFilesystem() to initialize the KJ filesystem API. Usually you should do | 
|---|
| 71 | //   this at a high level in your program, e.g. the main() function, and then pass down the | 
|---|
| 72 | //   appropriate File/Directory objects to the components that need them. Example: | 
|---|
| 73 | // | 
|---|
| 74 | //     auto fs = kj::newDiskFilesystem(); | 
|---|
| 75 | //     SchemaParser parser; | 
|---|
| 76 | //     auto schema = parser->parseFromDirectory(fs->getCurrent(), | 
|---|
| 77 | //         kj::Path::parse("foo/bar.capnp"), nullptr); | 
|---|
| 78 | // | 
|---|
| 79 | // Hint: To use in-memory data rather than real disk, you can use kj::newInMemoryDirectory(), | 
|---|
| 80 | //   write the files you want, then pass it to SchemaParser. Example: | 
|---|
| 81 | // | 
|---|
| 82 | //     auto dir = kj::newInMemoryDirectory(kj::nullClock()); | 
|---|
| 83 | //     auto path = kj::Path::parse("foo/bar.capnp"); | 
|---|
| 84 | //     dir->openFile(path, kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT) | 
|---|
| 85 | //        ->writeAll("struct Foo {}"); | 
|---|
| 86 | //     auto schema = parser->parseFromDirectory(*dir, path, nullptr); | 
|---|
| 87 | // | 
|---|
| 88 | // Hint: You can create an in-memory directory but then populate it with real files from disk, | 
|---|
| 89 | //   in order to control what is visible while also avoiding reading files yourself or making | 
|---|
| 90 | //   extra copies. Example: | 
|---|
| 91 | // | 
|---|
| 92 | //     auto fs = kj::newDiskFilesystem(); | 
|---|
| 93 | //     auto dir = kj::newInMemoryDirectory(kj::nullClock()); | 
|---|
| 94 | //     auto fakePath = kj::Path::parse("foo/bar.capnp"); | 
|---|
| 95 | //     auto realPath = kj::Path::parse("path/to/some/file.capnp"); | 
|---|
| 96 | //     dir->transfer(fakePath, kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT, | 
|---|
| 97 | //                   fs->getCurrent(), realPath, kj::TransferMode::LINK); | 
|---|
| 98 | //     auto schema = parser->parseFromDirectory(*dir, fakePath, nullptr); | 
|---|
| 99 | // | 
|---|
| 100 | //   In this example, note that any imports in the file will fail, since the in-memory directory | 
|---|
| 101 | //   you created contains no files except the specific one you linked in. | 
|---|
| 102 |  | 
|---|
| 103 | ParsedSchema parseDiskFile(kj::StringPtr displayName, kj::StringPtr diskPath, | 
|---|
| 104 | kj::ArrayPtr<const kj::StringPtr> importPath) const | 
|---|
| 105 | CAPNP_DEPRECATED( "Use parseFromDirectory() instead."); | 
|---|
| 106 | // Creates a private kj::Filesystem and uses it to parse files from the real disk. | 
|---|
| 107 | // | 
|---|
| 108 | // DO NOT USE in new code. Use parseFromDirectory() instead. | 
|---|
| 109 | // | 
|---|
| 110 | // This API has a serious problem: the file can import and embed files located anywhere on disk | 
|---|
| 111 | // using relative paths. Even if you specify no `importPath`, relative imports still work. By | 
|---|
| 112 | // using `parseFromDirectory()`, you can arrange so that imports are only allowed within a | 
|---|
| 113 | // particular directory, or even set up a dummy filesystem where other files are not visible. | 
|---|
| 114 |  | 
|---|
| 115 | void setDiskFilesystem(kj::Filesystem& fs) | 
|---|
| 116 | CAPNP_DEPRECATED( "Use parseFromDirectory() instead."); | 
|---|
| 117 | // Call before calling parseDiskFile() to choose an alternative disk filesystem implementation. | 
|---|
| 118 | // This exists mostly for testing purposes; new code should use parseFromDirectory() instead. | 
|---|
| 119 | // | 
|---|
| 120 | // If parseDiskFile() is called without having called setDiskFilesystem(), then | 
|---|
| 121 | // kj::newDiskFilesystem() will be used instead. | 
|---|
| 122 |  | 
|---|
| 123 | ParsedSchema parseFile(kj::Own<SchemaFile>&& file) const; | 
|---|
| 124 | // Advanced interface for parsing a file that may or may not be located in any global namespace. | 
|---|
| 125 | // Most users will prefer `parseFromDirectory()`. | 
|---|
| 126 | // | 
|---|
| 127 | // If the file has already been parsed (that is, a SchemaFile that compares equal to this one | 
|---|
| 128 | // was parsed previously), the existing schema will be returned again. | 
|---|
| 129 | // | 
|---|
| 130 | // This method reports errors by calling SchemaFile::reportError() on the file where the error | 
|---|
| 131 | // is located.  If that call does not throw an exception, `parseFile()` may in fact return | 
|---|
| 132 | // normally.  In this case, the result is a best-effort attempt to compile the schema, but it | 
|---|
| 133 | // may be invalid or corrupt, and using it for anything may cause exceptions to be thrown. | 
|---|
| 134 |  | 
|---|
| 135 | kj::Maybe<schema::Node::SourceInfo::Reader> getSourceInfo(Schema schema) const; | 
|---|
| 136 | // Look up source info (e.g. doc comments) for the given schema, which must have come from this | 
|---|
| 137 | // SchemaParser. Note that this will also work for implicit group and param types that don't have | 
|---|
| 138 | // a type name hence don't have a `ParsedSchema`. | 
|---|
| 139 |  | 
|---|
| 140 | template <typename T> | 
|---|
| 141 | inline void loadCompiledTypeAndDependencies() { | 
|---|
| 142 | // See SchemaLoader::loadCompiledTypeAndDependencies(). | 
|---|
| 143 | getLoader().loadCompiledTypeAndDependencies<T>(); | 
|---|
| 144 | } | 
|---|
| 145 |  | 
|---|
| 146 | private: | 
|---|
| 147 | struct Impl; | 
|---|
| 148 | struct DiskFileCompat; | 
|---|
| 149 | class ModuleImpl; | 
|---|
| 150 | kj::Own<Impl> impl; | 
|---|
| 151 | mutable bool hadErrors = false; | 
|---|
| 152 |  | 
|---|
| 153 | ModuleImpl& getModuleImpl(kj::Own<SchemaFile>&& file) const; | 
|---|
| 154 | SchemaLoader& getLoader(); | 
|---|
| 155 |  | 
|---|
| 156 | friend class ParsedSchema; | 
|---|
| 157 | }; | 
|---|
| 158 |  | 
|---|
| 159 | class ParsedSchema: public Schema { | 
|---|
| 160 | // ParsedSchema is an extension of Schema which also has the ability to look up nested nodes | 
|---|
| 161 | // by name.  See `SchemaParser`. | 
|---|
| 162 |  | 
|---|
| 163 | public: | 
|---|
| 164 | inline ParsedSchema(): parser(nullptr) {} | 
|---|
| 165 |  | 
|---|
| 166 | kj::Maybe<ParsedSchema> findNested(kj::StringPtr name) const; | 
|---|
| 167 | // Gets the nested node with the given name, or returns null if there is no such nested | 
|---|
| 168 | // declaration. | 
|---|
| 169 |  | 
|---|
| 170 | ParsedSchema getNested(kj::StringPtr name) const; | 
|---|
| 171 | // Gets the nested node with the given name, or throws an exception if there is no such nested | 
|---|
| 172 | // declaration. | 
|---|
| 173 |  | 
|---|
| 174 | schema::Node::SourceInfo::Reader getSourceInfo() const; | 
|---|
| 175 | // Get the source info for this schema. | 
|---|
| 176 |  | 
|---|
| 177 | private: | 
|---|
| 178 | inline ParsedSchema(Schema inner, const SchemaParser& parser): Schema(inner), parser(&parser) {} | 
|---|
| 179 |  | 
|---|
| 180 | const SchemaParser* parser; | 
|---|
| 181 | friend class SchemaParser; | 
|---|
| 182 | }; | 
|---|
| 183 |  | 
|---|
| 184 | // ======================================================================================= | 
|---|
| 185 | // Advanced API | 
|---|
| 186 |  | 
|---|
| 187 | class SchemaFile { | 
|---|
| 188 | // Abstract interface representing a schema file.  You can implement this yourself in order to | 
|---|
| 189 | // gain more control over how the compiler resolves imports and reads files.  For the | 
|---|
| 190 | // common case of files on disk or other global filesystem-like namespaces, use | 
|---|
| 191 | // `SchemaFile::newDiskFile()`. | 
|---|
| 192 |  | 
|---|
| 193 | public: | 
|---|
| 194 | // Note: Cap'n Proto 0.6.x and below had classes FileReader and DiskFileReader and a method | 
|---|
| 195 | //   newDiskFile() defined here. These were removed when SchemaParser was transitioned to use the | 
|---|
| 196 | //   KJ filesystem API. You should be able to get the same effect by subclassing | 
|---|
| 197 | //   kj::ReadableDirectory, or using kj::newInMemoryDirectory(). | 
|---|
| 198 |  | 
|---|
| 199 | static kj::Own<SchemaFile> newFromDirectory( | 
|---|
| 200 | const kj::ReadableDirectory& baseDir, kj::Path path, | 
|---|
| 201 | kj::ArrayPtr<const kj::ReadableDirectory* const> importPath, | 
|---|
| 202 | kj::Maybe<kj::String> displayNameOverride = nullptr); | 
|---|
| 203 | // Construct a SchemaFile representing a file in a kj::ReadableDirectory. This is used to | 
|---|
| 204 | // implement SchemaParser::parseFromDirectory(); see there for details. | 
|---|
| 205 | // | 
|---|
| 206 | // The SchemaFile compares equal to any other SchemaFile that has exactly the same `baseDir` | 
|---|
| 207 | // object (by identity) and `path` (by value). | 
|---|
| 208 |  | 
|---|
| 209 | // ----------------------------------------------------------------- | 
|---|
| 210 | // For more control, you can implement this interface. | 
|---|
| 211 |  | 
|---|
| 212 | virtual kj::StringPtr getDisplayName() const = 0; | 
|---|
| 213 | // Get the file's name, as it should appear in the schema. | 
|---|
| 214 |  | 
|---|
| 215 | virtual kj::Array<const char> readContent() const = 0; | 
|---|
| 216 | // Read the file's entire content and return it as a byte array. | 
|---|
| 217 |  | 
|---|
| 218 | virtual kj::Maybe<kj::Own<SchemaFile>> import(kj::StringPtr path) const = 0; | 
|---|
| 219 | // Resolve an import, relative to this file. | 
|---|
| 220 | // | 
|---|
| 221 | // `path` is exactly what appears between quotes after the `import` keyword in the source code. | 
|---|
| 222 | // It is entirely up to the `SchemaFile` to decide how to map this to another file.  Typically, | 
|---|
| 223 | // a leading '/' means that the file is an "absolute" path and is searched for in some list of | 
|---|
| 224 | // schema file repositories.  On the other hand, a path that doesn't start with '/' is relative | 
|---|
| 225 | // to the importing file. | 
|---|
| 226 |  | 
|---|
| 227 | virtual bool operator==(const SchemaFile& other) const = 0; | 
|---|
| 228 | virtual bool operator!=(const SchemaFile& other) const = 0; | 
|---|
| 229 | virtual size_t hashCode() const = 0; | 
|---|
| 230 | // Compare two SchemaFiles to see if they refer to the same underlying file.  This is an | 
|---|
| 231 | // optimization used to avoid the need to re-parse a file to check its ID. | 
|---|
| 232 |  | 
|---|
| 233 | struct SourcePos { | 
|---|
| 234 | uint byte; | 
|---|
| 235 | uint line; | 
|---|
| 236 | uint column; | 
|---|
| 237 | }; | 
|---|
| 238 | virtual void reportError(SourcePos start, SourcePos end, kj::StringPtr message) const = 0; | 
|---|
| 239 | // Report that the file contains an error at the given interval. | 
|---|
| 240 |  | 
|---|
| 241 | private: | 
|---|
| 242 | class DiskSchemaFile; | 
|---|
| 243 | }; | 
|---|
| 244 |  | 
|---|
| 245 | }  // namespace capnp | 
|---|
| 246 |  | 
|---|