rok-core 0.6.0

Core primitives for the rok ecosystem — errors, crypto, i18n, config, DI, and more
Documentation
use std::fmt;
use std::str::FromStr;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};

use rand::RngCore;
use serde::{Deserialize, Serialize};

use crate::crypto::ids::IdError;

const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
const CROCKFORD_DECODE: [i8; 128] = build_decode_table();

const fn build_decode_table() -> [i8; 128] {
    let mut table = [-1i8; 128];
    let alpha = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
    let mut i = 0usize;
    while i < 32 {
        table[alpha[i] as usize] = i as i8;
        if alpha[i].is_ascii_uppercase() {
            table[(alpha[i] + 32) as usize] = i as i8;
        }
        i += 1;
    }
    table
}

fn encode_ulid(ts_ms: u64, random: &[u8; 10]) -> [u8; 26] {
    let mut chars = [0u8; 26];

    chars[0] = CROCKFORD[((ts_ms >> 45) & 0x1F) as usize];
    chars[1] = CROCKFORD[((ts_ms >> 40) & 0x1F) as usize];
    chars[2] = CROCKFORD[((ts_ms >> 35) & 0x1F) as usize];
    chars[3] = CROCKFORD[((ts_ms >> 30) & 0x1F) as usize];
    chars[4] = CROCKFORD[((ts_ms >> 25) & 0x1F) as usize];
    chars[5] = CROCKFORD[((ts_ms >> 20) & 0x1F) as usize];
    chars[6] = CROCKFORD[((ts_ms >> 15) & 0x1F) as usize];
    chars[7] = CROCKFORD[((ts_ms >> 10) & 0x1F) as usize];
    chars[8] = CROCKFORD[((ts_ms >> 5) & 0x1F) as usize];
    chars[9] = CROCKFORD[(ts_ms & 0x1F) as usize];

    let mut r: u128 = 0;
    for &b in random.iter() {
        r = (r << 8) | b as u128;
    }
    for i in 0..16usize {
        chars[25 - i] = CROCKFORD[((r >> (5 * i)) & 0x1F) as usize];
    }

    chars
}

struct MonotonicState {
    last_ms: u64,
    last_random: [u8; 10],
}

static MONOTONIC: Mutex<Option<MonotonicState>> = Mutex::new(None);

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Ulid(String);

impl Ulid {
    pub fn generate() -> Self {
        let ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time went backwards")
            .as_millis() as u64;

        let mut rnd = [0u8; 10];
        rand::thread_rng().fill_bytes(&mut rnd);

        let chars = encode_ulid(ts, &rnd);
        Self(String::from_utf8(chars.to_vec()).unwrap())
    }

    pub fn monotonic() -> Self {
        let ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time went backwards")
            .as_millis() as u64;

        let mut guard = MONOTONIC.lock().unwrap();
        let random = match &mut *guard {
            Some(state) if state.last_ms == ts => {
                let mut i = 9usize;
                loop {
                    let (val, overflow) = state.last_random[i].overflowing_add(1);
                    state.last_random[i] = val;
                    if !overflow {
                        break;
                    }
                    if i == 0 {
                        rand::thread_rng().fill_bytes(&mut state.last_random);
                        break;
                    }
                    i -= 1;
                }
                state.last_random
            }
            _ => {
                let mut rnd = [0u8; 10];
                rand::thread_rng().fill_bytes(&mut rnd);
                *guard = Some(MonotonicState {
                    last_ms: ts,
                    last_random: rnd,
                });
                rnd
            }
        };

        let chars = encode_ulid(ts, &random);
        Self(String::from_utf8(chars.to_vec()).unwrap())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn timestamp_ms(&self) -> u64 {
        let bytes = self.0.as_bytes();
        let mut ts: u64 = 0;
        for &byte in bytes.iter().take(10) {
            let ch = byte as usize;
            let v = if ch < 128 { CROCKFORD_DECODE[ch] } else { -1 };
            ts = (ts << 5) | v as u64;
        }
        ts
    }
}

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

impl FromStr for Ulid {
    type Err = IdError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.len() != 26 {
            return Err(IdError::InvalidFormat("ulid", "expected 26 chars"));
        }
        for ch in s.chars() {
            let idx = ch as usize;
            if idx >= 128 || CROCKFORD_DECODE[idx] < 0 {
                return Err(IdError::InvalidFormat(
                    "ulid",
                    "invalid Crockford base32 char",
                ));
            }
        }
        Ok(Self(s.to_ascii_uppercase()))
    }
}

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

#[cfg(feature = "crypto-sqlx")]
mod sqlx_impl {
    use super::Ulid;
    use sqlx::{
        encode::IsNull,
        error::BoxDynError,
        postgres::{PgArgumentBuffer, PgTypeInfo, PgValueRef},
    };

    impl sqlx::Type<sqlx::Postgres> for Ulid {
        fn type_info() -> PgTypeInfo {
            <String as sqlx::Type<sqlx::Postgres>>::type_info()
        }
    }

    impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Ulid {
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
            <String as sqlx::Encode<'q, sqlx::Postgres>>::encode_by_ref(&self.0, buf)
        }
    }

    impl<'r> sqlx::Decode<'r, sqlx::Postgres> for Ulid {
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
            let s = <String as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
            Ok(Self(s))
        }
    }
}