commit 35c41c2a958f9d7b1b96713be6f1c3b8b1d4f70e Author: Michael Pfaff Date: Mon Jan 18 21:46:02 2021 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..272b761 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,271 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" + +[[package]] +name = "nrid" +version = "0.1.0" +dependencies = [ + "chrono", + "rand", + "serde", + "thiserror", + "tracing", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "serde" +version = "1.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bdd36f49e35b61d49efd8aa7fc068fd295961fd2286d0b2ee9a4c7a14e99cc3" + +[[package]] +name = "syn" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tracing" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a9810ed --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nrid" +version = "0.1.0" +authors = ["Michael Pfaff "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { version = "0.4.11", features = ["serde"] } +rand = { version = "0.7.3" } +#regex = { version = "1.3" } +serde = { version = "1" } +thiserror = { version = "1" } +tracing = { version = "0.1.15" } + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5922457 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# 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. + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c2a7e30 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,322 @@ +/*! + * 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); + } +} + diff --git a/src/sentence.rs b/src/sentence.rs new file mode 100644 index 0000000..cfec9ea --- /dev/null +++ b/src/sentence.rs @@ -0,0 +1,159 @@ +/*! + * Provides traits and helpers for generating and manipulating written lanuage constructs + */ + +/// A type that can be compiled into a sentence. +pub trait ToSentence { + /// Sentencifies self. The result should be suitable to be placed within a sentence. + fn to_sentence(&self) -> String; + + /// Sentencifies self, upper-casing the first letter and adding a period to the end. Using this is discouraged due to its lack of flexibility (i.e. needing alternate punctuation or a lowercase first letter). + fn to_sentence_finalize(&self) -> String { + let s = self.to_sentence(); + let mut c = s.chars(); + match s.len() { + 0 => format!(""), + 1 => format!("{}.", s.to_uppercase()), + _ => format!("{}{}.", c.nth(0).unwrap().to_uppercase(), c.as_str()), + } + } +} + +impl<'a, T> ToSentence for T where T: AsRef<[&'a str]> { + fn to_sentence(&self) -> String { + let s = self.as_ref(); + let len = s.len(); + let mut str = String::new(); + for (i, seg) in s.iter().map(|s| s.as_ref()).enumerate() { + if i == 0 { + str.push_str(seg); + } else if i == len-1 { + if len == 2 { + str.push_str(&format!(" and {}", seg)); + } else { + str.push_str(&format!(", and {}", seg)); + } + } else { + str.push_str(&format!(", {}", seg)); + } + } + str + } +} + +// impl ToSentence for T where T: AsRef<[String]> { +// fn to_sentence(&self) -> String { +// let s = self.as_ref(); +// let len = s.len(); +// let mut str = String::new(); +// for (i, seg) in s.iter().map(|s| s.as_ref()).enumerate() { +// if i == 0 { +// str.push_str(seg); +// } else if i == len { +// if len == 2 { +// str.push_str(&format!(" and {}", seg)); +// } else { +// str.push_str(&format!(", and {}", seg)); +// } +// } else { +// str.push_str(&format!(", {}", seg)); +// } +// } +// str +// } +// } + +#[cfg(test)] +mod test { + use super::ToSentence; + + const STRING_SLICE_A: &'static [&'static str] = &[ + "a", + ]; + + const STRING_SLICE_B: &'static [&'static str] = &[ + "a", + "b", + ]; + + const STRING_SLICE_C: &'static [&'static str] = &[ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + ]; + + #[test] + fn str_slice_to_sentence() { + assert_eq!(STRING_SLICE_A.to_sentence(), "a"); + assert_eq!(STRING_SLICE_B.to_sentence(), "a and b"); + assert_eq!(STRING_SLICE_C.to_sentence(), "a, b, c, d, e, f, and g"); + } + + #[test] + fn str_slice_to_sentence_finalize() { + assert_eq!(STRING_SLICE_A.to_sentence_finalize(), "A."); + assert_eq!(STRING_SLICE_B.to_sentence_finalize(), "A and b."); + assert_eq!(STRING_SLICE_C.to_sentence_finalize(), "A, b, c, d, e, f, and g."); + } + + #[test] + fn string_vec_to_sentence() { + let vec: Vec = vec!{ + "a".to_owned(), + }; + + assert_eq!(vec.iter().map(|e| e.as_ref()).collect::>().to_sentence(), "a"); + + let vec: Vec = vec!{ + "a".to_owned(), + "b".to_owned(), + }; + + assert_eq!(vec.iter().map(|e| e.as_ref()).collect::>().to_sentence(), "a and b"); + + let vec: Vec = vec!{ + "a".to_owned(), + "b".to_owned(), + "c".to_owned(), + "d".to_owned(), + "e".to_owned(), + "f".to_owned(), + "g".to_owned(), + }; + + assert_eq!(vec.iter().map(|e| e.as_ref()).collect::>().to_sentence(), "a, b, c, d, e, f, and g"); + } + + #[test] + fn string_vec_to_sentence_finalize() { + let vec: Vec = vec!{ + "a".to_owned(), + }; + + assert_eq!(vec.iter().map(|e| e.as_ref()).collect::>().to_sentence_finalize(), "A."); + + let vec: Vec = vec!{ + "a".to_owned(), + "b".to_owned(), + }; + + assert_eq!(vec.iter().map(|e| e.as_ref()).collect::>().to_sentence_finalize(), "A and b."); + + let vec: Vec = vec!{ + "a".to_owned(), + "b".to_owned(), + "c".to_owned(), + "d".to_owned(), + "e".to_owned(), + "f".to_owned(), + "g".to_owned(), + }; + + assert_eq!(vec.iter().map(|e| e.as_ref()).collect::>().to_sentence_finalize(), "A, b, c, d, e, f, and g."); + } +} +