dbc_rs/dbc/
parse.rs

1use crate::{
2    Dbc, Error, ExtendedMultiplexing, MAX_EXTENDED_MULTIPLEXING, MAX_MESSAGES, MAX_NODES,
3    MAX_SIGNALS_PER_MESSAGE, Message, Nodes, Parser, Result, Signal, ValueDescriptions, Version,
4    compat::{Comment, Name, ValueDescEntries, Vec},
5    dbc::{Messages, Validate, ValueDescriptionsMap},
6};
7
8impl Dbc {
9    /// Parse a DBC file from a string slice
10    ///
11    /// # Examples
12    ///
13    /// ```rust,no_run
14    /// use dbc_rs::Dbc;
15    ///
16    /// let dbc_content = r#"VERSION "1.0"
17    ///
18    /// BU_: ECM
19    ///
20    /// BO_ 256 EngineData : 8 ECM
21    ///  SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm""#;
22    ///
23    /// let dbc = Dbc::parse(dbc_content)?;
24    /// assert_eq!(dbc.messages().len(), 1);
25    /// # Ok::<(), dbc_rs::Error>(())
26    /// ```
27    pub fn parse(data: &str) -> Result<Self> {
28        let mut parser = Parser::new(data.as_bytes())?;
29
30        let mut messages_buffer: Vec<Message, { MAX_MESSAGES }> = Vec::new();
31
32        let mut message_count_actual = 0;
33
34        // Parse version, nodes, and messages
35        use crate::{
36            BA_, BA_DEF_, BA_DEF_DEF_, BO_, BO_TX_BU_, BS_, BU_, CM_, EV_, NS_, SG_, SG_MUL_VAL_,
37            SIG_GROUP_, SIG_VALTYPE_, VAL_, VAL_TABLE_, VERSION,
38        };
39
40        let mut version: Option<Version> = None;
41        let mut nodes: Option<Nodes> = None;
42
43        // Type aliases for parsing buffers
44        type ValueDescBufferEntry = (Option<u32>, Name, ValueDescEntries);
45        type ValueDescBuffer = Vec<ValueDescBufferEntry, { MAX_MESSAGES }>;
46        type ExtMuxBuffer = Vec<ExtendedMultiplexing, { MAX_EXTENDED_MULTIPLEXING }>;
47
48        // Comment buffers - CM_ entries can appear anywhere in the file
49        // so we collect them first and apply after parsing messages
50        type MessageCommentBuffer = Vec<(u32, Comment), { MAX_MESSAGES }>;
51        // Signal comments: (message_id, signal_name, comment)
52        type SignalCommentBuffer = Vec<(u32, Name, Comment), { MAX_MESSAGES * 4 }>;
53
54        let mut value_descriptions_buffer: ValueDescBuffer = ValueDescBuffer::new();
55        let mut extended_multiplexing_buffer: ExtMuxBuffer = ExtMuxBuffer::new();
56
57        // Comment buffers
58        let mut db_comment: Option<Comment> = None;
59        // Node comments: (node_name, comment)
60        type NodeCommentBuffer = Vec<(Name, Comment), { MAX_NODES }>;
61        let mut node_comments_buffer: NodeCommentBuffer = NodeCommentBuffer::new();
62        let mut message_comments_buffer: MessageCommentBuffer = MessageCommentBuffer::new();
63        let mut signal_comments_buffer: SignalCommentBuffer = SignalCommentBuffer::new();
64
65        loop {
66            // Skip comments (lines starting with //)
67            parser.skip_newlines_and_spaces();
68            if parser.starts_with(b"//") {
69                parser.skip_to_end_of_line();
70                continue;
71            }
72
73            let keyword_result = parser.peek_next_keyword();
74            let keyword = match keyword_result {
75                Ok(kw) => kw,
76                Err(Error::UnexpectedEof { .. }) => break,
77                Err(Error::Expected { .. }) => {
78                    if parser.starts_with(b"//") {
79                        parser.skip_to_end_of_line();
80                        continue;
81                    }
82                    return Err(keyword_result.unwrap_err());
83                }
84                Err(e) => return Err(e),
85            };
86
87            // Save position after peek_next_keyword (which skips whitespace, so we're at the keyword)
88            let pos_at_keyword = parser.pos();
89
90            match keyword {
91                NS_ => {
92                    // Consume NS_ keyword
93                    let line = parser.line();
94                    parser
95                        .expect(crate::NS_.as_bytes())
96                        .map_err(|_| Error::expected_at("Failed to consume NS_ keyword", line))?;
97                    parser.skip_newlines_and_spaces();
98                    let _ = parser.expect(b":").ok();
99                    loop {
100                        parser.skip_newlines_and_spaces();
101                        if parser.is_empty() {
102                            break;
103                        }
104                        if parser.starts_with(b" ") || parser.starts_with(b"\t") {
105                            parser.skip_to_end_of_line();
106                            continue;
107                        }
108                        if parser.starts_with(b"//") {
109                            parser.skip_to_end_of_line();
110                            continue;
111                        }
112                        if parser.starts_with(BS_.as_bytes())
113                            || parser.starts_with(BU_.as_bytes())
114                            || parser.starts_with(BO_.as_bytes())
115                            || parser.starts_with(SG_.as_bytes())
116                            || parser.starts_with(VERSION.as_bytes())
117                        {
118                            break;
119                        }
120                        parser.skip_to_end_of_line();
121                    }
122                    continue;
123                }
124                BS_ | VAL_TABLE_ | BA_DEF_ | BA_DEF_DEF_ | BA_ | SIG_GROUP_ | SIG_VALTYPE_
125                | EV_ | BO_TX_BU_ => {
126                    // Consume keyword then skip to end of line
127                    let _ = parser.expect(keyword.as_bytes()).ok();
128                    parser.skip_to_end_of_line();
129                    continue;
130                }
131                CM_ => {
132                    // Parse CM_ comment entry
133                    // Formats:
134                    //   CM_ "general comment";
135                    //   CM_ BU_ node_name "comment";
136                    //   CM_ BO_ message_id "comment";
137                    //   CM_ SG_ message_id signal_name "comment";
138                    let _ = parser.expect(crate::CM_.as_bytes()).ok();
139                    parser.skip_newlines_and_spaces();
140
141                    // Determine comment type by peeking next token
142                    if parser.starts_with(b"\"") {
143                        // General database comment: CM_ "string";
144                        if parser.expect(b"\"").is_ok() {
145                            if let Ok(comment_bytes) = parser.take_until_quote(false, 1024) {
146                                if let Ok(comment_str) = core::str::from_utf8(comment_bytes) {
147                                    if let Ok(comment) = Comment::try_from(comment_str) {
148                                        db_comment = Some(comment);
149                                    }
150                                }
151                            }
152                        }
153                        parser.skip_to_end_of_line();
154                    } else if parser.starts_with(BU_.as_bytes()) {
155                        // Node comment: CM_ BU_ node_name "string";
156                        let _ = parser.expect(BU_.as_bytes()).ok();
157                        parser.skip_newlines_and_spaces();
158                        if let Ok(node_name_bytes) = parser.parse_identifier() {
159                            if let Ok(node_name) = Name::try_from(node_name_bytes) {
160                                parser.skip_newlines_and_spaces();
161                                if parser.expect(b"\"").is_ok() {
162                                    if let Ok(comment_bytes) = parser.take_until_quote(false, 1024)
163                                    {
164                                        if let Ok(comment_str) = core::str::from_utf8(comment_bytes)
165                                        {
166                                            if let Ok(comment) = Comment::try_from(comment_str) {
167                                                let _ =
168                                                    node_comments_buffer.push((node_name, comment));
169                                            }
170                                        }
171                                    }
172                                }
173                            }
174                        }
175                        parser.skip_to_end_of_line();
176                    } else if parser.starts_with(BO_.as_bytes()) {
177                        // Message comment: CM_ BO_ message_id "string";
178                        let _ = parser.expect(BO_.as_bytes()).ok();
179                        parser.skip_newlines_and_spaces();
180                        if let Ok(message_id) = parser.parse_u32() {
181                            parser.skip_newlines_and_spaces();
182                            if parser.expect(b"\"").is_ok() {
183                                if let Ok(comment_bytes) = parser.take_until_quote(false, 1024) {
184                                    if let Ok(comment_str) = core::str::from_utf8(comment_bytes) {
185                                        if let Ok(comment) = Comment::try_from(comment_str) {
186                                            let _ =
187                                                message_comments_buffer.push((message_id, comment));
188                                        }
189                                    }
190                                }
191                            }
192                        }
193                        parser.skip_to_end_of_line();
194                    } else if parser.starts_with(SG_.as_bytes()) {
195                        // Signal comment: CM_ SG_ message_id signal_name "string";
196                        let _ = parser.expect(SG_.as_bytes()).ok();
197                        parser.skip_newlines_and_spaces();
198                        if let Ok(message_id) = parser.parse_u32() {
199                            parser.skip_newlines_and_spaces();
200                            if let Ok(signal_name_bytes) = parser.parse_identifier() {
201                                if let Ok(signal_name) = Name::try_from(signal_name_bytes) {
202                                    parser.skip_newlines_and_spaces();
203                                    if parser.expect(b"\"").is_ok() {
204                                        if let Ok(comment_bytes) =
205                                            parser.take_until_quote(false, 1024)
206                                        {
207                                            if let Ok(comment_str) =
208                                                core::str::from_utf8(comment_bytes)
209                                            {
210                                                if let Ok(comment) = Comment::try_from(comment_str)
211                                                {
212                                                    let _ = signal_comments_buffer.push((
213                                                        message_id,
214                                                        signal_name,
215                                                        comment,
216                                                    ));
217                                                }
218                                            }
219                                        }
220                                    }
221                                }
222                            }
223                        }
224                        parser.skip_to_end_of_line();
225                    } else {
226                        // Unknown comment type, skip
227                        parser.skip_to_end_of_line();
228                    }
229                    continue;
230                }
231                SG_MUL_VAL_ => {
232                    // Consume SG_MUL_VAL_ keyword
233                    let line = parser.line();
234                    parser.expect(SG_MUL_VAL_.as_bytes()).map_err(|_| {
235                        Error::expected_at("Failed to consume SG_MUL_VAL_ keyword", line)
236                    })?;
237
238                    // Parse the extended multiplexing entry
239                    if let Some(ext_mux) = ExtendedMultiplexing::parse(&mut parser) {
240                        if extended_multiplexing_buffer.push(ext_mux).is_err() {
241                            // Buffer full - return error instead of silently dropping entries
242                            return Err(Error::Validation(Error::EXTENDED_MULTIPLEXING_TOO_MANY));
243                        }
244                    } else {
245                        // Parsing failed, skip to end of line
246                        parser.skip_to_end_of_line();
247                    }
248                    continue;
249                }
250                VAL_ => {
251                    // Consume VAL_ keyword
252                    let _ = parser.expect(crate::VAL_.as_bytes()).ok();
253                    // Parse VAL_ statement: VAL_ message_id signal_name value1 "desc1" value2 "desc2" ... ;
254                    // Note: message_id of -1 (0xFFFFFFFF) means the value descriptions apply to
255                    // all signals with this name in ANY message (global value descriptions)
256                    parser.skip_newlines_and_spaces();
257                    let message_id = match parser.parse_i64() {
258                        Ok(id) => {
259                            // -1 (0xFFFFFFFF) is the magic number for global value descriptions
260                            if id == -1 {
261                                None
262                            } else if id >= 0 && id <= u32::MAX as i64 {
263                                Some(id as u32)
264                            } else {
265                                parser.skip_to_end_of_line();
266                                continue;
267                            }
268                        }
269                        Err(_) => {
270                            parser.skip_to_end_of_line();
271                            continue;
272                        }
273                    };
274                    parser.skip_newlines_and_spaces();
275                    let signal_name = match parser.parse_identifier() {
276                        Ok(name) => match Name::try_from(name) {
277                            Ok(s) => s,
278                            Err(_) => {
279                                parser.skip_to_end_of_line();
280                                continue;
281                            }
282                        },
283                        Err(_) => {
284                            parser.skip_to_end_of_line();
285                            continue;
286                        }
287                    };
288                    // Parse value-description pairs
289                    let mut entries: ValueDescEntries = ValueDescEntries::new();
290                    loop {
291                        parser.skip_newlines_and_spaces();
292                        // Check for semicolon (end of VAL_ statement)
293                        if parser.starts_with(b";") {
294                            parser.expect(b";").ok();
295                            break;
296                        }
297                        // Parse value (as i64 first to handle negative values like -1, then convert to u64)
298                        // Note: -1 (0xFFFFFFFF) is the magic number for global value descriptions in message_id,
299                        // but values in VAL_ can also be negative
300                        let value = match parser.parse_i64() {
301                            Ok(v) => {
302                                // Handle -1 specially: convert to 0xFFFFFFFF (u32::MAX) instead of large u64
303                                if v == -1 { 0xFFFF_FFFFu64 } else { v as u64 }
304                            }
305                            Err(_) => {
306                                parser.skip_to_end_of_line();
307                                break;
308                            }
309                        };
310                        parser.skip_newlines_and_spaces();
311                        // Parse description string (expect quote, then take until quote)
312                        if parser.expect(b"\"").is_err() {
313                            parser.skip_to_end_of_line();
314                            break;
315                        }
316                        let description_bytes = match parser.take_until_quote(false, 1024) {
317                            Ok(bytes) => bytes,
318                            Err(_) => {
319                                parser.skip_to_end_of_line();
320                                break;
321                            }
322                        };
323                        let description = match core::str::from_utf8(description_bytes)
324                            .ok()
325                            .and_then(|s| Name::try_from(s).ok())
326                        {
327                            Some(desc) => desc,
328                            None => {
329                                parser.skip_to_end_of_line();
330                                break;
331                            }
332                        };
333                        let _ = entries.push((value, description));
334                    }
335                    if !entries.is_empty() {
336                        let _ = value_descriptions_buffer.push((message_id, signal_name, entries));
337                    }
338                    continue;
339                }
340                VERSION => {
341                    // Version::parse expects VERSION keyword, don't consume it here
342                    version = Some(Version::parse(&mut parser)?);
343                    continue;
344                }
345                BU_ => {
346                    // Nodes::parse expects BU_ keyword, create parser from original input including it
347                    parser.skip_to_end_of_line();
348                    let bu_input = &data.as_bytes()[pos_at_keyword..parser.pos()];
349                    let mut bu_parser = Parser::new(bu_input)?;
350                    nodes = Some(Nodes::parse(&mut bu_parser)?);
351                    continue;
352                }
353                BO_ => {
354                    // Check limit using MAX_MESSAGES constant
355                    if message_count_actual >= MAX_MESSAGES {
356                        return Err(parser.err_nodes(Error::NODES_TOO_MANY));
357                    }
358
359                    // Save parser position (at BO_ keyword, so Message::parse can consume it)
360                    let message_start_pos = pos_at_keyword;
361
362                    // Don't manually parse - just find where the header ends by looking for the colon and sender
363                    // We need to find the end of the header line to separate it from signals
364                    let header_line_end = {
365                        // Skip to end of line to find where header ends
366                        let mut temp_parser = Parser::new(&data.as_bytes()[pos_at_keyword..])?;
367                        // Skip BO_ keyword
368                        temp_parser.expect(crate::BO_.as_bytes()).ok();
369                        temp_parser.skip_whitespace().ok();
370                        temp_parser.parse_u32().ok(); // ID
371                        temp_parser.skip_whitespace().ok();
372                        temp_parser.parse_identifier().ok(); // name
373                        temp_parser.skip_whitespace().ok();
374                        temp_parser.expect(b":").ok(); // colon
375                        temp_parser.skip_whitespace().ok();
376                        temp_parser.parse_u8().ok(); // DLC
377                        temp_parser.skip_whitespace().ok();
378                        temp_parser.parse_identifier().ok(); // sender
379                        pos_at_keyword + temp_parser.pos()
380                    };
381
382                    // Now parse signals from the original parser
383                    parser.skip_to_end_of_line(); // Skip past header line
384
385                    let mut signals_array: Vec<Signal, { MAX_SIGNALS_PER_MESSAGE }> = Vec::new();
386
387                    // Parse signals until we find a non-signal line
388                    loop {
389                        parser.skip_newlines_and_spaces();
390
391                        // Use peek_next_keyword to check for SG_ keyword
392                        // peek_next_keyword correctly distinguishes SG_ from SG_MUL_VAL_ (checks longer keywords first)
393                        let keyword_result = parser.peek_next_keyword();
394                        let keyword = match keyword_result {
395                            Ok(kw) => kw,
396                            Err(Error::UnexpectedEof { .. }) => break,
397                            Err(_) => break, // Not a keyword, no more signals
398                        };
399
400                        // Only process SG_ signals here (SG_MUL_VAL_ is handled in main loop)
401                        if keyword != SG_ {
402                            break; // Not a signal, exit signal parsing loop
403                        }
404
405                        // Check limit before parsing
406                        if signals_array.len() >= MAX_SIGNALS_PER_MESSAGE {
407                            return Err(parser.err_message(Error::MESSAGE_TOO_MANY_SIGNALS));
408                        }
409
410                        // Parse signal - Signal::parse consumes SG_ itself
411                        match Signal::parse(&mut parser) {
412                            Ok(signal) => {
413                                signals_array.push(signal).map_err(|_| {
414                                    parser.err_receivers(Error::SIGNAL_RECEIVERS_TOO_MANY)
415                                })?;
416                                // Receivers::parse stops at newline but doesn't consume it
417                                // Consume it so next iteration starts at the next line
418                                if parser.at_newline() {
419                                    parser.skip_to_end_of_line();
420                                }
421                            }
422                            Err(_) => {
423                                // Parsing failed, skip to end of line and stop
424                                parser.skip_to_end_of_line();
425                                break;
426                            }
427                        }
428                    }
429
430                    // Restore parser to start of message line and use Message::parse
431                    // Create a new parser from the original input, but only up to the end of the header
432                    // (not including signals, so Message::parse doesn't complain about extra content)
433                    let message_input = &data.as_bytes()[message_start_pos..header_line_end];
434                    let mut message_parser = Parser::new(message_input)?;
435
436                    // Use Message::parse which will parse the header and use our signals
437                    let message = Message::parse(&mut message_parser, signals_array.as_slice())?;
438
439                    messages_buffer
440                        .push(message)
441                        .map_err(|_| parser.err_message(Error::NODES_TOO_MANY))?;
442                    message_count_actual += 1;
443                    continue;
444                }
445                SG_ => {
446                    // Orphaned signal (not inside a message) - skip it
447                    parser.skip_to_end_of_line();
448                    continue;
449                }
450                _ => {
451                    parser.skip_to_end_of_line();
452                    continue;
453                }
454            }
455        }
456
457        // Allow empty nodes (DBC spec allows empty BU_: line)
458        let mut nodes = nodes.unwrap_or_default();
459
460        // Apply node comments to nodes
461        for (node_name, comment) in node_comments_buffer.iter() {
462            nodes.set_node_comment(node_name.as_str(), comment.clone());
463        }
464
465        // If no version was parsed, default to empty version
466        let version = version.or_else(|| {
467            static EMPTY_VERSION: &[u8] = b"VERSION \"\"";
468            let mut parser = Parser::new(EMPTY_VERSION).ok()?;
469            Version::parse(&mut parser).ok()
470        });
471
472        // Build value descriptions map for storage in Dbc
473        let value_descriptions_map = {
474            let mut map: crate::compat::BTreeMap<
475                (Option<u32>, Name),
476                ValueDescriptions,
477                { MAX_MESSAGES },
478            > = crate::compat::BTreeMap::new();
479            for (message_id, signal_name, entries) in value_descriptions_buffer.iter() {
480                let key = (*message_id, signal_name.clone());
481                let value_descriptions = ValueDescriptions::new(entries.clone());
482                let _ = map.insert(key, value_descriptions);
483            }
484            ValueDescriptionsMap::new(map)
485        };
486
487        // Apply comments to messages and signals
488        // Message comments are applied by matching message_id
489        for (message_id, comment) in message_comments_buffer.iter() {
490            for msg in messages_buffer.iter_mut() {
491                if msg.id() == *message_id || msg.id_with_flag() == *message_id {
492                    msg.set_comment(comment.clone());
493                    break;
494                }
495            }
496        }
497
498        // Signal comments are applied by matching (message_id, signal_name)
499        for (message_id, signal_name, comment) in signal_comments_buffer.iter() {
500            for msg in messages_buffer.iter_mut() {
501                if msg.id() == *message_id || msg.id_with_flag() == *message_id {
502                    if let Some(signal) = msg.signals_mut().find_mut(signal_name.as_str()) {
503                        signal.set_comment(comment.clone());
504                    }
505                    break;
506                }
507            }
508        }
509
510        // Convert messages buffer to slice for validation and construction
511        let messages_slice: &[Message] = messages_buffer.as_slice();
512        let extended_multiplexing_slice: &[ExtendedMultiplexing] =
513            extended_multiplexing_buffer.as_slice();
514
515        // Validate messages (duplicate IDs, sender in nodes, etc.)
516        Validate::validate(
517            &nodes,
518            messages_slice,
519            Some(&value_descriptions_map),
520            Some(extended_multiplexing_slice),
521        )
522        .map_err(|e| {
523            crate::error::map_val_error(e, Error::message, || {
524                Error::message(Error::MESSAGE_ERROR_PREFIX)
525            })
526        })?;
527
528        // Construct directly (validation already done)
529        let messages = Messages::new(messages_slice)?;
530
531        Ok(Dbc::new(
532            version,
533            nodes,
534            messages,
535            value_descriptions_map,
536            extended_multiplexing_buffer,
537            db_comment,
538        ))
539    }
540
541    /// Parse a DBC file from a byte slice
542    ///
543    /// # Examples
544    ///
545    /// ```rust,no_run
546    /// use dbc_rs::Dbc;
547    ///
548    /// let dbc_bytes = b"VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM";
549    /// let dbc = Dbc::parse_bytes(dbc_bytes)?;
550    /// println!("Parsed {} messages", dbc.messages().len());
551    /// # Ok::<(), dbc_rs::Error>(())
552    /// ```
553    pub fn parse_bytes(data: &[u8]) -> Result<Self> {
554        let content =
555            core::str::from_utf8(data).map_err(|_e| Error::expected(Error::INVALID_UTF8))?;
556        Dbc::parse(content)
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use crate::Dbc;
563
564    #[test]
565    fn test_parse_basic() {
566        let dbc_content = r#"VERSION "1.0"
567
568BU_: ECM
569
570BO_ 256 Engine : 8 ECM
571 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
572"#;
573        let dbc = Dbc::parse(dbc_content).unwrap();
574        assert_eq!(dbc.version().map(|v| v.as_str()), Some("1.0"));
575        assert!(dbc.nodes().contains("ECM"));
576        assert_eq!(dbc.messages().len(), 1);
577    }
578
579    #[test]
580    fn test_parse_bytes() {
581        let dbc_bytes = b"VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM";
582        let dbc = Dbc::parse_bytes(dbc_bytes).unwrap();
583        assert_eq!(dbc.version().map(|v| v.as_str()), Some("1.0"));
584        assert!(dbc.nodes().contains("ECM"));
585        assert_eq!(dbc.messages().len(), 1);
586    }
587
588    #[test]
589    fn test_parse_empty_nodes() {
590        let dbc_content = r#"VERSION "1.0"
591
592BU_:
593
594BO_ 256 Engine : 8 ECM
595"#;
596        let dbc = Dbc::parse(dbc_content).unwrap();
597        assert!(dbc.nodes().is_empty());
598    }
599
600    #[test]
601    fn test_parse_no_version() {
602        let dbc_content = r#"BU_: ECM
603
604BO_ 256 Engine : 8 ECM
605"#;
606        let dbc = Dbc::parse(dbc_content).unwrap();
607        // Should default to empty version
608        assert!(dbc.version().is_some());
609    }
610
611    #[test]
612    fn parses_real_dbc() {
613        let data = r#"VERSION "1.0"
614
615BU_: ECM TCM
616
617BO_ 256 Engine : 8 ECM
618 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
619 SG_ Temp : 16|8@1- (1,-40) [-40|215] "°C"
620
621BO_ 512 Brake : 4 TCM
622 SG_ Pressure : 0|16@1+ (0.1,0) [0|1000] "bar""#;
623
624        let dbc = Dbc::parse(data).unwrap();
625        assert_eq!(dbc.messages().len(), 2);
626        let mut messages_iter = dbc.messages().iter();
627        let msg0 = messages_iter.next().unwrap();
628        assert_eq!(msg0.signals().len(), 2);
629        let mut signals_iter = msg0.signals().iter();
630        assert_eq!(signals_iter.next().unwrap().name(), "RPM");
631        assert_eq!(signals_iter.next().unwrap().name(), "Temp");
632        let msg1 = messages_iter.next().unwrap();
633        assert_eq!(msg1.signals().len(), 1);
634        assert_eq!(msg1.signals().iter().next().unwrap().name(), "Pressure");
635    }
636
637    #[test]
638    fn test_parse_duplicate_message_id() {
639        use crate::Error;
640        // Test that parse also validates duplicate message IDs
641        let data = r#"VERSION "1.0"
642
643BU_: ECM
644
645BO_ 256 EngineData1 : 8 ECM
646 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
647
648BO_ 256 EngineData2 : 8 ECM
649 SG_ Temp : 16|8@0- (1,-40) [-40|215] "°C"
650"#;
651
652        let result = Dbc::parse(data);
653        assert!(result.is_err());
654        match result.unwrap_err() {
655            Error::Message { msg, .. } => {
656                assert!(msg.contains(Error::DUPLICATE_MESSAGE_ID));
657            }
658            _ => panic!("Expected Error::Message"),
659        }
660    }
661
662    #[test]
663    fn test_parse_sender_not_in_nodes() {
664        use crate::Error;
665        // Test that parse also validates message senders are in nodes list
666        let data = r#"VERSION "1.0"
667
668BU_: ECM
669
670BO_ 256 EngineData : 8 TCM
671 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
672"#;
673
674        let result = Dbc::parse(data);
675        assert!(result.is_err());
676        match result.unwrap_err() {
677            Error::Message { msg, .. } => {
678                assert!(msg.contains(Error::SENDER_NOT_IN_NODES));
679            }
680            _ => panic!("Expected Error::Message"),
681        }
682    }
683
684    #[test]
685    fn test_parse_empty_file() {
686        use crate::Error;
687        // Test parsing an empty file
688        let result = Dbc::parse("");
689        assert!(result.is_err());
690        match result.unwrap_err() {
691            Error::UnexpectedEof { .. } => {
692                // Empty file should result in unexpected EOF
693            }
694            _ => panic!("Expected Error::UnexpectedEof"),
695        }
696    }
697
698    #[test]
699    fn test_parse_bytes_invalid_utf8() {
700        use crate::Error;
701        // Invalid UTF-8 sequence
702        let invalid_bytes = &[0xFF, 0xFE, 0xFD];
703        let result = Dbc::parse_bytes(invalid_bytes);
704        assert!(result.is_err());
705        match result.unwrap_err() {
706            Error::Expected { msg, .. } => {
707                assert_eq!(msg, Error::INVALID_UTF8);
708            }
709            _ => panic!("Expected Error::Expected with INVALID_UTF8"),
710        }
711    }
712
713    #[test]
714    fn test_parse_without_version_with_comment() {
715        // DBC file with comment and no VERSION line
716        let data = r#"// This is a comment
717BU_: ECM
718
719BO_ 256 Engine : 8 ECM
720 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
721"#;
722        let dbc = Dbc::parse(data).unwrap();
723        assert_eq!(dbc.version().map(|v| v.as_str()), Some(""));
724    }
725
726    #[test]
727    fn test_parse_with_strict_boundary_check() {
728        // Test that strict mode (default) rejects signals that extend beyond boundaries
729        let data = r#"VERSION "1.0"
730
731BU_: ECM
732
733BO_ 256 Test : 8 ECM
734 SG_ CHECKSUM : 63|8@1+ (1,0) [0|255] ""
735"#;
736
737        // Default (strict) mode should fail
738        let result = Dbc::parse(data);
739        assert!(result.is_err());
740    }
741
742    #[cfg(feature = "std")]
743    #[test]
744    fn test_parse_val_value_descriptions() {
745        let data = r#"VERSION ""
746
747NS_ :
748
749BS_:
750
751BU_: Node1 Node2
752
753BO_ 100 Message1 : 8 Node1
754 SG_ Signal : 32|8@1- (1,0) [-1|4] "Gear" Node2
755
756VAL_ 100 Signal -1 "Reverse" 0 "Neutral" 1 "First" 2 "Second" 3 "Third" 4 "Fourth" ;
757"#;
758
759        let dbc = match Dbc::parse(data) {
760            Ok(dbc) => dbc,
761            Err(e) => panic!("Failed to parse DBC: {:?}", e),
762        };
763
764        // Verify basic structure
765        assert_eq!(dbc.messages().len(), 1);
766        let message = dbc.messages().iter().find(|m| m.id() == 100).unwrap();
767        assert_eq!(message.name(), "Message1");
768        assert_eq!(message.sender(), "Node1");
769
770        // Verify value descriptions
771        let value_descriptions = dbc
772            .value_descriptions_for_signal(100, "Signal")
773            .expect("Value descriptions should exist");
774        assert_eq!(value_descriptions.get(0xFFFFFFFF), Some("Reverse")); // -1 as u64
775        assert_eq!(value_descriptions.get(0), Some("Neutral"));
776        assert_eq!(value_descriptions.get(1), Some("First"));
777        assert_eq!(value_descriptions.get(2), Some("Second"));
778        assert_eq!(value_descriptions.get(3), Some("Third"));
779        assert_eq!(value_descriptions.get(4), Some("Fourth"));
780    }
781
782    #[cfg(feature = "std")]
783    #[test]
784    fn test_parse_val_global_value_descriptions() {
785        // Test global value descriptions (VAL_ -1) that apply to all signals with the same name
786        let data = r#"VERSION "1.0"
787
788NS_ :
789
790    VAL_
791
792BS_:
793
794BU_: ECU DASH
795
796BO_ 256 EngineData: 8 ECU
797 SG_ EngineRPM : 0|16@1+ (0.125,0) [0|8000] "rpm" Vector__XXX
798 SG_ DI_gear : 24|3@1+ (1,0) [0|7] "" Vector__XXX
799
800BO_ 512 DashboardDisplay: 8 DASH
801 SG_ DI_gear : 0|3@1+ (1,0) [0|7] "" Vector__XXX
802 SG_ SpeedDisplay : 8|16@1+ (0.01,0) [0|300] "km/h" Vector__XXX
803
804VAL_ -1 DI_gear 0 "INVALID" 1 "P" 2 "R" 3 "N" 4 "D" 5 "S" 6 "L" 7 "SNA" ;
805"#;
806
807        let dbc = match Dbc::parse(data) {
808            Ok(dbc) => dbc,
809            Err(e) => panic!("Failed to parse DBC: {:?}", e),
810        };
811
812        // Verify basic structure
813        assert_eq!(dbc.messages().len(), 2);
814
815        // Verify first message (EngineData)
816        let engine_msg = dbc.messages().iter().find(|m| m.id() == 256).unwrap();
817        assert_eq!(engine_msg.name(), "EngineData");
818        assert_eq!(engine_msg.sender(), "ECU");
819        let di_gear_signal1 = engine_msg.signals().find("DI_gear").unwrap();
820        assert_eq!(di_gear_signal1.name(), "DI_gear");
821        assert_eq!(di_gear_signal1.start_bit(), 24);
822
823        // Verify second message (DashboardDisplay)
824        let dash_msg = dbc.messages().iter().find(|m| m.id() == 512).unwrap();
825        assert_eq!(dash_msg.name(), "DashboardDisplay");
826        assert_eq!(dash_msg.sender(), "DASH");
827        let di_gear_signal2 = dash_msg.signals().find("DI_gear").unwrap();
828        assert_eq!(di_gear_signal2.name(), "DI_gear");
829        assert_eq!(di_gear_signal2.start_bit(), 0);
830
831        // Verify global value descriptions apply to DI_gear in message 256
832        let value_descriptions1 = dbc
833            .value_descriptions_for_signal(256, "DI_gear")
834            .expect("Global value descriptions should exist for DI_gear in message 256");
835
836        assert_eq!(value_descriptions1.get(0), Some("INVALID"));
837        assert_eq!(value_descriptions1.get(1), Some("P"));
838        assert_eq!(value_descriptions1.get(2), Some("R"));
839        assert_eq!(value_descriptions1.get(3), Some("N"));
840        assert_eq!(value_descriptions1.get(4), Some("D"));
841        assert_eq!(value_descriptions1.get(5), Some("S"));
842        assert_eq!(value_descriptions1.get(6), Some("L"));
843        assert_eq!(value_descriptions1.get(7), Some("SNA"));
844
845        // Verify global value descriptions also apply to DI_gear in message 512
846        let value_descriptions2 = dbc
847            .value_descriptions_for_signal(512, "DI_gear")
848            .expect("Global value descriptions should exist for DI_gear in message 512");
849
850        // Both should return the same value descriptions (same reference or same content)
851        assert_eq!(value_descriptions2.get(0), Some("INVALID"));
852        assert_eq!(value_descriptions2.get(1), Some("P"));
853        assert_eq!(value_descriptions2.get(2), Some("R"));
854        assert_eq!(value_descriptions2.get(3), Some("N"));
855        assert_eq!(value_descriptions2.get(4), Some("D"));
856        assert_eq!(value_descriptions2.get(5), Some("S"));
857        assert_eq!(value_descriptions2.get(6), Some("L"));
858        assert_eq!(value_descriptions2.get(7), Some("SNA"));
859
860        // Verify they should be the same instance (both reference the global entry)
861        // Since we store by (Option<u32>, &str), both should return the same entry
862        assert_eq!(value_descriptions1.len(), value_descriptions2.len());
863        assert_eq!(value_descriptions1.len(), 8);
864
865        // Verify other signals don't have value descriptions
866        assert_eq!(dbc.value_descriptions_for_signal(256, "EngineRPM"), None);
867        assert_eq!(dbc.value_descriptions_for_signal(512, "SpeedDisplay"), None);
868    }
869
870    // ============================================================================
871    // Specification Compliance Tests
872    // These tests verify against exact requirements from dbc/SPECIFICATIONS.md
873    // ============================================================================
874
875    /// Verify Section 8.3: DLC = 0 is valid
876    /// "CAN 2.0: 0 to 8 bytes"
877    /// "CAN FD: 0 to 64 bytes"
878    #[test]
879    fn test_spec_section_8_3_dlc_zero_is_valid() {
880        // DLC = 0 is valid per spec (e.g., for control messages without data payload)
881        let data = r#"VERSION "1.0"
882
883BU_: ECM
884
885BO_ 256 ControlMessage : 0 ECM
886"#;
887        let dbc = Dbc::parse(data).unwrap();
888        assert_eq!(dbc.messages().len(), 1);
889        let msg = dbc.messages().iter().next().unwrap();
890        assert_eq!(msg.dlc(), 0);
891    }
892
893    /// Verify Section 8.1: Extended CAN ID format
894    /// "Extended ID in DBC = 0x80000000 | actual_extended_id"
895    /// "Example: 0x80001234 represents extended ID 0x1234"
896    #[test]
897    fn test_spec_section_8_1_extended_can_id_format() {
898        // Extended ID 0x494 is stored as 0x80000000 | 0x494 = 0x80000494 = 2147484820
899        // 0x80000000 = 2147483648, 0x494 = 1172, 2147483648 + 1172 = 2147484820
900        let data = r#"VERSION "1.0"
901
902BU_: ECM
903
904BO_ 2147484820 ExtendedMessage : 8 ECM
905"#;
906        let dbc = Dbc::parse(data).unwrap();
907        assert_eq!(dbc.messages().len(), 1);
908        let msg = dbc.messages().iter().next().unwrap();
909        // id() returns the raw CAN ID without the extended flag
910        assert_eq!(msg.id(), 0x494); // Raw extended ID
911        assert!(msg.is_extended()); // is_extended() tells if it's a 29-bit ID
912    }
913
914    /// Verify Section 8.3: Maximum extended ID (0x1FFFFFFF) with bit 31 flag
915    #[test]
916    fn test_spec_section_8_1_max_extended_id() {
917        // Maximum extended ID: 0x80000000 | 0x1FFFFFFF = 0x9FFFFFFF = 2684354559
918        let data = r#"VERSION "1.0"
919
920BU_: ECM
921
922BO_ 2684354559 MaxExtendedId : 8 ECM
923"#;
924        let dbc = Dbc::parse(data).unwrap();
925        assert_eq!(dbc.messages().len(), 1);
926        let msg = dbc.messages().iter().next().unwrap();
927        // id() returns the raw 29-bit CAN ID without the extended flag
928        assert_eq!(msg.id(), 0x1FFFFFFF);
929        assert!(msg.is_extended());
930    }
931
932    /// Verify Section 8.4: Vector__XXX as transmitter
933    /// "Vector__XXX - No sender / unknown sender"
934    #[test]
935    fn test_spec_section_8_4_vector_xxx_transmitter() {
936        let data = r#"VERSION "1.0"
937
938BU_: Gateway
939
940BO_ 256 UnknownSender : 8 Vector__XXX
941 SG_ Signal1 : 0|8@1+ (1,0) [0|255] "" Gateway
942"#;
943        let dbc = Dbc::parse(data).unwrap();
944        assert_eq!(dbc.messages().len(), 1);
945        let msg = dbc.messages().iter().next().unwrap();
946        assert_eq!(msg.sender(), "Vector__XXX");
947    }
948
949    /// Verify Section 9.5: Receivers format
950    /// Parser accepts both comma-separated (per spec) and space-separated (tool extension)
951    #[test]
952    fn test_spec_section_9_5_receivers_comma_separated() {
953        // Comma-separated receivers (per spec)
954        // Note: The parser identifier function stops at commas, so we test that comma-separated
955        // receiver parsing works correctly
956        use crate::{Parser, Signal};
957
958        // Test comma-separated receivers directly via Signal::parse
959        let signal = Signal::parse(
960            &mut Parser::new(b"SG_ RPM : 0|16@1+ (0.25,0) [0|8000] \"rpm\" Gateway,Dashboard")
961                .unwrap(),
962        )
963        .unwrap();
964        assert_eq!(signal.receivers().len(), 2);
965        let mut receivers = signal.receivers().iter();
966        assert_eq!(receivers.next(), Some("Gateway"));
967        assert_eq!(receivers.next(), Some("Dashboard"));
968    }
969
970    /// Verify Section 9.4: Multiplexer indicator patterns
971    /// "M" for multiplexer switch, "m0", "m1", etc. for multiplexed signals
972    #[test]
973    fn test_spec_section_9_4_multiplexer_indicators() {
974        let data = r#"VERSION "1.0"
975
976BU_: ECM Gateway
977
978BO_ 400 MultiplexedMsg : 8 ECM
979 SG_ MuxSwitch M : 0|8@1+ (1,0) [0|255] "" Gateway
980 SG_ Signal_0 m0 : 8|16@1+ (0.1,0) [0|1000] "kPa" Gateway
981 SG_ Signal_1 m1 : 8|16@1+ (0.01,0) [0|100] "degC" Gateway
982"#;
983        let dbc = Dbc::parse(data).unwrap();
984        let msg = dbc.messages().iter().next().unwrap();
985
986        // Find signals by name
987        let mux_switch = msg.signals().find("MuxSwitch").unwrap();
988        let signal_0 = msg.signals().find("Signal_0").unwrap();
989        let signal_1 = msg.signals().find("Signal_1").unwrap();
990
991        // Verify multiplexer switch
992        assert!(mux_switch.is_multiplexer_switch());
993        assert_eq!(mux_switch.multiplexer_switch_value(), None);
994
995        // Verify multiplexed signals
996        assert!(!signal_0.is_multiplexer_switch());
997        assert_eq!(signal_0.multiplexer_switch_value(), Some(0));
998
999        assert!(!signal_1.is_multiplexer_switch());
1000        assert_eq!(signal_1.multiplexer_switch_value(), Some(1));
1001    }
1002
1003    #[test]
1004    fn test_error_includes_line_number() {
1005        // Test that parsing errors include line numbers
1006        let data = r#"VERSION "1.0"
1007
1008BU_: ECM
1009
1010BO_ invalid EngineData : 8 ECM
1011"#;
1012
1013        let result = Dbc::parse(data);
1014        assert!(result.is_err());
1015        let err = result.unwrap_err();
1016        // The error should have line information
1017        assert!(err.line().is_some(), "Error should include line number");
1018    }
1019
1020    // ============================================================================
1021    // CM_ Comment Parsing Tests (Section 14 of SPECIFICATIONS.md)
1022    // ============================================================================
1023
1024    /// Test parsing general database comment: CM_ "string";
1025    #[test]
1026    fn test_parse_cm_database_comment() {
1027        let data = r#"VERSION "1.0"
1028
1029BU_: ECM
1030
1031BO_ 256 Engine : 8 ECM
1032
1033CM_ "This is the database comment";
1034"#;
1035        let dbc = Dbc::parse(data).unwrap();
1036        assert_eq!(dbc.comment(), Some("This is the database comment"));
1037    }
1038
1039    /// Test parsing node comment: CM_ BU_ node_name "string";
1040    #[test]
1041    fn test_parse_cm_node_comment() {
1042        let data = r#"VERSION "1.0"
1043
1044BU_: ECM
1045
1046BO_ 256 Engine : 8 ECM
1047
1048CM_ BU_ ECM "Engine Control Module";
1049"#;
1050        let dbc = Dbc::parse(data).unwrap();
1051        assert_eq!(dbc.node_comment("ECM"), Some("Engine Control Module"));
1052    }
1053
1054    /// Test parsing message comment: CM_ BO_ message_id "string";
1055    #[test]
1056    fn test_parse_cm_message_comment() {
1057        let data = r#"VERSION "1.0"
1058
1059BU_: ECM
1060
1061BO_ 256 Engine : 8 ECM
1062
1063CM_ BO_ 256 "Engine status message";
1064"#;
1065        let dbc = Dbc::parse(data).unwrap();
1066        let msg = dbc.messages().iter().next().unwrap();
1067        assert_eq!(msg.comment(), Some("Engine status message"));
1068    }
1069
1070    /// Test parsing signal comment: CM_ SG_ message_id signal_name "string";
1071    #[test]
1072    fn test_parse_cm_signal_comment() {
1073        let data = r#"VERSION "1.0"
1074
1075BU_: ECM
1076
1077BO_ 256 Engine : 8 ECM
1078 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
1079
1080CM_ SG_ 256 RPM "Engine rotations per minute";
1081"#;
1082        let dbc = Dbc::parse(data).unwrap();
1083        let msg = dbc.messages().iter().next().unwrap();
1084        let signal = msg.signals().find("RPM").unwrap();
1085        assert_eq!(signal.comment(), Some("Engine rotations per minute"));
1086    }
1087
1088    /// Test multiple comments in one file
1089    #[test]
1090    fn test_parse_cm_multiple_comments() {
1091        let data = r#"VERSION "1.0"
1092
1093BU_: ECM TCM
1094
1095BO_ 256 Engine : 8 ECM
1096 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
1097
1098BO_ 512 Trans : 8 TCM
1099 SG_ Gear : 0|8@1+ (1,0) [0|6] ""
1100
1101CM_ "Vehicle CAN database";
1102CM_ BU_ ECM "Engine Control Module";
1103CM_ BU_ TCM "Transmission Control Module";
1104CM_ BO_ 256 "Engine status message";
1105CM_ BO_ 512 "Transmission status";
1106CM_ SG_ 256 RPM "Engine rotations per minute";
1107CM_ SG_ 512 Gear "Current gear position";
1108"#;
1109        let dbc = Dbc::parse(data).unwrap();
1110
1111        // Database comment
1112        assert_eq!(dbc.comment(), Some("Vehicle CAN database"));
1113
1114        // Node comments
1115        assert_eq!(dbc.node_comment("ECM"), Some("Engine Control Module"));
1116        assert_eq!(dbc.node_comment("TCM"), Some("Transmission Control Module"));
1117
1118        // Message comments
1119        let engine = dbc.messages().iter().find(|m| m.id() == 256).unwrap();
1120        let trans = dbc.messages().iter().find(|m| m.id() == 512).unwrap();
1121        assert_eq!(engine.comment(), Some("Engine status message"));
1122        assert_eq!(trans.comment(), Some("Transmission status"));
1123
1124        // Signal comments
1125        let rpm = engine.signals().find("RPM").unwrap();
1126        let gear = trans.signals().find("Gear").unwrap();
1127        assert_eq!(rpm.comment(), Some("Engine rotations per minute"));
1128        assert_eq!(gear.comment(), Some("Current gear position"));
1129    }
1130
1131    /// Test CM_ appearing before the entities they describe
1132    #[test]
1133    fn test_parse_cm_before_entity() {
1134        let data = r#"VERSION "1.0"
1135
1136BU_: ECM
1137
1138CM_ BO_ 256 "Engine status message";
1139CM_ SG_ 256 RPM "Engine RPM";
1140
1141BO_ 256 Engine : 8 ECM
1142 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
1143"#;
1144        let dbc = Dbc::parse(data).unwrap();
1145        let msg = dbc.messages().iter().next().unwrap();
1146        assert_eq!(msg.comment(), Some("Engine status message"));
1147        let signal = msg.signals().find("RPM").unwrap();
1148        assert_eq!(signal.comment(), Some("Engine RPM"));
1149    }
1150
1151    /// Test multiple CM_ entries for same entity - last wins
1152    #[test]
1153    fn test_parse_cm_last_wins() {
1154        let data = r#"VERSION "1.0"
1155
1156BU_: ECM
1157
1158BO_ 256 Engine : 8 ECM
1159 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
1160
1161CM_ BO_ 256 "First message comment";
1162CM_ BO_ 256 "Second message comment";
1163CM_ SG_ 256 RPM "First signal comment";
1164CM_ SG_ 256 RPM "Second signal comment";
1165CM_ BU_ ECM "First node comment";
1166CM_ BU_ ECM "Second node comment";
1167"#;
1168        let dbc = Dbc::parse(data).unwrap();
1169
1170        // Last comment wins for each entity
1171        let msg = dbc.messages().iter().next().unwrap();
1172        assert_eq!(msg.comment(), Some("Second message comment"));
1173        let signal = msg.signals().find("RPM").unwrap();
1174        assert_eq!(signal.comment(), Some("Second signal comment"));
1175        assert_eq!(dbc.node_comment("ECM"), Some("Second node comment"));
1176    }
1177
1178    /// Test comment round-trip (parse -> serialize -> parse)
1179    #[test]
1180    #[cfg(feature = "std")]
1181    fn test_parse_cm_round_trip() {
1182        let data = r#"VERSION "1.0"
1183
1184BU_: ECM TCM
1185
1186BO_ 256 Engine : 8 ECM
1187 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
1188
1189CM_ "Database comment";
1190CM_ BU_ ECM "Engine Control Module";
1191CM_ BO_ 256 "Engine status message";
1192CM_ SG_ 256 RPM "Engine rotations per minute";
1193"#;
1194        let dbc = Dbc::parse(data).unwrap();
1195
1196        // Serialize and re-parse
1197        let serialized = dbc.to_dbc_string();
1198        let dbc2 = Dbc::parse(&serialized).unwrap();
1199
1200        // Verify comments are preserved
1201        assert_eq!(dbc2.comment(), Some("Database comment"));
1202        assert_eq!(dbc2.node_comment("ECM"), Some("Engine Control Module"));
1203        let msg = dbc2.messages().iter().next().unwrap();
1204        assert_eq!(msg.comment(), Some("Engine status message"));
1205        let signal = msg.signals().find("RPM").unwrap();
1206        assert_eq!(signal.comment(), Some("Engine rotations per minute"));
1207    }
1208
1209    /// Test CM_ serialization in output
1210    #[test]
1211    #[cfg(feature = "std")]
1212    fn test_serialize_cm_comments() {
1213        let data = r#"VERSION "1.0"
1214
1215BU_: ECM
1216
1217BO_ 256 Engine : 8 ECM
1218 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
1219
1220CM_ "Database comment";
1221CM_ BU_ ECM "Engine Control Module";
1222CM_ BO_ 256 "Engine status";
1223CM_ SG_ 256 RPM "RPM signal";
1224"#;
1225        let dbc = Dbc::parse(data).unwrap();
1226        let serialized = dbc.to_dbc_string();
1227
1228        // Verify CM_ lines are present in output
1229        assert!(serialized.contains("CM_ \"Database comment\";"));
1230        assert!(serialized.contains("CM_ BU_ ECM \"Engine Control Module\";"));
1231        assert!(serialized.contains("CM_ BO_ 256 \"Engine status\";"));
1232        assert!(serialized.contains("CM_ SG_ 256 RPM \"RPM signal\";"));
1233    }
1234}