dbc_rs/version/
version.rs

1use crate::{
2    Parser,
3    error::{ParseError, ParseResult, lang},
4};
5
6/// Represents a version string from a DBC file.
7///
8/// The `VERSION` statement in a DBC file specifies the database version.
9/// This struct stores the version string as a borrowed reference.
10///
11/// # Examples
12///
13/// ```rust,no_run
14/// use dbc_rs::Dbc;
15///
16/// let dbc_content = r#"VERSION "1.0"
17///
18/// BU_: ECM
19///
20/// BO_ 256 Engine : 8 ECM
21///  SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
22/// "#;
23///
24/// let dbc = Dbc::parse(dbc_content)?;
25/// if let Some(version) = dbc.version() {
26///     println!("DBC version: {}", version);
27///     // Access the raw string
28///     assert_eq!(version.as_str(), "1.0");
29/// }
30/// # Ok::<(), dbc_rs::Error>(())
31/// ```
32///
33/// # Format
34///
35/// The version string can be any sequence of printable characters enclosed in quotes.
36/// Common formats include:
37/// - `"1.0"` - Simple semantic version
38/// - `"1.2.3"` - Full semantic version
39/// - `"1.0-beta"` - Version with suffix
40/// - `""` - Empty version string (allowed)
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub struct Version<'a> {
43    version: &'a str,
44}
45
46impl<'a> Version<'a> {
47    /// Creates a new `Version` from a version string.
48    ///
49    /// # Note
50    ///
51    /// This method is intended for internal use. For parsing from DBC content,
52    /// use `Version::parse()`. For programmatic construction, use `VersionBuilder`
53    /// (requires `alloc` feature).
54    ///
55    /// # Arguments
56    ///
57    /// * `version` - The version string (should be validated before calling this)
58    pub(crate) fn new(version: &'a str) -> Self {
59        // Validation should have been done prior (by builder or parse)
60        Self { version }
61    }
62
63    /// Parses a `VERSION` statement from a DBC file.
64    ///
65    /// This method expects the parser to be positioned at or after the `VERSION` keyword.
66    /// It will parse the version string enclosed in quotes.
67    ///
68    /// # Format
69    ///
70    /// The expected format is: `VERSION "version_string"`
71    ///
72    /// # Arguments
73    ///
74    /// * `parser` - The parser positioned at the VERSION statement
75    ///
76    /// # Returns
77    ///
78    /// Returns `Ok(Version)` if parsing succeeds, or `Err(ParseError)` if:
79    /// - The opening quote is missing
80    /// - The closing quote is missing
81    /// - The version string exceeds the maximum length (255 characters)
82    /// - The version string contains invalid UTF-8
83    ///
84    /// # Examples
85    ///
86    /// ```rust,no_run
87    /// use dbc_rs::Dbc;
88    ///
89    /// // Version is typically accessed from a parsed DBC
90    /// let dbc = Dbc::parse(r#"VERSION "1.0"
91    ///
92    /// BU_: ECM
93    /// "#)?;
94    ///
95    /// if let Some(version) = dbc.version() {
96    ///     assert_eq!(version.as_str(), "1.0");
97    /// }
98    /// # Ok::<(), dbc_rs::Error>(())
99    /// ```
100    #[must_use = "parse result should be checked"]
101    pub(crate) fn parse<'b: 'a>(parser: &mut Parser<'b>) -> ParseResult<Self> {
102        use crate::VERSION;
103        // Note: When called from Dbc::parse, find_next_keyword already consumed "VERSION"
104        // So we try to expect "VERSION" first, and if that fails, we're already past it
105        if parser.expect(VERSION.as_bytes()).is_ok() {
106            // Successfully consumed "VERSION", now skip whitespace and expect quote
107            parser
108                .skip_whitespace()?
109                .expect(b"\"")
110                .map_err(|_| ParseError::Expected("Expected opening quote after VERSION"))?;
111        } else {
112            // Check if we're at the start of input and input doesn't start with "VERSION"
113            // If so, "VERSION" is required
114            // Note: expect() doesn't change position on failure, so we can check is_at_start() here
115            if parser.is_at_start() && !parser.starts_with(VERSION.as_bytes()) {
116                // Use the constant in the error message (VERSION is "VERSION")
117                return Err(ParseError::Expected("Expected 'VERSION' keyword"));
118            }
119            // Already past "VERSION" from find_next_keyword
120            // find_next_keyword advances to right after "VERSION", which should be at whitespace or quote
121            // Skip whitespace if present, then expect quote
122            let _ = parser.skip_whitespace().ok(); // Ignore error if no whitespace
123            parser
124                .expect(b"\"")
125                .map_err(|_| ParseError::Expected("Expected opening quote after VERSION"))?;
126        }
127
128        // Read version content until closing quote (allow any printable characters)
129        // Use a reasonable max length for version strings (e.g., 255 characters)
130        // Note: take_until_quote already advances past the closing quote
131        const MAX_VERSION_LENGTH: u16 = 255;
132        let version_bytes = parser.take_until_quote(false, MAX_VERSION_LENGTH)?;
133
134        // Convert bytes to string slice using the parser's input
135        let version_str = core::str::from_utf8(version_bytes)
136            .map_err(|_| ParseError::Version(lang::VERSION_INVALID))?;
137
138        // Construct directly (validation already done during parsing)
139        Ok(Version::new(version_str))
140    }
141
142    /// Returns the version string as a `&str`.
143    ///
144    /// # Examples
145    ///
146    /// ```rust,no_run
147    /// use dbc_rs::Dbc;
148    ///
149    /// let dbc = Dbc::parse(r#"VERSION "1.2.3"
150    ///
151    /// BU_: ECM
152    /// "#)?;
153    ///
154    /// if let Some(version) = dbc.version() {
155    ///     assert_eq!(version.as_str(), "1.2.3");
156    /// }
157    /// # Ok::<(), dbc_rs::Error>(())
158    /// ```
159    #[must_use]
160    pub fn as_str(&self) -> &'a str {
161        self.version
162    }
163
164    /// Converts the version to its DBC file representation.
165    ///
166    /// Returns a string in the format: `VERSION "version_string"`
167    ///
168    /// # Examples
169    ///
170    /// ```rust,no_run
171    /// use dbc_rs::Dbc;
172    ///
173    /// let dbc = Dbc::parse(r#"VERSION "1.0"
174    ///
175    /// BU_: ECM
176    /// "#)?;
177    ///
178    /// if let Some(version) = dbc.version() {
179    ///     let dbc_string = version.to_dbc_string();
180    ///     assert_eq!(dbc_string, "VERSION \"1.0\"");
181    /// }
182    /// # Ok::<(), dbc_rs::Error>(())
183    /// ```
184    ///
185    /// # Empty Version
186    ///
187    /// Empty version strings are represented as `VERSION ""`:
188    ///
189    /// ```rust,no_run
190    /// use dbc_rs::Dbc;
191    ///
192    /// let dbc = Dbc::parse(r#"VERSION ""
193    ///
194    /// BU_: ECM
195    /// "#)?;
196    ///
197    /// if let Some(version) = dbc.version() {
198    ///     assert_eq!(version.to_dbc_string(), "VERSION \"\"");
199    /// }
200    /// # Ok::<(), dbc_rs::Error>(())
201    /// ```
202    ///
203    /// # Feature Requirements
204    ///
205    /// This method requires the `alloc` feature to be enabled.
206    #[must_use]
207    #[cfg(feature = "alloc")]
208    pub fn to_dbc_string(&self) -> alloc::string::String {
209        use crate::VERSION;
210        use alloc::format;
211        if self.version.is_empty() {
212            format!("{} \"\"", VERSION)
213        } else {
214            format!("{} \"{}\"", VERSION, self.version)
215        }
216    }
217}
218
219/// Display implementation for `Version`.
220///
221/// Formats the version as just the version string (without the `VERSION` keyword or quotes).
222///
223/// # Examples
224///
225/// ```rust,no_run
226/// use dbc_rs::Dbc;
227///
228/// let dbc = Dbc::parse(r#"VERSION "1.2.3"
229///
230/// BU_: ECM
231/// "#)?;
232///
233/// if let Some(version) = dbc.version() {
234///     // Display trait formats as just the version string
235///     assert_eq!(format!("{}", version), "1.2.3");
236///     // Use to_dbc_string() for full DBC format (requires alloc feature)
237///     #[cfg(feature = "alloc")]
238///     assert_eq!(version.to_dbc_string(), "VERSION \"1.2.3\"");
239/// }
240/// # Ok::<(), dbc_rs::Error>(())
241/// ```
242impl<'a> core::fmt::Display for Version<'a> {
243    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
244        write!(f, "{}", self.version)
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::Version;
251    use crate::Parser;
252    use crate::error::ParseError;
253
254    // Helper function to assert version string (works in all configurations)
255    fn assert_version_str(version: &Version, expected: &str) {
256        assert_eq!(version.as_str(), expected);
257        #[cfg(any(feature = "alloc", feature = "kernel"))]
258        {
259            #[cfg(feature = "alloc")]
260            use alloc::string::ToString;
261            #[cfg(all(feature = "kernel", not(feature = "alloc")))]
262            {
263                // In kernel mode, Display works but to_string() requires alloc
264                use alloc::format;
265                let formatted = format!("{}", version);
266                assert_eq!(formatted, expected);
267            }
268            #[cfg(feature = "alloc")]
269            assert_eq!(version.to_string(), expected);
270        }
271    }
272
273    // Tests that work in all configurations
274    #[test]
275    fn test_read_version() {
276        let line = b"VERSION \"1.0\"";
277        let mut parser = Parser::new(line).unwrap();
278        let version = Version::parse(&mut parser).unwrap();
279        assert_version_str(&version, "1.0");
280    }
281
282    #[test]
283    fn test_read_version_invalid() {
284        let line = b"VERSION 1.0";
285        let mut parser = Parser::new(line).unwrap();
286        let version = Version::parse(&mut parser).unwrap_err();
287        match version {
288            ParseError::Expected(_) => {}
289            _ => panic!("Expected Expected error, got {:?}", version),
290        }
291    }
292
293    #[test]
294    fn test_version_parse_empty() {
295        let line = b"";
296        let result = Parser::new(line);
297        assert!(result.is_err());
298        match result.unwrap_err() {
299            ParseError::UnexpectedEof => {}
300            _ => panic!("Expected UnexpectedEof"),
301        }
302    }
303
304    #[test]
305    fn test_version_parse_no_version_prefix() {
306        let line = b"\"1.0\"";
307        let mut parser = Parser::new(line).unwrap();
308        let result = Version::parse(&mut parser);
309        assert!(result.is_err());
310        match result.unwrap_err() {
311            ParseError::Expected(_) => {}
312            _ => panic!("Expected Expected error"),
313        }
314    }
315
316    #[test]
317    fn test_version_parse_no_quotes() {
318        let line = b"VERSION 1.0";
319        let mut parser = Parser::new(line).unwrap();
320        let result = Version::parse(&mut parser);
321        assert!(result.is_err());
322        match result.unwrap_err() {
323            ParseError::Expected(_) => {}
324            _ => panic!("Expected Expected error"),
325        }
326    }
327
328    #[test]
329    fn test_version_parse_major_only() {
330        let line = b"VERSION \"1\"";
331        let mut parser = Parser::new(line).unwrap();
332        let result = Version::parse(&mut parser);
333        assert!(result.is_ok());
334        let version = result.unwrap();
335        assert_version_str(&version, "1");
336    }
337
338    #[test]
339    fn test_version_parse_full_version() {
340        let line = b"VERSION \"1.2.3\"";
341        let mut parser = Parser::new(line).unwrap();
342        let result = Version::parse(&mut parser);
343        assert!(result.is_ok());
344        let version = result.unwrap();
345        assert_version_str(&version, "1.2.3");
346    }
347
348    #[test]
349    fn test_version_parse_with_whitespace() {
350        let line = b"VERSION  \"1.0\"";
351        let mut parser = Parser::new(line).unwrap();
352        let result = Version::parse(&mut parser);
353        assert!(result.is_ok());
354        let version = result.unwrap();
355        assert_version_str(&version, "1.0");
356    }
357
358    #[test]
359    fn test_version_parse_empty_quotes() {
360        let line = b"VERSION \"\"";
361        let mut parser = Parser::new(line).unwrap();
362        let version = Version::parse(&mut parser).unwrap();
363        assert_version_str(&version, "");
364    }
365
366    #[test]
367    fn test_version_parse_missing_closing_quote() {
368        let line = b"VERSION \"1.0";
369        let mut parser = Parser::new(line).unwrap();
370        let result = Version::parse(&mut parser);
371        assert!(result.is_err());
372        match result.unwrap_err() {
373            ParseError::UnexpectedEof => {}
374            _ => panic!("Expected UnexpectedEof"),
375        }
376    }
377
378    #[test]
379    fn test_version_parse_missing_opening_quote() {
380        let line = b"VERSION 1.0\"";
381        let mut parser = Parser::new(line).unwrap();
382        let result = Version::parse(&mut parser);
383        assert!(result.is_err());
384        match result.unwrap_err() {
385            ParseError::Expected(_) => {}
386            _ => panic!("Expected Expected error"),
387        }
388    }
389
390    #[test]
391    fn test_version_with_special_chars() {
392        let line = b"VERSION \"1.0-beta\"";
393        let mut parser = Parser::new(line).unwrap();
394        let version = Version::parse(&mut parser).unwrap();
395        assert_version_str(&version, "1.0-beta");
396    }
397
398    // Tests that require alloc (to_dbc_string is only available with alloc)
399    #[cfg(feature = "alloc")]
400    mod tests_with_alloc {
401        use super::*;
402
403        #[test]
404        fn test_version_to_dbc_string() {
405            let line1 = b"VERSION \"1\"";
406            let mut parser1 = Parser::new(line1).unwrap();
407            let v1 = Version::parse(&mut parser1).unwrap();
408            assert_eq!(v1.to_dbc_string(), "VERSION \"1\"");
409
410            let line2 = b"VERSION \"1.0\"";
411            let mut parser2 = Parser::new(line2).unwrap();
412            let v2 = Version::parse(&mut parser2).unwrap();
413            assert_eq!(v2.to_dbc_string(), "VERSION \"1.0\"");
414
415            let line3 = b"VERSION \"2.3.4\"";
416            let mut parser3 = Parser::new(line3).unwrap();
417            let v3 = Version::parse(&mut parser3).unwrap();
418            assert_eq!(v3.to_dbc_string(), "VERSION \"2.3.4\"");
419        }
420
421        #[test]
422        fn test_version_empty_round_trip() {
423            let line = b"VERSION \"\"";
424            let mut parser = Parser::new(line).unwrap();
425            let version = Version::parse(&mut parser).unwrap();
426            assert_eq!(version.to_dbc_string(), "VERSION \"\"");
427        }
428    }
429}