dbc_rs/nodes/
nodes.rs

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