This commit is contained in:
deeaitch committed 2026-10-20 13:21:43 -04:00
1 parent e555ebb571
commit 849ce31e2f
17 files changed
+651 -11

No files matched your search

+44 -4
View File
@@ -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
+15
View File
@@ -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)
+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
#ifndef DBC_ATTRIBUTES_H
#define DBC_ATTRIBUTES_H
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <variant>
#include <vector>
enum class AttributeScope {
Database,
Node,
Frame,
Signal,
Environment
};
enum class AttributeType {
Integer,
Hex,
Float,
String,
Enum
};
using AttributeValue = std::variant<std::int64_t, double, std::string>;
using AttributeMap = std::map<std::string, AttributeValue>;
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<std::string> enumValues;
std::optional<AttributeValue> 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
+2
View File
@@ -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 \
+342
View File
@@ -0,0 +1,342 @@
// SPDX-License-Identifier: MIT
#include "dbc_statement_handler.h"
#include <charconv>
#include <cmath>
#include <limits>
#include <set>
namespace {
struct Token {
std::string text;
bool quoted = false;
};
std::vector<Token> Tokenize(const std::string &text) {
std::vector<Token> 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<Token> 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<std::int64_t>(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<std::string> 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<std::int64_t>(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<std::string, AttributeDefinition>::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<std::uint32_t>::max() || (raw & 0x60000000U) != 0)
throw std::runtime_error("Invalid attribute CAN ID.");
const std::uint32_t id = static_cast<std::uint32_t>(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<std::int64_t>(value);
if (milliseconds < 0 || milliseconds > std::numeric_limits<std::uint32_t>::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<DbcDiagnostic> &diagnostics) {
std::map<std::string, std::size_t> defaultLines;
std::vector<std::size_t> 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<std::int64_t>(attribute.second);
if (attribute.first == "GenMsgCycleTime") {
frame.cycleTimeMs = static_cast<std::uint32_t>(value);
continue;
}
frame.frameFormat = database.attributeDefinitions.at(attribute.first).enumValues[static_cast<std::size_t>(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});
}
}
}
+3
View File
@@ -21,6 +21,9 @@
*/
struct DbcDatabase {
std::vector<FrameInfo> frames; /**< All frames found in the DBC file. */
std::map<std::string, AttributeDefinition> attributeDefinitions = {};
std::vector<AttributeAssignment> attributeAssignments = {};
AttributeMap attributes = {}; /**< Effective database attributes. */
};
#endif /* DBC_DATABASE_H */
+4
View File
@@ -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;
+3 -1
View File
@@ -19,7 +19,9 @@ enum class DbcDiagnosticCode {
InvalidStatement,
InvalidValueDescription,
InvalidMultiplexing,
UnsupportedStatement
UnsupportedStatement,
InvalidAttribute,
UnsupportedAttribute
};
struct DbcDiagnostic {
+1
View File
@@ -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;
+1
View File
@@ -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.
*
+3
View File
@@ -385,6 +385,9 @@ DbcStatementDispatcher::DbcStatementDispatcher() {
RegisterType<DbcSignalHandler>("SG_");
RegisterType<DbcCommentHandler>("CM_");
RegisterType<DbcValueDescriptionHandler>("VAL_");
RegisterType<DbcAttributeHandler>("BA_DEF_");
RegisterType<DbcAttributeHandler>("BA_DEF_DEF_");
RegisterType<DbcAttributeHandler>("BA_");
}
void DbcHeaderHandler::Handle(const std::string &, DbcParseContext &) const {}
+12
View File
@@ -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<PendingComment> pendingComments = {};
void ResolveComments(const std::string &filePath);
std::vector<PendingAttribute> pendingAttributes = {};
void ResolveAttributes(const std::string &filePath, std::vector<DbcDiagnostic> &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 {
+7
View File
@@ -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<DecodeSignal> signals; /**< Signal definitions. */
AttributeMap attributes;
std::optional<std::string> frameFormat;
std::optional<std::uint32_t> cycleTimeMs;
DecodeFrame() = default;
FrameKey GetKey() const {
+6
View File
@@ -12,6 +12,7 @@
#ifndef FRAME_INFO_H
#define FRAME_INFO_H
#include "attributes.h"
#include <cstdint>
#include <string>
#include <vector>
@@ -32,6 +33,11 @@ struct FrameInfo {
std::string comment; /**< Optional frame comment. */
std::vector<SignalInfo> signals; /**< Signals contained in the frame. */
std::optional<std::string> frameFormat; /**< VFrameFormat enum label. */
std::optional<std::uint32_t> cycleTimeMs; /**< GenMsgCycleTime; zero means unspecified. */
AttributeMap attributes; /**< Effective values, including defaults. */
FrameInfo() = default;
};
+4 -1
View File
@@ -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 <cstdint>
#include <string>
#include <vector>
@@ -37,6 +38,8 @@ struct SignalInfo {
ValueDescriptions valueDescriptions; /**< Raw integer state descriptions. */
std::string comment; /**< Optional signal comment. */
AttributeMap attributes; /**< Effective values, including defaults. */
SignalInfo() = default;
};
+18 -1
View File
@@ -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.
+133 -4
View File
@@ -3,6 +3,7 @@
#include "dbc_parser.h"
#include "dbc_statement_handler.h"
#include "dbc_tree_builder.h"
#include <algorithm>
#include <clocale>
#include <limits>
#include <locale>
@@ -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<std::string>(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<double>(database.frames[0].attributes.at("Ratio")), 0.15);
QCOMPARE(std::get<std::string>(database.frames[0].signals[0].attributes.at("Label")), std::string("a\"b\\c"));
QCOMPARE(std::get<std::string>(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<TreeNode> 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<std::int64_t>(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<std::uint32_t>::max());
QCOMPARE(boundaries.frames[1].cycleTimeMs.value(), 0U);
QVERIFY(!boundaries.frames[0].frameFormat);
QCOMPARE(std::get<std::int64_t>(boundaries.attributes.at("Wide")), std::numeric_limits<std::int64_t>::min());
QCOMPARE(std::get<std::int64_t>(*boundaries.attributeDefinitions.at("State").defaultValue), std::int64_t(1));
}
void DbcParserTest::invalidAttributes() {
Fixture fixture;
const std::vector<std::string> 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<std::string> 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;",