dbc_rs/
dbc.rs

1use crate::{Error, Message, Nodes, Signal, Version, error::messages};
2use alloc::{string::String, string::ToString, vec::Vec};
3
4/// Represents a complete DBC (CAN Database) file.
5///
6/// A `Dbc` contains all the information from a DBC file: version information,
7/// node definitions, and CAN messages with their signals.
8///
9/// # Examples
10///
11/// ```rust
12/// use dbc_rs::Dbc;
13///
14/// let content = "VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM";
15/// let dbc = Dbc::parse(content)?;
16/// # Ok::<(), dbc_rs::Error>(())
17/// ```
18#[derive(Debug)]
19pub struct Dbc {
20    version: Version,
21    nodes: Nodes,
22    messages: Vec<Message>,
23}
24
25impl Dbc {
26    /// Validate DBC parameters
27    fn validate(_version: &Version, nodes: &Nodes, messages: &[Message]) -> Result<(), Error> {
28        // Check for duplicate message IDs
29        for (i, msg1) in messages.iter().enumerate() {
30            for msg2 in messages.iter().skip(i + 1) {
31                if msg1.id() == msg2.id() {
32                    return Err(Error::Dbc(messages::duplicate_message_id(
33                        msg1.id(),
34                        msg1.name(),
35                        msg2.name(),
36                    )));
37                }
38            }
39        }
40
41        // Validate that all message senders are in the nodes list
42        for msg in messages {
43            if !nodes.contains(msg.sender()) {
44                return Err(Error::Dbc(messages::sender_not_in_nodes(
45                    msg.name(),
46                    msg.sender(),
47                )));
48            }
49        }
50
51        Ok(())
52    }
53
54    /// Create a new builder for constructing a `Dbc`
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use dbc_rs::{Dbc, Version, Nodes, Message, Signal, ByteOrder, Receivers};
60    ///
61    /// let version = Version::builder().major(1).minor(0).build()?;
62    /// let nodes = Nodes::builder().add_node("ECM").add_node("TCM").build()?;
63    ///
64    /// let signal = Signal::builder()
65    ///     .name("RPM")
66    ///     .start_bit(0)
67    ///     .length(16)
68    ///     .byte_order(ByteOrder::BigEndian)
69    ///     .unsigned(true)
70    ///     .factor(0.25)
71    ///     .offset(0.0)
72    ///     .min(0.0)
73    ///     .max(8000.0)
74    ///     .unit("rpm")
75    ///     .receivers(Receivers::Broadcast)
76    ///     .build()?;
77    ///
78    /// let message = Message::builder()
79    ///     .id(256)
80    ///     .name("EngineData")
81    ///     .dlc(8)
82    ///     .sender("ECM")
83    ///     .add_signal(signal)
84    ///     .build()?;
85    ///
86    /// let dbc = Dbc::builder()
87    ///     .version(version)
88    ///     .nodes(nodes)
89    ///     .add_message(message)
90    ///     .build()?;
91    /// # Ok::<(), dbc_rs::Error>(())
92    /// ```
93    ///
94    /// # See Also
95    ///
96    /// - [`parse`](Self::parse) - Parse from string slice
97    /// - [`Version::builder`](crate::Version::builder) - Create version using builder
98    /// - [`Nodes::builder`](crate::Nodes::builder) - Create nodes using builder
99    /// - [`Message::builder`](crate::Message::builder) - Create message using builder pattern
100    pub fn builder() -> DbcBuilder {
101        DbcBuilder::new()
102    }
103
104    /// This is an internal constructor. For public API usage, use [`Dbc::builder()`] instead.
105    pub(crate) fn new(
106        version: Version,
107        nodes: Nodes,
108        messages: Vec<Message>,
109    ) -> Result<Self, Error> {
110        Self::validate(&version, &nodes, &messages)?;
111
112        Ok(Self {
113            version,
114            nodes,
115            messages,
116        })
117    }
118
119    /// Get the version information
120    #[inline]
121    pub fn version(&self) -> &Version {
122        &self.version
123    }
124
125    /// Get the nodes information
126    #[inline]
127    pub fn nodes(&self) -> &Nodes {
128        &self.nodes
129    }
130
131    /// Get a read-only slice of messages
132    #[inline]
133    pub fn messages(&self) -> &[Message] {
134        &self.messages
135    }
136
137    /// Parse a DBC file from a string slice
138    ///
139    /// This is the core parsing method that works in both `std` and `no_std` environments.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if:
144    /// - The file is empty
145    /// - The version line is missing or invalid
146    /// - Nodes are not defined
147    /// - Any message or signal fails to parse
148    /// - Validation fails (duplicate IDs, invalid senders, etc.)
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use dbc_rs::Dbc;
154    ///
155    /// let content = "VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM\n SG_ RPM : 0|16@1+ (0.25,0) [0|8000] \"rpm\"";
156    /// let dbc = Dbc::parse(content)?;
157    /// # Ok::<(), dbc_rs::Error>(())
158    /// ```
159    ///
160    /// # See Also
161    ///
162    /// - [`parse_bytes`](Self::parse_bytes) - Parse from bytes
163    /// - [`parse_from`](Self::parse_from) - Parse from owned String
164    /// - [`from_reader`](Self::from_reader) - Parse from `std::io::Read` (requires `std` feature)
165    pub fn parse(data: &str) -> Result<Self, Error> {
166        let mut lines = data.lines().peekable();
167
168        // Must start with VERSION statement
169        let version = if let Some(v) = lines.next() {
170            v
171        } else {
172            return Err(Error::Dbc(messages::DBC_EMPTY_FILE.to_string()));
173        };
174        let version = Version::parse(version)?;
175
176        let mut nodes: Option<Nodes> = None;
177        let mut messages: Vec<Message> = Vec::new();
178
179        while let Some(line) = lines.next() {
180            if line.starts_with("BU_") {
181                nodes = Some(Nodes::parse(line)?);
182            } else if line.starts_with("BO_") {
183                let message = line;
184
185                // Get signals associated message
186                // Pre-allocate with estimated capacity (most messages have 1-8 signals)
187                let mut signals: Vec<Signal> = Vec::with_capacity(8);
188                while let Some(signal) = lines.peek() {
189                    let signal = signal.trim_start();
190
191                    if signal.trim_start().starts_with("SG_") {
192                        signals.push(Signal::parse(signal)?);
193                        lines.next();
194                    } else {
195                        break;
196                    }
197                }
198
199                messages.push(Message::parse(message, signals)?);
200            }
201        }
202
203        let nodes = match nodes {
204            Some(val) => val,
205            None => {
206                return Err(Error::Dbc(messages::DBC_NODES_NOT_DEFINED.to_string()));
207            }
208        };
209
210        // Validate the parsed DBC using the same validation as new()
211        Self::validate(&version, &nodes, &messages)?;
212
213        Ok(Self {
214            version,
215            nodes,
216            messages,
217        })
218    }
219
220    /// Parse a DBC file from a byte slice
221    ///
222    /// This method accepts `&[u8]` and converts it to a string for parsing.
223    /// Works in both `std` and `no_std` environments.
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if the bytes are not valid UTF-8.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use dbc_rs::Dbc;
233    ///
234    /// let bytes = b"VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM\n SG_ RPM : 0|16@1+ (0.25,0) [0|8000] \"rpm\"";
235    /// let dbc = Dbc::parse_bytes(bytes)?;
236    /// # Ok::<(), dbc_rs::Error>(())
237    /// ```
238    pub fn parse_bytes(data: &[u8]) -> Result<Self, Error> {
239        let content =
240            core::str::from_utf8(data).map_err(|e| Error::Dbc(messages::invalid_utf8(e)))?;
241        Self::parse(content)
242    }
243
244    /// Parse a DBC file from any type that can be converted to a string slice
245    ///
246    /// This is a convenience method that works with `String`, `&str`, `Box<str>`, etc.
247    /// Works in both `std` and `no_std` environments.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use dbc_rs::Dbc;
253    ///
254    /// let content = String::from("VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM\n SG_ RPM : 0|16@1+ (0.25,0) [0|8000] \"rpm\"");
255    /// let dbc = Dbc::parse_from(content)?;
256    /// # Ok::<(), dbc_rs::Error>(())
257    /// ```
258    ///
259    /// # See Also
260    ///
261    /// - [`parse`](Self::parse) - Parse from string slice
262    /// - [`parse_bytes`](Self::parse_bytes) - Parse from bytes
263    /// - [`from_reader`](Self::from_reader) - Parse from `std::io::Read` (requires `std` feature)
264    pub fn parse_from<S: AsRef<str>>(data: S) -> Result<Self, Error> {
265        Self::parse(data.as_ref())
266    }
267
268    /// Serialize the DBC structure back to DBC file format
269    ///
270    /// This method converts a `Dbc` instance back into a string representation
271    /// that matches the DBC file format. It uses the `to_dbc_string()` methods
272    /// of the individual components (Version, Nodes, Message, Signal) to compose
273    /// the complete DBC file.
274    ///
275    /// Works in both `std` and `no_std` environments.
276    ///
277    /// # Examples
278    ///
279    /// ```
280    /// use dbc_rs::Dbc;
281    ///
282    /// let content = "VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM\n SG_ RPM : 0|16@1+ (0.25,0) [0|8000] \"rpm\"";
283    /// let dbc = Dbc::parse(content)?;
284    /// let saved = dbc.save();
285    /// // The saved content should be equivalent to the original
286    /// # Ok::<(), dbc_rs::Error>(())
287    /// ```
288    ///
289    /// # See Also
290    ///
291    /// - [`parse`](Self::parse) - Parse a DBC file from string
292    /// - [`Version::to_dbc_string`](crate::Version::to_dbc_string) - Serialize version
293    /// - [`Message::to_dbc_string_with_signals`](crate::Message::to_dbc_string_with_signals) - Serialize message
294    pub fn save(&self) -> String {
295        // Pre-allocate with estimated capacity
296        // Estimate: ~50 chars per message + ~100 chars per signal
297        let estimated_capacity = 200
298            + (self.messages.len() * 50)
299            + (self.messages.iter().map(|m| m.signals().len()).sum::<usize>() * 100);
300        let mut result = String::with_capacity(estimated_capacity);
301
302        // VERSION line
303        result.push_str(&self.version.to_dbc_string());
304        result.push_str("\n\n");
305
306        // BU_ line
307        result.push_str(&self.nodes.to_dbc_string());
308        result.push('\n');
309
310        // BO_ and SG_ lines for each message
311        for message in &self.messages {
312            result.push('\n');
313            result.push_str(&message.to_dbc_string_with_signals());
314        }
315
316        result
317    }
318}
319
320#[cfg(feature = "std")]
321impl Dbc {
322    /// Parse a DBC file from any type implementing `std::io::Read`
323    ///
324    /// This method reads from files, network streams, in-memory buffers, or any other
325    /// source that implements `std::io::Read`. Only available when the `std` feature is enabled.
326    ///
327    /// # Errors
328    ///
329    /// Returns an error if:
330    /// - Reading from the source fails
331    /// - The data is not valid UTF-8
332    /// - The DBC file format is invalid
333    ///
334    /// # Examples
335    ///
336    /// ```no_run
337    /// use dbc_rs::Dbc;
338    /// use std::fs::File;
339    ///
340    /// let file = File::open("example.dbc").expect("file not found");
341    /// let dbc = Dbc::from_reader(file).expect("failed to parse");
342    /// ```
343    ///
344    /// Reading from a buffer:
345    ///
346    /// ```
347    /// use dbc_rs::Dbc;
348    /// use std::io::Cursor;
349    ///
350    /// let data = b"VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM\n SG_ RPM : 0|16@1+ (0.25,0) [0|8000] \"rpm\"";
351    /// let cursor = Cursor::new(data);
352    /// let dbc = Dbc::from_reader(cursor)?;
353    /// # Ok::<(), dbc_rs::Error>(())
354    /// ```
355    ///
356    /// # See Also
357    ///
358    /// - [`parse`](Self::parse) - Parse from string slice (works in `no_std`)
359    /// - [`parse_bytes`](Self::parse_bytes) - Parse from bytes (works in `no_std`)
360    /// - [`parse_from`](Self::parse_from) - Parse from owned string types (works in `no_std`)
361    pub fn from_reader<R: std::io::Read>(mut reader: R) -> Result<Self, Error> {
362        use alloc::string::String;
363
364        let mut buffer = String::new();
365        std::io::Read::read_to_string(&mut reader, &mut buffer)
366            .map_err(|e| Error::Dbc(messages::read_failed(e)))?;
367        Self::parse(&buffer)
368    }
369}
370
371/// Builder for constructing a `Dbc` with a fluent API
372///
373/// This builder provides a more ergonomic way to construct `Dbc` instances,
374/// especially when building DBC files with multiple messages.
375///
376/// # Examples
377///
378/// ```
379/// use dbc_rs::{Dbc, Version, Nodes, Message, Signal, ByteOrder, Receivers};
380///
381/// let version = Version::builder().major(1).minor(0).build()?;
382/// let nodes = Nodes::builder().add_node("ECM").add_node("TCM").build()?;
383///
384/// let signal = Signal::builder()
385///     .name("RPM")
386///     .start_bit(0)
387///     .length(16)
388///     .build()?;
389///
390/// let message = Message::builder()
391///     .id(256)
392///     .name("EngineData")
393///     .dlc(8)
394///     .sender("ECM")
395///     .add_signal(signal)
396///     .build()?;
397///
398/// let dbc = Dbc::builder()
399///     .version(version)
400///     .nodes(nodes)
401///     .add_message(message)
402///     .build()?;
403/// # Ok::<(), dbc_rs::Error>(())
404/// ```
405#[derive(Debug)]
406pub struct DbcBuilder {
407    version: Option<Version>,
408    nodes: Option<Nodes>,
409    messages: Vec<Message>,
410}
411
412impl DbcBuilder {
413    fn new() -> Self {
414        Self {
415            version: None,
416            nodes: None,
417            messages: Vec::new(),
418        }
419    }
420
421    /// Set the version (required)
422    pub fn version(mut self, version: Version) -> Self {
423        self.version = Some(version);
424        self
425    }
426
427    /// Set the nodes (required)
428    pub fn nodes(mut self, nodes: Nodes) -> Self {
429        self.nodes = Some(nodes);
430        self
431    }
432
433    /// Add a message to the DBC
434    pub fn add_message(mut self, message: Message) -> Self {
435        self.messages.push(message);
436        self
437    }
438
439    /// Add multiple messages to the DBC
440    pub fn add_messages(mut self, messages: impl IntoIterator<Item = Message>) -> Self {
441        self.messages.extend(messages);
442        self
443    }
444
445    /// Set all messages at once (replaces any existing messages)
446    pub fn messages(mut self, messages: Vec<Message>) -> Self {
447        self.messages = messages;
448        self
449    }
450
451    /// Clear all messages
452    pub fn clear_messages(mut self) -> Self {
453        self.messages.clear();
454        self
455    }
456
457    /// Validate the current builder state
458    ///
459    /// This method performs the same validation as `Dbc::validate()` but on the
460    /// builder's current state. Useful for checking validity before calling `build()`.
461    ///
462    /// # Errors
463    ///
464    /// Returns an error if:
465    /// - Required fields (`version`, `nodes`) are missing
466    /// - Validation fails (same as `Dbc::validate()`)
467    pub fn validate(&self) -> Result<(), Error> {
468        let version = self
469            .version
470            .as_ref()
471            .ok_or_else(|| Error::Dbc(messages::DBC_VERSION_REQUIRED.to_string()))?;
472        let nodes = self
473            .nodes
474            .as_ref()
475            .ok_or_else(|| Error::Dbc(messages::DBC_NODES_REQUIRED.to_string()))?;
476
477        Dbc::validate(version, nodes, &self.messages)
478    }
479
480    /// Build the `Dbc` from the builder
481    ///
482    /// # Errors
483    ///
484    /// Returns an error if:
485    /// - Required fields (`version`, `nodes`) are missing
486    /// - Validation fails (same validation logic as the internal constructor)
487    pub fn build(self) -> Result<Dbc, Error> {
488        let version = self
489            .version
490            .ok_or_else(|| Error::Dbc(messages::DBC_VERSION_REQUIRED.to_string()))?;
491        let nodes =
492            self.nodes.ok_or_else(|| Error::Dbc(messages::DBC_NODES_REQUIRED.to_string()))?;
493
494        Dbc::new(version, nodes, self.messages)
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::Dbc;
501    use crate::error::lang;
502    use crate::{ByteOrder, Error, Message, Nodes, Receivers, Signal, Version};
503
504    #[test]
505    fn test_dbc_new_valid() {
506        let version = Version::new(1, Some(0), None).unwrap();
507        let nodes = Nodes::new(["ECM", "TCM"]).unwrap();
508
509        let signal1 = Signal::new(
510            "RPM",
511            0,
512            16,
513            ByteOrder::BigEndian,
514            true,
515            0.25,
516            0.0,
517            0.0,
518            8000.0,
519            Some("rpm" as &str),
520            Receivers::Broadcast,
521        )
522        .unwrap();
523
524        let signal2 = Signal::new(
525            "Temperature",
526            16,
527            8,
528            ByteOrder::BigEndian,
529            true,
530            1.0,
531            -40.0,
532            -40.0,
533            215.0,
534            Some("°C" as &str),
535            Receivers::Broadcast,
536        )
537        .unwrap();
538
539        let message1 = Message::new(256, "EngineData", 8, "ECM", vec![signal1, signal2]).unwrap();
540        let message2 = Message::new(512, "BrakeData", 4, "TCM", vec![]).unwrap();
541
542        let dbc = Dbc::new(version, nodes, vec![message1, message2]).unwrap();
543        assert_eq!(dbc.messages().len(), 2);
544        assert_eq!(dbc.messages()[0].id(), 256);
545        assert_eq!(dbc.messages()[1].id(), 512);
546    }
547
548    #[test]
549    fn test_dbc_new_duplicate_message_id() {
550        let version = Version::new(1, Some(0), None).unwrap();
551        let nodes = Nodes::new(["ECM"]).unwrap();
552
553        let signal = Signal::new(
554            "RPM",
555            0,
556            16,
557            ByteOrder::BigEndian,
558            true,
559            1.0,
560            0.0,
561            0.0,
562            100.0,
563            None::<&str>,
564            Receivers::None,
565        )
566        .unwrap();
567
568        let message1 = Message::new(256, "EngineData1", 8, "ECM", vec![signal.clone()]).unwrap();
569        let message2 = Message::new(256, "EngineData2", 8, "ECM", vec![signal]).unwrap();
570
571        let result = Dbc::new(version, nodes, vec![message1, message2]);
572        assert!(result.is_err());
573        match result.unwrap_err() {
574            Error::Dbc(msg) => {
575                // Check for format template text (language-agnostic) - extract text before first placeholder
576                let template_text = lang::FORMAT_DUPLICATE_MESSAGE_ID.split("{}").next().unwrap();
577                assert!(msg.contains(template_text.trim_end_matches(':').trim_end()));
578            }
579            _ => panic!("Expected Dbc error"),
580        }
581    }
582
583    #[test]
584    fn test_dbc_new_sender_not_in_nodes() {
585        let version = Version::new(1, Some(0), None).unwrap();
586        let nodes = Nodes::new(["ECM"]).unwrap(); // Only ECM, but message uses TCM
587
588        let signal = Signal::new(
589            "RPM",
590            0,
591            16,
592            ByteOrder::BigEndian,
593            true,
594            1.0,
595            0.0,
596            0.0,
597            100.0,
598            None::<&str>,
599            Receivers::None,
600        )
601        .unwrap();
602
603        let message = Message::new(256, "EngineData", 8, "TCM", vec![signal]).unwrap();
604
605        let result = Dbc::new(version, nodes, vec![message]);
606        assert!(result.is_err());
607        match result.unwrap_err() {
608            Error::Dbc(msg) => {
609                // Check for format template text (language-agnostic) - extract text before first placeholder
610                let template_text = lang::FORMAT_SENDER_NOT_IN_NODES.split("{}").next().unwrap();
611                assert!(msg.contains(template_text.trim_end()));
612            }
613            _ => panic!("Expected Dbc error"),
614        }
615    }
616
617    #[test]
618    fn parses_real_dbc() {
619        let data = r#"VERSION "1.0"
620
621BU_: ECM TCM
622
623BO_ 256 Engine : 8 ECM
624 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
625 SG_ Temp : 16|8@0- (1,-40) [-40|215] "°C"
626
627BO_ 512 Brake : 4 TCM
628 SG_ Pressure : 0|16@1+ (0.1,0) [0|1000] "bar""#;
629
630        let dbc = Dbc::parse(data).unwrap();
631        assert_eq!(dbc.messages().len(), 2);
632        assert_eq!(dbc.messages()[0].signals().len(), 2);
633        assert_eq!(dbc.messages()[0].signals()[0].name(), "RPM");
634        assert_eq!(dbc.messages()[0].signals()[1].name(), "Temp");
635        assert_eq!(dbc.messages()[1].signals().len(), 1);
636        assert_eq!(dbc.messages()[1].signals()[0].name(), "Pressure");
637    }
638
639    #[test]
640    fn test_parse_duplicate_message_id() {
641        // Test that parse also validates duplicate message IDs
642        let data = r#"VERSION "1.0"
643
644BU_: ECM
645
646BO_ 256 EngineData1 : 8 ECM
647 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
648
649BO_ 256 EngineData2 : 8 ECM
650 SG_ Temp : 16|8@0- (1,-40) [-40|215] "°C"
651"#;
652
653        let result = Dbc::parse(data);
654        assert!(result.is_err());
655        match result.unwrap_err() {
656            Error::Dbc(msg) => {
657                // Check for format template text (language-agnostic) - extract text before first placeholder
658                let template_text = lang::FORMAT_DUPLICATE_MESSAGE_ID.split("{}").next().unwrap();
659                assert!(msg.contains(template_text.trim_end_matches(':').trim_end()));
660            }
661            _ => panic!("Expected Dbc error"),
662        }
663    }
664
665    #[test]
666    fn test_parse_sender_not_in_nodes() {
667        // Test that parse also validates message senders are in nodes list
668        let data = r#"VERSION "1.0"
669
670BU_: ECM
671
672BO_ 256 EngineData : 8 TCM
673 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
674"#;
675
676        let result = Dbc::parse(data);
677        assert!(result.is_err());
678        match result.unwrap_err() {
679            Error::Dbc(msg) => {
680                // Check for format template text (language-agnostic) - extract text before first placeholder
681                let template_text = lang::FORMAT_SENDER_NOT_IN_NODES.split("{}").next().unwrap();
682                assert!(msg.contains(template_text.trim_end()));
683            }
684            _ => panic!("Expected Dbc error"),
685        }
686    }
687
688    #[test]
689    fn test_parse_empty_file() {
690        // Test parsing an empty file
691        let result = Dbc::parse("");
692        assert!(result.is_err());
693        match result.unwrap_err() {
694            Error::Dbc(msg) => assert!(msg.contains(lang::DBC_EMPTY_FILE)),
695            _ => panic!("Expected Dbc error"),
696        }
697    }
698
699    #[test]
700    fn test_parse_missing_nodes() {
701        // Test parsing without BU_ statement
702        let data = r#"VERSION "1.0"
703
704BO_ 256 EngineData : 8 ECM
705 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
706"#;
707
708        let result = Dbc::parse(data);
709        assert!(result.is_err());
710        match result.unwrap_err() {
711            Error::Dbc(msg) => assert!(msg.contains(lang::DBC_NODES_NOT_DEFINED)),
712            _ => panic!("Expected Dbc error"),
713        }
714    }
715
716    #[test]
717    fn test_parse_bytes() {
718        let data = r#"VERSION "1.0"
719
720BU_: ECM
721
722BO_ 256 Engine : 8 ECM
723 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
724"#;
725
726        let bytes = data.as_bytes();
727        let dbc = Dbc::parse_bytes(bytes).unwrap();
728        assert_eq!(dbc.version().major(), 1);
729        assert_eq!(dbc.messages().len(), 1);
730    }
731
732    #[test]
733    fn test_parse_from_string() {
734        let data = String::from(
735            r#"VERSION "1.0"
736
737BU_: ECM
738
739BO_ 256 Engine : 8 ECM
740 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
741"#,
742        );
743
744        let dbc = Dbc::parse_from(data).unwrap();
745        assert_eq!(dbc.version().major(), 1);
746        assert_eq!(dbc.messages().len(), 1);
747    }
748
749    #[test]
750    fn test_parse_bytes_invalid_utf8() {
751        // Invalid UTF-8 sequence
752        let invalid_bytes = &[0xFF, 0xFE, 0xFD];
753        let result = Dbc::parse_bytes(invalid_bytes);
754        assert!(result.is_err());
755        match result.unwrap_err() {
756            Error::Dbc(msg) => {
757                // Check for format template text (language-agnostic) - extract text before first placeholder
758                let template_text = lang::FORMAT_INVALID_UTF8.split("{}").next().unwrap();
759                assert!(msg.contains(template_text.trim_end_matches(':').trim_end()));
760            }
761            _ => panic!("Expected Dbc error"),
762        }
763    }
764
765    #[test]
766    fn test_save_basic() {
767        let version = Version::new(1, Some(0), None).unwrap();
768        let nodes = Nodes::new(["ECM"]).unwrap();
769
770        let signal = Signal::new(
771            "RPM",
772            0,
773            16,
774            ByteOrder::BigEndian,
775            true,
776            0.25,
777            0.0,
778            0.0,
779            8000.0,
780            Some("rpm" as &str),
781            Receivers::Broadcast,
782        )
783        .unwrap();
784
785        let message = Message::new(256, "EngineData", 8, "ECM", vec![signal]).unwrap();
786        let dbc = Dbc::new(version, nodes, vec![message]).unwrap();
787
788        let saved = dbc.save();
789        assert!(saved.contains("VERSION \"1.0\""));
790        assert!(saved.contains("BU_: ECM"));
791        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
792        assert!(saved.contains("SG_ RPM : 0|16@1+ (0.25,0) [0|8000] \"rpm\" *"));
793    }
794
795    #[test]
796    fn test_save_round_trip() {
797        let original = r#"VERSION "1.0"
798
799BU_: ECM TCM
800
801BO_ 256 EngineData : 8 ECM
802 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
803 SG_ Temperature : 16|8@0- (1,-40) [-40|215] "°C" TCM
804
805BO_ 512 BrakeData : 4 TCM
806 SG_ Pressure : 0|16@0+ (0.1,0) [0|1000] "bar"
807"#;
808
809        let dbc = Dbc::parse(original).unwrap();
810        let saved = dbc.save();
811        let dbc2 = Dbc::parse(&saved).unwrap();
812
813        // Verify round-trip: parsed data should match
814        assert_eq!(dbc.version().major(), dbc2.version().major());
815        assert_eq!(dbc.version().minor(), dbc2.version().minor());
816        assert_eq!(dbc.messages().len(), dbc2.messages().len());
817
818        for (msg1, msg2) in dbc.messages().iter().zip(dbc2.messages().iter()) {
819            assert_eq!(msg1.id(), msg2.id());
820            assert_eq!(msg1.name(), msg2.name());
821            assert_eq!(msg1.dlc(), msg2.dlc());
822            assert_eq!(msg1.sender(), msg2.sender());
823            assert_eq!(msg1.signals().len(), msg2.signals().len());
824
825            for (sig1, sig2) in msg1.signals().iter().zip(msg2.signals().iter()) {
826                assert_eq!(sig1.name(), sig2.name());
827                assert_eq!(sig1.start_bit(), sig2.start_bit());
828                assert_eq!(sig1.length(), sig2.length());
829                assert_eq!(sig1.byte_order(), sig2.byte_order());
830                assert_eq!(sig1.is_unsigned(), sig2.is_unsigned());
831                assert_eq!(sig1.factor(), sig2.factor());
832                assert_eq!(sig1.offset(), sig2.offset());
833                assert_eq!(sig1.min(), sig2.min());
834                assert_eq!(sig1.max(), sig2.max());
835                assert_eq!(sig1.unit(), sig2.unit());
836                assert_eq!(sig1.receivers(), sig2.receivers());
837            }
838        }
839    }
840
841    #[test]
842    fn test_save_multiple_messages() {
843        let version = Version::new(1, Some(0), None).unwrap();
844        let nodes = Nodes::new(["ECM", "TCM"]).unwrap();
845
846        let signal1 = Signal::new(
847            "RPM",
848            0,
849            16,
850            ByteOrder::BigEndian,
851            true,
852            0.25,
853            0.0,
854            0.0,
855            8000.0,
856            Some("rpm" as &str),
857            Receivers::Broadcast,
858        )
859        .unwrap();
860
861        let signal2 = Signal::new(
862            "Pressure",
863            0,
864            16,
865            ByteOrder::LittleEndian,
866            true,
867            0.1,
868            0.0,
869            0.0,
870            1000.0,
871            Some("bar" as &str),
872            Receivers::None,
873        )
874        .unwrap();
875
876        let message1 = Message::new(256, "EngineData", 8, "ECM", vec![signal1]).unwrap();
877        let message2 = Message::new(512, "BrakeData", 4, "TCM", vec![signal2]).unwrap();
878
879        let dbc = Dbc::new(version, nodes, vec![message1, message2]).unwrap();
880        let saved = dbc.save();
881
882        // Verify both messages are present
883        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
884        assert!(saved.contains("BO_ 512 BrakeData : 4 TCM"));
885        assert!(saved.contains("SG_ RPM"));
886        assert!(saved.contains("SG_ Pressure"));
887    }
888}
889
890#[cfg(feature = "std")]
891#[cfg(test)]
892mod std_tests {
893    use super::Dbc;
894    use std::io::Cursor;
895
896    #[test]
897    fn test_from_reader_cursor() {
898        let data = r#"VERSION "1.0"
899
900BU_: ECM
901
902BO_ 256 Engine : 8 ECM
903 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
904"#;
905
906        let cursor = Cursor::new(data.as_bytes());
907        let dbc = Dbc::from_reader(cursor).unwrap();
908        assert_eq!(dbc.version().major(), 1);
909        assert_eq!(dbc.messages().len(), 1);
910    }
911
912    #[test]
913    fn test_from_reader_file() {
914        // Test reading from an actual file
915        let content =
916            std::fs::read_to_string("tests/data/simple.dbc").expect("Failed to read test file");
917        let cursor = Cursor::new(content.as_bytes());
918        let dbc = Dbc::from_reader(cursor).unwrap();
919        assert_eq!(dbc.messages().len(), 2);
920    }
921}