Closes #25
This commit is contained in:
1 parent
c43222939f
commit
a258abf7be
7 files changed
+148
-5
No files matched your search
@@ -659,7 +659,10 @@ The parser does not implement the warned features; consumers should review warni
|
|||||||
definitions. Integer/HEX values use decimal signed 64-bit numbers; floating
|
definitions. Integer/HEX values use decimal signed 64-bit numbers; floating
|
||||||
values are finite and locale independent. ENUM assignments use zero-based indices;
|
values are finite and locale independent. ENUM assignments use zero-based indices;
|
||||||
defaults accept an index or a quoted label. Strings support escaped quotes and
|
defaults accept an index or a quoted label. Strings support escaped quotes and
|
||||||
backslashes. Each statement occupies one physical line and ends with `;`.
|
backslashes. Statements end with `;` outside quoted text and may span physical
|
||||||
|
lines, including ENUM lists, defaults, and assignments. Newlines inside quoted
|
||||||
|
strings are preserved; semicolons inside them do not terminate the statement.
|
||||||
|
Attribute errors and format warnings refer to the statement's first physical line.
|
||||||
|
|
||||||
```dbc
|
```dbc
|
||||||
BA_DEF_ BO_ "VFrameFormat" ENUM "StandardCAN","ExtendedCAN","StandardCAN_FD";
|
BA_DEF_ BO_ "VFrameFormat" ENUM "StandardCAN","ExtendedCAN","StandardCAN_FD";
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ existing Git tag; subsequent changes are listed as unreleased.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### 2026-10-23
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Read attribute definitions, defaults, and assignments across physical lines.
|
||||||
|
Preserve quoted newlines and semicolons, and report attribute errors and
|
||||||
|
unsupported frame formats at the statement's first physical line.
|
||||||
|
- Add regressions for multiline frame formats and cycle times, deferred values,
|
||||||
|
CRLF input, escaped strings, malformed statements, and diagnostic locations.
|
||||||
|
|
||||||
### 2026-10-20
|
### 2026-10-20
|
||||||
|
|
||||||
#### Maintenance
|
#### Maintenance
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ namespace {
|
|||||||
bool quoted = false;
|
bool quoted = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool IsWhitespace(char character) {
|
||||||
|
return character == ' ' || character == '\t' || character == '\n' || character == '\r';
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Token> Tokenize(const std::string &text) {
|
std::vector<Token> Tokenize(const std::string &text) {
|
||||||
std::vector<Token> tokens;
|
std::vector<Token> tokens;
|
||||||
std::size_t index = 0U;
|
std::size_t index = 0U;
|
||||||
while (index < text.size()) {
|
while (index < text.size()) {
|
||||||
const char character = text[index++];
|
const char character = text[index++];
|
||||||
if (character == ' ' || character == '\t')
|
if (IsWhitespace(character))
|
||||||
continue;
|
continue;
|
||||||
Token token;
|
Token token;
|
||||||
if (character == '"') {
|
if (character == '"') {
|
||||||
@@ -39,14 +43,14 @@ namespace {
|
|||||||
}
|
}
|
||||||
if (!closed)
|
if (!closed)
|
||||||
throw std::runtime_error("Unterminated attribute string.");
|
throw std::runtime_error("Unterminated attribute string.");
|
||||||
if (index < text.size() && text[index] != ' ' && text[index] != '\t' &&
|
if (index < text.size() && !IsWhitespace(text[index]) &&
|
||||||
text[index] != ',' && text[index] != ';')
|
text[index] != ',' && text[index] != ';')
|
||||||
throw std::runtime_error("Missing separator after attribute string.");
|
throw std::runtime_error("Missing separator after attribute string.");
|
||||||
} else if (character == ';' || character == ',') {
|
} else if (character == ';' || character == ',') {
|
||||||
token.text = character;
|
token.text = character;
|
||||||
} else {
|
} else {
|
||||||
token.text = character;
|
token.text = character;
|
||||||
while (index < text.size() && text[index] != ' ' && text[index] != '\t' &&
|
while (index < text.size() && !IsWhitespace(text[index]) &&
|
||||||
text[index] != ';' && text[index] != ',') {
|
text[index] != ';' && text[index] != ',') {
|
||||||
if (text[index] == '"')
|
if (text[index] == '"')
|
||||||
throw std::runtime_error("Missing separator before attribute string.");
|
throw std::runtime_error("Missing separator before attribute string.");
|
||||||
@@ -259,7 +263,7 @@ void DbcParseContext::ResolveAttributes(const std::string &filePath, std::vector
|
|||||||
const char *keywords[] = {"BA_DEF_", "BA_DEF_DEF_", "BA_"};
|
const char *keywords[] = {"BA_DEF_", "BA_DEF_DEF_", "BA_"};
|
||||||
for (const char *phase: keywords) {
|
for (const char *phase: keywords) {
|
||||||
for (const PendingAttribute &pending: pendingAttributes) {
|
for (const PendingAttribute &pending: pendingAttributes) {
|
||||||
const std::string keyword = pending.statement.substr(0U, pending.statement.find_first_of(" \t"));
|
const std::string keyword = pending.statement.substr(0U, pending.statement.find_first_of(" \t\r\n"));
|
||||||
if (keyword != phase) continue;
|
if (keyword != phase) continue;
|
||||||
try {
|
try {
|
||||||
AttributeTokens tokens(pending.statement);
|
AttributeTokens tokens(pending.statement);
|
||||||
|
|||||||
@@ -19,6 +19,35 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
// Attribute statements end at a semicolon outside quoted text.
|
||||||
|
void ReadAttributeLines(std::ifstream &input, std::string &statement,
|
||||||
|
std::size_t &lineNumber) {
|
||||||
|
bool inQuote = false;
|
||||||
|
bool escaped = false;
|
||||||
|
std::string part = statement;
|
||||||
|
for (;;) {
|
||||||
|
for (const char character: part) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
} else if (inQuote && character == '\\') {
|
||||||
|
escaped = true;
|
||||||
|
} else if (character == '"') {
|
||||||
|
inQuote = !inQuote;
|
||||||
|
} else if (!inQuote && character == ';') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!std::getline(input, part))
|
||||||
|
return;
|
||||||
|
++lineNumber;
|
||||||
|
if (!part.empty() && part.back() == '\r')
|
||||||
|
part.pop_back();
|
||||||
|
statement += '\n';
|
||||||
|
statement += part;
|
||||||
|
escaped = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read through the closing unescaped quote, preserving comment whitespace.
|
// Read through the closing unescaped quote, preserving comment whitespace.
|
||||||
void ReadCommentLines(std::ifstream &input, std::string &statement,
|
void ReadCommentLines(std::ifstream &input, std::string &statement,
|
||||||
std::size_t &lineNumber) {
|
std::size_t &lineNumber) {
|
||||||
@@ -112,6 +141,9 @@ DbcParseResult DbcParser::ParseFileWithDiagnostics(const std::string &filePath)
|
|||||||
if (keyword == "CM_") {
|
if (keyword == "CM_") {
|
||||||
line = rawLine;
|
line = rawLine;
|
||||||
ReadCommentLines(input, line, lineNumber);
|
ReadCommentLines(input, line, lineNumber);
|
||||||
|
} else if (keyword == "BA_DEF_" || keyword == "BA_DEF_DEF_" || keyword == "BA_") {
|
||||||
|
line = rawLine.substr(rawLine.find(keyword));
|
||||||
|
ReadAttributeLines(input, line, lineNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ struct DbcParserOptions {
|
|||||||
* - BA_DEF_, BA_DEF_DEF_, BA_ typed attributes, frame format and cycle time
|
* - BA_DEF_, BA_DEF_DEF_, BA_ typed attributes, frame format and cycle time
|
||||||
*
|
*
|
||||||
* Frame/signal comments support multiline quoted text and deferred references.
|
* Frame/signal comments support multiline quoted text and deferred references.
|
||||||
|
* Attribute statements support multiline fields and quoted strings, ending at
|
||||||
|
* a semicolon outside quoted text. Diagnostics use the first physical line.
|
||||||
*
|
*
|
||||||
* Reports unsupported statements as warnings, except SG_MUL_VAL_ which is
|
* Reports unsupported statements as warnings, except SG_MUL_VAL_ which is
|
||||||
* an error. VERSION, NS_, BS_, BU_ and NS_ symbol declarations stay quiet.
|
* an error. VERSION, NS_, BS_, BU_ and NS_ symbol declarations stay quiet.
|
||||||
|
|||||||
@@ -223,6 +223,18 @@ fixture. Saved decoded results and trees retain their original metadata.
|
|||||||
|
|
||||||
## Attribute coverage
|
## Attribute coverage
|
||||||
|
|
||||||
|
`multilineAttributes` checks multiline definitions, defaults and assignments,
|
||||||
|
deferred cycle times, frame format overrides, quoted newlines/semicolons and
|
||||||
|
escapes, CRLF input, and warning locations for defaults and explicit values.
|
||||||
|
`invalidMultilineAttributes` checks malformed ENUM lists/ranges, missing
|
||||||
|
terminators, unterminated strings, undefined names, trailing fields, and error
|
||||||
|
locations at the first physical line through both parser APIs.
|
||||||
|
|
||||||
|
The multiline changes passed a clean qmake build and `make check` in
|
||||||
|
`/tmp/dbc-attributes-build` with Qt 6.8.2: 2,396 QtTest cases and the qmake
|
||||||
|
tree target passed. The standalone C++17 tree test also passed. One existing
|
||||||
|
C locale test skipped because no named comma-decimal locale is installed.
|
||||||
|
|
||||||
Parser slots `attributes`, `invalidAttributes`, and `attributeWarnings` cover
|
Parser slots `attributes`, `invalidAttributes`, and `attributeWarnings` cover
|
||||||
all five scopes and types, deferred definitions/defaults/targets, explicit
|
all five scopes and types, deferred definitions/defaults/targets, explicit
|
||||||
values overriding defaults, repeated assignments, escaped strings, typed
|
values overriding defaults, repeated assignments, escaped strings, typed
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ private:
|
|||||||
}
|
}
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void attributes();
|
void attributes();
|
||||||
|
void multilineAttributes();
|
||||||
|
void invalidMultilineAttributes();
|
||||||
void invalidAttributes();
|
void invalidAttributes();
|
||||||
void attributeNumbers();
|
void attributeNumbers();
|
||||||
void invalidAttributeNumbers_data();
|
void invalidAttributeNumbers_data();
|
||||||
@@ -186,6 +188,84 @@ void DbcParserTest::attributes() {
|
|||||||
QCOMPARE(std::get<std::int64_t>(*boundaries.attributeDefinitions.at("State").defaultValue), std::int64_t(1));
|
QCOMPARE(std::get<std::int64_t>(*boundaries.attributeDefinitions.at("State").defaultValue), std::int64_t(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DbcParserTest::multilineAttributes() {
|
||||||
|
Fixture fixture;
|
||||||
|
fixture.Write(R"DBC(BA_
|
||||||
|
"GenMsgCycleTime" BO_ 1
|
||||||
|
25;
|
||||||
|
BA_DEF_ BO_ "VFrameFormat" ENUM
|
||||||
|
"StandardCAN",
|
||||||
|
"StandardCAN_FD";
|
||||||
|
BA_DEF_DEF_
|
||||||
|
"VFrameFormat"
|
||||||
|
"StandardCAN_FD";
|
||||||
|
BA_DEF_ BO_ "GenMsgCycleTime"
|
||||||
|
INT 0 100;
|
||||||
|
BA_DEF_DEF_ "GenMsgCycleTime"
|
||||||
|
50;
|
||||||
|
BA_DEF_ SG_ "Label" STRING;
|
||||||
|
BA_ "Label" SG_ 1 Value
|
||||||
|
"first; \"quoted\"
|
||||||
|
second\\path";
|
||||||
|
BA_ "VFrameFormat" BO_ 1
|
||||||
|
0;
|
||||||
|
BO_ 1 F: 1 ECU
|
||||||
|
SG_ Value : 0|8@1+ (1,0) [0|255] "" ECU
|
||||||
|
BO_ 2 Default: 0 ECU
|
||||||
|
)DBC");
|
||||||
|
const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
|
||||||
|
QVERIFY(!result.HasErrors());
|
||||||
|
QCOMPARE(result.database.frames.size(), std::size_t(2));
|
||||||
|
QCOMPARE(result.database.frames[0].cycleTimeMs.value(), 25U);
|
||||||
|
QCOMPARE(result.database.frames[1].cycleTimeMs.value(), 50U);
|
||||||
|
QCOMPARE(result.database.frames[0].frameFormat.value(), std::string("StandardCAN"));
|
||||||
|
QCOMPARE(result.database.frames[1].frameFormat.value(), std::string("StandardCAN_FD"));
|
||||||
|
QCOMPARE(std::get<std::string>(result.database.frames[0].signals[0].attributes.at("Label")),
|
||||||
|
std::string("first; \"quoted\"\nsecond\\path"));
|
||||||
|
QCOMPARE(result.diagnostics.size(), std::size_t(1));
|
||||||
|
QCOMPARE(result.diagnostics[0].code, DbcDiagnosticCode::UnsupportedAttribute);
|
||||||
|
QCOMPARE(result.diagnostics[0].lineNumber, std::size_t(7));
|
||||||
|
|
||||||
|
fixture.Write("BA_DEF_ BO_ \"VFrameFormat\" ENUM\r\n \"StandardCAN\",\r\n \"ExtendedCAN_FD\";\r\n"
|
||||||
|
"BA_\r\n \"VFrameFormat\" BO_ 1\r\n 1;\r\nBO_ 1 F: 0 ECU\r\n");
|
||||||
|
const DbcParseResult explicitFormat = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
|
||||||
|
QVERIFY(!explicitFormat.HasErrors());
|
||||||
|
QCOMPARE(explicitFormat.diagnostics.size(), std::size_t(1));
|
||||||
|
QCOMPARE(explicitFormat.diagnostics[0].code, DbcDiagnosticCode::UnsupportedAttribute);
|
||||||
|
QCOMPARE(explicitFormat.diagnostics[0].lineNumber, std::size_t(4));
|
||||||
|
QCOMPARE(explicitFormat.database.frames[0].frameFormat.value(), std::string("ExtendedCAN_FD"));
|
||||||
|
|
||||||
|
fixture.Write("BA_DEF_ \"Text\" STRING;\n BA_ \"Text\" \"first \nsecond\";\n"
|
||||||
|
"UnsupportedStatement\n");
|
||||||
|
const DbcParseResult whitespace = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
|
||||||
|
QVERIFY(!whitespace.HasErrors());
|
||||||
|
QCOMPARE(std::get<std::string>(whitespace.database.attributes.at("Text")), std::string("first \nsecond"));
|
||||||
|
QCOMPARE(whitespace.diagnostics.size(), std::size_t(1));
|
||||||
|
QCOMPARE(whitespace.diagnostics[0].code, DbcDiagnosticCode::UnsupportedStatement);
|
||||||
|
QCOMPARE(whitespace.diagnostics[0].lineNumber, std::size_t(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbcParserTest::invalidMultilineAttributes() {
|
||||||
|
Fixture fixture;
|
||||||
|
const std::vector<std::string> invalid = {
|
||||||
|
"BA_DEF_\n BO_ \"A\" INT\n 10 0;\n",
|
||||||
|
"BA_DEF_ BO_ \"A\" ENUM\n \"one\",\n;\n",
|
||||||
|
"BA_DEF_ BO_ \"A\" STRING\n",
|
||||||
|
"BA_DEF_ BO_ \"A\" ENUM\n \"unterminated;\n",
|
||||||
|
"BA_DEF_DEF_\n \"Unknown\"\n 0;\n",
|
||||||
|
"BA_\n \"Unknown\" BO_ 1\n 0;\n",
|
||||||
|
"BA_DEF_ BO_ \"A\" STRING\n ; trailing\n"};
|
||||||
|
for (const std::string &statement: invalid) {
|
||||||
|
fixture.Write("BO_ 1 F: 0 ECU\n" + statement);
|
||||||
|
const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
|
||||||
|
QVERIFY2(result.HasErrors(), statement.c_str());
|
||||||
|
QVERIFY(result.database.frames.empty());
|
||||||
|
QCOMPARE(result.diagnostics.back().code, DbcDiagnosticCode::InvalidAttribute);
|
||||||
|
QCOMPARE(result.diagnostics.back().lineNumber, std::size_t(2));
|
||||||
|
QVERIFY_THROWS_EXCEPTION(std::runtime_error, DbcParser().ParseFile(fixture.path.string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DbcParserTest::invalidAttributes() {
|
void DbcParserTest::invalidAttributes() {
|
||||||
Fixture fixture;
|
Fixture fixture;
|
||||||
const std::vector<std::string> invalid = {
|
const std::vector<std::string> invalid = {
|
||||||
|
|||||||
Reference in new issue
Block a user