aquarium_control/utilities/
version_information.rs

1/* Copyright 2024 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9
10#[cfg(not(test))]
11use log::info;
12
13use std::env;
14use std::fmt::{Display, Formatter};
15use std::num::ParseIntError;
16use thiserror::Error;
17
18/// Contains error definition for version information retrieval
19#[derive(Debug, Error)]
20pub enum VersionInformationError {
21    /// Major number could not be parsed from version string
22    #[error("Could not parse major number from version string ({0})")]
23    ParseErrorMajor(String),
24
25    /// Minor number could not be parsed from version string
26    #[error("Could not parse minor number from version string ({0})")]
27    ParseErrorMinor(String),
28
29    /// Build number could not be parsed from version string
30    #[error("Could not parse build number from version string ({0})")]
31    ParseErrorBuild(String),
32
33    /// Major number could not be converted to numeric value
34    #[error("Could not parse major number from version string ({version_string})")]
35    ConversionErrorMajor {
36        version_string: String,
37
38        #[source]
39        source: ParseIntError,
40    },
41
42    /// Minor number could not be converted to numeric value
43    #[error("Could not parse minor number from version string ({version_string})")]
44    ConversionErrorMinor {
45        version_string: String,
46
47        #[source]
48        source: ParseIntError,
49    },
50
51    /// Build number could not be converted to numeric value
52    #[error("Could not parse build number from version string ({version_string})")]
53    ConversionErrorBuild {
54        version_string: String,
55
56        #[source]
57        source: ParseIntError,
58    },
59
60    #[error("Failed to retrieve current executable path.")]
61    CurrentExecutablePathRetrievalFailure {
62        #[source]
63        source: std::io::Error,
64    },
65
66    #[error("Failed to read executable file content.")]
67    ExecutableFileReadFailure {
68        #[source]
69        source: std::io::Error,
70    },
71}
72
73/// Holds the data structure and implementation for retrieving and storing version information of the application.
74pub struct VersionInformation {
75    /// Major version number (used to identify major changes between releases)
76    pub major: i32,
77
78    /// Minor version number (used to identify minor changes between releases)
79    pub minor: i32,
80
81    /// Build number (increases with every published build)
82    pub build: i32,
83
84    /// Hash serves as a fingerprint to match SW with database configuration
85    pub hash: String,
86}
87
88impl VersionInformation {
89    /// Creates a new `VersionInformation` instance by reading the version from
90    /// `Cargo.toml` at compile time and calculating the SHA-256 hash of the
91    /// current executable.
92    ///
93    /// This is the primary constructor for production use. It performs the following steps:
94    /// 1.  Retrieves the package version string (e.g., "1.2.3") from the `CARGO_PKG_VERSION`
95    ///     environment variable, which Cargo sets at compile time.
96    /// 2.  Determines the full path to the running executable.
97    /// 3.  Reads the executable file's binary content.
98    /// 4.  Calculates a SHA-256 hash of the binary content to serve as a unique fingerprint.
99    /// 5.  Parses the version string and combines it with the hash.
100    ///
101    /// # Returns
102    /// A `Result` containing a new `VersionInformation` struct on success.
103    ///
104    /// # Errors
105    /// Returns a `VersionInformationError` if any part of the process fails:
106    /// - `VersionInformationError::CurrentExecutablePathRetrievalFailure`: If the path
107    ///   to the current executable cannot be determined.
108    /// - `VersionInformationError::ExecutableFileReadFailure`: If the executable file
109    ///   cannot be read from the filesystem, which could be due to a permissions issue.
110    /// - It will also propagate any parsing or conversion errors from the underlying
111    ///   `from_string_and_hash` function if the version string from `Cargo.toml` is malformed.
112    pub fn new() -> Result<VersionInformation, VersionInformationError> {
113        let version_string = env!("CARGO_PKG_VERSION").to_string();
114
115        // Get the fully qualified path and file name.
116        let pathfilename = env::current_exe().map_err(|e| {
117            VersionInformationError::CurrentExecutablePathRetrievalFailure { source: e }
118        })?;
119
120        #[cfg(not(test))]
121        info!(
122            target: module_path!(),
123            "path of this executable is: {}",
124            pathfilename.display()
125        );
126
127        // Calculate hash from the executable.
128        let bytes = std::fs::read(&pathfilename)
129            .map_err(|e| VersionInformationError::ExecutableFileReadFailure { source: e })?;
130
131        let hash = sha256::digest(&bytes);
132
133        // Further process information in the testable function.
134        Self::from_string_and_hash(version_string, hash)
135    }
136
137    /// Creates a new `VersionInformation` instance from a version string and a hash.
138    ///
139    /// This function is designed to be used in testing environments, allowing for the injection
140    /// of arbitrary version strings and hashes to validate parsing and behavior.
141    ///
142    /// # Arguments
143    /// * `version_string` - A string slice representing the version (e.g., "1.2.3").
144    /// * `hash` - The `String` to use for the hash value.
145    ///
146    /// # Returns
147    /// A `Result` containing a new `VersionInformation` struct, or a `VersionInformationError`
148    /// if the version string cannot be parsed.
149    pub fn from_string_and_hash(
150        version_string: String,
151        hash: String,
152    ) -> Result<VersionInformation, VersionInformationError> {
153        // Split the "x.y.z" string into its parts, returning specific error on failure
154        let mut parts = version_string.split('.');
155        let major_str = match parts.next() {
156            Some(major_str) => major_str,
157            None => {
158                return Err(VersionInformationError::ParseErrorMajor(version_string));
159            }
160        };
161
162        let minor_str = match parts.next() {
163            Some(minor_str) => minor_str,
164            None => {
165                return Err(VersionInformationError::ParseErrorMinor(version_string));
166            }
167        };
168
169        // The patch/build number might have suffixes like "-alpha", so we take only the numeric part.
170        let build_str = match parts.next() {
171            Some(build_part) => match build_part.split('-').next() {
172                Some(build_str) => build_str,
173                None => {
174                    return Err(VersionInformationError::ParseErrorBuild(version_string));
175                }
176            },
177            None => {
178                return Err(VersionInformationError::ParseErrorBuild(version_string));
179            }
180        };
181
182        // Parse each part into an i32, returning a specific error on failure.
183        let major = major_str.parse::<i32>().map_err(|e| {
184            VersionInformationError::ConversionErrorMajor {
185                version_string: major_str.to_string(),
186                source: e,
187            }
188        })?;
189
190        let minor = minor_str.parse::<i32>().map_err(|e| {
191            VersionInformationError::ConversionErrorMinor {
192                version_string: minor_str.to_string(),
193                source: e,
194            }
195        })?;
196
197        let build = build_str.parse::<i32>().map_err(|e| {
198            VersionInformationError::ConversionErrorBuild {
199                version_string: build_str.to_string(),
200                source: e,
201            }
202        })?;
203
204        Ok(VersionInformation {
205            major,
206            minor,
207            build,
208            hash,
209        })
210    }
211}
212
213impl Display for VersionInformation {
214    /// Formats the `VersionInformation` for display.
215    ///
216    /// This implementation provides a human-readable, multi-line representation of the
217    /// version details, including major, minor, build numbers, and the executable hash.
218    ///
219    /// # Arguments
220    /// * `f` - A mutable reference to a `Formatter` where the output will be written.
221    ///
222    /// # Returns
223    /// An `Ok(())` on successful formatting.
224    ///
225    /// # Errors
226    /// Returns an `Err` containing a `std::fmt::Error` if an I/O error occurs
227    /// while writing to the formatter.
228    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
229        writeln!(f, "Version information:")?;
230        writeln!(f, "  Major: {}", self.major)?;
231        writeln!(f, "  Minor: {}", self.minor)?;
232        writeln!(f, "  Build: {}", self.build)?;
233        writeln!(f, "  Hash:  {}", self.hash)?;
234        Ok(())
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    fn dummy_hash() -> String {
243        "dummy_hash_for_testing".to_string()
244    }
245
246    #[test]
247    fn test_valid_version_string() {
248        let info =
249            VersionInformation::from_string_and_hash("1.2.3".to_string(), dummy_hash()).unwrap();
250        assert_eq!(info.major, 1);
251        assert_eq!(info.minor, 2);
252        assert_eq!(info.build, 3);
253    }
254
255    #[test]
256    fn test_version_string_with_prerelease_tag() {
257        let info =
258            VersionInformation::from_string_and_hash("0.4.5-beta.1".to_string(), dummy_hash())
259                .unwrap();
260        assert_eq!(info.major, 0);
261        assert_eq!(info.minor, 4);
262        assert_eq!(info.build, 5);
263    }
264
265    #[test]
266    fn test_invalid_major_version() {
267        let result = VersionInformation::from_string_and_hash("a.2.3".to_string(), dummy_hash());
268        assert!(matches!(
269            result,
270            Err(VersionInformationError::ConversionErrorMajor { .. })
271        ));
272    }
273
274    #[test]
275    fn test_invalid_minor_version() {
276        let result = VersionInformation::from_string_and_hash("1.a.3".to_string(), dummy_hash());
277        assert!(matches!(
278            result,
279            Err(VersionInformationError::ConversionErrorMinor { .. })
280        ));
281    }
282
283    #[test]
284    fn test_invalid_build_version() {
285        let result = VersionInformation::from_string_and_hash("1.2.x".to_string(), dummy_hash());
286        assert!(matches!(
287            result,
288            Err(VersionInformationError::ConversionErrorBuild { .. })
289        ));
290    }
291
292    #[test]
293    fn test_incomplete_version_string() {
294        let result = VersionInformation::from_string_and_hash("1.2".to_string(), dummy_hash());
295        assert!(matches!(
296            result,
297            Err(VersionInformationError::ParseErrorBuild { .. })
298        ));
299    }
300}