snowid 3.0.1

A Rust library for generating SnowID - a Snowflake-like timestamp-based distributed unique identifier
Documentation
//! Base62 encoding and decoding for `SnowID` values
//!
//! Provides zero-allocation variants for hot paths:
//! - `encode_array`: Returns [u8; 11] + length
//! - `encode_into`: Writes to caller buffer
//! - `encode`: Convenience String wrapper
//!
#![allow(clippy::unwrap_used)]

use std::error::Error;
use std::fmt;

/// Maximum size needed for u64 in base62 encoding (11 bytes)
pub const MAX_LEN: usize = 11;

/// Zero-allocation base62 encoding to a fixed-size array
/// Returns the array and the actual length of encoded bytes
///
/// # Panics
///
/// Panics if the base62 encoding fails, which should never happen with valid `u64` input.
#[inline]
#[must_use]
pub fn encode_array(id: u64) -> ([u8; MAX_LEN], usize) {
    let mut buf = [0u8; MAX_LEN];
    let len = base62::encode_bytes(id, &mut buf).unwrap();
    (buf, len)
}

/// Zero-allocation base62 encoding into caller-provided buffer
/// Returns a str slice of the encoded portion
///
/// # Panics
///
/// Panics if base62 encoding fails or produces invalid UTF-8, which should never
/// happen with valid `u64` input.
#[inline]
pub fn encode_into(id: u64, buf: &mut [u8; MAX_LEN]) -> &str {
    let len = base62::encode_bytes(id, buf).unwrap();
    std::str::from_utf8(&buf[..len]).unwrap()
}

/// Base62 encode with String allocation (convenience wrapper)
/// For hot paths, prefer `encode_array` or `encode_into`
///
/// # Panics
///
/// Panics if encoding fails or produces invalid UTF-8, which should never happen
/// with valid `u64` input.
#[inline]
#[must_use]
pub fn encode(id: u64) -> String {
    let (buf, len) = encode_array(id);
    std::str::from_utf8(&buf[..len]).unwrap().to_owned()
}

/// Decode a base62 string to a u64, handling potential overflow
///
/// # Errors
///
/// Returns [`DecodeError`] if the input contains invalid base62 characters or
/// the decoded value overflows a `u64`.
pub fn decode(encoded: &str) -> Result<u64, DecodeError> {
    let decoded = base62::decode(encoded).map_err(DecodeError::from)?;

    let Ok(decoded_value) = u64::try_from(decoded) else {
        return Err(DecodeError::Overflow);
    };

    Ok(decoded_value)
}

/// Error type for base62 decoding operations
#[derive(Debug)]
pub enum DecodeError {
    /// Invalid character in base62 string
    InvalidCharacter,
    /// Decoded value would overflow u64
    Overflow,
    /// Other decoding error from base62 crate
    Other(base62::DecodeError),
}

impl fmt::Display for DecodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            Self::InvalidCharacter => write!(f, "Invalid base62 character"),
            Self::Overflow => write!(f, "Decoded value would overflow u64"),
            Self::Other(ref e) => write!(f, "Base62 decode error: {e}"),
        }
    }
}

impl Error for DecodeError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match *self {
            Self::Other(ref e) => Some(e),
            Self::InvalidCharacter | Self::Overflow => None,
        }
    }
}

impl From<base62::DecodeError> for DecodeError {
    fn from(err: base62::DecodeError) -> Self {
        Self::Other(err)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_encode_decode_roundtrip() {
        let values = [1u64, 1000, u64::MAX / 2, u64::MAX];
        for &value in &values {
            let encoded = encode(value);
            let decoded = decode(&encoded).unwrap();
            assert_eq!(value, decoded);
        }
    }

    #[test]
    fn test_encode_array_matches_string() {
        let id = 12345678901234u64;
        let string_encoded = encode(id);
        let (arr, len) = encode_array(id);
        let array_str = std::str::from_utf8(&arr[..len]).unwrap();
        assert_eq!(string_encoded, array_str);
    }

    #[test]
    fn test_encode_into_matches_string() {
        let id = 98765432109876u64;
        let string_encoded = encode(id);
        let mut buf = [0u8; MAX_LEN];
        let into_str = encode_into(id, &mut buf);
        assert_eq!(string_encoded, into_str);
    }

    #[test]
    fn test_max_len() {
        let (_, len) = encode_array(u64::MAX);
        assert!(len <= MAX_LEN);
    }

    #[test]
    fn test_decode_error() {
        assert!(decode("!!!").is_err());
    }
}