// SPDX-License-Identifier: BSD-2-Clause // This code is part of the sfizz library and is licensed under a BSD 2-clause // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "Parser.h" #include "ParserPrivate.h" #include "absl/memory/memory.h" #include namespace sfz { Parser::Parser() { } Parser::~Parser() { } void Parser::reset() { _pathsIncluded.clear(); _currentDefinitions = _externalDefinitions; _currentHeader.reset(); _currentOpcodes.clear(); _errorCount = 0; _warningCount = 0; } void Parser::addExternalDefinition(absl::string_view id, absl::string_view value) { _externalDefinitions[id] = std::string(value); } void Parser::clearExternalDefinitions() { _externalDefinitions.clear(); } void Parser::parseFile(const fs::path& path) { parseVirtualFile(path, nullptr); } void Parser::parseString(const fs::path& path, absl::string_view sfzView) { parseVirtualFile(path, absl::make_unique(path, sfzView)); } void Parser::parseVirtualFile(const fs::path& path, std::unique_ptr reader) { reset(); if (_listener) _listener->onParseBegin(); includeNewFile(path, std::move(reader), {}); processTopLevel(); flushCurrentHeader(); if (_listener) _listener->onParseEnd(); } void Parser::includeNewFile(const fs::path& path, std::unique_ptr reader, const SourceRange& includeStmtRange) { fs::path fullPath = (path.empty() || path.is_absolute()) ? path : _originalDirectory / path; if (_pathsIncluded.empty()) _originalDirectory = fullPath.parent_path(); else if (_pathsIncluded.find(fullPath.string()) != _pathsIncluded.end()) { if (_recursiveIncludeGuardEnabled) return; } auto makeErrorRange = [&]() -> SourceRange { if (!includeStmtRange) { SourceLocation loc; loc.filePath = std::make_shared(fullPath); return {loc, loc}; } return includeStmtRange; }; if (_included.size() == _maxIncludeDepth) { emitError(makeErrorRange(), "Exceeded maximum include depth (" + std::to_string(_maxIncludeDepth) + ")"); return; } if (!reader) { auto fileReader = absl::make_unique(fullPath); if (fileReader->hasError()) { SourceLocation loc = fileReader->location(); emitError(makeErrorRange(), "Cannot open file for reading: " + fullPath.string()); return; } reader = std::move(fileReader); } _pathsIncluded.insert(fullPath.string()); _included.push_back(std::move(reader)); } void Parser::addDefinition(absl::string_view id, absl::string_view value) { _currentDefinitions[id] = std::string(value); } void Parser::processTopLevel() { while (!_included.empty()) { Reader& reader = *_included.back(); while (reader.skipChars(" \t\r\n") || skipComment()); switch (reader.peekChar()) { case Reader::kEof: _included.pop_back(); break; case '#': processDirective(); break; case '<': processHeader(); break; default: processOpcode(); break; } } } void Parser::processDirective() { Reader& reader = *_included.back(); SourceLocation start = reader.location(); if (reader.getChar() != '#') { SourceLocation end = reader.location(); emitError({ start, end }, "Expected `#` at start of directive."); recover(); return; } std::string directive; reader.extractWhile(&directive, isIdentifierChar); if (directive == "define") { reader.skipChars(" \t"); std::string id; if (!reader.extractExactChar('$') || !reader.extractWhile(&id, isIdentifierChar)) { SourceLocation end = reader.location(); emitError({ start, end }, "Expected $identifier after #define."); recover(); return; } reader.skipChars(" \t"); std::string value; extractToEol(reader, &value); #if 1 // ARIA/not Cakewalk: cut the value after the first word size_t position = value.find_first_of(" \t"); if (position != value.npos) { absl::string_view excess(&value[position], value.size() - position); reader.putBackChars(excess); value.resize(position); } #else trimRight(value); #endif addDefinition(id, value); } else if (directive == "include") { reader.skipChars(" \t"); std::string path; bool valid = false; SourceLocation valueStart; SourceLocation valueEnd; if (reader.extractExactChar('"')) { valueStart = reader.location(); reader.extractWhile(&path, [](char c) { return c != '"' && c != '\r' && c != '\n'; }); valueEnd = reader.location(); valid = reader.extractExactChar('"'); } SourceLocation end = reader.location(); if (!valid) { emitError({ start, end }, "Expected \"file.sfz\" after #include."); recover(); return; } path = expandDollarVars({ valueStart, valueEnd }, path); std::replace(path.begin(), path.end(), '\\', '/'); includeNewFile(path, nullptr, { start, end }); } else { SourceLocation end = reader.location(); emitError({ start, end }, "Unrecognized directive `" + directive + "`"); recover(); } } void Parser::processHeader() { Reader& reader = *_included.back(); SourceLocation start = reader.location(); if (reader.getChar() != '<') { SourceLocation end = reader.location(); emitError({ start, end }, "Expected `<` at start of header."); recover(); return; } std::string name; reader.extractWhile(&name, [](char c) { return c != '\r' && c != '\n' && c != '>'; }); if (reader.peekChar() != '>') { SourceLocation end = reader.location(); emitError({ start, end }, "Expected `>` at end of header."); recover(); return; } reader.getChar(); SourceLocation end = reader.location(); if (!isIdentifier(name)) { emitError({ start, end }, "The header name `" + name + "` is not a valid identifier."); recover(); return; } flushCurrentHeader(); _currentHeader = name; if (_listener) _listener->onParseHeader({ start, end }, name); } void Parser::processOpcode() { Reader& reader = *_included.back(); SourceLocation opcodeStart = reader.location(); auto isRawOpcodeNameChar = [](char c) { return isIdentifierChar(c) || c == '$'; }; std::string nameRaw; reader.extractWhile(&nameRaw, isRawOpcodeNameChar); SourceLocation opcodeEnd = reader.location(); if (nameRaw.empty()) { emitError({ opcodeStart, opcodeEnd }, "Expected opcode name."); recover(); return; } if (reader.peekChar() != '=') { emitError({ opcodeStart, opcodeEnd }, "Expected `=` after opcode name."); recover(); return; } std::string nameExpanded = expandDollarVars({ opcodeStart, opcodeEnd }, nameRaw); if (!isIdentifier(nameExpanded)) { emitError({ opcodeStart, opcodeEnd }, "The opcode name `" + nameExpanded + "` is not a valid identifier."); recover(); return; } reader.getChar(); SourceLocation valueStart = reader.location(); std::string valueRaw; extractToEol(reader, &valueRaw); size_t endPosition = 0; for (size_t valueSize = valueRaw.size(); endPosition < valueSize;) { size_t i = endPosition + 1; bool stop = false; // if a "<" character is next, a header follows if (valueRaw[endPosition] == '<') stop = true; // if space, check if the rest of the string is to consume or not else if (isSpaceChar(valueRaw[endPosition])) { // consume space characters following while (i < valueSize && isSpaceChar(valueRaw[i])) ++i; // if there aren't non-space characters following, do not extract if (i == valueSize) stop = true; // if a "<" or "#" character is next, a header or a directive follows else if (valueRaw[i] == '<' || valueRaw[i] == '#') stop = true; // if sequence of identifier chars and then "=", an opcode follows else if (isIdentifierChar(valueRaw[i])) { ++i; while (i < valueSize && (isIdentifierChar(valueRaw[i]) || valueRaw[i] == '$')) ++i; if (i < valueSize && valueRaw[i] == '=') stop = true; } } if (stop) break; endPosition = i; } if (endPosition != valueRaw.size()) { absl::string_view excess(&valueRaw[endPosition], valueRaw.size() - endPosition); reader.putBackChars(excess); valueRaw.resize(endPosition); } SourceLocation valueEnd = reader.location(); if (!_currentHeader) emitWarning({ opcodeStart, valueEnd }, "The opcode is not under any header."); std::string valueExpanded = expandDollarVars({ valueStart, valueEnd }, valueRaw); _currentOpcodes.emplace_back(nameExpanded, valueExpanded); if (_listener) _listener->onParseOpcode({ opcodeStart, opcodeEnd }, { valueStart, valueEnd }, nameExpanded, valueExpanded); } void Parser::emitError(const SourceRange& range, const std::string& message) { ++_errorCount; if (_listener) _listener->onParseError(range, message); } void Parser::emitWarning(const SourceRange& range, const std::string& message) { ++_warningCount; if (_listener) _listener->onParseWarning(range, message); } void Parser::recover() { Reader& reader = *_included.back(); // skip the current line and let the parser proceed at the next reader.skipWhile([](char c) { return c != '\n'; }); } void Parser::flushCurrentHeader() { if (_currentHeader) { if (_listener) _listener->onParseFullBlock(*_currentHeader, _currentOpcodes); _currentHeader.reset(); } _currentOpcodes.clear(); } Parser::CommentType Parser::getCommentType(Reader& reader) { if (reader.peekChar() != '/') return CommentType::None; reader.getChar(); CommentType ret = CommentType::None; switch (reader.peekChar()) { case '/': ret = CommentType::Line; break; case '*': ret = CommentType::Block; break; } reader.putBackChar('/'); return ret; } size_t Parser::skipComment() { Reader& reader = *_included.back(); const CommentType commentType = getCommentType(reader); if (commentType == CommentType::None) return 0; SourceLocation start = reader.location(); size_t count = 2; reader.getChar(); reader.getChar(); bool terminated = false; switch (commentType) { case CommentType::Line: while (!terminated) { int c = reader.getChar(); count += (c != Reader::kEof); terminated = c == Reader::kEof || c == '\r' || c == '\n'; } break; case CommentType::Block: { int c1 = 0; int c2 = reader.getChar(); count += (c2 != Reader::kEof); while (!terminated && c2 != Reader::kEof) { c1 = c2; c2 = reader.getChar(); count += (c2 != Reader::kEof); terminated = c1 == '*' && c2 == '/'; } } break; default: assert(false); break; } if (!terminated) { SourceLocation end = reader.location(); emitError({ start, end }, "Unterminated block comment."); } return count; } void Parser::trimRight(std::string& text) { while (!text.empty() && isSpaceChar(text.back())) text.pop_back(); } size_t Parser::extractToEol(Reader& reader, std::string* dst) { return reader.extractWhile(dst, [&reader](char c) { if (c == '\r' || c == '\n') return false; if (c == '/') { int c2 = reader.peekChar(); if (c2 == '/' || c2 == '*') // stop at comment return false; } return true; }); } std::string Parser::expandDollarVars(const SourceRange& range, absl::string_view src) { std::string dst; std::string srcbuf; // temporary for retries when recursive std::string name; // temporary for variable name bool keepExpanding = true; dst.reserve(2 * src.size()); name.reserve(64); while (keepExpanding) { size_t i = 0; size_t n = src.size(); size_t numExpansions = 0; while (i < n) { char c = src[i++]; if (c != '$') dst.push_back(c); else { ++numExpansions; name.clear(); // ARIA: we will accumulate any chars after $, until this is the // name of a known variable auto def = _currentDefinitions.end(); while (i < n && isIdentifierChar(src[i]) && def == _currentDefinitions.end()) { name.push_back(src[i++]); def = _currentDefinitions.find(name); } if (name.empty()) { emitWarning(range, "Expected variable name after $."); continue; } if (def == _currentDefinitions.end()) { emitWarning(range, "The variable `" + name + "` is not defined."); continue; } dst.append(def->second); } } keepExpanding = numExpansions > 0; if (keepExpanding) { srcbuf = dst; src = srcbuf; dst.clear(); } } return dst; } bool Parser::isIdentifierChar(char c) { return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); } bool Parser::isSpaceChar(char c) { return c == ' ' || c == '\t'; } bool Parser::isIdentifier(absl::string_view s) { if (s.empty()) return false; for (char c : s) { if (!isIdentifierChar(c)) return false; } return true; } } // namespace sfz