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}