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}