diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2c6ae7 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +Contents +======== +- [Introduction](#introduction) +- [Usage](#usage) +- [Internals](#internals) + +## Introduction +This crate aims to provide methods for parsing, formatting, comparing and ordering +version numbers in most of the common formats in use today. In a perfect world, all +software be it free or non-free would use semantic versioning and strictly stick to +the rules governing semver. In the real world version numbers may be given using 1, +2, 3 or 4 digits and may or may not be consistent between releases. One release may +take the form "2.4" and be followed by a "2.4.1" release. A robust library for parsing +and comparing versions must be able to compare the two for ordering purposes in +order to determine that "2.4.1" is a newer patch release in the "2.4" series. In +addition, Pre-release numbering must be accounted for and the library must be able to +determine that an alpha is older than a beta, a full release newer than a release +candidate. + +This library provides a `Version` struct which can handle 1 to 4 numerical fields for +semver-like versioning plus alpha, beta or rc prereleases, plus git revisions. It also +tracks machine architecture, and will return -ne if the version numbers match but not +the arch when doing comparisons. + +## Usage +For the time being, this crate is not yet release on crates.io. It can be used from +git in your Cargo.toml file like so: +```Toml +version = { git = "https://codeberg.org/jeang3nie/version-rs" } +``` +### Parsing and formatting +`Version` implements `Display` and `FromStr`. +``` +use version::{prelude::VersionError, Version}; + +fn main() -> Result::<(), VersionError> { + let vstr = "2.0.3_alpha1-aarch64"; + let v: Version = vstr.parse()?; + assert_eq!(v.to_string(), vstr); + Ok(()) +} +``` +### Comparison +`Version` implements `PartialOrd` and `PartialEq` +``` +use version::{prelude::VersionError, Version}; + +fn main() -> Result<(), VersionError> { + let va: Version = "1.0-x86_64".parse()?; + let vb: Version = "1.0.0-x86_64".parse()?; + let vc: Version = "1.0_rc4-x86_64".parse()?; + let vd: Version = "1.0-riscv64".parse()?; + assert_eq!(va, vb); + assert!(vb > vc); + assert_ne!(va, vd); + Ok(()) +} +``` +### Git +Git revisions may be used, but may only be compared with other Git revisions. Git +revisions are ordered by date/time and formatted as Unix timestamps. +``` +use version::{prelude::VersionError, Version}; + +fn main() -> Result<(), VersionError> { + let va: Version = "git_r2d2xxx.1705881508-i486".parse()?; + let vb: Version = "git_c3p0xxx.1705881612-i486".parse()?; + assert!(va < vb); + Ok(()) +} +``` + +## Internals +When comparing version numbers for equality or ordering, the semver-like versions +are first encoded into a single `u64`, allowing for an easy comparison using the +`==`, `<` and `>` operators. This is done using some simple bit shifting logic. +The twelve least significant bits, or LSB, are given for any numerical component to +the prerelease number. The next four bits are used as bitflags representing the +type of prerelease, which may be `PreRelease::None` (which gets the highest bit in +the set). + +| bits | 15 | 14 | 13 | 12 | 0-11 | +| ---- | --- | --- | --- | --- | ---- | +| use | PreRelease::None | PreRelease::Rc | PreRelease::Beta | PreRelease::Alpha | Numerical component | + +This gives an upper limit of 2^12 or 4096 for the numerical component, which is more +than adequate. By placing the flags in this way, alpha releases will always be lower +than beta, which will be lower than release candidates, which will be lower than no +pre-release versions. + +The fields major, minor, patch and build each get 12 of the remaining bits, arranged +so that major is the most significant followed by minor, patch and build. + +| bits | 52-63 | 40-51 | 28-39 | 16-27 | +| ---- | ----- | ----- | ----- | ----- | +| use | major | minor | patch | build | + +In this way, version "1.0" will be equal to "1.0.0" and less than "1.0.1" in a simple +comparison operation, without requiring complex match statements. + +In this way the traits PartialEq, Eq, PartialOrd and Ord can be implemented in a +straightforward and concise manner for the various versioning schemes. diff --git a/src/extended.rs b/src/extended.rs index 6d63d34..dbd5d01 100644 --- a/src/extended.rs +++ b/src/extended.rs @@ -66,7 +66,7 @@ impl From for u64 { let major = u64::from(value.major) << 52; let minor = u64::from(value.minor) << 40; let patch = u64::from(value.patch) << 28; - let build = u64::from(value.build) << 14; + let build = u64::from(value.build) << 16; let pre = u64::from(u16::from(value.pre)); major | minor | patch | build | pre } @@ -82,8 +82,8 @@ impl TryFrom for Extended { let minor = (value & mask) >> 40; mask = 0o7777 << 28; let patch = (value & mask) >> 28; - mask = 0o7777 << 14; - let build = (value & mask) >> 14; + mask = 0o7777 << 16; + let build = (value & mask) >> 16; mask = 0o37777; let p = u16::try_from(value & mask)?; let pre: PreRelease = p.try_into()?; diff --git a/src/gitrev.rs b/src/gitrev.rs index 5a168b4..5d0da71 100644 --- a/src/gitrev.rs +++ b/src/gitrev.rs @@ -15,6 +15,23 @@ use { /// that a Git revision can only be compared against another Git revision, making it /// impossible to properly order updates between a proper `SemVer` type release and a\ /// Git revision. +/// ### Notes on display formatting +/// The hash as stored in this struct should be the short form revision hash (the first +/// 7 characters of the full hash). The full date and time information is saved minus +/// any fractional seconds. When displaying, the date and time should be converted to a +/// Unix timestamp. +/// ### Parsing from a string +/// In order to parse this struct from a string, two fields `hash` and `datetime` should +/// be separated by a period character '.' and the date/time take the form of a Unix +/// timestamp. This information can be retrieved from git for a given commit using the +/// following git commandline: +/// ```Sh +/// git show --pretty=format:"%h.%at" | head -n 1 +/// ``` +/// The resulting string can then be parsed using the `FromStr` trait, which provides +/// both `from_str` and `parse`. +/// ### Comparison - ordering +/// Git revisions are ordered by date/time #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct GitRev { /// the short revision hash diff --git a/src/lib.rs b/src/lib.rs index 7c23730..1d50456 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ #![warn(clippy::all, clippy::pedantic)] +#![doc = include_str!("../README.md")] + mod arch; mod error; mod extended; diff --git a/src/prerelease.rs b/src/prerelease.rs index 9d9af1c..40e4e2c 100644 --- a/src/prerelease.rs +++ b/src/prerelease.rs @@ -1,11 +1,9 @@ use { - crate::error::Error, + crate::{error::Error, MAX_U12}, serde::{Deserialize, Serialize}, std::{cmp, convert::Into, fmt, num::NonZeroU16, str}, }; -static MAX_U10: u16 = 1024; - #[derive(Clone, Copy, Debug, Deserialize, Serialize)] /// A specification for non-production releases pub enum PreRelease { @@ -47,7 +45,7 @@ impl PreRelease { match self { Self::Alpha(n) | Self::Beta(n) | Self::RC(n) => match n { Some(num) => { - if u16::from(*num) >= MAX_U10 { + if u16::from(*num) >= MAX_U12 { return Err(Error::Range); } *num = num.saturating_add(1); @@ -155,20 +153,20 @@ impl From for u16 { match value { PreRelease::Alpha(Some(v)) => { let v = u16::from(v) & mask; - v | 0o2_000 + v | 0o10_000 } PreRelease::Alpha(None) => 0o2_000, PreRelease::Beta(Some(v)) => { let v = u16::from(v) & mask; - v | 0o4_000 + v | 0o20_000 } PreRelease::Beta(None) => 0o4_000, PreRelease::RC(Some(v)) => { let v = u16::from(v) & mask; - v | 0o10_000 + v | 0o40_000 } PreRelease::RC(None) => 0o10_000, - PreRelease::None => 0o20_000, + PreRelease::None => 0o100_000, } } } @@ -181,19 +179,19 @@ impl TryFrom for PreRelease { let v = value & mask; let flag = value & !mask; match flag { - 0o2_000 => { + 0o10_000 => { let v = if v > 0 { Some(v.try_into()?) } else { None }; Ok(Self::Alpha(v)) } - 0o4_000 => { + 0o20_000 => { let v = if v > 0 { Some(v.try_into()?) } else { None }; Ok(Self::Beta(v)) } - 0o10_000 => { + 0o40_000 => { let v = if v > 0 { Some(v.try_into()?) } else { None }; Ok(Self::RC(v)) } - 0o20_000 => Ok(Self::None), + 0o100_000 => Ok(Self::None), _ => Err(Error::FromUint), } }