1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
5#include "qcandbcfileparser.h"
6#include "qcandbcfileparser_p.h"
7#include "qcanmessagedescription.h"
8#include "qcansignaldescription.h"
9#include "qcanuniqueiddescription.h"
10#include "private/qcanmessagedescription_p.h"
11#include "private/qcansignaldescription_p.h"
12
13#include <QtCore/QFile>
14#include <QtCore/QRegularExpression>
15
16#include <optional>
17
18QT_BEGIN_NAMESPACE
19
20/*!
21 \class QCanDbcFileParser
22 \inmodule QtSerialBus
23 \since 6.5
24 \preliminary
25
26 \brief The QCanDbcFileParser class can be used to parse DBC files.
27
28 A CAN database or CAN DBC file is an ASCII text file that contains
29 information on how to decode and interpret raw CAN bus data. Some more
30 details about the format can be found \l {CSSElectronics DBC Intro}{here}
31 or \l {OpenVehicles DBC Intro}{here}.
32
33 The QCanDbcFileParser class takes the input DBC file, parses it, and
34 provides a list of \l {QCanMessageDescription}s as an output. These message
35 descriptions can be forwarded to \l QCanFrameProcessor, and later used
36 as rules to encode or decode \l {QCanBusFrame}s.
37
38 Use one of \l parse() overloads to specify a file or a list of files that
39 will be processed. Both overloads return \c true if the parsing completes
40 successfully and \c false otherwise.
41
42 Call the \l error() method to get the error which occurred during the
43 parsing. If the parsing completes successfully, this method will return
44 \l {QCanDbcFileParser::}{None}. Otherwise, you can use an
45 \l errorString() method to get the string representation of an error.
46
47 During the parsing some non-critical problems may occur as well. Such
48 problems will be logged, but the parsing process will not be aborted. You
49 can use the \l warnings() method to get the full list of such problems
50 after the parsing is completed.
51
52 If the parsing completes successfully, call \l messageDescriptions() to get
53 a list of the message descriptions that were extracted during the last
54 \l parse() call. Call \l messageValueDescriptions() to get the textual
55 descriptions of signal raw values, if they are available.
56
57 Use the static \l uniqueIdDescription() function to get a
58 \l QCanUniqueIdDescription for the DBC format.
59
60 \code
61 QCanDbcFileParser fileParser;
62 const bool result = fileParser.parse(u"path/to/file.dbc"_s);
63 // Check result, call error() and warnings() if needed
64
65 // Prepare a QCanFrameProcessor to decode or encode DBC frames
66 QCanFrameProcessor frameProcessor;
67 frameProcessor.setUniqueIdDescription(QCanDbcFileParser::uniqueIdDescription());
68 frameProcessor.setMessageDescriptions(fileParser.messageDescriptions());
69 \endcode
70
71 \note The parser is stateful, which means that all the results (like
72 extracted message descriptions, error code, or warnings) are reset once the
73 next parsing starts.
74
75 \section2 Supported Keywords
76
77 The current implementation supports only a subset of keywords that you can
78 find in a DBC file:
79
80 \list
81 \li \c {BO_} - message description.
82 \li \c {SG_} - signal description.
83 \li \c {SIG_VALTYPE_} - signal type description.
84 \li \c {SG_MUL_VAL_} - extended multiplexing description.
85 \li \c {CM_} - comments (only for message and signal descriptions).
86 \li \c {VAL_} - textual descriptions for raw signal values.
87 \endlist
88
89 Lines starting from other keywords are simply ignored.
90
91 \sa QCanMessageDescription, QCanFrameProcessor
92*/
93
94/*!
95 \typealias QCanDbcFileParser::ValueDescriptions
96
97 This is a type alias for \c {QHash<quint32, QString>}.
98
99 The keys of the hash represent raw signal values, and the values of the
100 hash represent corresponding string descriptions.
101*/
102
103/*!
104 \typealias QCanDbcFileParser::SignalValueDescriptions
105
106 This is a type alias for \c {QHash<QString, ValueDescriptions>}.
107
108 The keys of the hash represent signal names, and the values of the
109 hash contain the corresponding \l QCanDbcFileParser::ValueDescriptions
110 entries.
111
112 \sa QCanDbcFileParser::ValueDescriptions
113*/
114
115/*!
116 \typealias QCanDbcFileParser::MessageValueDescriptions
117
118 This is a type alias for
119 \c {QHash<QtCanBus::UniqueId, SignalValueDescriptions>}.
120
121 The keys of the hash represent message unique ids, and the values of the
122 hash contain the corresponding \l QCanDbcFileParser::SignalValueDescriptions
123 entries.
124
125 \sa QCanDbcFileParser::SignalValueDescriptions
126*/
127
128/*!
129 \enum QCanDbcFileParser::Error
130
131 This enum represents the possible errors that can happen during the parsing
132 of a DBC file.
133
134 \value None No error occurred.
135 \value FileReading An error occurred while opening or reading the file.
136 \value Parsing An error occurred while parsing the content of the file.
137*/
138
139/*!
140 Constructs a DBC file parser.
141*/
142QCanDbcFileParser::QCanDbcFileParser()
143 : d(std::make_unique<QCanDbcFileParserPrivate>())
144{
145}
146
147/*!
148 Destroys this DBC file parser.
149*/
150QCanDbcFileParser::~QCanDbcFileParser() = default;
151
152/*!
153 Parses the file \a fileName. Returns \c true if the parsing completed
154 successfully or \c false otherwise.
155
156 If the parsing completed successfully, call the \l messageDescriptions()
157 method to get the list of all extracted message descriptions.
158
159 If the parsing failed, call the \l error() and \l errorString() methods
160 to get the information about the error.
161
162 Call the \l warnings() method to get the list of warnings that were
163 logged during the parsing.
164
165 \note This method expects the file contents to be encoded in UTF-8. If the
166 file has a different encoding, decode it first, and use \l parseData()
167 to extract the DBC information.
168
169 \sa messageDescriptions(), error(), warnings(), parseData()
170*/
171bool QCanDbcFileParser::parse(const QString &fileName)
172{
173 d->reset();
174 return d->parseFile(fileName);
175}
176
177/*!
178 \overload
179
180 Parses a list of files \a fileNames. Returns \c true if the parsing
181 completed successfully or \c false otherwise.
182
183 If the parsing completed successfully, call the \l messageDescriptions()
184 method to get the list of all extracted message descriptions.
185
186 The parsing stops at the first error. Call the \l error() and
187 \l errorString() methods to get the information about the error.
188
189 Call the \l warnings() method to get the list of warnings that were
190 logged during the parsing.
191
192 \note This method expects the file contents to be encoded in UTF-8. If the
193 file has a different encoding, decode it first, and use \l parseData()
194 to extract the DBC information.
195
196 \sa messageDescriptions(), error(), warnings(), parseData()
197*/
198bool QCanDbcFileParser::parse(const QStringList &fileNames)
199{
200 d->reset();
201 for (const auto &fileName : fileNames) {
202 if (!d->parseFile(fileName))
203 return false;
204 }
205 return true;
206}
207
208/*!
209 \since 6.7
210
211 Parses the input data \a data and returns \c true if the parsing completed
212 successfully or \c false otherwise.
213
214 If the parsing completed successfully, call the \l messageDescriptions()
215 method to get the list of all extracted message descriptions.
216
217 If the parsing failed, call the \l error() and \l errorString() methods
218 to get the information about the error.
219
220 Call the \l warnings() method to get the list of warnings that were
221 logged during the parsing.
222
223 The method expects that \a data is the content of a valid DBC file,
224 properly converted to QStringView.
225
226 Use this method when the input file has an encoding different from UTF-8.
227
228 \code
229 // Read the data from a DBC file with custom encoding
230 const QByteArray initialData = ...;
231 // Convert to UTF-16 using QStringDecoder or some other way
232 const QString decodedData = ...;
233 QCanDbcFileParser parser;
234 const bool result = parser.parseData(decodedData);
235 \endcode
236
237 \sa messageDescriptions(), error(), warnings(), parse()
238*/
239bool QCanDbcFileParser::parseData(QStringView data)
240{
241 d->reset();
242 return d->parseData(data);
243}
244
245/*!
246 Returns the list of message descriptions that were extracted during the
247 last \l parse() call.
248
249 \sa parse(), error()
250*/
251QList<QCanMessageDescription> QCanDbcFileParser::messageDescriptions() const
252{
253 return d->getMessages();
254}
255
256/*!
257 Returns the textual descriptions for signal raw values.
258
259 DBC supports the possibility to provide textual descriptions to signal raw
260 values. If such data exists in the parsed DBC file(s), it can be accessed
261 using this function.
262
263 The textual descriptions are unique for a certain signal within a specific
264 message, so the returned structure contains the information about the
265 message unique id and the signal name, as well as the actual value
266 descriptions.
267
268 \sa QCanDbcFileParser::MessageValueDescriptions,
269 QCanDbcFileParser::SignalValueDescriptions,
270 QCanDbcFileParser::ValueDescriptions
271*/
272QCanDbcFileParser::MessageValueDescriptions QCanDbcFileParser::messageValueDescriptions() const
273{
274 return d->m_valueDescriptions;
275}
276
277/*!
278 Returns the last error which occurred during the parsing.
279
280 \sa errorString(), parse()
281*/
282QCanDbcFileParser::Error QCanDbcFileParser::error() const
283{
284 return d->m_error;
285}
286
287/*!
288 Returns the text representation of the last error which occurred during the
289 parsing or an empty string if there was no error.
290
291 \sa error()
292*/
293QString QCanDbcFileParser::errorString() const
294{
295 return d->m_errorString;
296}
297
298/*!
299 Returns the list of non-critical problems which occurred during the parsing.
300
301 A typical problem can be a malformed message or signal description. In such
302 cases the malformed message or signal is skipped, but the rest of the file
303 can be processed as usual.
304
305 \sa error(), parse()
306*/
307QStringList QCanDbcFileParser::warnings() const
308{
309 return d->m_warnings;
310}
311
312/*!
313 Returns a unique identifier description. DBC protocol always uses the
314 Frame Id as an identifier, and therefore the unique identifier description
315 is always the same.
316
317 Use this method to get an instance of \l QCanUniqueIdDescription and pass
318 it to \l QCanFrameProcessor.
319
320 \sa QCanFrameProcessor::setUniqueIdDescription()
321*/
322QCanUniqueIdDescription QCanDbcFileParser::uniqueIdDescription()
323{
324 QCanUniqueIdDescription desc;
325 desc.setSource(QtCanBus::DataSource::FrameId);
326 desc.setEndian(QSysInfo::Endian::LittleEndian);
327 desc.setStartBit(0);
328 desc.setBitLength(29); // for both extended and normal frame id
329 return desc;
330}
331
332/* QCanDbcFileParserPrivate implementation */
333
334using namespace Qt::StringLiterals;
335
336// signal name with whitespaces is invalid in DBC, so we can safely use it
337// for internal purposes
338static const auto kQtDummySignal = u"Qt Dummy Signal"_s;
339
340static constexpr auto kMessageDef = "BO_ "_L1;
341static constexpr auto kSignalDef = "SG_ "_L1;
342static constexpr auto kSigValTypeDef = "SIG_VALTYPE_ "_L1;
343static constexpr auto kCommentDef = "CM_ "_L1;
344static constexpr auto kExtendedMuxDef = "SG_MUL_VAL_ "_L1;
345static constexpr auto kValDef = "VAL_ "_L1;
346
347static constexpr auto kUnsignedIntRegExp = "\\d+"_L1;
348static constexpr auto kDoubleRegExp = "[+-]?\\d+((.\\d*)?([eE][+-]?\\d+)?)?"_L1;
349static constexpr auto kDbcIdentRegExp = "[_[:alpha:]][_[:alnum:]]+"_L1;
350static constexpr auto kOneOrMoreSpaceRegExp = "[ ]+"_L1;
351static constexpr auto kMaybeSpaceRegExp = "[ ]*"_L1;
352static constexpr auto kMuxIndicatorRegExp = "M|m\\d+M?"_L1;
353static constexpr auto kByteOrderRegExp = "0|1"_L1;
354static constexpr auto kValueTypeRegExp = "\\+|\\-"_L1;
355// The pattern matches all printable characters, except double-quote (") and backslash (\).
356static constexpr auto kCharStrRegExp = "((?![\\\"\\\\])\\P{Cc})*"_L1;
357
358void QCanDbcFileParserPrivate::reset()
359{
360 m_fileName.clear();
361 m_error = QCanDbcFileParser::Error::None;
362 m_errorString.clear();
363 m_warnings.clear();
364 m_lineOffset = 0;
365 m_isProcessingMessage = false;
366 m_seenExtraData = false;
367 m_currentMessage = {};
368 m_messageDescriptions.clear();
369 m_valueDescriptions.clear();
370}
371
372/*!
373 \internal
374 Returns \c false only in case of hard error. Returns \c true even if some
375 warnings occurred during parsing.
376*/
377bool QCanDbcFileParserPrivate::parseFile(const QString &fileName)
378{
379 QFile f(fileName);
380 if (!f.open(flags: QIODevice::ReadOnly)) {
381 m_error = QCanDbcFileParser::Error::FileReading;
382 m_errorString = f.errorString();
383 return false;
384 }
385 m_fileName = fileName;
386 m_seenExtraData = false;
387
388 while (!f.atEnd()) {
389 const QString str = QString::fromUtf8(ba: f.readLine().trimmed());
390 if (!processLine(line: {str.constData(), str.size()})) // also sets the error properly
391 return false;
392 }
393 addCurrentMessage(); // check if we need to add the message
394 // now when we parsed the whole file, we can verify the signal multiplexing
395 postProcessSignalMultiplexing();
396
397 return true;
398}
399
400struct ReadData
401{
402 qsizetype index;
403 QStringView result;
404};
405
406static ReadData readUntilNewline(QStringView in, qsizetype from)
407{
408 const qsizetype idx = in.indexOf(c: '\n'_L1, from);
409
410 return (idx == -1) ? ReadData{.index: idx, .result: in.sliced(pos: from).trimmed()}
411 : ReadData{.index: idx, .result: in.sliced(pos: from, n: idx - from).trimmed()};
412}
413
414/*!
415 \internal
416 The implementation basically copies parseFile(), including all
417 post-processing. The only difference is in extracting the data.
418*/
419bool QCanDbcFileParserPrivate::parseData(QStringView data)
420{
421 if (data.isEmpty()) {
422 m_error = QCanDbcFileParser::Error::Parsing;
423 m_errorString = QObject::tr(s: "Empty input data.");
424 return false;
425 }
426 m_seenExtraData = false;
427 qsizetype from = 0;
428 while (true) {
429 const auto [idx, sv] = readUntilNewline(in: data, from);
430 if (!processLine(line: sv))
431 return false;
432 if (idx == -1) // reached the end of the string
433 break;
434 from = idx + 1;
435 }
436 addCurrentMessage();
437 postProcessSignalMultiplexing();
438 return true;
439}
440
441/*!
442 \internal
443 Returns \c false only in case of hard error. Returns \c true even if some
444 warnings occurred during parsing.
445*/
446bool QCanDbcFileParserPrivate::processLine(const QStringView line)
447{
448 QStringView data = line;
449 m_lineOffset = 0;
450
451 auto handleParsingError = [this](QLatin1StringView section) {
452 m_error = QCanDbcFileParser::Error::Parsing;
453 m_errorString = !m_fileName.isEmpty()
454 ? QObject::tr(s: "Failed to parse file %1. Unexpected position "
455 "of %2 section.").arg(args&: m_fileName, args&: section)
456 : QObject::tr(s: "Failed to parse input data. Unexpected position "
457 "of %1 section.").arg(a: section);
458 };
459
460 if (data.startsWith(s: kMessageDef)) {
461 if (m_seenExtraData) {
462 // Unexpected position of message description
463 handleParsingError(kMessageDef);
464 return false;
465 }
466 addCurrentMessage();
467 if (!parseMessage(data))
468 return false;
469 }
470 // signal definitions can be on the same line as message definition,
471 // or on a separate line
472 data = data.sliced(pos: m_lineOffset).trimmed();
473 while (data.startsWith(s: kSignalDef)) {
474 if (!m_isProcessingMessage || m_seenExtraData) {
475 // Unexpected position of signal description
476 handleParsingError(kSignalDef);
477 return false;
478 }
479 if (!parseSignal(data))
480 return false;
481 data = data.sliced(pos: m_lineOffset).trimmed();
482 }
483 // If we detect one of the following lines, then message description is
484 // finished. We also assume that we can have only one key at each line.
485 if (data.startsWith(s: kSigValTypeDef)) {
486 m_seenExtraData = true;
487 addCurrentMessage();
488 parseSignalType(data);
489 } else if (data.startsWith(s: kCommentDef)) {
490 m_seenExtraData = true;
491 addCurrentMessage();
492 parseComment(data);
493 } else if (data.startsWith(s: kExtendedMuxDef)) {
494 m_seenExtraData = true;
495 addCurrentMessage();
496 parseExtendedMux(data);
497 } else if (data.startsWith(s: kValDef)) {
498 m_seenExtraData = true;
499 addCurrentMessage();
500 parseValueDescriptions(data);
501 }
502 return true;
503}
504
505static std::optional<QtCanBus::UniqueId> extractUniqueId(QStringView view)
506{
507 bool ok = false;
508 const uint value = view.toUInt(ok: &ok);
509 if (ok)
510 return QtCanBus::UniqueId{value & 0x1FFFFFFF};
511 return std::nullopt;
512}
513
514/*!
515 \internal
516 Returns \c false only in case of hard error. Returns \c true even if some
517 warnings occurred during parsing.
518*/
519bool QCanDbcFileParserPrivate::parseMessage(const QStringView data)
520{
521 // The regexp matches the following definition:
522 // BO_ message_id message_name ':' message_size transmitter
523 // also considering the fact that spaces around ':' seem to be optional, and
524 // allowing more than one space between parts.
525
526 // %1 - messageDef
527 // %2 - maybeSpace
528 // %3 - unsignedInt
529 // %4 - oneOrMoreSpace
530 // %5 - DbcIdentifier
531 static const QString regExStr =
532 "%1%2(?<messageId>%3)%4(?<name>%5)%2:%2(?<size>%3)%4(?<transmitter>%5)"_L1.
533 arg(args: kMessageDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp, args: kOneOrMoreSpaceRegExp,
534 args: kDbcIdentRegExp);
535 static const QRegularExpression messageRegExp(regExStr);
536
537 m_isProcessingMessage = false;
538 const auto match = messageRegExp.matchView(subjectView: data);
539 if (match.hasMatch()) {
540 m_currentMessage = extractMessage(match);
541 // can't check for isValid() here, because demands signal descriptions
542 if (!m_currentMessage.name().isEmpty()) {
543 m_isProcessingMessage = true;
544 } else {
545 addWarning(warning: QObject::tr(s: "Failed to parse message description from "
546 "string %1").arg(a: data));
547 }
548 m_lineOffset = match.capturedEnd(nth: 0);
549 } else {
550 addWarning(warning: QObject::tr(s: "Failed to find message description in string %1").arg(a: data));
551 m_lineOffset = data.size(); // skip this string
552 }
553 return true;
554}
555
556QCanMessageDescription
557QCanDbcFileParserPrivate::extractMessage(const QRegularExpressionMatch &match)
558{
559 Q_ASSERT(match.hasMatch());
560 QCanMessageDescription desc;
561 desc.setName(match.captured(name: u"name"_s));
562
563 const auto id = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
564 if (id.has_value()) {
565 desc.setUniqueId(id.value());
566 } else {
567 addWarning(warning: QObject::tr(s: "Failed to parse frame id for message %1").arg(a: desc.name()));
568 return {};
569 }
570
571 bool ok = false;
572 const auto size = match.capturedView(name: u"size"_s).toUInt(ok: &ok);
573 if (ok) {
574 desc.setSize(size);
575 } else {
576 addWarning(warning: QObject::tr(s: "Failed to parse size for message %1").arg(a: desc.name()));
577 return {};
578 }
579
580 desc.setTransmitter(match.captured(name: u"transmitter"_s));
581
582 return desc;
583}
584
585/*!
586 \internal
587 Returns \c false only in case of hard error. Returns \c true even if some
588 warnings occurred during parsing.
589*/
590bool QCanDbcFileParserPrivate::parseSignal(const QStringView data)
591{
592 // The regexp should match the following pattern:
593 // SG_ signal_name multiplexer_indicator : start_bit |
594 // signal_size @ byte_order value_type ( factor , offset )
595 // [ minimum | maximum ] unit receiver {, receiver}
596 // We also need to consider the fact that some of the spaces might be
597 // optional, and we can potentially allow more spaces between parts.
598 // Note that the end of the signal description can contain multiple
599 // receivers. The regexp is supposed to extract all of them, but we use
600 // only the first one for now.
601
602 // %1 - SignalDef
603 // %2 - MaybeSpace
604 // %3 - DbcIdentifier
605 // %4 - OneOrMoreSpace
606 // %5 - MuxIndicator
607 // %6 - unsignedInt
608 // %7 - byteOrder
609 // %8 - valueType
610 // %9 - double
611 // %10 - charStr
612 static const QString regExStr =
613 "%1%2(?<name>%3)(%4(?<mux>%5))?%2:%2(?<startBit>%6)%2\\|%2(?<sigSize>%6)%2@%2"
614 "(?<byteOrder>%7)%2(?<valueType>%8)%4\\(%2(?<factor>%9)%2,%2(?<offset>%9)%2\\)"
615 "%4\\[%2(?<min>%9)%2\\|%2(?<max>%9)%2\\]%4\"(?<unit>%10)\""
616 "%4(?<receiver>%3)(%2,%2%3)*"_L1.
617 arg(args: kSignalDef, args: kMaybeSpaceRegExp, args: kDbcIdentRegExp, args: kOneOrMoreSpaceRegExp,
618 args: kMuxIndicatorRegExp, args: kUnsignedIntRegExp, args: kByteOrderRegExp, args: kValueTypeRegExp,
619 args: kDoubleRegExp, args: kCharStrRegExp);
620 static const QRegularExpression signalRegExp(regExStr);
621
622 const auto match = signalRegExp.matchView(subjectView: data);
623 if (match.hasMatch()) {
624 QCanSignalDescription desc = extractSignal(match);
625
626 if (desc.isValid())
627 m_currentMessage.addSignalDescription(description: desc);
628 else
629 addWarning(warning: QObject::tr(s: "Failed to parse signal description from string %1").arg(a: data));
630
631 m_lineOffset = match.capturedEnd(nth: 0);
632 } else {
633 addWarning(warning: QObject::tr(s: "Failed to find signal description in string %1").arg(a: data));
634 m_lineOffset = data.size(); // skip this string
635 }
636 return true;
637}
638
639QCanSignalDescription QCanDbcFileParserPrivate::extractSignal(const QRegularExpressionMatch &match)
640{
641 Q_ASSERT(match.hasMatch());
642 QCanSignalDescription desc;
643 desc.setName(match.captured(name: u"name"_s));
644
645 bool ok = false;
646
647 if (match.hasCaptured(name: u"mux"_s)) {
648 const auto muxStr = match.capturedView(name: u"mux"_s);
649 if (muxStr == u"M"_s) {
650 desc.setMultiplexState(QtCanBus::MultiplexState::MultiplexorSwitch);
651 } else if (muxStr.endsWith(s: u"M"_s, cs: Qt::CaseSensitive)) {
652 desc.setMultiplexState(QtCanBus::MultiplexState::SwitchAndSignal);
653 const auto val = muxStr.sliced(pos: 1, n: muxStr.size() - 2).toUInt(ok: &ok);
654 if (!ok) {
655 addWarning(warning: QObject::tr(s: "Failed to parse multiplexor value for signal %1").
656 arg(a: desc.name()));
657 return {};
658 }
659 // We have the value, but we do not really know the multiplexor
660 // switch name. To know it, we potentially need to parse all signals
661 // for the message. So for now we just create a dummy entry, and
662 // the actual signal name will be updated later;
663 desc.addMultiplexSignal(name: kQtDummySignal, value: val);
664 } else {
665 desc.setMultiplexState(QtCanBus::MultiplexState::MultiplexedSignal);
666 const auto val = muxStr.sliced(pos: 1).toUInt(ok: &ok);
667 if (!ok) {
668 addWarning(warning: QObject::tr(s: "Failed to parse multiplexor value for signal %1").
669 arg(a: desc.name()));
670 return {};
671 }
672 // Same as above
673 desc.addMultiplexSignal(name: kQtDummySignal, value: val);
674 }
675 }
676
677 const uint startBit = match.capturedView(name: u"startBit"_s).toUInt(ok: &ok);
678 if (ok) {
679 desc.setStartBit(startBit);
680 } else {
681 addWarning(warning: QObject::tr(s: "Failed to parse start bit for signal %1").arg(a: desc.name()));
682 return {};
683 }
684
685 const uint bitLength = match.capturedView(name: u"sigSize"_s).toUInt(ok: &ok);
686 if (ok) {
687 desc.setBitLength(bitLength);
688 } else {
689 addWarning(warning: QObject::tr(s: "Failed to parse bit length for signal %1").arg(a: desc.name()));
690 return {};
691 }
692
693 // 0 = BE; 1 = LE
694 const auto endian = match.capturedView(name: u"byteOrder"_s) == u"0"_s
695 ? QSysInfo::Endian::BigEndian : QSysInfo::Endian::LittleEndian;
696 desc.setDataEndian(endian);
697
698 // + = unsigned; - = signed
699 const auto dataFormat = match.capturedView(name: u"valueType"_s) == u"+"_s
700 ? QtCanBus::DataFormat::UnsignedInteger : QtCanBus::DataFormat::SignedInteger;
701 desc.setDataFormat(dataFormat);
702
703 const double factor = match.capturedView(name: u"factor"_s).toDouble(ok: &ok);
704 if (ok) {
705 desc.setFactor(factor);
706 } else {
707 addWarning(warning: QObject::tr(s: "Failed to parse factor for signal %1").arg(a: desc.name()));
708 return {};
709 }
710
711 const double offset = match.capturedView(name: u"offset"_s).toDouble(ok: &ok);
712 if (ok) {
713 desc.setOffset(offset);
714 } else {
715 addWarning(warning: QObject::tr(s: "Failed to parse offset for signal %1").arg(a: desc.name()));
716 return {};
717 }
718
719 const double min = match.capturedView(name: u"min"_s).toDouble(ok: &ok);
720 if (ok) {
721 const double max = match.capturedView(name: u"max"_s).toDouble(ok: &ok);
722 if (ok)
723 desc.setRange(minimum: min, maximum: max);
724 }
725 if (!ok) {
726 addWarning(warning: QObject::tr(s: "Failed to parse value range from signal %1").arg(a: desc.name()));
727 return {};
728 }
729
730 desc.setPhysicalUnit(match.captured(name: u"unit"_s));
731 desc.setReceiver(match.captured(name: u"receiver"_s));
732
733 return desc;
734}
735
736void QCanDbcFileParserPrivate::parseSignalType(const QStringView data)
737{
738 // The regexp should match the following pattern:
739 // SIG_VALTYPE_ message_id signal_name signal_extended_value_type ;
740 // We also need to consider the fact that we can potentially allow more
741 // spaces between parts.
742
743 // %1 sigValTypeDef
744 // %2 maybeSpace
745 // %3 unsignedInt
746 // %4 oneOrMoreSpace
747 // %5 DbcIdentifier
748 const QString regExStr =
749 "%1%2(?<messageId>%3)%4(?<sigName>%5)%2:%2(?<type>%3)%2;"_L1.
750 arg(args: kSigValTypeDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp,
751 args: kOneOrMoreSpaceRegExp, args: kDbcIdentRegExp);
752 const QRegularExpression sigValTypeRegEx(regExStr);
753
754 const auto match = sigValTypeRegEx.matchView(subjectView: data);
755 if (!match.hasMatch()) {
756 m_lineOffset = data.size();
757 addWarning(warning: QObject::tr(s: "Failed to find signal value type description in string %1").
758 arg(a: data));
759 return;
760 }
761
762 m_lineOffset = match.capturedEnd(nth: 0);
763
764 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
765 if (!uidOptional) {
766 addWarning(warning: QObject::tr(s: "Failed to parse frame id from string %1").arg(a: data));
767 return;
768 }
769
770 const QtCanBus::UniqueId uid = uidOptional.value();
771 auto msgDesc = m_messageDescriptions.value(key: uid);
772 if (msgDesc.isValid()) {
773 const QString sigName = match.captured(name: u"sigName"_s);
774 auto sigDesc = msgDesc.signalDescriptionForName(name: sigName);
775 if (sigDesc.isValid()) {
776 bool ok = false;
777 const auto type = match.capturedView(name: u"type").toUInt(ok: &ok);
778 if (ok) {
779 bool sigDescChanged = false;
780 switch (type) {
781 case 0: /* signed or unsigned integer */
782 // do nothing, as we already have signed/unsinged integer
783 // based on "SG_ " string
784 break;
785 case 1: /* 32-bit IEEE-float */
786 sigDesc.setDataFormat(QtCanBus::DataFormat::Float);
787 sigDesc.setBitLength(32);
788 sigDescChanged = true;
789 break;
790 case 2: /* 64-bit IEEE-double */
791 sigDesc.setDataFormat(QtCanBus::DataFormat::Double);
792 sigDesc.setBitLength(64);
793 sigDescChanged = true;
794 break;
795 default:
796 // invalid value
797 break;
798 }
799 if (sigDescChanged) {
800 msgDesc.addSignalDescription(description: sigDesc);
801 m_messageDescriptions.insert(key: msgDesc.uniqueId(), value: msgDesc);
802 }
803 } else {
804 addWarning(warning: QObject::tr(s: "Failed to parse data type from string %1").arg(a: data));
805 }
806 } else {
807 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
808 "Skipping string %2").arg(args: sigName, args: data));
809 }
810 } else {
811 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
812 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
813 }
814}
815
816void QCanDbcFileParserPrivate::parseComment(const QStringView data)
817{
818 // The comment for message or signal description is represented by the
819 // following pattern:
820 // CM_ (BO_ message_id char_string | SG_ message_id signal_name char_string);
821
822 // %1 commentDef
823 // %2 maybeSpace
824 // %3 messageDef
825 // %4 signalDef
826 // %5 oneOrMoreSpace
827 // %6 unsignedInt
828 // %7 DbcIdentifier
829 // %8 charStr
830 const QString regExStr =
831 "%1%2(?<type>(%3|%4))%2(?<messageId>%6)%5((?<sigName>%7)%5)?\"(?<comment>%8)\"%2;"_L1.
832 arg(args: kCommentDef, args: kMaybeSpaceRegExp, args: kMessageDef, args: kSignalDef, args: kOneOrMoreSpaceRegExp,
833 args: kUnsignedIntRegExp, args: kDbcIdentRegExp, args: kCharStrRegExp);
834 const QRegularExpression commentRegExp(regExStr);
835
836 const auto match = commentRegExp.matchView(subjectView: data);
837 if (!match.hasMatch()) {
838 // no warning here, as we ignore some "general" comments, and parse only
839 // comments related to messages and signals
840 m_lineOffset = data.size();
841 return;
842 }
843
844 m_lineOffset = match.capturedEnd(nth: 0);
845
846 const auto type = match.capturedView(name: u"type"_s);
847
848 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
849 if (!uidOptional) {
850 addWarning(warning: QObject::tr(s: "Failed to parse frame id from string %1").arg(a: data));
851 return;
852 }
853
854 const QtCanBus::UniqueId uid = uidOptional.value();
855 auto messageDesc = m_messageDescriptions.value(key: uid);
856 if (!messageDesc.isValid()) {
857 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
858 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
859 return;
860 }
861
862 if (type == kMessageDef) {
863 const QString comment = match.captured(name: u"comment"_s);
864 messageDesc.setComment(comment);
865 m_messageDescriptions.insert(key: uid, value: messageDesc);
866 } else if (type == kSignalDef) {
867 const QString sigName = match.captured(name: u"sigName"_s);
868 auto signalDesc = messageDesc.signalDescriptionForName(name: sigName);
869 if (signalDesc.isValid()) {
870 const QString comment = match.captured(name: u"comment"_s);
871 signalDesc.setComment(comment);
872 messageDesc.addSignalDescription(description: signalDesc);
873 m_messageDescriptions.insert(key: uid, value: messageDesc);
874 } else {
875 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
876 "Skipping string %2").arg(args: sigName, args: data));
877 }
878 }
879}
880
881void QCanDbcFileParserPrivate::parseExtendedMux(const QStringView data)
882{
883 // The extended multiplexing is defined by the following pattern:
884 // SG_MUL_VAL_ message_id multiplexed_signal_name
885 // multiplexor_switch_name multiplexor_value_ranges ;
886 // Here multiplexor_value_ranges consists of multiple ranges, separated
887 // by a whitespace, and one range is defined as follows:
888 // multiplexor_value_range = unsigned_integer - unsigned_integer
889
890 // %1 extendedMuxDef
891 // %2 maybeSpace
892 // %3 unsignedInt
893 // %4 oneOrMoreSpace
894 // %5 DbcIdentifier
895 const QString regExStr =
896 "%1%2(?<messageId>%3)%4(?<multiplexedSignal>%5)%4(?<multiplexorSwitch>%5)%4"
897 "(?<firstRange>%3%2-%2%3)(%2,%2%3%2-%2%3)*%2;"_L1.
898 arg(args: kExtendedMuxDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp, args: kOneOrMoreSpaceRegExp,
899 args: kDbcIdentRegExp);
900 const QRegularExpression extendedMuxRegExp(regExStr);
901
902 const auto match = extendedMuxRegExp.matchView(subjectView: data);
903 if (!match.hasMatch()) {
904 m_lineOffset = data.size();
905 addWarning(warning: QObject::tr(s: "Failed to find extended multiplexing description in string %1").
906 arg(a: data));
907 return;
908 }
909
910 m_lineOffset = match.capturedEnd(nth: 0);
911
912 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
913 if (!uidOptional) {
914 addWarning(warning: QObject::tr(s: "Failed to parse frame id from string %1").arg(a: data));
915 return;
916 }
917
918 const QtCanBus::UniqueId uid = uidOptional.value();
919 auto messageDesc = m_messageDescriptions.value(key: uid);
920 if (!messageDesc.isValid()) {
921 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
922 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
923 return;
924 }
925
926 const QString multiplexedSignalName = match.captured(name: u"multiplexedSignal"_s);
927 const QString multiplexorSwitchName = match.captured(name: u"multiplexorSwitch"_s);
928
929 auto multiplexedSignal = messageDesc.signalDescriptionForName(name: multiplexedSignalName);
930 auto multiplexorSwitch = messageDesc.signalDescriptionForName(name: multiplexorSwitchName);
931
932 if (!multiplexedSignal.isValid() || !multiplexorSwitch.isValid()) {
933 const QString invalidName = multiplexedSignal.isValid() ? multiplexorSwitchName
934 : multiplexedSignalName;
935 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
936 "Skipping string %2").arg(args: invalidName, args: data));
937 return;
938 }
939
940 auto signalRanges = multiplexedSignal.multiplexSignals();
941 signalRanges.remove(key: kQtDummySignal); // dummy signal not needed anymore
942
943 QCanSignalDescription::MultiplexValues rangeValues;
944 auto rangeView = match.capturedView(name: u"firstRange"_s);
945 const auto sepIdx = rangeView.indexOf(c: u'-');
946 if (sepIdx != -1) {
947 const auto min = rangeView.first(n: sepIdx).trimmed().toUInt();
948 const auto max = rangeView.sliced(pos: sepIdx + 1).trimmed().toUInt();
949 rangeValues.push_back(t: {.minimum: min, .maximum: max});
950 }
951
952 // We can have an arbitrary amount of ranges, so we can't use capture groups
953 // to capture them. But we know that they follow a specific pattern (because
954 // the full string matched the regexp). So we need to parse the rest of the
955 // matched string manually
956 const auto totalEnd = match.capturedEnd(nth: 0); // including the ';'
957 const auto firstRangeEnd = match.capturedEnd(name: u"firstRange"_s);
958 const auto len = totalEnd - firstRangeEnd - 1;
959 if (len > 0) {
960 const auto otherRangesView = data.sliced(pos: firstRangeEnd, n: len).trimmed();
961 const QStringTokenizer parts = otherRangesView.tokenize(needle: u',', flags: Qt::SkipEmptyParts);
962 for (const QStringView range : parts) {
963 const auto sepIdx = range.indexOf(c: u'-');
964 if (sepIdx != -1) {
965 const auto min = range.first(n: sepIdx).trimmed().toUInt();
966 const auto max = range.sliced(pos: sepIdx + 1).trimmed().toUInt();
967 rangeValues.push_back(t: {.minimum: min, .maximum: max});
968 }
969 }
970 }
971
972 if (!rangeValues.isEmpty())
973 signalRanges.insert(key: multiplexorSwitchName, value: rangeValues);
974 else
975 signalRanges.remove(key: multiplexorSwitchName);
976
977 // update the value
978 multiplexedSignal.setMultiplexSignals(signalRanges);
979 messageDesc.addSignalDescription(description: multiplexedSignal);
980 m_messageDescriptions.insert(key: uid, value: messageDesc);
981}
982
983void QCanDbcFileParserPrivate::parseValueDescriptions(const QStringView data)
984{
985 // The regexp should match the following pattern:
986 // VAL_ message_id signal_name { value_description };
987 // Here the value_description is defined as follows
988 // value_description = unsigned_int char_string
989
990 // %1 valDef
991 // %2 maybeSpace
992 // %3 unsignedInt
993 // %4 oneOrMoreSpace
994 // %5 DbcIdentifier
995 // %6 charStr
996 const QString regExStr =
997 "%1%2(?<messageId>%3)%4(?<signalName>%5)(%4%3%4\"(%6)\")+%2;"_L1.
998 arg(args: kValDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp, args: kOneOrMoreSpaceRegExp,
999 args: kDbcIdentRegExp, args: kCharStrRegExp);
1000
1001 const QRegularExpression valueDescRegExp(regExStr);
1002
1003 const auto match = valueDescRegExp.matchView(subjectView: data);
1004 if (!match.hasMatch()) {
1005 m_lineOffset = data.size();
1006 addWarning(warning: QObject::tr(s: "Failed to parse value description from string %1").arg(a: data));
1007 return;
1008 }
1009
1010 m_lineOffset = match.capturedEnd(nth: 0);
1011
1012 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
1013 if (!uidOptional) {
1014 addWarning(warning: QObject::tr(s: "Failed to parse value description from string %1").arg(a: data));
1015 return;
1016 }
1017
1018 const QtCanBus::UniqueId uid = uidOptional.value();
1019 // Check if the message exists
1020 const auto messageDesc = m_messageDescriptions.value(key: uid);
1021 if (!messageDesc.isValid()) {
1022 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
1023 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
1024 return;
1025 }
1026
1027 // Check if the signal exists within the message
1028 const QString signalName = match.captured(name: u"signalName"_s);
1029 if (!messageDesc.signalDescriptionForName(name: signalName).isValid()) {
1030 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
1031 "Skipping string %2").arg(args: signalName, args: data));
1032 return;
1033 }
1034
1035 // We can have an arbitrary amount of value descriptions, so we can't use
1036 // capture groups to capture them. But we know that they follow a specific
1037 // pattern (because the full string matched the regexp). So we need to parse
1038 // the rest of the matched string manually
1039 const auto totalEnd = match.capturedEnd(nth: 0); // including the ';'
1040 const auto signalNameEnd = match.capturedEnd(name: u"signalName"_s);
1041 const auto len = totalEnd - signalNameEnd - 1;
1042 if (len > 0) {
1043 auto signalDescriptionsView = data.sliced(pos: signalNameEnd, n: len).trimmed();
1044 while (signalDescriptionsView.size()) {
1045 const auto spacePos = signalDescriptionsView.indexOf(c: u' ');
1046 if (spacePos == -1)
1047 break;
1048 bool ok = false;
1049 const auto value = signalDescriptionsView.sliced(pos: 0, n: spacePos).toUInt(ok: &ok);
1050 if (!ok)
1051 break;
1052 const auto firstQuotePos = signalDescriptionsView.indexOf(c: u'"', from: spacePos + 1);
1053 if (firstQuotePos == -1)
1054 break;
1055 const auto nextQuotePos = signalDescriptionsView.indexOf(c: u'"', from: firstQuotePos + 1);
1056 if (nextQuotePos == -1)
1057 break;
1058 const auto description = signalDescriptionsView.sliced(
1059 pos: firstQuotePos + 1, n: nextQuotePos - firstQuotePos - 1);
1060
1061 m_valueDescriptions[uid][signalName].insert(key: value, value: description.toString());
1062 signalDescriptionsView = signalDescriptionsView.sliced(pos: nextQuotePos + 1).trimmed();
1063 }
1064 }
1065}
1066
1067void QCanDbcFileParserPrivate::postProcessSignalMultiplexing()
1068{
1069 // For the case of simple multiplexing we need to do the following for
1070 // every message description:
1071 // 1. Find the multiplexor signal
1072 // 2. Replace all kQtDummySignal entries with the name of the multiplexor
1073 // 3. While doing so, check if we have any signals with type
1074 // SwitchAndSignal. This will mean that extended multiplexing is used
1075 // 4. Also detect conditions when we have more than one multiplexor signal.
1076 // This is an error as well, and message description should be discarded.
1077
1078 QList<QtCanBus::UniqueId> uidsToRemove;
1079
1080 for (auto &messageDesc : m_messageDescriptions) {
1081 bool useExtendedMux = false;
1082 QString multiplexorSignal;
1083 auto &signalDescriptions = QCanMessageDescriptionPrivate::get(desc: messageDesc)->messageSignals;
1084 for (const auto &signalDesc : std::as_const(t&: signalDescriptions)) {
1085 if (signalDesc.multiplexState() == QtCanBus::MultiplexState::MultiplexorSwitch) {
1086 if (multiplexorSignal.isEmpty()) {
1087 multiplexorSignal = signalDesc.name();
1088 } else {
1089 // invalid config
1090 multiplexorSignal.clear();
1091 uidsToRemove.push_back(t: messageDesc.uniqueId());
1092 break;
1093 }
1094 } else if (signalDesc.multiplexState() == QtCanBus::MultiplexState::SwitchAndSignal) {
1095 // extended multiplexing
1096 useExtendedMux = true;
1097 }
1098 }
1099 if (!useExtendedMux && !multiplexorSignal.isEmpty()) {
1100 // iterate through all signal descriptions and update kQtDummySignal
1101 for (auto &signalDesc : signalDescriptions) {
1102 if (signalDesc.multiplexState() == QtCanBus::MultiplexState::MultiplexedSignal) {
1103 auto &muxValues = QCanSignalDescriptionPrivate::get(desc: signalDesc)->muxSignals;
1104 auto val = muxValues.value(key: kQtDummySignal);
1105 muxValues.remove(key: kQtDummySignal);
1106 muxValues.insert(key: multiplexorSignal, value: val);
1107 }
1108 }
1109 } else if (useExtendedMux) {
1110 // Iterate through all signal descriptions and check that we do
1111 // not have any kQtDummySignal entries. It such entry exists, this
1112 // means that there were errors while parsing extended multiplexing
1113 // table, and this message description is invalid
1114 for (const auto &signalDesc : std::as_const(t&: signalDescriptions)) {
1115 const auto muxSignals = signalDesc.multiplexSignals();
1116 if (muxSignals.contains(key: kQtDummySignal)) {
1117 uidsToRemove.push_back(t: messageDesc.uniqueId());
1118 break;
1119 }
1120 }
1121 }
1122 }
1123
1124 for (const auto &uid : std::as_const(t&: uidsToRemove)) {
1125 m_messageDescriptions.remove(key: uid);
1126 addWarning(warning: QObject::tr(s: "Message description with unique id %1 is skipped because "
1127 "it has invalid multiplexing description.").
1128 arg(a: qToUnderlying(e: uid)));
1129 }
1130}
1131
1132void QCanDbcFileParserPrivate::addWarning(QString &&warning)
1133{
1134 m_warnings.emplace_back(args&: warning);
1135}
1136
1137void QCanDbcFileParserPrivate::addCurrentMessage()
1138{
1139 if (m_isProcessingMessage) {
1140 auto uid = m_currentMessage.uniqueId();
1141 if (!m_currentMessage.isValid()) {
1142 addWarning(warning: QObject::tr(s: "Message description with unique id %1 is skipped "
1143 "because it's not valid.").arg(a: qToUnderlying(e: uid)));
1144 } else if (m_messageDescriptions.contains(key: uid)) {
1145 addWarning(warning: QObject::tr(s: "Message description with unique id %1 is skipped "
1146 "because such unique id is already used.").
1147 arg(a: qToUnderlying(e: uid)));
1148 } else {
1149 m_messageDescriptions.insert(key: uid, value: m_currentMessage);
1150 }
1151 m_currentMessage = {};
1152 m_isProcessingMessage = false;
1153 }
1154}
1155
1156QList<QCanMessageDescription> QCanDbcFileParserPrivate::getMessages() const
1157{
1158 return QList<QCanMessageDescription>(m_messageDescriptions.cbegin(),
1159 m_messageDescriptions.cend());
1160}
1161
1162QT_END_NAMESPACE
1163

source code of qtserialbus/src/serialbus/qcandbcfileparser.cpp