dbc_rs/dbc/
dbc.rs

1#[cfg(feature = "alloc")]
2use crate::{Error, Result, error::messages as error_messages};
3use crate::{
4    Message, Messages, Nodes, ParseOptions, Parser, Signal, Signals, Version,
5    error::{ParseError, ParseResult},
6};
7
8#[cfg(feature = "alloc")]
9use alloc::string::String;
10
11/// Represents a complete DBC (CAN database) file.
12///
13/// A `Dbc` contains:
14/// - An optional version string
15/// - A list of nodes (ECUs)
16/// - A collection of messages with their signals
17///
18/// # Examples
19///
20/// ```rust,no_run
21/// use dbc_rs::Dbc;
22///
23/// let dbc_content = r#"VERSION "1.0"
24///
25/// BU_: ECM TCM
26///
27/// BO_ 256 EngineData : 8 ECM
28///  SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm" TCM
29/// "#;
30///
31/// let dbc = Dbc::parse(dbc_content)?;
32/// println!("Parsed {} messages", dbc.messages().len());
33/// # Ok::<(), dbc_rs::Error>(())
34/// ```
35#[derive(Debug)]
36pub struct Dbc<'a> {
37    version: Option<Version<'a>>,
38    nodes: Nodes<'a>,
39    messages: Messages<'a>,
40}
41
42impl<'a> Dbc<'a> {
43    pub(crate) fn validate(
44        _version: Option<&Version<'_>>,
45        nodes: &Nodes<'_>,
46        messages: &[Option<Message<'_>>],
47        message_count: usize,
48    ) -> ParseResult<()> {
49        #[cfg(feature = "alloc")]
50        use crate::error::messages as error_messages;
51
52        // Check for duplicate message IDs
53        let messages_slice = &messages[..message_count];
54        for (i, msg1_opt) in messages_slice.iter().enumerate() {
55            let msg1 = match msg1_opt {
56                Some(m) => m,
57                None => continue, // Should not happen, but be safe
58            };
59            for msg2_opt in messages_slice.iter().skip(i + 1) {
60                let msg2 = match msg2_opt {
61                    Some(m) => m,
62                    None => continue, // Should not happen, but be safe
63                };
64                if msg1.id() == msg2.id() {
65                    #[cfg(feature = "alloc")]
66                    {
67                        let msg = error_messages::duplicate_message_id(
68                            msg1.id(),
69                            msg1.name(),
70                            msg2.name(),
71                        );
72                        return Err(ParseError::Version(msg.leak()));
73                    }
74                    #[cfg(not(feature = "alloc"))]
75                    {
76                        return Err(ParseError::Version(
77                            crate::error::lang::FORMAT_DUPLICATE_MESSAGE_ID,
78                        ));
79                    }
80                }
81            }
82        }
83
84        // Validate that all message senders are in the nodes list
85        // Skip validation if nodes list is empty (empty nodes allowed per DBC spec)
86        if !nodes.is_empty() {
87            for msg_opt in messages_slice {
88                let msg = match msg_opt {
89                    Some(m) => m,
90                    None => continue, // Should not happen, but be safe
91                };
92                if !nodes.contains(msg.sender()) {
93                    #[cfg(feature = "alloc")]
94                    {
95                        let msg_str = error_messages::sender_not_in_nodes(msg.name(), msg.sender());
96                        return Err(ParseError::Version(msg_str.leak()));
97                    }
98                    #[cfg(not(feature = "alloc"))]
99                    {
100                        return Err(ParseError::Version(
101                            crate::error::lang::FORMAT_SENDER_NOT_IN_NODES,
102                        ));
103                    }
104                }
105            }
106        }
107
108        Ok(())
109    }
110
111    #[allow(dead_code)] // Only used by builders (std-only)
112    pub(crate) fn new(
113        version: Option<Version<'a>>,
114        nodes: Nodes<'a>,
115        messages: &'a [Message<'a>],
116    ) -> Self {
117        // Validation should have been done prior (by builder)
118        Self {
119            version,
120            nodes,
121            messages: Messages::from_messages_slice(messages),
122        }
123    }
124
125    fn new_from_options(
126        version: Option<Version<'a>>,
127        nodes: Nodes<'a>,
128        messages: &[Option<Message<'a>>],
129        message_count: usize,
130    ) -> Self {
131        // Validation should have been done prior (by parse)
132        Self {
133            version,
134            nodes,
135            messages: Messages::from_options_slice(messages, message_count),
136        }
137    }
138
139    /// Get the version of the DBC file
140    ///
141    /// # Examples
142    ///
143    /// ```rust,no_run
144    /// use dbc_rs::Dbc;
145    ///
146    /// let dbc = Dbc::parse("VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM")?;
147    /// if let Some(version) = dbc.version() {
148    ///     // Version is available
149    ///     let _ = version.as_str();
150    /// }
151    /// # Ok::<(), dbc_rs::Error>(())
152    /// ```
153    #[inline]
154    #[must_use]
155    pub fn version(&self) -> Option<&Version<'a>> {
156        self.version.as_ref()
157    }
158
159    /// Get a reference to the nodes collection
160    ///
161    /// # Examples
162    ///
163    /// ```rust,no_run
164    /// use dbc_rs::Dbc;
165    ///
166    /// let dbc = Dbc::parse("VERSION \"1.0\"\n\nBU_: ECM TCM\n\nBO_ 256 Engine : 8 ECM")?;
167    /// let nodes = dbc.nodes();
168    /// assert_eq!(nodes.len(), 2);
169    /// // Iterate over nodes
170    /// let mut iter = nodes.iter();
171    /// assert_eq!(iter.next(), Some("ECM"));
172    /// assert_eq!(iter.next(), Some("TCM"));
173    /// assert_eq!(iter.next(), None);
174    /// # Ok::<(), dbc_rs::Error>(())
175    /// ```
176    #[inline]
177    #[must_use]
178    pub fn nodes(&self) -> &Nodes<'a> {
179        &self.nodes
180    }
181
182    /// Get a reference to the messages collection
183    ///
184    /// # Examples
185    ///
186    /// ```rust,no_run
187    /// use dbc_rs::Dbc;
188    ///
189    /// let dbc = Dbc::parse("VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM")?;
190    /// let messages = dbc.messages();
191    /// assert_eq!(messages.len(), 1);
192    /// let message = messages.at(0).unwrap();
193    /// assert_eq!(message.name(), "Engine");
194    /// assert_eq!(message.id(), 256);
195    /// # Ok::<(), dbc_rs::Error>(())
196    /// ```
197    #[inline]
198    #[must_use]
199    pub fn messages(&self) -> &Messages<'a> {
200        &self.messages
201    }
202
203    /// Parse a DBC file from a string slice
204    ///
205    /// # Examples
206    ///
207    /// ```rust,no_run
208    /// use dbc_rs::Dbc;
209    ///
210    /// let dbc_content = r#"VERSION "1.0"
211    ///
212    /// BU_: ECM
213    ///
214    /// BO_ 256 EngineData : 8 ECM
215    ///  SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm""#;
216    ///
217    /// let dbc = Dbc::parse(dbc_content)?;
218    /// assert_eq!(dbc.messages().len(), 1);
219    /// # Ok::<(), dbc_rs::Error>(())
220    /// ```
221    pub fn parse(data: &'a str) -> ParseResult<Self> {
222        Self::parse_with_options(data, ParseOptions::default())
223    }
224
225    /// Parses a DBC file from a string with custom parsing options.
226    ///
227    /// # Arguments
228    ///
229    /// * `data` - The DBC file content as a string
230    /// * `options` - Parsing options to control validation behavior
231    ///
232    /// # Examples
233    ///
234    /// ```rust,no_run
235    /// use dbc_rs::{Dbc, ParseOptions};
236    ///
237    /// let dbc_content = r#"VERSION "1.0"
238    ///
239    /// BU_: ECM
240    ///
241    /// BO_ 256 Test : 8 ECM
242    ///  SG_ Signal1 : 0|8@1+ (1,0) [0|255] ""
243    /// "#;
244    ///
245    /// // Use lenient mode to allow signals that extend beyond message boundaries
246    /// let options = ParseOptions::lenient();
247    /// let dbc = Dbc::parse_with_options(dbc_content, options)?;
248    /// # Ok::<(), dbc_rs::Error>(())
249    /// ```
250    pub fn parse_with_options(data: &'a str, options: ParseOptions) -> ParseResult<Self> {
251        // FIRST PASS: Count messages (two-pass parsing to allocate correct sizes)
252        let mut parser1 = Parser::new(data.as_bytes())?;
253        let _ = Messages::count_messages_and_signals(&mut parser1)?;
254
255        // SECOND PASS: Parse into messages array
256        let mut parser2 = Parser::new(data.as_bytes())?;
257
258        // Allocate messages buffer - Messages will handle the size internally
259        // We use a temporary buffer that Messages can work with (no alloc in no_std)
260        // Messages handles capacity internally, we just need a buffer
261        #[cfg(not(any(feature = "alloc", feature = "kernel")))]
262        let mut messages_buffer = Messages::new_parse_buffer();
263
264        #[cfg(any(feature = "alloc", feature = "kernel"))]
265        let mut messages_buffer: alloc::vec::Vec<Option<Message<'a>>> = {
266            use crate::compat::vec_with_capacity;
267            vec_with_capacity(Messages::max_capacity())
268        };
269
270        let mut message_count_actual = 0;
271
272        // Parse version, nodes, and messages
273        use crate::{
274            BA_, BA_DEF_, BA_DEF_DEF_, BO_, BO_TX_BU_, BS_, BU_, CM_, EV_, NS_, SG_, SIG_GROUP_,
275            SIG_VALTYPE_, VAL_, VAL_TABLE_, VERSION,
276        };
277
278        let mut version: Option<Version<'a>> = None;
279        let mut nodes: Option<Nodes<'a>> = None;
280
281        loop {
282            // Skip comments (lines starting with //)
283            parser2.skip_newlines_and_spaces();
284            if parser2.starts_with(b"//") {
285                parser2.skip_to_end_of_line();
286                continue;
287            }
288
289            let keyword_result = parser2.find_next_keyword();
290            let keyword = match keyword_result {
291                Ok(kw) => kw,
292                Err(ParseError::UnexpectedEof) => break,
293                Err(ParseError::Expected(_)) => {
294                    if parser2.starts_with(b"//") {
295                        parser2.skip_to_end_of_line();
296                        continue;
297                    }
298                    return Err(keyword_result.unwrap_err());
299                }
300                Err(e) => return Err(e),
301            };
302
303            match keyword {
304                NS_ => {
305                    parser2.skip_newlines_and_spaces();
306                    let _ = parser2.expect(b":").ok();
307                    loop {
308                        parser2.skip_newlines_and_spaces();
309                        if parser2.is_empty() {
310                            break;
311                        }
312                        if parser2.starts_with(b" ") || parser2.starts_with(b"\t") {
313                            parser2.skip_to_end_of_line();
314                            continue;
315                        }
316                        if parser2.starts_with(b"//") {
317                            parser2.skip_to_end_of_line();
318                            continue;
319                        }
320                        if parser2.starts_with(BS_.as_bytes())
321                            || parser2.starts_with(BU_.as_bytes())
322                            || parser2.starts_with(BO_.as_bytes())
323                            || parser2.starts_with(SG_.as_bytes())
324                            || parser2.starts_with(VERSION.as_bytes())
325                        {
326                            break;
327                        }
328                        parser2.skip_to_end_of_line();
329                    }
330                    continue;
331                }
332                CM_ | BS_ | VAL_TABLE_ | BA_DEF_ | BA_DEF_DEF_ | BA_ | VAL_ | SIG_GROUP_
333                | SIG_VALTYPE_ | EV_ | BO_TX_BU_ => {
334                    parser2.skip_to_end_of_line();
335                    continue;
336                }
337                VERSION => {
338                    version = Some(Version::parse(&mut parser2)?);
339                    continue;
340                }
341                BU_ => {
342                    nodes = Some(Nodes::parse(&mut parser2)?);
343                    continue;
344                }
345                BO_ => {
346                    // Check limit using Messages (which knows about the capacity)
347                    if message_count_actual >= Messages::max_capacity() {
348                        return Err(ParseError::Version(crate::error::lang::NODES_TOO_MANY));
349                    }
350
351                    // Save parser position (after BO_ keyword, before message header)
352                    let message_start_pos = parser2.pos();
353
354                    // Parse message header to get past it, then parse signals
355                    parser2.skip_newlines_and_spaces();
356                    let _id = parser2.parse_u32().ok();
357                    parser2.skip_newlines_and_spaces();
358                    let _name = parser2.parse_identifier().ok();
359                    parser2.skip_newlines_and_spaces();
360                    let _ = parser2.expect(b":").ok();
361                    parser2.skip_newlines_and_spaces();
362                    let _dlc = parser2.parse_u8().ok();
363                    parser2.skip_newlines_and_spaces();
364                    let _sender = parser2.parse_identifier().ok();
365                    let message_header_end_pos = parser2.pos();
366                    parser2.skip_to_end_of_line();
367
368                    // Parse signals into fixed array
369                    #[cfg(not(any(feature = "alloc", feature = "kernel")))]
370                    let mut signals_array = Signals::new_parse_buffer();
371
372                    #[cfg(any(feature = "alloc", feature = "kernel"))]
373                    let mut signals_array: alloc::vec::Vec<Option<Signal<'a>>> = {
374                        use crate::compat::vec_with_capacity;
375                        vec_with_capacity(Signals::max_capacity())
376                    };
377
378                    let mut signal_count = 0;
379                    loop {
380                        parser2.skip_newlines_and_spaces();
381                        if parser2.starts_with(crate::SG_.as_bytes()) {
382                            if let Some(next_byte) = parser2.peek_byte_at(3) {
383                                if matches!(next_byte, b' ' | b'\n' | b'\r' | b'\t') {
384                                    if signal_count >= Signals::max_capacity() {
385                                        return Err(ParseError::Version(
386                                            crate::error::messages::SIGNAL_RECEIVERS_TOO_MANY,
387                                        ));
388                                    }
389                                    let _kw = parser2.find_next_keyword().map_err(|e| match e {
390                                        ParseError::Expected(_) => {
391                                            ParseError::Expected("Expected SG_ keyword")
392                                        }
393                                        _ => e,
394                                    })?;
395                                    let signal = Signal::parse(&mut parser2)?;
396                                    #[cfg(not(any(feature = "alloc", feature = "kernel")))]
397                                    {
398                                        signals_array[signal_count] = Some(signal);
399                                    }
400                                    #[cfg(any(feature = "alloc", feature = "kernel"))]
401                                    {
402                                        signals_array.push(Some(signal));
403                                    }
404                                    signal_count += 1;
405                                    continue;
406                                }
407                            }
408                        }
409                        break;
410                    }
411
412                    // Restore parser to start of message line and use Message::parse
413                    // Create a new parser from the original input, but only up to the end of the header
414                    // (not including signals, so Message::parse doesn't complain about extra content)
415                    let message_input = &data.as_bytes()[message_start_pos..message_header_end_pos];
416                    let mut message_parser = Parser::new(message_input)?;
417
418                    // Use Message::parse which will parse the header and use our signals
419                    let signals_slice: &[Option<Signal<'a>>] = {
420                        #[cfg(not(any(feature = "alloc", feature = "kernel")))]
421                        {
422                            &signals_array[..signal_count]
423                        }
424                        #[cfg(any(feature = "alloc", feature = "kernel"))]
425                        {
426                            &signals_array[..]
427                        }
428                    };
429                    let message =
430                        Message::parse(&mut message_parser, signals_slice, signal_count, options)?;
431
432                    #[cfg(not(any(feature = "alloc", feature = "kernel")))]
433                    {
434                        messages_buffer[message_count_actual] = Some(message);
435                    }
436                    #[cfg(any(feature = "alloc", feature = "kernel"))]
437                    {
438                        messages_buffer.push(Some(message));
439                    }
440                    message_count_actual += 1;
441                    continue;
442                }
443                SG_ => {
444                    let _ = Signal::parse(&mut parser2)?;
445                    continue;
446                }
447                _ => {
448                    parser2.skip_to_end_of_line();
449                    continue;
450                }
451            }
452        }
453
454        // Allow empty nodes (DBC spec allows empty BU_: line)
455        let nodes = nodes.unwrap_or_default();
456
457        // If no version was parsed, default to empty version
458        let version = version.or_else(|| {
459            static EMPTY_VERSION: &[u8] = b"VERSION \"\"";
460            let mut parser = Parser::new(EMPTY_VERSION).ok()?;
461            Version::parse(&mut parser).ok()
462        });
463
464        // Convert messages buffer to slice for validation and construction
465        let messages_slice: &[Option<Message<'a>>] = {
466            #[cfg(not(any(feature = "alloc", feature = "kernel")))]
467            {
468                &messages_buffer[..message_count_actual]
469            }
470            #[cfg(any(feature = "alloc", feature = "kernel"))]
471            {
472                &messages_buffer[..]
473            }
474        };
475
476        // Validate messages (duplicate IDs, sender in nodes, etc.)
477        Self::validate(
478            version.as_ref(),
479            &nodes,
480            messages_slice,
481            message_count_actual,
482        )?;
483
484        // Construct directly (validation already done)
485        Ok(Self::new_from_options(
486            version,
487            nodes,
488            messages_slice,
489            message_count_actual,
490        ))
491    }
492
493    /// Parse a DBC file from a byte slice
494    ///
495    /// # Examples
496    ///
497    /// ```rust,no_run
498    /// use dbc_rs::Dbc;
499    ///
500    /// let dbc_bytes = b"VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM";
501    /// let dbc = Dbc::parse_bytes(dbc_bytes)?;
502    /// println!("Parsed {} messages", dbc.messages().len());
503    /// # Ok::<(), dbc_rs::Error>(())
504    /// ```
505    #[cfg(feature = "alloc")]
506    pub fn parse_bytes(data: &[u8]) -> Result<Dbc<'static>> {
507        let content =
508            core::str::from_utf8(data).map_err(|e| Error::Dbc(error_messages::invalid_utf8(e)))?;
509        // Convert to owned string, box it, and leak to get 'static lifetime
510        use alloc::boxed::Box;
511        let owned = String::from(content);
512        let boxed = owned.into_boxed_str();
513        let content_ref: &'static str = Box::leak(boxed);
514        Dbc::parse(content_ref).map_err(Error::ParseError)
515    }
516
517    /// Parse a DBC file from a file path
518    ///
519    /// # Examples
520    ///
521    /// ```rust,no_run
522    /// use dbc_rs::Dbc;
523    ///
524    /// // Create a temporary file for the example
525    /// let dbc_content = r#"VERSION "1.0"
526    ///
527    /// BU_: ECM
528    ///
529    /// BO_ 256 Engine : 8 ECM
530    ///  SG_ Signal1 : 0|8@1+ (1,0) [0|255] ""
531    /// "#;
532    /// std::fs::write("/tmp/example.dbc", dbc_content)?;
533    ///
534    /// let dbc = Dbc::from_file("/tmp/example.dbc")?;
535    /// println!("Parsed {} messages", dbc.messages().len());
536    /// # Ok::<(), Box<dyn std::error::Error>>(())
537    /// ```
538    #[cfg(feature = "std")]
539    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Dbc<'static>> {
540        let file =
541            std::fs::File::open(path).map_err(|e| Error::Dbc(error_messages::read_failed(e)))?;
542        Self::from_reader(file)
543    }
544
545    /// Parse a DBC file from a reader
546    ///
547    /// # Examples
548    ///
549    /// ```rust,no_run
550    /// use dbc_rs::Dbc;
551    /// use std::io::Cursor;
552    ///
553    /// let data = b"VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM";
554    /// let reader = Cursor::new(data);
555    /// let dbc = Dbc::from_reader(reader)?;
556    /// println!("Parsed {} messages", dbc.messages().len());
557    /// # Ok::<(), dbc_rs::Error>(())
558    /// ```
559    #[cfg(feature = "std")]
560    pub fn from_reader<R: std::io::Read>(mut reader: R) -> Result<Dbc<'static>> {
561        let mut buffer = String::new();
562        std::io::Read::read_to_string(&mut reader, &mut buffer)
563            .map_err(|e| Error::Dbc(error_messages::read_failed(e)))?;
564        // Convert to boxed str and leak to get 'static lifetime
565        // The leaked memory will live for the duration of the program
566        use alloc::boxed::Box;
567        let boxed = buffer.into_boxed_str();
568        let content_ref: &'static str = Box::leak(boxed);
569        Dbc::parse(content_ref).map_err(Error::ParseError)
570    }
571
572    /// Serialize this DBC to a DBC format string
573    ///
574    /// # Examples
575    ///
576    /// ```rust,no_run
577    /// use dbc_rs::Dbc;
578    ///
579    /// let dbc = Dbc::parse("VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM")?;
580    /// let dbc_string = dbc.to_dbc_string();
581    /// // The string can be written to a file or used elsewhere
582    /// assert!(dbc_string.contains("VERSION"));
583    /// # Ok::<(), dbc_rs::Error>(())
584    /// ```
585    #[cfg(feature = "alloc")]
586    #[must_use]
587    pub fn to_dbc_string(&self) -> alloc::string::String {
588        use alloc::string::String;
589        // Pre-allocate with estimated capacity
590        // Estimate: ~50 chars per message + ~100 chars per signal
591        let signal_count: usize = self.messages().iter().map(|m| m.signals().len()).sum();
592        let estimated_capacity = 200 + (self.messages.len() * 50) + (signal_count * 100);
593        let mut result = String::with_capacity(estimated_capacity);
594
595        // VERSION line
596        if let Some(version) = &self.version {
597            result.push_str(&version.to_dbc_string());
598            result.push_str("\n\n");
599        }
600
601        // BU_ line
602        result.push_str(&self.nodes.to_dbc_string());
603        result.push('\n');
604
605        // BO_ and SG_ lines for each message
606        for message in self.messages().iter() {
607            result.push('\n');
608            result.push_str(&message.to_dbc_string_with_signals());
609        }
610
611        result
612    }
613}
614
615#[cfg(feature = "alloc")]
616impl<'a> core::fmt::Display for Dbc<'a> {
617    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
618        write!(f, "{}", self.to_dbc_string())
619    }
620}
621
622#[cfg(all(test, feature = "std"))]
623mod tests {
624    #![allow(clippy::float_cmp)]
625    use super::*;
626    use crate::{
627        Error,
628        error::{ParseError, lang},
629    };
630
631    #[test]
632    fn parses_real_dbc() {
633        let data = r#"VERSION "1.0"
634
635BU_: ECM TCM
636
637BO_ 256 Engine : 8 ECM
638 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
639 SG_ Temp : 16|8@0- (1,-40) [-40|215] "°C"
640
641BO_ 512 Brake : 4 TCM
642 SG_ Pressure : 0|16@1+ (0.1,0) [0|1000] "bar""#;
643
644        let dbc = Dbc::parse(data).unwrap();
645        assert_eq!(dbc.messages().len(), 2);
646        let mut messages_iter = dbc.messages().iter();
647        let msg0 = messages_iter.next().unwrap();
648        assert_eq!(msg0.signals().len(), 2);
649        let mut signals_iter = msg0.signals().iter();
650        assert_eq!(signals_iter.next().unwrap().name(), "RPM");
651        assert_eq!(signals_iter.next().unwrap().name(), "Temp");
652        let msg1 = messages_iter.next().unwrap();
653        assert_eq!(msg1.signals().len(), 1);
654        assert_eq!(msg1.signals().iter().next().unwrap().name(), "Pressure");
655    }
656
657    #[test]
658    fn test_parse_duplicate_message_id() {
659        // Test that parse also validates duplicate message IDs
660        let data = r#"VERSION "1.0"
661
662BU_: ECM
663
664BO_ 256 EngineData1 : 8 ECM
665 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
666
667BO_ 256 EngineData2 : 8 ECM
668 SG_ Temp : 16|8@0- (1,-40) [-40|215] "°C"
669"#;
670
671        let result = Dbc::parse(data);
672        assert!(result.is_err());
673        match result.unwrap_err() {
674            ParseError::Version(msg) => {
675                // Check for format template text (language-agnostic) - extract text before first placeholder
676                let template_text = lang::FORMAT_DUPLICATE_MESSAGE_ID.split("{}").next().unwrap();
677                assert!(msg.contains(template_text.trim_end_matches(':').trim_end()));
678            }
679            _ => panic!("Expected ParseError::Version"),
680        }
681    }
682
683    #[test]
684    fn test_parse_sender_not_in_nodes() {
685        // Test that parse also validates message senders are in nodes list
686        let data = r#"VERSION "1.0"
687
688BU_: ECM
689
690BO_ 256 EngineData : 8 TCM
691 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
692"#;
693
694        let result = Dbc::parse(data);
695        assert!(result.is_err());
696        match result.unwrap_err() {
697            ParseError::Version(msg) => {
698                // Check for format template text (language-agnostic) - extract text before first placeholder
699                let template_text = lang::FORMAT_SENDER_NOT_IN_NODES.split("{}").next().unwrap();
700                assert!(msg.contains(template_text.trim_end()));
701            }
702            _ => panic!("Expected ParseError::Version"),
703        }
704    }
705
706    #[test]
707    fn test_parse_empty_file() {
708        // Test parsing an empty file
709        let result = Dbc::parse("");
710        assert!(result.is_err());
711        match result.unwrap_err() {
712            ParseError::UnexpectedEof => {
713                // Empty file should result in unexpected EOF
714            }
715            _ => panic!("Expected ParseError::UnexpectedEof"),
716        }
717    }
718
719    #[test]
720    fn test_parse_missing_nodes() {
721        // Test parsing without BU_ statement
722        // Note: The parser allows missing BU_ line and treats it as empty nodes
723        // This is consistent with allowing empty nodes per DBC spec
724        let data = r#"VERSION "1.0"
725
726BO_ 256 EngineData : 8 ECM
727 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
728"#;
729
730        let result = Dbc::parse(data);
731        // Parser should succeed with empty nodes (missing BU_ is treated as empty nodes)
732        assert!(result.is_ok());
733        let dbc = result.unwrap();
734        assert!(dbc.nodes().is_empty());
735    }
736
737    #[test]
738    fn test_parse_bytes() {
739        let data = r#"VERSION "1.0"
740
741BU_: ECM
742
743BO_ 256 Engine : 8 ECM
744 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
745"#;
746
747        let bytes = data.as_bytes();
748        let dbc = Dbc::parse_bytes(bytes).unwrap();
749        assert_eq!(
750            dbc.version().map(|v| v.to_string()),
751            Some("1.0".to_string())
752        );
753        assert_eq!(dbc.messages().len(), 1);
754    }
755
756    #[test]
757    #[cfg(feature = "alloc")]
758    fn test_parse_from_string() {
759        let data = String::from(
760            r#"VERSION "1.0"
761
762BU_: ECM
763
764BO_ 256 Engine : 8 ECM
765 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
766"#,
767        );
768
769        let dbc = Dbc::parse(&data).unwrap();
770        assert_eq!(
771            dbc.version().map(|v| v.to_string()),
772            Some("1.0".to_string())
773        );
774        assert_eq!(dbc.messages().len(), 1);
775    }
776
777    #[test]
778    fn test_parse_bytes_invalid_utf8() {
779        // Invalid UTF-8 sequence
780        let invalid_bytes = &[0xFF, 0xFE, 0xFD];
781        let result = Dbc::parse_bytes(invalid_bytes);
782        assert!(result.is_err());
783        match result.unwrap_err() {
784            Error::Dbc(msg) => {
785                // Check for format template text (language-agnostic) - extract text before first placeholder
786                let template_text = lang::FORMAT_INVALID_UTF8.split("{}").next().unwrap();
787                assert!(msg.contains(template_text.trim_end_matches(':').trim_end()));
788            }
789            _ => panic!("Expected Dbc error"),
790        }
791    }
792
793    #[test]
794    #[cfg(feature = "alloc")]
795    fn test_save_basic() {
796        // Use parsing instead of builders
797        let dbc_content = r#"VERSION "1.0"
798
799BU_: ECM
800
801BO_ 256 EngineData : 8 ECM
802 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm" *
803"#;
804        let dbc = Dbc::parse(dbc_content).unwrap();
805
806        let saved = dbc.to_dbc_string();
807        assert!(saved.contains("VERSION \"1.0\""));
808        assert!(saved.contains("BU_: ECM"));
809        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
810        assert!(saved.contains("SG_ RPM : 0|16@0+ (0.25,0) [0|8000] \"rpm\" *")); // BigEndian = @0
811    }
812
813    #[test]
814    fn test_save_round_trip() {
815        let original = r#"VERSION "1.0"
816
817BU_: ECM TCM
818
819BO_ 256 EngineData : 8 ECM
820 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
821 SG_ Temperature : 16|8@0- (1,-40) [-40|215] "°C" TCM
822
823BO_ 512 BrakeData : 4 TCM
824 SG_ Pressure : 0|16@0+ (0.1,0) [0|1000] "bar"
825"#;
826
827        let dbc = Dbc::parse(original).unwrap();
828        let saved = dbc.to_dbc_string();
829        let dbc2 = Dbc::parse(&saved).unwrap();
830
831        // Verify round-trip: parsed data should match
832        assert_eq!(
833            dbc.version().map(|v| v.to_string()),
834            dbc2.version().map(|v| v.to_string())
835        );
836        assert_eq!(dbc.messages().len(), dbc2.messages().len());
837
838        for (msg1, msg2) in dbc.messages().iter().zip(dbc2.messages().iter()) {
839            assert_eq!(msg1.id(), msg2.id());
840            assert_eq!(msg1.name(), msg2.name());
841            assert_eq!(msg1.dlc(), msg2.dlc());
842            assert_eq!(msg1.sender(), msg2.sender());
843            assert_eq!(msg1.signals().len(), msg2.signals().len());
844
845            for (sig1, sig2) in msg1.signals().iter().zip(msg2.signals().iter()) {
846                assert_eq!(sig1.name(), sig2.name());
847                assert_eq!(sig1.start_bit(), sig2.start_bit());
848                assert_eq!(sig1.length(), sig2.length());
849                assert_eq!(sig1.byte_order(), sig2.byte_order());
850                assert_eq!(sig1.is_unsigned(), sig2.is_unsigned());
851                assert_eq!(sig1.factor(), sig2.factor());
852                assert_eq!(sig1.offset(), sig2.offset());
853                assert_eq!(sig1.min(), sig2.min());
854                assert_eq!(sig1.max(), sig2.max());
855                assert_eq!(sig1.unit(), sig2.unit());
856                assert_eq!(sig1.receivers(), sig2.receivers());
857            }
858        }
859    }
860
861    #[test]
862    #[cfg(feature = "alloc")]
863    fn test_save_multiple_messages() {
864        // Use parsing instead of builders
865        let dbc_content = r#"VERSION "1.0"
866
867BU_: ECM TCM
868
869BO_ 256 EngineData : 8 ECM
870 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm" *
871
872BO_ 512 BrakeData : 4 TCM
873 SG_ Pressure : 0|16@1+ (0.1,0) [0|1000] "bar"
874"#;
875        let dbc = Dbc::parse(dbc_content).unwrap();
876        let saved = dbc.to_dbc_string();
877
878        // Verify both messages are present
879        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
880        assert!(saved.contains("BO_ 512 BrakeData : 4 TCM"));
881        assert!(saved.contains("SG_ RPM"));
882        assert!(saved.contains("SG_ Pressure"));
883    }
884
885    // Note: Builder limit tests have been moved to dbc_builder.rs
886    // These tests require building many messages programmatically, which is builder functionality
887
888    #[test]
889    fn test_parse_without_version() {
890        // DBC file without VERSION line should default to empty version
891        let data = r#"
892BU_: ECM
893
894BO_ 256 Engine : 8 ECM
895 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
896"#;
897        let dbc = Dbc::parse(data).unwrap();
898        assert_eq!(dbc.version().map(|v| v.to_string()), Some("".to_string()));
899    }
900
901    #[test]
902    fn test_parse_without_version_with_comment() {
903        // DBC file with comment and no VERSION line
904        let data = r#"// This is a comment
905BU_: ECM
906
907BO_ 256 Engine : 8 ECM
908 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
909"#;
910        let dbc = Dbc::parse(data).unwrap();
911        assert_eq!(dbc.version().map(|v| v.to_string()), Some("".to_string()));
912    }
913
914    #[test]
915    fn test_parse_error_with_line_number() {
916        // Test that errors include line numbers (or at least that errors are returned)
917        let data = r#"VERSION "1.0"
918
919BU_: ECM
920
921BO_ 256 Engine : 8 ECM
922 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
923BO_ 257 Invalid : 8 ECM
924 SG_ InvalidSignal : invalid|16@1+ (0.25,0) [0|8000] "rpm"
925"#;
926        let result = Dbc::parse(data);
927        assert!(result.is_err());
928        let err = result.unwrap_err();
929        // Accept any ParseError - line number tracking is not yet implemented
930        match err {
931            ParseError::Version(_)
932            | ParseError::UnexpectedEof
933            | ParseError::Expected(_)
934            | ParseError::InvalidChar(_) => {
935                // Accept various parse errors
936            }
937            _ => panic!("Expected ParseError"),
938        };
939        // Note: Line number tracking is not yet implemented, so we just verify an error is returned
940    }
941
942    #[test]
943    fn test_parse_error_version_with_line_number() {
944        // Test that version parsing errors are returned (line number tracking not yet implemented)
945        let data = r#"VERSION invalid
946
947BU_: ECM
948"#;
949        let result = Dbc::parse(data);
950        assert!(result.is_err());
951        let err = result.unwrap_err();
952        // Accept any ParseError - line number tracking is not yet implemented
953        match err {
954            ParseError::Version(_) | ParseError::UnexpectedEof | ParseError::Expected(_) => {
955                // Accept various parse errors
956            }
957            _ => panic!("Expected ParseError"),
958        };
959        // Note: Line number tracking is not yet implemented, so we just verify an error is returned
960    }
961
962    #[test]
963    fn test_parse_with_lenient_boundary_check() {
964        // Test that lenient mode allows signals that extend beyond message boundaries
965        let data = r#"VERSION "1.0"
966
967BU_: ECM
968
969BO_ 256 Test : 8 ECM
970 SG_ CHECKSUM : 63|8@1+ (1,0) [0|255] ""
971"#;
972
973        // Strict mode should fail
974        let result = Dbc::parse(data);
975        assert!(result.is_err());
976
977        // Lenient mode should succeed
978        let options = ParseOptions::lenient();
979        let dbc = Dbc::parse_with_options(data, options).unwrap();
980        assert_eq!(dbc.messages().len(), 1);
981        let message = dbc.messages().at(0).unwrap();
982        assert_eq!(message.signals().len(), 1);
983        assert_eq!(message.signals().at(0).unwrap().name(), "CHECKSUM");
984    }
985
986    #[test]
987    fn test_parse_with_strict_boundary_check() {
988        // Test that strict mode (default) rejects signals that extend beyond boundaries
989        let data = r#"VERSION "1.0"
990
991BU_: ECM
992
993BO_ 256 Test : 8 ECM
994 SG_ CHECKSUM : 63|8@1+ (1,0) [0|255] ""
995"#;
996
997        // Default (strict) mode should fail
998        let result = Dbc::parse(data);
999        assert!(result.is_err());
1000
1001        // Explicit strict mode should also fail
1002        let options = ParseOptions::new();
1003        let result = Dbc::parse_with_options(data, options);
1004        assert!(result.is_err());
1005    }
1006}