diff --git a/src/OpenFOAM/db/IOstreams/Sstreams/ISstream.C b/src/OpenFOAM/db/IOstreams/Sstreams/ISstream.C index 10969f20f2..bdb22307ae 100644 --- a/src/OpenFOAM/db/IOstreams/Sstreams/ISstream.C +++ b/src/OpenFOAM/db/IOstreams/Sstreams/ISstream.C @@ -30,6 +30,7 @@ License #include "int.H" #include "token.H" #include +#include // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * // @@ -42,14 +43,14 @@ namespace { // Convert a single character to a word with length 1 -inline static Foam::word charToWord(char c) +inline Foam::word charToWord(char c) { return Foam::word(std::string(1, c), false); } // Permit slash-scoping of entries -static inline bool validVariableChar(char c) +inline bool validVariableChar(char c) { return (Foam::word::valid(c) || c == '/'); } @@ -59,23 +60,51 @@ static inline bool validVariableChar(char c) // * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * * // +bool Foam::ISstream::seekCommentEnd_Cstyle() +{ + // Search for end of C-style comment - "*/" + + // Can use getLine(nullptr, '*') in the logic, + // but written out looks less obscure + + char c = 0; + bool star = false; + + while (get(c)) + { + if (c == '*') + { + star = true; + } + else if (star) + { + star = false; + if (c == '/') + { + // Matched "*/" + return true; + } + } + } + + // Exhausted stream without finding "*/" sequence + return false; +} + + char Foam::ISstream::nextValid() { char c = 0; - while (true) + // Get next non-whitespace character + while (get(c)) { - // Get next non-whitespace character - while (get(c) && isspace(c)) - {} - - // Return if stream is bad - ie, previous get() failed - if (bad() || isspace(c)) + if (isspace(c)) { - return 0; + continue; } - // Is this the start of a C/C++ comment? + // Check if this starts a C/C++ comment if (c == '/') { if (!get(c)) @@ -86,37 +115,15 @@ char Foam::ISstream::nextValid() if (c == '/') { - // C++ style single-line comment - skip through past end-of-line - while (get(c) && c != '\n') - {} + // C++ comment: discard through newline + (void) getLine(nullptr, '\n'); } else if (c == '*') { - // Within a C-style comment - while (true) + // C-style comment: discard through to "*/" ending + if (!seekCommentEnd_Cstyle()) { - // Search for end of C-style comment - '*/' - if (get(c) && c == '*') - { - if (get(c)) - { - if (c == '/') - { - // matched '*/' - break; - } - else if (c == '*') - { - // check again - putback(c); - } - } - } - - if (!good()) - { - return 0; - } + return 0; } } else @@ -137,28 +144,261 @@ char Foam::ISstream::nextValid() } -void Foam::ISstream::readWordToken(token& t) +// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * // + +namespace Foam { - word val; - if (read(val).bad()) + +// Read a verbatim string (excluding block delimiters), +// continuing until a closing "#}" has been found. +// +// The leading "#{" removed from stream prior to calling. +static ISstream& readVerbatim +( + ISstream& is, + std::string& str +) +{ + constexpr const unsigned bufLen = 8000; + static char buf[bufLen]; + + unsigned nChar = 0; + char c; + + str.clear(); + while (is.get(c)) { - t.setBad(); - } - else if (token::compound::isCompound(val)) - { - t = token::compound::New(val, *this).ptr(); - } - else - { - t = std::move(val); // Move contents to token + if (c == token::HASH) + { + char nextC; + is.get(nextC); + if (nextC == token::END_BLOCK) + { + // Found closing "#}" sequence + str.append(buf, nChar); + return is; + } + else + { + // Re-analyze the character + is.putback(nextC); + } + } + + buf[nChar++] = c; + if (nChar == bufLen) // Flush full buffer + { + str.append(buf, nChar); + nChar = 0; + } } + + + // Abnormal exit of the loop + str.append(buf, nChar); // Finalize pending content + strncpy(buf, str.c_str(), errLen); + buf[errLen] = '\0'; + + FatalIOErrorInFunction(is) + << "Problem while reading verbatim \"" << buf + << "...\" [after " << str.length() << " chars]\n" + << exit(FatalIOError); + + return is; } +// Read a variable or expression. +// Handles "$var" and "${var}" forms, permits '/' scoping character. +// Also handles "${{expr}}". +// +// Return the token type or ERROR +// +// The leading "${" or "$c" removed from stream prior to calling. +static token::tokenType readVariable +( + ISstream& is, + std::string& str, + char c // Next character after '$' +) +{ + constexpr const unsigned bufLen = 1024; + static char buf[bufLen]; + + token::tokenType tokType(token::tokenType::VARIABLE); + + // The first two characters are known: + buf[0] = token::DOLLAR; + buf[1] = c; + + unsigned nChar = 2; // Starts with two characters + unsigned depth = 0; // Depth of {..} nesting + + str.clear(); + if (c == token::BEGIN_BLOCK) + { + // Processing '${variable}' or '${{expr}}' + ++depth; + + int lookahead = is.peek(); + if (lookahead == token::BEGIN_BLOCK) + { + // Looks like '${{expr...' + tokType = token::tokenType::EXPRESSION; + } + else if (lookahead == token::END_BLOCK) + { + // Looks like '${}' + IOWarningInFunction(is) + << "Ignoring empty ${}" << endl; + return token::tokenType::ERROR; + } + + while (is.get(c)) + { + buf[nChar++] = c; + + if (c == token::BEGIN_BLOCK) + { + ++depth; + } + else if (c == token::END_BLOCK) + { + --depth; + if (!depth) + { + // Found closing '}' character + str.append(buf, nChar); + return tokType; + } + } + else if (c == '/' && tokType == token::tokenType::EXPRESSION) + { + // Strip C/C++ comments from expressions + // Note: could also peek instead of get/putback + + if (!is.get(c)) + { + break; // Premature end of stream + } + else if (c == '/') + { + --nChar; // Remove initial '/' from buffer + + // C++ comment: discard through newline + (void) is.getLine(nullptr, '\n'); + } + else if (c == '*') + { + --nChar; // Remove initial '/' from buffer + + // C-style comment: seek "*/" ending + if (!is.seekCommentEnd_Cstyle()) + { + break; // Premature end of stream + } + } + else + { + // Re-analyze the character + is.putback(c); + } + } + + if (nChar == bufLen) // Flush full buffer + { + str.append(buf, nChar); + nChar = 0; + } + } + + + // Abnormal exit of the loop + + str.append(buf, nChar); // Finalize pending content + strncpy(buf, str.c_str(), errLen); + buf[errLen] = '\0'; + + FatalIOErrorInFunction(is) + << "stream terminated while reading variable '" << buf + << "...' [after " << str.length() << " chars]\n" + << exit(FatalIOError); + + return token::tokenType::ERROR; + } + else if (validVariableChar(c)) + { + // Processing '$variable' + + while (is.get(c)) + { + if (!validVariableChar(c)) + { + is.putback(c); + break; + } + + if (c == token::BEGIN_LIST) + { + ++depth; + } + else if (c == token::END_LIST) + { + if (!depth) + { + // Closed ')' without opening '(': + // - don't consider it part of our input + is.putback(c); + break; + } + --depth; + } + + buf[nChar++] = c; + if (nChar == bufLen) // Flush full buffer + { + str.append(buf, nChar); + nChar = 0; + } + } + + str.append(buf, nChar); // Finalize pending content + + if (depth) + { + strncpy(buf, str.c_str(), errLen); + buf[errLen] = '\0'; + + IOWarningInFunction(is) + << "Missing " << depth + << " closing ')' while parsing" << nl << nl + << buf << endl; + } + + return tokType; + } + else + { + // Invalid character. Terminate string (for message) + + buf[nChar--] = '\0'; + + IOWarningInFunction(is) + << "Ignoring bad variable name: " << buf << nl << endl; + } + + return token::tokenType::ERROR; +} + +} // End namespace Foam + + +// * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * * // + Foam::Istream& Foam::ISstream::read(token& t) { - constexpr const unsigned maxLen = 128; // Max length for labels/scalars - static char buf[maxLen]; + constexpr const unsigned bufLen = 128; // Max length for labels/scalars + static char buf[bufLen]; // Return the put back token if it exists if (Istream::getBack(t)) @@ -209,7 +449,7 @@ Foam::Istream& Foam::ISstream::read(token& t) } // String: enclosed by double quotes. - case token::BEGIN_STRING : + case token::DQUOTE : { putback(c); @@ -226,21 +466,21 @@ Foam::Istream& Foam::ISstream::read(token& t) return *this; } - // Possible verbatim string or dictionary functionEntry + // Verbatim string '#{ .. #}' or dictionary '#directive' case token::HASH : { char nextC; - if (read(nextC).bad()) - { - // Return lone '#' as word - t = charToWord(c); - } - else if (nextC == token::BEGIN_BLOCK) + int lookahead = peek(); + + if (lookahead == token::BEGIN_BLOCK) { // Verbatim string: #{ ... #} + // Token stored without the surrounding delimiters + + (void) get(nextC); // Discard '{' lookahead string val; - if (readVerbatim(val).bad()) + if (readVerbatim(*this, val).bad()) { t.setBad(); } @@ -250,9 +490,14 @@ Foam::Istream& Foam::ISstream::read(token& t) t.setType(token::tokenType::VERBATIM); } } - else + else if (read(nextC).bad()) { - // Word beginning with '#'. Eg, "#include" + // Return lone '#' as word + t = charToWord(c); + } + else if (word::valid(nextC)) + { + // Directive (wordToken) beginning with '#'. Eg, "#include" // Put back both so that '#...' is included in the directive putback(nextC); @@ -269,34 +514,44 @@ Foam::Istream& Foam::ISstream::read(token& t) t.setType(token::tokenType::DIRECTIVE); } } + else + { + // '#' followed by non-word. Just ignore leading '#'? + putback(nextC); + + IOWarningInFunction(*this) + << "Invalid sequence #" << char(nextC) + << " ... ignoring the leading '#'" << nl << endl; + } return *this; } - // Dictionary variable (as rvalue) + // Dictionary variable or ${{ expression }} case token::DOLLAR : { char nextC; if (read(nextC).bad()) { - // Return lone '$' as word + // Return lone '$' as word. Could also ignore t = charToWord(c); } else { - // Put back both so that '$...' is included in the variable - putback(nextC); - putback(c); + // NB: the parser is slightly generous here. + // It will also accept '$ {' as input. + // - to be revisited (2021-05-17) string val; - if (readVariable(val).bad()) + token::tokenType tokType = readVariable(*this, val, nextC); + if (tokType == token::tokenType::ERROR) { t.setBad(); } else { t = std::move(val); // Move contents to token - t.setType(token::tokenType::VARIABLE); + t.setType(tokType); } } @@ -340,14 +595,14 @@ Foam::Istream& Foam::ISstream::read(token& t) } buf[nChar++] = c; - if (nChar == maxLen) + if (nChar == bufLen) { // Runaway argument - avoid buffer overflow - buf[maxLen-1] = '\0'; + buf[bufLen-1] = '\0'; FatalIOErrorInFunction(*this) - << "number '" << buf << "...'\n" - << " is too long (max. " << maxLen << " characters)" + << "Number '" << buf << "...'\n" + << " is too long (max. " << bufLen << " characters)" << exit(FatalIOError); t.setBad(); @@ -397,7 +652,20 @@ Foam::Istream& Foam::ISstream::read(token& t) default: { putback(c); - readWordToken(t); + + word val; + if (read(val).bad()) + { + t.setBad(); + } + else if (token::compound::isCompound(val)) + { + t = token::compound::New(val, *this).ptr(); + } + else + { + t = std::move(val); // Move contents to token + } return *this; } @@ -414,20 +682,22 @@ Foam::Istream& Foam::ISstream::read(char& c) Foam::Istream& Foam::ISstream::read(word& str) { - constexpr const unsigned maxLen = 1024; - static char buf[maxLen]; + constexpr const unsigned bufLen = 1024; + static char buf[bufLen]; unsigned nChar = 0; - unsigned depth = 0; // Track depth of (..) nesting + unsigned depth = 0; // Depth of (..) nesting char c; - while - ( - (nChar < maxLen) - && get(c) - && word::valid(c) - ) + str.clear(); + while (get(c)) { + if (!word::valid(c)) + { + putback(c); + break; + } + if (c == token::BEGIN_LIST) { ++depth; @@ -436,42 +706,40 @@ Foam::Istream& Foam::ISstream::read(word& str) { if (!depth) { - break; // Closed ')' without an opening '(' ? ... stop + // Closed ')' without opening '(': + // - don't consider it part of our input + putback(c); + break; } --depth; } buf[nChar++] = c; + if (nChar == bufLen) // Flush full buffer + { + str.append(buf, nChar); + nChar = 0; + } } - if (nChar >= maxLen) - { - buf[errLen] = '\0'; - - FatalIOErrorInFunction(*this) - << "word '" << buf << "...'\n" - << " is too long (max. " << maxLen << " characters)" - << exit(FatalIOError); - - return *this; - } - - buf[nChar] = '\0'; // Terminate string + str.append(buf, nChar); // Finalize pending content if (bad()) { // Could probably skip this check + + strncpy(buf, str.c_str(), errLen); buf[errLen] = '\0'; FatalIOErrorInFunction(*this) - << "Problem while reading word '" << buf << "...' after " - << nChar << " characters\n" + << "Problem while reading word '" << buf + << "...' [after " << str.length() << " chars]\n" << exit(FatalIOError); return *this; } - if (nChar == 0) + if (str.empty()) { FatalIOErrorInFunction(*this) << "Invalid first character found : " << c @@ -479,25 +747,25 @@ Foam::Istream& Foam::ISstream::read(word& str) } else if (depth) { + strncpy(buf, str.c_str(), errLen); + buf[errLen] = '\0'; + IOWarningInFunction(*this) << "Missing " << depth << " closing ')' while parsing" << nl << nl << buf << nl << endl; } - // Finalize: content already validated, assign without additional checks. - str.assign(buf, nChar); - putback(c); - return *this; } Foam::Istream& Foam::ISstream::read(string& str) { - constexpr const unsigned maxLen = 1024; - static char buf[maxLen]; + constexpr const unsigned bufLen = 1024; + static char buf[bufLen]; + unsigned nChar = 0; char c; if (!get(c)) @@ -510,7 +778,7 @@ Foam::Istream& Foam::ISstream::read(string& str) } // Note, we could also handle single-quoted strings here (if desired) - if (c != token::BEGIN_STRING) + if (c != token::DQUOTE) { FatalIOErrorInFunction(*this) << "Incorrect start of string character found : " << c @@ -519,26 +787,25 @@ Foam::Istream& Foam::ISstream::read(string& str) return *this; } - unsigned nChar = 0; + str.clear(); bool escaped = false; - - while - ( - (nChar < maxLen) - && get(c) - ) + while (get(c)) { - if (c == token::END_STRING) + if (c == '\\') + { + escaped = !escaped; // Toggle state (retains backslashes) + } + else if (c == token::DQUOTE) { if (escaped) { escaped = false; - --nChar; // Overwrite backslash + --nChar; // Overwrite backslash } else { // Done reading - str.assign(buf, nChar); + str.append(buf, nChar); return *this; } } @@ -547,253 +814,44 @@ Foam::Istream& Foam::ISstream::read(string& str) if (escaped) { escaped = false; - --nChar; // Overwrite backslash + --nChar; // Overwrite backslash } else { - buf[errLen] = buf[nChar] = '\0'; + str.append(buf, nChar); // Finalize pending content + strncpy(buf, str.c_str(), errLen); + buf[errLen] = '\0'; FatalIOErrorInFunction(*this) - << "found '\\n' while reading string \"" - << buf << "...\"" + << "Unescaped '\\n' while reading string \"" << buf + << "...\" [after " << str.length() << " chars]\n" << exit(FatalIOError); return *this; } } - else if (c == '\\') - { - escaped = !escaped; // toggle state (retains backslashes) - } else { escaped = false; } buf[nChar++] = c; + if (nChar == bufLen) // Flush full buffer + { + // Keep lookback character (eg, for backslash escaping) + str.append(buf, nChar-1); + nChar = 1; + buf[0] = c; + } } - if (nChar >= maxLen) - { - buf[errLen] = '\0'; - - FatalIOErrorInFunction(*this) - << "string \"" << buf << "...\"\n" - << " is too long (max. " << maxLen << " characters)" - << exit(FatalIOError); - - return *this; - } + // Abnormal exit of the loop // Don't worry about a dangling backslash if string terminated prematurely - buf[errLen] = buf[nChar] = '\0'; - FatalIOErrorInFunction(*this) - << "Problem while reading string \"" << buf << "...\"" - << exit(FatalIOError); - - return *this; -} - - -Foam::Istream& Foam::ISstream::readVariable(std::string& str) -{ - constexpr const unsigned maxLen = 1024; - static char buf[maxLen]; - - unsigned nChar = 0; - unsigned depth = 0; // Track depth of (..) or {..} nesting - char c; - - // First character must be '$' - if (!get(c) || c != token::DOLLAR) - { - FatalIOErrorInFunction(*this) - << "Invalid first character found : " << c << nl - << exit(FatalIOError); - } - buf[nChar++] = c; - - // Next character should also exist. - // This should never fail, since it was checked before calling. - if (!get(c)) - { - str.assign(buf, nChar); - - IOWarningInFunction(*this) - << "Truncated variable name : " << str << nl; - - return *this; - } - buf[nChar++] = c; - - str.clear(); - if (c == token::BEGIN_BLOCK) - { - // Processing ${...} style. - ++depth; - - // Could check that the next char is good and not one of '{}' - // since this would indicate "${}", "${{..." or truncated "${" - - while (get(c)) - { - buf[nChar++] = c; - if (nChar == maxLen) - { - str.append(buf, nChar); - nChar = 0; - } - if (c == token::BEGIN_BLOCK) - { - ++depth; - } - else if (c == token::END_BLOCK) - { - --depth; - if (!depth) - { - // Found closing '}' character - str.append(buf, nChar); - return *this; - } - } - } - - // Should never reach here on normal input - - str.append(buf, nChar); // Finalize pending buffer input - - nChar = str.length(); - if (str.length() > errLen) - { - str.erase(errLen); - } - - FatalIOErrorInFunction(*this) - << "stream terminated while reading variable '" - << str.c_str() << "...' [" << nChar << "]\n" - << exit(FatalIOError); - - return *this; - } - else if (validVariableChar(c)) - { - // Processing $var style - - while - ( - (nChar < maxLen) && get(c) - && (validVariableChar(c)) - ) - { - if (c == token::BEGIN_LIST) - { - ++depth; - } - else if (c == token::END_LIST) - { - if (!depth) - { - break; // Closed ')' without an opening '(' ? ... stop - } - --depth; - } - - buf[nChar++] = c; - } - } - else - { - // Invalid character. Terminate string (for message) without - // including the invalid character in the count. - - buf[nChar--] = '\0'; - - IOWarningInFunction(*this) - << "Bad variable name: " << buf << nl << endl; - } - - if (nChar >= maxLen) - { - buf[errLen] = '\0'; - - FatalIOErrorInFunction(*this) - << "variable '" << buf << "...'\n" - << " is too long (max. " << maxLen << " characters)" - << exit(FatalIOError); - - return *this; - } - - buf[nChar] = '\0'; // Terminate string - - if (bad()) - { - // Could probably skip this check - buf[errLen] = '\0'; - - FatalIOErrorInFunction(*this) - << "Problem while reading variable '" << buf << "...' after " - << nChar << " characters\n" - << exit(FatalIOError); - - return *this; - } - - if (depth) - { - IOWarningInFunction(*this) - << "Missing " << depth - << " closing ')' while parsing" << nl << nl - << buf << nl << endl; - } - - // Finalize - str.assign(buf, nChar); - putback(c); - - return *this; -} - - -Foam::Istream& Foam::ISstream::readVerbatim(std::string& str) -{ - constexpr const unsigned maxLen = 8000; - static char buf[maxLen]; - - unsigned nChar = 0; - char c; - - str.clear(); - while (get(c)) - { - if (c == token::HASH) - { - char nextC; - get(nextC); - if (nextC == token::END_BLOCK) - { - // Found closing "#}" sequence - str.append(buf, nChar); - return *this; - } - else - { - putback(nextC); - } - } - - buf[nChar++] = c; - if (nChar == maxLen) - { - str.append(buf, nChar); - nChar = 0; - } - } - - // Truncated terminated prematurely - buf[errLen] = buf[nChar] = '\0'; + str.append(buf, nChar); // Finalize pending content + strncpy(buf, str.c_str(), errLen); + buf[errLen] = '\0'; FatalIOErrorInFunction(*this) << "Problem while reading string \"" << buf << "...\"" diff --git a/src/OpenFOAM/db/IOstreams/Sstreams/ISstream.H b/src/OpenFOAM/db/IOstreams/Sstreams/ISstream.H index 14f264595b..db8bba40fa 100644 --- a/src/OpenFOAM/db/IOstreams/Sstreams/ISstream.H +++ b/src/OpenFOAM/db/IOstreams/Sstreams/ISstream.H @@ -69,18 +69,6 @@ class ISstream //- Get the next valid character char nextValid(); - //- Read a word token - void readWordToken(token& t); - - //- Read a verbatim string (excluding block delimiters). - // The leading "#{" has been removed prior to calling, - // continues until the closing "#}" has been found. - Istream& readVerbatim(std::string& str); - - //- Read a variable name starting with '$'. - // Handles "$var" and "${var}" forms, permits '/' scoping character. - Istream& readVariable(std::string& str); - //- No copy assignment void operator=(const ISstream&) = delete; @@ -137,6 +125,13 @@ public: virtual ios_base::fmtflags flags() const; + // Special-purpose Functions + + //- Discard until end of C-style comment '*/' + // \return False if stream exhausted before finding the comment end + bool seekCommentEnd_Cstyle(); + + // Read Functions //- Raw, low-level get character function. diff --git a/src/OpenFOAM/db/IOstreams/Sstreams/OSstream.C b/src/OpenFOAM/db/IOstreams/Sstreams/OSstream.C index eea67551a8..78644b4e89 100644 --- a/src/OpenFOAM/db/IOstreams/Sstreams/OSstream.C +++ b/src/OpenFOAM/db/IOstreams/Sstreams/OSstream.C @@ -133,7 +133,7 @@ Foam::Ostream& Foam::OSstream::writeQuoted // Output with surrounding quotes and backslash escaping - os_ << token::BEGIN_STRING; + os_ << token::DQUOTE; unsigned backslash = 0; for (auto iter = str.cbegin(); iter != str.cend(); ++iter) @@ -150,7 +150,7 @@ Foam::Ostream& Foam::OSstream::writeQuoted ++lineNumber_; ++backslash; // backslash escape for newline } - else if (c == token::END_STRING) + else if (c == token::DQUOTE) { ++backslash; // backslash escape for quote } @@ -167,7 +167,7 @@ Foam::Ostream& Foam::OSstream::writeQuoted // silently drop any trailing backslashes // they would otherwise appear like an escaped end-quote - os_ << token::END_STRING; + os_ << token::DQUOTE; setState(os_.rdstate()); return *this; diff --git a/src/OpenFOAM/db/dictionary/functionEntries/evalEntry/evalEntry.C b/src/OpenFOAM/db/dictionary/functionEntries/evalEntry/evalEntry.C index 6673631339..efce884ea6 100644 --- a/src/OpenFOAM/db/dictionary/functionEntries/evalEntry/evalEntry.C +++ b/src/OpenFOAM/db/dictionary/functionEntries/evalEntry/evalEntry.C @@ -56,60 +56,163 @@ namespace functionEntries } // End namespace Foam -// * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * // +// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * // + +namespace +{ + // This is akin to a SafeIOWarning, which does not yet exist + inline void safeIOWarning + ( + const Foam::IOstream& is, + const std::string& msg + ) + { + std::cerr + << "--> FOAM Warning :\n" + << " Reading \"" << is.name() << "\" at line " + << is.lineNumber() << '\n' + << " " << msg << std::endl; + } + +} // End anonymous namespace + + +namespace Foam +{ + +// Slurp a string until a closing '}' is found. +// Track balanced bracket/brace pairs, with max stack depth of 60. +static bool slurpUntilBalancedBrace(ISstream& is, std::string& str) +{ + constexpr const unsigned bufLen = 1024; + static char buf[bufLen]; + + is.fatalCheck(FUNCTION_NAME); + + unsigned nChar = 0; + unsigned depth = 1; // Initial '{' already seen by caller + char c; + + str.clear(); + while (is.get(c)) + { + buf[nChar++] = c; + + if (c == token::BEGIN_BLOCK) + { + ++depth; + } + else if (c == token::END_BLOCK) + { + --depth; + if (!depth) + { + // Closing '}' character - do not include in output + --nChar; + str.append(buf, nChar); + return true; + } + } + else if (c == '/') + { + // Strip C/C++ comments from expressions + // Note: could also peek instead of get/putback + + if (!is.get(c)) + { + break; // Premature end of stream + } + else if (c == '/') + { + --nChar; // Remove initial '/' from buffer + + // C++ comment: discard through newline + (void) is.getLine(nullptr, '\n'); + } + else if (c == '*') + { + --nChar; // Remove initial '/' from buffer + + // C-style comment: discard through to "*/" ending + if (!is.seekCommentEnd_Cstyle()) + { + break; // Premature end of stream + } + } + else + { + // Reanalyze the char + is.putback(c); + } + } + + if (nChar == bufLen) + { + str.append(buf, nChar); // Flush full buffer + nChar = 0; + } + } + + + // Abnormal exit of the loop + + str.append(buf, nChar); // Finalize pending content + + safeIOWarning(is, "Premature end while reading expression - missing '}'?"); + + is.fatalCheck(FUNCTION_NAME); + return false; +} + +} // End namespace Foam + + +// * * * * * * * * * * * * * Static Member Functions * * * * * * * * * * * * // Foam::tokenList Foam::functionEntries::evalEntry::evaluate ( const dictionary& parentDict, - Istream& is + const string& inputExpr, + label fieldWidth, + const Istream& is ) { - #ifdef FULLDEBUG - DetailInfo - << "Using #eval - line " - << is.lineNumber() << " in file " << parentDict.name() << nl; - #endif - - token tok(is); - label fieldWidth(1); // Field width for the result - if (tok.isLabel()) - { - // - #eval INT "expr" - // - #eval INT { expr } - // - #eval INT #{ expr #} - fieldWidth = max(1, tok.labelToken()); - is >> tok; - } - - string s; // String to evaluate - if (tok.isString()) - { - // - #eval "expr" - // - #eval #{ expr #} - s = tok.stringToken(); - } - else if (tok.isPunctuation(token::BEGIN_BLOCK)) - { - // - #eval { expr } - dynamic_cast(is).getLine(s, token::END_BLOCK); - } - else + // Field width for the result + if (fieldWidth < 1) { FatalIOErrorInFunction(is) - << "Invalid input for #eval." - " Expecting a string or block to evaluate, but found" << nl - << tok.info() << endl + << "Invalid field width: " << fieldWidth << nl << endl << exit(FatalIOError); } #ifdef FULLDEBUG DetailInfo - << "input: " << s << endl; + << "input: " << inputExpr << endl; #endif // Expand with env=true, empty=true, subDict=false // with comments stripped. // Special handling of $[...] syntax enabled. + + string s; + + // Passed '${{ expr }}' by accident, or on purpuse + if + ( + inputExpr[0] == token::DOLLAR + && inputExpr[1] == token::BEGIN_BLOCK + && inputExpr[2] == token::BEGIN_BLOCK + && inputExpr[inputExpr.length()-1] == token::END_BLOCK + && inputExpr[inputExpr.length()-2] == token::END_BLOCK + ) + { + s.assign(inputExpr, 3, inputExpr.length()-5); + } + else + { + s.assign(inputExpr); + } + expressions::exprString::inplaceExpand(s, parentDict, true); stringOps::inplaceTrim(s); @@ -184,6 +287,60 @@ Foam::tokenList Foam::functionEntries::evalEntry::evaluate } +Foam::tokenList Foam::functionEntries::evalEntry::evaluate +( + const dictionary& parentDict, + Istream& is +) +{ + #ifdef FULLDEBUG + DetailInfo + << "Using #eval - line " + << is.lineNumber() << " in file " << parentDict.name() << nl; + #endif + + token tok(is); + label fieldWidth(1); // Field width for the result + if (tok.isLabel()) + { + // - #eval INT "expr" + // - #eval INT { expr } + // - #eval INT #{ expr #} + fieldWidth = max(1, tok.labelToken()); + is >> tok; + } + + string str; // The string to evaluate + if (tok.isString()) + { + // - #eval "expr" + // - #eval #{ expr #} + // - #eval ${{ expr }} - wierd but handled + str = tok.stringToken(); + } + else if (tok.isPunctuation(token::BEGIN_BLOCK)) + { + // - #eval { expr } + slurpUntilBalancedBrace(dynamic_cast(is), str); + } + else + { + FatalIOErrorInFunction(is) + << "Invalid input for #eval." + " Expecting a string or block to evaluate, but found" << nl + << tok.info() << endl + << exit(FatalIOError); + } + + tokenList toks + ( + evalEntry::evaluate(parentDict, str, fieldWidth, is) + ); + + return toks; +} + + // * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * // bool Foam::functionEntries::evalEntry::execute @@ -201,4 +358,21 @@ bool Foam::functionEntries::evalEntry::execute } +bool Foam::functionEntries::evalEntry::execute +( + const dictionary& parentDict, + primitiveEntry& entry, + const string& inputExpr, + label fieldWidth, + Istream& is +) +{ + tokenList toks(evaluate(parentDict, inputExpr, fieldWidth, is)); + + entry.append(std::move(toks), true); // Lazy resizing + + return true; +} + + // ************************************************************************* // diff --git a/src/OpenFOAM/db/dictionary/functionEntries/evalEntry/evalEntry.H b/src/OpenFOAM/db/dictionary/functionEntries/evalEntry/evalEntry.H index c4e482b624..58d450dd9f 100644 --- a/src/OpenFOAM/db/dictionary/functionEntries/evalEntry/evalEntry.H +++ b/src/OpenFOAM/db/dictionary/functionEntries/evalEntry/evalEntry.H @@ -87,18 +87,36 @@ class evalEntry { //- Evaluate and return a token list - static tokenList evaluate(const dictionary& parentDict, Istream& is); + static tokenList evaluate + ( + const dictionary& parentDict, + const string& inputExpr, //!< String to expand and evaluate + label fieldWidth, //!< Field width for the result + const Istream& is //!< For reporting errors + ); + //- Evaluate and return a token list + static tokenList evaluate(const dictionary& parentDict, Istream& is); public: - //- Execute in a primitiveEntry context + //- Execute in a primitiveEntry context, extracts token or line static bool execute ( const dictionary& parentDict, primitiveEntry& thisEntry, Istream& is ); + + //- Execute in a primitiveEntry context, evaluating the given content + static bool execute + ( + const dictionary& parentDict, + primitiveEntry& entry, + const string& inputExpr, + label fieldWidth, + Istream& is + ); }; diff --git a/src/OpenFOAM/db/dictionary/primitiveEntry/primitiveEntry.C b/src/OpenFOAM/db/dictionary/primitiveEntry/primitiveEntry.C index 53c5b301ef..20a7b23333 100644 --- a/src/OpenFOAM/db/dictionary/primitiveEntry/primitiveEntry.C +++ b/src/OpenFOAM/db/dictionary/primitiveEntry/primitiveEntry.C @@ -35,7 +35,7 @@ License // Find the type/position of the ":-" or ":+" alternative values // Returns 0, '-', '+' corresponding to not-found or ':-' or ':+' -static inline int findParameterAlternative +static inline char findParameterAlternative ( const std::string& s, std::string::size_type& pos, @@ -50,7 +50,7 @@ static inline int findParameterAlternative if (pos < endPos) { // in-range: check for '+' or '-' following the ':' - const int altType = s[pos+1]; + const char altType = s[pos+1]; if (altType == '+' || altType == '-') { return altType; @@ -78,11 +78,13 @@ bool Foam::primitiveEntry::expandVariable const dictionary& dict ) { - int altType = 0; // Type ('-' or '+') for ":-" or ":+" alternatives + char altType = 0; // Type ('-' or '+') for ":-" or ":+" alternatives word expanded; string altValue; - if (varName.size() > 1 && varName[0] == token::BEGIN_BLOCK) + // Any ${{ expr }} entries have been trapped and processed elsewhere + + if (varName[0] == token::BEGIN_BLOCK && varName.size() > 1) { // Replace content between {} with string expansion and // handle ${parameter:-word} or ${parameter:+word} diff --git a/src/OpenFOAM/db/dictionary/primitiveEntry/primitiveEntryIO.C b/src/OpenFOAM/db/dictionary/primitiveEntry/primitiveEntryIO.C index eb1f21d224..5dccf2fb0a 100644 --- a/src/OpenFOAM/db/dictionary/primitiveEntry/primitiveEntryIO.C +++ b/src/OpenFOAM/db/dictionary/primitiveEntry/primitiveEntryIO.C @@ -28,6 +28,7 @@ License #include "primitiveEntry.H" #include "functionEntry.H" +#include "evalEntry.H" // * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * // @@ -63,25 +64,51 @@ bool Foam::primitiveEntry::acceptToken if (tok.isDirective()) { - // Directive: wordToken starts with '#' + // Directive (wordToken) begins with '#'. Eg, "#include" + // Remove leading '#' sigil before dispatching + const word& key = tok.wordToken(); + // Min-size is 2: sigil '#' with any content accept = ( - disableFunctionEntries - || key.size() == 1 + (disableFunctionEntries || key.size() < 2) || !expandFunction(key.substr(1), dict, is) ); } + else if (tok.isExpression()) + { + // Expression (stringToken): ${{ expr }} + // Surrounding delimiters are stripped as required in evalEntry + + const string& key = tok.stringToken(); + + // Min-size is 6: decorators '${{}}' with any content + accept = + ( + (disableFunctionEntries || key.size() < 6) + || !functionEntries::evalEntry::execute + ( + dict, + *this, + key, + 1, // Field width is 1 + is // For error messages + ) + ); + } else if (tok.isVariable()) { - // Variable: stringToken starts with '$' + // Variable (stringToken): starts with '$' + // Eg, "$varName" or "${varName}" + // Remove leading '$' sigil before dispatching + const string& key = tok.stringToken(); + // Min-size is 2: sigil '$' with any content accept = ( - disableFunctionEntries - || key.size() == 1 + (disableFunctionEntries || key.size() < 2) || !expandVariable(key.substr(1), dict) ); } @@ -116,7 +143,7 @@ bool Foam::primitiveEntry::read(const dictionary& dict, Istream& is) // - similarly, the bitmask is tested *after* decreasing depth uint64_t balanced = 0u; - label depth = 0; + int depth = 0; token tok; while @@ -274,19 +301,18 @@ void Foam::primitiveEntry::write(Ostream& os, const bool contentsOnly) const os.writeKeyword(keyword()); } - bool addSpace = false; // Separate from previous tokens with a space + bool addSpace = false; // Separate from previous token with a space for (const token& tok : *this) { if (addSpace) os << token::SPACE; + addSpace = true; - // Try to output token directly, with special handling in Ostreams. - + // Output token with direct handling in Ostream(s), + // or use normal '<<' output operator if (!os.write(tok)) { - os << tok; // Revert to normal '<<' output operator + os << tok; } - - addSpace = true; // Separate from following tokens } if (!contentsOnly) diff --git a/src/fileFormats/obj/OBJstream.C b/src/fileFormats/obj/OBJstream.C index 893ea3f55a..904b585095 100644 --- a/src/fileFormats/obj/OBJstream.C +++ b/src/fileFormats/obj/OBJstream.C @@ -6,7 +6,7 @@ \\/ M anipulation | ------------------------------------------------------------------------------- Copyright (C) 2012-2016 OpenFOAM Foundation - Copyright (C) 2020 OpenCFD Ltd. + Copyright (C) 2020-2021 OpenCFD Ltd. ------------------------------------------------------------------------------- License This file is part of OpenFOAM. @@ -120,8 +120,8 @@ Foam::Ostream& Foam::OBJstream::writeQuoted return *this; } - - OFstream::write(static_cast(token::BEGIN_STRING)); + // Output with surrounding quotes and backslash escaping + OFstream::write(static_cast(token::DQUOTE)); unsigned backslash = 0; for (auto iter = str.cbegin(); iter != str.cend(); ++iter) @@ -138,7 +138,7 @@ Foam::Ostream& Foam::OBJstream::writeQuoted ++lineNumber_; ++backslash; // backslash escape for newline } - else if (c == token::END_STRING) + else if (c == token::DQUOTE) { ++backslash; // backslash escape for quote } @@ -155,7 +155,7 @@ Foam::Ostream& Foam::OBJstream::writeQuoted // silently drop any trailing backslashes // they would otherwise appear like an escaped end-quote - OFstream::write(static_cast(token::END_STRING)); + OFstream::write(static_cast(token::DQUOTE)); return *this; }