From a258abf7be33af20eff7d26f6eb2192e849d6f38 Mon Sep 17 00:00:00 2001 From: deeaitch Date: Fri, 23 Oct 2026 20:10:39 -0400 Subject: [PATCH] Closes #25 --- README.md | 5 ++- changelog.md | 10 +++++ core/dbc_attributes.cpp | 12 ++++-- core/dbc_parser.cpp | 32 ++++++++++++++++ core/dbc_parser.h | 2 + tests/README.md | 12 ++++++ tests/dbc_parser_test.cpp | 80 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 148 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index eeb0eec..aa3e7b2 100644 --- a/README.md +++ b/README.md @@ -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 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 -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 BA_DEF_ BO_ "VFrameFormat" ENUM "StandardCAN","ExtendedCAN","StandardCAN_FD"; diff --git a/changelog.md b/changelog.md index 9769c41..eeed2d1 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,16 @@ existing Git tag; subsequent changes are listed as 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 #### Maintenance diff --git a/core/dbc_attributes.cpp b/core/dbc_attributes.cpp index d06eeb6..49bb6db 100644 --- a/core/dbc_attributes.cpp +++ b/core/dbc_attributes.cpp @@ -11,12 +11,16 @@ namespace { bool quoted = false; }; + bool IsWhitespace(char character) { + return character == ' ' || character == '\t' || character == '\n' || character == '\r'; + } + std::vector Tokenize(const std::string &text) { std::vector tokens; std::size_t index = 0U; while (index < text.size()) { const char character = text[index++]; - if (character == ' ' || character == '\t') + if (IsWhitespace(character)) continue; Token token; if (character == '"') { @@ -39,14 +43,14 @@ namespace { } if (!closed) 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] != ';') throw std::runtime_error("Missing separator after attribute string."); } else if (character == ';' || character == ',') { token.text = character; } else { token.text = character; - while (index < text.size() && text[index] != ' ' && text[index] != '\t' && + while (index < text.size() && !IsWhitespace(text[index]) && text[index] != ';' && text[index] != ',') { if (text[index] == '"') 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_"}; for (const char *phase: keywords) { 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; try { AttributeTokens tokens(pending.statement); diff --git a/core/dbc_parser.cpp b/core/dbc_parser.cpp index ab74734..892ac03 100644 --- a/core/dbc_parser.cpp +++ b/core/dbc_parser.cpp @@ -19,6 +19,35 @@ #include 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. void ReadCommentLines(std::ifstream &input, std::string &statement, std::size_t &lineNumber) { @@ -112,6 +141,9 @@ DbcParseResult DbcParser::ParseFileWithDiagnostics(const std::string &filePath) if (keyword == "CM_") { line = rawLine; 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 { diff --git a/core/dbc_parser.h b/core/dbc_parser.h index 417703b..c8f6be5 100644 --- a/core/dbc_parser.h +++ b/core/dbc_parser.h @@ -34,6 +34,8 @@ struct DbcParserOptions { * - BA_DEF_, BA_DEF_DEF_, BA_ typed attributes, frame format and cycle time * * 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 * an error. VERSION, NS_, BS_, BU_ and NS_ symbol declarations stay quiet. diff --git a/tests/README.md b/tests/README.md index 88ebc08..3f8e589 100644 --- a/tests/README.md +++ b/tests/README.md @@ -223,6 +223,18 @@ fixture. Saved decoded results and trees retain their original metadata. ## 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 all five scopes and types, deferred definitions/defaults/targets, explicit values overriding defaults, repeated assignments, escaped strings, typed diff --git a/tests/dbc_parser_test.cpp b/tests/dbc_parser_test.cpp index 4bb43d2..987daf9 100644 --- a/tests/dbc_parser_test.cpp +++ b/tests/dbc_parser_test.cpp @@ -89,6 +89,8 @@ private: } private Q_SLOTS: void attributes(); + void multilineAttributes(); + void invalidMultilineAttributes(); void invalidAttributes(); void attributeNumbers(); void invalidAttributeNumbers_data(); @@ -186,6 +188,84 @@ void DbcParserTest::attributes() { QCOMPARE(std::get(*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(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(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 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() { Fixture fixture; const std::vector invalid = {