dbc_rs/nodes/
nodes.rs

1use crate::{
2    BU_, Error, MAX_NAME_SIZE, MAX_NODES, Parser, Result,
3    compat::{String, Vec},
4    error::lang,
5};
6
7/// Represents a collection of node (ECU) names from a DBC file.
8///
9/// The `BU_` statement in a DBC file lists all nodes (ECUs) on the CAN bus.
10/// This struct stores the node names as borrowed references.
11///
12/// # Examples
13///
14/// ```rust,no_run
15/// use dbc_rs::Dbc;
16///
17/// let dbc = Dbc::parse(r#"VERSION "1.0"
18///
19/// BU_: ECM TCM BCM
20///
21/// BO_ 256 Engine : 8 ECM
22///  SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
23/// "#)?;
24///
25/// // Access nodes
26/// assert_eq!(dbc.nodes().len(), 3);
27/// assert!(dbc.nodes().contains("ECM"));
28/// assert!(dbc.nodes().contains("TCM"));
29///
30/// // Iterate over nodes
31/// for node in dbc.nodes().iter() {
32///     println!("Node: {}", node);
33/// }
34/// # Ok::<(), dbc_rs::Error>(())
35/// ```
36///
37/// # Empty Nodes
38///
39/// A DBC file may have an empty node list (`BU_:` with no nodes):
40///
41/// ```rust,no_run
42/// use dbc_rs::Dbc;
43///
44/// let dbc = Dbc::parse(r#"VERSION "1.0"
45///
46/// BU_:
47///
48/// BO_ 256 Engine : 8 ECM
49/// "#)?;
50///
51/// assert!(dbc.nodes().is_empty());
52/// # Ok::<(), dbc_rs::Error>(())
53/// ```
54///
55/// # DBC Format
56///
57/// In DBC files, nodes are specified on the `BU_` line:
58/// - Format: `BU_: Node1 Node2 Node3 ...`
59/// - Node names are space-separated
60/// - Maximum of 256 nodes (DoS protection)
61/// - Duplicate node names are not allowed (case-sensitive)
62#[derive(Debug, Default, PartialEq, Eq, Hash)]
63pub struct Nodes {
64    nodes: Vec<String<{ MAX_NAME_SIZE }>, { MAX_NODES }>,
65}
66
67impl Nodes {
68    // Shared validation function
69    pub(crate) fn validate(nodes: &[impl AsRef<str>]) -> Result<()> {
70        // Check for too many nodes (DoS protection)
71        if let Some(err) = crate::check_max_limit(
72            nodes.len(),
73            MAX_NODES,
74            Error::Validation(lang::NODES_TOO_MANY),
75        ) {
76            return Err(err);
77        }
78
79        // Check for duplicate node names (case-sensitive)
80        for (i, node1) in nodes.iter().enumerate() {
81            for node2 in nodes.iter().skip(i + 1) {
82                if node1.as_ref() == node2.as_ref() {
83                    return Err(Error::Validation(lang::NODES_DUPLICATE_NAME));
84                }
85            }
86        }
87        Ok(())
88    }
89
90    #[cfg(feature = "std")]
91    pub(crate) fn new(nodes: &[impl AsRef<str>]) -> Self {
92        // Validation should have been done prior (by builder)
93        let nodes_vec: Vec<String<{ MAX_NAME_SIZE }>, { MAX_NODES }> =
94            nodes.iter().take(MAX_NODES).map(|n| n.as_ref().to_string().into()).collect();
95        Self { nodes: nodes_vec }
96    }
97
98    #[must_use = "parse result should be checked"]
99    pub(crate) fn parse(parser: &mut Parser) -> Result<Self> {
100        // Nodes parsing must always start with "BU_" keyword
101        parser
102            .expect(BU_.as_bytes())
103            .map_err(|_| Error::Expected("Expected BU_ keyword"))?;
104
105        // Expect ":" after "BU_"
106        parser.expect(b":").map_err(|_| Error::Expected("Expected colon after BU_"))?;
107
108        // Skip optional whitespace after ":"
109        parser.skip_newlines_and_spaces();
110
111        // Parse node names into Vec
112        let mut node_names: Vec<String<{ MAX_NAME_SIZE }>, { MAX_NODES }> = Vec::new();
113
114        loop {
115            // Skip whitespace before each node name
116            let _ = parser.skip_whitespace();
117
118            // Try to parse an identifier (node name)
119            // parse_identifier() will fail if we're at EOF
120            match parser.parse_identifier() {
121                Ok(node) => {
122                    if let Some(err) = crate::check_max_limit(
123                        node_names.len(),
124                        MAX_NODES - 1,
125                        Error::Nodes(lang::NODES_TOO_MANY),
126                    ) {
127                        return Err(err);
128                    }
129                    let node_str = crate::validate_name(node)?;
130                    node_names.push(node_str).map_err(|_| Error::Nodes(lang::NODES_TOO_MANY))?;
131                }
132                Err(_) => {
133                    // No more identifiers, break
134                    break;
135                }
136            }
137        }
138
139        if node_names.is_empty() {
140            return Ok(Nodes { nodes: Vec::new() });
141        }
142
143        // Validate before construction
144        Self::validate(node_names.as_slice()).map_err(|e| {
145            crate::error::map_val_error(e, Error::Nodes, || Error::Nodes(lang::NODES_ERROR_PREFIX))
146        })?;
147        // Construct directly (validation already done)
148        Ok(Self { nodes: node_names })
149    }
150
151    /// Returns an iterator over the node names.
152    ///
153    /// # Examples
154    ///
155    /// ```rust,no_run
156    /// use dbc_rs::Dbc;
157    ///
158    /// let dbc = Dbc::parse(r#"VERSION "1.0"
159    ///
160    /// BU_: ECM TCM BCM
161    /// "#)?;
162    ///
163    /// // Iterate over nodes
164    /// let mut iter = dbc.nodes().iter();
165    /// assert_eq!(iter.next(), Some("ECM"));
166    /// assert_eq!(iter.next(), Some("TCM"));
167    /// assert_eq!(iter.next(), Some("BCM"));
168    /// assert_eq!(iter.next(), None);
169    ///
170    /// // Or use in a loop
171    /// for node in dbc.nodes().iter() {
172    ///     println!("Node: {}", node);
173    /// }
174    /// # Ok::<(), dbc_rs::Error>(())
175    /// ```
176    #[inline]
177    #[must_use = "iterator is lazy and does nothing unless consumed"]
178    pub fn iter(&self) -> impl Iterator<Item = &str> + '_ {
179        self.nodes.iter().map(|s| s.as_str())
180    }
181
182    /// Checks if a node name is in the list.
183    ///
184    /// The check is case-sensitive.
185    ///
186    /// # Arguments
187    ///
188    /// * `node` - The node name to check
189    ///
190    /// # Examples
191    ///
192    /// ```rust,no_run
193    /// use dbc_rs::Dbc;
194    ///
195    /// let dbc = Dbc::parse(r#"VERSION "1.0"
196    ///
197    /// BU_: ECM TCM
198    /// "#)?;
199    ///
200    /// assert!(dbc.nodes().contains("ECM"));
201    /// assert!(dbc.nodes().contains("TCM"));
202    /// assert!(!dbc.nodes().contains("BCM"));
203    /// assert!(!dbc.nodes().contains("ecm")); // Case-sensitive
204    /// # Ok::<(), dbc_rs::Error>(())
205    /// ```
206    #[inline]
207    #[must_use]
208    pub fn contains(&self, node: &str) -> bool {
209        self.iter().any(|n| n == node)
210    }
211
212    /// Returns the number of nodes in the collection.
213    ///
214    /// # Examples
215    ///
216    /// ```rust,no_run
217    /// use dbc_rs::Dbc;
218    ///
219    /// let dbc = Dbc::parse(r#"VERSION "1.0"
220    ///
221    /// BU_: ECM TCM BCM
222    /// "#)?;
223    ///
224    /// assert_eq!(dbc.nodes().len(), 3);
225    /// # Ok::<(), dbc_rs::Error>(())
226    /// ```
227    #[inline]
228    #[must_use]
229    pub fn len(&self) -> usize {
230        self.nodes.len()
231    }
232
233    /// Returns `true` if there are no nodes in the collection.
234    ///
235    /// # Examples
236    ///
237    /// ```rust,no_run
238    /// use dbc_rs::Dbc;
239    ///
240    /// // Empty node list
241    /// let dbc = Dbc::parse(r#"VERSION "1.0"
242    ///
243    /// BU_:
244    /// "#)?;
245    /// assert!(dbc.nodes().is_empty());
246    ///
247    /// // With nodes
248    /// let dbc2 = Dbc::parse(r#"VERSION "1.0"
249    ///
250    /// BU_: ECM
251    /// "#)?;
252    /// assert!(!dbc2.nodes().is_empty());
253    /// # Ok::<(), dbc_rs::Error>(())
254    /// ```
255    pub fn is_empty(&self) -> bool {
256        self.nodes.is_empty()
257    }
258
259    /// Gets a node by index.
260    ///
261    /// Returns `None` if the index is out of bounds.
262    ///
263    /// # Arguments
264    ///
265    /// * `index` - The zero-based index of the node
266    ///
267    /// # Examples
268    ///
269    /// ```rust,no_run
270    /// use dbc_rs::Dbc;
271    ///
272    /// let dbc = Dbc::parse(r#"VERSION "1.0"
273    ///
274    /// BU_: ECM TCM BCM
275    /// "#)?;
276    ///
277    /// assert_eq!(dbc.nodes().at(0), Some("ECM"));
278    /// assert_eq!(dbc.nodes().at(1), Some("TCM"));
279    /// assert_eq!(dbc.nodes().at(2), Some("BCM"));
280    /// assert_eq!(dbc.nodes().at(3), None); // Out of bounds
281    /// # Ok::<(), dbc_rs::Error>(())
282    /// ```
283    #[inline]
284    #[must_use]
285    pub fn at(&self, index: usize) -> Option<&str> {
286        self.nodes.get(index).map(|s| s.as_str())
287    }
288
289    /// Converts the nodes to their DBC file representation.
290    ///
291    /// Returns a string in the format: `BU_: Node1 Node2 Node3 ...`
292    ///
293    /// # Examples
294    ///
295    /// ```rust,no_run
296    /// use dbc_rs::Dbc;
297    ///
298    /// let dbc = Dbc::parse(r#"VERSION "1.0"
299    ///
300    /// BU_: ECM TCM BCM
301    /// "#)?;
302    ///
303    /// let dbc_string = dbc.nodes().to_dbc_string();
304    /// assert_eq!(dbc_string, "BU_: ECM TCM BCM");
305    /// # Ok::<(), dbc_rs::Error>(())
306    /// ```
307    ///
308    /// # Empty Nodes
309    ///
310    /// Empty node lists are represented as `BU_:`:
311    ///
312    /// ```rust,no_run
313    /// use dbc_rs::Dbc;
314    ///
315    /// let dbc = Dbc::parse(r#"VERSION "1.0"
316    ///
317    /// BU_:
318    /// "#)?;
319    ///
320    /// assert_eq!(dbc.nodes().to_dbc_string(), "BU_:");
321    /// # Ok::<(), dbc_rs::Error>(())
322    /// ```
323    ///
324    /// # Feature Requirements
325    ///
326    /// This method requires the `std` feature to be enabled.
327    #[cfg(feature = "std")]
328    #[must_use]
329    pub fn to_dbc_string(&self) -> std::string::String {
330        let mut result = format!("{}:", BU_);
331        let nodes_str = format!("{}", self);
332        if !nodes_str.is_empty() {
333            result.push(' ');
334            result.push_str(&nodes_str);
335        }
336        result
337    }
338}
339
340#[cfg(feature = "std")]
341impl core::fmt::Display for Nodes {
342    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
343        if self.nodes.is_empty() {
344            return Ok(());
345        }
346        for (i, node) in self.iter().enumerate() {
347            if i > 0 {
348                write!(f, " ")?;
349            }
350            write!(f, "{}", node)?;
351        }
352        Ok(())
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    #![allow(clippy::float_cmp)]
359    use super::*;
360    use crate::{Error, Parser, error::lang};
361
362    #[test]
363    fn test_nodes_from_valid_line() {
364        let line = b"BU_: ECM TCM BCM ABS";
365        let mut parser = Parser::new(line).unwrap();
366        let nodes = Nodes::parse(&mut parser).unwrap();
367        let mut iter = nodes.iter();
368        assert_eq!(iter.next(), Some("ECM"));
369        assert_eq!(iter.next(), Some("TCM"));
370        assert_eq!(iter.next(), Some("BCM"));
371        assert_eq!(iter.next(), Some("ABS"));
372        assert_eq!(iter.next(), None);
373    }
374
375    #[test]
376    fn test_nodes_from_single_node() {
377        let line = b"BU_: ONLYONE";
378        let mut parser = Parser::new(line).unwrap();
379        let nodes = Nodes::parse(&mut parser).unwrap();
380        let mut iter = nodes.iter();
381        assert_eq!(iter.next(), Some("ONLYONE"));
382        assert_eq!(iter.next(), None);
383    }
384
385    #[test]
386    fn test_nodes_from_with_extra_spaces() {
387        let line = b"BU_:   Node1   Node2   ";
388        let mut parser = Parser::new(line).unwrap();
389        let nodes = Nodes::parse(&mut parser).unwrap();
390        let mut iter = nodes.iter();
391        assert_eq!(iter.next(), Some("Node1"));
392        assert_eq!(iter.next(), Some("Node2"));
393        assert_eq!(iter.next(), None);
394    }
395
396    #[test]
397    fn test_nodes_from_empty_list() {
398        let line = b"BU_:";
399        let mut parser = Parser::new(line).unwrap();
400        let nodes = Nodes::parse(&mut parser).unwrap();
401        assert!(nodes.is_empty());
402    }
403
404    // Note: Builder tests have been moved to nodes_builder.rs
405    // This module only tests Nodes parsing and direct API usage
406
407    #[test]
408    fn test_nodes_parse_duplicate() {
409        let line = b"BU_: ECM TCM ECM";
410        let mut parser = Parser::new(line).unwrap();
411        let result = Nodes::parse(&mut parser);
412        assert!(result.is_err());
413        match result.unwrap_err() {
414            Error::Nodes(msg) => assert!(msg == lang::NODES_DUPLICATE_NAME),
415            _ => panic!("Expected Error::Nodes"),
416        }
417    }
418}