1mod meta {
2 pub const TAG_MASK: u8 = 0b11000000;
3 pub const VALUE_MASK: u8 = 0b00111111;
4 pub const MAX_VALUE: u8 = VALUE_MASK;
5
6 pub const RC: u8 = 0b01000000;
7 pub const ALPHA: u8 = 0b10000000;
8 pub const DEV_ALPHA: u8 = 0b11000000;
9}
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub struct CrateVersion {
45 pub major: u8,
46 pub minor: u8,
47 pub patch: u8,
48 pub meta: Option<Meta>,
49}
50
51impl Ord for CrateVersion {
52 #[inline]
53 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
54 let Self {
55 major,
56 minor,
57 patch,
58 meta: _,
59 } = self;
60
61 match major.cmp(&other.major) {
62 core::cmp::Ordering::Equal => {}
63 ord => return ord,
64 }
65 match minor.cmp(&other.minor) {
66 core::cmp::Ordering::Equal => {}
67 ord => return ord,
68 }
69 patch.cmp(&other.patch)
70 }
71}
72
73impl PartialOrd for CrateVersion {
74 #[inline]
75 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
76 Some(self.cmp(other))
77 }
78}
79
80impl CrateVersion {
81 pub const LOCAL: Self = Self::parse(env!("CARGO_PKG_VERSION"));
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
86pub enum Meta {
87 Rc(u8),
88 Alpha(u8),
89
90 DevAlpha {
92 alpha: u8,
93
94 commit: Option<&'static [u8]>,
106 },
107}
108
109impl Meta {
110 pub fn to_byte(self) -> u8 {
111 match self {
112 Self::Rc(value) => value | meta::RC,
113 Self::Alpha(value) => value | meta::ALPHA,
114
115 Self::DevAlpha { alpha, .. } => alpha | meta::DEV_ALPHA,
117 }
118 }
119
120 pub const fn from_byte(v: u8) -> Option<Self> {
121 let tag = v & meta::TAG_MASK;
122 let value = v & meta::VALUE_MASK;
123 match tag {
124 meta::RC => Some(Self::Rc(value)),
125 meta::ALPHA => Some(Self::Alpha(value)),
126 meta::DEV_ALPHA => Some(Self::DevAlpha {
127 alpha: value,
128 commit: None,
129 }),
130 _ => None,
131 }
132 }
133}
134
135const fn const_u8_slice_util(v: &[u8], start: Option<usize>, end: Option<usize>) -> &[u8] {
140 let (start, end) = match (start, end) {
141 (Some(start), Some(end)) => (start, end),
142 (Some(start), None) => (start, v.len()),
143 (None, Some(end)) => (0, end),
144 (None, None) => return v,
145 };
146
147 assert!(start <= v.len());
148 assert!(end <= v.len());
149 assert!(start <= end);
150
151 {
152 #![allow(unsafe_code)]
154
155 let ptr = v.as_ptr();
156 unsafe { std::slice::from_raw_parts(ptr.add(start), end - start) }
166 }
167}
168
169macro_rules! slice {
174 ($s:expr, .., $end:expr) => {
175 const_u8_slice_util($s, None, Some($end))
176 };
177 ($s:expr, $start:expr, ..) => {
178 const_u8_slice_util($s, Some($start), None)
179 };
180 ($s:expr, $start:expr, $end:expr) => {
181 const_u8_slice_util($s, Some($start), Some($end))
182 };
183}
184
185const fn equals(a: &[u8], b: &[u8]) -> bool {
186 if a.len() != b.len() {
187 return false;
188 }
189
190 let len = a.len();
191 let mut i = 0;
192 while i < len {
193 if a[i] != b[i] {
194 return false;
195 }
196 i += 1;
197 }
198 true
199}
200
201const fn split_at(s: &[u8], i: usize) -> (&[u8], &[u8]) {
202 (slice!(s, .., i), slice!(s, i, ..))
203}
204
205impl CrateVersion {
206 pub const fn new(major: u8, minor: u8, patch: u8) -> Self {
207 Self {
208 major,
209 minor,
210 patch,
211 meta: None,
212 }
213 }
214
215 pub fn is_release(&self) -> bool {
219 self.meta.is_none()
220 }
221
222 pub fn is_dev(&self) -> bool {
227 matches!(self.meta, Some(Meta::DevAlpha { .. }))
228 }
229
230 pub fn is_alpha(&self) -> bool {
232 matches!(self.meta, Some(Meta::Alpha(..) | Meta::DevAlpha { .. }))
233 }
234
235 pub fn is_rc(&self) -> bool {
237 matches!(self.meta, Some(Meta::Rc(..)))
238 }
239
240 pub fn from_bytes([major, minor, patch, meta]: [u8; 4]) -> Self {
242 Self {
243 major,
244 minor,
245 patch,
246 meta: Meta::from_byte(meta),
247 }
248 }
249
250 pub fn to_bytes(self) -> [u8; 4] {
252 [
253 self.major,
254 self.minor,
255 self.patch,
256 self.meta.map(Meta::to_byte).unwrap_or_default(),
257 ]
258 }
259
260 #[allow(clippy::unnested_or_patterns)]
261 pub fn is_compatible_with(self, other: Self) -> bool {
262 match (self.meta, other.meta) {
263 (Some(Meta::Rc(..)), Some(Meta::Rc(..)))
267 | (Some(Meta::Rc(..)), None)
268 | (None, Some(Meta::Rc(..))) => {}
269 (this, other) => {
270 if this != other {
271 return false;
273 }
274 }
275 }
276
277 if self.major == 0 {
278 (self.major, self.minor) == (other.major, other.minor)
280 } else {
281 self.major == other.major
283 }
284 }
285}
286
287impl CrateVersion {
288 pub const fn parse(version_string: &'static str) -> Self {
292 match Self::try_parse(version_string) {
293 Ok(version) => version,
294 Err(_err) => {
295 panic!("invalid version string")
298 }
299 }
300 }
301
302 pub const fn try_parse(version_string: &'static str) -> Result<Self, &'static str> {
306 const fn maybe(s: &[u8], c: u8) -> (bool, &[u8]) {
309 if !s.is_empty() && s[0] == c {
310 (true, slice!(s, 1, ..))
311 } else {
312 (false, s)
313 }
314 }
315
316 const fn maybe_token<'a>(s: &'a [u8], token: &[u8]) -> (bool, &'a [u8]) {
317 if s.len() < token.len() {
318 return (false, s);
319 }
320
321 let (left, right) = split_at(s, token.len());
322 if equals(left, token) {
323 (true, right)
324 } else {
325 (false, s)
326 }
327 }
328
329 macro_rules! eat {
330 ($s:ident, $c:expr, $msg:literal) => {{
331 if $s.is_empty() || $s[0] != $c {
332 return Err($msg);
333 }
334 slice!($s, 1, ..)
335 }};
336 }
337
338 macro_rules! eat_u8 {
339 ($s:ident, $msg:literal) => {{
340 if $s.is_empty() {
341 return Err($msg);
342 }
343
344 if $s.len() > 1 && $s[1].is_ascii_digit() {
345 if $s[0] == b'0' {
346 return Err("multi-digit number cannot start with zero");
347 }
348 }
349
350 let mut num = 0u64;
351 let mut i = 0;
352 while i < $s.len() && $s[i].is_ascii_digit() {
353 let digit = ($s[i] - b'0') as u64;
354 num = num * 10 + digit;
355 i += 1;
356 }
357
358 if num > u8::MAX as u64 {
359 return Err("digit cannot be larger than 255");
360 }
361 let num = num as u8;
362 let remainder = slice!($s, i, ..);
363
364 (num, remainder)
365 }};
366 }
367
368 let mut s = version_string.as_bytes();
369 let (major, minor, patch);
370 let mut meta = None;
371
372 (major, s) = eat_u8!(s, "expected major version number");
373 s = eat!(s, b'.', "expected `.` after major version number");
374 (minor, s) = eat_u8!(s, "expected minor version number");
375 s = eat!(s, b'.', "expected `.` after minor version number");
376 (patch, s) = eat_u8!(s, "expected patch version number");
377
378 if let (true, remainder) = maybe(s, b'-') {
379 s = remainder;
380
381 let build;
382 if let (true, remainder) = maybe_token(s, b"alpha") {
383 s = eat!(remainder, b'.', "expected `.` after `-alpha`");
384 (build, s) = eat_u8!(s, "expected digit after `-alpha.`");
385 if build > meta::MAX_VALUE {
386 return Err("`-alpha` build number is larger than 63");
387 }
388 meta = Some(Meta::Alpha(build));
389 } else if let (true, remainder) = maybe_token(s, b"rc") {
390 s = eat!(remainder, b'.', "expected `.` after `-rc`");
391 (build, s) = eat_u8!(s, "expected digit after `-rc.`");
392 if build > meta::MAX_VALUE {
393 return Err("`-rc` build number is larger than 63");
394 }
395 meta = Some(Meta::Rc(build));
396 } else {
397 return Err("expected `alpha` or `rc` after `-`");
398 }
399 }
400
401 if let (true, remainder) = maybe(s, b'+') {
402 s = remainder;
403 match meta {
404 Some(Meta::Alpha(build)) => {
405 if let (true, remainder) = maybe_token(s, b"dev") {
406 s = remainder;
407 meta = Some(Meta::DevAlpha {
408 alpha: build,
409 commit: None,
410 });
411 } else if s.is_empty() {
412 return Err("expected `dev` after `+`");
413 } else {
414 let commit_hash = s;
415 s = &[];
416 meta = Some(Meta::DevAlpha {
417 alpha: build,
418 commit: Some(commit_hash),
419 });
420 }
421 }
422 Some(..) => return Err("unexpected `-rc` with `+dev`"),
423 None => return Err("unexpected `+dev` without `-alpha`"),
424 }
425 };
426
427 if !s.is_empty() {
428 return Err("expected end of string");
429 }
430
431 Ok(Self {
432 major,
433 minor,
434 patch,
435 meta,
436 })
437 }
438}
439
440impl std::fmt::Display for Meta {
441 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442 match self {
443 Self::Rc(build) => write!(f, "-rc.{build}"),
444 Self::Alpha(build) => write!(f, "-alpha.{build}"),
445 Self::DevAlpha { alpha, commit } => {
446 if let Some(commit) = commit.and_then(|s| std::str::from_utf8(s).ok()) {
447 write!(f, "-alpha.{alpha}+{commit}")
448 } else {
449 write!(f, "-alpha.{alpha}+dev")
450 }
451 }
452 }
453 }
454}
455
456impl std::fmt::Display for CrateVersion {
457 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458 let Self {
459 major,
460 minor,
461 patch,
462 meta,
463 } = *self;
464
465 write!(f, "{major}.{minor}.{patch}")?;
466 if let Some(meta) = meta {
467 write!(f, "{meta}")?;
468 }
469 Ok(())
470 }
471}
472
473impl re_byte_size::SizeBytes for CrateVersion {
474 #[inline]
475 fn heap_size_bytes(&self) -> u64 {
476 0
477 }
478}
479
480#[test]
481fn test_parse_version() {
482 macro_rules! assert_parse_ok {
483 ($input:literal, $expected:expr) => {
484 assert_eq!(CrateVersion::try_parse($input), Ok($expected))
485 };
486 }
487
488 assert_parse_ok!("0.2.0", CrateVersion::new(0, 2, 0));
489 assert_parse_ok!("0.2.0", CrateVersion::new(0, 2, 0));
490 assert_parse_ok!("1.2.3", CrateVersion::new(1, 2, 3));
491 assert_parse_ok!("12.23.24", CrateVersion::new(12, 23, 24));
492 assert_parse_ok!(
493 "12.23.24-rc.63",
494 CrateVersion {
495 major: 12,
496 minor: 23,
497 patch: 24,
498 meta: Some(Meta::Rc(63)),
499 }
500 );
501 assert_parse_ok!(
502 "12.23.24-alpha.63",
503 CrateVersion {
504 major: 12,
505 minor: 23,
506 patch: 24,
507 meta: Some(Meta::Alpha(63)),
508 }
509 );
510 assert_parse_ok!(
511 "12.23.24-alpha.63+dev",
512 CrateVersion {
513 major: 12,
514 minor: 23,
515 patch: 24,
516 meta: Some(Meta::DevAlpha {
517 alpha: 63,
518 commit: None
519 }),
520 }
521 );
522 assert_parse_ok!(
524 "12.23.24-alpha.63+aab0b4e",
525 CrateVersion {
526 major: 12,
527 minor: 23,
528 patch: 24,
529 meta: Some(Meta::DevAlpha {
530 alpha: 63,
531 commit: Some(b"aab0b4e")
532 }),
533 }
534 );
535}
536
537#[test]
538fn test_format_parse_roundtrip() {
539 for version in [
540 "0.2.0",
541 "1.2.3",
542 "12.23.24",
543 "12.23.24-rc.63",
544 "12.23.24-alpha.63",
545 "12.23.24-alpha.63+dev",
546 "12.23.24-alpha.63+aab0b4e",
547 ] {
548 assert_eq!(CrateVersion::parse(version).to_string(), version);
549 }
550}
551
552#[test]
553fn test_format_parse_roundtrip_bytes() {
554 for version in [
555 "0.2.0",
556 "1.2.3",
557 "12.23.24",
558 "12.23.24-rc.63",
559 "12.23.24-alpha.63",
560 "12.23.24-alpha.63+dev",
561 ] {
563 let version = CrateVersion::parse(version);
564 let bytes = version.to_bytes();
565 assert_eq!(CrateVersion::from_bytes(bytes), version);
566 }
567}
568
569#[test]
570fn test_compatibility() {
571 fn are_compatible(a: &'static str, b: &'static str) -> bool {
572 CrateVersion::parse(a).is_compatible_with(CrateVersion::parse(b))
573 }
574
575 assert!(are_compatible("0.2.0", "0.2.0"));
576 assert!(are_compatible("0.2.0", "0.2.1"));
577 assert!(are_compatible("1.2.0", "1.3.0"));
578 assert!(
579 !are_compatible("0.2.0", "1.2.0"),
580 "Different major versions are incompatible"
581 );
582 assert!(
583 !are_compatible("0.2.0", "0.3.0"),
584 "Different minor versions are incompatible"
585 );
586 assert!(are_compatible("0.2.0-alpha.0", "0.2.0-alpha.0"));
587 assert!(are_compatible("0.2.0-rc.0", "0.2.0-rc.0"));
588 assert!(
589 !are_compatible("0.2.0-rc.0", "0.2.0-alpha.0"),
590 "Rc and Alpha are incompatible"
591 );
592 assert!(
593 !are_compatible("0.2.0-rc.0", "0.2.0-alpha.0+dev"),
594 "Rc and Dev are incompatible"
595 );
596 assert!(
597 !are_compatible("0.2.0-alpha.0", "0.2.0-alpha.0+dev"),
598 "Alpha and Dev are incompatible"
599 );
600 assert!(
601 !are_compatible("0.2.0-alpha.0", "0.2.0-alpha.1"),
602 "Different alpha builds are always incompatible"
603 );
604 assert!(
605 are_compatible("0.2.0-rc.0", "0.2.0-rc.1"),
606 "Different rc builds are always compatible"
607 );
608 assert!(
609 are_compatible("0.2.0-rc.0", "0.2.0"),
610 "rc build is compatible with the finalized version"
611 );
612 assert!(
613 are_compatible("0.2.0", "0.2.1-rc.0"),
614 "rc build is compatible by patch version"
615 );
616}
617
618#[test]
619fn test_bad_parse() {
620 macro_rules! assert_parse_err {
621 ($input:literal, $expected:literal) => {
622 assert_eq!(CrateVersion::try_parse($input), Err($expected))
623 };
624 }
625
626 assert_parse_err!("10", "expected `.` after major version number");
627 assert_parse_err!("10.", "expected minor version number");
628 assert_parse_err!("10.0", "expected `.` after minor version number");
629 assert_parse_err!("10.0.", "expected patch version number");
630 assert_parse_err!("10.0.2-", "expected `alpha` or `rc` after `-`");
631 assert_parse_err!("10.0.2-alpha", "expected `.` after `-alpha`");
632 assert_parse_err!("10.0.2-alpha.", "expected digit after `-alpha.`");
633 assert_parse_err!(
634 "10.0.2-alpha.255",
635 "`-alpha` build number is larger than 63"
636 );
637 assert_parse_err!("10.0.2-rc", "expected `.` after `-rc`");
638 assert_parse_err!("10.0.2-rc.", "expected digit after `-rc.`");
639 assert_parse_err!("10.0.2-rc.255", "`-rc` build number is larger than 63");
640 assert_parse_err!("10.0.2-alpha.1+", "expected `dev` after `+`");
641 assert_parse_err!("10.0.2-rc.1+dev", "unexpected `-rc` with `+dev`");
642 assert_parse_err!("10.0.2+dev", "unexpected `+dev` without `-alpha`");
643 assert_parse_err!(
644 "10.0.2-alpha.1+dev extra_characters",
645 "expected end of string"
646 );
647 assert_parse_err!("256.0.2-alpha.1+dev", "digit cannot be larger than 255");
648 assert_parse_err!("10.256.2-alpha.1+dev", "digit cannot be larger than 255");
649 assert_parse_err!("10.0.256-alpha.1+dev", "digit cannot be larger than 255");
650 assert_parse_err!("10.0.2-alpha.256+dev", "digit cannot be larger than 255");
651 assert_parse_err!(
652 "01.0.2-alpha.256+dev",
653 "multi-digit number cannot start with zero"
654 );
655 assert_parse_err!(
656 "10.01.2-alpha.256+dev",
657 "multi-digit number cannot start with zero"
658 );
659 assert_parse_err!(
660 "10.0.01-alpha.256+dev",
661 "multi-digit number cannot start with zero"
662 );
663 assert_parse_err!(
664 "10.0.2-alpha.01+dev",
665 "multi-digit number cannot start with zero"
666 );
667}