From 849ce31e2fce540911c810877d59a542db3e7f34 Mon Sep 17 00:00:00 2001 From: deeaitch Date: Tue, 20 Oct 2026 13:21:43 -0400 Subject: [PATCH] Closes #25 --- README.md | 48 ++++- changelog.md | 15 ++ core/attributes.h | 53 +++++ core/core_files.pri | 2 + core/dbc_attributes.cpp | 342 +++++++++++++++++++++++++++++++++ core/dbc_database.h | 3 + core/dbc_decode_builder.cpp | 4 + core/dbc_diagnostic.h | 8 +- core/dbc_parser.cpp | 1 + core/dbc_parser.h | 1 + core/dbc_statement_handler.cpp | 3 + core/dbc_statement_handler.h | 12 ++ core/decode_database.h | 7 + core/frame_info.h | 24 ++- core/signal_info.h | 31 +-- tests/README.md | 19 +- tests/dbc_parser_test.cpp | 137 ++++++++++++- 17 files changed, 675 insertions(+), 35 deletions(-) create mode 100644 core/attributes.h create mode 100644 core/dbc_attributes.cpp diff --git a/README.md b/README.md index ce2726b..586a562 100644 --- a/README.md +++ b/README.md @@ -647,11 +647,52 @@ retain their original source lines. Invalid signal markers use `InvalidStatement `HasErrors()` checks severity; warnings alone do not make a parse fail. Unknown statements are reported conservatively, including `SIG_VALTYPE_`, -attributes (`BA_`, `BA_DEF_`, defaults and relation forms), signal types/references, +attribute relation forms (`BA_REL_`, `BA_DEF_REL_`), signal types/references, environment variables, named `VAL_TABLE_` tables, and vendor extensions. For example, ignoring `SIG_VALTYPE_` may decode a floating-point signal as an integer. The parser does not implement the warned features; consumers should review warnings. +### Attributes and frame metadata + +`BA_DEF_`, `BA_DEF_DEF_`, and `BA_` support database, `BO_`, `SG_`, +`BU_`, and `EV_` scopes with `INT`, `HEX`, `FLOAT`, `STRING`, and `ENUM` +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 `;`. + +```dbc +BA_DEF_ BO_ "VFrameFormat" ENUM "StandardCAN","ExtendedCAN","StandardCAN_FD"; +BA_DEF_DEF_ "VFrameFormat" "StandardCAN"; +BA_DEF_ BO_ "GenMsgCycleTime" INT 0 10000; +BA_DEF_DEF_ "GenMsgCycleTime" 100; +BA_ "GenMsgCycleTime" BO_ 256 20; +``` + +Definitions, defaults, assignments, and frame/signal declarations may appear in +any order. Defaults apply to every object of the defined scope; explicit values +override them, with the last repeated assignment winning. Duplicate definitions +or defaults, undefined names, wrong types/scopes, out-of-range values, and missing +frame/signal targets produce `Error / InvalidAttribute` at the source line. + +`DbcDatabase::attributeDefinitions` retains types, ranges, enum labels, and defaults; +`attributeAssignments` retains explicit values and targets. Effective values are +available in `attributes` on the database, frames and signals. Node/environment +assignments are retained by name without checking declarations; their defaults +remain on the definitions. Frame/signal attributes are copied into runtime +snapshots and tree payloads. Other valid attributes remain available as metadata +without changing decoding. + +`FrameInfo` and `DecodeFrame` expose optional `frameFormat` (the enum label) and +`cycleTimeMs` (`GenMsgCycleTime`, unsigned 32-bit milliseconds). Missing values +remain unset; zero cycle time means unspecified. `StandardCAN` and `ExtendedCAN` +are recognized. Other format labels, including CAN FD and `J1939PG`, are preserved +with `Warning / UnsupportedAttribute` when effective on a frame. A supported +format conflicting with the encoded `BO_` ID also warns. Warnings identify the +assignment or default line. Attributes do not change CAN ID normalization or +automatically enable CAN FD or J1939 decoding; J1939 still requires parser opt-in. +Use `ParseFileWithDiagnostics()` to review these warnings. + Common `VERSION`, `NS_`, `BS_`, and `BU_` headers produce no diagnostics. Single-token symbols inside the `NS_` declaration list are quiet regardless of indentation, even when they name unsupported syntax. Frame, signal, bus, and @@ -1207,8 +1248,7 @@ Not supported yet: - extended and nested multiplexing (`SG_MUL_VAL_`, `mNM`) - named `VAL_TABLE_` tables and environment-variable value descriptions -- `BA_` attributes -- `BA_DEF_` definitions and attribute defaults +- attribute relation forms (`BA_REL_`, `BA_DEF_REL_`) - `SIG_VALTYPE_` floating-point signal types - signal types/references (`SGTYPE_`, `SIG_TYPE_REF_`) and environment variables - advanced comment handling @@ -1264,7 +1304,7 @@ Recommended additions: Recommended additions: - basic multiplexing (implemented) -- attribute parsing +- attribute parsing (implemented for definitions, defaults and values) - richer J1939 support - CSV export of decoded traces - optimized filtering and signal selection diff --git a/changelog.md b/changelog.md index 62762f5..94cae52 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,21 @@ existing Git tag; subsequent changes are listed as unreleased. ### 2026-10-20 +#### Added + +- Parse typed `BA_DEF_`, `BA_DEF_DEF_`, and `BA_` attributes with deferred + definitions/references, validated ranges, defaults, and explicit overrides. + Preserve definitions and assignments plus effective database/frame/signal values. +- Expose optional frame format and cycle time metadata in parsed frames, runtime + snapshots, and tree payloads. Preserve unknown frame format labels with source + warnings, and report format/ID conflicts without changing ID or J1939 behavior. +- Add attribute syntax, scope, reference, numeric, override, diagnostics, and + metadata propagation regressions; document supported forms and limitations. + Verified the qmake build, all six QtTest suites, the qmake tree target, and + the standalone C++17 tree test. The existing named C locale test skipped + because the required locale is not installed. + + #### Changed - J1939 interpretation now requires `DbcParserOptions::enableJ1939` (default off) diff --git a/core/attributes.h b/core/attributes.h new file mode 100644 index 0000000..c5b58d3 --- /dev/null +++ b/core/attributes.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +#ifndef DBC_ATTRIBUTES_H +#define DBC_ATTRIBUTES_H + +#include +#include +#include +#include +#include +#include + +enum class AttributeScope { + Database, + Node, + Frame, + Signal, + Environment +}; + +enum class AttributeType { + Integer, + Hex, + Float, + String, + Enum +}; + +using AttributeValue = std::variant; +using AttributeMap = std::map; + +struct AttributeDefinition { + AttributeScope scope = AttributeScope::Database; + AttributeType type = AttributeType::String; + std::int64_t integerMinimum = 0; + std::int64_t integerMaximum = 0; + double minimum = 0.0; + double maximum = 0.0; + std::vector enumValues; + std::optional defaultValue; +}; + +/** Explicit assignment; defaults remain on definitions. Named nodes/environment + * variables are retained without validating declarations, which are not parsed. */ +struct AttributeAssignment { + std::string name; + AttributeScope scope = AttributeScope::Database; + std::uint32_t canId = 0U; + bool isExtended = false; + std::string targetName; + AttributeValue value; +}; + +#endif diff --git a/core/core_files.pri b/core/core_files.pri index d93b7da..5dd7322 100644 --- a/core/core_files.pri +++ b/core/core_files.pri @@ -1,4 +1,5 @@ SOURCES += \ + $$PWD/dbc_attributes.cpp \ $$PWD/decode_database.cpp \ $$PWD/dbc_decode_builder.cpp \ $$PWD/dbc_decoder.cpp \ @@ -8,6 +9,7 @@ SOURCES += \ $$PWD/tree_node.cpp HEADERS += \ + $$PWD/attributes.h \ $$PWD/j1939.h \ $$PWD/signal_layout.h \ $$PWD/definition_key.h \ diff --git a/core/dbc_attributes.cpp b/core/dbc_attributes.cpp new file mode 100644 index 0000000..b81c751 --- /dev/null +++ b/core/dbc_attributes.cpp @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: MIT +#include "dbc_statement_handler.h" +#include +#include +#include +#include + +namespace { + struct Token { + std::string text; + bool quoted = false; + }; + + 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') + continue; + Token token; + if (character == '"') { + token.quoted = true; + bool closed = false; + while (index < text.size()) { + char next = text[index++]; + if (next == '"') { + closed = true; + break; + } + if (next == '\\') { + if (index == text.size()) + throw std::runtime_error("Incomplete attribute escape."); + next = text[index++]; + if (next != '"' && next != '\\') + throw std::runtime_error("Invalid attribute escape."); + } + token.text += next; + } + if (!closed) + throw std::runtime_error("Unterminated attribute string."); + if (index < text.size() && text[index] != ' ' && text[index] != '\t' && + 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' && + text[index] != ';' && text[index] != ',') { + if (text[index] == '"') + throw std::runtime_error("Missing separator before attribute string."); + token.text += text[index++]; + } + } + tokens.push_back(token); + } + if (tokens.empty() || tokens.back().quoted || tokens.back().text != ";") + throw std::runtime_error("Attribute statement requires a semicolon."); + tokens.pop_back(); + return tokens; + } + + class AttributeTokens { + public: + explicit AttributeTokens(const std::string &line) : tokens(Tokenize(line)) {} + Token Take() { + if (position == tokens.size()) + throw std::runtime_error("Incomplete attribute statement."); + return tokens[position++]; + } + std::string Word() { + const Token token = Take(); + if (token.quoted || token.text == "," || token.text == ";") + throw std::runtime_error("Expected an attribute keyword or number."); + return token.text; + } + std::string Quoted() { + const Token token = Take(); + if (!token.quoted) + throw std::runtime_error("Expected a quoted attribute name or string."); + return token.text; + } + bool Empty() const { return position == tokens.size(); } + void End() const { + if (!Empty()) + throw std::runtime_error("Unexpected trailing attribute fields."); + } + + private: + std::vector tokens; + std::size_t position = 0U; + }; + + bool IsScope(const std::string &text) { + return text == "BO_" || text == "SG_" || text == "BU_" || text == "EV_"; + } + + AttributeScope ParseScope(const std::string &text) { + if (text == "BO_") return AttributeScope::Frame; + if (text == "SG_") return AttributeScope::Signal; + if (text == "BU_") return AttributeScope::Node; + if (text == "EV_") return AttributeScope::Environment; + throw std::runtime_error("Unsupported attribute scope: " + text); + } + + std::int64_t Integer(const std::string &text) { + const char *begin = text.data(); + const char *end = begin + text.size(); + if (begin != end && *begin == '+') { + ++begin; + if (begin != end && (*begin == '-' || *begin == '+')) + throw std::runtime_error("Invalid attribute number: " + text); + } + std::int64_t value = 0; + const std::from_chars_result parsed = std::from_chars(begin, end, value); + if (begin == end || parsed.ec != std::errc() || parsed.ptr != end) + throw std::runtime_error("Invalid attribute integer: " + text); + return value; + } + + double Number(const std::string &text) { + const char *begin = text.data(); + const char *end = begin + text.size(); + if (begin != end && *begin == '+') { + ++begin; + if (begin != end && (*begin == '-' || *begin == '+')) + throw std::runtime_error("Invalid attribute number: " + text); + } + double value = 0.0; + const std::from_chars_result parsed = std::from_chars(begin, end, value); + if (begin == end || parsed.ec != std::errc() || parsed.ptr != end || !std::isfinite(value)) + throw std::runtime_error("Invalid attribute number: " + text); + return value; + } + + AttributeValue Value(const Token &token, const AttributeDefinition &definition, bool isDefault) { + if (definition.type == AttributeType::String) { + if (!token.quoted) throw std::runtime_error("Attribute STRING requires quoted text."); + return token.text; + } + if (definition.type == AttributeType::Enum && isDefault && token.quoted) { + for (std::size_t index = 0U; index < definition.enumValues.size(); ++index) { + if (definition.enumValues[index] == token.text) + return static_cast(index); + } + throw std::runtime_error("Unknown attribute ENUM default label."); + } + if (token.quoted) throw std::runtime_error("Numeric attribute requires an unquoted number."); + if (definition.type == AttributeType::Float) { + const double value = Number(token.text); + if (value < definition.minimum || value > definition.maximum) + throw std::runtime_error("Attribute FLOAT value outside definition range."); + return value; + } + const std::int64_t value = Integer(token.text); + if (value < definition.integerMinimum || value > definition.integerMaximum) + throw std::runtime_error("Attribute integer/ENUM value outside definition range."); + return value; + } + + void Define(AttributeTokens &tokens, DbcDatabase &database) { + AttributeDefinition definition; + const Token first = tokens.Take(); + std::string name = first.text; + if (!first.quoted) { + definition.scope = ParseScope(first.text); + name = tokens.Quoted(); + } + if (name.empty()) throw std::runtime_error("Empty attribute name."); + const std::string type = tokens.Word(); + if (type == "INT" || type == "HEX") { + definition.type = type == "INT" ? AttributeType::Integer : AttributeType::Hex; + definition.integerMinimum = Integer(tokens.Word()); + definition.integerMaximum = Integer(tokens.Word()); + if (definition.integerMinimum > definition.integerMaximum) + throw std::runtime_error("Reversed attribute integer range."); + } else if (type == "FLOAT") { + definition.type = AttributeType::Float; + definition.minimum = Number(tokens.Word()); + definition.maximum = Number(tokens.Word()); + if (definition.minimum > definition.maximum) + throw std::runtime_error("Reversed attribute FLOAT range."); + } else if (type == "STRING") { + definition.type = AttributeType::String; + } else if (type == "ENUM") { + definition.type = AttributeType::Enum; + std::set labels; + for (;;) { + const std::string label = tokens.Quoted(); + if (!labels.insert(label).second) + throw std::runtime_error("Duplicate attribute ENUM label."); + definition.enumValues.push_back(label); + if (tokens.Empty()) break; + const Token separator = tokens.Take(); + if (separator.quoted || separator.text != ",") throw std::runtime_error("Expected ENUM comma."); + } + definition.integerMaximum = static_cast(definition.enumValues.size() - 1U); + } else { + throw std::runtime_error("Unsupported attribute definition type: " + type); + } + tokens.End(); + if (name == "VFrameFormat" && (definition.scope != AttributeScope::Frame || definition.type != AttributeType::Enum)) + throw std::runtime_error("VFrameFormat requires a BO_ ENUM definition."); + if (name == "GenMsgCycleTime" && (definition.scope != AttributeScope::Frame || + (definition.type != AttributeType::Integer && definition.type != AttributeType::Hex))) + throw std::runtime_error("GenMsgCycleTime requires a BO_ integer definition."); + if (!database.attributeDefinitions.emplace(name, definition).second) + throw std::runtime_error("Duplicate attribute definition: " + name); + } + + AttributeDefinition &FindDefinition(DbcDatabase &database, const std::string &name) { + const std::map::iterator found = database.attributeDefinitions.find(name); + if (found == database.attributeDefinitions.end()) + throw std::runtime_error("Undefined attribute: " + name); + return found->second; + } + + void ParseId(const std::string &text, AttributeAssignment &assignment) { + const std::int64_t raw = Integer(text); + if (raw < 0 || raw > std::numeric_limits::max() || (raw & 0x60000000U) != 0) + throw std::runtime_error("Invalid attribute CAN ID."); + const std::uint32_t id = static_cast(raw); + assignment.canId = id & 0x1fffffffU; + assignment.isExtended = (id & 0x80000000U) != 0U || id > 0x7ffU; + } + + AttributeMap &Target(DbcDatabase &database, const AttributeAssignment &assignment) { + if (assignment.scope == AttributeScope::Database) return database.attributes; + for (FrameInfo &frame: database.frames) { + if (frame.canId != assignment.canId || frame.isExtended != assignment.isExtended) continue; + if (assignment.scope == AttributeScope::Frame) return frame.attributes; + for (SignalInfo &signal: frame.signals) { + if (signal.name == assignment.targetName) return signal.attributes; + } + throw std::runtime_error("Unresolved attribute signal: " + assignment.targetName); + } + throw std::runtime_error("Unresolved attribute frame."); + } + + void ValidateCycleTime(const std::string &name, const AttributeValue &value) { + if (name != "GenMsgCycleTime") return; + const std::int64_t milliseconds = std::get(value); + if (milliseconds < 0 || milliseconds > std::numeric_limits::max()) + throw std::runtime_error("GenMsgCycleTime must fit unsigned 32-bit milliseconds."); + } + + void ApplyDefault(DbcDatabase &database, const std::string &name, const AttributeDefinition &definition) { + if (!definition.defaultValue) return; + if (definition.scope == AttributeScope::Database) database.attributes[name] = *definition.defaultValue; + for (FrameInfo &frame: database.frames) { + if (definition.scope == AttributeScope::Frame) frame.attributes[name] = *definition.defaultValue; + if (definition.scope == AttributeScope::Signal) { + for (SignalInfo &signal: frame.signals) signal.attributes[name] = *definition.defaultValue; + } + } + } +}// namespace + +void DbcAttributeHandler::Handle(const std::string &line, DbcParseContext &context) const { + context.pendingAttributes.push_back({line, context.lineNumber}); +} + +void DbcParseContext::ResolveAttributes(const std::string &filePath, std::vector &diagnostics) { + std::map defaultLines; + std::vector assignmentLines; + // Resolve each phase independently of source ordering. + const char *keywords[] = {"BA_DEF_", "BA_DEF_DEF_", "BA_"}; + for (int phase = 0; phase < 3; ++phase) { + for (const PendingAttribute &pending: pendingAttributes) { + const std::string keyword = pending.statement.substr(0U, pending.statement.find_first_of(" \t")); + if (keyword != keywords[phase]) continue; + try { + AttributeTokens tokens(pending.statement); + tokens.Word(); + if (phase == 0) { + Define(tokens, database); + continue; + } + const std::string name = tokens.Quoted(); + AttributeDefinition &definition = FindDefinition(database, name); + if (phase == 1) { + if (definition.defaultValue) throw std::runtime_error("Duplicate attribute default: " + name); + definition.defaultValue = Value(tokens.Take(), definition, true); + ValidateCycleTime(name, *definition.defaultValue); + tokens.End(); + defaultLines[name] = pending.lineNumber; + ApplyDefault(database, name, definition); + continue; + } + AttributeAssignment assignment; + assignment.name = name; + Token value = tokens.Take(); + if (!value.quoted && IsScope(value.text)) { + assignment.scope = ParseScope(value.text); + if (assignment.scope == AttributeScope::Frame || assignment.scope == AttributeScope::Signal) + ParseId(tokens.Word(), assignment); + if (assignment.scope != AttributeScope::Frame) + assignment.targetName = tokens.Word(); + value = tokens.Take(); + } + if (assignment.scope != definition.scope) throw std::runtime_error("Attribute assignment scope differs from definition."); + assignment.value = Value(value, definition, false); + ValidateCycleTime(name, assignment.value); + tokens.End(); + // Repeated explicit assignments use the last value, matching comments. + if (assignment.scope != AttributeScope::Node && assignment.scope != AttributeScope::Environment) + Target(database, assignment)[name] = assignment.value; + database.attributeAssignments.push_back(assignment); + assignmentLines.push_back(pending.lineNumber); + } catch (const std::exception &error) { + throw DbcDiagnosticError(DbcDiagnosticCode::InvalidAttribute, filePath, pending.lineNumber, error.what()); + } + } + } + for (FrameInfo &frame: database.frames) { + for (const AttributeMap::value_type &attribute: frame.attributes) { + if (attribute.first != "VFrameFormat" && attribute.first != "GenMsgCycleTime") continue; + std::size_t line = defaultLines[attribute.first]; + for (std::size_t index = 0U; index < database.attributeAssignments.size(); ++index) { + const AttributeAssignment &assignment = database.attributeAssignments[index]; + if (assignment.name == attribute.first && assignment.scope == AttributeScope::Frame && + assignment.canId == frame.canId && assignment.isExtended == frame.isExtended) + line = assignmentLines[index]; + } + const std::int64_t value = std::get(attribute.second); + if (attribute.first == "GenMsgCycleTime") { + frame.cycleTimeMs = static_cast(value); + continue; + } + frame.frameFormat = database.attributeDefinitions.at(attribute.first).enumValues[static_cast(value)]; + const std::string &format = *frame.frameFormat; + std::string warning; + if (format != "StandardCAN" && format != "ExtendedCAN") + warning = "Unsupported VFrameFormat '" + format + "' is preserved; decoding may be incomplete."; + else if ((format == "ExtendedCAN") != frame.isExtended) + warning = "VFrameFormat '" + format + "' conflicts with the BO_ CAN ID; ID interpretation is unchanged."; + if (!warning.empty()) + diagnostics.push_back({DbcDiagnosticSeverity::Warning, DbcDiagnosticCode::UnsupportedAttribute, filePath, line, warning}); + } + } +} diff --git a/core/dbc_database.h b/core/dbc_database.h index 350c426..26e6eac 100644 --- a/core/dbc_database.h +++ b/core/dbc_database.h @@ -21,6 +21,9 @@ */ struct DbcDatabase { std::vector frames; /**< All frames found in the DBC file. */ + std::map attributeDefinitions = {}; + std::vector attributeAssignments = {}; + AttributeMap attributes = {}; /**< Effective database attributes. */ }; #endif /* DBC_DATABASE_H */ diff --git a/core/dbc_decode_builder.cpp b/core/dbc_decode_builder.cpp index 4740a8c..d8f0d86 100644 --- a/core/dbc_decode_builder.cpp +++ b/core/dbc_decode_builder.cpp @@ -19,6 +19,9 @@ DecodeDatabase DbcDecodeBuilder::Build(const DbcDatabase &source) const { for (const FrameInfo &sourceFrame: source.frames) { DecodeFrame targetFrame; + targetFrame.attributes = sourceFrame.attributes; + targetFrame.frameFormat = sourceFrame.frameFormat; + targetFrame.cycleTimeMs = sourceFrame.cycleTimeMs; targetFrame.name = sourceFrame.name; targetFrame.canId = sourceFrame.canId; targetFrame.isExtended = sourceFrame.isExtended; @@ -32,6 +35,7 @@ DecodeDatabase DbcDecodeBuilder::Build(const DbcDatabase &source) const { for (const SignalInfo &sourceSignal: sourceFrame.signals) { DecodeSignal targetSignal; + targetSignal.attributes = sourceSignal.attributes; targetSignal.multiplexing = sourceSignal.multiplexing; targetSignal.name = sourceSignal.name; targetSignal.startBit = sourceSignal.startBit; diff --git a/core/dbc_diagnostic.h b/core/dbc_diagnostic.h index 6eb1f11..fab159c 100644 --- a/core/dbc_diagnostic.h +++ b/core/dbc_diagnostic.h @@ -19,14 +19,16 @@ enum class DbcDiagnosticCode { InvalidStatement, InvalidValueDescription, InvalidMultiplexing, - UnsupportedStatement + UnsupportedStatement, + InvalidAttribute, + UnsupportedAttribute }; struct DbcDiagnostic { DbcDiagnosticSeverity severity; DbcDiagnosticCode code; std::string filePath; - std::size_t lineNumber; // One-based physical line; zero for file-level errors. + std::size_t lineNumber;// One-based physical line; zero for file-level errors. std::string message; }; @@ -35,7 +37,7 @@ struct DbcParseResult { std::vector diagnostics; bool HasErrors() const { - for (const DbcDiagnostic &diagnostic : diagnostics) { + for (const DbcDiagnostic &diagnostic: diagnostics) { if (diagnostic.severity == DbcDiagnosticSeverity::Error) return true; } diff --git a/core/dbc_parser.cpp b/core/dbc_parser.cpp index f729ad0..b4f9e2c 100644 --- a/core/dbc_parser.cpp +++ b/core/dbc_parser.cpp @@ -142,6 +142,7 @@ DbcParseResult DbcParser::ParseFileWithDiagnostics(const std::string &filePath) context.ResolveComments(filePath); context.ResolveValueDescriptions(filePath); context.ValidateMultiplexing(filePath); + context.ResolveAttributes(filePath, result.diagnostics); } catch (const DbcDiagnosticError &error) { result.diagnostics.push_back(error.diagnostic); return result; diff --git a/core/dbc_parser.h b/core/dbc_parser.h index f606f5c..dc700e4 100644 --- a/core/dbc_parser.h +++ b/core/dbc_parser.h @@ -31,6 +31,7 @@ struct DbcParserOptions { * - CM_ SG_ * - basic M / mN multiplexing * - VAL_ signal value descriptions + * - BA_DEF_, BA_DEF_DEF_, BA_ typed attributes, frame format and cycle time * * Frame/signal comments support multiline quoted text and deferred references. * diff --git a/core/dbc_statement_handler.cpp b/core/dbc_statement_handler.cpp index 47e4723..fdc6bdd 100644 --- a/core/dbc_statement_handler.cpp +++ b/core/dbc_statement_handler.cpp @@ -385,6 +385,9 @@ DbcStatementDispatcher::DbcStatementDispatcher() { RegisterType("SG_"); RegisterType("CM_"); RegisterType("VAL_"); + RegisterType("BA_DEF_"); + RegisterType("BA_DEF_DEF_"); + RegisterType("BA_"); } void DbcHeaderHandler::Handle(const std::string &, DbcParseContext &) const {} diff --git a/core/dbc_statement_handler.h b/core/dbc_statement_handler.h index 7cab1e4..44d7d58 100644 --- a/core/dbc_statement_handler.h +++ b/core/dbc_statement_handler.h @@ -37,6 +37,11 @@ public: DbcDiagnostic diagnostic; }; +struct PendingAttribute { + std::string statement; + std::size_t lineNumber; +}; + /** Mutable state belonging to one file parse. Handlers do not retain it. */ struct DbcParseContext { DbcDatabase &database; @@ -51,6 +56,9 @@ struct DbcParseContext { std::vector pendingComments = {}; void ResolveComments(const std::string &filePath); + std::vector pendingAttributes = {}; + void ResolveAttributes(const std::string &filePath, std::vector &diagnostics); + void ResolveValueDescriptions(const std::string &filePath); }; @@ -79,7 +87,11 @@ private: class DbcCommentHandler : public DbcStatementHandler { public: void Handle(const std::string &line, DbcParseContext &context) const override; +}; +class DbcAttributeHandler : public DbcStatementHandler { +public: + void Handle(const std::string &line, DbcParseContext &context) const override; }; class DbcValueDescriptionHandler : public DbcStatementHandler { diff --git a/core/decode_database.h b/core/decode_database.h index 3c85847..bca8c38 100644 --- a/core/decode_database.h +++ b/core/decode_database.h @@ -12,6 +12,7 @@ #ifndef DECODE_DATABASE_H #define DECODE_DATABASE_H +#include "attributes.h" #include "definition_key.h" #include "multiplexing.h" #include "value_descriptions.h" @@ -56,6 +57,8 @@ struct DecodeSignal { ValueDescriptions valueDescriptions; /**< Raw integer state descriptions. */ std::string comment; /**< Comment. */ + AttributeMap attributes; + DecodeSignal() = default; }; @@ -73,6 +76,10 @@ struct DecodeFrame { std::string comment; /**< Frame comment. */ std::vector signals; /**< Signal definitions. */ + AttributeMap attributes; + std::optional frameFormat; + std::optional cycleTimeMs; + DecodeFrame() = default; FrameKey GetKey() const { diff --git a/core/frame_info.h b/core/frame_info.h index c4ee869..31f5c14 100644 --- a/core/frame_info.h +++ b/core/frame_info.h @@ -12,6 +12,7 @@ #ifndef FRAME_INFO_H #define FRAME_INFO_H +#include "attributes.h" #include #include #include @@ -22,15 +23,20 @@ * @brief Describes one CAN frame from a DBC file. */ struct FrameInfo { - std::string name; /**< Frame name. */ - std::uint32_t canId = 0U; /**< Normalized CAN identifier. */ - bool isExtended = false; /**< true for extended frame. */ - std::uint32_t pgn = 0U; /**< J1939 PGN if applicable. */ - bool hasPgn = false; /**< Explicit J1939 opt-in; pgn is derived from canId. */ - std::uint8_t dlc = 0U; /**< Frame payload length. */ - std::string transmitter; /**< Transmitter ECU name. */ - std::string comment; /**< Optional frame comment. */ - std::vector signals; /**< Signals contained in the frame. */ + std::string name; /**< Frame name. */ + std::uint32_t canId = 0U; /**< Normalized CAN identifier. */ + bool isExtended = false; /**< true for extended frame. */ + std::uint32_t pgn = 0U; /**< J1939 PGN if applicable. */ + bool hasPgn = false; /**< Explicit J1939 opt-in; pgn is derived from canId. */ + std::uint8_t dlc = 0U; /**< Frame payload length. */ + std::string transmitter; /**< Transmitter ECU name. */ + std::string comment; /**< Optional frame comment. */ + std::vector signals; /**< Signals contained in the frame. */ + + std::optional frameFormat; /**< VFrameFormat enum label. */ + std::optional cycleTimeMs; /**< GenMsgCycleTime; zero means unspecified. */ + + AttributeMap attributes; /**< Effective values, including defaults. */ FrameInfo() = default; }; diff --git a/core/signal_info.h b/core/signal_info.h index 16bf7f7..38de26e 100644 --- a/core/signal_info.h +++ b/core/signal_info.h @@ -12,8 +12,9 @@ #ifndef SIGNAL_INFO_H #define SIGNAL_INFO_H -#include "value_descriptions.h" +#include "attributes.h" #include "multiplexing.h" +#include "value_descriptions.h" #include #include #include @@ -23,19 +24,21 @@ */ struct SignalInfo { Multiplexing multiplexing; - std::string name; /**< Signal name. */ - std::uint32_t startBit = 0U; /**< Start bit in DBC notation. */ - std::uint32_t length = 0U; /**< Signal length in bits. */ - bool isLittleEndian = true; /**< true for Intel, false for Motorola. */ - bool isSigned = false; /**< true if signal is signed. */ - double factor = 1.0; /**< Scaling factor. */ - double offset = 0.0; /**< Physical offset. */ - double minimum = 0.0; /**< Minimum physical value. */ - double maximum = 0.0; /**< Maximum physical value. */ - std::string unit; /**< Physical unit. */ - std::vector receivers; /**< Signal receivers. */ - ValueDescriptions valueDescriptions; /**< Raw integer state descriptions. */ - std::string comment; /**< Optional signal comment. */ + std::string name; /**< Signal name. */ + std::uint32_t startBit = 0U; /**< Start bit in DBC notation. */ + std::uint32_t length = 0U; /**< Signal length in bits. */ + bool isLittleEndian = true; /**< true for Intel, false for Motorola. */ + bool isSigned = false; /**< true if signal is signed. */ + double factor = 1.0; /**< Scaling factor. */ + double offset = 0.0; /**< Physical offset. */ + double minimum = 0.0; /**< Minimum physical value. */ + double maximum = 0.0; /**< Maximum physical value. */ + std::string unit; /**< Physical unit. */ + std::vector receivers; /**< Signal receivers. */ + ValueDescriptions valueDescriptions; /**< Raw integer state descriptions. */ + std::string comment; /**< Optional signal comment. */ + + AttributeMap attributes; /**< Effective values, including defaults. */ SignalInfo() = default; }; diff --git a/tests/README.md b/tests/README.md index 05cb630..fb61761 100644 --- a/tests/README.md +++ b/tests/README.md @@ -66,7 +66,7 @@ line numbers for skipped syntax and immediate/deferred errors; warnings precedin an error are retained, and failed parses return an empty database. Common headers and namespace symbol lists stay quiet. Tests also check file-open errors, fresh parse sessions, and compatibility with the legacy throwing API. -The diagnostic audit also covers 15 unsupported-statement variants, all namespace +The diagnostic audit also covers 13 unsupported-statement variants, all namespace termination forms, CRLF and missing final newlines, relative paths with spaces, first-error stopping, deferred diagnostic ordering, duplicate definitions/values, selector ranges, overlaps, and preservation of supported decoding and metadata. @@ -220,3 +220,20 @@ Empty and headers-only fixtures produce an empty runtime database and a valid empty tree root. Replacement fixtures change definitions, metadata and decoded values, remove old lookup keys, and then clear both outputs through an empty fixture. Saved decoded results and trees retain their original metadata. + +## Attribute coverage + +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 +range/scope errors, duplicate definitions/defaults, unknown attributes and +missing targets. They check source diagnostics and the throwing API, distinct +standard/extended targets, frame cycle times and enum labels, unsupported +formats and ID conflicts, unchanged J1939 opt-in, and runtime/tree propagation. +Existing unsupported-statement checks now use relation forms, which remain +unsupported, instead of supported attribute forms. + +Attribute changes were verified with `qmake6 ../dbc.pro`, `make -j4`, and +`make check` in `build/`: all six QtTest suites and the qmake tree target +passed. The standalone C++17 tree test also passed. The existing named +comma-decimal C locale test skipped because the locale is not installed. diff --git a/tests/dbc_parser_test.cpp b/tests/dbc_parser_test.cpp index 450d5e5..1632707 100644 --- a/tests/dbc_parser_test.cpp +++ b/tests/dbc_parser_test.cpp @@ -3,6 +3,7 @@ #include "dbc_parser.h" #include "dbc_statement_handler.h" #include "dbc_tree_builder.h" +#include #include #include #include @@ -87,6 +88,9 @@ private: } } private Q_SLOTS: + void attributes(); + void invalidAttributes(); + void attributeWarnings(); void diagnostics(); void diagnosticErrors(); void diagnosticUnsupportedStatements(); @@ -113,6 +117,133 @@ private Q_SLOTS: void invalidValueDescriptions(); }; +void DbcParserTest::attributes() { + Fixture fixture; + fixture.Write( + "BA_ \"GenMsgCycleTime\" BO_ 1 25;\n" + "BA_ \"VFrameFormat\" BO_ 2147483649 1;\n" + "BA_ \"Label\" SG_ 1 Value \"a\\\"b\\\\c\";\n" + "BA_ \"NodeLabel\" BU_ ECU \"node\";\n" + "BA_ \"Environment\" EV_ Env 2;\n" + "BA_DEF_DEF_ \"GenMsgCycleTime\" 100;\n" + "BA_DEF_DEF_ \"VFrameFormat\" \"StandardCAN\";\n" + "BA_DEF_DEF_ \"Label\" \"default\";\n" + "BA_DEF_DEF_ \"BusLabel\" \"CAN\";\n" + "BA_DEF_DEF_ \"Ratio\" 1.5e-1;\n" + "BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 10000;\n" + "BA_DEF_ BO_ \"VFrameFormat\" ENUM \"StandardCAN\",\"ExtendedCAN\",\"StandardCAN_FD\";\n" + "BA_DEF_ SG_ \"Label\" STRING;\n" + "BA_DEF_ \"BusLabel\" STRING;\n" + "BA_DEF_ BO_ \"Ratio\" FLOAT -1 1;\n" + "BA_DEF_ BU_ \"NodeLabel\" STRING;\n" + "BA_DEF_ EV_ \"Environment\" HEX 0 255;\n" + "BO_ 1 Standard: 1 ECU\n" + "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n" + "BO_ 2147483649 Extended: 1 ECU\n" + "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"); + const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); + QVERIFY(!result.HasErrors()); + QVERIFY(result.diagnostics.empty()); + const DbcDatabase &database = result.database; + QCOMPARE(database.attributeDefinitions.size(), std::size_t(7)); + QCOMPARE(database.attributeAssignments.size(), std::size_t(5)); + QCOMPARE(std::get(database.attributes.at("BusLabel")), std::string("CAN")); + QCOMPARE(database.frames[0].cycleTimeMs.value(), 25U); + QCOMPARE(database.frames[1].cycleTimeMs.value(), 100U); + QCOMPARE(database.frames[1].frameFormat.value(), std::string("ExtendedCAN")); + QVERIFY(!database.frames[1].hasPgn); + QCOMPARE(std::get(database.frames[0].attributes.at("Ratio")), 0.15); + QCOMPARE(std::get(database.frames[0].signals[0].attributes.at("Label")), std::string("a\"b\\c")); + QCOMPARE(std::get(database.frames[1].signals[0].attributes.at("Label")), std::string("default")); + const DecodeDatabase runtime = DbcDecodeBuilder().Build(database); + QCOMPARE(runtime.GetFrames()[0].cycleTimeMs.value(), 25U); + QCOMPARE(runtime.GetFrames()[1].frameFormat.value(), std::string("ExtendedCAN")); + QCOMPARE(runtime.GetFrames()[0].signals[0].attributes, database.frames[0].signals[0].attributes); + const std::unique_ptr tree = DbcTreeBuilder().Build(database); + QCOMPARE(tree->GetChild(0)->GetFrame()->cycleTimeMs.value(), 25U); + QCOMPARE(tree->GetChild(0)->GetChild(0)->GetSignal()->attributes, database.frames[0].signals[0].attributes); + fixture.Write("BA_DEF_ BO_ \"Period\" INT 0 10;\nBA_DEF_DEF_ \"Period\" 1;\n" + "BA_ \"Period\" BO_ 1 2;\nBA_ \"Period\" BO_ 1 3;\nBO_ 1 F: 0 ECU\n"); + const DbcDatabase repeated = DbcParser().ParseFile(fixture.path.string()); + QCOMPARE(std::get(repeated.frames[0].attributes.at("Period")), std::int64_t(3)); + + fixture.Write("BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 4294967295;\n" + "BA_DEF_DEF_ \"GenMsgCycleTime\" 0;\n" + "BA_ \"GenMsgCycleTime\" BO_ 1 4294967295;\n" + "BA_DEF_ \"Wide\" INT -9223372036854775808 9223372036854775807;\n" + "BA_DEF_DEF_ \"Wide\" -9223372036854775808;\n" + "BA_DEF_ SG_ \"State\" ENUM \"off\",\"on\";\n" + "BA_DEF_DEF_ \"State\" 1;\n" + "BO_ 1 F: 0 ECU\nBO_ 2 Empty: 0 ECU\n"); + const DbcDatabase boundaries = DbcParser().ParseFile(fixture.path.string()); + QCOMPARE(boundaries.frames[0].cycleTimeMs.value(), std::numeric_limits::max()); + QCOMPARE(boundaries.frames[1].cycleTimeMs.value(), 0U); + QVERIFY(!boundaries.frames[0].frameFormat); + QCOMPARE(std::get(boundaries.attributes.at("Wide")), std::numeric_limits::min()); + QCOMPARE(std::get(*boundaries.attributeDefinitions.at("State").defaultValue), std::int64_t(1)); +} + +void DbcParserTest::invalidAttributes() { + Fixture fixture; + const std::vector invalid = { + "BA_DEF_ BO_ \"A\" INT 2 1;", + "BA_DEF_ BO_ \"A\" FLOAT nan 1;", + "BA_DEF_ BO_ \"A\" FLOAT 0 inf;", + "BA_DEF_ BO_ \"A\" INT 0 9223372036854775808;", + "BA_DEF_ BO_ \"A\" STRING; trailing", + "BA_DEF_ BO_ \"A\" ENUM;", + "BA_DEF_ BO_ \"A\" ENUM \"one\",;", + "BA_DEF_ BO_ \"A\" ENUM \"one\",\"one\";", + "BA_DEF_ BO_ \"VFrameFormat\" STRING;", + "BA_DEF_ SG_ \"GenMsgCycleTime\" INT 0 10;", + "BA_ \"Unknown\" BO_ 1 0;", + "BA_DEF_DEF_ \"Unknown\" 0;", + "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 1 2;", + "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 1 \"0\";", + "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" SG_ 1 S 0;", + "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 2 0;", + "BA_DEF_ SG_ \"A\" INT 0 1;\nBA_ \"A\" SG_ 1 Missing 0;", + "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_DEF_ BO_ \"A\" INT 0 1;", + "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_DEF_DEF_ \"A\" 0;\nBA_DEF_DEF_ \"A\" 1;", + "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 \"bad\\q\";", + "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 \"unterminated;", + "BA_DEF_ BO_ \"A\" ENUM \"one\";\nBA_DEF_DEF_ \"A\" \"missing\";", + "BA_DEF_ BO_ \"A\" ENUM \"one\";\nBA_ \"A\" BO_ 1 1;", + "BA_DEF_ BO_ \"GenMsgCycleTime\" INT -1 4294967296;\nBA_DEF_DEF_ \"GenMsgCycleTime\" -1;", + "BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 4294967296;\nBA_ \"GenMsgCycleTime\" BO_ 1 4294967296;"}; + for (const std::string &statement: invalid) { + fixture.Write("BO_ 1 F: 0 ECU\n" + statement + '\n'); + 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 + std::count(statement.begin(), statement.end(), '\n'))); + QVERIFY_THROWS_EXCEPTION(std::runtime_error, DbcParser().ParseFile(fixture.path.string())); + } +} + +void DbcParserTest::attributeWarnings() { + Fixture fixture; + fixture.Write("BA_DEF_ BO_ \"VFrameFormat\" ENUM \"StandardCAN\",\"ExtendedCAN\",\"J1939PG\",\"StandardCAN_FD\";\n" + "BA_DEF_DEF_ \"VFrameFormat\" \"StandardCAN_FD\";\n" + "BA_ \"VFrameFormat\" BO_ 1 2;\n" + "BA_ \"VFrameFormat\" BO_ 2147483649 0;\n" + "BO_ 1 F: 0 ECU\nBO_ 2147483649 E: 0 ECU\nBO_ 2 Default: 0 ECU\n"); + const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); + QVERIFY(!result.HasErrors()); + QCOMPARE(result.diagnostics.size(), std::size_t(3)); + const std::size_t lines[] = {3U, 4U, 2U}; + for (std::size_t index = 0; index < 3U; ++index) { + QCOMPARE(result.diagnostics[index].code, DbcDiagnosticCode::UnsupportedAttribute); + QCOMPARE(result.diagnostics[index].severity, DbcDiagnosticSeverity::Warning); + QCOMPARE(result.diagnostics[index].lineNumber, lines[index]); + QCOMPARE(result.diagnostics[index].filePath, fixture.path.string()); + } + QCOMPARE(result.database.frames[0].frameFormat.value(), std::string("J1939PG")); + QVERIFY(!result.database.frames[0].hasPgn); + QVERIFY(result.database.frames[1].isExtended); +} + void DbcParserTest::diagnostics() { Fixture fixture; fixture.Write("VERSION \"test\"\n" @@ -126,7 +257,7 @@ void DbcParserTest::diagnostics() { "BO_ 1 Frame: 1 ECU\n" "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "SIG_VALTYPE_ 1 Value : 1;\n" - "BA_ \"SignalType\" SG_ 1 Value 1;\n" + "BA_REL_ \"SignalType\" SG_ 1 Value 1;\n" "VENDOR_EXTENSION_ data;\n"); const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); CHECK(!result.HasErrors() && result.database.frames.size() == 1U, @@ -264,9 +395,7 @@ void DbcParserTest::diagnosticUnsupportedStatements() { "CM_ SG_ 1 Value \"Signal comment\";\nVAL_ 1 Value 1 \"On\";\n"; const std::vector statements = { "SIG_VALTYPE_ 1 Value : 1;", "SIG_VALTYPE_ 1 Value : 2;", - "BA_ \"SignalType\" SG_ 1 Value 1;", - "BA_DEF_ SG_ \"SignalType\" INT 0 1;", - "BA_DEF_DEF_ \"SignalType\" 0;", + "BA_REL_ \"SignalType\" SG_ 1 Value 1;", "BA_DEF_REL_ BU_SG_REL_ \"Relation\" INT 0 1;", "BA_REL_ \"Relation\" BU_SG_REL_ ECU SG_ 1 Value 1;", "SGTYPE_ Type : 8@1+ (1,0) [0|255] \"\" 0, Table;",