This commit is contained in:
deeaitch committed 2026-10-05 18:33:19 -04:00
1 parent 88b5883dda
commit 8d82b13dad
5 files changed
+226 -93

No files matched your search

+33 -5
View File
@@ -532,9 +532,8 @@ one-based physical line number, including blank lines:
example.dbc:3: Invalid CAN ID: invalid example.dbc:3: Invalid CAN ID: invalid
``` ```
This change validates frame definitions. Signal syntax and unsupported DBC This change validates frame definitions. Signal definitions are also validated as described below; comprehensive
constructs retain their existing behavior; comprehensive validation is still DBC grammar coverage is still planned. File-open errors contain the path but have no line number.
planned. File-open errors contain the path but have no line number.
--- ---
@@ -562,6 +561,35 @@ Parsed fields:
--- ---
### Signal validation and parse errors
`SG_` statements are matched completely. Signal names must be nonempty and
contain no whitespace. Spaces and tabs are accepted between syntax elements;
byte order and signedness remain adjacent (`@0+`, `@0-`, `@1+`, or `@1-`).
A quoted unit (which may be empty) and a nonempty comma-separated receiver
list are required. Unexpected trailing fields are rejected.
Start bits must be unsigned decimal integers fitting `uint32_t`; lengths must
be within `1..64` bits. Signs, fractional values, numeric suffixes, and overflow
are rejected for these fields. Every signal bit must fit within the frame's
declared payload length, using Intel increasing bit positions or Motorola
sawtooth traversal. This uses the stored payload length without introducing
an additional CAN or CAN FD payload limit.
Factor, offset, minimum, and maximum accept signed decimal numbers, fractions,
and scientific notation. The entire numeric token must be valid; hexadecimal
numbers, NaN, infinity, conversion overflow, and conversion underflow are
rejected. Minimum must not exceed maximum. Zero and negative factors are allowed.
Multiplexing markers (`M`, `mN`, and combinations) are currently rejected rather
than interpreted as part of a signal name. Multiplexing support and duplicate
frame/signal detection remain separate work.
Invalid signal definitions stop parsing with `path:line: description`, including
when the signal exceeds the declared payload length.
---
## Comments ## Comments
Frame comment example: Frame comment example:
@@ -805,8 +833,8 @@ RegisterType<DbcValueHandler>("VAL_"); // after implementing DbcValueHandler
Registration creates one handler; replacing a registration replaces that instance. Registration creates one handler; replacing a registration replaces that instance.
Register during initialization before dispatching. A line is routed using its Register during initialization before dispatching. A line is routed using its
full first whitespace-delimited keyword, so `BO_TX_BU_` cannot match `BO_`. full first whitespace-delimited keyword, so `BO_TX_BU_` cannot match `BO_`.
Unregistered keywords remain ignored. Existing `SG_` and `CM_` syntax policies Unregistered keywords remain ignored. `SG_` accepts spaces and tabs after its keyword; `CM_` retains its existing
are preserved, including requiring a literal space after those keywords. policy requiring a literal space.
This factory handles statements inside DBC files. A separate format-level This factory handles statements inside DBC files. A separate format-level
factory can be introduced when another file format is implemented. The decoder factory can be introduced when another file format is implemented. The decoder
+53 -88
View File
@@ -5,6 +5,8 @@
#include <cctype> #include <cctype>
#include <charconv> #include <charconv>
#include <limits> #include <limits>
#include <regex>
#include <cmath>
namespace { namespace {
std::uint32_t ParseDecimal (const std::string &text, std::uint32_t ParseDecimal (const std::string &text,
@@ -142,95 +144,50 @@ FrameInfo DbcFrameHandler::ParseFrameLine (const std::string &line) {
} }
SignalInfo DbcSignalHandler::ParseSignalLine (const std::string &line) { SignalInfo DbcSignalHandler::ParseSignalLine (const std::string &line) {
/* // Match the complete statement before converting any numeric fields.
* Example: static const std::regex syntax (
* SG_ EngineSpeed : 0|16@1+ (0.125,0) [0|8000] "rpm" ECU1,ECU2 R"dbc(^SG_[ \t]+([^:]+):[ \t]*([^| \t]+)[ \t]*\|[ \t]*([^@ \t]+)[ \t]*@[ \t]*([01])([+-])[ \t]*\([ \t]*([^, \t]+)[ \t]*,[ \t]*([^ )\t]+)[ \t]*\)[ \t]*\[[ \t]*([^| \t]+)[ \t]*\|[ \t]*([^\] \t]+)[ \t]*\][ \t]*"([^"\r\n]*)"[ \t]+([^ \t,]+(?:[ \t]*,[ \t]*[^ \t,]+)*)[ \t]*$)dbc");
*/ std::smatch fields;
if (!std::regex_match (line, fields, syntax))
throw std::runtime_error ("Malformed or unsupported SG_ signal definition.");
SignalInfo signal; SignalInfo signal;
signal.name = TrimText (fields[1].str());
if (signal.name.empty() || signal.name.find_first_of (" \t\r\n") != std::string::npos)
throw std::runtime_error ("Invalid signal name or unsupported multiplexing syntax.");
std::string work = TrimText (line); signal.startBit = ParseDecimal (fields[2].str(),
if (work.compare (0U, 4U, "SG_ ") != 0) std::numeric_limits<std::uint32_t>::max(), "signal start bit");
throw std::runtime_error ("Invalid signal line: " + line); signal.length = ParseDecimal (fields[3].str(), 64U, "signal length");
if (signal.length == 0U)
throw std::runtime_error ("Signal length must be within 1..64 bits.");
signal.isLittleEndian = fields[4].str() == "1";
signal.isSigned = fields[5].str() == "-";
work.erase (0U, 4U); const auto parsePhysical = [] (const std::string &text, const char *field) {
static const std::regex number (
const std::string::size_type colonPos = work.find (':'); R"(^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$)");
if (colonPos == std::string::npos) if (!std::regex_match (text, number))
throw std::runtime_error ("Signal line missing ':' : " + line); throw std::runtime_error (std::string ("Invalid ") + field + ": " + text);
std::size_t consumed = 0U;
signal.name = TrimText (work.substr (0U, colonPos)); double value;
std::string rest = TrimText (work.substr (colonPos + 1U)); try {
value = std::stod (text, &consumed);
const std::string::size_type pipePos = rest.find ('|'); } catch (const std::exception &) {
const std::string::size_type atPos = rest.find ('@'); throw std::runtime_error (std::string ("Invalid ") + field + ": " + text);
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');
} }
if (consumed != text.size() || !std::isfinite (value))
{ throw std::runtime_error (std::string ("Invalid ") + field + ": " + text);
const char signChar = rest[signPos]; return value;
signal.isSigned = (signChar == '-'); };
} signal.factor = parsePhysical (fields[6].str(), "signal factor");
signal.offset = parsePhysical (fields[7].str(), "signal offset");
signal.factor = std::stod ( signal.minimum = parsePhysical (fields[8].str(), "signal minimum");
TrimText (rest.substr (factorBegin + 1U, factorComma - factorBegin - 1U)) signal.maximum = parsePhysical (fields[9].str(), "signal maximum");
); if (signal.minimum > signal.maximum)
throw std::runtime_error ("Signal minimum exceeds maximum.");
signal.offset = std::stod ( signal.unit = fields[10].str();
TrimText (rest.substr (factorComma + 1U, factorEnd - factorComma - 1U)) signal.receivers = SplitReceivers (fields[11].str());
);
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; return signal;
} }
@@ -326,12 +283,20 @@ void DbcFrameHandler::Handle (const std::string &line, DbcParseContext &context)
} }
void DbcSignalHandler::Handle (const std::string &line, DbcParseContext &context) const { 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) if (context.currentFrame == nullptr)
throw std::runtime_error ("Signal found before any frame definition."); throw std::runtime_error ("Signal found before any frame definition.");
context.currentFrame->signals.push_back (ParseSignalLine (line)); SignalInfo signal = ParseSignalLine (line);
const std::uint64_t payloadBits = static_cast<std::uint64_t> (context.currentFrame->dlc) * 8U;
std::uint64_t bit = signal.startBit;
for (std::uint32_t index = 0U; index < signal.length; ++index) {
if (bit >= payloadBits)
throw std::runtime_error ("Signal exceeds the frame payload length.");
if (signal.isLittleEndian)
++bit;
else
bit = (bit % 8U == 0U) ? bit + 15U : bit - 1U;
}
context.currentFrame->signals.push_back (std::move (signal));
} }
void DbcCommentHandler::Handle (const std::string &line, DbcParseContext &context) const { void DbcCommentHandler::Handle (const std::string &line, DbcParseContext &context) const {
+40
View File
@@ -0,0 +1,40 @@
# Validate `SG_` signal definitions and report malformed or unsupported syntax
## Description
Following strict validation of `BO_` frame definitions, add equivalent validation for `SG_` signal definitions. Currently, some malformed statements are accepted silently, and unsupported syntax can produce incorrect decoding.
Examples of current behavior:
- `@2+` is interpreted as Motorola instead of being rejected.
- Numeric fields containing suffixes, such as `0junk` or `1junk`, are partially parsed and accepted.
- `SG_ Value m1 : ...` is treated as an ordinary signal named `Value m1`, without multiplexing conditions.
- A statement containing a tab after `SG_` is silently skipped.
## Scope
- Validate statement structure and require a nonempty signal name without whitespace.
- Accept spaces and tabs between syntax elements.
- Parse numeric fields completely, rejecting overflow and trailing characters; preserve support for fractional values and scientific notation in physical parameters.
- Accept only byte order `@0` or `@1` and signedness `+` or `-`.
- Require signal lengths within `1..64` bits.
- Validate that the signal fits within the frame's declared payload length, using the appropriate Intel or Motorola bit traversal.
- Reject NaN and infinite values for factor, offset, minimum, and maximum; require `minimum <= maximum`.
- Explicitly reject multiplexing markers `M`, `mN`, and their combinations until multiplexing is implemented.
- Preserve the error format `path:line: description`.
## Acceptance criteria
- [ ] Existing valid signal definitions continue to parse successfully.
- [ ] Malformed and unsupported definitions stop parsing with a clear error.
- [ ] Regression tests cover the cases above, numeric boundaries, and Intel/Motorola payload boundaries.
- [ ] Existing parser and decoder tests pass.
- [ ] README documents the new validation rules and limitations.
## Out of scope
Duplicate detection, multiplexing support, and `VAL_` support remain separate tasks.
## Suggested branch
`codex/validate-sg-definitions`
+3
View File
@@ -43,6 +43,9 @@ following the decoder instructions above, then run `./dbc_parser_test`.
Parser tests cover standard and extended CAN IDs, whitespace, payload storage Parser tests cover standard and extended CAN IDs, whitespace, payload storage
boundaries, missing fields, malformed and overflowing numbers, reserved ID bits, boundaries, missing fields, malformed and overflowing numbers, reserved ID bits,
error paths and line numbers, and integration with signals and comments. error paths and line numbers, and integration with signals and comments.
Signal tests cover complete syntax and numeric validation, scientific notation,
finite physical parameters and ordered ranges, spaces and tabs, receiver lists,
unsupported multiplexing, and Intel/Motorola declared-payload boundaries.
Temporary DBC fixtures are created in the system temporary directory and removed Temporary DBC fixtures are created in the system temporary directory and removed
automatically. automatically.
+97
View File
@@ -100,6 +100,103 @@ int main() {
check (std::string (error.what()).find (fixture.path.string() + ":2: ") == 0U, check (std::string (error.what()).find (fixture.path.string() + ":2: ") == 0U,
"Signal error location"); "Signal error location");
} }
const DbcParser signalParser;
const auto signalLine = [] (const std::string &start, const std::string &length,
const std::string &order, const std::string &factor,
const std::string &offset, const std::string &minimum,
const std::string &maximum) {
return "SG_ Value : " + start + "|" + length + "@" + order +
" (" + factor + "," + offset + ") [" + minimum + "|" + maximum + "] \"\" ECU";
};
const auto rejectSignal = [&] (const std::string &line, unsigned payload = 8U) {
fixture.Write ("BO_ 1 Frame: " + std::to_string (payload) + " ECU\n\n" + line + "\n");
try {
signalParser.ParseFile (fixture.path.string());
check (false, "Reject " + 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(), "Reject " + line);
}
};
const std::string ordinary = signalLine ("0", "8", "1+", "1", "0", "0", "255");
for (const std::string value : {"-1", "+1", "0junk", "0x10", "1.5",
"4294967296", "999999999999999999999"}) {
rejectSignal (signalLine (value, "8", "1+", "1", "0", "0", "255"));
rejectSignal (signalLine ("0", value, "1+", "1", "0", "0", "255"));
}
for (const std::string length : {"0", "65"})
rejectSignal (signalLine ("0", length, "1+", "1", "0", "0", "255"));
for (const std::string order : {"2+", "9-", "1", "1x", "1++", "01+"})
rejectSignal (signalLine ("0", "8", order, "1", "0", "0", "255"));
for (const std::string value : {"1junk", "nan", "NaN", "inf", "-inf", "1e999", "1e-999", "0x1p2", "", "1e", "1.2.3"}) {
rejectSignal (signalLine ("0", "8", "1+", value, "0", "0", "255"));
rejectSignal (signalLine ("0", "8", "1+", "1", value, "0", "255"));
rejectSignal (signalLine ("0", "8", "1+", "1", "0", value, "255"));
rejectSignal (signalLine ("0", "8", "1+", "1", "0", "0", value));
}
rejectSignal (signalLine ("0", "8", "1+", "1", "0", "2", "1"));
for (const std::string name : {"", "Two Names", "Value M", "Value m1", "Value m1M"}) {
std::string line = ordinary;
line.replace (4U, 5U, name);
rejectSignal (line);
}
for (const std::string broken : {
"SG_", "SG_ Value", "SG_ Value : 0|8@1+ (1,0) [0|255] ECU",
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\"",
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU,",
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU extra"})
rejectSignal (broken);
rejectSignal (signalLine ("64", "1", "1+", "1", "0", "0", "1"));
rejectSignal (signalLine ("63", "2", "1+", "1", "0", "0", "3"));
rejectSignal (signalLine ("56", "2", "0+", "1", "0", "0", "3"));
rejectSignal (signalLine ("4294967295", "64", "1+", "1", "0", "0", "1"));
rejectSignal (ordinary, 0U);
for (const char delimiter : {'|', '@', '(', ')', '[', ']', '"'}) {
std::string line = ordinary;
line.erase (line.find (delimiter), 1U);
rejectSignal (line);
}
fixture.Write ("BO_ 1 Frame: 8 ECU\n" +
signalLine ("0", "8", "1+", "0", "0", "0", "0") + "\n" +
signalLine ("8", "8", "1-", "-1", "0", "-255", "0") + "\n");
const auto factors = signalParser.ParseFile (fixture.path.string());
check (factors.frames[0].signals.size() == 2U &&
factors.frames[0].signals[0].factor == 0.0 &&
factors.frames[0].signals[1].factor == -1.0,
"Zero and negative factors and equal range boundaries");
struct SignalCase { const char *start; const char *length; const char *order; unsigned payload; };
const SignalCase validSignals[] = {
{"0", "1", "1+", 1U}, {"7", "1", "0-", 1U},
{"0", "64", "1+", 8U}, {"7", "64", "0-", 8U},
{"63", "1", "1+", 8U}, {"56", "1", "0+", 8U},
{"3", "12", "0+", 2U}, {"2039", "1", "1+", 255U}
};
for (const auto &test : validSignals) {
fixture.Write ("BO_ 1 Frame: " + std::to_string (test.payload) + " ECU\n" +
signalLine (test.start, test.length, test.order, ".5e+1", "-2.5E-1", "-1.", "+2e2") + "\n");
try {
const auto parsed = signalParser.ParseFile (fixture.path.string());
const auto &signal = parsed.frames.at (0).signals.at (0);
check (signal.startBit == std::stoul (test.start) &&
signal.length == std::stoul (test.length) &&
signal.isLittleEndian == (test.order[0] == '1') &&
signal.isSigned == (test.order[1] == '-') &&
signal.factor == 5.0 && signal.offset == -0.25 &&
signal.minimum == -1.0 && signal.maximum == 200.0,
"Valid signal boundary and numeric metadata");
} catch (const std::exception &error) { check (false, error.what()); }
}
fixture.Write ("BO_ 1 Frame: 8 ECU\n"
"SG_\tValue\t:\t0 | 8 @1+ ( 1 , 0 ) [ 0 | 255 ] \"rpm\" ECU1,\tECU2\n");
try {
const auto parsed = signalParser.ParseFile (fixture.path.string());
const auto &signal = parsed.frames.at (0).signals.at (0);
check (signal.name == "Value" && signal.unit == "rpm" &&
signal.receivers == std::vector<std::string> ({"ECU1", "ECU2"}),
"Signal tabs, spaces, and receiver list");
} catch (const std::exception &error) { check (false, error.what()); }
DbcDatabase dispatchDatabase; DbcDatabase dispatchDatabase;
DbcParseContext context {dispatchDatabase, nullptr}; DbcParseContext context {dispatchDatabase, nullptr};
DbcStatementDispatcher dispatcher; DbcStatementDispatcher dispatcher;