xous_semver/
lib.rs

1#![no_std]
2
3use core::{
4    cmp::Ordering,
5    convert::{
6        From,
7        TryInto,
8    },
9    fmt::Formatter,
10    str::FromStr,
11};
12
13#[cfg(any(feature = "std", test))]
14extern crate std;
15
16#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
17pub struct SemVer {
18    pub maj:    u16,
19    pub min:    u16,
20    pub rev:    u16,
21    pub extra:  u16,
22    pub commit: Option<u32>,
23}
24
25impl core::fmt::Display for SemVer {
26    #[inline]
27    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
28        write!(f, "v{}.{}.{}-{}", self.maj, self.min, self.rev, self.extra)?;
29
30        if let Some(commit) = self.commit {
31            write!(f, "-g{commit:x}")?;
32        }
33
34        Ok(())
35    }
36}
37
38impl SemVer {
39    #[cfg(feature = "std")]
40    pub fn from_git() -> Result<Self, &'static str> {
41        let output = std::process::Command::new("git")
42            .args(&["describe"])
43            .output()
44            .map_err(|_| "failed to execute git")?;
45
46        let gitver = output.stdout;
47        let semver = core::str::from_utf8(&gitver).map_err(|_| "semver was not utf-8")?;
48
49        FromStr::from_str(semver)
50    }
51}
52
53impl FromStr for SemVer {
54    type Err = &'static str;
55
56    fn from_str(revstr: &str) -> Result<Self, &'static str> {
57        let revstr = revstr.trim_end();
58        let revstr = revstr.strip_prefix('v').unwrap_or(revstr);
59
60        #[inline]
61        fn parse_ver_int(s: &str) -> Result<u16, &'static str> {
62            u16::from_str(s).map_err(|_| "failed to parse version number as u16")
63        }
64
65        let (maj, rest): (_, &str) = revstr.split_once('.').ok_or_else(|| "no major version")?;
66        let maj = parse_ver_int(maj)?;
67
68        let (min, rest): (_, &str) = rest.split_once('.').ok_or_else(|| "no minor version")?;
69        let min = parse_ver_int(min)?;
70
71        let patch = rest.split_once('-');
72        let (patch, rest) = if let Some((patch, rest)) = patch {
73            (patch, rest)
74        } else {
75            (rest, "")
76        };
77        let patch = parse_ver_int(patch)?;
78
79        if rest.is_empty() {
80            return Ok(SemVer {
81                maj,
82                min,
83                rev: patch,
84                extra: 0,
85                commit: None,
86            });
87        }
88
89        let (extra, commit) = if let Some((extra, commit)) = rest.split_once('-') {
90            if !commit.starts_with('g') {
91                return Err("invalid commit format (no 'g' prefix)");
92            }
93
94            (parse_ver_int(extra)?, Some(&commit[1..commit.len().min(9)]))
95        } else {
96            if let Some(commit) = rest.strip_prefix('g') {
97                (0, Some(commit))
98            } else {
99                (parse_ver_int(rest)?, None)
100            }
101        };
102
103        let commit = commit
104            .map(|commit| u32::from_str_radix(commit, 16).map_err(|_| "parsing commit"))
105            .transpose()?;
106
107        Ok(SemVer {
108            maj,
109            min,
110            rev: patch,
111            extra,
112            commit,
113        })
114    }
115}
116
117impl From<[u8; 16]> for SemVer {
118    fn from(bytes: [u8; 16]) -> SemVer {
119        // we use a whole word to store the `Option` flag, just to keep alignment at word alignment.
120        let has_commit = u32::from_le_bytes(bytes[12..16].try_into().unwrap());
121        SemVer {
122            maj:    u16::from_le_bytes(bytes[0..2].try_into().unwrap()),
123            min:    u16::from_le_bytes(bytes[2..4].try_into().unwrap()),
124            rev:    u16::from_le_bytes(bytes[4..6].try_into().unwrap()),
125            extra:  u16::from_le_bytes(bytes[6..8].try_into().unwrap()),
126            commit: if has_commit != 0 {
127                Some(u32::from_le_bytes(bytes[8..12].try_into().unwrap()))
128            } else {
129                None
130            },
131        }
132    }
133}
134
135impl From<&[u8; 16]> for SemVer {
136    #[inline]
137    fn from(bytes: &[u8; 16]) -> Self {
138        // we use a whole word to store the `Option` flag, just to keep alignment at word alignment.
139        let has_commit = u32::from_le_bytes(bytes[12..16].try_into().unwrap());
140        SemVer {
141            maj:    u16::from_le_bytes(bytes[0..2].try_into().unwrap()),
142            min:    u16::from_le_bytes(bytes[2..4].try_into().unwrap()),
143            rev:    u16::from_le_bytes(bytes[4..6].try_into().unwrap()),
144            extra:  u16::from_le_bytes(bytes[6..8].try_into().unwrap()),
145            commit: if has_commit != 0 {
146                Some(u32::from_le_bytes(bytes[8..12].try_into().unwrap()))
147            } else {
148                None
149            },
150        }
151    }
152}
153
154impl From<SemVer> for [u8; 16] {
155    fn from(value: SemVer) -> Self {
156        let mut ser = [0u8; 16];
157        ser[0..2].copy_from_slice(&value.maj.to_le_bytes());
158        ser[2..4].copy_from_slice(&value.min.to_le_bytes());
159        ser[4..6].copy_from_slice(&value.rev.to_le_bytes());
160        ser[6..8].copy_from_slice(&value.extra.to_le_bytes());
161        ser[8..12].copy_from_slice(&value.commit.unwrap_or(0).to_le_bytes());
162        ser[12..16].copy_from_slice(
163            &(if value.commit.is_some() {
164                1u32
165            } else {
166                0u32
167            })
168            .to_le_bytes(),
169        );
170        ser
171    }
172}
173
174impl From<&SemVer> for [u8; 16] {
175    fn from(value: &SemVer) -> Self {
176        let mut ser = [0u8; 16];
177        ser[0..2].copy_from_slice(&value.maj.to_le_bytes());
178        ser[2..4].copy_from_slice(&value.min.to_le_bytes());
179        ser[4..6].copy_from_slice(&value.rev.to_le_bytes());
180        ser[6..8].copy_from_slice(&value.extra.to_le_bytes());
181        ser[8..12].copy_from_slice(&value.commit.unwrap_or(0).to_le_bytes());
182        ser[12..16].copy_from_slice(
183            &(if value.commit.is_some() {
184                1u32
185            } else {
186                0u32
187            })
188            .to_le_bytes(),
189        );
190        ser
191    }
192}
193
194impl PartialOrd for SemVer {
195    #[inline]
196    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
197        Some(self.cmp(other))
198    }
199}
200
201impl Ord for SemVer {
202    fn cmp(&self, other: &Self) -> Ordering {
203        self.maj
204            .cmp(&other.maj)
205            .then(self.min.cmp(&other.min))
206            .then(self.rev.cmp(&other.rev))
207            .then(self.extra.cmp(&other.extra))
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use std::string::ToString;
214
215    use super::*;
216
217    #[cfg(feature = "std")]
218    #[test]
219    fn test_gitver() {
220        let gitver = SemVer::from_git().unwrap();
221        std::println!("{:?}", gitver);
222    }
223
224    #[test]
225    fn test_strver() {
226        assert_eq!(
227            SemVer::from_str("v0.9.8-760-gabcd1234"),
228            Ok(SemVer {
229                maj:    0,
230                min:    9,
231                rev:    8,
232                extra:  760,
233                commit: Some(0xabcd1234),
234            })
235        );
236        assert_eq!(
237            SemVer::from_str("v0.9.8-760"),
238            Ok(SemVer {
239                maj:    0,
240                min:    9,
241                rev:    8,
242                extra:  760,
243                commit: None,
244            })
245        );
246        assert_eq!(
247            SemVer::from_str("v0.9.8-gabcd1234"),
248            Ok(SemVer {
249                maj:    0,
250                min:    9,
251                rev:    8,
252                extra:  0,
253                commit: Some(0xabcd1234),
254            })
255        );
256        let bytes: [u8; 16] = SemVer::from_str("v0.9.8-760-gabcd1234").unwrap().into();
257        assert_eq!(bytes, [0, 0, 9, 0, 8, 0, 248, 2, 0x34, 0x12, 0xcd, 0xab, 0x01, 0, 0, 0]);
258        let bytes: [u8; 16] = SemVer::from_str("v0.9.8-760").unwrap().into();
259        assert_eq!(bytes, [0, 0, 9, 0, 8, 0, 248, 2, 0, 0, 0, 0, 0x00, 0, 0, 0]);
260        let bytes: [u8; 16] = SemVer::from_str("v0.9.8-gabcd1234").unwrap().into();
261        assert_eq!(bytes, [0, 0, 9, 0, 8, 0, 0, 0, 0x34, 0x12, 0xcd, 0xab, 0x01, 0, 0, 0]);
262        let bytes: [u8; 16] = SemVer::from_str("v0.9.8").unwrap().into();
263        assert_eq!(bytes, [0, 0, 9, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0x0, 0, 0, 0]);
264        let bytes = [0, 0, 9, 0, 8, 0, 248, 2, 0x34, 0x12, 0xcd, 0xab, 0x01, 0, 0, 0];
265        assert_eq!(SemVer::from_str("v0.9.8-760-gabcd1234").unwrap(), SemVer::from(bytes));
266        let bytes = [
267            0, 0, 9, 0, 8, 0, 248, 2, 0x34, 0x12, 0xcd,
268            0xab, // these values should be ignored
269            0x00, 0, 0, 0,
270        ];
271        assert_eq!(SemVer::from_str("v0.9.8-760").unwrap(), SemVer::from(bytes));
272        assert!(
273            SemVer::from_str("v0.9.8-760-gabcd1234").unwrap()
274                < SemVer::from_str("v0.9.8-761-g0123456").unwrap()
275        );
276        assert!(
277            SemVer::from_str("v0.9.8-760-gabcd1234").unwrap()
278                < SemVer::from_str("v0.9.9-2").unwrap()
279        );
280        assert!(
281            SemVer::from_str("v0.9.8-760-gabcd1234").unwrap() < SemVer::from_str("v1.0.0").unwrap()
282        );
283        assert!(
284            SemVer::from_str("v0.9.8-760-gabcd1234").unwrap()
285                != SemVer::from_str("v0.9.8-760").unwrap()
286        );
287        assert!(
288            SemVer::from_str("v0.9.8-760-gabcd1234").unwrap()
289                != SemVer::from_str("v0.9.8-760-g1234").unwrap()
290        );
291        assert!(
292            SemVer::from_str("v0.9.8-760-gabcd1234").unwrap()
293                == SemVer::from_str("v0.9.8-760-gabcd1234").unwrap()
294        );
295        let sv = Some(SemVer::from_str("v0.9.8-760-gabcd1234").unwrap());
296        let bytes: [u8; 16] = if let Some(svb) = sv {
297            svb.into()
298        } else {
299            [0u8; 16]
300        };
301        assert_eq!(bytes, [0, 0, 9, 0, 8, 0, 248, 2, 0x34, 0x12, 0xcd, 0xab, 0x01, 0, 0, 0]);
302        let bytes = [
303            0, 0, 9, 0, 8, 0, 248, 2, 0x34, 0x12, 0xcd,
304            0xab, // these values should be ignored
305            0x00, 0, 0, 0,
306        ];
307        assert_eq!(SemVer::from_str("v0.9.8-760").unwrap(), SemVer::from(bytes));
308        assert_eq!(
309            SemVer {
310                maj:    0,
311                min:    9,
312                rev:    8,
313                extra:  42,
314                commit: None,
315            }
316            .to_string(),
317            "v0.9.8-42".to_string()
318        );
319        assert_eq!(
320            SemVer {
321                maj:    0,
322                min:    9,
323                rev:    8,
324                extra:  42,
325                commit: Some(0x123abc),
326            }
327            .to_string(),
328            "v0.9.8-42-g123abc".to_string()
329        );
330    }
331}