Add README, give prerelease numbers 12 bits rather than 10

This commit is contained in:
Nathan Fisher 2024-01-22 01:10:33 -05:00
parent ab572849c6
commit 12ac9a84ce
5 changed files with 134 additions and 15 deletions

102
README.md Normal file
View File

@ -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.

View File

@ -66,7 +66,7 @@ impl From<Extended> for u64 {
let major = u64::from(value.major) << 52; let major = u64::from(value.major) << 52;
let minor = u64::from(value.minor) << 40; let minor = u64::from(value.minor) << 40;
let patch = u64::from(value.patch) << 28; 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)); let pre = u64::from(u16::from(value.pre));
major | minor | patch | build | pre major | minor | patch | build | pre
} }
@ -82,8 +82,8 @@ impl TryFrom<u64> for Extended {
let minor = (value & mask) >> 40; let minor = (value & mask) >> 40;
mask = 0o7777 << 28; mask = 0o7777 << 28;
let patch = (value & mask) >> 28; let patch = (value & mask) >> 28;
mask = 0o7777 << 14; mask = 0o7777 << 16;
let build = (value & mask) >> 14; let build = (value & mask) >> 16;
mask = 0o37777; mask = 0o37777;
let p = u16::try_from(value & mask)?; let p = u16::try_from(value & mask)?;
let pre: PreRelease = p.try_into()?; let pre: PreRelease = p.try_into()?;

View File

@ -15,6 +15,23 @@ use {
/// that a Git revision can only be compared against another Git revision, making it /// 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\ /// impossible to properly order updates between a proper `SemVer` type release and a\
/// Git revision. /// 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" <hash> | 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)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct GitRev { pub struct GitRev {
/// the short revision hash /// the short revision hash

View File

@ -1,4 +1,6 @@
#![warn(clippy::all, clippy::pedantic)] #![warn(clippy::all, clippy::pedantic)]
#![doc = include_str!("../README.md")]
mod arch; mod arch;
mod error; mod error;
mod extended; mod extended;

View File

@ -1,11 +1,9 @@
use { use {
crate::error::Error, crate::{error::Error, MAX_U12},
serde::{Deserialize, Serialize}, serde::{Deserialize, Serialize},
std::{cmp, convert::Into, fmt, num::NonZeroU16, str}, std::{cmp, convert::Into, fmt, num::NonZeroU16, str},
}; };
static MAX_U10: u16 = 1024;
#[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
/// A specification for non-production releases /// A specification for non-production releases
pub enum PreRelease { pub enum PreRelease {
@ -47,7 +45,7 @@ impl PreRelease {
match self { match self {
Self::Alpha(n) | Self::Beta(n) | Self::RC(n) => match n { Self::Alpha(n) | Self::Beta(n) | Self::RC(n) => match n {
Some(num) => { Some(num) => {
if u16::from(*num) >= MAX_U10 { if u16::from(*num) >= MAX_U12 {
return Err(Error::Range); return Err(Error::Range);
} }
*num = num.saturating_add(1); *num = num.saturating_add(1);
@ -155,20 +153,20 @@ impl From<PreRelease> for u16 {
match value { match value {
PreRelease::Alpha(Some(v)) => { PreRelease::Alpha(Some(v)) => {
let v = u16::from(v) & mask; let v = u16::from(v) & mask;
v | 0o2_000 v | 0o10_000
} }
PreRelease::Alpha(None) => 0o2_000, PreRelease::Alpha(None) => 0o2_000,
PreRelease::Beta(Some(v)) => { PreRelease::Beta(Some(v)) => {
let v = u16::from(v) & mask; let v = u16::from(v) & mask;
v | 0o4_000 v | 0o20_000
} }
PreRelease::Beta(None) => 0o4_000, PreRelease::Beta(None) => 0o4_000,
PreRelease::RC(Some(v)) => { PreRelease::RC(Some(v)) => {
let v = u16::from(v) & mask; let v = u16::from(v) & mask;
v | 0o10_000 v | 0o40_000
} }
PreRelease::RC(None) => 0o10_000, PreRelease::RC(None) => 0o10_000,
PreRelease::None => 0o20_000, PreRelease::None => 0o100_000,
} }
} }
} }
@ -181,19 +179,19 @@ impl TryFrom<u16> for PreRelease {
let v = value & mask; let v = value & mask;
let flag = value & !mask; let flag = value & !mask;
match flag { match flag {
0o2_000 => { 0o10_000 => {
let v = if v > 0 { Some(v.try_into()?) } else { None }; let v = if v > 0 { Some(v.try_into()?) } else { None };
Ok(Self::Alpha(v)) Ok(Self::Alpha(v))
} }
0o4_000 => { 0o20_000 => {
let v = if v > 0 { Some(v.try_into()?) } else { None }; let v = if v > 0 { Some(v.try_into()?) } else { None };
Ok(Self::Beta(v)) Ok(Self::Beta(v))
} }
0o10_000 => { 0o40_000 => {
let v = if v > 0 { Some(v.try_into()?) } else { None }; let v = if v > 0 { Some(v.try_into()?) } else { None };
Ok(Self::RC(v)) Ok(Self::RC(v))
} }
0o20_000 => Ok(Self::None), 0o100_000 => Ok(Self::None),
_ => Err(Error::FromUint), _ => Err(Error::FromUint),
} }
} }