/*! * 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. */ #[macro_use] extern crate tracing; use core::cmp::max; use core::cmp::min; use core::convert::TryFrom; mod sentence; /// Nano Unique Identifier. Stores the seconds since the epoch and nanoseconds since the second, 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, } impl Nrid { /// Returns a new `ObjectKey` with its timestamp taken from [`Utc::now`]. #[inline] pub fn now() -> Self { let now = chrono::Utc::now(); 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(), } } } 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 ) } } 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 ) } } impl From for String { fn from(value: Nrid) -> Self { value.to_string() } } impl serde::Serialize for Nrid { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { serializer.serialize_str(&self.to_string()) } } struct NridVisitor; impl<'de> serde::de::Visitor<'de> for NridVisitor { type Value = Nrid; fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { use crate::sentence::ToSentence; formatter.write_str(&format!("a string that must {}", NRID_RULES.to_sentence())) } 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))), } } } impl<'de> serde::Deserialize<'de> for Nrid { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { deserializer.deserialize_str(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, ), chrono::Utc, ) } } // commented out because it can't be deterministic due to the randomness in a Nrid not being representable in a DateTime. // impl From<&DateTime> for Nrid { // #[inline] // fn from(value: &DateTime) -> Self { // // let value = value.borrow(); // Self( // // we ensure that the seconds value is not negative before casting out of an abundance of safety. // max(value.timestamp(), 0) as u64, // value.timestamp_subsec_nanos(), // ) // } // } const NRID_RULES: &'static [&'static str] = &["consist of 12 hex characters, a hyphen, 9 hex characters, a hyphen, and 9 hex characters"]; const NRID_LEN_RULES: &'static [&'static str] = &["be 32 characters long"]; const NRID_CHARS_RULES: &'static [&'static str] = &["consist of hexadecimal characters and hyphens"]; const SECONDS_RULES: &'static [&'static str] = &["be 12 hex characters"]; const NANOSECONDS_RULES: &'static [&'static str] = &["be 9 hex characters"]; const RANDOMNESS_RULES: &'static [&'static str] = &["be 9 hex characters"]; const SECONDS_LEN: usize = 12; 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; impl TryFrom<&str> for Nrid { type Error = ValidationError; 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(ValidationError::Rules { rules: NRID_LEN_RULES, offending_segment: value.to_string(), } .into()); } for (i, c) in value.chars().enumerate() { if i == NANOSECONDS_OFFSET-1 || i == RANDOMNESS_OFFSET-1 { if c != '-' { return Err(ValidationError::Rules { rules: NRID_CHARS_RULES, offending_segment: c.to_string(), } .into()); } } else { if !char::is_ascii_hexdigit(&c) { return Err(ValidationError::Rules { rules: NRID_CHARS_RULES, offending_segment: c.to_string(), } .into()); } } } 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(err) => { trace!(error = ?err, "parse randomness failed"); return Err(ValidationError::Rules { rules: RANDOMNESS_RULES, offending_segment: rand.to_string(), } .into()); } } } Err(err) => { trace!(error = ?err, "parse nanoseconds failed"); return Err(ValidationError::Rules { rules: NANOSECONDS_RULES, offending_segment: nsec.to_string(), } .into()); } } } Err(err) => { trace!(error = ?err, "parse seconds failed"); return Err(ValidationError::Rules { rules: SECONDS_RULES, offending_segment: sec.to_string(), } .into()); } } } } impl core::str::FromStr for Nrid { type Err = ValidationError; fn from_str(value: &str) -> std::result::Result { Self::try_from(value) } } #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum ValidationError { /// Length of input was not as expected. LengthEqual { /// The expected input length. expected: usize, /// The input segment that failed validation. offending_segment: String, }, /// Length of input was greater than expected. LengthMax { /// The maximum expected input length. max: usize, /// The input segment that failed validation. offending_segment: String, }, /// Length of input was less than expected LengthMin { /// The minimum expected input length. min: usize, /// The input segment that failed validation. offending_segment: String, }, /// Input failed to match rules. Rules { /// The rules the input must follow. rules: &'static [&'static str], /// The input segment that failed validation. offending_segment: String, }, } impl std::fmt::Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { use crate::sentence::ToSentence; match self { Self::LengthEqual { expected, offending_segment, } => write!(f, "`{}` is not equal to {}", offending_segment, expected), Self::LengthMax { max, offending_segment, } => write!(f, "`{}` is longer than {}", offending_segment, max), Self::LengthMin { min, offending_segment, } => write!(f, "`{}` is shorter than {}", offending_segment, min), Self::Rules { rules, offending_segment, } => write!( f, "`{}` must {} and fails one or more", offending_segment, rules.to_sentence() ), } } } #[cfg(test)] mod test { use core::convert::TryFrom; use super::Nrid; const NRID_STRING: &'static str = "00000d0525a4-0000052fa-000069540"; const NRID: Nrid = Nrid { seconds: 218441124, nanoseconds: 21242, randomness: 431424, }; #[test] fn parse_nrid() { assert_eq!(Nrid::try_from(NRID_STRING).unwrap(), NRID); } #[test] fn serialize_nrid() { assert_eq!(NRID.to_string(), NRID_STRING); } }