use base64::{Engine as _, alphabet, engine};
use rand::Rng;
use secrecy::ExposeSecret;
use serde::{Serialize, Serializer};
pub type StreamId = ubid::Ubid160;
pub type LinkId = ubid::Ubid120;
pub type LinkSecret = secrecy::SecretString;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct WriterId([u8; 16]);
impl WriterId {
pub const BYTE_LEN: usize = 16;
pub fn new_random() -> Self {
let mut bytes = [0_u8; Self::BYTE_LEN];
fill_random(&mut bytes);
Self(bytes)
}
pub const fn from_bytes(bytes: [u8; Self::BYTE_LEN]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; Self::BYTE_LEN] {
&self.0
}
}
fn fill_random(bytes: &mut [u8]) {
rand::rng().fill_bytes(bytes);
}
impl Serialize for WriterId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_bytes(self.as_bytes())
}
}
pub(crate) fn serialize_link_secret<S>(
secret: &LinkSecret,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(secret.expose_secret())
}
pub(crate) const BASE64URL_32_ENCODED_LEN: usize = 43;
const STRICT_URL_SAFE_NO_PAD: engine::GeneralPurpose = engine::GeneralPurpose::new(
&alphabet::URL_SAFE,
engine::general_purpose::NO_PAD.with_decode_allow_trailing_bits(false),
);
pub(crate) fn encode_base64url_32(bytes: &[u8; 32]) -> String {
STRICT_URL_SAFE_NO_PAD.encode(bytes)
}
pub(crate) fn is_canonical_base64url_32(value: &str) -> bool {
if value.len() != BASE64URL_32_ENCODED_LEN {
return false;
}
let mut decoded = [0_u8; 32];
STRICT_URL_SAFE_NO_PAD
.decode_slice(value, &mut decoded)
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_value_encodes_to_all_a() {
assert_eq!(encode_base64url_32(&[0_u8; 32]), "A".repeat(43));
}
#[test]
fn sequential_bytes_encode_to_known_base64url() {
let bytes: [u8; 32] = core::array::from_fn(|i| i as u8);
let encoded = encode_base64url_32(&bytes);
assert_eq!(encoded, "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8");
assert!(is_canonical_base64url_32(&encoded));
}
#[test]
fn rejects_malleable_trailing_bits() {
let canonical = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
assert!(is_canonical_base64url_32(canonical));
let mut chars: Vec<char> = canonical.chars().collect();
for bad in ['B', 'C', 'D'] {
chars[42] = bad;
let link: String = chars.iter().collect();
assert!(!is_canonical_base64url_32(&link), "link={link}");
}
}
}