vomit-m2dir 0.3.1

Library for the m2dir email storage format
Documentation
use std::fmt::Display;

use base64::{engine::general_purpose, Engine};
use rand::RngExt;

use crate::{util::fnv64, Error};

/// A [Unique ID](https://man.sr.ht/~bitfehler/m2dir/#unique-id) for a message.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ID(String);

impl ID {
    /// Creates a new ID for a message with the given content by computing its
    /// checksum and generating a random nonce.
    pub fn new(msg: &[u8]) -> Self {
        let mut checksum = [0u8; 12];
        checksum[0..4].copy_from_slice(&(msg.len() as u32).to_le_bytes());
        let digest = &fnv64(&checksum[0..4], msg).to_le_bytes();
        checksum[4..].copy_from_slice(digest); // the digest part
        let checksum_b64 = general_purpose::URL_SAFE.encode(checksum);

        let mut rng = rand::rng();
        let nonce: u32 = rng.random();
        let nonce_b64 = general_purpose::URL_SAFE_NO_PAD.encode(&nonce.to_le_bytes()[..3]);

        ID(format!("{}.{}", checksum_b64, nonce_b64))
    }

    /// Returns only the base64url-encoded checksum part of the ID.
    pub fn checksum(&self) -> &str {
        &self.0[0..16]
    }

    /// Returns only the base64url-encoded nonce part of the ID.
    pub fn nonce(&self) -> &str {
        &self.0[17..]
    }

    /// Creates an ID from the provided string without checking its validity.
    ///
    /// This is very fast, but can lead to hard-to-debug issues if used with
    /// invalid IDs.
    pub fn from_unchecked(value: String) -> Self {
        ID(value)
    }
}

impl Display for ID {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

impl TryFrom<String> for ID {
    type Error = Error;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        if let Some((hash, nonce)) = value.split_once('.') {
            let h = general_purpose::URL_SAFE.decode(hash);
            let n = general_purpose::URL_SAFE_NO_PAD.decode(nonce);
            if h.is_ok_and(|h| h.len() == 12) && n.is_ok_and(|n| n.len() == 3) {
                return Ok(ID(value.to_string()));
            }
        }
        Err(Error::InvalidMsgID(value))
    }
}

impl AsRef<str> for ID {
    fn as_ref(&self) -> &str {
        self.0.as_str()
    }
}