Compare commits

...
3 Commits
9 changed files with 668 additions and 329 deletions

No files matched your search

+71 -10
View File
@@ -151,8 +151,11 @@ Files:
- `dbc_parser.h` - `dbc_parser.h`
- `dbc_parser.cpp` - `dbc_parser.cpp`
- `dbc_statement_handler.h`
- `dbc_statement_handler.cpp`
- `typefactory.h`
These parse the DBC file itself. The parser reads files and reports errors; registered statement handlers parse DBC constructs.
### Demo ### Demo
@@ -505,6 +508,36 @@ Parsed fields:
--- ---
### Frame validation and parse errors
`BO_` definitions require a decimal CAN ID, a nonempty single-token frame name,
a colon, a decimal payload length, and a transmitter token. Spaces or tabs may
separate fields, and whitespace before the colon is accepted. Additional trailing
fields are rejected.
Numeric fields must contain decimal digits only; signs, hexadecimal notation,
fractional values, suffixes, and overflow are rejected. CAN IDs may contain the
29-bit identifier plus the optional DBC extended flag (bit 31). Bits 29 and 30
are rejected rather than silently discarded. For compatibility, unflagged IDs
above `0x7FF` are still treated as extended.
The payload length must fit `uint8_t` (`0..255`). This is a storage-range check,
not CAN or CAN FD protocol validation; no new 8-byte or 64-byte limit is imposed.
Parsing stops at the first detected error. Exceptions raised while parsing a
line are reported as `std::runtime_error` with the supplied file path and the
one-based physical line number, including blank lines:
```text
example.dbc:3: Invalid CAN ID: invalid
```
This change validates frame definitions. Signal syntax and unsupported DBC
constructs retain their existing behavior; comprehensive validation is still
planned. File-open errors contain the path but have no line number.
---
## Signal definition ## Signal definition
Example: Example:
@@ -737,20 +770,47 @@ That is why `DecodeDatabase` is a separate layer.
--- ---
## Why No Abstract Factory Is Used ## Statement Handler Factory
At the current stage, abstract factory is intentionally avoided. `DbcParser` owns file reading, trimming, line numbers, and error reporting.
Each `ParseFile()` call creates a fresh `DbcParseContext` and a
`DbcStatementDispatcher`. The context contains the parsed database and current
frame; handlers receive it by reference and retain no parsing state.
The current design is already clean: The dispatcher uses `TypeFactory<std::string, DbcStatementHandler>` to create
registered handlers once per parsing session, then reuses them for all matching
lines:
```text - `BO_` → `DbcFrameHandler`
parser → parsed database → decode database - `SG_` → `DbcSignalHandler`
↘ tree builder → UI tree - `CM_` → `DbcCommentHandler`
Grammar implementations live in `dbc_statement_handler.cpp`; their interface,
context, and dispatcher live in `dbc_statement_handler.h`. `typefactory.h`
provides the generic registry and object creation.
To add a construct, implement a default-constructible handler deriving from
`DbcStatementHandler` with:
```cpp
void Handle(const std::string &line, DbcParseContext &context) const override;
``` ```
Introducing factory layers now would increase complexity without solving an immediate problem. Register it in the dispatcher constructor:
If later the project requires multiple output representations or multiple build strategies, that can be added then. ```cpp
RegisterType<DbcValueHandler>("VAL_"); // after implementing DbcValueHandler
```
Registration creates one handler; replacing a registration replaces that instance.
Register during initialization before dispatching. A line is routed using its
full first whitespace-delimited keyword, so `BO_TX_BU_` cannot match `BO_`.
Unregistered keywords remain ignored. Existing `SG_` and `CM_` syntax policies
are preserved, including requiring a literal space after those keywords.
This factory handles statements inside DBC files. A separate format-level
factory can be introduced when another file format is implemented. The decoder
and database builders remain independent of statement dispatch.
--- ---
@@ -871,5 +931,6 @@ This gives FrameTap a much better foundation for real use, because both browsing
## Regression tests ## Regression tests
See [tests/README.md](tests/README.md) for compiler and qmake commands. See [tests/README.md](tests/README.md) for compiler and qmake commands.
Tests cover Intel and Motorola extraction, signed and unsigned 64-bit boundaries, Tests cover frame validation and error locations, Intel and Motorola extraction,
signed and unsigned 64-bit boundaries,
physical conversion, numeric output, and invalid signal payloads. physical conversion, numeric output, and invalid signal payloads.
+3
View File
@@ -7,6 +7,7 @@ SOURCES += \
dbc_decode_builder.cpp \ dbc_decode_builder.cpp \
dbc_decoder.cpp \ dbc_decoder.cpp \
dbc_parser.cpp \ dbc_parser.cpp \
dbc_statement_handler.cpp \
dbc_tree_builder.cpp \ dbc_tree_builder.cpp \
main.cpp \ main.cpp \
tree_node.cpp tree_node.cpp
@@ -16,6 +17,8 @@ HEADERS += \
dbc_decode_builder.h \ dbc_decode_builder.h \
dbc_decoder.h \ dbc_decoder.h \
dbc_parser.h \ dbc_parser.h \
dbc_statement_handler.h \
typefactory.h \
dbc_tree_builder.h \ dbc_tree_builder.h \
decode_database.h \ decode_database.h \
frame_info.h \ frame_info.h \
+11 -297
View File
@@ -10,9 +10,9 @@
*/ */
#include "dbc_parser.h" #include "dbc_parser.h"
#include "dbc_statement_handler.h"
#include <fstream> #include <fstream>
#include <sstream>
#include <stdexcept> #include <stdexcept>
#include <cctype> #include <cctype>
@@ -43,314 +43,28 @@ DbcDatabase DbcParser::ParseFile (const std::string &filePath) const {
throw std::runtime_error ("Failed to open DBC file: " + filePath); throw std::runtime_error ("Failed to open DBC file: " + filePath);
DbcDatabase database; DbcDatabase database;
FrameInfo *currentFrame = nullptr; DbcParseContext context {database, nullptr};
const DbcStatementDispatcher dispatcher;
std::size_t lineNumber = 0U;
std::string line; std::string line;
while (std::getline (input, line)) { while (std::getline (input, line)) {
++lineNumber;
line = Trim (line); line = Trim (line);
if (line.empty()) if (line.empty())
continue; continue;
if (IsFrameLine (line)) { try {
FrameInfo frame = ParseFrameLine (line); dispatcher.Dispatch (line, context);
database.frames.push_back (frame); } catch (const std::exception &error) {
currentFrame = &database.frames.back(); throw std::runtime_error (filePath + ":" + std::to_string (lineNumber) +
} else if (IsSignalLine (line)) { ": " + error.what());
if (currentFrame == nullptr) }
throw std::runtime_error ("Signal found before any frame definition.");
SignalInfo signal = ParseSignalLine (line);
currentFrame->signals.push_back (signal);
} else if (IsCommentLine (line))
ParseCommentLine (line, database);
} }
return database; return database;
} }
bool DbcParser::IsFrameLine (const std::string &line) {
return (line.size() >= 4U) && (line.compare (0U, 4U, "BO_ ") == 0);
}
bool DbcParser::IsSignalLine (const std::string &line) {
return (line.size() >= 4U) && (line.compare (0U, 4U, "SG_ ") == 0);
}
bool DbcParser::IsCommentLine (const std::string &line) {
return (line.size() >= 4U) && (line.compare (0U, 4U, "CM_ ") == 0);
}
std::string DbcParser::Trim (const std::string &text) { std::string DbcParser::Trim (const std::string &text) {
return TrimText (text); return TrimText (text);
} }
std::vector<std::string> DbcParser::SplitReceivers (const std::string &text) {
std::vector<std::string> receivers;
std::string token;
std::istringstream stream (text);
while (std::getline (stream, token, ',')) {
token = TrimText (token);
if (!token.empty())
receivers.push_back (token);
}
return receivers;
}
void DbcParser::NormalizeCanId (std::uint32_t rawCanId,
std::uint32_t &normalizedCanId,
bool &isExtended) {
/*
* DBC commonly stores extended identifiers with bit 31 set.
* Example:
* raw id = 0x80000000 | actual_29_bit_id
*/
if ((rawCanId & 0x80000000U) != 0U) {
isExtended = true;
normalizedCanId = (rawCanId & 0x1FFFFFFFU);
} else {
isExtended = (rawCanId > 0x7FFU);
normalizedCanId = rawCanId;
}
}
std::uint32_t DbcParser::TryExtractPgn (std::uint32_t canId, bool isExtended, bool &hasPgn) {
hasPgn = false;
if (!isExtended)
return 0U;
if ((canId & 0x1FFFFFFFU) != canId)
return 0U;
const std::uint32_t pf = (canId >> 16U) & 0xFFU;
const std::uint32_t ps = (canId >> 8U) & 0xFFU;
const std::uint32_t dp = (canId >> 24U) & 0x01U;
std::uint32_t pgn = 0U;
if (pf < 240U)
pgn = (dp << 16U) | (pf << 8U);
else
pgn = (dp << 16U) | (pf << 8U) | ps;
hasPgn = true;
return pgn;
}
FrameInfo DbcParser::ParseFrameLine (const std::string &line) {
/*
* Example:
* BO_ 256 EngineData: 8 EEC1
*/
std::istringstream stream (line);
std::string token;
FrameInfo frame;
std::uint32_t rawCanId = 0U;
stream >> token;
if (token != "BO_")
throw std::runtime_error ("Invalid frame line: " + line);
stream >> rawCanId;
NormalizeCanId (rawCanId, frame.canId, frame.isExtended);
stream >> token;
if (token.empty())
throw std::runtime_error ("Missing frame name: " + line);
if (token[token.size() - 1U] == ':')
token.erase (token.size() - 1U, 1U);
frame.name = token;
{
unsigned int dlcValue = 0U;
stream >> dlcValue;
frame.dlc = static_cast<std::uint8_t> (dlcValue);
}
stream >> frame.transmitter;
frame.pgn = TryExtractPgn (frame.canId, frame.isExtended, frame.hasPgn);
return frame;
}
SignalInfo DbcParser::ParseSignalLine (const std::string &line) {
/*
* Example:
* SG_ EngineSpeed : 0|16@1+ (0.125,0) [0|8000] "rpm" ECU1,ECU2
*/
SignalInfo signal;
std::string work = TrimText (line);
if (work.compare (0U, 4U, "SG_ ") != 0)
throw std::runtime_error ("Invalid signal line: " + line);
work.erase (0U, 4U);
const std::string::size_type colonPos = work.find (':');
if (colonPos == std::string::npos)
throw std::runtime_error ("Signal line missing ':' : " + line);
signal.name = TrimText (work.substr (0U, colonPos));
std::string rest = TrimText (work.substr (colonPos + 1U));
const std::string::size_type pipePos = rest.find ('|');
const std::string::size_type atPos = rest.find ('@');
const std::string::size_type signPos = rest.find_first_of ("+-", atPos);
const std::string::size_type factorBegin = rest.find ('(');
const std::string::size_type factorComma = rest.find (',', factorBegin);
const std::string::size_type factorEnd = rest.find (')', factorComma);
const std::string::size_type rangeBegin = rest.find ('[');
const std::string::size_type rangeSep = rest.find ('|', rangeBegin);
const std::string::size_type rangeEnd = rest.find (']', rangeSep);
const std::string::size_type unitBegin = rest.find ('"', rangeEnd);
const std::string::size_type unitEnd = rest.find ('"', unitBegin + 1U);
if ((pipePos == std::string::npos) ||
(atPos == std::string::npos) ||
(signPos == std::string::npos) ||
(factorBegin == std::string::npos) ||
(factorComma == std::string::npos) ||
(factorEnd == std::string::npos) ||
(rangeBegin == std::string::npos) ||
(rangeSep == std::string::npos) ||
(rangeEnd == std::string::npos) ||
(unitBegin == std::string::npos) ||
(unitEnd == std::string::npos))
throw std::runtime_error ("Unsupported signal syntax: " + line);
signal.startBit = static_cast<std::uint32_t> (
std::stoul (TrimText (rest.substr (0U, pipePos)))
);
signal.length = static_cast<std::uint32_t> (
std::stoul (TrimText (rest.substr (pipePos + 1U, atPos - pipePos - 1U)))
);
{
if ((atPos + 1U) >= rest.size())
throw std::runtime_error ("Invalid endianness in signal: " + line);
const char endianChar = rest[atPos + 1U];
signal.isLittleEndian = (endianChar == '1');
}
{
const char signChar = rest[signPos];
signal.isSigned = (signChar == '-');
}
signal.factor = std::stod (
TrimText (rest.substr (factorBegin + 1U, factorComma - factorBegin - 1U))
);
signal.offset = std::stod (
TrimText (rest.substr (factorComma + 1U, factorEnd - factorComma - 1U))
);
signal.minimum = std::stod (
TrimText (rest.substr (rangeBegin + 1U, rangeSep - rangeBegin - 1U))
);
signal.maximum = std::stod (
TrimText (rest.substr (rangeSep + 1U, rangeEnd - rangeSep - 1U))
);
signal.unit = rest.substr (unitBegin + 1U, unitEnd - unitBegin - 1U);
{
const std::string receiversText = TrimText (rest.substr (unitEnd + 1U));
signal.receivers = SplitReceivers (receiversText);
}
return signal;
}
void DbcParser::ParseCommentLine (const std::string &line, DbcDatabase &database) {
/*
* Examples:
* CM_ BO_ 256 "Frame comment";
* CM_ SG_ 256 EngineSpeed "Signal comment";
*/
std::istringstream stream (line);
std::string token;
stream >> token;
if (token != "CM_")
return;
stream >> token;
if (token == "BO_") {
std::uint32_t rawCanId = 0U;
std::uint32_t canId = 0U;
bool isExtended = false;
stream >> rawCanId;
NormalizeCanId (rawCanId, canId, isExtended);
const std::string::size_type quoteBegin = line.find ('"');
const std::string::size_type quoteEnd = line.rfind ('"');
if ((quoteBegin == std::string::npos) ||
(quoteEnd == std::string::npos) ||
(quoteEnd <= quoteBegin))
return;
FrameInfo *frame = FindFrameById (database, canId, isExtended);
if (frame != nullptr)
frame->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U);
} else if (token == "SG_") {
std::uint32_t rawCanId = 0U;
std::uint32_t canId = 0U;
bool isExtended = false;
std::string signalName;
stream >> rawCanId;
stream >> signalName;
NormalizeCanId (rawCanId, canId, isExtended);
const std::string::size_type quoteBegin = line.find ('"');
const std::string::size_type quoteEnd = line.rfind ('"');
if ((quoteBegin == std::string::npos) ||
(quoteEnd == std::string::npos) ||
(quoteEnd <= quoteBegin))
return;
FrameInfo *frame = FindFrameById (database, canId, isExtended);
if (frame != nullptr) {
SignalInfo *signal = FindSignalByName (*frame, signalName);
if (signal != nullptr)
signal->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U);
}
}
}
FrameInfo *DbcParser::FindFrameById (DbcDatabase &database,
std::uint32_t canId,
bool isExtended) {
for (std::size_t index = 0U; index < database.frames.size(); ++index) {
if ((database.frames[index].canId == canId) &&
(database.frames[index].isExtended == isExtended))
return &database.frames[index];
}
return nullptr;
}
SignalInfo *DbcParser::FindSignalByName (FrameInfo &frame, const std::string &signalName) {
for (std::size_t index = 0U; index < frame.signals.size(); ++index) {
if (frame.signals[index].name == signalName)
return &frame.signals[index];
}
return nullptr;
}
+1 -22
View File
@@ -13,8 +13,6 @@
#define DBC_PARSER_H #define DBC_PARSER_H
#include <string> #include <string>
#include <vector>
#include <cstdint>
#include "dbc_database.h" #include "dbc_database.h"
@@ -39,31 +37,12 @@ class DbcParser {
* @param filePath Path to input file. * @param filePath Path to input file.
* @return Parsed database. * @return Parsed database.
* @throws std::runtime_error on file or parse errors. * @throws std::runtime_error on file or parse errors.
* Line parse errors include the file path and one-based line number.
*/ */
DbcDatabase ParseFile (const std::string &filePath) const; DbcDatabase ParseFile (const std::string &filePath) const;
private: private:
static bool IsFrameLine (const std::string &line);
static bool IsSignalLine (const std::string &line);
static bool IsCommentLine (const std::string &line);
static std::string Trim (const std::string &text); static std::string Trim (const std::string &text);
static std::vector<std::string> SplitReceivers (const std::string &text);
static std::uint32_t TryExtractPgn (std::uint32_t canId, bool isExtended, bool &hasPgn);
static void NormalizeCanId (std::uint32_t rawCanId,
std::uint32_t &normalizedCanId,
bool &isExtended);
static FrameInfo ParseFrameLine (const std::string &line);
static SignalInfo ParseSignalLine (const std::string &line);
static void ParseCommentLine (const std::string &line, DbcDatabase &database);
static FrameInfo *FindFrameById (DbcDatabase &database,
std::uint32_t canId,
bool isExtended);
static SignalInfo *FindSignalByName (FrameInfo &frame, const std::string &signalName);
}; };
#endif /* DBC_PARSER_H */ #endif /* DBC_PARSER_H */
+358
View File
@@ -0,0 +1,358 @@
// SPDX-License-Identifier: MIT
#include "dbc_statement_handler.h"
#include <sstream>
#include <stdexcept>
#include <cctype>
#include <charconv>
#include <limits>
namespace {
std::uint32_t ParseDecimal (const std::string &text,
std::uint32_t maximum,
const char *field) {
std::uint32_t value = 0U;
const auto parsed = std::from_chars (text.data(), text.data() + text.size(), value);
if (text.empty() || parsed.ec != std::errc() ||
parsed.ptr != text.data() + text.size() || value > maximum)
throw std::runtime_error (std::string ("Invalid ") + field + ": " + text);
return value;
}
/**
* @brief Remove leading and trailing spaces.
* @param text Input text.
* @return Trimmed text.
*/
std::string TrimText (const std::string &text) {
std::string::size_type begin = 0U;
while ((begin < text.size()) &&
std::isspace (static_cast<unsigned char> (text[begin])))
++begin;
std::string::size_type end = text.size();
while ((end > begin) &&
std::isspace (static_cast<unsigned char> (text[end - 1U])))
--end;
return text.substr (begin, end - begin);
}
FrameInfo *FindFrameById (DbcDatabase &, std::uint32_t, bool);
SignalInfo *FindSignalByName (FrameInfo &, const std::string &);
}
namespace {
std::vector<std::string> SplitReceivers (const std::string &text) {
std::vector<std::string> receivers;
std::string token;
std::istringstream stream (text);
while (std::getline (stream, token, ',')) {
token = TrimText (token);
if (!token.empty())
receivers.push_back (token);
}
return receivers;
}
void NormalizeCanId (std::uint32_t rawCanId,
std::uint32_t &normalizedCanId,
bool &isExtended) {
/*
* DBC commonly stores extended identifiers with bit 31 set.
* Example:
* raw id = 0x80000000 | actual_29_bit_id
*/
if ((rawCanId & 0x80000000U) != 0U) {
isExtended = true;
normalizedCanId = (rawCanId & 0x1FFFFFFFU);
} else {
isExtended = (rawCanId > 0x7FFU);
normalizedCanId = rawCanId;
}
}
std::uint32_t TryExtractPgn (std::uint32_t canId, bool isExtended, bool &hasPgn) {
hasPgn = false;
if (!isExtended)
return 0U;
if ((canId & 0x1FFFFFFFU) != canId)
return 0U;
const std::uint32_t pf = (canId >> 16U) & 0xFFU;
const std::uint32_t ps = (canId >> 8U) & 0xFFU;
const std::uint32_t dp = (canId >> 24U) & 0x01U;
std::uint32_t pgn = 0U;
if (pf < 240U)
pgn = (dp << 16U) | (pf << 8U);
else
pgn = (dp << 16U) | (pf << 8U) | ps;
hasPgn = true;
return pgn;
}
}
FrameInfo DbcFrameHandler::ParseFrameLine (const std::string &line) {
/*
* Example:
* BO_ 256 EngineData: 8 EEC1
*/
std::istringstream stream (line);
std::string keyword;
std::string idText;
if (!(stream >> keyword >> idText) || keyword != "BO_")
throw std::runtime_error ("Missing CAN ID in frame definition.");
const std::uint32_t rawCanId = ParseDecimal (
idText, std::numeric_limits<std::uint32_t>::max(), "CAN ID");
// Only bit 31 may accompany the actual 29-bit identifier.
if ((rawCanId & 0x60000000U) != 0U)
throw std::runtime_error ("CAN ID exceeds the 29-bit identifier range.");
std::string nameText;
if (!std::getline (stream, nameText, ':') || stream.eof())
throw std::runtime_error ("Missing ':' in frame definition.");
std::istringstream nameStream (nameText);
FrameInfo frame;
std::string extra;
if (!(nameStream >> frame.name) || (nameStream >> extra))
throw std::runtime_error ("Missing or invalid frame name.");
std::string lengthText;
if (!(stream >> lengthText))
throw std::runtime_error ("Missing payload length in frame definition.");
frame.dlc = static_cast<std::uint8_t> (ParseDecimal (
lengthText, std::numeric_limits<std::uint8_t>::max(), "payload length"));
if (!(stream >> frame.transmitter))
throw std::runtime_error ("Missing transmitter in frame definition.");
if (stream >> extra)
throw std::runtime_error ("Unexpected trailing field in frame definition: " + extra);
NormalizeCanId (rawCanId, frame.canId, frame.isExtended);
frame.pgn = TryExtractPgn (frame.canId, frame.isExtended, frame.hasPgn);
return frame;
}
SignalInfo DbcSignalHandler::ParseSignalLine (const std::string &line) {
/*
* Example:
* SG_ EngineSpeed : 0|16@1+ (0.125,0) [0|8000] "rpm" ECU1,ECU2
*/
SignalInfo signal;
std::string work = TrimText (line);
if (work.compare (0U, 4U, "SG_ ") != 0)
throw std::runtime_error ("Invalid signal line: " + line);
work.erase (0U, 4U);
const std::string::size_type colonPos = work.find (':');
if (colonPos == std::string::npos)
throw std::runtime_error ("Signal line missing ':' : " + line);
signal.name = TrimText (work.substr (0U, colonPos));
std::string rest = TrimText (work.substr (colonPos + 1U));
const std::string::size_type pipePos = rest.find ('|');
const std::string::size_type atPos = rest.find ('@');
const std::string::size_type signPos = rest.find_first_of ("+-", atPos);
const std::string::size_type factorBegin = rest.find ('(');
const std::string::size_type factorComma = rest.find (',', factorBegin);
const std::string::size_type factorEnd = rest.find (')', factorComma);
const std::string::size_type rangeBegin = rest.find ('[');
const std::string::size_type rangeSep = rest.find ('|', rangeBegin);
const std::string::size_type rangeEnd = rest.find (']', rangeSep);
const std::string::size_type unitBegin = rest.find ('"', rangeEnd);
const std::string::size_type unitEnd = rest.find ('"', unitBegin + 1U);
if ((pipePos == std::string::npos) ||
(atPos == std::string::npos) ||
(signPos == std::string::npos) ||
(factorBegin == std::string::npos) ||
(factorComma == std::string::npos) ||
(factorEnd == std::string::npos) ||
(rangeBegin == std::string::npos) ||
(rangeSep == std::string::npos) ||
(rangeEnd == std::string::npos) ||
(unitBegin == std::string::npos) ||
(unitEnd == std::string::npos))
throw std::runtime_error ("Unsupported signal syntax: " + line);
signal.startBit = static_cast<std::uint32_t> (
std::stoul (TrimText (rest.substr (0U, pipePos)))
);
signal.length = static_cast<std::uint32_t> (
std::stoul (TrimText (rest.substr (pipePos + 1U, atPos - pipePos - 1U)))
);
{
if ((atPos + 1U) >= rest.size())
throw std::runtime_error ("Invalid endianness in signal: " + line);
const char endianChar = rest[atPos + 1U];
signal.isLittleEndian = (endianChar == '1');
}
{
const char signChar = rest[signPos];
signal.isSigned = (signChar == '-');
}
signal.factor = std::stod (
TrimText (rest.substr (factorBegin + 1U, factorComma - factorBegin - 1U))
);
signal.offset = std::stod (
TrimText (rest.substr (factorComma + 1U, factorEnd - factorComma - 1U))
);
signal.minimum = std::stod (
TrimText (rest.substr (rangeBegin + 1U, rangeSep - rangeBegin - 1U))
);
signal.maximum = std::stod (
TrimText (rest.substr (rangeSep + 1U, rangeEnd - rangeSep - 1U))
);
signal.unit = rest.substr (unitBegin + 1U, unitEnd - unitBegin - 1U);
{
const std::string receiversText = TrimText (rest.substr (unitEnd + 1U));
signal.receivers = SplitReceivers (receiversText);
}
return signal;
}
void DbcCommentHandler::ParseCommentLine (const std::string &line, DbcDatabase &database) {
/*
* Examples:
* CM_ BO_ 256 "Frame comment";
* CM_ SG_ 256 EngineSpeed "Signal comment";
*/
std::istringstream stream (line);
std::string token;
stream >> token;
if (token != "CM_")
return;
stream >> token;
if (token == "BO_") {
std::uint32_t rawCanId = 0U;
std::uint32_t canId = 0U;
bool isExtended = false;
stream >> rawCanId;
NormalizeCanId (rawCanId, canId, isExtended);
const std::string::size_type quoteBegin = line.find ('"');
const std::string::size_type quoteEnd = line.rfind ('"');
if ((quoteBegin == std::string::npos) ||
(quoteEnd == std::string::npos) ||
(quoteEnd <= quoteBegin))
return;
FrameInfo *frame = FindFrameById (database, canId, isExtended);
if (frame != nullptr)
frame->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U);
} else if (token == "SG_") {
std::uint32_t rawCanId = 0U;
std::uint32_t canId = 0U;
bool isExtended = false;
std::string signalName;
stream >> rawCanId;
stream >> signalName;
NormalizeCanId (rawCanId, canId, isExtended);
const std::string::size_type quoteBegin = line.find ('"');
const std::string::size_type quoteEnd = line.rfind ('"');
if ((quoteBegin == std::string::npos) ||
(quoteEnd == std::string::npos) ||
(quoteEnd <= quoteBegin))
return;
FrameInfo *frame = FindFrameById (database, canId, isExtended);
if (frame != nullptr) {
SignalInfo *signal = FindSignalByName (*frame, signalName);
if (signal != nullptr)
signal->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U);
}
}
}
namespace {
FrameInfo *FindFrameById (DbcDatabase &database,
std::uint32_t canId,
bool isExtended) {
for (std::size_t index = 0U; index < database.frames.size(); ++index) {
if ((database.frames[index].canId == canId) &&
(database.frames[index].isExtended == isExtended))
return &database.frames[index];
}
return nullptr;
}
SignalInfo *FindSignalByName (FrameInfo &frame, const std::string &signalName) {
for (std::size_t index = 0U; index < frame.signals.size(); ++index) {
if (frame.signals[index].name == signalName)
return &frame.signals[index];
}
return nullptr;
}
}
void DbcFrameHandler::Handle (const std::string &line, DbcParseContext &context) const {
context.database.frames.push_back (ParseFrameLine (line));
context.currentFrame = &context.database.frames.back();
}
void DbcSignalHandler::Handle (const std::string &line, DbcParseContext &context) const {
// Preserve the existing signal syntax policy during this refactor.
if (line.compare (0U, 4U, "SG_ ") != 0)
return;
if (context.currentFrame == nullptr)
throw std::runtime_error ("Signal found before any frame definition.");
context.currentFrame->signals.push_back (ParseSignalLine (line));
}
void DbcCommentHandler::Handle (const std::string &line, DbcParseContext &context) const {
if (line.compare (0U, 4U, "CM_ ") == 0)
ParseCommentLine (line, context.database);
}
DbcStatementDispatcher::DbcStatementDispatcher() {
RegisterType<DbcFrameHandler> ("BO_");
RegisterType<DbcSignalHandler> ("SG_");
RegisterType<DbcCommentHandler> ("CM_");
}
bool DbcStatementDispatcher::Dispatch (const std::string &line,
DbcParseContext &context) const {
std::istringstream stream (line);
std::string keyword;
stream >> keyword;
const auto handler = m_handlers.find (keyword);
if (handler == m_handlers.end())
return false;
handler->second->Handle (line, context);
return true;
}
+62
View File
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT
#ifndef DBC_STATEMENT_HANDLER_H
#define DBC_STATEMENT_HANDLER_H
#include "dbc_database.h"
#include "typefactory.h"
#include <string>
#include <unordered_map>
/** Mutable state belonging to one file parse. Handlers do not retain it. */
struct DbcParseContext {
DbcDatabase &database;
FrameInfo *currentFrame = nullptr;
};
class DbcStatementHandler {
public:
virtual ~DbcStatementHandler() = default;
virtual void Handle (const std::string &line, DbcParseContext &context) const = 0;
};
class DbcFrameHandler : public DbcStatementHandler {
public:
void Handle (const std::string &line, DbcParseContext &context) const override;
private:
static FrameInfo ParseFrameLine (const std::string &line);
};
class DbcSignalHandler : public DbcStatementHandler {
public:
void Handle (const std::string &line, DbcParseContext &context) const override;
private:
static SignalInfo ParseSignalLine (const std::string &line);
};
class DbcCommentHandler : public DbcStatementHandler {
public:
void Handle (const std::string &line, DbcParseContext &context) const override;
private:
static void ParseCommentLine (const std::string &line, DbcDatabase &database);
};
/** Exact-key dispatch to handlers created once per registration via TypeFactory. */
class DbcStatementDispatcher {
public:
DbcStatementDispatcher();
template <class Handler>
void RegisterType (const std::string &keyword) {
m_factory.registerType<Handler> (keyword);
m_handlers.insert_or_assign (keyword, m_factory.create (keyword));
}
/** Returns false for an unregistered keyword; input must be trimmed. */
bool Dispatch (const std::string &line, DbcParseContext &context) const;
private:
TypeFactory<std::string, DbcStatementHandler> m_factory;
std::unordered_map<std::string, std::shared_ptr<DbcStatementHandler> > m_handlers;
};
#endif /* DBC_STATEMENT_HANDLER_H */
+22
View File
@@ -26,3 +26,25 @@ signals, signed values, scaling, 64-bit extraction, signed/unsigned boundary val
(including `INT64_MIN`, `INT64_MAX`, and `UINT64_MAX`), numeric output, invalid lengths, insufficient (including `INT64_MIN`, `INT64_MAX`, and `UINT64_MAX`), numeric output, invalid lengths, insufficient
payloads, and Intel regression cases. Frame validity means the definition was payloads, and Intel regression cases. Frame validity means the definition was
found; insufficient payloads are checked through signal validity. found; insufficient payloads are checked through signal validity.
## Parser regression tests
Run from the repository root:
```sh
g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -I. \
tests/dbc_parser_test.cpp dbc_parser.cpp dbc_statement_handler.cpp -o /tmp/dbc_parser_test
/tmp/dbc_parser_test
```
For qmake, use `tests/dbc_parser_test.pro` in a separate build directory,
following the decoder instructions above, then run `./dbc_parser_test`.
Parser tests cover standard and extended CAN IDs, whitespace, payload storage
boundaries, missing fields, malformed and overflowing numbers, reserved ID bits,
error paths and line numbers, and integration with signals and comments.
Temporary DBC fixtures are created in the system temporary directory and removed
automatically.
Dispatch tests also verify exact keyword matching, handler construction and reuse,
unknown statements, and independent parsing sessions, including recovery after errors.
+133
View File
@@ -0,0 +1,133 @@
#include "dbc_parser.h"
#include "dbc_statement_handler.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
struct Fixture {
std::filesystem::path path = std::filesystem::temp_directory_path() /
("dbc_parser_test_" + std::to_string (
std::chrono::steady_clock::now().time_since_epoch().count()) + ".dbc");
~Fixture() { std::error_code error; std::filesystem::remove (path, error); }
void Write (const std::string &text) const {
std::ofstream output (path);
output << text;
if (!output)
throw std::runtime_error ("Cannot write test fixture.");
}
};
class CountingHandler : public DbcStatementHandler {
public:
inline static int instances = 0;
inline static int calls = 0;
CountingHandler() { ++instances; }
void Handle (const std::string &, DbcParseContext &context) const override {
++calls;
context.database.frames.emplace_back();
}
};
int main() {
Fixture fixture;
int failures = 0;
std::size_t count = 0U;
const auto check = [&] (bool passed, const std::string &name) {
++count;
if (!passed) { ++failures; std::cerr << "FAIL: " << name << '\n'; }
};
struct ValidCase { const char *line; std::uint32_t id; bool extended; unsigned length; };
const ValidCase valid[] = {
{"BO_ 0 Frame: 0 ECU", 0U, false, 0U},
{"BO_ 2047 Frame: 8 ECU", 2047U, false, 8U},
{"BO_ 2048 Frame: 64 ECU", 2048U, true, 64U},
{"BO_ 536870911 Frame: 255 ECU", 0x1FFFFFFFU, true, 255U},
{"BO_ 2147483649 Frame: 8 ECU", 1U, true, 8U},
{"BO_ 2684354559 Frame: 8 ECU", 0x1FFFFFFFU, true, 8U},
{" BO_\t256\tFrame :\t8\tECU \r", 256U, false, 8U}
};
for (const auto &test : valid) {
fixture.Write (std::string (test.line) + '\n');
try {
const auto database = DbcParser().ParseFile (fixture.path.string());
check (database.frames.size() == 1U && database.frames[0].name == "Frame" &&
database.frames[0].canId == test.id &&
database.frames[0].isExtended == test.extended &&
database.frames[0].dlc == test.length &&
database.frames[0].transmitter == "ECU", test.line);
} catch (const std::exception &error) { check (false, error.what()); }
}
const char *invalid[] = {
"BO_ invalid", "BO_", "BO_ 1", "BO_ 1 : 8 ECU", "BO_ 1 Frame 8 ECU",
"BO_ 1 Frame:", "BO_ 1 Frame: 8", "BO_ 1 Frame: 8 ECU extra",
"BO_ 1 Two Names: 8 ECU", "BO_ -1 Frame: 8 ECU", "BO_ +1 Frame: 8 ECU",
"BO_ 1x Frame: 8 ECU", "BO_ 0x123 Frame: 8 ECU", "BO_ 1.5 Frame: 8 ECU",
"BO_ 4294967296 Frame: 8 ECU", "BO_ 999999999999999999999 Frame: 8 ECU",
"BO_ 536870912 Frame: 8 ECU", "BO_ 1073741824 Frame: 8 ECU",
"BO_ 2684354560 Frame: 8 ECU", "BO_ 4294967295 Frame: 8 ECU",
"BO_ 1 Frame: -1 ECU", "BO_ 1 Frame: +8 ECU", "BO_ 1 Frame: 8x ECU",
"BO_ 1 Frame: 8.5 ECU", "BO_ 1 Frame: 256 ECU",
"BO_ 1 Frame: 4294967296 ECU", "BO_\tinvalid"
};
for (const auto *line : invalid) {
fixture.Write (std::string ("VERSION \"test\"\n\n") + line + '\n');
try {
DbcParser().ParseFile (fixture.path.string());
check (false, line);
} catch (const std::runtime_error &error) {
const std::string prefix = fixture.path.string() + ":3: ";
check (std::string (error.what()).find (prefix) == 0U &&
std::string (error.what()).size() > prefix.size(), line);
}
}
fixture.Write (
"BO_TX_BU_ 256 : ECU;\nBO_ 256 Frame: 8 ECU\n"
" SG_ Speed : 0|16@1+ (0.5,1) [0|100] \"rpm\" ECU\n"
"CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Speed \"Signal comment\";\n"
"BO_ 257 Second: 8 ECU\n");
const auto database = DbcParser().ParseFile (fixture.path.string());
check (database.frames.size() == 2U && database.frames[0].signals.size() == 1U &&
database.frames[0].comment == "Frame comment" &&
database.frames[0].signals[0].comment == "Signal comment", "Integration");
fixture.Write ("\nSG_ Speed : invalid\n");
try {
DbcParser().ParseFile (fixture.path.string());
check (false, "Signal error location");
} catch (const std::runtime_error &error) {
check (std::string (error.what()).find (fixture.path.string() + ":2: ") == 0U,
"Signal error location");
}
DbcDatabase dispatchDatabase;
DbcParseContext context {dispatchDatabase, nullptr};
DbcStatementDispatcher dispatcher;
dispatcher.RegisterType<CountingHandler> ("TEST_");
check (dispatcher.Dispatch ("TEST_ first", context) &&
dispatcher.Dispatch ("TEST_ second", context) &&
CountingHandler::instances == 1 && CountingHandler::calls == 2 &&
dispatchDatabase.frames.size() == 2U, "Factory dispatch and handler reuse");
check (!dispatcher.Dispatch ("TEST_OTHER data", context) &&
!dispatcher.Dispatch ("BO_TX_BU_ 1 : ECU;", context) &&
!dispatcher.Dispatch ("VAL_ 1 Speed 0 \"Stopped\";", context) &&
CountingHandler::calls == 2, "Exact and unknown keyword dispatch");
const DbcParser parser;
fixture.Write ("BO_ 1 First: 8 ECU\n");
check (parser.ParseFile (fixture.path.string()).frames.size() == 1U, "First parse");
fixture.Write ("SG_ Speed : invalid\n");
try {
parser.ParseFile (fixture.path.string());
check (false, "Independent parse context");
} catch (const std::runtime_error &error) {
check (std::string (error.what()).find ("Signal found before any frame") !=
std::string::npos, "Independent parse context");
}
fixture.Write ("BO_ 2 Second: 8 ECU\n");
const auto recovered = parser.ParseFile (fixture.path.string());
check (recovered.frames.size() == 1U && recovered.frames[0].canId == 2U,
"Fresh parse after error");
if (failures != 0) return 1;
std::cout << "All " << count << " parser cases passed.\n";
return 0;
}
+7
View File
@@ -0,0 +1,7 @@
TEMPLATE = app
TARGET = dbc_parser_test
CONFIG += console c++17
CONFIG -= app_bundle qt
INCLUDEPATH += ..
SOURCES += dbc_parser_test.cpp ../dbc_parser.cpp ../dbc_statement_handler.cpp
HEADERS += ../dbc_statement_handler.h ../typefactory.h ../dbc_parser.h ../dbc_database.h ../frame_info.h ../signal_info.h