Closes #25
This commit is contained in:
1 parent
e555ebb571
commit
849ce31e2f
17 files changed
+675
-35
No files matched your search
@@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user