ENH: improve stream handling of expansions (#2095)

* removed internal upper limit on word/string length for parsed input.

  - Although it has not caused many problems, no reason to retain
    these limits.
  - simplify some of the internal logic for reading string-like items.
  - localize parsers for better separation from the header

  - expose new function seekCommentEnd_Cstyle(), as useful
    handler of C-style comments

* exclude imbalanced closing ')' from word/variable

  - previously included this into the word/variable, but makes more
    sense to leave on the parser for the following token.

    Prevents content like 'vector (10 20 $zmax);' from being parsed
    as '$zmax)' instead of as '$zmax' followed by a ')'.
    No conceivable reason that the former would actually be desirable,
    but can still be obtained with brace notation: Eg, '${zmax)}'

* consistent handling of ${{ ... }} expressions

  - within a dictionary content, the following construct was
    incorrectly processed:

        value ${{2*sqrt(0.5)}};

    Complains about no dictionary/env variable "{2*sqrt(0.5)}"

    Now trap expressions directly and assign their own token type
    while reading. Later expansion can then be properly passed to
    the exprDriver (evalEntry) instead of incorrectly trying
    variable expansion.

    Does not alter the use of expressions embedded within other
    expansions. Eg, "file${{10*2}}"

* improve #eval { ... } brace slurping

  - the initial implementation of this was rudimentary and simply
    grabbed everything until the next '}'.  Now continue to grab
    content until braces are properly balanced

    Eg, the content:   value #eval{${radius}*2};

    would have previously terminated prematurely with "${radius" for
    the expression!

NOTE:
    both the ${{ expr }} parsed input and the #eval { ... } input
    discard C/C++ comments during reading to reduce intermediate
    overhead for content that will be discarded before evaluation
    anyhow.

* tighten recognition of verbatim strings and expressions.

  - parser was previously sloppy and would have accepted content such
    as "# { ..." (for example) as an verbatim string introducer.
    Now only accept parse if there are no intermediate characters
    discarded.
This commit is contained in:
Mark Olesen 2021-05-17 17:25:20 +02:00
parent efd1ac4b5f
commit 44a243a94d
8 changed files with 692 additions and 419 deletions

View File

@ -30,6 +30,7 @@ License
#include "int.H"
#include "token.H"
#include <cctype>
#include <cstring>
// * * * * * * * * * * * * * * 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 << "...\""

View File

@ -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.

View File

@ -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;

View File

@ -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<ISstream&>(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<ISstream&>(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;
}
// ************************************************************************* //

View File

@ -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
);
};

View File

@ -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}

View File

@ -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)

View File

@ -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<char>(token::BEGIN_STRING));
// Output with surrounding quotes and backslash escaping
OFstream::write(static_cast<char>(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<char>(token::END_STRING));
OFstream::write(static_cast<char>(token::DQUOTE));
return *this;
}