From eec8cfa79aed31d7b7b39f3d5fbc76dee2d9c054 Mon Sep 17 00:00:00 2001 From: Michael Pfaff Date: Mon, 30 May 2022 13:32:36 -0400 Subject: [PATCH] Overhaul the crate --- Cargo.toml | 17 +- src/lib.rs | 525 +++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 399 insertions(+), 143 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5d091b7..bf8958a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,15 +2,20 @@ name = "nrid" version = "0.2.0" authors = ["Michael Pfaff "] -edition = "2018" +edition = "2021" license = "MIT OR Apache-2.0" [features] -default = ["serde"] +default = [ ] +chrono = [ "dep:chrono" ] +serde = [ "dep:serde" ] +# the source of the timestamp is an implementation detail, so a feature is required for the From impl +time = [ ] [dependencies] -chrono = "0.4.19" -rand = "0.8.3" -serde = { version = "1", optional = true } -thiserror = "1" +chrono = { version = "0.4.19", default-features = false, optional = true } +rand = { version = "0.8.5", default-features = false, features = [ "getrandom" ] } +serde = { version = "1", default-features = false, features = [ "derive" ], optional = true } +thiserror = { version = "1", default-features = false } +time = { version = "0.3.9", default-features = false, features = [ "std" ] } diff --git a/src/lib.rs b/src/lib.rs index 7fa58e6..b8ba465 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,73 +1,311 @@ /*! * Nano-Random IDentifier. - * + * * Made of a 64-bit seconds, 32-bit nanoseconds, and 32-bit secure-randomness, the NRID is suitable for cases where you want a secure-random, unique identifier like a UUID, but you also want the identifier to be correlated with the time of creation. */ -use core::cmp::max; -use core::cmp::min; -use core::convert::TryFrom; +use std::convert::TryFrom; +use std::str::FromStr; -/// rules: consist of 12 hex characters, a hyphen, 9 hex characters, a hyphen, and 9 hex characters -#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] -pub enum NridError { - #[error("must be a total of 32 characters long")] - WrongLength, +pub mod error { + #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] + #[error("Value for segment '{segment}' must be range {minimum}..={maximum}.")] + pub struct SegmentRange { + /// The segment in question. + pub segment: &'static str, - #[error("must consist of hexadecimal characters and hyphens")] - IllegalCharacters, + /// The minimum value for the segment, inclusive. + pub minimum: u64, - #[error("segment {0} must be {1} hex characters long")] - WrongSegmentLength(u8, u8), + /// The maximum value for the segment, inclusive. + pub maximum: u64, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] + #[error( + "Encoded value for segment '{segment}' must be {expected_length} characters in length." + )] + pub struct SegmentLength { + /// The segment in question. + pub segment: &'static str, + + /// The expected length of the segment encoded as a string. + pub expected_length: usize, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] + pub enum Segment { + #[error("Invalid character in segment {segment}.")] + Character { segment: &'static str }, + + #[error(transparent)] + Length(#[from] SegmentLength), + + #[error(transparent)] + Range(#[from] SegmentRange), + + #[error("Unknown error parsing segment")] + Unknown, + } + + impl Segment { + #[inline] + pub(crate) fn character(segment: &'static str) -> Self { + Self::Character { segment } + } + + #[inline] + pub(crate) fn length(segment: &'static str, expected_length: usize) -> Self { + SegmentLength { + segment, + expected_length, + } + .into() + } + + #[inline] + pub(crate) fn range(segment: &'static str, minimum: u64, maximum: u64) -> Self { + SegmentRange { + segment, + minimum, + maximum, + } + .into() + } + + #[inline] + pub(crate) fn new( + segment: &'static str, + expected_length: usize, + minimum: u64, + maximum: u64, + err: std::num::ParseIntError, + ) -> Self { + use std::num::IntErrorKind; + match err.kind() { + IntErrorKind::Empty => Self::length(segment, expected_length), + IntErrorKind::InvalidDigit => Self::character(segment), + IntErrorKind::PosOverflow | IntErrorKind::NegOverflow => { + Self::range(segment, minimum, maximum) + } + IntErrorKind::Zero => { + assert!(minimum > 0); + Self::range(segment, minimum, maximum) + } + _ => Self::Unknown, + } + } + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] + pub enum Parse { + #[error("Entire value must be 32 characters in length, not {0}.")] + Length(usize), + + #[error("Invalid segment delimiter at {0}.")] + Separator(usize), + + #[error(transparent)] + Segment(Segment), + } + + impl From for Parse + where + E: Into, + { + fn from(e: E) -> Self { + >::into(e).into() + } + } + + /// An error in conversion to a foreign type. + #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] + #[error("{0}")] + pub struct ConversionError(pub(crate) std::borrow::Cow<'static, str>); } -/// Nano-Random IDentifier. Stores the seconds since the epoch and nanoseconds since the second, along with 4 bytes of randomness. +/// **N**anosecond-precision **R**andom **Id**entifier. Stores the seconds since the epoch and nanoseconds offset, along with 4 bytes of randomness. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[repr(C)] pub struct Nrid { - seconds: u64, - nanoseconds: u32, - randomness: u32, + secs: u64, + nanos: u32, + rand: u32, } impl Nrid { + pub const fn new(secs: u64, nanos: u32, rand: u32) -> Result { + if nanos > 999_999_999 { + return Err(error::SegmentRange { + segment: "nanos", + minimum: 0, + maximum: 999_999_999, + }); + } + + Ok(Self { secs, nanos, rand }) + } + /// Returns a new `Nrid` with its timestamp taken from [`Utc::now`]. - #[inline] pub fn now() -> Self { - let now = chrono::Utc::now(); + let now = time::OffsetDateTime::now_utc(); use rand::RngCore; - Self { - // we ensure that the seconds value is not negative before casting out of an abundance of safety. - seconds: max(now.timestamp(), 0) as u64, - nanoseconds: now.timestamp_subsec_nanos(), - randomness: rand::rngs::OsRng.next_u32(), + assert!( + now.unix_timestamp() >= 0, + "System time is before the Unix Epoch!" + ); + assert!( + now.nanosecond() <= 999_999_999, + "Invalid nanosecond offset!" + ); + Self::new( + now.unix_timestamp() as u64, + now.nanosecond(), + rand::rngs::OsRng.next_u32(), + ) + .unwrap() + } + + /// The seconds component of the id. + #[inline(always)] + pub const fn secs(&self) -> u64 { + self.secs + } + + /// The seconds component of the id. + #[inline(always)] + pub const fn nanos(&self) -> u32 { + self.nanos + } + + /// The random component of the id. + #[inline(always)] + pub const fn rand(&self) -> u32 { + self.rand + } +} + +impl std::fmt::Debug for Nrid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!( + f, + "{:012x}-{:09x}-{:09x}", + self.secs(), + self.nanos(), + self.rand() + ) + } +} + +impl std::fmt::Display for Nrid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!( + f, + "{:012x}-{:09x}-{:09x}", + self.secs(), + self.nanos(), + self.rand() + ) + } +} + +#[cfg(feature = "serde")] +mod serde_mod { + use super::*; + + #[derive(Debug, Clone, Copy, serde::Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + pub enum Field { + Secs, + Nanos, + Rand, + } + + impl Field { + #[inline] + pub const fn as_str(self) -> &'static str { + match self { + Self::Secs => "secs", + Self::Nanos => "nanos", + Self::Rand => "rand", + } } } -} -impl core::fmt::Debug for Nrid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - write!( - f, - "{:012x}-{:09x}-{:09x}", - self.seconds, self.nanoseconds, self.randomness - ) - } -} + pub const NRID_FIELDS: [&'static str; 3] = [ + Field::Secs.as_str(), + Field::Nanos.as_str(), + Field::Rand.as_str(), + ]; -impl core::fmt::Display for Nrid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - write!( - f, - "{:012x}-{:09x}-{:09x}", - self.seconds, self.nanoseconds, self.randomness - ) - } -} + pub struct NridVisitor; -impl From for String { - fn from(value: Nrid) -> Self { - value.to_string() + impl<'de> serde::de::Visitor<'de> for NridVisitor { + type Value = Nrid; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct Nrid") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Nrid::try_from(value).map_err(|e| serde::de::Error::custom(format!("{}", e))) + } + + fn visit_seq(self, mut seq: V) -> Result + where + V: serde::de::SeqAccess<'de>, + { + let secs = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + let nanos = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + let rand = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; + Nrid::new(secs, nanos, rand).map_err(|e| serde::de::Error::custom(format!("{}", e))) + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut secs = None; + let mut nanos = None; + let mut rand = None; + while let Some(key) = map.next_key()? { + match key { + Field::Secs => { + if secs.is_some() { + return Err(serde::de::Error::duplicate_field(key.as_str())); + } + secs = Some(map.next_value()?); + } + Field::Nanos => { + if nanos.is_some() { + return Err(serde::de::Error::duplicate_field(key.as_str())); + } + nanos = Some(map.next_value()?); + } + Field::Rand => { + if rand.is_some() { + return Err(serde::de::Error::duplicate_field(key.as_str())); + } + rand = Some(map.next_value()?); + } + } + } + + let secs = secs.ok_or_else(|| serde::de::Error::missing_field(Field::Secs.as_str()))?; + let nanos = + nanos.ok_or_else(|| serde::de::Error::missing_field(Field::Nanos.as_str()))?; + let rand = rand.ok_or_else(|| serde::de::Error::missing_field(Field::Rand.as_str()))?; + Nrid::new(secs, nanos, rand).map_err(|e| serde::de::Error::custom(format!("{}", e))) + } } } @@ -77,29 +315,15 @@ impl serde::Serialize for Nrid { where S: serde::Serializer, { - serializer.serialize_str(&self.to_string()) - } -} - -#[cfg(feature = "serde")] -struct NridVisitor; - - -#[cfg(feature = "serde")] -impl<'de> serde::de::Visitor<'de> for NridVisitor { - type Value = Nrid; - - fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - formatter.write_str(&format!("a string that must consist of 12 hex characters, a hyphen, 9 hex characters, a hyphen, and 9 hex characters")) - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - match Nrid::try_from(value) { - Ok(nrid) => Ok(nrid), - Err(err) => Err(serde::de::Error::custom(format!("{}", err))), + if serializer.is_human_readable() { + serializer.serialize_str(&self.to_string()) + } else { + use serde::ser::SerializeStruct; + let mut s = serializer.serialize_struct("Nrid", 3)?; + s.serialize_field(serde_mod::Field::Secs.as_str(), &self.secs())?; + s.serialize_field(serde_mod::Field::Nanos.as_str(), &self.nanos())?; + s.serialize_field(serde_mod::Field::Rand.as_str(), &self.rand())?; + s.end() } } } @@ -110,19 +334,65 @@ impl<'de> serde::Deserialize<'de> for Nrid { where D: serde::Deserializer<'de>, { - deserializer.deserialize_str(NridVisitor) + if deserializer.is_human_readable() { + deserializer.deserialize_str(serde_mod::NridVisitor) + } else { + deserializer.deserialize_struct("Nrid", &serde_mod::NRID_FIELDS, serde_mod::NridVisitor) + } } } -impl From for chrono::DateTime { - fn from(value: Nrid) -> Self { - // we ensure that the seconds value is not greater than the maximum i64, rather than letting the value overflow to 0 (IIRC). - chrono::DateTime::from_utc( - chrono::NaiveDateTime::from_timestamp( - min(value.seconds, i64::MAX as u64) as i64, - value.nanoseconds, - ), +#[cfg(feature = "chrono")] +impl TryFrom for chrono::DateTime { + type Error = error::ConversionError; + + fn try_from(value: Nrid) -> Result { + if value.secs() > i64::MAX as u64 { + return Err(error::ConversionError( + concat!( + stringify!(chrono::DateTime), + " cannot handle secs greater than ", + stringify!(i64::MAX) + ) + .into(), + )); + } + Ok(chrono::DateTime::from_utc( + chrono::NaiveDateTime::from_timestamp_opt(value.secs() as i64, value.nanos()) + .ok_or_else(|| { + error::ConversionError( + concat!( + stringify!(chrono::DateTime), + " cannot handle the value" + ) + .into(), + ) + })?, chrono::Utc, + )) + } +} + +#[cfg(feature = "time")] +impl TryFrom for time::OffsetDateTime { + type Error = error::ConversionError; + + fn try_from(value: Nrid) -> Result { + if value.secs() > i64::MAX as u64 { + return Err(error::ConversionError( + concat!( + stringify!(time::OffsetDateTime), + " cannot handle secs greater than ", + stringify!(i64::MAX) + ) + .into(), + )); + } + Ok( + time::OffsetDateTime::from_unix_timestamp(value.secs() as i64) + .map_err(|e| error::ConversionError(e.to_string().into()))? + .replace_nanosecond(value.nanos()) + .map_err(|e| error::ConversionError(e.to_string().into()))?, ) } } @@ -132,90 +402,71 @@ const NANOSECONDS_LEN: usize = 9; const RANDOMNESS_LEN: usize = 9; const SECONDS_OFFSET: usize = 0; -const NANOSECONDS_OFFSET: usize = SECONDS_OFFSET+SECONDS_LEN+1; -const RANDOMNESS_OFFSET: usize = NANOSECONDS_OFFSET+NANOSECONDS_LEN+1; +const NANOSECONDS_OFFSET: usize = SECONDS_OFFSET + SECONDS_LEN + 1; +const RANDOMNESS_OFFSET: usize = NANOSECONDS_OFFSET + NANOSECONDS_LEN + 1; -impl TryFrom<&str> for Nrid { - type Error = NridError; +impl FromStr for Nrid { + type Err = error::Parse; - fn try_from(value: &str) -> std::result::Result { - let value_len = value.len(); - - if value_len != (SECONDS_LEN + 1 + NANOSECONDS_LEN + 1 + RANDOMNESS_LEN) { - return Err(NridError::WrongLength); + fn from_str(value: &str) -> std::result::Result { + if value.len() != (SECONDS_LEN + 1 + NANOSECONDS_LEN + 1 + RANDOMNESS_LEN) { + return Err(error::Parse::Length(value.len())); } - for (i, c) in value.chars().enumerate() { - if i == NANOSECONDS_OFFSET-1 || i == RANDOMNESS_OFFSET-1 { - if c != '-' { - return Err(NridError::IllegalCharacters); - } - } else { - if !char::is_ascii_hexdigit(&c) { - return Err(NridError::IllegalCharacters); - } - } + if &value[NANOSECONDS_OFFSET - 1..NANOSECONDS_OFFSET] != "-" { + return Err(error::Parse::Separator(NANOSECONDS_OFFSET - 1)); + } + if &value[RANDOMNESS_OFFSET - 1..RANDOMNESS_OFFSET] != "-" { + return Err(error::Parse::Separator(RANDOMNESS_OFFSET - 1)); } - let sec = &value[SECONDS_OFFSET..SECONDS_OFFSET+SECONDS_LEN]; - match u64::from_str_radix(sec, 16) { - Ok(sec) => { - let nsec = &value[NANOSECONDS_OFFSET..NANOSECONDS_OFFSET+NANOSECONDS_LEN]; - match u32::from_str_radix(nsec, 16) { - Ok(nsec) => { - let rand = &value[RANDOMNESS_OFFSET..RANDOMNESS_OFFSET+RANDOMNESS_LEN]; - match u32::from_str_radix(rand, 16) { - Ok(rand) => Ok(Self { - seconds: sec, - nanoseconds: nsec, - randomness: rand, - }), - Err(_) => { - return Err(NridError::WrongSegmentLength(3, 9)); - } - } - } - Err(_) => { - return Err(NridError::WrongSegmentLength(2, 9)); - } - } - } - Err(_) => { - return Err(NridError::WrongSegmentLength(1, 12)); - } + match u64::from_str_radix(&value[SECONDS_OFFSET..SECONDS_OFFSET + SECONDS_LEN], 16) { + Ok(secs) => match u32::from_str_radix( + &value[NANOSECONDS_OFFSET..NANOSECONDS_OFFSET + NANOSECONDS_LEN], + 16, + ) { + Ok(nanos) => match u32::from_str_radix( + &value[RANDOMNESS_OFFSET..RANDOMNESS_OFFSET + RANDOMNESS_LEN], + 16, + ) { + Ok(rand) => Ok(Self::new(secs, nanos, rand)?), + Err(e) => Err(error::Segment::new("rand", 9, 0, u32::MAX.into(), e).into()), + }, + Err(e) => Err(error::Segment::new("nanos", 9, 0, 999_999_999, e).into()), + }, + Err(e) => Err(error::Segment::new("secs", 12, 0, u64::MAX, e).into()), } } } -impl core::str::FromStr for Nrid { - type Err = NridError; +impl TryFrom<&str> for Nrid { + type Error = error::Parse; - fn from_str(value: &str) -> std::result::Result { - Self::try_from(value) + #[inline] + fn try_from(value: &str) -> std::result::Result { + Self::from_str(value) } } #[cfg(test)] mod test { - use core::convert::TryFrom; + use std::convert::TryFrom; - use super::Nrid; + use super::{error, Nrid}; const NRID_STRING: &'static str = "00000d0525a4-0000052fa-000069540"; - const NRID: Nrid = Nrid { - seconds: 218441124, - nanoseconds: 21242, - randomness: 431424, - }; + const NRID: Result = Nrid::new(218441124, 21242, 431424); #[test] fn parse_nrid() { - assert_eq!(Nrid::try_from(NRID_STRING).unwrap(), NRID); + assert_eq!( + Nrid::try_from(NRID_STRING).unwrap(), + NRID.expect("invalid Nrid") + ); } #[test] fn serialize_nrid() { - assert_eq!(NRID.to_string(), NRID_STRING); + assert_eq!(NRID.expect("invalid Nrid").to_string(), NRID_STRING); } } -