From 201c039cfef943c45ed1bfaa99f4871378f5bfd6 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 13 Feb 2025 14:20:32 +0300 Subject: [PATCH] feat(common): remove unnecessary Result<> in infallible functions --- pubky-common/src/auth.rs | 32 +++- pubky-common/src/capabilities.rs | 15 +- pubky-common/src/constants.rs | 9 + pubky-common/src/crypto.rs | 42 +++-- pubky-common/src/lib.rs | 8 + pubky-common/src/namespaces.rs | 3 + pubky-common/src/recovery_file.rs | 36 ++-- pubky-common/src/session.rs | 20 ++- pubky-common/src/timestamp.rs | 280 ------------------------------ 9 files changed, 125 insertions(+), 320 deletions(-) delete mode 100644 pubky-common/src/timestamp.rs diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index ba182a8..81a87e0 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -19,6 +19,7 @@ const CURRENT_VERSION: u8 = 0; const TIMESTAMP_WINDOW: i64 = 45 * 1_000_000; #[derive(Debug, PartialEq, Serialize, Deserialize)] +/// Implementation of the [Pubky Auth spec](https://pubky.github.io/pubky-core/spec/auth.html). pub struct AuthToken { /// Signature over the token. signature: Signature, @@ -41,6 +42,7 @@ pub struct AuthToken { } impl AuthToken { + /// Sign a new AuthToken with given capabilities. pub fn sign(keypair: &Keypair, capabilities: impl Into) -> Self { let timestamp = Timestamp::now(); @@ -60,10 +62,21 @@ impl AuthToken { token } + // === Getters === + + /// Returns the pubky that is providing this AuthToken + pub fn pubky(&self) -> &PublicKey { + &self.pubky + } + + /// Returns the capabilities in this AuthToken. pub fn capabilities(&self) -> &[Capability] { &self.capabilities.0 } + // === Public Methods === + + /// Parse and verify an AuthToken. pub fn verify(bytes: &[u8]) -> Result { if bytes[75] > CURRENT_VERSION { return Err(Error::UnknownVersion); @@ -95,19 +108,17 @@ impl AuthToken { } } + /// Serialize this AuthToken to its canonical binary representation. pub fn serialize(&self) -> Vec { postcard::to_allocvec(self).unwrap() } + /// Deserialize an AuthToken from its canonical binary representation. pub fn deserialize(bytes: &[u8]) -> Result { Ok(postcard::from_bytes(bytes)?) } - pub fn pubky(&self) -> &PublicKey { - &self.pubky - } - - /// A unique ID for this [AuthToken], which is a concatenation of + /// Returns the unique ID for this [AuthToken], which is a concatenation of /// [AuthToken::pubky] and [AuthToken::timestamp]. /// /// Assuming that [AuthToken::timestamp] is unique for every [AuthToken::pubky]. @@ -133,6 +144,8 @@ pub struct AuthVerifier { } impl AuthVerifier { + /// Verify an [AuthToken] by parsing it from its canonical binary representation, + /// verifying its signature, and confirm it wasn't already used. pub fn verify(&self, bytes: &[u8]) -> Result { self.gc(); @@ -168,18 +181,25 @@ impl AuthVerifier { } #[derive(thiserror::Error, Debug, PartialEq, Eq)] +/// Error verifying an [AuthToken] pub enum Error { #[error("Unknown version")] + /// Unknown version UnknownVersion, #[error("AuthToken has a timestamp that is more than 45 seconds in the future")] + /// AuthToken has a timestamp that is more than 45 seconds in the future TooFarInTheFuture, #[error("AuthToken has a timestamp that is more than 45 seconds in the past")] + /// AuthToken has a timestamp that is more than 45 seconds in the past Expired, #[error("Invalid Signature")] + /// Invalid Signature InvalidSignature, #[error(transparent)] - Postcard(#[from] postcard::Error), + /// Error parsing [AuthToken] using Postcard + Parsing(#[from] postcard::Error), #[error("AuthToken already used")] + /// AuthToken already used AlreadyUsed, } diff --git a/pubky-common/src/capabilities.rs b/pubky-common/src/capabilities.rs index 83848b9..b1d4617 100644 --- a/pubky-common/src/capabilities.rs +++ b/pubky-common/src/capabilities.rs @@ -1,10 +1,16 @@ +//! Capabilities defining what scopes of resources can be accessed with what actions. + use std::fmt::Display; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq)] +/// A Capability defines the scope of resources and the actions that the holder +/// of this capability can access. pub struct Capability { + /// Scope of resources (for example directories). pub scope: String, + /// Actions allowed on the [Capability::scope]. pub actions: Vec, } @@ -19,6 +25,7 @@ impl Capability { } #[derive(Debug, Clone, PartialEq, Eq)] +/// Actions allowed on a given resource or scope of resources. pub enum Action { /// Can read the scope at the specified path (GET requests). Read, @@ -125,14 +132,19 @@ impl<'de> Deserialize<'de> for Capability { } #[derive(thiserror::Error, Debug, PartialEq, Eq)] +/// Error parsing a [Capability]. pub enum Error { #[error("Capability: Invalid scope: does not start with `/`")] + /// Capability: Invalid scope: does not start with `/` InvalidScope, #[error("Capability: Invalid format should be :")] + /// Capability: Invalid format should be : InvalidFormat, #[error("Capability: Invalid Action")] + /// Capability: Invalid Action InvalidAction, #[error("Capabilities: Invalid capabilities format")] + /// Capabilities: Invalid capabilities format InvalidCapabilities, } @@ -142,6 +154,7 @@ pub enum Error { pub struct Capabilities(pub Vec); impl Capabilities { + /// Returns true if the list of capabilities contains a given capability. pub fn contains(&self, capability: &Capability) -> bool { self.0.contains(capability) } @@ -227,7 +240,7 @@ mod tests { actions: vec![Action::Read, Action::Write], }; - // Read and write withing directory `/pub/pubky.app/`. + // Read and write within directory `/pub/pubky.app/`. let expected_string = "/pub/pubky.app/:rw"; assert_eq!(cap.to_string(), expected_string); diff --git a/pubky-common/src/constants.rs b/pubky-common/src/constants.rs index e7f1210..a028e6c 100644 --- a/pubky-common/src/constants.rs +++ b/pubky-common/src/constants.rs @@ -1,9 +1,18 @@ +//! Constants used across Pubky. + /// [Reserved param keys](https://www.rfc-editor.org/rfc/rfc9460#name-initial-contents) for HTTPS Resource Records pub mod reserved_param_keys { + /// HTTPS (RFC 9460) record's private param key, used to inform browsers + /// about the HTTP port to use when the domain is localhost. pub const HTTP_PORT: u16 = 65280; } +/// Local test network's hardcoded port numbers for local development. pub mod testnet_ports { + /// The local test network's hardcorded DHT bootstrapping node's port number. + pub const BOOTSTRAP: u16 = 6881; + /// The local test network's hardcorded Pkarr Relay port number. pub const PKARR_RELAY: u16 = 15411; + /// The local test network's hardcorded HTTP Relay port number. pub const HTTP_RELAY: u16 = 15412; } diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index 5c1e45c..209458f 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -1,3 +1,5 @@ +//! Cryptographic functions (hashing, encryption, and signatures). + use crypto_secretbox::{ aead::{Aead, AeadCore, KeyInit, OsRng}, XSalsa20Poly1305, @@ -8,57 +10,69 @@ pub use pkarr::{Keypair, PublicKey}; pub use ed25519_dalek::Signature; +/// Blake3 Hash. pub type Hash = blake3::Hash; pub use blake3::hash; pub use blake3::Hasher; +/// Create a random hash. pub fn random_hash() -> Hash { Hash::from_bytes(random()) } +/// Create an array of random bytes with a size `N`. pub fn random_bytes() -> [u8; N] { let arr: [u8; N] = random(); arr } -pub fn encrypt(plain_text: &[u8], encryption_key: &[u8; 32]) -> Result, EncryptError> { +/// Encrypt a message using `XSalsa20Poly1305`. +pub fn encrypt(plain_text: &[u8], encryption_key: &[u8; 32]) -> Vec { + if plain_text.is_empty() { + return plain_text.to_vec(); + } + let cipher = XSalsa20Poly1305::new(encryption_key.into()); let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng); // unique per message - let ciphertext = cipher.encrypt(&nonce, plain_text)?; + let ciphertext = cipher + .encrypt(&nonce, plain_text) + .expect("XSalsa20Poly1305 encrypt should be infallible"); let mut out: Vec = Vec::with_capacity(nonce.len() + ciphertext.len()); out.extend_from_slice(nonce.as_slice()); out.extend_from_slice(&ciphertext); - Ok(out) + out } +/// Encrypt an encrypted message using `XSalsa20Poly1305`. pub fn decrypt(bytes: &[u8], encryption_key: &[u8; 32]) -> Result, DecryptError> { + if bytes.is_empty() { + return Ok(bytes.to_vec()); + } + let cipher = XSalsa20Poly1305::new(encryption_key.into()); if bytes.len() < 24 { - return Err(DecryptError::PayloadTooSmall(bytes.len())); + return Err(DecryptError::TooSmall(bytes.len())); } Ok(cipher.decrypt(bytes[..24].into(), &bytes[24..])?) } #[derive(thiserror::Error, Debug)] -pub enum EncryptError { - #[error(transparent)] - SecretBox(#[from] crypto_secretbox::Error), -} - -#[derive(thiserror::Error, Debug)] +/// Error while decrypting a message pub enum DecryptError { #[error(transparent)] - SecretBox(#[from] crypto_secretbox::Error), + /// Failed to decrypt message. + Fail(#[from] crypto_secretbox::Error), - #[error("Encrypted message too small, expected at least 24 bytes nonce, receieved {0} bytes")] - PayloadTooSmall(usize), + #[error("Encrypted message too small, expected at least 24 bytes nonce, received {0} bytes")] + /// Encrypted message too small, expected at least 24 bytes nonce, received {0} bytes + TooSmall(usize), } #[cfg(test)] @@ -70,7 +84,7 @@ mod tests { let plain_text = "Plain text!"; let encryption_key = [0; 32]; - let encrypted = encrypt(plain_text.as_bytes(), &encryption_key).unwrap(); + let encrypted = encrypt(plain_text.as_bytes(), &encryption_key); let decrypted = decrypt(&encrypted, &encryption_key).unwrap(); assert_eq!(decrypted, plain_text.as_bytes()) diff --git a/pubky-common/src/lib.rs b/pubky-common/src/lib.rs index e71fb7c..c8a4eaf 100644 --- a/pubky-common/src/lib.rs +++ b/pubky-common/src/lib.rs @@ -1,3 +1,10 @@ +#![doc = include_str!("../README.md")] +//! + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] +#![cfg_attr(any(), deny(clippy::unwrap_used))] + pub mod auth; pub mod capabilities; pub mod constants; @@ -7,5 +14,6 @@ pub mod recovery_file; pub mod session; pub mod timestamp { + //! Timestamp used across Pubky crates. pub use pubky_timestamp::*; } diff --git a/pubky-common/src/namespaces.rs b/pubky-common/src/namespaces.rs index 6aa37cd..b03cd2e 100644 --- a/pubky-common/src/namespaces.rs +++ b/pubky-common/src/namespaces.rs @@ -1 +1,4 @@ +//! Namespaces using to prepend signed messages to avoid collisions. + +/// Pubky Auth namespace as defined at the [spec](https://pubky.github.io/pubky-core/spec/auth.html) pub const PUBKY_AUTH: &[u8; 10] = b"PUBKY:AUTH"; diff --git a/pubky-common/src/recovery_file.rs b/pubky-common/src/recovery_file.rs index 088dac9..3e889e8 100644 --- a/pubky-common/src/recovery_file.rs +++ b/pubky-common/src/recovery_file.rs @@ -1,3 +1,5 @@ +//! Tools for encrypting and decrypting a recovery file storing user's root key's secret. + use argon2::Argon2; use pkarr::Keypair; @@ -6,8 +8,9 @@ use crate::crypto::{decrypt, encrypt}; static SPEC_NAME: &str = "recovery"; static SPEC_LINE: &str = "pubky.org/recovery"; +/// Decrypt a recovery file. pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { - let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?; + let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase); let newline_index = recovery_file .iter() @@ -38,11 +41,12 @@ pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result Result, Error> { - let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?; +/// Encrypt a recovery file. +pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Vec { + let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase); let secret_key = keypair.secret_key(); - let encrypted_secret_key = encrypt(&secret_key, &encryption_key)?; + let encrypted_secret_key = encrypt(&secret_key, &encryption_key); let mut out = Vec::with_capacity(SPEC_LINE.len() + 1 + encrypted_secret_key.len()); @@ -50,42 +54,44 @@ pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result Result<[u8; 32], Error> { +fn recovery_file_encryption_key_from_passphrase(passphrase: &str) -> [u8; 32] { let argon2id = Argon2::default(); let mut out = [0; 32]; - argon2id.hash_password_into(passphrase.as_bytes(), SPEC_NAME.as_bytes(), &mut out)?; + argon2id + .hash_password_into(passphrase.as_bytes(), SPEC_NAME.as_bytes(), &mut out) + .expect("Output is the correct length, so this should be infallible"); - Ok(out) + out } #[derive(thiserror::Error, Debug)] +/// Error decrypting a recovery file pub enum Error { // === Recovery file == #[error("Recovery file should start with a spec line, followed by a new line character")] + /// Recovery file should start with a spec line, followed by a new line character RecoveryFileMissingSpecLine, #[error("Recovery file should start with a spec line, followed by a new line character")] + /// Recovery file should start with a spec line, followed by a new line character RecoveryFileVersionNotSupported, #[error("Recovery file should contain an encrypted secret key after the new line character")] + /// Recovery file should contain an encrypted secret key after the new line character RecoverFileMissingEncryptedSecretKey, #[error("Recovery file encrypted secret key should be 32 bytes, got {0}")] + /// Recovery file encrypted secret key should be 32 bytes, got {0} RecoverFileInvalidSecretKeyLength(usize), #[error(transparent)] - Argon(#[from] argon2::Error), - - #[error(transparent)] + /// Error while decrypting a message DecryptError(#[from] crate::crypto::DecryptError), - - #[error(transparent)] - EncryptError(#[from] crate::crypto::EncryptError), } #[cfg(test)] @@ -97,7 +103,7 @@ mod tests { let passphrase = "very secure password"; let keypair = Keypair::random(); - let recovery_file = create_recovery_file(&keypair, passphrase).unwrap(); + let recovery_file = create_recovery_file(&keypair, passphrase); let recovered = decrypt_recovery_file(&recovery_file, passphrase).unwrap(); assert_eq!(recovered.public_key(), keypair.public_key()); diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs index af34978..e9ad3fb 100644 --- a/pubky-common/src/session.rs +++ b/pubky-common/src/session.rs @@ -1,3 +1,5 @@ +//! Pubky homeserver session struct. + use pkarr::PublicKey; use postcard::{from_bytes, to_allocvec}; use serde::{Deserialize, Serialize}; @@ -11,6 +13,7 @@ use crate::{capabilities::Capability, timestamp::Timestamp}; // TODO: use https://crates.io/crates/user-agent-parser to parse the session // and get more informations from the user-agent. #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +/// Pubky homeserver session struct. pub struct Session { version: usize, pubky: PublicKey, @@ -22,6 +25,7 @@ pub struct Session { } impl Session { + /// Create a new session. pub fn new(pubky: &PublicKey, capabilities: &[Capability], user_agent: Option) -> Self { Self { version: 0, @@ -35,16 +39,19 @@ impl Session { // === Getters === + /// Returns the pubky of this session authorizes for. pub fn pubky(&self) -> &PublicKey { &self.pubky } + /// Returns the capabilities this session provide on this session's pubky's resources. pub fn capabilities(&self) -> &Vec { &self.capabilities } // === Setters === + /// Set this session user agent. pub fn set_user_agent(&mut self, user_agent: String) -> &mut Self { self.user_agent = user_agent; @@ -55,6 +62,7 @@ impl Session { self } + /// Set this session's capabilities. pub fn set_capabilities(&mut self, capabilities: Vec) -> &mut Self { self.capabilities = capabilities; @@ -63,11 +71,13 @@ impl Session { // === Public Methods === + /// Serialize this session to its canonical binary representation. pub fn serialize(&self) -> Vec { to_allocvec(self).expect("Session::serialize") } - pub fn deserialize(bytes: &[u8]) -> Result { + /// Deserialize this session from its canonical binary representation. + pub fn deserialize(bytes: &[u8]) -> Result { if bytes.is_empty() { return Err(Error::EmptyPayload); } @@ -82,16 +92,18 @@ impl Session { // TODO: add `can_read()`, `can_write()` and `is_root()` methods } -pub type Result = core::result::Result; - #[derive(thiserror::Error, Debug, PartialEq)] +/// Error deserializing a [Session]. pub enum Error { #[error("Empty payload")] + /// Empty payload EmptyPayload, #[error("Unknown version")] + /// Unknown version UnknownVersion, #[error(transparent)] - Postcard(#[from] postcard::Error), + /// Error parsing the binary representation. + Parsing(#[from] postcard::Error), } #[cfg(test)] diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs deleted file mode 100644 index 4317484..0000000 --- a/pubky-common/src/timestamp.rs +++ /dev/null @@ -1,280 +0,0 @@ -//! Absolutely monotonic unix timestamp in microseconds - -use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use std::{ - ops::{Add, Sub}, - sync::Mutex, -}; - -use once_cell::sync::Lazy; -use rand::Rng; - -#[cfg(not(target_arch = "wasm32"))] -use std::time::SystemTime; - -/// ~4% chance of none of 10 clocks have matching id. -const CLOCK_MASK: u64 = (1 << 8) - 1; -const TIME_MASK: u64 = !0 >> 8; - -pub struct TimestampFactory { - clock_id: u64, - last_time: u64, -} - -impl TimestampFactory { - pub fn new() -> Self { - Self { - clock_id: rand::thread_rng().gen::() & CLOCK_MASK, - last_time: system_time() & TIME_MASK, - } - } - - pub fn now(&mut self) -> Timestamp { - // Ensure absolute monotonicity. - self.last_time = (system_time() & TIME_MASK).max(self.last_time + CLOCK_MASK + 1); - - // Add clock_id to the end of the timestamp - Timestamp(self.last_time | self.clock_id) - } -} - -impl Default for TimestampFactory { - fn default() -> Self { - Self::new() - } -} - -static DEFAULT_FACTORY: Lazy> = - Lazy::new(|| Mutex::new(TimestampFactory::default())); - -/// Absolutely monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds as u64. -/// -/// The purpose of this timestamp is to unique per "user", not globally, -/// it achieves this by: -/// 1. Override the last byte with a random `clock_id`, reducing the probability -/// of two matching timestamps across multiple machines/threads. -/// 2. Gurantee that the remaining 3 bytes are ever increasing (absolutely monotonic) within -/// the same thread regardless of the wall clock value -/// -/// This timestamp is also serialized as BE bytes to remain sortable. -/// If a `utf-8` encoding is necessary, it is encoded as [base32::Alphabet::Crockford] -/// to act as a sortable Id. -/// -/// U64 of microseconds is valid for the next 500 thousand years! -#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)] -pub struct Timestamp(u64); - -impl Timestamp { - pub fn now() -> Self { - DEFAULT_FACTORY.lock().unwrap().now() - } - - /// Return big endian bytes - pub fn to_bytes(&self) -> [u8; 8] { - self.0.to_be_bytes() - } - - pub fn difference(&self, rhs: &Timestamp) -> i64 { - (self.0 as i64) - (rhs.0 as i64) - } - - pub fn into_inner(&self) -> u64 { - self.0 - } -} - -impl Default for Timestamp { - fn default() -> Self { - Timestamp::now() - } -} - -impl Display for Timestamp { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let bytes: [u8; 8] = self.into(); - f.write_str(&base32::encode(base32::Alphabet::Crockford, &bytes)) - } -} - -impl TryFrom for Timestamp { - type Error = TimestampError; - - fn try_from(value: String) -> Result { - match base32::decode(base32::Alphabet::Crockford, &value) { - Some(vec) => { - let bytes: [u8; 8] = vec - .try_into() - .map_err(|_| TimestampError::InvalidEncoding)?; - - Ok(bytes.into()) - } - None => Err(TimestampError::InvalidEncoding), - } - } -} - -impl TryFrom<&[u8]> for Timestamp { - type Error = TimestampError; - - fn try_from(bytes: &[u8]) -> Result { - let bytes: [u8; 8] = bytes - .try_into() - .map_err(|_| TimestampError::InvalidBytesLength(bytes.len()))?; - - Ok(bytes.into()) - } -} - -impl From<&Timestamp> for [u8; 8] { - fn from(timestamp: &Timestamp) -> Self { - timestamp.0.to_be_bytes() - } -} - -impl From<[u8; 8]> for Timestamp { - fn from(bytes: [u8; 8]) -> Self { - Self(u64::from_be_bytes(bytes)) - } -} - -// === U64 conversion === - -impl From for u64 { - fn from(value: Timestamp) -> Self { - value.into_inner() - } -} - -impl Add for &Timestamp { - type Output = Timestamp; - - fn add(self, rhs: u64) -> Self::Output { - Timestamp(self.0 + rhs) - } -} - -impl Sub for &Timestamp { - type Output = Timestamp; - - fn sub(self, rhs: u64) -> Self::Output { - Timestamp(self.0 - rhs) - } -} - -impl Serialize for Timestamp { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let bytes = self.to_bytes(); - bytes.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for Timestamp { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let bytes: [u8; 8] = Deserialize::deserialize(deserializer)?; - Ok(Timestamp(u64::from_be_bytes(bytes))) - } -} - -#[cfg(not(target_arch = "wasm32"))] -/// Return the number of microseconds since [SystemTime::UNIX_EPOCH] -fn system_time() -> u64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("time drift") - .as_micros() as u64 -} - -#[cfg(target_arch = "wasm32")] -/// Return the number of microseconds since [SystemTime::UNIX_EPOCH] -pub fn system_time() -> u64 { - // Won't be an issue for more than 5000 years! - (js_sys::Date::now() as u64 ) - // Turn miliseconds to microseconds - * 1000 -} - -#[derive(thiserror::Error, Debug)] -pub enum TimestampError { - #[error("Invalid bytes length, Timestamp should be encoded as 8 bytes, got {0}")] - InvalidBytesLength(usize), - #[error("Invalid timestamp encoding")] - InvalidEncoding, -} - -#[cfg(test)] -mod tests { - use std::collections::HashSet; - - use super::*; - - #[test] - fn absolutely_monotonic() { - const COUNT: usize = 100; - - let mut set = HashSet::with_capacity(COUNT); - let mut vec = Vec::with_capacity(COUNT); - - for _ in 0..COUNT { - let timestamp = Timestamp::now(); - - set.insert(timestamp.clone()); - vec.push(timestamp); - } - - let mut ordered = vec.clone(); - ordered.sort(); - - assert_eq!(set.len(), COUNT, "unique"); - assert_eq!(ordered, vec, "ordered"); - } - - #[test] - fn strings() { - const COUNT: usize = 100; - - let mut set = HashSet::with_capacity(COUNT); - let mut vec = Vec::with_capacity(COUNT); - - for _ in 0..COUNT { - let string = Timestamp::now().to_string(); - - set.insert(string.clone()); - vec.push(string) - } - - let mut ordered = vec.clone(); - ordered.sort(); - - assert_eq!(set.len(), COUNT, "unique"); - assert_eq!(ordered, vec, "ordered"); - } - - #[test] - fn to_from_string() { - let timestamp = Timestamp::now(); - let string = timestamp.to_string(); - let decoded: Timestamp = string.try_into().unwrap(); - - assert_eq!(decoded, timestamp) - } - - #[test] - fn serde() { - let timestamp = Timestamp::now(); - - let serialized = postcard::to_allocvec(×tamp).unwrap(); - - assert_eq!(serialized, timestamp.to_bytes()); - - let deserialized: Timestamp = postcard::from_bytes(&serialized).unwrap(); - - assert_eq!(deserialized, timestamp); - } -}