re_build_info/
crate_version.rs

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/// The version of a Rerun crate.
12///
13/// Sub-set of semver supporting `major.minor.patch-{alpha,rc}.N+dev`.
14///
15/// The string value of build metadata is not preserved.
16///
17/// Examples: `1.2.3`, `1.2.3-alpha.4`, `1.2.3-alpha.1+dev`.
18///
19/// `-alpha.N+dev` versions are used for local or CI builds.
20/// `-alpha.N` versions are used for weekly releases.
21/// `-rc.N` versions are used for release candidates as we're preparing for a full release.
22///
23/// The version numbers (`N`) aren't allowed to be very large (current max: 63).
24/// This limited subset is chosen so that we can encode the version in 32 bits
25/// in our `.rrd` files and on the wire.
26///
27/// Here is the current binary format:
28/// ```text,ignore
29/// major    minor    patch    meta
30/// 00000000 00000000 00000000 00NNNNNN
31///                            ▲▲▲    ▲
32///                            ││└─┬──┘
33///                            ││  └─ N
34///                            │└─ rc/dev
35///                            └─ alpha
36/// ```
37///
38/// The valid bit patterns for `meta` are:
39/// - `10NNNNNN` -> `-alpha.N`
40/// - `11NNNNNN` -> `-alpha.N+dev`
41/// - `01NNNNNN` -> `-rc.N`
42/// - `00000000` -> none of the above
43#[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    /// `0.19.1-alpha.2+dev` or `0.19.1-alpha.2+aab0b4e`
91    DevAlpha {
92        alpha: u8,
93
94        /// The commit hash, if known
95        ///
96        /// `None` corresponds to `+dev`.
97        ///
98        /// In order to support compile-time parsing of versions strings
99        /// this needs to be `&'static` and `[u8]` instead of `String`.
100        /// But in practice this is guaranteed to be valid UTF-8.
101        ///
102        /// The commit hash is NOT sent over the wire,
103        /// so `0.19.1-alpha.2+aab0b4e` will end up as `0.19.1-alpha.2+dev`
104        /// on the other end.
105        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            // We ignore the commit hash, if any
116            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
135/// Helper function to slice slices in a `const` context.
136/// Instead of using this directly, use the `slice` macro.
137///
138/// This is equivalent to `v[start..end]`.
139const 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        // The only reason we do this is to allow slicing in `const` functions.
153        #![allow(unsafe_code)]
154
155        let ptr = v.as_ptr();
156        // SAFETY:
157        // - the read is valid, because the following is true:
158        //   - `ptr` is valid for reads of `len` elements, because it is taken from a valid slice.
159        //     this means it is already guaranteed to be non-null and properly aligned, and the
160        //     entire length of the slice is contained within a single allocated object.
161        //   - `start <= len && end <= len && start <= end`
162        // - the returned slice appears to be a shared borrow from `v`,
163        //   so the borrow checker will ensure users will not mutate `v`
164        //   until this slice is dropped.
165        unsafe { std::slice::from_raw_parts(ptr.add(start), end - start) }
166    }
167}
168
169/// Slice `s` by some `start` and `end` bounds.
170///
171/// This is equivalent to doing `s[start..end]`,
172/// but works in a `const` context.
173macro_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    /// True is this version has no metadata at all (rc, dev, alpha, etc).
216    ///
217    /// I.e. it's an actual, final release.
218    pub fn is_release(&self) -> bool {
219        self.meta.is_none()
220    }
221
222    /// Whether or not this build has a `+dev` suffix.
223    ///
224    /// This is used to identify builds which are not explicit releases,
225    /// such as local builds and CI builds for every commit.
226    pub fn is_dev(&self) -> bool {
227        matches!(self.meta, Some(Meta::DevAlpha { .. }))
228    }
229
230    /// Whether or not this is an alpha version (`-alpha.N` or `-alpha.N+dev`).
231    pub fn is_alpha(&self) -> bool {
232        matches!(self.meta, Some(Meta::Alpha(..) | Meta::DevAlpha { .. }))
233    }
234
235    /// Whether or not this is a release candidate (`-rc.N`).
236    pub fn is_rc(&self) -> bool {
237        matches!(self.meta, Some(Meta::Rc(..)))
238    }
239
240    /// From a compact 32-bit representation crated with [`Self::to_bytes`].
241    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    /// A compact 32-bit representation. See also [`Self::from_bytes`].
251    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            // release candidates are always compatible with each other
264            // and their finalized version:
265            //   1.0.0-rc.1 == 1.0.0-rc.2 == 1.0.0
266            (Some(Meta::Rc(..)), Some(Meta::Rc(..)))
267            | (Some(Meta::Rc(..)), None)
268            | (None, Some(Meta::Rc(..))) => {}
269            (this, other) => {
270                if this != other {
271                    // Alphas can contain breaking changes
272                    return false;
273                }
274            }
275        }
276
277        if self.major == 0 {
278            // before 1.0.0 we break compatibility using the minor:
279            (self.major, self.minor) == (other.major, other.minor)
280        } else {
281            // major version is the only breaking change:
282            self.major == other.major
283        }
284    }
285}
286
287impl CrateVersion {
288    /// Parse a version string according to our subset of semver.
289    ///
290    /// See [`CrateVersion`] for more information.
291    pub const fn parse(version_string: &'static str) -> Self {
292        match Self::try_parse(version_string) {
293            Ok(version) => version,
294            Err(_err) => {
295                // We previously used const_panic to concatenate the actual version but it crashed
296                // the 1.72.0 linker on mac :/
297                panic!("invalid version string")
298            }
299        }
300    }
301
302    /// Parse a version string according to our subset of semver.
303    ///
304    /// See [`CrateVersion`] for more information.
305    pub const fn try_parse(version_string: &'static str) -> Result<Self, &'static str> {
306        // Note that this is a const function, which means we are extremely limited in what we can do!
307
308        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    // We use commit hash suffixes in some cases:
523    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        // "12.23.24-alpha.63+aab0b4e", // we don't serialize commit hashes!
562    ] {
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}