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 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 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, 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, 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}