diff --git a/README.md b/README.md index 736b118..6984da4 100644 --- a/README.md +++ b/README.md @@ -151,8 +151,11 @@ Files: - `dbc_parser.h` - `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 @@ -767,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` to create +registered handlers once per parsing session, then reuses them for all matching +lines: -```text -parser → parsed database → decode database - ↘ tree builder → UI tree +- `BO_` → `DbcFrameHandler` +- `SG_` → `DbcSignalHandler` +- `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("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. --- diff --git a/dbc.pro b/dbc.pro index 06d7c4b..5699d2b 100644 --- a/dbc.pro +++ b/dbc.pro @@ -7,6 +7,7 @@ SOURCES += \ dbc_decode_builder.cpp \ dbc_decoder.cpp \ dbc_parser.cpp \ + dbc_statement_handler.cpp \ dbc_tree_builder.cpp \ main.cpp \ tree_node.cpp @@ -16,6 +17,8 @@ HEADERS += \ dbc_decode_builder.h \ dbc_decoder.h \ dbc_parser.h \ + dbc_statement_handler.h \ + typefactory.h \ dbc_tree_builder.h \ decode_database.h \ frame_info.h \ diff --git a/dbc_parser.cpp b/dbc_parser.cpp index edb1141..3882efe 100644 --- a/dbc_parser.cpp +++ b/dbc_parser.cpp @@ -10,26 +10,13 @@ */ #include "dbc_parser.h" +#include "dbc_statement_handler.h" #include -#include #include #include -#include -#include 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. @@ -56,7 +43,8 @@ DbcDatabase DbcParser::ParseFile (const std::string &filePath) const { throw std::runtime_error ("Failed to open DBC file: " + filePath); DbcDatabase database; - FrameInfo *currentFrame = nullptr; + DbcParseContext context {database, nullptr}; + const DbcStatementDispatcher dispatcher; std::size_t lineNumber = 0U; std::string line; @@ -67,18 +55,7 @@ DbcDatabase DbcParser::ParseFile (const std::string &filePath) const { continue; try { - if (IsFrameLine (line)) { - FrameInfo frame = ParseFrameLine (line); - database.frames.push_back (frame); - currentFrame = &database.frames.back(); - } else if (IsSignalLine (line)) { - 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); + dispatcher.Dispatch (line, context); } catch (const std::exception &error) { throw std::runtime_error (filePath + ":" + std::to_string (lineNumber) + ": " + error.what()); @@ -88,294 +65,6 @@ DbcDatabase DbcParser::ParseFile (const std::string &filePath) const { return database; } -bool DbcParser::IsFrameLine (const std::string &line) { - return (line.compare (0U, 3U, "BO_") == 0) && - ((line.size() == 3U) || std::isspace (static_cast (line[3U]))); -} - -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) { return TrimText (text); } - -std::vector DbcParser::SplitReceivers (const std::string &text) { - std::vector 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 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::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 (ParseDecimal ( - lengthText, std::numeric_limits::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 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::stoul (TrimText (rest.substr (0U, pipePos))) - ); - - signal.length = static_cast ( - 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; -} diff --git a/dbc_parser.h b/dbc_parser.h index dc3f360..fa58508 100644 --- a/dbc_parser.h +++ b/dbc_parser.h @@ -13,8 +13,6 @@ #define DBC_PARSER_H #include -#include -#include #include "dbc_database.h" @@ -44,27 +42,7 @@ class DbcParser { DbcDatabase ParseFile (const std::string &filePath) const; 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::vector 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 */ diff --git a/dbc_statement_handler.cpp b/dbc_statement_handler.cpp new file mode 100644 index 0000000..293caa7 --- /dev/null +++ b/dbc_statement_handler.cpp @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: MIT +#include "dbc_statement_handler.h" +#include +#include +#include +#include +#include + +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 (text[begin]))) + ++begin; + + std::string::size_type end = text.size(); + while ((end > begin) && + std::isspace (static_cast (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 SplitReceivers (const std::string &text) { + std::vector 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::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 (ParseDecimal ( + lengthText, std::numeric_limits::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::stoul (TrimText (rest.substr (0U, pipePos))) + ); + + signal.length = static_cast ( + 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 ("BO_"); + RegisterType ("SG_"); + RegisterType ("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; +} diff --git a/dbc_statement_handler.h b/dbc_statement_handler.h new file mode 100644 index 0000000..fccea45 --- /dev/null +++ b/dbc_statement_handler.h @@ -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 +#include + +/** 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 + void RegisterType (const std::string &keyword) { + m_factory.registerType (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 m_factory; + std::unordered_map > m_handlers; +}; + +#endif /* DBC_STATEMENT_HANDLER_H */ diff --git a/tests/README.md b/tests/README.md index bd5a2de..d23afc7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -33,7 +33,7 @@ Run from the repository root: ```sh g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -I. \ - tests/dbc_parser_test.cpp dbc_parser.cpp -o /tmp/dbc_parser_test + tests/dbc_parser_test.cpp dbc_parser.cpp dbc_statement_handler.cpp -o /tmp/dbc_parser_test /tmp/dbc_parser_test ``` @@ -45,3 +45,6 @@ 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. diff --git a/tests/dbc_parser_test.cpp b/tests/dbc_parser_test.cpp index 2a30ab0..5bee06b 100644 --- a/tests/dbc_parser_test.cpp +++ b/tests/dbc_parser_test.cpp @@ -1,4 +1,5 @@ #include "dbc_parser.h" +#include "dbc_statement_handler.h" #include #include @@ -19,6 +20,17 @@ struct 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; @@ -88,6 +100,33 @@ int main() { check (std::string (error.what()).find (fixture.path.string() + ":2: ") == 0U, "Signal error location"); } + DbcDatabase dispatchDatabase; + DbcParseContext context {dispatchDatabase, nullptr}; + DbcStatementDispatcher dispatcher; + dispatcher.RegisterType ("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; diff --git a/tests/dbc_parser_test.pro b/tests/dbc_parser_test.pro index 6b6970f..d6341b3 100644 --- a/tests/dbc_parser_test.pro +++ b/tests/dbc_parser_test.pro @@ -3,5 +3,5 @@ TARGET = dbc_parser_test CONFIG += console c++17 CONFIG -= app_bundle qt INCLUDEPATH += .. -SOURCES += dbc_parser_test.cpp ../dbc_parser.cpp -HEADERS += ../dbc_parser.h ../dbc_database.h ../frame_info.h ../signal_info.h +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