#include "../include/Conf.hpp" #include "../include/types.hpp" #include "../external/utf8/unchecked.h" using namespace std; using u8string = utf8::unchecked::iterator; namespace sibs { class UnexpectedTokenException : public std::runtime_error { public: UnexpectedTokenException(const string &errMsg) : runtime_error(errMsg) { } }; enum class Token { NONE, END_OF_FILE, IDENTIFIER, OPEN_BRACKET, CLOSING_BRACKET, EQUALS, STRING }; const char *getTokenName(Token token) { switch(token) { case Token::NONE: return "NONE"; case Token::END_OF_FILE: return ""; case Token::IDENTIFIER: return "identifier"; case Token::OPEN_BRACKET: return "["; case Token::CLOSING_BRACKET: return "]"; case Token::EQUALS: return "="; case Token::STRING: return "string"; default: return "Unknown"; } } class Tokenizer { public: Tokenizer(const char *_code) : currentToken(Token::NONE), code((char*)_code) { } Token nextToken() { u32 c = *code; while(isWhitespace(c)) { ++code; c = *code; } if(isAlpha(c) || c == '_') { char *startOfIdentifier = code.base(); ++code; c = *code; while(isAlpha(c) || isDigit(c) || c == '_' || c == '-') { ++code; c = *code; } char *endOfIdentifier = code.base(); identifier = StringView(startOfIdentifier, endOfIdentifier - startOfIdentifier); return Token::IDENTIFIER; } else if(c == '[') { ++code; return Token::OPEN_BRACKET; } else if(c == ']') { ++code; return Token::CLOSING_BRACKET; } else if(c == '=') { ++code; return Token::EQUALS; } else if(c == '"') { bool escapeQuote = false; ++code; char *startOfStr = code.base(); while(true) { c = *code; if(c == '"' && !escapeQuote) break; else if(c == '\\') escapeQuote = !escapeQuote; else if(c == '\0') throw UnexpectedTokenException("Reached end of file before string end"); else escapeQuote = false; ++code; } str = StringView(startOfStr, code.base() - startOfStr); ++code; return Token::STRING; } else if(c == '\0') { return Token::END_OF_FILE; } else { string errMsg = "Unexpected token: "; errMsg += (char)c; throw UnexpectedTokenException(errMsg); } } StringView getIdentifier() const { return identifier; } StringView getString() const { return str; } private: bool isWhitespace(u32 c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } bool isAlpha(u32 c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } bool isDigit(u32 c) { return c >= '0' && c <= '9'; } private: Token currentToken; u8string code; union { StringView identifier; StringView str; }; }; class Parser { public: static Result parse(const char *code, const ConfigCallback &callback) { try { Parser parser(code, (ConfigCallback*)&callback); parser.parse(); return Result::Ok(true); } catch (const UnexpectedTokenException &e) { return Result::Err(e.what()); } catch (const ParserException &e) { return Result::Err(e.what()); } } private: Parser(const char *code, ConfigCallback *_callback) : tokenizer(code), callback(_callback), objectDefined(false) { } void parse() { while(true) { Token token = tokenizer.nextToken(); switch(token) { case Token::IDENTIFIER: { parseConfigField(); break; } case Token::OPEN_BRACKET: { parseConfigObject(); break; } case Token::END_OF_FILE: { callback->finished(); return; } default: { string errMsg = "Expected identifier or object, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } } } void parseConfigField() { StringView fieldName = tokenizer.getIdentifier(); if (!objectDefined) { string errMsg = "An object has to be the first element defined in a config file"; throw ParserException(errMsg); } Token token = tokenizer.nextToken(); if(token == Token::EQUALS) { parseConfigFieldRhs(fieldName); } else { string errMsg = "Expected '=' after identifier, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } void parseConfigFieldRhs(StringView fieldName) { Token token = tokenizer.nextToken(); if(token == Token::STRING) { callback->processField(fieldName, tokenizer.getString()); } else if(token == Token::OPEN_BRACKET) { token = tokenizer.nextToken(); if(token == Token::STRING) { StringView str = tokenizer.getString(); token = tokenizer.nextToken(); if(token == Token::CLOSING_BRACKET) { vector values; values.push_back(str); callback->processField(fieldName, values); } else { string errMsg = "Expected ']' to close value list, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } else { string errMsg = "Expected string value inside list in field definition, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } else { string errMsg = "Expected string on right-hand side of field '"; errMsg += string(fieldName.data, fieldName.size); errMsg += "', got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } void parseConfigObject() { Token token = tokenizer.nextToken(); if(token == Token::IDENTIFIER) { StringView objectName = tokenizer.getIdentifier(); token = tokenizer.nextToken(); if(token == Token::CLOSING_BRACKET) { objectDefined = true; callback->processObject(objectName); } else { string errMsg = "Expected ']' after identifier to close object definition, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } else { string errMsg = "Expected identifier after '[', got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } private: Tokenizer tokenizer; ConfigCallback *callback; bool objectDefined; }; Result Config::readFromFile(const _tinydir_char_t *filepath, const ConfigCallback &callback) { Result fileContentResult = getFileContent(filepath); if(fileContentResult.isErr()) return Result::Err(fileContentResult.getErrMsg()); const char *code = fileContentResult.unwrap().data; if(!utf8::is_valid(code, code + fileContentResult.unwrap().size)) return Result::Err("File is not in valid utf8 format"); if(fileContentResult.unwrap().size >= 3 && utf8::is_bom(code)) code += 3; return Parser::parse(code, callback); } const char* asString(OptimizationLevel optLevel) { switch(optLevel) { case OPT_LEV_NONE: return "none"; case OPT_LEV_DEBUG: return "debug"; case OPT_LEV_RELEASE: return "release"; } } void SibsConfig::processObject(StringView name) { currentObject = name; //printf("Process object: %.*s\n", name.size, name.data); } void SibsConfig::processField(StringView name, const ConfigValue &value) { /* printf("Process field: %.*s, value: ", name.size, name.data); if(value.isSingle()) { printf("\"%.*s\"", value.asSingle().size, value.asSingle().data); } else { printf("["); int i = 0; for(auto listElement : value.asList()) { if(i > 0) printf(", "); printf("\"%.*s\"", listElement.size, listElement.data); ++i; } printf("]"); } printf("\n"); */ if(currentObject.equals("package")) { if(name.equals("name")) { if (value.isSingle()) packageName = string(value.asSingle().data, value.asSingle().size); else throw ParserException("Expected package.name to be a single value, was a list"); } else if(name.equals("type")) { if (value.isSingle()) { const StringView &packageTypeStr = value.asSingle(); if(packageTypeStr.equals("executable")) packageType = PackageType::EXECUTABLE; else if(packageTypeStr.equals("static")) packageType = PackageType::STATIC; else if(packageTypeStr.equals("dynamic")) packageType = PackageType::DYNAMIC; else if(packageTypeStr.equals("library")) packageType = PackageType::LIBRARY; else { string errMsg = "Expected package.type to be either 'executable', 'static', 'dynamic' or 'library', was: "; errMsg += string(packageTypeStr.data, packageTypeStr.size); throw ParserException(errMsg); } } else throw ParserException("Expected package.type to be a single value, was a list"); } else if(name.equals("tests")) { if (value.isSingle()) { testPath = projectPath; testPath += TINYDIR_STRING("/"); #if OS_FAMILY == OS_FAMILY_POSIX testPath += FileString(value.asSingle().data, value.asSingle().size); #else testPath += utf8To16(value.asSingle()); #endif Result testRealPathResult = getRealPath(testPath.c_str()); if(!testRealPathResult) { string errMsg = "Failed to resolve package.tests path: "; errMsg += testRealPathResult.getErrMsg(); throw ParserException(errMsg); } testPath = testRealPathResult.unwrap(); } else throw ParserException("Expected package.tests to be a single value, was a list"); } else if(name.equals("include_dirs")) { if(value.isList()) { // TODO: Checking for duplicate declaration should be done in the config parser if(!includeDirs.empty()) throw ParserException("Found duplicate declaration of package.include_dirs"); for(const StringView &includeDir : value.asList()) { includeDirs.emplace_back(string(includeDir.data, includeDir.size)); } } else throw ParserException("Expected package.include_dirs to be a list, was a single value"); } } else if(currentObject.equals("dependencies")) { if(value.isSingle()) { // TODO: Validate version is number in correct format Dependency dependency; dependency.name = string(name.data, name.size); dependency.version = string(value.asSingle().data, value.asSingle().size); dependencies.emplace_back(dependency); } else throw ParserException("Expected field under dependencies to be a single value, was a list"); } } void SibsConfig::finished() { if((int)packageType == -1) throw ParserException("Missing required config package.type. Expected to be one either 'executable', 'static', 'dynamic' or 'library'"); finishedProcessing = true; } void SibsTestConfig::processObject(StringView name) { currentObject = name; } void SibsTestConfig::processField(StringView name, const ConfigValue &value) { if(currentObject.equals("dependencies")) { if(value.isSingle()) { // TODO: Validate version is number in correct format Dependency dependency; dependency.name = string(name.data, name.size); dependency.version = string(value.asSingle().data, value.asSingle().size); dependencies.emplace_back(dependency); } else throw ParserException("Expected field under dependencies to be a single value, was a list"); } else { string errMsg = "project.conf: Expected category to be 'dependencies', was: '"; errMsg += string(currentObject.data, currentObject.size); errMsg += "'"; throw ParserException(errMsg); } } void SibsTestConfig::finished() { finishedProcessing = true; } }